Merge "[GH] [Room][Compiler Processing] Originating element improvements" into androidx-main
diff --git a/activity/activity-compose/build.gradle b/activity/activity-compose/build.gradle
index 5d0169f..20451ba 100644
--- a/activity/activity-compose/build.gradle
+++ b/activity/activity-compose/build.gradle
@@ -40,6 +40,7 @@
 
     androidTestImplementation projectOrArtifact(":compose:ui:ui-test-junit4")
     androidTestImplementation projectOrArtifact(":compose:material:material")
+    androidTestImplementation projectOrArtifact(":compose:test-utils")
     androidTestImplementation("androidx.lifecycle:lifecycle-runtime-testing:2.3.1")
     androidTestImplementation(ANDROIDX_TEST_RUNNER)
     androidTestImplementation(ANDROIDX_TEST_EXT_KTX)
diff --git a/buildSrc/src/main/kotlin/androidx/build/dependencies/Dependencies.kt b/buildSrc/src/main/kotlin/androidx/build/dependencies/Dependencies.kt
index 5cb2672..78c9e69 100644
--- a/buildSrc/src/main/kotlin/androidx/build/dependencies/Dependencies.kt
+++ b/buildSrc/src/main/kotlin/androidx/build/dependencies/Dependencies.kt
@@ -108,7 +108,7 @@
 const val REACTIVE_STREAMS = "org.reactivestreams:reactive-streams:1.0.0"
 const val RX_JAVA = "io.reactivex.rxjava2:rxjava:2.2.9"
 const val RX_JAVA3 = "io.reactivex.rxjava3:rxjava:3.0.0"
-val SKIKO_VERSION = System.getenv("SKIKO_VERSION") ?: "0.2.21"
+val SKIKO_VERSION = System.getenv("SKIKO_VERSION") ?: "0.2.22"
 val SKIKO = "org.jetbrains.skiko:skiko-jvm:$SKIKO_VERSION"
 val SKIKO_LINUX_X64 = "org.jetbrains.skiko:skiko-jvm-runtime-linux-x64:$SKIKO_VERSION"
 val SKIKO_MACOS_X64 = "org.jetbrains.skiko:skiko-jvm-runtime-macos-x64:$SKIKO_VERSION"
diff --git a/buildSrc/src/main/kotlin/androidx/build/docs/AndroidXDocsPlugin.kt b/buildSrc/src/main/kotlin/androidx/build/docs/AndroidXDocsPlugin.kt
index ec49186..7a95cae 100644
--- a/buildSrc/src/main/kotlin/androidx/build/docs/AndroidXDocsPlugin.kt
+++ b/buildSrc/src/main/kotlin/androidx/build/docs/AndroidXDocsPlugin.kt
@@ -298,7 +298,7 @@
             dependencies.add(project.dependencies.create(DACKKA_DEPENDENCY))
         }
 
-        project.tasks.register("dackkaDocs", DackkaTask::class.java) { task ->
+        val dackkaTask = project.tasks.register("dackkaDocs", DackkaTask::class.java) { task ->
             task.apply {
                 dependsOn(dackkaConfiguration)
                 dependsOn(unzipDocsTask)
@@ -314,6 +314,26 @@
                 sourcesDir = unzippedDocsSources
             }
         }
+
+        project.tasks.register("zipDackkaDocs", Zip::class.java) { task ->
+            task.apply {
+                dependsOn(dackkaTask)
+                from(generatedDocsDir)
+
+                val baseName = "dackka-$docsType-docs"
+                val buildId = getBuildId()
+                archiveBaseName.set(baseName)
+                archiveVersion.set(buildId)
+                destinationDirectory.set(project.getDistributionDirectory())
+                group = JavaBasePlugin.DOCUMENTATION_GROUP
+
+                val filePath = "${project.getDistributionDirectory().canonicalPath}/"
+                val fileName = "$baseName-$buildId.zip"
+                val destinationFile = filePath + fileName
+                description = "Zips Java and Kotlin documentation (generated via Dackka in the" +
+                    " style of d.android.com) into $destinationFile"
+            }
+        }
     }
 
     private fun configureDokka(
diff --git a/camera/camera-camera2/src/androidTest/AndroidManifest.xml b/camera/camera-camera2/src/androidTest/AndroidManifest.xml
index b53d4ff..4a5d228 100644
--- a/camera/camera-camera2/src/androidTest/AndroidManifest.xml
+++ b/camera/camera-camera2/src/androidTest/AndroidManifest.xml
@@ -1,42 +1,23 @@
 <?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2019 The Android Open Source Project
+<!--
+  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
+  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
+       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.
--->
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+  -->
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
     package="androidx.camera.camera2.test">
-
-
     <uses-permission android:name="android.permission.CAMERA" />
     <uses-permission android:name="android.permission.RECORD_AUDIO" />
     <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
 
-    <application>
-        <activity
-            android:name="androidx.camera.testing.activity.CameraXTestActivity"
-            android:label="CameraX TestActivity">
-            <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
-            </intent-filter>
-        </activity>
-        <activity
-            android:name="androidx.camera.testing.activity.Camera2TestActivity"
-            android:label="Camera2 TestActivity">
-            <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
-            </intent-filter>
-        </activity>
-    </application>
 </manifest>
diff --git a/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/CameraDisconnectTest.java b/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/CameraDisconnectTest.java
deleted file mode 100644
index 2b8db23..0000000
--- a/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/CameraDisconnectTest.java
+++ /dev/null
@@ -1,125 +0,0 @@
-/*
- * Copyright 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.camera.camera2;
-
-import static androidx.camera.testing.CoreAppTestUtil.ForegroundOccupiedError;
-import static androidx.camera.testing.CoreAppTestUtil.assumeCanTestCameraDisconnect;
-import static androidx.camera.testing.CoreAppTestUtil.assumeCompatibleDevice;
-import static androidx.camera.testing.CoreAppTestUtil.prepareDeviceUI;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import android.content.Context;
-import android.content.Intent;
-import android.os.Build;
-
-import androidx.camera.core.CameraX;
-import androidx.camera.testing.CameraUtil;
-import androidx.camera.testing.activity.Camera2TestActivity;
-import androidx.camera.testing.activity.CameraXTestActivity;
-import androidx.test.core.app.ActivityScenario;
-import androidx.test.core.app.ApplicationProvider;
-import androidx.test.espresso.Espresso;
-import androidx.test.espresso.IdlingRegistry;
-import androidx.test.espresso.IdlingResource;
-import androidx.test.espresso.idling.CountingIdlingResource;
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.LargeTest;
-import androidx.test.filters.SdkSuppress;
-import androidx.test.platform.app.InstrumentationRegistry;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.rules.TestRule;
-import org.junit.runner.RunWith;
-
-import java.util.concurrent.ExecutionException;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.TimeoutException;
-import java.util.concurrent.atomic.AtomicReference;
-
-@RunWith(AndroidJUnit4.class)
-@LargeTest
-public class CameraDisconnectTest {
-
-    @Rule
-    public TestRule mCameraRule = CameraUtil.grantCameraPermissionAndPreTest();
-
-    @Before
-    public void setUp() throws ForegroundOccupiedError {
-        assumeCompatibleDevice();
-        assumeCanTestCameraDisconnect();
-
-        final Context context = ApplicationProvider.getApplicationContext();
-        CameraX.initialize(context, Camera2Config.defaultConfig());
-
-        // Clear the device UI and check if there is no dialog or lock screen on the top of the
-        // window before start the test.
-        prepareDeviceUI(InstrumentationRegistry.getInstrumentation());
-    }
-
-    @After
-    public void tearDown() throws ExecutionException, InterruptedException, TimeoutException {
-        CameraX.shutdown().get(10_000, TimeUnit.MILLISECONDS);
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.M) // Known issue, checkout b/147393563.
-    public void testDisconnect_launchCamera2App() {
-        // Launch CameraX test activity
-        final ActivityScenario<CameraXTestActivity> cameraXActivity = ActivityScenario.launch(
-                CameraXTestActivity.class);
-
-        // Wait for preview to become active
-        final AtomicReference<CountingIdlingResource> cameraXPreviewReady = new AtomicReference<>();
-        cameraXActivity.onActivity(activity -> cameraXPreviewReady.set(activity.getPreviewReady()));
-        waitFor(cameraXPreviewReady.get());
-
-        // Get id of camera opened by CameraX test activity
-        final AtomicReference<String> cameraId = new AtomicReference<>();
-        cameraXActivity.onActivity(activity -> cameraId.set(activity.getCameraId()));
-        assertThat(cameraId.get()).isNotNull();
-
-        // Launch Camera2 test activity. It should cause the camera to disconnect from CameraX.
-        final Intent intent = new Intent(ApplicationProvider.getApplicationContext(),
-                Camera2TestActivity.class);
-        intent.putExtra(Camera2TestActivity.EXTRA_CAMERA_ID, cameraId.get());
-        final ActivityScenario<Camera2TestActivity> camera2Activity = ActivityScenario.launch(
-                intent);
-
-        // Wait for preview to become active
-        final AtomicReference<CountingIdlingResource> camera2PreviewReady = new AtomicReference<>();
-        camera2Activity.onActivity(activity -> camera2PreviewReady.set(activity.mPreviewReady));
-        waitFor(camera2PreviewReady.get());
-
-        // Close Camera2 test activity, and verify the CameraX Preview resumes successfully.
-        camera2Activity.close();
-        waitFor(cameraXPreviewReady.get());
-
-        // Close CameraX test activity
-        cameraXActivity.close();
-    }
-
-    private static void waitFor(IdlingResource idlingResource) {
-        IdlingRegistry.getInstance().register(idlingResource);
-        Espresso.onIdle();
-        IdlingRegistry.getInstance().unregister(idlingResource);
-    }
-
-}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/ImageCapture.java b/camera/camera-core/src/main/java/androidx/camera/core/ImageCapture.java
index 2bd0527..991bec0 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/ImageCapture.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/ImageCapture.java
@@ -113,6 +113,8 @@
 import androidx.camera.core.internal.IoConfig;
 import androidx.camera.core.internal.TargetConfig;
 import androidx.camera.core.internal.YuvToJpegProcessor;
+import androidx.camera.core.internal.compat.quirk.DeviceQuirks;
+import androidx.camera.core.internal.compat.quirk.ImageCaptureWashedOutImageQuirk;
 import androidx.camera.core.internal.compat.quirk.SoftwareJpegEncodingPreferredQuirk;
 import androidx.camera.core.internal.compat.workaround.ExifRotationAvailability;
 import androidx.camera.core.internal.utils.ImageUtil;
@@ -303,6 +305,14 @@
      */
     private boolean mUseSoftwareJpeg = false;
 
+    /**
+     * Whether the torch flash will be used.
+     *
+     * <p>When the flag is set, torch will be opened and closed to replace the flash fired by flash
+     * mode.
+     */
+    private final boolean mUseTorchFlash;
+
     ////////////////////////////////////////////////////////////////////////////////////////////
     // [UseCase attached dynamic] - Can change but is only available when the UseCase is attached.
     ////////////////////////////////////////////////////////////////////////////////////////////
@@ -351,6 +361,11 @@
         } else {
             mEnableCheck3AConverged = false; // skip 3A convergence in MIN_LATENCY mode
         }
+
+        mUseTorchFlash = DeviceQuirks.get(ImageCaptureWashedOutImageQuirk.class) != null;
+        if (mUseTorchFlash) {
+            Logger.d(TAG, "Open and close torch to replace the flash fired by flash mode.");
+        }
     }
 
     @UiThread
@@ -521,7 +536,7 @@
     @RestrictTo(Scope.LIBRARY_GROUP)
     @NonNull
     @Override
-    UseCaseConfig<?> onMergeConfig(@NonNull CameraInfoInternal cameraInfo,
+    protected UseCaseConfig<?> onMergeConfig(@NonNull CameraInfoInternal cameraInfo,
             @NonNull UseCaseConfig.Builder<?, ?, ?> builder) {
         if (builder.getUseCaseConfig().retrieveOption(OPTION_CAPTURE_PROCESSOR, null)
                 != null && Build.VERSION.SDK_INT >= 29) {
@@ -1320,9 +1335,12 @@
                 .transformAsync(captureResult -> {
                     state.mPreCaptureState = captureResult;
                     triggerAfIfNeeded(state);
-                    if (isAePrecaptureRequired(state)) {
-                        // trigger AE precapture and await the result.
-                        return triggerAePrecapture(state);
+                    if (isFlashRequired(state)) {
+                        if (mUseTorchFlash) {
+                            return openTorch(state);
+                        } else {
+                            return triggerAePrecapture(state);
+                        }
                     }
                     return Futures.immediateFuture(null);
                 }, mExecutor)
@@ -1337,10 +1355,37 @@
      * <p>For example, cancel 3A scan, close torch if necessary.
      */
     void postTakePicture(final TakePictureState state) {
+        closeTorch(state);
         cancelAfAeTrigger(state);
         unlockFlashMode();
     }
 
+    @NonNull
+    private ListenableFuture<Void> openTorch(@NonNull TakePictureState state) {
+        CameraInternal camera = getCamera();
+        if (camera != null && camera.getCameraInfo().getTorchState().getValue() == TorchState.ON) {
+            // Torch is already opened.
+            return Futures.immediateFuture(null);
+        }
+
+        Logger.d(TAG, "openTorch");
+
+        // Create a new future in order to ignore any fail from CameraControl.enableTorch().
+        return CallbackToFutureAdapter.getFuture(completer -> {
+            getCameraControl().enableTorch(state.mIsTorchOpened = true).addListener(
+                    () -> completer.set(null), CameraXExecutors.directExecutor());
+            return "openTorch";
+        });
+    }
+
+    private void closeTorch(@NonNull TakePictureState state) {
+        if (state.mIsTorchOpened) {
+            // Add listener to avoid FutureReturnValueIgnored error.
+            getCameraControl().enableTorch(state.mIsTorchOpened = false).addListener(() -> {
+            }, CameraXExecutors.directExecutor());
+        }
+    }
+
     /**
      * Gets a capture result or not according to current configuration.
      *
@@ -1373,7 +1418,7 @@
         return Futures.immediateFuture(null);
     }
 
-    boolean isAePrecaptureRequired(TakePictureState state) {
+    boolean isFlashRequired(@NonNull TakePictureState state) {
         switch (getFlashMode()) {
             case FLASH_MODE_ON:
                 return true;
@@ -1386,9 +1431,7 @@
     }
 
     ListenableFuture<Boolean> check3AConverged(TakePictureState state) {
-        // Skip the 3A converged check if enableCheck3AConverged is false and AE precapture is
-        // not triggered.
-        if (!mEnableCheck3AConverged && !state.mIsAePrecaptureTriggered) {
+        if (!mEnableCheck3AConverged && !state.mIsAePrecaptureTriggered && !state.mIsTorchOpened) {
             return Futures.immediateFuture(false);
         }
 
@@ -1467,10 +1510,12 @@
     }
 
     /** Issues a request to start auto exposure scan. */
-    ListenableFuture<CameraCaptureResult> triggerAePrecapture(TakePictureState state) {
+    ListenableFuture<Void> triggerAePrecapture(TakePictureState state) {
         Logger.d(TAG, "triggerAePrecapture");
         state.mIsAePrecaptureTriggered = true;
-        return getCameraControl().triggerAePrecapture();
+        // Transform type from CameraCaptureResult to Void
+        return Futures.transform(getCameraControl().triggerAePrecapture(), captureResult -> null,
+                CameraXExecutors.directExecutor());
     }
 
     /** Issues a request to cancel auto focus and/or auto exposure scan. */
@@ -2059,6 +2104,7 @@
      */
     static final class TakePictureState {
         CameraCaptureResult mPreCaptureState = EmptyCameraCaptureResult.create();
+        boolean mIsTorchOpened = false;
         boolean mIsAfTriggered = false;
         boolean mIsAePrecaptureTriggered = false;
     }
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/Preview.java b/camera/camera-core/src/main/java/androidx/camera/core/Preview.java
index e9af877..99b6dc6 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/Preview.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/Preview.java
@@ -468,7 +468,7 @@
     @RestrictTo(Scope.LIBRARY_GROUP)
     @NonNull
     @Override
-    UseCaseConfig<?> onMergeConfig(@NonNull CameraInfoInternal cameraInfo,
+    protected UseCaseConfig<?> onMergeConfig(@NonNull CameraInfoInternal cameraInfo,
             @NonNull UseCaseConfig.Builder<?, ?, ?> builder) {
         if (builder.getMutableConfig().retrieveOption(OPTION_PREVIEW_CAPTURE_PROCESSOR, null)
                 != null) {
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/UseCase.java b/camera/camera-core/src/main/java/androidx/camera/core/UseCase.java
index ca32637..247b2311 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/UseCase.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/UseCase.java
@@ -243,7 +243,7 @@
      */
     @RestrictTo(Scope.LIBRARY_GROUP)
     @NonNull
-    UseCaseConfig<?> onMergeConfig(@NonNull CameraInfoInternal cameraInfo,
+    protected UseCaseConfig<?> onMergeConfig(@NonNull CameraInfoInternal cameraInfo,
             @NonNull UseCaseConfig.Builder<?, ?, ?> builder) {
         return builder.getUseCaseConfig();
     }
@@ -652,7 +652,7 @@
      *
      * @hide
      */
-    @RestrictTo(Scope.LIBRARY)
+    @RestrictTo(Scope.LIBRARY_GROUP)
     public void setViewPortCropRect(@NonNull Rect viewPortCropRect) {
         mViewPortCropRect = viewPortCropRect;
     }
@@ -662,7 +662,7 @@
      *
      * @hide
      */
-    @RestrictTo(Scope.LIBRARY)
+    @RestrictTo(Scope.LIBRARY_GROUP)
     @Nullable
     public Rect getViewPortCropRect() {
         return mViewPortCropRect;
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/internal/compat/quirk/DeviceQuirksLoader.java b/camera/camera-core/src/main/java/androidx/camera/core/internal/compat/quirk/DeviceQuirksLoader.java
index 0a15ab7..debf4f3 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/internal/compat/quirk/DeviceQuirksLoader.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/internal/compat/quirk/DeviceQuirksLoader.java
@@ -47,6 +47,10 @@
             quirks.add(new ImageCaptureRotationOptionQuirk());
         }
 
+        if (ImageCaptureWashedOutImageQuirk.load()) {
+            quirks.add(new ImageCaptureWashedOutImageQuirk());
+        }
+
         return quirks;
     }
 }
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/internal/compat/quirk/ImageCaptureWashedOutImageQuirk.java b/camera/camera-core/src/main/java/androidx/camera/core/internal/compat/quirk/ImageCaptureWashedOutImageQuirk.java
new file mode 100644
index 0000000..62fa918
--- /dev/null
+++ b/camera/camera-core/src/main/java/androidx/camera/core/internal/compat/quirk/ImageCaptureWashedOutImageQuirk.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.core.internal.compat.quirk;
+
+import android.os.Build;
+
+import androidx.camera.core.impl.Quirk;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Locale;
+
+/**
+ * Quirk that prevents from getting washed out image while taking picture with flash ON/AUTO mode.
+ *
+ * <p>See b/176399765 and b/181966663.
+ */
+public class ImageCaptureWashedOutImageQuirk implements Quirk {
+
+    // List of devices with the issue. See b/181966663.
+    private static final List<String> DEVICE_MODELS = Arrays.asList(
+            // Galaxy S7
+            "SM-G9300",
+            "SM-G930R",
+            "SM-G930A",
+            "SM-G930V",
+            "SM-G930T",
+            "SM-G930U",
+            "SM-G930P",
+
+            // Galaxy S7+
+            "SM-SC02H",
+            "SM-SCV33",
+            "SM-G9350",
+            "SM-G935R",
+            "SM-G935A",
+            "SM-G935V",
+            "SM-G935T",
+            "SM-G935U",
+            "SM-G935P"
+    );
+
+    static boolean load() {
+        return "SAMSUNG".equals(Build.BRAND.toUpperCase(Locale.US))
+                && DEVICE_MODELS.contains(Build.MODEL.toUpperCase(Locale.US));
+    }
+}
diff --git a/camera/camera-testing/src/main/AndroidManifest.xml b/camera/camera-testing/src/main/AndroidManifest.xml
index 20e22e1..958dea7 100644
--- a/camera/camera-testing/src/main/AndroidManifest.xml
+++ b/camera/camera-testing/src/main/AndroidManifest.xml
@@ -17,6 +17,7 @@
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
     package="androidx.camera.testing">
     <uses-permission android:name="android.permission.BROADCAST_CLOSE_SYSTEM_DIALOGS"/>
+    <uses-permission android:name="android.permission.CAMERA" />
     <application>
         <activity
             android:name="androidx.camera.testing.activity.ForegroundTestActivity"
@@ -26,6 +27,22 @@
                 <category android:name="android.intent.category.DEFAULT" />
             </intent-filter>
         </activity>
+        <activity
+            android:name="androidx.camera.testing.activity.CameraXTestActivity"
+            android:label="CameraX TestActivity">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="android.intent.category.DEFAULT" />
+            </intent-filter>
+        </activity>
+        <activity
+            android:name="androidx.camera.testing.activity.Camera2TestActivity"
+            android:label="Camera2 TestActivity">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="android.intent.category.DEFAULT" />
+            </intent-filter>
+        </activity>
     </application>
 </manifest>
 
diff --git a/camera/camera-testing/src/main/java/androidx/camera/testing/activity/CameraXTestActivity.java b/camera/camera-testing/src/main/java/androidx/camera/testing/activity/CameraXTestActivity.java
index 0952135..30e7dea 100644
--- a/camera/camera-testing/src/main/java/androidx/camera/testing/activity/CameraXTestActivity.java
+++ b/camera/camera-testing/src/main/java/androidx/camera/testing/activity/CameraXTestActivity.java
@@ -17,11 +17,13 @@
 package androidx.camera.testing.activity;
 
 
+import static androidx.camera.testing.SurfaceTextureProvider.createSurfaceTextureProvider;
+
 import android.graphics.SurfaceTexture;
 import android.os.Bundle;
 import android.util.Size;
-import android.view.Surface;
 import android.view.TextureView;
+import android.view.ViewGroup;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
@@ -32,10 +34,10 @@
 import androidx.camera.core.Logger;
 import androidx.camera.core.Preview;
 import androidx.camera.core.impl.CameraInternal;
-import androidx.camera.core.impl.utils.executor.CameraXExecutors;
 import androidx.camera.core.internal.CameraUseCaseAdapter;
 import androidx.camera.testing.CameraUtil;
 import androidx.camera.testing.R;
+import androidx.camera.testing.SurfaceTextureProvider;
 import androidx.test.espresso.idling.CountingIdlingResource;
 
 import java.util.Collections;
@@ -113,7 +115,6 @@
                     public void onSurfaceTextureAvailable(@NonNull SurfaceTexture surfaceTexture,
                             int width, int height) {
                         Logger.d(TAG, "SurfaceTexture available");
-                        setSurfaceProvider(surfaceTexture);
                     }
 
                     @Override
@@ -138,6 +139,24 @@
                     }
                 });
 
+        mPreview.setSurfaceProvider(createSurfaceTextureProvider(
+                new SurfaceTextureProvider.SurfaceTextureCallback() {
+                    @Override
+                    public void onSurfaceTextureReady(@NonNull SurfaceTexture surfaceTexture,
+                            @NonNull Size resolution) {
+                        ViewGroup viewGroup = (ViewGroup) textureView.getParent();
+                        viewGroup.removeView(textureView);
+                        viewGroup.addView(textureView, resolution.getWidth(),
+                                resolution.getHeight());
+                        textureView.setSurfaceTexture(surfaceTexture);
+                    }
+
+                    @Override
+                    public void onSafeToRelease(@NonNull SurfaceTexture surfaceTexture) {
+                        surfaceTexture.release();
+                    }
+                }));
+
         try {
             final CameraX cameraX = CameraX.getOrCreateInstance(this).get();
             final LinkedHashSet<CameraInternal> cameras =
@@ -158,25 +177,6 @@
                 cameraSelector).getCameraInfoInternal().getCameraId();
     }
 
-    void setSurfaceProvider(@NonNull SurfaceTexture surfaceTexture) {
-        if (mPreview == null) {
-            return;
-        }
-        mPreview.setSurfaceProvider((surfaceRequest) -> {
-            final Size resolution = surfaceRequest.getResolution();
-            surfaceTexture.setDefaultBufferSize(resolution.getWidth(), resolution.getHeight());
-
-            final Surface surface = new Surface(surfaceTexture);
-            surfaceRequest.provideSurface(
-                    surface,
-                    CameraXExecutors.directExecutor(),
-                    (surfaceResponse) -> {
-                        surface.release();
-                        surfaceTexture.release();
-                    });
-        });
-    }
-
     @Nullable
     public String getCameraId() {
         return mCameraId;
diff --git a/camera/camera-testing/src/main/res/layout/activity_camera_main.xml b/camera/camera-testing/src/main/res/layout/activity_camera_main.xml
index fdc92c0..5a08f695 100644
--- a/camera/camera-testing/src/main/res/layout/activity_camera_main.xml
+++ b/camera/camera-testing/src/main/res/layout/activity_camera_main.xml
@@ -13,7 +13,7 @@
 See the License for the specific language governing permissions and
 limitations under the License.
 -->
-<RelativeLayout
+<FrameLayout
     xmlns:android="http://schemas.android.com/apk/res/android"
     android:layout_width="match_parent"
     android:layout_height="match_parent">
@@ -23,4 +23,4 @@
         android:layout_width="match_parent"
         android:layout_height="match_parent" />
 
-</RelativeLayout>
+</FrameLayout>
diff --git a/camera/camera-video/src/androidTest/java/androidx/camera/video/VideoCaptureDeviceTest.kt b/camera/camera-video/src/androidTest/java/androidx/camera/video/VideoCaptureDeviceTest.kt
new file mode 100644
index 0000000..b94f63e
--- /dev/null
+++ b/camera/camera-video/src/androidTest/java/androidx/camera/video/VideoCaptureDeviceTest.kt
@@ -0,0 +1,300 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.video
+
+import android.content.Context
+import android.graphics.SurfaceTexture
+import android.os.Build
+import android.view.Surface
+import androidx.camera.camera2.Camera2Config
+import androidx.camera.core.CameraInfo
+import androidx.camera.core.CameraSelector
+import androidx.camera.core.CameraX
+import androidx.camera.core.SurfaceRequest
+import androidx.camera.core.impl.MutableStateObservable
+import androidx.camera.core.impl.Observable
+import androidx.camera.core.internal.CameraUseCaseAdapter
+import androidx.camera.testing.CameraUtil
+import androidx.camera.testing.GLUtil
+import androidx.camera.video.QualitySelector.QUALITY_HIGHEST
+import androidx.camera.video.QualitySelector.QUALITY_LOWEST
+import androidx.camera.video.VideoOutput.StreamState
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.LargeTest
+import androidx.test.platform.app.InstrumentationRegistry
+import com.google.common.truth.Truth.assertThat
+import com.google.common.truth.Truth.assertWithMessage
+import kotlinx.coroutines.asCoroutineDispatcher
+import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.withContext
+import org.junit.After
+import org.junit.Assume.assumeFalse
+import org.junit.Assume.assumeTrue
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import java.util.concurrent.ArrayBlockingQueue
+import java.util.concurrent.Executors
+import java.util.concurrent.Semaphore
+import java.util.concurrent.TimeUnit
+
+@LargeTest
+@RunWith(AndroidJUnit4::class)
+class VideoCaptureDeviceTest {
+
+    @get:Rule
+    val cameraRule = CameraUtil.grantCameraPermissionAndPreTest()
+
+    private val instrumentation = InstrumentationRegistry.getInstrumentation()
+    private val context: Context = ApplicationProvider.getApplicationContext()
+    private val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
+
+    private lateinit var cameraUseCaseAdapter: CameraUseCaseAdapter
+    private lateinit var cameraInfo: CameraInfo
+
+    @Before
+    fun setUp() {
+        CameraX.initialize(context, Camera2Config.defaultConfig()).get()
+
+        cameraUseCaseAdapter = CameraUtil.createCameraUseCaseAdapter(context, cameraSelector)
+        cameraInfo = cameraUseCaseAdapter.cameraInfo
+    }
+
+    @After
+    fun tearDown() {
+        if (this::cameraUseCaseAdapter.isInitialized) {
+            instrumentation.runOnMainSync {
+                cameraUseCaseAdapter.apply {
+                    removeUseCases(useCases)
+                }
+            }
+        }
+        CameraX.shutdown().get(10, TimeUnit.SECONDS)
+    }
+
+    @Test
+    fun addUseCases_canReceiveFrame() = runBlocking {
+        // Arrange.
+        val videoOutput = TestVideoOutput()
+        val videoCapture = VideoCapture.withOutput(videoOutput)
+
+        // Act.
+        instrumentation.runOnMainSync {
+            cameraUseCaseAdapter.addUseCases(listOf(videoCapture))
+        }
+
+        // Assert.
+        val surfaceRequest = videoOutput.nextSurfaceRequest(5, TimeUnit.SECONDS)
+        val frameUpdateSemaphore = surfaceRequest.provideUpdatingSurface()
+        assertThat(frameUpdateSemaphore.tryAcquire(5, 10, TimeUnit.SECONDS)).isTrue()
+    }
+
+    @Test
+    fun changeStreamState_canReceiveFrame() = runBlocking {
+        // Arrange.
+        val videoOutput = TestVideoOutput(streamState = StreamState.INACTIVE)
+        val videoCapture = VideoCapture.withOutput(videoOutput)
+
+        // Act.
+        instrumentation.runOnMainSync {
+            cameraUseCaseAdapter.addUseCases(listOf(videoCapture))
+        }
+
+        // Assert.
+        val surfaceRequest = videoOutput.nextSurfaceRequest(5, TimeUnit.SECONDS)
+        val frameUpdateSemaphore = surfaceRequest.provideUpdatingSurface()
+        // No frame should be updated by INACTIVE state
+        assertThat(frameUpdateSemaphore.tryAcquire(1, 2, TimeUnit.SECONDS)).isFalse()
+
+        // Act.
+        videoOutput.setStreamState(StreamState.ACTIVE)
+
+        // Assert.
+        assertThat(frameUpdateSemaphore.tryAcquire(5, 10, TimeUnit.SECONDS)).isTrue()
+    }
+
+    @Test
+    fun addUseCases_setSupportedQuality_getCorrectResolution() {
+        assumeTrue(QualitySelector.getSupportedQualities(cameraInfo).isNotEmpty())
+        // Cuttlefish API 29 has inconsistent resolution issue. See b/184015059.
+        assumeFalse(Build.MODEL.contains("Cuttlefish") && Build.VERSION.SDK_INT == 29)
+
+        // Arrange.
+        val qualityList = QualitySelector.getSupportedQualities(cameraInfo) + arrayOf(
+            QUALITY_HIGHEST,
+            QUALITY_LOWEST
+        )
+        qualityList.forEach { quality ->
+            val targetResolution = QualitySelector.getResolution(cameraInfo, quality)
+            val videoOutput = TestVideoOutput(
+                mediaSpec = MediaSpec.builder().configureVideo {
+                    it.setQualitySelector(QualitySelector.of(quality))
+                }.build()
+            )
+            val videoCapture = VideoCapture.withOutput(videoOutput)
+
+            // Act.
+            instrumentation.runOnMainSync {
+                cameraUseCaseAdapter.addUseCases(listOf(videoCapture))
+            }
+
+            // Assert.
+            val surfaceRequest = videoOutput.nextSurfaceRequest(5, TimeUnit.SECONDS)
+            assertWithMessage("Set quality value by $quality")
+                .that(surfaceRequest.resolution).isEqualTo(targetResolution)
+
+            // Cleanup.
+            instrumentation.runOnMainSync {
+                cameraUseCaseAdapter.apply {
+                    removeUseCases(listOf(videoCapture))
+                }
+            }
+        }
+    }
+
+    @Test
+    fun addUseCases_setQualityWithRotation_getCorrectResolution() {
+        assumeTrue(QualitySelector.getSupportedQualities(cameraInfo).isNotEmpty())
+        // Cuttlefish API 29 has inconsistent resolution issue. See b/184015059.
+        assumeFalse(Build.MODEL.contains("Cuttlefish") && Build.VERSION.SDK_INT == 29)
+
+        val targetResolution = QualitySelector.getResolution(cameraInfo, QUALITY_LOWEST)
+
+        arrayOf(
+            Surface.ROTATION_0, Surface.ROTATION_90, Surface.ROTATION_180, Surface.ROTATION_270
+        ).forEach { rotation ->
+            // Arrange.
+            val videoOutput = TestVideoOutput(
+                mediaSpec = MediaSpec.builder().configureVideo {
+                    it.setQualitySelector(QualitySelector.of(QUALITY_LOWEST))
+                }.build()
+            )
+            val videoCapture = VideoCapture.withOutput(videoOutput)
+
+            // Act.
+            instrumentation.runOnMainSync {
+                cameraUseCaseAdapter.addUseCases(listOf(videoCapture))
+            }
+
+            // Assert.
+            val surfaceRequest = videoOutput.nextSurfaceRequest(5, TimeUnit.SECONDS)
+            assertWithMessage("Set rotation value by $rotation")
+                .that(surfaceRequest.resolution).isEqualTo(targetResolution)
+
+            // Cleanup.
+            instrumentation.runOnMainSync {
+                cameraUseCaseAdapter.apply {
+                    removeUseCases(listOf(videoCapture))
+                }
+            }
+        }
+    }
+
+    @Test
+    fun useCaseCanBeReused() = runBlocking {
+        // Arrange.
+        val videoOutput = TestVideoOutput()
+        val videoCapture = VideoCapture.withOutput(videoOutput)
+
+        // Act.
+        instrumentation.runOnMainSync {
+            cameraUseCaseAdapter.addUseCases(listOf(videoCapture))
+        }
+
+        // Assert.
+        var surfaceRequest = videoOutput.nextSurfaceRequest(5, TimeUnit.SECONDS)
+        var frameUpdateSemaphore = surfaceRequest.provideUpdatingSurface()
+        assertThat(frameUpdateSemaphore.tryAcquire(5, 10, TimeUnit.SECONDS)).isTrue()
+
+        // Act.
+        // Reuse use case
+        instrumentation.runOnMainSync {
+            cameraUseCaseAdapter.apply {
+                removeUseCases(listOf(videoCapture))
+            }
+            cameraUseCaseAdapter.addUseCases(listOf(videoCapture))
+        }
+
+        // Assert.
+        surfaceRequest = videoOutput.nextSurfaceRequest(5, TimeUnit.SECONDS)
+        frameUpdateSemaphore = surfaceRequest.provideUpdatingSurface()
+        assertThat(frameUpdateSemaphore.tryAcquire(5, 10, TimeUnit.SECONDS)).isTrue()
+    }
+
+    private class TestVideoOutput(
+        streamState: StreamState = StreamState.ACTIVE,
+        mediaSpec: MediaSpec = MediaSpec.builder().build()
+    ) : VideoOutput {
+        private val surfaceRequests = ArrayBlockingQueue<SurfaceRequest>(10)
+
+        private val streamStateObservable: MutableStateObservable<StreamState> =
+            MutableStateObservable.withInitialState(streamState)
+
+        private val mediaSpecObservable: MutableStateObservable<MediaSpec> =
+            MutableStateObservable.withInitialState(mediaSpec)
+
+        override fun onSurfaceRequested(surfaceRequest: SurfaceRequest) {
+            surfaceRequests.put(surfaceRequest)
+        }
+
+        override fun getStreamState(): Observable<StreamState> = streamStateObservable
+
+        override fun getMediaSpec(): Observable<MediaSpec> = mediaSpecObservable
+
+        fun nextSurfaceRequest(timeout: Long, timeUnit: TimeUnit): SurfaceRequest {
+            return surfaceRequests.poll(timeout, timeUnit)
+        }
+
+        fun setStreamState(streamState: StreamState) = streamStateObservable.setState(streamState)
+
+        fun setMediaSpec(mediaSpec: MediaSpec) = mediaSpecObservable.setState(mediaSpec)
+    }
+
+    private suspend fun SurfaceRequest.provideUpdatingSurface(): Semaphore {
+        var isReleased = false
+        val frameUpdateSemaphore = Semaphore(0)
+        val executor = Executors.newFixedThreadPool(1)
+
+        val surfaceTexture = withContext(executor.asCoroutineDispatcher()) {
+            SurfaceTexture(0).apply {
+                setDefaultBufferSize(640, 480)
+                detachFromGLContext()
+                attachToGLContext(GLUtil.getTexIdFromGLContext())
+                setOnFrameAvailableListener {
+                    frameUpdateSemaphore.release()
+                    executor.execute {
+                        if (!isReleased) {
+                            updateTexImage()
+                        }
+                    }
+                }
+            }
+        }
+        val surface = Surface(surfaceTexture)
+
+        provideSurface(surface, executor) {
+            surfaceTexture.release()
+            surface.release()
+            executor.shutdown()
+            isReleased = true
+        }
+
+        return frameUpdateSemaphore
+    }
+}
diff --git a/camera/camera-video/src/androidTest/java/androidx/camera/video/VideoCaptureTest.kt b/camera/camera-video/src/androidTest/java/androidx/camera/video/VideoCaptureIntegrationTest.kt
similarity index 99%
rename from camera/camera-video/src/androidTest/java/androidx/camera/video/VideoCaptureTest.kt
rename to camera/camera-video/src/androidTest/java/androidx/camera/video/VideoCaptureIntegrationTest.kt
index 0ed0e9c..9513f29 100644
--- a/camera/camera-video/src/androidTest/java/androidx/camera/video/VideoCaptureTest.kt
+++ b/camera/camera-video/src/androidTest/java/androidx/camera/video/VideoCaptureIntegrationTest.kt
@@ -76,7 +76,7 @@
 
 @LargeTest
 @RunWith(Parameterized::class)
-class VideoCaptureTest(
+class VideoCaptureIntegrationTest(
     private var cameraSelector: CameraSelector
 ) {
     companion object {
diff --git a/camera/camera-video/src/main/java/androidx/camera/video/VideoCapture.java b/camera/camera-video/src/main/java/androidx/camera/video/VideoCapture.java
index 347532b..900c7f8 100644
--- a/camera/camera-video/src/main/java/androidx/camera/video/VideoCapture.java
+++ b/camera/camera-video/src/main/java/androidx/camera/video/VideoCapture.java
@@ -16,9 +16,757 @@
 
 package androidx.camera.video;
 
+import static androidx.camera.core.impl.ImageOutputConfig.OPTION_DEFAULT_RESOLUTION;
+import static androidx.camera.core.impl.ImageOutputConfig.OPTION_MAX_RESOLUTION;
+import static androidx.camera.core.impl.ImageOutputConfig.OPTION_SUPPORTED_RESOLUTIONS;
+import static androidx.camera.core.impl.ImageOutputConfig.OPTION_TARGET_ASPECT_RATIO;
+import static androidx.camera.core.impl.ImageOutputConfig.OPTION_TARGET_RESOLUTION;
+import static androidx.camera.core.impl.ImageOutputConfig.OPTION_TARGET_ROTATION;
+import static androidx.camera.core.impl.UseCaseConfig.OPTION_ATTACHED_USE_CASES_UPDATE_LISTENER;
+import static androidx.camera.core.impl.UseCaseConfig.OPTION_CAMERA_SELECTOR;
+import static androidx.camera.core.impl.UseCaseConfig.OPTION_CAPTURE_CONFIG_UNPACKER;
+import static androidx.camera.core.impl.UseCaseConfig.OPTION_DEFAULT_CAPTURE_CONFIG;
+import static androidx.camera.core.impl.UseCaseConfig.OPTION_DEFAULT_SESSION_CONFIG;
+import static androidx.camera.core.impl.UseCaseConfig.OPTION_SESSION_CONFIG_UNPACKER;
+import static androidx.camera.core.impl.UseCaseConfig.OPTION_SURFACE_OCCUPANCY_PRIORITY;
+import static androidx.camera.core.internal.TargetConfig.OPTION_TARGET_CLASS;
+import static androidx.camera.core.internal.TargetConfig.OPTION_TARGET_NAME;
+import static androidx.camera.core.internal.ThreadConfig.OPTION_BACKGROUND_EXECUTOR;
+import static androidx.camera.core.internal.UseCaseEventConfig.OPTION_USE_CASE_EVENT_CALLBACK;
+import static androidx.camera.video.impl.VideoCaptureConfig.OPTION_VIDEO_OUTPUT;
+
+import android.graphics.Rect;
+import android.util.Pair;
+import android.util.Size;
+import android.view.Display;
+import android.view.Surface;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.OptIn;
+import androidx.annotation.RestrictTo;
+import androidx.annotation.RestrictTo.Scope;
+import androidx.annotation.UiThread;
+import androidx.camera.core.AspectRatio;
+import androidx.camera.core.CameraSelector;
+import androidx.camera.core.ExperimentalUseCaseGroup;
+import androidx.camera.core.Logger;
+import androidx.camera.core.SurfaceRequest;
+import androidx.camera.core.UseCase;
+import androidx.camera.core.impl.CameraInfoInternal;
+import androidx.camera.core.impl.CaptureConfig;
+import androidx.camera.core.impl.Config;
+import androidx.camera.core.impl.ConfigProvider;
+import androidx.camera.core.impl.DeferrableSurface;
+import androidx.camera.core.impl.ImageOutputConfig;
+import androidx.camera.core.impl.ImageOutputConfig.RotationValue;
+import androidx.camera.core.impl.MutableConfig;
+import androidx.camera.core.impl.MutableOptionsBundle;
+import androidx.camera.core.impl.Observable;
+import androidx.camera.core.impl.Observable.Observer;
+import androidx.camera.core.impl.OptionsBundle;
+import androidx.camera.core.impl.SessionConfig;
+import androidx.camera.core.impl.UseCaseConfig;
+import androidx.camera.core.impl.UseCaseConfigFactory;
+import androidx.camera.core.impl.utils.Threads;
+import androidx.camera.core.impl.utils.executor.CameraXExecutors;
+import androidx.camera.core.internal.ThreadConfig;
+import androidx.camera.video.VideoOutput.StreamState;
+import androidx.camera.video.impl.VideoCaptureConfig;
+import androidx.core.util.Consumer;
+import androidx.core.util.Preconditions;
+
+import com.google.common.util.concurrent.ListenableFuture;
+
+import java.lang.reflect.Type;
+import java.util.Collection;
+import java.util.List;
+import java.util.UUID;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Executor;
+
 /**
- * A use case for taking a video.
+ * A use case that provides camera stream suitable for video application.
+ *
+ * <p>VideoCapture is used to create a camera stream suitable for video application. This stream
+ * is used by the implementation of {@link VideoOutput}. Calling {@link #withOutput(VideoOutput)}
+ * can generate a VideoCapture use case binding to the given VideoOutput.
+ *
+ * <p>When binding VideoCapture, VideoCapture will initialize the camera stream according to the
+ * resolution found by the {@link QualitySelector} in VideoOutput. Then VideoCapture will invoke
+ * {@link VideoOutput#onSurfaceRequested(SurfaceRequest)} to request VideoOutput to provide a
+ * {@link Surface} via {@link SurfaceRequest#provideSurface} to complete the initialization
+ * process. After VideoCapture is bound, updating the QualitySelector in VideoOutput will have no
+ * effect. If it needs to change the resolution of the camera stream after VideoCapture is bound,
+ * it has to unbind the original VideoCapture, update the QualitySelector in VideoOutput and then
+ * re-bind the VideoCapture. If the implementation of VideoOutput does not support modifying the
+ * QualitySelector afterwards, it has to create a new VideoOutput and VideoCapture for re-bind.
+ *
+ * @param <T> the type of VideoOutput
  */
-public class VideoCapture {
-    private VideoCapture() {}
+public final class VideoCapture<T extends VideoOutput> extends UseCase {
+    private static final String TAG = "VideoCapture";
+    private static final Defaults DEFAULT_CONFIG = new Defaults();
+
+    private DeferrableSurface mDeferrableSurface;
+    private SurfaceRequest mSurfaceRequest;
+
+    /**
+     * Create a VideoCapture builder with a {@link VideoOutput}.
+     *
+     * @param videoOutput the associated VideoOutput.
+     * @return the new Builder
+     */
+    @NonNull
+    public static <T extends VideoOutput> VideoCapture<T> withOutput(@NonNull T videoOutput) {
+        return new VideoCapture.Builder<T>(videoOutput).build();
+    }
+
+    /**
+     * Creates a new video capture use case from the given configuration.
+     *
+     * @param config for this use case instance
+     */
+    VideoCapture(@NonNull VideoCaptureConfig<T> config) {
+        super(config);
+    }
+
+    /**
+     * Gets the {@link VideoOutput} associated to this VideoCapture.
+     */
+    @SuppressWarnings("unchecked")
+    @NonNull
+    public T getOutput() {
+        return ((VideoCaptureConfig<T>) getCurrentConfig()).getVideoOutput();
+    }
+
+    /**
+     * Sets the desired rotation of the output video.
+     *
+     * <p>In most cases this should be set to the current rotation returned by {@link
+     * Display#getRotation()}.
+     *
+     * @param rotation Desired rotation of the output video.
+     */
+    public void setTargetRotation(@RotationValue int rotation) {
+        setTargetRotationInternal(rotation);
+    }
+
+    @Override
+    public void onAttached() {
+        getOutput().getStreamState().addObserver(CameraXExecutors.mainThreadExecutor(),
+                mStreamStateObserver);
+    }
+
+    /**
+     * {@inheritDoc}
+     *
+     * @hide
+     */
+    @SuppressWarnings("unchecked")
+    @Override
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @NonNull
+    protected Size onSuggestedResolutionUpdated(@NonNull Size suggestedResolution) {
+        String cameraId = getCameraId();
+        VideoCaptureConfig<T> config = (VideoCaptureConfig<T>) getCurrentConfig();
+
+        SessionConfig.Builder sessionConfigBuilder = createPipeline(cameraId, config,
+                suggestedResolution);
+        updateSessionConfig(sessionConfigBuilder.build());
+
+        return suggestedResolution;
+    }
+
+    /**
+     * {@inheritDoc}
+     *
+     * @hide
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @Override
+    public void onDetached() {
+        clearPipeline();
+
+        getOutput().getStreamState().removeObserver(mStreamStateObserver);
+    }
+
+    @NonNull
+    @Override
+    public String toString() {
+        return TAG + ":" + getName();
+    }
+
+    /**
+     * {@inheritDoc}
+     *
+     * @hide
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @Override
+    @Nullable
+    public UseCaseConfig<?> getDefaultConfig(boolean applyDefaultConfig,
+            @NonNull UseCaseConfigFactory factory) {
+        Config captureConfig = factory.getConfig(UseCaseConfigFactory.CaptureType.VIDEO_CAPTURE);
+
+        if (applyDefaultConfig) {
+            captureConfig = Config.mergeConfigs(captureConfig, DEFAULT_CONFIG.getConfig());
+        }
+
+        return captureConfig == null ? null :
+                getUseCaseConfigBuilder(captureConfig).getUseCaseConfig();
+    }
+
+    /**
+     * {@inheritDoc}
+     *
+     * @hide
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @NonNull
+    @Override
+    protected UseCaseConfig<?> onMergeConfig(@NonNull CameraInfoInternal cameraInfo,
+            @NonNull UseCaseConfig.Builder<?, ?, ?> builder) {
+
+        updateTargetResolutionByQuality(cameraInfo, builder);
+
+        return builder.getUseCaseConfig();
+    }
+
+    /**
+     * {@inheritDoc}
+     *
+     * @hide
+     */
+    @NonNull
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @Override
+    public UseCaseConfig.Builder<?, ?, ?> getUseCaseConfigBuilder(@NonNull Config config) {
+        return Builder.fromConfig(config);
+    }
+
+    @UiThread
+    @OptIn(markerClass = ExperimentalUseCaseGroup.class)
+    @NonNull
+    private SessionConfig.Builder createPipeline(@NonNull String cameraId,
+            @NonNull VideoCaptureConfig<T> config,
+            @NonNull Size resolution) {
+        Threads.checkMainThread();
+
+        mSurfaceRequest = new SurfaceRequest(resolution, getCamera(), false);
+        config.getVideoOutput().onSurfaceRequested(mSurfaceRequest);
+        Rect cropRect = getViewPortCropRect() != null ? getViewPortCropRect() : new Rect(0, 0,
+                resolution.getWidth(), resolution.getHeight());
+        mSurfaceRequest.updateTransformationInfo(SurfaceRequest.TransformationInfo.of(cropRect,
+                getRelativeRotation(getCamera()), getTargetRotationInternal()));
+        mDeferrableSurface = mSurfaceRequest.getDeferrableSurface();
+
+        SessionConfig.Builder sessionConfigBuilder = SessionConfig.Builder.createFrom(config);
+        sessionConfigBuilder.addSurface(mDeferrableSurface);
+        sessionConfigBuilder.addErrorListener(
+                (sessionConfig, error) -> resetPipeline(cameraId, config, resolution));
+
+        return sessionConfigBuilder;
+    }
+
+    /**
+     * Clear the internal pipeline so that the pipeline can be set up again.
+     */
+    @UiThread
+    private void clearPipeline() {
+        Threads.checkMainThread();
+
+        if (mDeferrableSurface != null) {
+            mDeferrableSurface.close();
+            mDeferrableSurface = null;
+        }
+
+        mSurfaceRequest = null;
+    }
+
+    @UiThread
+    @SuppressWarnings("WeakerAccess") /* synthetic accessor */
+    void resetPipeline(@NonNull String cameraId,
+            @NonNull VideoCaptureConfig<T> config,
+            @NonNull Size resolution) {
+        clearPipeline();
+
+        // Ensure the attached camera has not changed before resetting.
+        // TODO(b/143915543): Ensure this never gets called by a camera that is not attached
+        //  to this use case so we don't need to do this check.
+        if (isCurrentCamera(cameraId)) {
+            // Only reset the pipeline when the bound camera is the same.
+            SessionConfig.Builder sessionConfigBuilder = createPipeline(cameraId, config,
+                    resolution);
+            updateSessionConfig(sessionConfigBuilder.build());
+            notifyReset();
+        }
+    }
+
+    /**
+     * Provides a base static default configuration for the VideoCapture
+     *
+     * <p>These values may be overridden by the implementation. They only provide a minimum set of
+     * defaults that are implementation independent.
+     *
+     * @hide
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    public static final class Defaults implements ConfigProvider<VideoCaptureConfig<?>> {
+        /** Surface occupancy priority to this use case */
+        private static final int DEFAULT_SURFACE_OCCUPANCY_PRIORITY = 3;
+        private static final VideoOutput DEFAULT_VIDEO_OUTPUT =
+                SurfaceRequest::willNotProvideSurface;
+        static final Size DEFAULT_RESOLUTION = new Size(1920, 1080);
+
+        private static final VideoCaptureConfig<?> DEFAULT_CONFIG;
+
+        static {
+            Builder<?> builder = new Builder<>(DEFAULT_VIDEO_OUTPUT)
+                    .setSurfaceOccupancyPriority(DEFAULT_SURFACE_OCCUPANCY_PRIORITY);
+
+            DEFAULT_CONFIG = builder.getUseCaseConfig();
+        }
+
+        @NonNull
+        @Override
+        public VideoCaptureConfig<?> getConfig() {
+            return DEFAULT_CONFIG;
+        }
+    }
+
+    @Nullable
+    private MediaSpec getMediaSpec() {
+        return fetchObservableValue(getOutput().getMediaSpec(), null);
+    }
+
+    private final Observer<StreamState> mStreamStateObserver = new Observer<StreamState>() {
+        @Override
+        public void onNewData(@Nullable StreamState streamState) {
+            Logger.d(TAG, "Receive streamState = " + streamState);
+            if (streamState == StreamState.ACTIVE) {
+                notifyActive();
+            } else {
+                notifyInactive();
+            }
+        }
+
+        @Override
+        public void onError(@NonNull Throwable t) {
+            Logger.w(TAG, "Receive onError from StreamState observer", t);
+        }
+    };
+
+    /**
+     * Set {@link ImageOutputConfig#OPTION_TARGET_RESOLUTION} according to the resolution found
+     * by the {@link QualitySelector} in VideoOutput.
+     *
+     * <p>If the device doesn't have any supported quality, a default resolution will be set.
+     *
+     * @throws IllegalArgumentException if not able to find a resolution by the QualitySelector
+     * in VideoOutput.
+     */
+    private void updateTargetResolutionByQuality(@NonNull CameraInfoInternal cameraInfo,
+            @NonNull UseCaseConfig.Builder<?, ?, ?> builder) throws IllegalArgumentException {
+        MediaSpec mediaSpec = getMediaSpec();
+
+        Preconditions.checkArgument(mediaSpec != null,
+                "Unable to update target resolution by null MediaSpec.");
+
+        Size resolution;
+        if (QualitySelector.getSupportedQualities(cameraInfo).isEmpty()) {
+            // When the device does not have any supported quality, even the most flexible
+            // QualitySelector such as QualitySelector.of(QUALITY_HIGHEST), still cannot
+            // find any resolution. This should be a rare case but will cause VideoCapture
+            // to always fail to bind. The workaround is to set a default resolution.
+            Logger.w(TAG,
+                    "Can't find any supported quality on the device, use default resolution.");
+            resolution = Defaults.DEFAULT_RESOLUTION;
+        } else {
+            QualitySelector qualitySelector = mediaSpec.getVideoSpec().getQualitySelector();
+
+            int quality = qualitySelector.select(cameraInfo);
+
+            Logger.d(TAG, "Found quality " + quality + " by qualitySelector " + qualitySelector);
+
+            if (quality != QualitySelector.QUALITY_NONE) {
+                resolution = QualitySelector.getResolution(cameraInfo, quality);
+            } else {
+                throw new IllegalArgumentException(
+                        "Unable to find a resolution by QualitySelector: " + qualitySelector);
+            }
+        }
+
+        int relativeRotation = cameraInfo.getSensorRotationDegrees(getTargetRotationInternal());
+        boolean needRotate = relativeRotation == 90 || relativeRotation == 270;
+        if (needRotate) {
+            resolution = new Size(/* width= */resolution.getHeight(),
+                    /* height= */resolution.getWidth());
+        }
+        Logger.d(TAG,
+                "relativeRotation = " + relativeRotation + " and final resolution = " + resolution);
+
+        builder.getMutableConfig().insertOption(OPTION_TARGET_RESOLUTION, resolution);
+    }
+
+    /**
+     * Gets the snapshot value of the given {@link Observable}.
+     *
+     * <p>Note: Set {@code valueIfMissing} to a non-{@code null} value doesn't mean the method
+     * will never return a {@code null} value. The observable could contain exact {@code null}
+     * value.
+     *
+     * @param observable the observable
+     * @param valueIfMissing if the observable doesn't contain value.
+     * @param <T> the value type
+     * @return the snapshot value of the given {@link Observable}.
+     */
+    @Nullable
+    private static <T> T fetchObservableValue(@NonNull Observable<T> observable,
+            @Nullable T valueIfMissing) {
+        ListenableFuture<T> future = observable.fetchData();
+        if (!future.isDone()) {
+            return valueIfMissing;
+        }
+        try {
+            return future.get();
+        } catch (ExecutionException | InterruptedException e) {
+            // Should not happened
+            throw new IllegalStateException(e);
+        }
+    }
+
+    /**
+     * Builder for a {@link VideoCapture}.
+     *
+     * @param <T> the type of VideoOutput
+     * @hide
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @SuppressWarnings("ObjectToString")
+    public static final class Builder<T extends VideoOutput> implements
+            UseCaseConfig.Builder<VideoCapture<T>, VideoCaptureConfig<T>, Builder<T>>,
+            ImageOutputConfig.Builder<Builder<T>>, ThreadConfig.Builder<Builder<T>> {
+        private final MutableOptionsBundle mMutableConfig;
+
+        /** Creates a new Builder object. */
+        Builder(@NonNull T videoOutput) {
+            this(createInitialBundle(videoOutput));
+        }
+
+        @SuppressWarnings("unchecked")
+        private Builder(@NonNull MutableOptionsBundle mutableConfig) {
+            mMutableConfig = mutableConfig;
+
+            if (!mMutableConfig.containsOption(OPTION_VIDEO_OUTPUT)) {
+                throw new IllegalArgumentException("VideoOutput is required");
+            }
+
+            Class<?> oldConfigClass =
+                    mutableConfig.retrieveOption(OPTION_TARGET_CLASS, null);
+            if (oldConfigClass != null && !oldConfigClass.equals(VideoCapture.class)) {
+                throw new IllegalArgumentException(
+                        "Invalid target class configuration for "
+                                + Builder.this
+                                + ": "
+                                + oldConfigClass);
+            }
+
+            setTargetClass((Class<VideoCapture<T>>) (Type) VideoCapture.class);
+        }
+
+        /** @hide */
+        @RestrictTo(Scope.LIBRARY_GROUP)
+        @NonNull
+        static Builder<? extends VideoOutput> fromConfig(@NonNull Config configuration) {
+            return new Builder<>(MutableOptionsBundle.from(configuration));
+        }
+
+        /**
+         * Generates a Builder from another Config object
+         *
+         * @param configuration An immutable configuration to pre-populate this builder.
+         * @return The new Builder.
+         */
+        @NonNull
+        public static <T extends VideoOutput> Builder<T> fromConfig(
+                @NonNull VideoCaptureConfig<T> configuration) {
+            return new Builder<>(MutableOptionsBundle.from(configuration));
+        }
+
+        @NonNull
+        private static <T extends VideoOutput> MutableOptionsBundle createInitialBundle(
+                @NonNull T videoOutput) {
+            MutableOptionsBundle bundle = MutableOptionsBundle.create();
+            bundle.insertOption(OPTION_VIDEO_OUTPUT, videoOutput);
+            return bundle;
+        }
+
+        /**
+         * {@inheritDoc}
+         *
+         * @hide
+         */
+        @RestrictTo(Scope.LIBRARY_GROUP)
+        @Override
+        @NonNull
+        public MutableConfig getMutableConfig() {
+            return mMutableConfig;
+        }
+
+        /**
+         * {@inheritDoc}
+         *
+         * @hide
+         */
+        @RestrictTo(Scope.LIBRARY_GROUP)
+        @NonNull
+        @Override
+        public VideoCaptureConfig<T> getUseCaseConfig() {
+            return new VideoCaptureConfig<>(OptionsBundle.from(mMutableConfig));
+        }
+
+        /**
+         * Builds an immutable {@link VideoCaptureConfig} from the current state.
+         *
+         * @return A {@link VideoCaptureConfig} populated with the current state.
+         */
+        @Override
+        @NonNull
+        public VideoCapture<T> build() {
+            return new VideoCapture<>(getUseCaseConfig());
+        }
+
+        // Implementations of TargetConfig.Builder default methods
+
+        /** @hide */
+        @RestrictTo(Scope.LIBRARY_GROUP)
+        @Override
+        @NonNull
+        public Builder<T> setTargetClass(@NonNull Class<VideoCapture<T>> targetClass) {
+            getMutableConfig().insertOption(OPTION_TARGET_CLASS, targetClass);
+
+            // If no name is set yet, then generate a unique name
+            if (null == getMutableConfig().retrieveOption(OPTION_TARGET_NAME, null)) {
+                String targetName = targetClass.getCanonicalName() + "-" + UUID.randomUUID();
+                setTargetName(targetName);
+            }
+
+            return this;
+        }
+
+        /**
+         * Sets the name of the target object being configured, used only for debug logging.
+         *
+         * <p>The name should be a value that can uniquely identify an instance of the object being
+         * configured.
+         *
+         * <p>If not set, the target name will default to an unique name automatically generated
+         * with the class canonical name and random UUID.
+         *
+         * @param targetName A unique string identifier for the instance of the class being
+         *                   configured.
+         * @return the current Builder.
+         */
+        @Override
+        @NonNull
+        public Builder<T> setTargetName(@NonNull String targetName) {
+            getMutableConfig().insertOption(OPTION_TARGET_NAME, targetName);
+            return this;
+        }
+
+        // Implementations of ImageOutputConfig.Builder default methods
+
+        /**
+         * Sets the aspect ratio of the intended target for images from this configuration.
+         *
+         * <p>It is not allowed to set both target aspect ratio and target resolution on the same
+         * use case.
+         *
+         * <p>The target aspect ratio is used as a hint when determining the resulting output aspect
+         * ratio which may differ from the request, possibly due to device constraints.
+         * Application code should check the resulting output's resolution.
+         *
+         * <p>If not set, resolutions with aspect ratio 16:9 will be considered in higher
+         * priority.
+         *
+         * @param aspectRatio A {@link AspectRatio} representing the ratio of the target's width
+         *                    and height.
+         * @return The current Builder.
+         * @hide
+         */
+        @RestrictTo(Scope.LIBRARY_GROUP)
+        @NonNull
+        @Override
+        public Builder<T> setTargetAspectRatio(@AspectRatio.Ratio int aspectRatio) {
+            getMutableConfig().insertOption(OPTION_TARGET_ASPECT_RATIO, aspectRatio);
+            return this;
+        }
+
+        /**
+         * Sets the rotation of the intended target for images from this configuration.
+         *
+         * <p>This is one of four valid values: {@link Surface#ROTATION_0}, {@link
+         * Surface#ROTATION_90}, {@link Surface#ROTATION_180}, {@link Surface#ROTATION_270}.
+         * Rotation values are relative to the "natural" rotation, {@link Surface#ROTATION_0}.
+         *
+         * <p>If not set, the target rotation will default to the value of
+         * {@link Display#getRotation()} of the default display at the time the use case is
+         * created. The use case is fully created once it has been attached to a camera.
+         *
+         * @param rotation The rotation of the intended target.
+         * @return The current Builder.
+         */
+        @NonNull
+        @Override
+        public Builder<T> setTargetRotation(@RotationValue int rotation) {
+            getMutableConfig().insertOption(OPTION_TARGET_ROTATION, rotation);
+            return this;
+        }
+
+        /**
+         * setTargetResolution is not supported on VideoCapture
+         *
+         * @hide
+         */
+        @RestrictTo(Scope.LIBRARY_GROUP)
+        @NonNull
+        @Override
+        public Builder<T> setTargetResolution(@NonNull Size resolution) {
+            throw new UnsupportedOperationException("setTargetResolution is not supported.");
+        }
+
+        /**
+         * Sets the default resolution of the intended target from this configuration.
+         *
+         * @param resolution The default resolution to choose from supported output sizes list.
+         * @return The current Builder.
+         * @hide
+         */
+        @RestrictTo(Scope.LIBRARY_GROUP)
+        @NonNull
+        @Override
+        public Builder<T> setDefaultResolution(@NonNull Size resolution) {
+            getMutableConfig().insertOption(OPTION_DEFAULT_RESOLUTION, resolution);
+            return this;
+        }
+
+        /** @hide */
+        @RestrictTo(Scope.LIBRARY_GROUP)
+        @NonNull
+        @Override
+        public Builder<T> setMaxResolution(@NonNull Size resolution) {
+            getMutableConfig().insertOption(OPTION_MAX_RESOLUTION, resolution);
+            return this;
+        }
+
+        /** @hide */
+        @RestrictTo(Scope.LIBRARY_GROUP)
+        @Override
+        @NonNull
+        public Builder<T> setSupportedResolutions(
+                @NonNull List<Pair<Integer, Size[]>> resolutions) {
+            getMutableConfig().insertOption(OPTION_SUPPORTED_RESOLUTIONS, resolutions);
+            return this;
+        }
+
+        // Implementations of ThreadConfig.Builder default methods
+
+        /**
+         * Sets the default executor that will be used for background tasks.
+         *
+         * <p>If not set, the background executor will default to an automatically generated
+         * {@link Executor}.
+         *
+         * @param executor The executor which will be used for background tasks.
+         * @return the current Builder.
+         * @hide
+         */
+        @RestrictTo(Scope.LIBRARY_GROUP)
+        @Override
+        @NonNull
+        public Builder<T> setBackgroundExecutor(@NonNull Executor executor) {
+            getMutableConfig().insertOption(OPTION_BACKGROUND_EXECUTOR, executor);
+            return this;
+        }
+
+        // Implementations of UseCaseConfig.Builder default methods
+
+        /** @hide */
+        @RestrictTo(Scope.LIBRARY_GROUP)
+        @Override
+        @NonNull
+        public Builder<T> setDefaultSessionConfig(@NonNull SessionConfig sessionConfig) {
+            getMutableConfig().insertOption(OPTION_DEFAULT_SESSION_CONFIG, sessionConfig);
+            return this;
+        }
+
+        /** @hide */
+        @RestrictTo(Scope.LIBRARY_GROUP)
+        @Override
+        @NonNull
+        public Builder<T> setDefaultCaptureConfig(@NonNull CaptureConfig captureConfig) {
+            getMutableConfig().insertOption(OPTION_DEFAULT_CAPTURE_CONFIG, captureConfig);
+            return this;
+        }
+
+        /** @hide */
+        @RestrictTo(Scope.LIBRARY_GROUP)
+        @Override
+        @NonNull
+        public Builder<T> setSessionOptionUnpacker(
+                @NonNull SessionConfig.OptionUnpacker optionUnpacker) {
+            getMutableConfig().insertOption(OPTION_SESSION_CONFIG_UNPACKER, optionUnpacker);
+            return this;
+        }
+
+        /** @hide */
+        @RestrictTo(Scope.LIBRARY_GROUP)
+        @Override
+        @NonNull
+        public Builder<T> setCaptureOptionUnpacker(
+                @NonNull CaptureConfig.OptionUnpacker optionUnpacker) {
+            getMutableConfig().insertOption(OPTION_CAPTURE_CONFIG_UNPACKER, optionUnpacker);
+            return this;
+        }
+
+        /** @hide */
+        @RestrictTo(Scope.LIBRARY_GROUP)
+        @Override
+        @NonNull
+        public Builder<T> setSurfaceOccupancyPriority(int priority) {
+            getMutableConfig().insertOption(OPTION_SURFACE_OCCUPANCY_PRIORITY, priority);
+            return this;
+        }
+
+        /** @hide */
+        @RestrictTo(Scope.LIBRARY)
+        @Override
+        @NonNull
+        public Builder<T> setCameraSelector(@NonNull CameraSelector cameraSelector) {
+            getMutableConfig().insertOption(OPTION_CAMERA_SELECTOR, cameraSelector);
+            return this;
+        }
+
+        /** @hide */
+        @RestrictTo(Scope.LIBRARY_GROUP)
+        @Override
+        @NonNull
+        public Builder<T> setUseCaseEventCallback(
+                @NonNull EventCallback useCaseEventCallback) {
+            getMutableConfig().insertOption(OPTION_USE_CASE_EVENT_CALLBACK, useCaseEventCallback);
+            return this;
+        }
+
+        /** @hide */
+        @RestrictTo(Scope.LIBRARY_GROUP)
+        @Override
+        @NonNull
+        public Builder<T> setAttachedUseCasesUpdateListener(
+                @NonNull Consumer<Collection<UseCase>> attachedUseCasesUpdateListener) {
+            getMutableConfig().insertOption(OPTION_ATTACHED_USE_CASES_UPDATE_LISTENER,
+                    attachedUseCasesUpdateListener);
+            return this;
+        }
+    }
 }
diff --git a/camera/camera-video/src/main/java/androidx/camera/video/impl/VideoCaptureConfig.java b/camera/camera-video/src/main/java/androidx/camera/video/impl/VideoCaptureConfig.java
new file mode 100644
index 0000000..f5a628a
--- /dev/null
+++ b/camera/camera-video/src/main/java/androidx/camera/video/impl/VideoCaptureConfig.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.video.impl;
+
+import androidx.annotation.NonNull;
+import androidx.camera.core.impl.Config;
+import androidx.camera.core.impl.ImageFormatConstants;
+import androidx.camera.core.impl.ImageOutputConfig;
+import androidx.camera.core.impl.OptionsBundle;
+import androidx.camera.core.impl.UseCaseConfig;
+import androidx.camera.core.internal.ThreadConfig;
+import androidx.camera.video.VideoCapture;
+import androidx.camera.video.VideoOutput;
+
+/**
+ * Config for a video capture use case.
+ *
+ * <p>In the earlier stage, the VideoCapture is deprioritized.
+ *
+ * @param <T> the type of VideoOutput
+ */
+public final class VideoCaptureConfig<T extends VideoOutput>
+        implements UseCaseConfig<VideoCapture<T>>,
+        ImageOutputConfig,
+        ThreadConfig {
+
+    // Option Declarations:
+    // *********************************************************************************************
+
+    public static final Option<VideoOutput> OPTION_VIDEO_OUTPUT =
+            Option.create("camerax.video.VideoCapture.videoOutput", VideoOutput.class);
+
+    // *********************************************************************************************
+
+    private final OptionsBundle mConfig;
+
+    public VideoCaptureConfig(@NonNull OptionsBundle config) {
+        mConfig = config;
+    }
+
+    @SuppressWarnings("unchecked")
+    @NonNull
+    public T getVideoOutput() {
+        return (T) retrieveOption(OPTION_VIDEO_OUTPUT);
+    }
+
+    /**
+     * Retrieves the format of the image that is fed as input.
+     *
+     * <p>This should always be PRIVATE for VideoCapture.
+     */
+    @Override
+    public int getInputFormat() {
+        return ImageFormatConstants.INTERNAL_DEFINED_IMAGE_FORMAT_PRIVATE;
+    }
+
+    @NonNull
+    @Override
+    public Config getConfig() {
+        return mConfig;
+    }
+}
diff --git a/camera/camera-video/src/test/java/androidx/camera/video/VideoCaptureTest.kt b/camera/camera-video/src/test/java/androidx/camera/video/VideoCaptureTest.kt
new file mode 100644
index 0000000..a22f778
--- /dev/null
+++ b/camera/camera-video/src/test/java/androidx/camera/video/VideoCaptureTest.kt
@@ -0,0 +1,334 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.video
+
+import android.content.Context
+import android.os.Build
+import android.util.Size
+import android.view.Surface
+import androidx.camera.core.CameraSelector
+import androidx.camera.core.CameraX
+import androidx.camera.core.CameraXConfig
+import androidx.camera.core.SurfaceRequest
+import androidx.camera.core.impl.CameraFactory
+import androidx.camera.core.impl.ImageOutputConfig
+import androidx.camera.core.impl.MutableStateObservable
+import androidx.camera.core.impl.Observable
+import androidx.camera.core.impl.utils.executor.CameraXExecutors
+import androidx.camera.core.internal.CameraUseCaseAdapter
+import androidx.camera.testing.CamcorderProfileUtil
+import androidx.camera.testing.CamcorderProfileUtil.PROFILE_2160P
+import androidx.camera.testing.CamcorderProfileUtil.PROFILE_720P
+import androidx.camera.testing.CamcorderProfileUtil.RESOLUTION_2160P
+import androidx.camera.testing.CameraUtil
+import androidx.camera.testing.fakes.FakeAppConfig
+import androidx.camera.testing.fakes.FakeCamcorderProfileProvider
+import androidx.camera.testing.fakes.FakeCamera
+import androidx.camera.testing.fakes.FakeCameraFactory
+import androidx.camera.testing.fakes.FakeCameraInfoInternal
+import androidx.camera.video.QualitySelector.QUALITY_FHD
+import androidx.camera.video.QualitySelector.QUALITY_UHD
+import androidx.camera.video.VideoOutput.StreamState
+import androidx.core.util.Consumer
+import androidx.test.core.app.ApplicationProvider
+import com.google.common.truth.Truth.assertThat
+import org.junit.After
+import org.junit.Assert.assertThrows
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.any
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.verify
+import org.robolectric.RobolectricTestRunner
+import org.robolectric.annotation.Config
+import org.robolectric.annotation.internal.DoNotInstrument
+
+private val ANY_SIZE = Size(640, 480)
+private const val CAMERA_ID_0 = "0"
+private const val CAMERA_ID_1 = "1"
+private val CAMERA_0_PROFILE_HIGH = CamcorderProfileUtil.asHighQuality(PROFILE_2160P)
+private val CAMERA_0_PROFILE_LOW = CamcorderProfileUtil.asLowQuality(PROFILE_720P)
+
+@RunWith(RobolectricTestRunner::class)
+@DoNotInstrument
+@Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
+class VideoCaptureTest {
+
+    private val context: Context = ApplicationProvider.getApplicationContext()
+    private lateinit var cameraUseCaseAdapter0: CameraUseCaseAdapter
+    private lateinit var cameraUseCaseAdapter1: CameraUseCaseAdapter
+
+    @Before
+    fun setUp() {
+        // Prepare camera0
+        // Camera 0 support 2160P(UHD) and 720P(HD)
+        val cameraInfo0 = FakeCameraInfoInternal(CAMERA_ID_0).apply {
+            camcorderProfileProvider = FakeCamcorderProfileProvider.Builder()
+                .addProfile(CAMERA_0_PROFILE_HIGH)
+                .addProfile(PROFILE_2160P)
+                .addProfile(PROFILE_720P)
+                .addProfile(CAMERA_0_PROFILE_LOW)
+                .build()
+        }
+        val camera0 = FakeCamera(CAMERA_ID_0, null, cameraInfo0)
+
+        // Prepare camera 1
+        // camera1 has no supported quality
+        val cameraInfo1 = FakeCameraInfoInternal(CAMERA_ID_1, 0, CameraSelector.LENS_FACING_FRONT)
+        val camera1 = FakeCamera(CAMERA_ID_1, null, cameraInfo1)
+
+        val cameraFactoryProvider =
+            CameraFactory.Provider { _, _, _ ->
+                FakeCameraFactory().apply {
+                    insertDefaultBackCamera(CAMERA_ID_0) { camera0 }
+                    insertDefaultFrontCamera(CAMERA_ID_1) { camera1 }
+                }
+            }
+
+        val cameraXConfig = CameraXConfig.Builder.fromConfig(FakeAppConfig.create())
+            .setCameraFactoryProvider(cameraFactoryProvider)
+            .build()
+        CameraX.initialize(context, cameraXConfig).get()
+
+        cameraUseCaseAdapter0 =
+            CameraUtil.createCameraUseCaseAdapter(context, CameraSelector.DEFAULT_BACK_CAMERA)
+        cameraUseCaseAdapter1 =
+            CameraUtil.createCameraUseCaseAdapter(context, CameraSelector.DEFAULT_FRONT_CAMERA)
+    }
+
+    @After
+    fun tearDown() {
+        if (this::cameraUseCaseAdapter0.isInitialized) {
+            cameraUseCaseAdapter0.apply {
+                removeUseCases(useCases)
+            }
+        }
+        if (this::cameraUseCaseAdapter1.isInitialized) {
+            cameraUseCaseAdapter1.apply {
+                removeUseCases(useCases)
+            }
+        }
+        CameraX.shutdown().get()
+    }
+
+    @Test
+    fun setTargetResolution_throwsException() {
+        val videoOutput = createVideoOutput()
+
+        assertThrows(UnsupportedOperationException::class.java) {
+            VideoCapture.Builder(videoOutput)
+                .setTargetResolution(ANY_SIZE)
+                .build()
+        }
+    }
+
+    @Test
+    fun canGetVideoOutput() {
+        // Arrange.
+        val videoOutput = createVideoOutput()
+
+        // Act.
+        val videoCapture = VideoCapture.withOutput(videoOutput)
+
+        // Assert.
+        assertThat(videoCapture.output).isEqualTo(videoOutput)
+    }
+
+    @Test
+    fun addUseCases_receiveOnSurfaceRequest() {
+        // Arrange.
+        var surfaceRequest: SurfaceRequest? = null
+        val videoOutput = createVideoOutput(surfaceRequestListener = { surfaceRequest = it })
+        val videoCapture = VideoCapture.Builder(videoOutput)
+            .setSessionOptionUnpacker { _, _ -> }
+            .build()
+
+        // Act.
+        cameraUseCaseAdapter0.addUseCases(listOf(videoCapture))
+
+        // Assert.
+        assertThat(surfaceRequest).isNotNull()
+    }
+
+    @Test
+    fun addUseCases_withNullMediaSpec_throwException() {
+        // Arrange.
+        val videoOutput = createVideoOutput(mediaSpec = null)
+        val videoCapture = VideoCapture.Builder(videoOutput)
+            .setSessionOptionUnpacker { _, _ -> }
+            .build()
+
+        // Assert.
+        assertThrows(CameraUseCaseAdapter.CameraException::class.java) {
+            // Act.
+            cameraUseCaseAdapter0.addUseCases(listOf(videoCapture))
+        }
+    }
+
+    @Test
+    fun setQualitySelector_sameResolutionAsQualitySelector() {
+        // Arrange.
+        // Camera 0 support 2160P(UHD) and 720P(HD)
+        val videoOutput = createVideoOutput(
+            mediaSpec = MediaSpec.builder().configureVideo {
+                it.setQualitySelector(QualitySelector.of(QUALITY_UHD))
+            }.build()
+        )
+        val videoCapture = VideoCapture.Builder(videoOutput)
+            .setSessionOptionUnpacker { _, _ -> }
+            .build()
+
+        // Act.
+        cameraUseCaseAdapter0.addUseCases(listOf(videoCapture))
+
+        // Assert.
+        val targetResolution = videoCapture.currentConfig.retrieveOption(
+            ImageOutputConfig.OPTION_TARGET_RESOLUTION
+        )
+        assertThat(targetResolution).isEqualTo(RESOLUTION_2160P)
+    }
+
+    @Test
+    fun setQualitySelector_notSupportedQuality_throwException() {
+        // Arrange.
+        // Camera 0 support 2160P(UHD) and 720P(HD)
+        val videoOutput = createVideoOutput(
+            mediaSpec = MediaSpec.builder().configureVideo {
+                it.setQualitySelector(QualitySelector.of(QUALITY_FHD))
+            }.build()
+        )
+        val videoCapture = VideoCapture.Builder(videoOutput)
+            .setSessionOptionUnpacker { _, _ -> }
+            .build()
+
+        // Assert.
+        assertThrows(CameraUseCaseAdapter.CameraException::class.java) {
+            // Act.
+            cameraUseCaseAdapter0.addUseCases(listOf(videoCapture))
+        }
+    }
+
+    @Test
+    fun noSupportedQuality_useDefaultResolution() {
+        // Arrange.
+        val videoOutput = createVideoOutput(
+            mediaSpec = MediaSpec.builder().configureVideo {
+                it.setQualitySelector(QualitySelector.of(QUALITY_UHD))
+            }.build()
+        )
+        val videoCapture = VideoCapture.Builder(videoOutput)
+            .setSessionOptionUnpacker { _, _ -> }
+            .build()
+
+        // Act.
+        // camera1 has no supported quality
+        cameraUseCaseAdapter1.addUseCases(listOf(videoCapture))
+
+        // Assert.
+        val targetResolution = videoCapture.currentConfig.retrieveOption(
+            ImageOutputConfig.OPTION_TARGET_RESOLUTION
+        )
+        assertThat(targetResolution).isEqualTo(VideoCapture.Defaults.DEFAULT_RESOLUTION)
+    }
+
+    @Test
+    fun removeUseCases_receiveResultOfSurfaceRequest() {
+        // Arrange.
+        var surfaceResult: SurfaceRequest.Result? = null
+        val videoOutput = createVideoOutput { surfaceRequest ->
+            surfaceRequest.provideSurface(
+                mock(Surface::class.java),
+                CameraXExecutors.directExecutor(),
+                { surfaceResult = it }
+            )
+        }
+        val videoCapture = VideoCapture.Builder(videoOutput)
+            .setSessionOptionUnpacker { _, _ -> }
+            .build()
+
+        // Act.
+        cameraUseCaseAdapter0.addUseCases(listOf(videoCapture))
+
+        // Assert.
+        // Surface is in use, should not receive any result.
+        assertThat(surfaceResult).isNull()
+
+        // Act.
+        cameraUseCaseAdapter0.removeUseCases(listOf(videoCapture))
+
+        // Assert.
+        assertThat(surfaceResult!!.resultCode).isEqualTo(
+            SurfaceRequest.Result.RESULT_SURFACE_USED_SUCCESSFULLY
+        )
+    }
+
+    @Test
+    fun addUseCases_transformationInfoUpdated() {
+        // Arrange.
+        val listener = mock(SurfaceRequest.TransformationInfoListener::class.java)
+        val videoOutput = createVideoOutput(
+            surfaceRequestListener = {
+                it.setTransformationInfoListener(
+                    CameraXExecutors.directExecutor(),
+                    listener
+                )
+            }
+        )
+
+        val videoCapture = VideoCapture.Builder(videoOutput)
+            .setSessionOptionUnpacker { _, _ -> }
+            .build()
+
+        // Act.
+        cameraUseCaseAdapter0.addUseCases(listOf(videoCapture))
+
+        // Assert.
+        verify(listener).onTransformationInfoUpdate(any())
+    }
+
+    private fun createVideoOutput(
+        streamState: StreamState = StreamState.ACTIVE,
+        mediaSpec: MediaSpec? = MediaSpec.builder().build(),
+        surfaceRequestListener: Consumer<SurfaceRequest> = Consumer { it.willNotProvideSurface() }
+    ): TestVideoOutput = TestVideoOutput(streamState, mediaSpec, surfaceRequestListener)
+
+    private class TestVideoOutput constructor(
+        streamState: StreamState,
+        mediaSpec: MediaSpec?,
+        val surfaceRequestCallback: Consumer<SurfaceRequest>
+    ) : VideoOutput {
+
+        private val streamStateObservable: MutableStateObservable<StreamState> =
+            MutableStateObservable.withInitialState(streamState)
+
+        private val mediaSpecObservable: MutableStateObservable<MediaSpec> =
+            MutableStateObservable.withInitialState(mediaSpec)
+
+        override fun onSurfaceRequested(surfaceRequest: SurfaceRequest) {
+            surfaceRequestCallback.accept(surfaceRequest)
+        }
+
+        override fun getStreamState(): Observable<StreamState> = streamStateObservable
+
+        override fun getMediaSpec(): Observable<MediaSpec> = mediaSpecObservable
+
+        fun setStreamState(streamState: StreamState) = streamStateObservable.setState(streamState)
+
+        fun setMediaSpec(mediaSpec: MediaSpec) = mediaSpecObservable.setState(mediaSpec)
+    }
+}
diff --git a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/CameraDisconnectTest.kt b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/CameraDisconnectTest.kt
new file mode 100644
index 0000000..45de9b6
--- /dev/null
+++ b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/CameraDisconnectTest.kt
@@ -0,0 +1,158 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.integration.core
+
+import android.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.CameraX
+import androidx.camera.core.CameraXConfig
+import androidx.camera.testing.CameraUtil
+import androidx.camera.testing.CoreAppTestUtil
+import androidx.camera.testing.activity.Camera2TestActivity
+import androidx.camera.testing.activity.CameraXTestActivity
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.espresso.Espresso
+import androidx.test.espresso.IdlingPolicies
+import androidx.test.espresso.IdlingRegistry
+import androidx.test.espresso.IdlingResource
+import androidx.test.filters.LargeTest
+import androidx.test.filters.SdkSuppress
+import androidx.test.platform.app.InstrumentationRegistry
+import com.google.common.truth.Truth
+import kotlinx.coroutines.runBlocking
+import org.junit.After
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+import java.util.concurrent.TimeUnit
+
+/** Tests for [CameraX] which varies use case combinations to run. */
+@LargeTest
+@RunWith(Parameterized::class)
+class CameraDisconnectTest(
+    private val implName: String,
+    private val cameraConfig: CameraXConfig
+) {
+
+    @get:Rule
+    val cameraRule = CameraUtil.grantCameraPermissionAndPreTest()
+
+    companion object {
+        @JvmStatic
+        @Parameterized.Parameters(name = "{0}")
+        fun data() = listOf(
+            arrayOf(Camera2Config::class.simpleName, Camera2Config.defaultConfig()),
+            arrayOf(CameraPipeConfig::class.simpleName, CameraPipeConfig.defaultConfig())
+        )
+    }
+
+    @Suppress("DEPRECATION")
+    @get:Rule
+    val cameraXTestActivityRule = androidx.test.rule.ActivityTestRule(
+        CameraXTestActivity::class.java, true, false
+    )
+
+    @Suppress("DEPRECATION")
+    @get:Rule
+    val camera2ActivityRule = androidx.test.rule.ActivityTestRule(
+        Camera2TestActivity::class.java, true, false
+    )
+
+    private val context: Context = ApplicationProvider.getApplicationContext()
+
+    @Before
+    fun setUp() {
+        IdlingPolicies.setIdlingResourceTimeout(10, TimeUnit.SECONDS)
+        CoreAppTestUtil.assumeCompatibleDevice()
+        CoreAppTestUtil.assumeCanTestCameraDisconnect()
+        runBlocking {
+            CameraX.initialize(context, cameraConfig).get(10, TimeUnit.SECONDS)
+        }
+
+        // Clear the device UI and check if there is no dialog or lock screen on the top of the
+        // window before start the test.
+        CoreAppTestUtil.prepareDeviceUI(InstrumentationRegistry.getInstrumentation())
+    }
+
+    @After
+    fun tearDown() {
+        if (cameraXTestActivityRule.activity != null) {
+            cameraXTestActivityRule.finishActivity()
+        }
+
+        if (camera2ActivityRule.activity != null) {
+            camera2ActivityRule.finishActivity()
+        }
+
+        runBlocking {
+            CameraX.shutdown().get(10, TimeUnit.SECONDS)
+        }
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.M) // Known issue, checkout b/147393563.
+    fun testCameraDisconnect() {
+
+        // TODO(b/184603071): Migrate the ActivityTestRule to ActivityScenario
+        // Launch CameraX test activity
+        with(cameraXTestActivityRule.launchActivity(Intent())) {
+
+            // Wait for preview to become active
+            waitForCameraXPreview()
+
+            // Get id of camera opened by CameraX test activity
+            Truth.assertThat(cameraId).isNotNull()
+
+            // Launch Camera2 test activity. It should cause the camera to disconnect from CameraX.
+            val intent = Intent(
+                context,
+                Camera2TestActivity::class.java
+            ).apply {
+                putExtra(Camera2TestActivity.EXTRA_CAMERA_ID, cameraId)
+            }
+            camera2ActivityRule.launchActivity(intent)
+
+            // Wait for preview to become active
+            camera2ActivityRule.activity.waitForCamera2Preview()
+
+            // Close Camera2 test activity, and verify the CameraX Preview resumes successfully.
+            camera2ActivityRule.finishActivity()
+
+            // Verify the CameraX Preview can resume successfully.
+            waitForCameraXPreview()
+        }
+    }
+
+    private fun CameraXTestActivity.waitForCameraXPreview() {
+        waitFor(previewReady)
+    }
+
+    private fun Camera2TestActivity.waitForCamera2Preview() {
+        waitFor(mPreviewReady)
+    }
+
+    private fun waitFor(idlingResource: IdlingResource) {
+        IdlingRegistry.getInstance().register(idlingResource)
+        Espresso.onIdle()
+        IdlingRegistry.getInstance().unregister(idlingResource)
+    }
+}
\ No newline at end of file
diff --git a/compose/animation/animation-core/build.gradle b/compose/animation/animation-core/build.gradle
index ef336ff..0e876b3 100644
--- a/compose/animation/animation-core/build.gradle
+++ b/compose/animation/animation-core/build.gradle
@@ -59,6 +59,7 @@
         androidTestImplementation(JUNIT)
         androidTestImplementation(project(":compose:animation:animation"))
         androidTestImplementation(project(":compose:ui:ui-test-junit4"))
+        androidTestImplementation(project(":compose:test-utils"))
 
         lintPublish project(":compose:animation:animation-core-lint")
     }
@@ -104,6 +105,7 @@
                 implementation(JUNIT)
                 implementation(project(":compose:animation:animation"))
                 implementation(project(":compose:ui:ui-test-junit4"))
+                implementation(project(":compose:test-utils"))
             }
         }
     }
diff --git a/compose/animation/animation/build.gradle b/compose/animation/animation/build.gradle
index 0ab8607..b4b428d 100644
--- a/compose/animation/animation/build.gradle
+++ b/compose/animation/animation/build.gradle
@@ -54,6 +54,7 @@
 
         androidTestImplementation(project(":compose:foundation:foundation"))
         androidTestImplementation(project(":compose:ui:ui-test-junit4"))
+        androidTestImplementation(project(":compose:test-utils"))
         androidTestImplementation(ANDROIDX_TEST_RULES)
         androidTestImplementation(ANDROIDX_TEST_RUNNER)
         androidTestImplementation(JUNIT)
@@ -104,6 +105,7 @@
                 implementation(JUNIT)
                 implementation(project(":compose:foundation:foundation"))
                 implementation(project(":compose:ui:ui-test-junit4"))
+                implementation(project(":compose:test-utils"))
             }
         }
     }
diff --git a/compose/benchmark-utils/benchmark/src/androidTest/java/androidx/compose/benchmarkutils/benchmark/EmptyBenchmark.kt b/compose/benchmark-utils/benchmark/src/androidTest/java/androidx/compose/benchmarkutils/benchmark/EmptyBenchmark.kt
new file mode 100644
index 0000000..a3ffd44
--- /dev/null
+++ b/compose/benchmark-utils/benchmark/src/androidTest/java/androidx/compose/benchmarkutils/benchmark/EmptyBenchmark.kt
@@ -0,0 +1,72 @@
+/*
+ * Copyright 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.ui.testutils.benchmark
+
+import androidx.compose.runtime.Composable
+import androidx.compose.testutils.ComposeTestCase
+import androidx.compose.testutils.benchmark.ComposeBenchmarkRule
+import androidx.compose.testutils.benchmark.benchmarkDrawPerf
+import androidx.compose.testutils.benchmark.benchmarkFirstCompose
+import androidx.compose.testutils.benchmark.benchmarkFirstDraw
+import androidx.compose.testutils.benchmark.benchmarkFirstLayout
+import androidx.compose.testutils.benchmark.benchmarkFirstMeasure
+import androidx.test.filters.LargeTest
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import androidx.test.ext.junit.runners.AndroidJUnit4
+
+@LargeTest
+@RunWith(AndroidJUnit4::class)
+class EmptyBenchmark {
+
+    @get:Rule
+    val benchmarkRule = ComposeBenchmarkRule()
+
+    private val textCaseFactory = { EmptyTestCase() }
+
+    @Test
+    fun first_compose() {
+        benchmarkRule.benchmarkFirstCompose(textCaseFactory)
+    }
+
+    @Test
+    fun first_measure() {
+        benchmarkRule.benchmarkFirstMeasure(textCaseFactory)
+    }
+
+    @Test
+    fun first_layout() {
+        benchmarkRule.benchmarkFirstLayout(textCaseFactory)
+    }
+
+    @Test
+    fun first_draw() {
+        benchmarkRule.benchmarkFirstDraw(textCaseFactory)
+    }
+
+    @Test
+    fun draw() {
+        benchmarkRule.benchmarkDrawPerf(textCaseFactory)
+    }
+}
+
+class EmptyTestCase : ComposeTestCase {
+    @Composable
+    override fun Content() {
+    }
+}
\ No newline at end of file
diff --git a/compose/benchmark-utils/benchmark/src/androidTest/java/androidx/compose/benchmarkutils/benchmark/EmptyFirstFastBenchmark.kt b/compose/benchmark-utils/benchmark/src/androidTest/java/androidx/compose/benchmarkutils/benchmark/EmptyFirstFastBenchmark.kt
new file mode 100644
index 0000000..e5b3694
--- /dev/null
+++ b/compose/benchmark-utils/benchmark/src/androidTest/java/androidx/compose/benchmarkutils/benchmark/EmptyFirstFastBenchmark.kt
@@ -0,0 +1,73 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.ui.testutils.benchmark
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.runtime.Composable
+import androidx.compose.testutils.LayeredComposeTestCase
+import androidx.compose.testutils.benchmark.ComposeBenchmarkRule
+import androidx.compose.testutils.benchmark.benchmarkFirstComposeFast
+import androidx.compose.testutils.benchmark.benchmarkFirstDrawFast
+import androidx.compose.testutils.benchmark.benchmarkFirstLayoutFast
+import androidx.compose.testutils.benchmark.benchmarkFirstMeasureFast
+import androidx.test.filters.LargeTest
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import androidx.test.ext.junit.runners.AndroidJUnit4
+
+@LargeTest
+@RunWith(AndroidJUnit4::class)
+class EmptyFirstFastBenchmark {
+
+    @get:Rule
+    val benchmarkRule = ComposeBenchmarkRule()
+
+    private val testCaseFactory = { EmptyLayeredTestCase() }
+
+    @Test
+    fun first_compose() {
+        benchmarkRule.benchmarkFirstComposeFast(testCaseFactory)
+    }
+
+    @Test
+    fun first_measure() {
+        benchmarkRule.benchmarkFirstMeasureFast(testCaseFactory)
+    }
+
+    @Test
+    fun first_layout() {
+        benchmarkRule.benchmarkFirstLayoutFast(testCaseFactory)
+    }
+
+    @Test
+    fun first_draw() {
+        benchmarkRule.benchmarkFirstDrawFast(testCaseFactory)
+    }
+}
+
+class EmptyLayeredTestCase : LayeredComposeTestCase {
+    @Composable
+    override fun MeasuredContent() {}
+
+    @Composable
+    override fun ContentWrappers(content: @Composable () -> Unit) {
+        Box {
+            content()
+        }
+    }
+}
\ No newline at end of file
diff --git a/compose/benchmark-utils/src/main/java/androidx/compose/testutils/benchmark/BenchmarkFirstExtensions.kt b/compose/benchmark-utils/src/main/java/androidx/compose/testutils/benchmark/BenchmarkFirstExtensions.kt
index 74f1e03..1765fa0 100644
--- a/compose/benchmark-utils/src/main/java/androidx/compose/testutils/benchmark/BenchmarkFirstExtensions.kt
+++ b/compose/benchmark-utils/src/main/java/androidx/compose/testutils/benchmark/BenchmarkFirstExtensions.kt
@@ -24,7 +24,6 @@
 import androidx.compose.testutils.ComposeTestCase
 import androidx.compose.testutils.LayeredComposeTestCase
 import androidx.compose.testutils.assertNoPendingChanges
-import androidx.compose.testutils.benchmark.android.AndroidTestCase
 import androidx.compose.testutils.doFramesUntilNoChangesPending
 import org.junit.Assert.assertTrue
 
@@ -32,7 +31,7 @@
  * Measures the time of the first composition right after the given test case is added to an
  * already existing hierarchy.
  */
-fun ComposeBenchmarkRule.benchmarkFirstCompose(caseFactory: () -> LayeredComposeTestCase) {
+fun ComposeBenchmarkRule.benchmarkFirstComposeFast(caseFactory: () -> LayeredComposeTestCase) {
     runBenchmarkFor(LayeredCaseAdapter.of(caseFactory)) {
         measureRepeated {
             runWithTimingDisabled {
@@ -54,7 +53,7 @@
  * Measures the time of the first measure right after the given test case is added to an already
  * existing hierarchy.
  */
-fun ComposeBenchmarkRule.benchmarkFirstMeasure(caseFactory: () -> LayeredComposeTestCase) {
+fun ComposeBenchmarkRule.benchmarkFirstMeasureFast(caseFactory: () -> LayeredComposeTestCase) {
     runBenchmarkFor(LayeredCaseAdapter.of(caseFactory)) {
         measureRepeated {
             runWithTimingDisabled {
@@ -79,7 +78,7 @@
  * Measures the time of the first layout right after the given test case is added to an already
  * existing hierarchy.
  */
-fun ComposeBenchmarkRule.benchmarkFirstLayout(caseFactory: () -> LayeredComposeTestCase) {
+fun ComposeBenchmarkRule.benchmarkFirstLayoutFast(caseFactory: () -> LayeredComposeTestCase) {
     runBenchmarkFor(LayeredCaseAdapter.of(caseFactory)) {
         measureRepeated {
             runWithTimingDisabled {
@@ -105,7 +104,7 @@
  * Measures the time of the first draw right after the given test case is added to an already
  * existing hierarchy.
  */
-fun ComposeBenchmarkRule.benchmarkFirstDraw(caseFactory: () -> LayeredComposeTestCase) {
+fun ComposeBenchmarkRule.benchmarkFirstDrawFast(caseFactory: () -> LayeredComposeTestCase) {
     runBenchmarkFor(LayeredCaseAdapter.of(caseFactory)) {
         measureRepeated {
             runWithTimingDisabled {
@@ -131,85 +130,6 @@
 }
 
 /**
- * Measures the time of the first set content of the given Android test case.
- */
-fun AndroidBenchmarkRule.benchmarkFirstSetContent(caseFactory: () -> AndroidTestCase) {
-    runBenchmarkFor(caseFactory) {
-        measureRepeated {
-            setupContent()
-            runWithTimingDisabled {
-                disposeContent()
-            }
-        }
-    }
-}
-
-/**
- * Measures the time of the first measure of the given test case.
- */
-fun AndroidBenchmarkRule.benchmarkFirstMeasure(caseFactory: () -> AndroidTestCase) {
-    runBenchmarkFor(caseFactory) {
-        measureRepeated {
-            runWithTimingDisabled {
-                setupContent()
-                requestLayout()
-            }
-
-            measure()
-
-            runWithTimingDisabled {
-                disposeContent()
-            }
-        }
-    }
-}
-
-/**
- * Measures the time of the first layout of the given test case.
- */
-fun AndroidBenchmarkRule.benchmarkFirstLayout(caseFactory: () -> AndroidTestCase) {
-    runBenchmarkFor(caseFactory) {
-        measureRepeated {
-            runWithTimingDisabled {
-                setupContent()
-                requestLayout()
-                measure()
-            }
-
-            layout()
-
-            runWithTimingDisabled {
-                disposeContent()
-            }
-        }
-    }
-}
-
-/**
- * Measures the time of the first draw of the given test case.
- */
-fun AndroidBenchmarkRule.benchmarkFirstDraw(caseFactory: () -> AndroidTestCase) {
-    runBenchmarkFor(caseFactory) {
-        measureRepeated {
-            runWithTimingDisabled {
-                setupContent()
-                requestLayout()
-                measure()
-                layout()
-                drawPrepare()
-            }
-
-            draw()
-
-            runWithTimingDisabled {
-                drawFinish()
-                disposeContent()
-            }
-        }
-    }
-}
-
-/**
  * Runs recompositions until there are no changes pending.
  *
  * @param maxAmountOfStep Max amount of recomposition to perform before giving up and throwing
diff --git a/compose/benchmark-utils/src/main/java/androidx/compose/testutils/benchmark/BenchmarksExtensions.kt b/compose/benchmark-utils/src/main/java/androidx/compose/testutils/benchmark/BenchmarksExtensions.kt
index ea64883..c53a78e 100644
--- a/compose/benchmark-utils/src/main/java/androidx/compose/testutils/benchmark/BenchmarksExtensions.kt
+++ b/compose/benchmark-utils/src/main/java/androidx/compose/testutils/benchmark/BenchmarksExtensions.kt
@@ -26,6 +26,7 @@
 import androidx.compose.testutils.benchmark.android.AndroidTestCase
 import androidx.compose.testutils.doFramesUntilNoChangesPending
 import androidx.compose.testutils.recomposeAssertHadChanges
+import androidx.compose.testutils.setupContent
 
 /**
  * Measures measure and layout performance of the given test case by toggling measure constraints.
@@ -145,6 +146,180 @@
 }
 
 /**
+ * Measures the time of the first composition of the given compose test case.
+ *
+ * @param assertNoPendingRecompositions whether the benchmark will fail if there are pending
+ * recompositions after the first composition. By default this is true to enforce correctness in
+ * the benchmark, but for components that have an initial animation after being composed this can
+ * be turned off to benchmark just the first composition without any pending animations.
+ */
+fun ComposeBenchmarkRule.benchmarkFirstCompose(
+    caseFactory: () -> ComposeTestCase,
+    assertNoPendingRecompositions: Boolean = true
+) {
+    runBenchmarkFor(caseFactory) {
+        measureRepeated {
+            runWithTimingDisabled {
+                createTestCase()
+            }
+
+            emitContent()
+
+            runWithTimingDisabled {
+                if (assertNoPendingRecompositions) {
+                    assertNoPendingChanges()
+                }
+                disposeContent()
+            }
+        }
+    }
+}
+
+/**
+ * Measures the time of the first set content of the given Android test case.
+ */
+fun AndroidBenchmarkRule.benchmarkFirstSetContent(caseFactory: () -> AndroidTestCase) {
+    runBenchmarkFor(caseFactory) {
+        measureRepeated {
+            setupContent()
+            runWithTimingDisabled {
+                disposeContent()
+            }
+        }
+    }
+}
+
+/**
+ * Measures the time of the first measure of the given test case.
+ */
+fun ComposeBenchmarkRule.benchmarkFirstMeasure(caseFactory: () -> ComposeTestCase) {
+    runBenchmarkFor(caseFactory) {
+        measureRepeated {
+            runWithTimingDisabled {
+                setupContent()
+                requestLayout()
+            }
+
+            measure()
+
+            runWithTimingDisabled {
+                disposeContent()
+            }
+        }
+    }
+}
+
+/**
+ * Measures the time of the first measure of the given test case.
+ */
+fun AndroidBenchmarkRule.benchmarkFirstMeasure(caseFactory: () -> AndroidTestCase) {
+    runBenchmarkFor(caseFactory) {
+        measureRepeated {
+            runWithTimingDisabled {
+                setupContent()
+                requestLayout()
+            }
+
+            measure()
+
+            runWithTimingDisabled {
+                disposeContent()
+            }
+        }
+    }
+}
+
+/**
+ * Measures the time of the first layout of the given test case.
+ */
+fun ComposeBenchmarkRule.benchmarkFirstLayout(caseFactory: () -> ComposeTestCase) {
+    runBenchmarkFor(caseFactory) {
+        measureRepeated {
+            runWithTimingDisabled {
+                setupContent()
+                requestLayout()
+                measure()
+            }
+
+            layout()
+
+            runWithTimingDisabled {
+                disposeContent()
+            }
+        }
+    }
+}
+
+/**
+ * Measures the time of the first layout of the given test case.
+ */
+fun AndroidBenchmarkRule.benchmarkFirstLayout(caseFactory: () -> AndroidTestCase) {
+    runBenchmarkFor(caseFactory) {
+        measureRepeated {
+            runWithTimingDisabled {
+                setupContent()
+                requestLayout()
+                measure()
+            }
+
+            layout()
+
+            runWithTimingDisabled {
+                disposeContent()
+            }
+        }
+    }
+}
+
+/**
+ * Measures the time of the first draw of the given test case.
+ */
+fun ComposeBenchmarkRule.benchmarkFirstDraw(caseFactory: () -> ComposeTestCase) {
+    runBenchmarkFor(caseFactory) {
+        measureRepeated {
+            runWithTimingDisabled {
+                setupContent()
+                requestLayout()
+                measure()
+                layout()
+                drawPrepare()
+            }
+
+            draw()
+
+            runWithTimingDisabled {
+                drawFinish()
+                disposeContent()
+            }
+        }
+    }
+}
+
+/**
+ * Measures the time of the first draw of the given test case.
+ */
+fun AndroidBenchmarkRule.benchmarkFirstDraw(caseFactory: () -> AndroidTestCase) {
+    runBenchmarkFor(caseFactory) {
+        measureRepeated {
+            runWithTimingDisabled {
+                setupContent()
+                requestLayout()
+                measure()
+                layout()
+                drawPrepare()
+            }
+
+            draw()
+
+            runWithTimingDisabled {
+                drawFinish()
+                disposeContent()
+            }
+        }
+    }
+}
+
+/**
  *  Measures recomposition time of the hierarchy after changing a state.
  *
  * @param assertOneRecomposition whether the benchmark will fail if there are pending
diff --git a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/AbstractComposeLowering.kt b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/AbstractComposeLowering.kt
index ce00673..b64b7f3 100644
--- a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/AbstractComposeLowering.kt
+++ b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/AbstractComposeLowering.kt
@@ -1214,7 +1214,6 @@
     }
 
     protected fun dexSafeName(name: Name): Name {
-        val unsafeSymbolsRegex = "[ <>]".toRegex()
         return if (
             name.isSpecial || name.asString().contains(unsafeSymbolsRegex)
         ) {
@@ -1226,6 +1225,8 @@
     }
 }
 
+private val unsafeSymbolsRegex = "[ <>]".toRegex()
+
 fun IrFunction.composerParam(): IrValueParameter? {
     for (param in valueParameters.asReversed()) {
         if (param.isComposerParam()) return param
diff --git a/compose/foundation/foundation-layout/benchmark/src/androidTest/java/androidx/compose/foundation/layout/benchmark/NestedBoxesTestCase.kt b/compose/foundation/foundation-layout/benchmark/src/androidTest/java/androidx/compose/foundation/layout/benchmark/NestedBoxesTestCase.kt
index 237f9fb..4e5a26b 100644
--- a/compose/foundation/foundation-layout/benchmark/src/androidTest/java/androidx/compose/foundation/layout/benchmark/NestedBoxesTestCase.kt
+++ b/compose/foundation/foundation-layout/benchmark/src/androidTest/java/androidx/compose/foundation/layout/benchmark/NestedBoxesTestCase.kt
@@ -18,7 +18,7 @@
 
 import androidx.compose.foundation.layout.Box
 import androidx.compose.runtime.Composable
-import androidx.compose.testutils.LayeredComposeTestCase
+import androidx.compose.testutils.ComposeTestCase
 
 /**
  * Test case representing a layout hierarchy of nested boxes.
@@ -26,10 +26,10 @@
 class NestedBoxesTestCase(
     private val depth: Int,
     private val children: Int
-) : LayeredComposeTestCase {
+) : ComposeTestCase {
 
     @Composable
-    override fun MeasuredContent() {
+    override fun Content() {
         Box {
             Boxes(depth - 1, children)
         }
diff --git a/compose/foundation/foundation-layout/benchmark/src/androidTest/java/androidx/compose/foundation/layout/benchmark/RectsInColumnSharedModelTestCase.kt b/compose/foundation/foundation-layout/benchmark/src/androidTest/java/androidx/compose/foundation/layout/benchmark/RectsInColumnSharedModelTestCase.kt
index 48fb82f..34791dd 100644
--- a/compose/foundation/foundation-layout/benchmark/src/androidTest/java/androidx/compose/foundation/layout/benchmark/RectsInColumnSharedModelTestCase.kt
+++ b/compose/foundation/foundation-layout/benchmark/src/androidTest/java/androidx/compose/foundation/layout/benchmark/RectsInColumnSharedModelTestCase.kt
@@ -20,9 +20,10 @@
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.size
+import androidx.compose.material.MaterialTheme
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.mutableStateOf
-import androidx.compose.testutils.LayeredComposeTestCase
+import androidx.compose.testutils.ComposeTestCase
 import androidx.compose.testutils.ToggleableTestCase
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.Color
@@ -37,18 +38,20 @@
  */
 class RectsInColumnSharedModelTestCase(
     private val amountOfRectangles: Int
-) : LayeredComposeTestCase, ToggleableTestCase {
+) : ComposeTestCase, ToggleableTestCase {
 
     private val color = mutableStateOf(Color.Black)
 
     @Composable
-    override fun MeasuredContent() {
-        Column {
-            repeat(amountOfRectangles) { i ->
-                if (i == 0) {
-                    Box(Modifier.size(100.dp, 50.dp).background(color = color.value))
-                } else {
-                    Box(Modifier.size(100.dp, 50.dp).background(color = Color.Green))
+    override fun Content() {
+        MaterialTheme {
+            Column {
+                repeat(amountOfRectangles) { i ->
+                    if (i == 0) {
+                        Box(Modifier.size(100.dp, 50.dp).background(color = color.value))
+                    } else {
+                        Box(Modifier.size(100.dp, 50.dp).background(color = Color.Green))
+                    }
                 }
             }
         }
diff --git a/compose/foundation/foundation-layout/benchmark/src/androidTest/java/androidx/compose/foundation/layout/benchmark/RectsInColumnTestCase.kt b/compose/foundation/foundation-layout/benchmark/src/androidTest/java/androidx/compose/foundation/layout/benchmark/RectsInColumnTestCase.kt
index 88548cc..fce4d49 100644
--- a/compose/foundation/foundation-layout/benchmark/src/androidTest/java/androidx/compose/foundation/layout/benchmark/RectsInColumnTestCase.kt
+++ b/compose/foundation/foundation-layout/benchmark/src/androidTest/java/androidx/compose/foundation/layout/benchmark/RectsInColumnTestCase.kt
@@ -20,11 +20,13 @@
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.size
+import androidx.compose.material.MaterialTheme
+import androidx.compose.material.Surface
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.MutableState
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
-import androidx.compose.testutils.LayeredComposeTestCase
+import androidx.compose.testutils.ComposeTestCase
 import androidx.compose.testutils.ToggleableTestCase
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.Color
@@ -38,15 +40,19 @@
  */
 class RectsInColumnTestCase(
     private val amountOfRectangles: Int
-) : LayeredComposeTestCase, ToggleableTestCase {
+) : ComposeTestCase, ToggleableTestCase {
 
     private val states = mutableListOf<MutableState<Color>>()
 
     @Composable
-    override fun MeasuredContent() {
-        Column {
-            repeat(amountOfRectangles) {
-                ColoredRectWithModel()
+    override fun Content() {
+        MaterialTheme {
+            Surface {
+                Column {
+                    repeat(amountOfRectangles) {
+                        ColoredRectWithModel()
+                    }
+                }
             }
         }
     }
diff --git a/compose/foundation/foundation-layout/benchmark/src/androidTest/java/androidx/compose/foundation/layout/benchmark/SpacingBenchmark.kt b/compose/foundation/foundation-layout/benchmark/src/androidTest/java/androidx/compose/foundation/layout/benchmark/SpacingBenchmark.kt
index 54c3b2e..ca00d7b 100644
--- a/compose/foundation/foundation-layout/benchmark/src/androidTest/java/androidx/compose/foundation/layout/benchmark/SpacingBenchmark.kt
+++ b/compose/foundation/foundation-layout/benchmark/src/androidTest/java/androidx/compose/foundation/layout/benchmark/SpacingBenchmark.kt
@@ -24,7 +24,7 @@
 import androidx.compose.runtime.MutableState
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
-import androidx.compose.testutils.LayeredComposeTestCase
+import androidx.compose.testutils.ComposeTestCase
 import androidx.compose.testutils.ToggleableTestCase
 import androidx.compose.testutils.benchmark.ComposeBenchmarkRule
 import androidx.compose.testutils.benchmark.benchmarkDrawPerf
@@ -158,7 +158,7 @@
     }
 }
 
-private sealed class PaddingTestCase : LayeredComposeTestCase, ToggleableTestCase {
+private sealed class PaddingTestCase : ComposeTestCase, ToggleableTestCase {
 
     var paddingState: MutableState<Dp>? = null
 
@@ -169,7 +169,7 @@
     }
 
     @Composable
-    override fun MeasuredContent() {
+    override fun Content() {
         val padding = remember { mutableStateOf(5.dp) }
         paddingState = padding
 
diff --git a/compose/foundation/foundation-layout/build.gradle b/compose/foundation/foundation-layout/build.gradle
index 30c0190a..d4582c7 100644
--- a/compose/foundation/foundation-layout/build.gradle
+++ b/compose/foundation/foundation-layout/build.gradle
@@ -50,6 +50,7 @@
 
         androidTestImplementation(project(":compose:foundation:foundation"))
         androidTestImplementation(project(":compose:ui:ui-test-junit4"))
+        androidTestImplementation(project(":compose:test-utils"))
         androidTestImplementation(project(":activity:activity-compose"))
         androidTestImplementation(ANDROIDX_TEST_RULES)
         androidTestImplementation(ANDROIDX_TEST_RUNNER)
@@ -93,6 +94,7 @@
             androidAndroidTest.dependencies {
                 implementation(project(":compose:foundation:foundation"))
                 implementation(project(":compose:ui:ui-test-junit4"))
+                implementation(project(":compose:test-utils"))
                 implementation(project(":activity:activity-compose"))
 
                 implementation(ANDROIDX_TEST_RULES)
diff --git a/compose/foundation/foundation/api/1.0.0-beta05.txt b/compose/foundation/foundation/api/1.0.0-beta05.txt
index f85c7ad..16ae359 100644
--- a/compose/foundation/foundation/api/1.0.0-beta05.txt
+++ b/compose/foundation/foundation/api/1.0.0-beta05.txt
@@ -149,7 +149,7 @@
     method public suspend Object? drag(optional androidx.compose.foundation.MutatePriority dragPriority, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.gestures.DragScope,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> block, kotlin.coroutines.Continuation<? super kotlin.Unit> p);
   }
 
-  public interface FlingBehavior {
+  @androidx.compose.runtime.Stable public interface FlingBehavior {
     method public suspend Object? performFling(androidx.compose.foundation.gestures.ScrollScope, float initialVelocity, kotlin.coroutines.Continuation<? super java.lang.Float> p);
   }
 
diff --git a/compose/foundation/foundation/api/current.txt b/compose/foundation/foundation/api/current.txt
index f85c7ad..16ae359 100644
--- a/compose/foundation/foundation/api/current.txt
+++ b/compose/foundation/foundation/api/current.txt
@@ -149,7 +149,7 @@
     method public suspend Object? drag(optional androidx.compose.foundation.MutatePriority dragPriority, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.gestures.DragScope,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> block, kotlin.coroutines.Continuation<? super kotlin.Unit> p);
   }
 
-  public interface FlingBehavior {
+  @androidx.compose.runtime.Stable public interface FlingBehavior {
     method public suspend Object? performFling(androidx.compose.foundation.gestures.ScrollScope, float initialVelocity, kotlin.coroutines.Continuation<? super java.lang.Float> p);
   }
 
diff --git a/compose/foundation/foundation/api/public_plus_experimental_1.0.0-beta05.txt b/compose/foundation/foundation/api/public_plus_experimental_1.0.0-beta05.txt
index 7453b13..ce801c7 100644
--- a/compose/foundation/foundation/api/public_plus_experimental_1.0.0-beta05.txt
+++ b/compose/foundation/foundation/api/public_plus_experimental_1.0.0-beta05.txt
@@ -157,7 +157,7 @@
     method public suspend Object? drag(optional androidx.compose.foundation.MutatePriority dragPriority, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.gestures.DragScope,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> block, kotlin.coroutines.Continuation<? super kotlin.Unit> p);
   }
 
-  public interface FlingBehavior {
+  @androidx.compose.runtime.Stable public interface FlingBehavior {
     method public suspend Object? performFling(androidx.compose.foundation.gestures.ScrollScope, float initialVelocity, kotlin.coroutines.Continuation<? super java.lang.Float> p);
   }
 
diff --git a/compose/foundation/foundation/api/public_plus_experimental_current.txt b/compose/foundation/foundation/api/public_plus_experimental_current.txt
index 7453b13..ce801c7 100644
--- a/compose/foundation/foundation/api/public_plus_experimental_current.txt
+++ b/compose/foundation/foundation/api/public_plus_experimental_current.txt
@@ -157,7 +157,7 @@
     method public suspend Object? drag(optional androidx.compose.foundation.MutatePriority dragPriority, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.gestures.DragScope,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> block, kotlin.coroutines.Continuation<? super kotlin.Unit> p);
   }
 
-  public interface FlingBehavior {
+  @androidx.compose.runtime.Stable public interface FlingBehavior {
     method public suspend Object? performFling(androidx.compose.foundation.gestures.ScrollScope, float initialVelocity, kotlin.coroutines.Continuation<? super java.lang.Float> p);
   }
 
diff --git a/compose/foundation/foundation/api/restricted_1.0.0-beta05.txt b/compose/foundation/foundation/api/restricted_1.0.0-beta05.txt
index f85c7ad..16ae359 100644
--- a/compose/foundation/foundation/api/restricted_1.0.0-beta05.txt
+++ b/compose/foundation/foundation/api/restricted_1.0.0-beta05.txt
@@ -149,7 +149,7 @@
     method public suspend Object? drag(optional androidx.compose.foundation.MutatePriority dragPriority, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.gestures.DragScope,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> block, kotlin.coroutines.Continuation<? super kotlin.Unit> p);
   }
 
-  public interface FlingBehavior {
+  @androidx.compose.runtime.Stable public interface FlingBehavior {
     method public suspend Object? performFling(androidx.compose.foundation.gestures.ScrollScope, float initialVelocity, kotlin.coroutines.Continuation<? super java.lang.Float> p);
   }
 
diff --git a/compose/foundation/foundation/api/restricted_current.txt b/compose/foundation/foundation/api/restricted_current.txt
index f85c7ad..16ae359 100644
--- a/compose/foundation/foundation/api/restricted_current.txt
+++ b/compose/foundation/foundation/api/restricted_current.txt
@@ -149,7 +149,7 @@
     method public suspend Object? drag(optional androidx.compose.foundation.MutatePriority dragPriority, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.gestures.DragScope,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> block, kotlin.coroutines.Continuation<? super kotlin.Unit> p);
   }
 
-  public interface FlingBehavior {
+  @androidx.compose.runtime.Stable public interface FlingBehavior {
     method public suspend Object? performFling(androidx.compose.foundation.gestures.ScrollScope, float initialVelocity, kotlin.coroutines.Continuation<? super java.lang.Float> p);
   }
 
diff --git a/compose/foundation/foundation/benchmark/src/androidTest/java/androidx/compose/foundation/benchmark/NestedScrollerTestCase.kt b/compose/foundation/foundation/benchmark/src/androidTest/java/androidx/compose/foundation/benchmark/NestedScrollerTestCase.kt
index 2db5f25..2ad14ed 100644
--- a/compose/foundation/foundation/benchmark/src/androidTest/java/androidx/compose/foundation/benchmark/NestedScrollerTestCase.kt
+++ b/compose/foundation/foundation/benchmark/src/androidTest/java/androidx/compose/foundation/benchmark/NestedScrollerTestCase.kt
@@ -28,10 +28,12 @@
 import androidx.compose.foundation.layout.size
 import androidx.compose.foundation.lazy.LazyColumn
 import androidx.compose.foundation.rememberScrollState
+import androidx.compose.material.MaterialTheme
+import androidx.compose.material.Surface
 import androidx.compose.material.Text
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.remember
-import androidx.compose.testutils.LayeredComposeTestCase
+import androidx.compose.testutils.ComposeTestCase
 import androidx.compose.testutils.ToggleableTestCase
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
@@ -43,16 +45,20 @@
 /**
  * Test case that puts many horizontal scrollers in a vertical scroller
  */
-class NestedScrollerTestCase : LayeredComposeTestCase, ToggleableTestCase {
+class NestedScrollerTestCase : ComposeTestCase, ToggleableTestCase {
     // ScrollerPosition must now be constructed during composition to obtain the Density
     private lateinit var scrollState: ScrollState
 
     @Composable
-    override fun MeasuredContent() {
+    override fun Content() {
         scrollState = rememberScrollState()
-        LazyColumn {
-            items(5) { index ->
-                SquareRow(index == 0)
+        MaterialTheme {
+            Surface {
+                LazyColumn {
+                    items(5) { index ->
+                        SquareRow(index == 0)
+                    }
+                }
             }
         }
     }
diff --git a/compose/foundation/foundation/benchmark/src/androidTest/java/androidx/compose/foundation/benchmark/ScrollerTestCase.kt b/compose/foundation/foundation/benchmark/src/androidTest/java/androidx/compose/foundation/benchmark/ScrollerTestCase.kt
index ec48333..1a2fcc1 100644
--- a/compose/foundation/foundation/benchmark/src/androidTest/java/androidx/compose/foundation/benchmark/ScrollerTestCase.kt
+++ b/compose/foundation/foundation/benchmark/src/androidTest/java/androidx/compose/foundation/benchmark/ScrollerTestCase.kt
@@ -24,7 +24,7 @@
 import androidx.compose.foundation.rememberScrollState
 import androidx.compose.foundation.verticalScroll
 import androidx.compose.runtime.Composable
-import androidx.compose.testutils.LayeredComposeTestCase
+import androidx.compose.testutils.ComposeTestCase
 import androidx.compose.testutils.ToggleableTestCase
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.Color
@@ -34,11 +34,11 @@
 /**
  * Test case that puts a large number of boxes in a column in a vertical scroller to force scrolling.
  */
-class ScrollerTestCase : LayeredComposeTestCase, ToggleableTestCase {
+class ScrollerTestCase : ComposeTestCase, ToggleableTestCase {
     private lateinit var scrollState: ScrollState
 
     @Composable
-    override fun MeasuredContent() {
+    override fun Content() {
         scrollState = rememberScrollState()
         Column(Modifier.verticalScroll(scrollState)) {
             Column(Modifier.fillMaxHeight()) {
diff --git a/compose/foundation/foundation/benchmark/src/androidTest/java/androidx/compose/foundation/benchmark/SimpleComponentImplementationBenchmark.kt b/compose/foundation/foundation/benchmark/src/androidTest/java/androidx/compose/foundation/benchmark/SimpleComponentImplementationBenchmark.kt
index a23c9cd..0e9330f 100644
--- a/compose/foundation/foundation/benchmark/src/androidTest/java/androidx/compose/foundation/benchmark/SimpleComponentImplementationBenchmark.kt
+++ b/compose/foundation/foundation/benchmark/src/androidTest/java/androidx/compose/foundation/benchmark/SimpleComponentImplementationBenchmark.kt
@@ -28,7 +28,7 @@
 import androidx.compose.runtime.MutableState
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
-import androidx.compose.testutils.LayeredComposeTestCase
+import androidx.compose.testutils.ComposeTestCase
 import androidx.compose.testutils.ToggleableTestCase
 import androidx.compose.testutils.benchmark.ComposeBenchmarkRule
 import androidx.compose.testutils.benchmark.benchmarkDrawPerf
@@ -231,7 +231,7 @@
 class ComponentWithModifiersTestCase : SimpleComponentImplementationTestCase() {
 
     @Composable
-    override fun MeasuredContent() {
+    override fun Content() {
         val innerSize = getInnerSize()
         Box(
             Modifier.size(48.dp)
@@ -249,7 +249,7 @@
 class ComponentWithRedrawTestCase : SimpleComponentImplementationTestCase() {
 
     @Composable
-    override fun MeasuredContent() {
+    override fun Content() {
         val innerSize = getInnerSize()
         val stroke = Stroke()
         Canvas(Modifier.size(48.dp)) {
@@ -261,7 +261,7 @@
 
 class ComponentWithTwoLayoutNodesTestCase : SimpleComponentImplementationTestCase() {
     @Composable
-    override fun MeasuredContent() {
+    override fun Content() {
         Box(
             modifier = Modifier
                 .size(48.dp)
@@ -280,7 +280,7 @@
     }
 }
 
-abstract class SimpleComponentImplementationTestCase : LayeredComposeTestCase, ToggleableTestCase {
+abstract class SimpleComponentImplementationTestCase : ComposeTestCase, ToggleableTestCase {
 
     private var state: MutableState<Dp>? = null
 
diff --git a/compose/foundation/foundation/benchmark/src/androidTest/java/androidx/compose/foundation/benchmark/TrailingLambdaBenchmark.kt b/compose/foundation/foundation/benchmark/src/androidTest/java/androidx/compose/foundation/benchmark/TrailingLambdaBenchmark.kt
index f823649..335d283 100644
--- a/compose/foundation/foundation/benchmark/src/androidTest/java/androidx/compose/foundation/benchmark/TrailingLambdaBenchmark.kt
+++ b/compose/foundation/foundation/benchmark/src/androidTest/java/androidx/compose/foundation/benchmark/TrailingLambdaBenchmark.kt
@@ -23,7 +23,7 @@
 import androidx.compose.runtime.MutableState
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
-import androidx.compose.testutils.LayeredComposeTestCase
+import androidx.compose.testutils.ComposeTestCase
 import androidx.compose.testutils.ToggleableTestCase
 import androidx.compose.testutils.benchmark.ComposeBenchmarkRule
 import androidx.compose.testutils.benchmark.benchmarkFirstCompose
@@ -44,7 +44,7 @@
 
     @Test
     fun withTrailingLambdas_compose() {
-        benchmarkRule.benchmarkFirstCompose { WithTrailingLambdas() }
+        benchmarkRule.benchmarkFirstCompose({ WithTrailingLambdas() })
     }
 
     @Test
@@ -54,7 +54,7 @@
 
     @Test
     fun withoutTrailingLambdas_compose() {
-        benchmarkRule.benchmarkFirstCompose { WithoutTrailingLambdas() }
+        benchmarkRule.benchmarkFirstCompose({ WithoutTrailingLambdas() })
     }
 
     @Test
@@ -63,12 +63,12 @@
     }
 }
 
-private sealed class TrailingLambdaTestCase : LayeredComposeTestCase, ToggleableTestCase {
+private sealed class TrailingLambdaTestCase : ComposeTestCase, ToggleableTestCase {
 
     var numberState: MutableState<Int>? = null
 
     @Composable
-    override fun MeasuredContent() {
+    override fun Content() {
         val number = remember { mutableStateOf(5) }
         numberState = number
 
diff --git a/compose/foundation/foundation/benchmark/src/androidTest/java/androidx/compose/foundation/benchmark/text/TextBasicBenchmark.kt b/compose/foundation/foundation/benchmark/src/androidTest/java/androidx/compose/foundation/benchmark/text/TextBasicBenchmark.kt
index f7f3edd..6ddf510 100644
--- a/compose/foundation/foundation/benchmark/src/androidTest/java/androidx/compose/foundation/benchmark/text/TextBasicBenchmark.kt
+++ b/compose/foundation/foundation/benchmark/src/androidTest/java/androidx/compose/foundation/benchmark/text/TextBasicBenchmark.kt
@@ -18,10 +18,10 @@
 
 import androidx.compose.testutils.benchmark.ComposeBenchmarkRule
 import androidx.compose.testutils.benchmark.benchmarkDrawPerf
-import androidx.compose.testutils.benchmark.benchmarkFirstCompose
-import androidx.compose.testutils.benchmark.benchmarkFirstDraw
-import androidx.compose.testutils.benchmark.benchmarkFirstLayout
-import androidx.compose.testutils.benchmark.benchmarkFirstMeasure
+import androidx.compose.testutils.benchmark.benchmarkFirstComposeFast
+import androidx.compose.testutils.benchmark.benchmarkFirstDrawFast
+import androidx.compose.testutils.benchmark.benchmarkFirstLayoutFast
+import androidx.compose.testutils.benchmark.benchmarkFirstMeasureFast
 import androidx.compose.testutils.benchmark.benchmarkLayoutPerf
 import androidx.compose.testutils.benchmark.toggleStateBenchmarkDraw
 import androidx.compose.testutils.benchmark.toggleStateBenchmarkLayout
@@ -85,7 +85,7 @@
      */
     @Test
     fun first_compose() {
-        benchmarkRule.benchmarkFirstCompose(caseFactory)
+        benchmarkRule.benchmarkFirstComposeFast(caseFactory)
     }
 
     /**
@@ -94,7 +94,7 @@
      */
     @Test
     fun first_measure() {
-        benchmarkRule.benchmarkFirstMeasure(caseFactory)
+        benchmarkRule.benchmarkFirstMeasureFast(caseFactory)
     }
 
     /**
@@ -103,7 +103,7 @@
      */
     @Test
     fun first_layout() {
-        benchmarkRule.benchmarkFirstLayout(caseFactory)
+        benchmarkRule.benchmarkFirstLayoutFast(caseFactory)
     }
 
     /**
@@ -111,7 +111,7 @@
      */
     @Test
     fun first_draw() {
-        benchmarkRule.benchmarkFirstDraw(caseFactory)
+        benchmarkRule.benchmarkFirstDrawFast(caseFactory)
     }
 
     /**
diff --git a/compose/foundation/foundation/benchmark/src/androidTest/java/androidx/compose/foundation/benchmark/text/TextFieldToggleTextBenchmark.kt b/compose/foundation/foundation/benchmark/src/androidTest/java/androidx/compose/foundation/benchmark/text/TextFieldToggleTextBenchmark.kt
index e64f1de..14f1941 100644
--- a/compose/foundation/foundation/benchmark/src/androidTest/java/androidx/compose/foundation/benchmark/text/TextFieldToggleTextBenchmark.kt
+++ b/compose/foundation/foundation/benchmark/src/androidTest/java/androidx/compose/foundation/benchmark/text/TextFieldToggleTextBenchmark.kt
@@ -18,10 +18,10 @@
 
 import androidx.compose.testutils.benchmark.ComposeBenchmarkRule
 import androidx.compose.testutils.benchmark.benchmarkDrawPerf
-import androidx.compose.testutils.benchmark.benchmarkFirstCompose
-import androidx.compose.testutils.benchmark.benchmarkFirstDraw
-import androidx.compose.testutils.benchmark.benchmarkFirstLayout
-import androidx.compose.testutils.benchmark.benchmarkFirstMeasure
+import androidx.compose.testutils.benchmark.benchmarkFirstComposeFast
+import androidx.compose.testutils.benchmark.benchmarkFirstDrawFast
+import androidx.compose.testutils.benchmark.benchmarkFirstLayoutFast
+import androidx.compose.testutils.benchmark.benchmarkFirstMeasureFast
 import androidx.compose.testutils.benchmark.benchmarkLayoutPerf
 import androidx.compose.testutils.benchmark.toggleStateBenchmarkDraw
 import androidx.compose.testutils.benchmark.toggleStateBenchmarkLayout
@@ -77,7 +77,7 @@
      */
     @Test
     fun first_compose() {
-        benchmarkRule.benchmarkFirstCompose(caseFactory)
+        benchmarkRule.benchmarkFirstComposeFast(caseFactory)
     }
 
     /**
@@ -87,7 +87,7 @@
      */
     @Test
     fun first_measure() {
-        benchmarkRule.benchmarkFirstMeasure(caseFactory)
+        benchmarkRule.benchmarkFirstMeasureFast(caseFactory)
     }
 
     /**
@@ -96,7 +96,7 @@
      */
     @Test
     fun first_layout() {
-        benchmarkRule.benchmarkFirstLayout(caseFactory)
+        benchmarkRule.benchmarkFirstLayoutFast(caseFactory)
     }
 
     /**
@@ -105,7 +105,7 @@
      */
     @Test
     fun first_draw() {
-        benchmarkRule.benchmarkFirstDraw(caseFactory)
+        benchmarkRule.benchmarkFirstDrawFast(caseFactory)
     }
 
     /**
diff --git a/compose/foundation/foundation/benchmark/src/androidTest/java/androidx/compose/foundation/benchmark/text/selection/SelectionContainerTestCase.kt b/compose/foundation/foundation/benchmark/src/androidTest/java/androidx/compose/foundation/benchmark/text/selection/SelectionContainerTestCase.kt
index 93caa92..e94b226 100644
--- a/compose/foundation/foundation/benchmark/src/androidTest/java/androidx/compose/foundation/benchmark/text/selection/SelectionContainerTestCase.kt
+++ b/compose/foundation/foundation/benchmark/src/androidTest/java/androidx/compose/foundation/benchmark/text/selection/SelectionContainerTestCase.kt
@@ -20,13 +20,13 @@
 import androidx.compose.foundation.text.selection.SelectionContainer
 import androidx.compose.material.Text
 import androidx.compose.runtime.Composable
-import androidx.compose.testutils.LayeredComposeTestCase
+import androidx.compose.testutils.ComposeTestCase
 import androidx.compose.ui.text.TextStyle
 import androidx.compose.ui.unit.sp
 
-class SelectionContainerTestCase(private val childrenCount: Int) : LayeredComposeTestCase {
+class SelectionContainerTestCase(private val childrenCount: Int) : ComposeTestCase {
     @Composable
-    override fun MeasuredContent() {
+    override fun Content() {
         SelectionContainer {
             Column {
                 repeat(childrenCount) {
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/LazyListHeadersTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/LazyListHeadersTest.kt
index 7a2051f..276a499 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/LazyListHeadersTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/LazyListHeadersTest.kt
@@ -20,8 +20,8 @@
 import androidx.compose.foundation.layout.PaddingValues
 import androidx.compose.foundation.layout.Spacer
 import androidx.compose.foundation.layout.height
-import androidx.compose.foundation.layout.width
 import androidx.compose.foundation.layout.requiredSize
+import androidx.compose.foundation.layout.width
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.platform.testTag
 import androidx.compose.ui.test.assertIsDisplayed
@@ -159,7 +159,7 @@
         }
 
         rule.onNodeWithTag(LazyListTag)
-            .scrollBy(y = 102.dp, density = rule.density)
+            .scrollBy(y = 105.dp, density = rule.density)
 
         rule.onNodeWithTag(firstHeaderTag)
             .assertDoesNotExist()
@@ -286,7 +286,7 @@
         }
 
         rule.onNodeWithTag(LazyListTag)
-            .scrollBy(x = 102.dp, density = rule.density)
+            .scrollBy(x = 105.dp, density = rule.density)
 
         rule.onNodeWithTag(firstHeaderTag)
             .assertDoesNotExist()
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/LazyListsContentPaddingTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/LazyListsContentPaddingTest.kt
index f319f730..41f9358 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/LazyListsContentPaddingTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/LazyListsContentPaddingTest.kt
@@ -56,11 +56,13 @@
     val rule = createComposeRule()
 
     private var itemSize: Dp = Dp.Infinity
+    private var smallPaddingSize: Dp = Dp.Infinity
 
     @Before
     fun before() {
         with(rule.density) {
             itemSize = 50.toDp()
+            smallPaddingSize = 12.toDp()
         }
     }
 
@@ -68,7 +70,6 @@
     fun column_contentPaddingIsApplied() {
         lateinit var state: LazyListState
         val containerSize = itemSize * 2
-        val smallPaddingSize = itemSize / 4
         val largePaddingSize = itemSize
         rule.setContent {
             LazyColumn(
@@ -341,7 +342,6 @@
     fun row_contentPaddingIsApplied() {
         lateinit var state: LazyListState
         val containerSize = itemSize * 2
-        val smallPaddingSize = itemSize / 4
         val largePaddingSize = itemSize
         rule.setContent {
             LazyRow(
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/LazyRowTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/LazyRowTest.kt
index c7b6335..bd6fae3 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/LazyRowTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/LazyRowTest.kt
@@ -233,7 +233,7 @@
         }
 
         rule.onNodeWithTag(LazyListTag)
-            .scrollBy(x = 102.dp, density = rule.density)
+            .scrollBy(x = 105.dp, density = rule.density)
 
         rule.onNodeWithTag("1")
             .assertDoesNotExist()
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/textfield/TextFieldTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/textfield/TextFieldTest.kt
index 676634a..d01ccfa 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/textfield/TextFieldTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/textfield/TextFieldTest.kt
@@ -801,6 +801,7 @@
             // reset flag
             onValueChangeCalled = false
         }
+        rule.waitUntil { onValueChangeCalled == false }
 
         // set selection to same value, no change should occur
         @OptIn(ExperimentalTestApi::class)
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/FlingBehavior.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/FlingBehavior.kt
index fce4545..733b4f4 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/FlingBehavior.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/FlingBehavior.kt
@@ -16,12 +16,15 @@
 
 package androidx.compose.foundation.gestures
 
+import androidx.compose.runtime.Stable
+
 /**
  * Interface to specify fling behavior.
  *
  * When drag has ended with velocity in [scrollable], [performFling] is invoked to perform fling
  * animation and update state via [ScrollScope.scrollBy]
  */
+@Stable
 interface FlingBehavior {
     /**
      * Perform settling via fling animation with given velocity and suspend until fling has
diff --git a/compose/integration-tests/docs-snippets/src/main/AndroidManifest.xml b/compose/integration-tests/docs-snippets/src/main/AndroidManifest.xml
index 46be6d8..db5bed8 100644
--- a/compose/integration-tests/docs-snippets/src/main/AndroidManifest.xml
+++ b/compose/integration-tests/docs-snippets/src/main/AndroidManifest.xml
@@ -14,12 +14,4 @@
   ~ See the License for the specific language governing permissions and
   ~ limitations under the License.
   -->
-<manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="androidx.compose.integration.docs">
-
-    <application>
-        <activity
-            android:name="androidx.activity.ComponentActivity"
-            android:theme="@style/TestTheme" />
-    </application>
-</manifest>
+<manifest package="androidx.compose.integration.docs" />
diff --git a/compose/integration-tests/docs-snippets/src/main/java/androidx/compose/integration/docs/layout/Layout.kt b/compose/integration-tests/docs-snippets/src/main/java/androidx/compose/integration/docs/layout/Layout.kt
index e6186af..ab2fb87 100644
--- a/compose/integration-tests/docs-snippets/src/main/java/androidx/compose/integration/docs/layout/Layout.kt
+++ b/compose/integration-tests/docs-snippets/src/main/java/androidx/compose/integration/docs/layout/Layout.kt
@@ -274,7 +274,7 @@
 private object LayoutSnippet12 {
     fun Modifier.firstBaselineToTop(
         firstBaselineToTop: Dp
-    ) = Modifier.layout { measurable, constraints ->
+    ) = layout { measurable, constraints ->
         // Measure the composable
         val placeable = measurable.measure(constraints)
 
diff --git a/compose/lint/common/src/main/java/androidx/compose/lint/KotlinMetadataUtils.kt b/compose/lint/common/src/main/java/androidx/compose/lint/KotlinMetadataUtils.kt
index 347b842..8b20f3e 100644
--- a/compose/lint/common/src/main/java/androidx/compose/lint/KotlinMetadataUtils.kt
+++ b/compose/lint/common/src/main/java/androidx/compose/lint/KotlinMetadataUtils.kt
@@ -116,7 +116,8 @@
  * signature.
  */
 private fun KmDeclarationContainer.findKmFunctionForPsiMethod(method: PsiMethod): KmFunction? {
-    val expectedName = method.name
+    // Strip any mangled part of the name in case of inline classes
+    val expectedName = method.name.substringBefore("-")
     val expectedSignature = ClassUtil.getAsmMethodSignature(method)
 
     return functions.find {
diff --git a/compose/lint/common/src/main/java/androidx/compose/lint/KotlinUtils.kt b/compose/lint/common/src/main/java/androidx/compose/lint/KotlinUtils.kt
index e2dc2d6..9ec00a4 100644
--- a/compose/lint/common/src/main/java/androidx/compose/lint/KotlinUtils.kt
+++ b/compose/lint/common/src/main/java/androidx/compose/lint/KotlinUtils.kt
@@ -16,42 +16,15 @@
 
 package androidx.compose.lint
 
-import com.intellij.psi.impl.compiled.ClsMethodImpl
-import kotlinx.metadata.Flag
-import org.jetbrains.kotlin.lexer.KtTokens.INLINE_KEYWORD
-import org.jetbrains.kotlin.psi.KtFunction
 import org.jetbrains.kotlin.psi.KtLambdaExpression
 import org.jetbrains.kotlin.psi.KtParameter
 import org.jetbrains.kotlin.psi.KtSimpleNameExpression
 import org.jetbrains.kotlin.psi.psiUtil.collectDescendantsOfType
 import org.jetbrains.kotlin.psi.psiUtil.isAncestor
-import org.jetbrains.uast.UCallExpression
 import org.jetbrains.uast.ULambdaExpression
-import org.jetbrains.uast.resolveToUElement
 import org.jetbrains.uast.toUElement
 
 /**
- * @return whether the resolved declaration for this call expression is an inline function
- */
-val UCallExpression.isDeclarationInline: Boolean
-    get() {
-        return when (val source = resolveToUElement()?.sourcePsi) {
-            // Parsing a method defined in a class file
-            is ClsMethodImpl -> {
-                val flags = source.toKmFunction()?.flags ?: return false
-                return Flag.Function.IS_INLINE(flags)
-            }
-            // Parsing a method defined in Kotlin source
-            is KtFunction -> {
-                source.hasModifier(INLINE_KEYWORD)
-            }
-            // Parsing another declaration (such as a property) which cannot be inline, or
-            // a non-Kotlin declaration
-            else -> false
-        }
-    }
-
-/**
  * Returns a list of unreferenced parameters in [this]. If no parameters have been specified, but
  * there is an implicit `it` parameter, this will return a list containing an
  * [UnreferencedParameter] with `it` as the name.
diff --git a/compose/material/material/benchmark/src/androidTest/java/androidx/compose/material/benchmark/CheckboxesInRowsBenchmark.kt b/compose/material/material/benchmark/src/androidTest/java/androidx/compose/material/benchmark/CheckboxesInRowsBenchmark.kt
index dec4d18..f610771 100644
--- a/compose/material/material/benchmark/src/androidTest/java/androidx/compose/material/benchmark/CheckboxesInRowsBenchmark.kt
+++ b/compose/material/material/benchmark/src/androidTest/java/androidx/compose/material/benchmark/CheckboxesInRowsBenchmark.kt
@@ -53,7 +53,10 @@
 
     @Test
     fun first_compose() {
-        benchmarkRule.benchmarkFirstCompose(checkboxCaseFactory)
+        benchmarkRule.benchmarkFirstCompose(
+            checkboxCaseFactory,
+            assertNoPendingRecompositions = false
+        )
     }
 
     @Test
diff --git a/compose/material/material/benchmark/src/androidTest/java/androidx/compose/material/benchmark/CheckboxesInRowsTestCase.kt b/compose/material/material/benchmark/src/androidTest/java/androidx/compose/material/benchmark/CheckboxesInRowsTestCase.kt
index a9526be..979074bc6 100644
--- a/compose/material/material/benchmark/src/androidTest/java/androidx/compose/material/benchmark/CheckboxesInRowsTestCase.kt
+++ b/compose/material/material/benchmark/src/androidTest/java/androidx/compose/material/benchmark/CheckboxesInRowsTestCase.kt
@@ -27,7 +27,7 @@
 import androidx.compose.runtime.MutableState
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
-import androidx.compose.testutils.LayeredComposeTestCase
+import androidx.compose.testutils.ComposeTestCase
 import androidx.compose.testutils.ToggleableTestCase
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
@@ -38,29 +38,24 @@
  */
 class CheckboxesInRowsTestCase(
     private val amountOfCheckboxes: Int
-) : LayeredComposeTestCase, ToggleableTestCase {
+) : ComposeTestCase, ToggleableTestCase {
 
     private val states = mutableListOf<MutableState<Boolean>>()
 
     @Composable
-    override fun MeasuredContent() {
-        Column {
-            repeat(amountOfCheckboxes) {
-                Row {
-                    Text(text = "Check Me!")
-                    CheckboxWithState(
-                        Modifier.weight(1f).wrapContentSize(Alignment.CenterEnd)
-                    )
-                }
-            }
-        }
-    }
-
-    @Composable
-    override fun ContentWrappers(content: () -> Unit) {
+    override fun Content() {
         MaterialTheme {
             Surface {
-                content()
+                Column {
+                    repeat(amountOfCheckboxes) {
+                        Row {
+                            Text(text = "Check Me!")
+                            CheckboxWithState(
+                                Modifier.weight(1f).wrapContentSize(Alignment.CenterEnd)
+                            )
+                        }
+                    }
+                }
             }
         }
     }
diff --git a/compose/material/material/benchmark/src/androidTest/java/androidx/compose/material/benchmark/RadioGroupBenchmark.kt b/compose/material/material/benchmark/src/androidTest/java/androidx/compose/material/benchmark/RadioGroupBenchmark.kt
index e6fed73..4a33072 100644
--- a/compose/material/material/benchmark/src/androidTest/java/androidx/compose/material/benchmark/RadioGroupBenchmark.kt
+++ b/compose/material/material/benchmark/src/androidTest/java/androidx/compose/material/benchmark/RadioGroupBenchmark.kt
@@ -24,7 +24,7 @@
 import androidx.compose.material.Text
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.mutableStateOf
-import androidx.compose.testutils.LayeredComposeTestCase
+import androidx.compose.testutils.ComposeTestCase
 import androidx.compose.testutils.ToggleableTestCase
 import androidx.compose.testutils.benchmark.ComposeBenchmarkRule
 import androidx.compose.testutils.benchmark.benchmarkFirstCompose
@@ -57,7 +57,7 @@
 
     @Test
     fun first_compose() {
-        benchmarkRule.benchmarkFirstCompose(radioCaseFactory)
+        benchmarkRule.benchmarkFirstCompose(radioCaseFactory, assertNoPendingRecompositions = false)
     }
 
     @Test
@@ -99,7 +99,7 @@
     }
 }
 
-internal class RadioGroupTestCase : LayeredComposeTestCase, ToggleableTestCase {
+internal class RadioGroupTestCase : ComposeTestCase, ToggleableTestCase {
 
     private val radiosCount = 10
     private val options = (0 until radiosCount).toList()
@@ -110,30 +110,25 @@
     }
 
     @Composable
-    override fun MeasuredContent() {
-        Column {
-            options.forEach { item ->
-                Row(
-                    modifier = Modifier.selectable(
-                        selected = (select.value == item),
-                        onClick = { select.value = item }
-                    ),
-                    verticalAlignment = Alignment.CenterVertically
-                ) {
-                    Text(item.toString())
-                    RadioButton(
-                        selected = (select.value == item),
-                        onClick = { select.value = item }
-                    )
+    override fun Content() {
+        MaterialTheme {
+            Column {
+                options.forEach { item ->
+                    Row(
+                        modifier = Modifier.selectable(
+                            selected = (select.value == item),
+                            onClick = { select.value = item }
+                        ),
+                        verticalAlignment = Alignment.CenterVertically
+                    ) {
+                        Text(item.toString())
+                        RadioButton(
+                            selected = (select.value == item),
+                            onClick = { select.value = item }
+                        )
+                    }
                 }
             }
         }
     }
-
-    @Composable
-    override fun ContentWrappers(content: @Composable () -> Unit) {
-        MaterialTheme {
-            content()
-        }
-    }
 }
\ No newline at end of file
diff --git a/compose/material/material/benchmark/src/androidTest/java/androidx/compose/material/benchmark/TextInColumnSizeToggleTestCase.kt b/compose/material/material/benchmark/src/androidTest/java/androidx/compose/material/benchmark/TextInColumnSizeToggleTestCase.kt
index 270df6f..339de17 100644
--- a/compose/material/material/benchmark/src/androidTest/java/androidx/compose/material/benchmark/TextInColumnSizeToggleTestCase.kt
+++ b/compose/material/material/benchmark/src/androidTest/java/androidx/compose/material/benchmark/TextInColumnSizeToggleTestCase.kt
@@ -22,7 +22,7 @@
 import androidx.compose.material.Text
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.mutableStateOf
-import androidx.compose.testutils.LayeredComposeTestCase
+import androidx.compose.testutils.ComposeTestCase
 import androidx.compose.testutils.ToggleableTestCase
 import androidx.compose.ui.text.TextStyle
 import androidx.compose.ui.unit.sp
@@ -35,28 +35,23 @@
  */
 class TextInColumnSizeToggleTestCase(
     private val numberOfTexts: Int
-) : LayeredComposeTestCase, ToggleableTestCase {
+) : ComposeTestCase, ToggleableTestCase {
 
     private val fontSize = mutableStateOf(20.sp)
 
     @Composable
-    override fun MeasuredContent() {
-        Column {
-            repeat(numberOfTexts) {
-                // 32-character text to match dashboards
-                Text(
-                    "Hello World Hello World Hello W",
-                    style = TextStyle(fontSize = fontSize.value)
-                )
-            }
-        }
-    }
-
-    @Composable
-    override fun ContentWrappers(content: @Composable () -> Unit) {
+    override fun Content() {
         MaterialTheme {
             Surface {
-                content()
+                Column {
+                    repeat(numberOfTexts) {
+                        // 32-character text to match dashboards
+                        Text(
+                            "Hello World Hello World Hello W",
+                            style = TextStyle(fontSize = fontSize.value)
+                        )
+                    }
+                }
             }
         }
     }
diff --git a/compose/runtime/runtime-rxjava2/build.gradle b/compose/runtime/runtime-rxjava2/build.gradle
index 3ec8592..fedae8c 100644
--- a/compose/runtime/runtime-rxjava2/build.gradle
+++ b/compose/runtime/runtime-rxjava2/build.gradle
@@ -35,6 +35,7 @@
     api(RX_JAVA)
 
     androidTestImplementation(project(":compose:ui:ui-test-junit4"))
+    androidTestImplementation(project(":compose:test-utils"))
     androidTestImplementation(ANDROIDX_TEST_RUNNER)
     androidTestImplementation(JUNIT)
     androidTestImplementation(TRUTH)
diff --git a/compose/runtime/runtime-rxjava3/build.gradle b/compose/runtime/runtime-rxjava3/build.gradle
index 949a855..c56f6dc 100644
--- a/compose/runtime/runtime-rxjava3/build.gradle
+++ b/compose/runtime/runtime-rxjava3/build.gradle
@@ -35,6 +35,7 @@
     api(RX_JAVA3)
 
     androidTestImplementation(project(":compose:ui:ui-test-junit4"))
+    androidTestImplementation(project(":compose:test-utils"))
     androidTestImplementation(ANDROIDX_TEST_RUNNER)
     androidTestImplementation(JUNIT)
     androidTestImplementation(TRUTH)
diff --git a/compose/runtime/runtime-saveable/build.gradle b/compose/runtime/runtime-saveable/build.gradle
index 2f53274..eaf9e66 100644
--- a/compose/runtime/runtime-saveable/build.gradle
+++ b/compose/runtime/runtime-saveable/build.gradle
@@ -47,6 +47,7 @@
 
         androidTestImplementation project(':compose:ui:ui')
         androidTestImplementation project(":compose:ui:ui-test-junit4")
+        androidTestImplementation project(":compose:test-utils")
         androidTestImplementation "androidx.fragment:fragment:1.3.0"
         androidTestImplementation project(":activity:activity-compose")
         androidTestImplementation(ANDROIDX_TEST_UIAUTOMATOR)
@@ -94,6 +95,7 @@
             androidAndroidTest.dependencies {
                 implementation project(':compose:ui:ui')
                 implementation project(":compose:ui:ui-test-junit4")
+                implementation project(":compose:test-utils")
                 implementation "androidx.fragment:fragment:1.3.0"
                 implementation project(":activity:activity-compose")
                 implementation(ANDROIDX_TEST_UIAUTOMATOR)
diff --git a/compose/runtime/runtime/integration-tests/build.gradle b/compose/runtime/runtime/integration-tests/build.gradle
index 78faeb9..7d12b90 100644
--- a/compose/runtime/runtime/integration-tests/build.gradle
+++ b/compose/runtime/runtime/integration-tests/build.gradle
@@ -35,6 +35,7 @@
         androidTestImplementation(project(":compose:ui:ui"))
         androidTestImplementation(project(":compose:ui:ui-test-junit4"))
         androidTestImplementation(project(":compose:runtime:runtime"))
+        androidTestImplementation(project(":compose:test-utils"))
         androidTestImplementation(project(":activity:activity-compose"))
 
         androidTestImplementation(JUNIT)
@@ -90,6 +91,7 @@
             androidAndroidTest.dependencies {
                 implementation(project(":compose:ui:ui"))
                 implementation(project(":compose:ui:ui-test-junit4"))
+                implementation(project(":compose:test-utils"))
                 implementation(project(":activity:activity-compose"))
                 implementation(ANDROIDX_TEST_EXT_JUNIT)
                 implementation(ANDROIDX_TEST_RULES)
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Composer.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Composer.kt
index d4ba000..2570c64 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Composer.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Composer.kt
@@ -22,6 +22,7 @@
 
 import androidx.compose.runtime.collection.IdentityArraySet
 import androidx.compose.runtime.collection.IdentityScopeMap
+import androidx.compose.runtime.snapshots.currentSnapshot
 import androidx.compose.runtime.snapshots.fastForEach
 import androidx.compose.runtime.snapshots.fastMap
 import androidx.compose.runtime.snapshots.fastToSet
@@ -954,6 +955,7 @@
     private var providersInvalid = false
     private val providersInvalidStack = IntStack()
     private var childrenComposing: Int = 0
+    private var snapshot = currentSnapshot()
 
     private val invalidateStack = Stack<RecomposeScopeImpl>()
 
@@ -2477,11 +2479,13 @@
             val scope = RecomposeScopeImpl(composition as CompositionImpl)
             invalidateStack.push(scope)
             updateValue(scope)
+            scope.start(snapshot.id)
         } else {
             val invalidation = invalidations.removeLocation(reader.parent)
             val scope = reader.next() as RecomposeScopeImpl
             scope.requiresRecompose = invalidation != null
             invalidateStack.push(scope)
+            scope.start(snapshot.id)
         }
     }
 
@@ -2498,6 +2502,9 @@
         val scope = if (invalidateStack.isNotEmpty()) invalidateStack.pop()
         else null
         scope?.requiresRecompose = false
+        scope?.end(snapshot.id)?.let {
+            record { _, _, _ -> it(composition) }
+        }
         val result = if (scope != null && (scope.used || collectParameterInformation)) {
             if (scope.anchor == null) {
                 scope.anchor = if (inserting) {
@@ -2547,6 +2554,7 @@
         check(!isComposing) { "Reentrant composition is not supported" }
         trace("Compose:recompose") {
             invalidations.clear()
+            snapshot = currentSnapshot()
             invalidationsRequested.forEach { scope ->
                 val location = scope.anchor?.location ?: return
                 invalidations.add(Invalidation(scope, location))
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Composition.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Composition.kt
index d6672f2..153ba94 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Composition.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Composition.kt
@@ -527,6 +527,7 @@
             composer.currentRecomposeScope?.let {
                 it.used = true
                 observations.add(value, it)
+                it.recordRead(value)
             }
         }
     }
@@ -617,6 +618,10 @@
         return if (isComposing) InvalidationResult.DEFERRED else InvalidationResult.SCHEDULED
     }
 
+    internal fun removeObservation(instance: Any, scope: RecomposeScopeImpl) {
+        observations.remove(instance, scope)
+    }
+
     /**
      * This takes ownership of the invalidations. Invalidations
      */
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/RecomposeScopeImpl.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/RecomposeScopeImpl.kt
index 6b14571..031000f 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/RecomposeScopeImpl.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/RecomposeScopeImpl.kt
@@ -16,6 +16,8 @@
 
 package androidx.compose.runtime
 
+import androidx.compose.runtime.collection.IdentityArrayIntMap
+
 /**
  * Represents a recomposable scope or section of the composition hierarchy. Can be used to
  * manually invalidate the scope to schedule it for recomposition.
@@ -117,4 +119,53 @@
      * and implements [ScopeUpdateScope].
      */
     override fun updateScope(block: (Composer, Int) -> Unit) { this.block = block }
-}
\ No newline at end of file
+
+    private var currentToken = 0
+    private var trackedInstances: IdentityArrayIntMap? = null
+
+    /**
+     * Called when composition start composing into this scope. The [token] is a value that is
+     * unique everytime this is called. This is currently the snapshot id but that shouldn't be
+     * relied on.
+     */
+    fun start(token: Int) { currentToken = token }
+
+    /**
+     * Track instances that were read in scope.
+     */
+    fun recordRead(instance: Any) {
+        (trackedInstances ?: IdentityArrayIntMap().also { trackedInstances = it })
+            .add(instance, currentToken)
+    }
+
+    /**
+     * Called when composition is completed for this scope. The [token] is the same token passed
+     * in the previous call to [start]. If [end] returns a non-null value the lambda returned
+     * will be called during [ControlledComposition.applyChanges].
+     */
+    fun end(token: Int): ((Composition) -> Unit)? {
+        return trackedInstances?.let { instances ->
+            // If any value previous observed was not read in this current composition
+            // schedule the value to be removed from the observe scope and removed from the
+            // observations tracked by the composition.
+            // [used] is false if the scope was skipped. If the scope was skipped we should
+            // leave the observations unmodified.
+            if (
+                used && instances.any { _, instanceToken -> instanceToken != token }
+            ) { composition ->
+                if (
+                    currentToken == token && instances == trackedInstances &&
+                    composition is CompositionImpl
+                ) {
+                    instances.removeValueIf { instance, instanceToken ->
+                        (instanceToken != token).also { remove ->
+                            if (remove)
+                                composition.removeObservation(instance, this)
+                        }
+                    }
+                    if (instances.size == 0) trackedInstances = null
+                }
+            } else null
+        }
+    }
+}
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/collection/IdentityArrayIntMap.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/collection/IdentityArrayIntMap.kt
new file mode 100644
index 0000000..420e107
--- /dev/null
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/collection/IdentityArrayIntMap.kt
@@ -0,0 +1,211 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.runtime.collection
+
+import androidx.compose.runtime.identityHashCode
+import kotlin.contracts.ExperimentalContracts
+
+@OptIn(ExperimentalContracts::class)
+internal class IdentityArrayIntMap {
+    @PublishedApi
+    internal var size = 0
+
+    @PublishedApi
+    internal var keys: Array<Any?> = arrayOfNulls(4)
+
+    @PublishedApi
+    internal var values: IntArray = IntArray(4)
+
+    operator fun get(key: Any): Int {
+        val index = find(key)
+        return if (index >= 0) values[index] else error("Key not found")
+    }
+    /**
+     * Add [value] to the set and return `true` if it was added or `false` if it already existed.
+     */
+    fun add(key: Any, value: Int) {
+        val index: Int
+        if (size > 0) {
+            index = find(key)
+            if (index >= 0) {
+                values[index] = value
+                return
+            }
+        } else {
+            index = -1
+        }
+
+        val insertIndex = -(index + 1)
+
+        if (size == keys.size) {
+            val newKeys = arrayOfNulls<Any>(keys.size * 2)
+            val newValues = IntArray(keys.size * 2)
+            keys.copyInto(
+                destination = newKeys,
+                destinationOffset = insertIndex + 1,
+                startIndex = insertIndex,
+                endIndex = size
+            )
+            values.copyInto(
+                destination = newValues,
+                destinationOffset = insertIndex + 1,
+                startIndex = insertIndex,
+                endIndex = size
+            )
+            keys.copyInto(
+                destination = newKeys,
+                endIndex = insertIndex
+            )
+            values.copyInto(
+                destination = newValues,
+                endIndex = insertIndex
+            )
+            keys = newKeys
+            values = newValues
+        } else {
+            keys.copyInto(
+                destination = keys,
+                destinationOffset = insertIndex + 1,
+                startIndex = insertIndex,
+                endIndex = size
+            )
+            values.copyInto(
+                destination = values,
+                destinationOffset = insertIndex + 1,
+                startIndex = insertIndex,
+                endIndex = size
+            )
+        }
+        keys[insertIndex] = key
+        values[insertIndex] = value
+        size++
+    }
+
+    /**
+     * Remove [key] from the map.
+     */
+    fun remove(key: Any): Boolean {
+        val index = find(key)
+        if (index >= 0) {
+            if (index < size - 1) {
+                keys.copyInto(
+                    destination = keys,
+                    destinationOffset = index,
+                    startIndex = index + 1,
+                    endIndex = size
+                )
+                values.copyInto(
+                    destination = values,
+                    destinationOffset = index,
+                    startIndex = index + 1,
+                    endIndex = size
+                )
+            }
+            size--
+            keys[size] = null
+            return true
+        }
+        return false
+    }
+
+    /**
+     * Removes all values that match [predicate].
+     */
+    inline fun removeValueIf(predicate: (Any, Int) -> Boolean) {
+        var destinationIndex = 0
+        for (i in 0 until size) {
+            @Suppress("UNCHECKED_CAST")
+            val key = keys[i] as Any
+            val value = values[i]
+            if (!predicate(key, value)) {
+                if (destinationIndex != i) {
+                    keys[destinationIndex] = key
+                    values[destinationIndex] = value
+                }
+                destinationIndex++
+            }
+        }
+        for (i in destinationIndex until size) {
+            keys[i] = null
+        }
+        size = destinationIndex
+    }
+
+    inline fun any(predicate: (Any, Int) -> Boolean): Boolean {
+        for (i in 0 until size) {
+            if (predicate(keys[i] as Any, values[i])) return true
+        }
+        return false
+    }
+
+    /**
+     * Returns the index of [key] in the set or the negative index - 1 of the location where
+     * it would have been if it had been in the set.
+     */
+    private fun find(key: Any?): Int {
+        var low = 0
+        var high = size - 1
+        val valueIdentity = identityHashCode(key)
+
+        while (low <= high) {
+            val mid = (low + high).ushr(1)
+            val midVal = keys[mid]
+            val comparison = identityHashCode(midVal) - valueIdentity
+            when {
+                comparison < 0 -> low = mid + 1
+                comparison > 0 -> high = mid - 1
+                midVal === key -> return mid
+                else -> return findExactIndex(mid, key, valueIdentity)
+            }
+        }
+        return -(low + 1)
+    }
+
+    /**
+     * When multiple items share the same [identityHashCode], then we must find the specific
+     * index of the target item. This method assumes that [midIndex] has already been checked
+     * for an exact match for [value], but will look at nearby values to find the exact item index.
+     * If no match is found, the negative index - 1 of the position in which it would be will
+     * be returned, which is always after the last item with the same [identityHashCode].
+     */
+    private fun findExactIndex(midIndex: Int, value: Any?, valueHash: Int): Int {
+        // hunt down first
+        for (i in midIndex - 1 downTo 0) {
+            val v = keys[i]
+            if (v === value) {
+                return i
+            }
+            if (identityHashCode(v) != valueHash) {
+                break // we've gone too far
+            }
+        }
+
+        for (i in midIndex + 1 until size) {
+            val v = keys[i]
+            if (v === value) {
+                return i
+            }
+            if (identityHashCode(v) != valueHash) {
+                // We've gone too far. We should insert here.
+                return -(i + 1)
+            }
+        }
+
+        // We should insert at the end
+        return -(size + 1)
+    }
+}
\ No newline at end of file
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/snapshots/SnapshotIdSet.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/snapshots/SnapshotIdSet.kt
index 0416373..8c5502f 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/snapshots/SnapshotIdSet.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/snapshots/SnapshotIdSet.kt
@@ -310,15 +310,15 @@
         base += 32
         b = b shr 32
     }
-    if (bits and 0xFFFF == 0L) {
+    if (b and 0xFFFF == 0L) {
         base += 16
         b = b shr 16
     }
-    if (bits and 0xFF == 0L) {
+    if (b and 0xFF == 0L) {
         base += 8
         b = b shr 8
     }
-    if (bits and 0xF == 0L) {
+    if (b and 0xF == 0L) {
         base += 4
         b = b shr 4
     }
diff --git a/compose/runtime/runtime/src/test/kotlin/androidx/compose/runtime/CompositionTests.kt b/compose/runtime/runtime/src/test/kotlin/androidx/compose/runtime/CompositionTests.kt
index 9ca8a64..8f86427 100644
--- a/compose/runtime/runtime/src/test/kotlin/androidx/compose/runtime/CompositionTests.kt
+++ b/compose/runtime/runtime/src/test/kotlin/androidx/compose/runtime/CompositionTests.kt
@@ -45,6 +45,7 @@
 import kotlinx.coroutines.coroutineScope
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.test.runBlockingTest
+import kotlin.random.Random
 import kotlin.test.Test
 import kotlin.test.assertEquals
 import kotlin.test.assertFalse
@@ -2479,6 +2480,42 @@
         expectNoChanges()
     }
 
+    @Suppress("UnrememberedMutableState")
+    @Composable
+    fun Indirect(iteration: Int, states: MutableList<MutableState<Int>>) {
+        val state = mutableStateOf(Random.nextInt())
+        states.add(state)
+        Text("$iteration state = ${state.value}")
+    }
+
+    @Composable
+    fun ComposeIndirect(iteration: State<Int>, states: MutableList<MutableState<Int>>) {
+        Text("Iteration ${iteration.value}")
+        Indirect(iteration.value, states)
+    }
+
+    @Test // Regression b/182822837
+    fun testObservationScopes_IndirectInvalidate() = compositionTest {
+        val states = mutableListOf<MutableState<Int>>()
+        val iteration = mutableStateOf(0)
+
+        compose {
+            ComposeIndirect(iteration, states)
+        }
+
+        fun nextIteration() = iteration.value++
+        fun invalidateLast() = states.last().value++
+        fun invalidateFirst() = states.first().value++
+
+        repeat(10) {
+            nextIteration()
+            expectChanges()
+        }
+
+        invalidateFirst()
+        expectNoChanges()
+    }
+
     @OptIn(ComposeCompilerApi::class)
     @Test
     fun testApplierBeginEndCallbacks() = compositionTest {
diff --git a/compose/runtime/runtime/src/test/kotlin/androidx/compose/runtime/collection/IdentityArrayIntMapTests.kt b/compose/runtime/runtime/src/test/kotlin/androidx/compose/runtime/collection/IdentityArrayIntMapTests.kt
new file mode 100644
index 0000000..03a3e88
--- /dev/null
+++ b/compose/runtime/runtime/src/test/kotlin/androidx/compose/runtime/collection/IdentityArrayIntMapTests.kt
@@ -0,0 +1,105 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.runtime.collection
+
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertFalse
+import kotlin.test.assertTrue
+
+class IdentityArrayIntMapTests {
+
+    @Test
+    fun emptyConstruction() {
+        val m = IdentityArrayIntMap()
+        assertEquals(0, m.size)
+    }
+
+    @Test
+    fun canAddValues() {
+        val map = IdentityArrayIntMap()
+        val keys = Array<Any>(100) { Any() }
+        for (i in keys.indices) {
+            map.add(keys[i], i)
+        }
+        for (i in keys.indices) {
+            assertEquals(i, map[keys[i]])
+        }
+        map.removeValueIf { key, value ->
+            assertEquals(keys[value], key)
+            false
+        }
+    }
+
+    @Test
+    fun canRemoveValues() {
+        val map = IdentityArrayIntMap()
+        val keys = Array<Any>(100) { Any() }
+        for (i in keys.indices) {
+            map.add(keys[i], i)
+        }
+        for (i in keys.indices step 2) {
+            map.remove(keys[i])
+        }
+        assertEquals(50, map.size)
+        map.removeValueIf { key, value ->
+            assertEquals(keys[value], key)
+            assertTrue(value % 2 == 1)
+            false
+        }
+    }
+
+    @Test
+    fun canRemoveIfValues() {
+        val map = IdentityArrayIntMap()
+        val keys = Array<Any>(100) { Any() }
+        for (i in keys.indices) {
+            map.add(keys[i], i)
+        }
+        map.removeValueIf { _, value -> value % 2 == 0 }
+        assertEquals(50, map.size)
+    }
+
+    @Test
+    fun canReplaceValues() {
+        val map = IdentityArrayIntMap()
+        val keys = Array<Any>(100) { Any() }
+        for (i in keys.indices) {
+            map.add(keys[i], i)
+        }
+
+        for (i in keys.indices) {
+            map.add(keys[i], i + 100)
+        }
+
+        assertEquals(100, map.size)
+        for (i in keys.indices) {
+            assertEquals(i + 100, map[keys[i]])
+        }
+    }
+
+    @Test
+    fun anyFindsCorrectValue() {
+        val map = IdentityArrayIntMap()
+        val keys = Array<Any>(100) { Any() }
+        for (i in keys.indices) {
+            map.add(keys[i], i)
+        }
+        assertTrue(map.any { _, value -> value == 20 })
+        assertFalse(map.any { _, value -> value > 100 })
+    }
+}
\ No newline at end of file
diff --git a/compose/runtime/runtime/src/test/kotlin/androidx/compose/runtime/snapshots/SnapshotIdSetTests.kt b/compose/runtime/runtime/src/test/kotlin/androidx/compose/runtime/snapshots/SnapshotIdSetTests.kt
index 3b8999a..848bcb2 100644
--- a/compose/runtime/runtime/src/test/kotlin/androidx/compose/runtime/snapshots/SnapshotIdSetTests.kt
+++ b/compose/runtime/runtime/src/test/kotlin/androidx/compose/runtime/snapshots/SnapshotIdSetTests.kt
@@ -269,6 +269,18 @@
         test(1024)
     }
 
+    @Test // Regression b/182822837
+    fun shouldReportTheCorrectLowest() {
+        fun test(number: Int) {
+            val set = SnapshotIdSet.EMPTY.set(number)
+            assertEquals(number, set.lowest(-1))
+        }
+
+        repeat(64) {
+            test(it)
+        }
+    }
+
     @Test // Regression: b/147836978
     fun shouldValidWhenSetIsLarge() {
         val data = """
diff --git a/compose/test-utils/src/androidMain/AndroidManifest.xml b/compose/test-utils/src/androidMain/AndroidManifest.xml
index 5e81de1..33068f3 100644
--- a/compose/test-utils/src/androidMain/AndroidManifest.xml
+++ b/compose/test-utils/src/androidMain/AndroidManifest.xml
@@ -14,4 +14,11 @@
      limitations under the License.
 -->
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="androidx.compose.testutils"/>
+    package="androidx.compose.testutils">
+    <application>
+        <activity
+            android:name="androidx.activity.ComponentActivity"
+            android:theme="@style/TestTheme"
+            android:exported="true" />
+    </application>
+</manifest>
diff --git a/compose/ui/ui-test/src/androidMain/res/values/styles.xml b/compose/test-utils/src/androidMain/res/values/styles.xml
similarity index 100%
rename from compose/ui/ui-test/src/androidMain/res/values/styles.xml
rename to compose/test-utils/src/androidMain/res/values/styles.xml
diff --git a/compose/test-utils/src/commonMain/kotlin/androidx/compose/testutils/ComposeTestCase.kt b/compose/test-utils/src/commonMain/kotlin/androidx/compose/testutils/ComposeTestCase.kt
index f45d284..ed01647 100644
--- a/compose/test-utils/src/commonMain/kotlin/androidx/compose/testutils/ComposeTestCase.kt
+++ b/compose/test-utils/src/commonMain/kotlin/androidx/compose/testutils/ComposeTestCase.kt
@@ -71,7 +71,5 @@
      * The lifecycle rules for this method are same as for [Content]
      */
     @Composable
-    fun ContentWrappers(content: @Composable () -> Unit) {
-        content()
-    }
+    fun ContentWrappers(content: @Composable () -> Unit)
 }
\ No newline at end of file
diff --git a/compose/ui/ui-graphics/benchmark/src/main/java/androidx/compose/ui/graphics/benchmark/ImageVectorTestCase.kt b/compose/ui/ui-graphics/benchmark/src/main/java/androidx/compose/ui/graphics/benchmark/ImageVectorTestCase.kt
index 5b89155..e7e5175 100644
--- a/compose/ui/ui-graphics/benchmark/src/main/java/androidx/compose/ui/graphics/benchmark/ImageVectorTestCase.kt
+++ b/compose/ui/ui-graphics/benchmark/src/main/java/androidx/compose/ui/graphics/benchmark/ImageVectorTestCase.kt
@@ -29,7 +29,7 @@
 import androidx.compose.ui.graphics.vector.PathData
 import androidx.compose.ui.graphics.vector.ImageVector
 import androidx.compose.foundation.layout.size
-import androidx.compose.testutils.LayeredComposeTestCase
+import androidx.compose.testutils.ComposeTestCase
 import androidx.compose.ui.graphics.vector.rememberVectorPainter
 import androidx.compose.ui.res.painterResource
 import androidx.compose.ui.unit.dp
@@ -40,21 +40,16 @@
  * Subclasses are responsible for providing the vector asset, so we can test and benchmark different
  * methods of loading / creating this asset.
  */
-sealed class ImageVectorTestCase : LayeredComposeTestCase {
+sealed class ImageVectorTestCase : ComposeTestCase {
 
     @Composable
-    override fun MeasuredContent() {
-        Box(
-            Modifier.testTag(testTag)
-                .size(24.dp)
-                .paint(getPainter())
-        )
-    }
-
-    @Composable
-    override fun ContentWrappers(content: @Composable () -> Unit) {
+    override fun Content() {
         Box {
-            content()
+            Box(
+                Modifier.testTag(testTag)
+                    .size(24.dp)
+                    .paint(getPainter())
+            )
         }
     }
 
diff --git a/compose/ui/ui-lint/src/main/java/androidx/compose/ui/lint/ModifierDeclarationDetector.kt b/compose/ui/ui-lint/src/main/java/androidx/compose/ui/lint/ModifierDeclarationDetector.kt
index 1797d44..4e01e22 100644
--- a/compose/ui/ui-lint/src/main/java/androidx/compose/ui/lint/ModifierDeclarationDetector.kt
+++ b/compose/ui/ui-lint/src/main/java/androidx/compose/ui/lint/ModifierDeclarationDetector.kt
@@ -21,6 +21,7 @@
 import androidx.compose.lint.Names
 import androidx.compose.lint.inheritsFrom
 import androidx.compose.lint.isComposable
+import androidx.compose.lint.toKmFunction
 import androidx.compose.ui.lint.ModifierDeclarationDetector.Companion.ComposableModifierFactory
 import androidx.compose.ui.lint.ModifierDeclarationDetector.Companion.ModifierFactoryReturnType
 import com.android.tools.lint.client.api.UElementHandler
@@ -35,6 +36,8 @@
 import com.android.tools.lint.detector.api.SourceCodeScanner
 import com.intellij.psi.PsiClass
 import com.intellij.psi.PsiType
+import com.intellij.psi.impl.compiled.ClsMethodImpl
+import kotlinx.metadata.KmClassifier
 import org.jetbrains.kotlin.psi.KtCallableDeclaration
 import org.jetbrains.kotlin.psi.KtDeclarationWithBody
 import org.jetbrains.kotlin.psi.KtFunction
@@ -43,9 +46,15 @@
 import org.jetbrains.kotlin.psi.KtPropertyAccessor
 import org.jetbrains.kotlin.psi.KtUserType
 import org.jetbrains.kotlin.psi.psiUtil.containingClass
+import org.jetbrains.uast.UCallExpression
 import org.jetbrains.uast.UMethod
+import org.jetbrains.uast.UThisExpression
+import org.jetbrains.uast.UTypeReferenceExpression
+import org.jetbrains.uast.getContainingUClass
+import org.jetbrains.uast.resolveToUElement
 import org.jetbrains.uast.toUElement
 import org.jetbrains.uast.tryResolve
+import org.jetbrains.uast.visitor.AbstractUastVisitor
 import java.util.EnumSet
 
 /**
@@ -56,6 +65,8 @@
  * chaining
  * - Modifier factory functions should not be marked as @Composable, and should use `composed`
  * instead
+ * - Modifier factory functions should reference the receiver parameter inside their body to make
+ * sure they don't drop old Modifiers in the chain
  */
 class ModifierDeclarationDetector : Detector(), SourceCodeScanner {
     override fun getApplicableUastTypes() = listOf(UMethod::class.java)
@@ -133,6 +144,25 @@
                 EnumSet.of(Scope.JAVA_FILE, Scope.TEST_SOURCES)
             )
         )
+
+        val ModifierFactoryUnreferencedReceiver = Issue.create(
+            "ModifierFactoryUnreferencedReceiver",
+            "Modifier factory functions must use the receiver Modifier instance",
+            "Modifier factory functions are fluently chained to construct a chain of " +
+                "Modifier objects that will be applied to a layout. As a result, each factory " +
+                "function *must* use the receiver `Modifier` parameter, to ensure that the " +
+                "function is returning a chain that includes previous items in the chain. Make " +
+                "sure the returned chain either explicitly includes `this`, such as " +
+                "`return this.then(MyModifier)` or implicitly by returning a chain that starts " +
+                "with an implicit call to another factory function, such as " +
+                "`return myModifier()`, where `myModifier` is defined as " +
+                "`fun Modifier.myModifier(): Modifier`.",
+            Category.CORRECTNESS, 3, Severity.ERROR,
+            Implementation(
+                ModifierDeclarationDetector::class.java,
+                EnumSet.of(Scope.JAVA_FILE, Scope.TEST_SOURCES)
+            )
+        )
     }
 }
 
@@ -238,11 +268,84 @@
                     .autoFix()
                     .build()
             )
+        } else {
+            // Ignore interface / abstract methods with no body
+            if (uastBody != null) {
+                ensureReceiverIsReferenced(context)
+            }
         }
     }
 }
 
 /**
+ * See [ModifierDeclarationDetector.ModifierFactoryUnreferencedReceiver]
+ */
+private fun UMethod.ensureReceiverIsReferenced(context: JavaContext) {
+    var isReceiverReferenced = false
+    accept(object : AbstractUastVisitor() {
+        /**
+         * If there is no receiver on the call, but the call has a Modifier receiver
+         * type, then the call is implicitly using the Modifier receiver
+         * TODO: consider checking for nested receivers, in case the implicit
+         *  receiver is an inner scope, and not the outer Modifier receiver
+         */
+        override fun visitCallExpression(node: UCallExpression): Boolean {
+            // We account for a receiver of `this` in `visitThisExpression`
+            if (node.receiver == null) {
+                val declaration = node.resolveToUElement()
+                // If the declaration is a member of `Modifier` (such as `then`)
+                if (declaration?.getContainingUClass()
+                    ?.qualifiedName == Names.Ui.Modifier.javaFqn
+                ) {
+                    isReceiverReferenced = true
+                    // Otherwise if the declaration is an extension of `Modifier`
+                } else {
+                    // Whether the declaration itself has a Modifier receiver - UAST might think the
+                    // receiver on the node is different if it is inside another scope.
+                    val hasModifierReceiver = when (val source = declaration?.sourcePsi) {
+                        // Parsing a method defined in a class file
+                        is ClsMethodImpl -> {
+                            val receiverClassifier = source.toKmFunction()
+                                ?.receiverParameterType?.classifier
+                            receiverClassifier == KmClassifier.Class(Names.Ui.Modifier.kmClassName)
+                        }
+                        // Parsing a method defined in Kotlin source
+                        is KtFunction -> {
+                            val receiver = source.receiverTypeReference
+                            (receiver.toUElement() as? UTypeReferenceExpression)
+                                ?.getQualifiedName() == Names.Ui.Modifier.javaFqn
+                        }
+                        else -> false
+                    }
+                    if (hasModifierReceiver) {
+                        isReceiverReferenced = true
+                    }
+                }
+            }
+            return isReceiverReferenced
+        }
+
+        /**
+         * If `this` is explicitly referenced, no error.
+         * TODO: consider checking for nested receivers, in case `this` refers to an
+         * inner scope, and not the outer Modifier receiver
+         */
+        override fun visitThisExpression(node: UThisExpression): Boolean {
+            isReceiverReferenced = true
+            return isReceiverReferenced
+        }
+    })
+    if (!isReceiverReferenced) {
+        context.report(
+            ModifierDeclarationDetector.ModifierFactoryUnreferencedReceiver,
+            this,
+            context.getNameLocation(this),
+            "Modifier factory functions must use the receiver Modifier instance"
+        )
+    }
+}
+
+/**
  * @see [ModifierDeclarationDetector.ModifierFactoryReturnType]
  */
 private fun UMethod.checkReturnType(context: JavaContext, returnType: PsiType) {
diff --git a/compose/ui/ui-lint/src/main/java/androidx/compose/ui/lint/UiIssueRegistry.kt b/compose/ui/ui-lint/src/main/java/androidx/compose/ui/lint/UiIssueRegistry.kt
index 181977f..1d834e7 100644
--- a/compose/ui/ui-lint/src/main/java/androidx/compose/ui/lint/UiIssueRegistry.kt
+++ b/compose/ui/ui-lint/src/main/java/androidx/compose/ui/lint/UiIssueRegistry.kt
@@ -30,6 +30,7 @@
         ModifierDeclarationDetector.ComposableModifierFactory,
         ModifierDeclarationDetector.ModifierFactoryExtensionFunction,
         ModifierDeclarationDetector.ModifierFactoryReturnType,
+        ModifierDeclarationDetector.ModifierFactoryUnreferencedReceiver,
         ModifierParameterDetector.ModifierParameter
     )
 }
diff --git a/compose/ui/ui-lint/src/test/java/androidx/compose/ui/lint/ModifierDeclarationDetectorTest.kt b/compose/ui/ui-lint/src/test/java/androidx/compose/ui/lint/ModifierDeclarationDetectorTest.kt
index 33aac1f..e3be716 100644
--- a/compose/ui/ui-lint/src/test/java/androidx/compose/ui/lint/ModifierDeclarationDetectorTest.kt
+++ b/compose/ui/ui-lint/src/test/java/androidx/compose/ui/lint/ModifierDeclarationDetectorTest.kt
@@ -40,6 +40,7 @@
             ModifierDeclarationDetector.ComposableModifierFactory,
             ModifierDeclarationDetector.ModifierFactoryExtensionFunction,
             ModifierDeclarationDetector.ModifierFactoryReturnType,
+            ModifierDeclarationDetector.ModifierFactoryUnreferencedReceiver
         )
 
     @Test
@@ -54,7 +55,7 @@
                 object TestModifier : Modifier.Element
 
                 fun Modifier.fooModifier(): Modifier.Element {
-                    return TestModifier
+                    return this.then(TestModifier)
                 }
             """
             ),
@@ -91,14 +92,14 @@
                 object TestModifier : Modifier.Element
 
                 val Modifier.fooModifier get(): Modifier.Element {
-                    return TestModifier
+                    return this.then(TestModifier)
                 }
 
                 val Modifier.fooModifier2: Modifier.Element get() {
-                    return TestModifier
+                    return this.then(TestModifier)
                 }
 
-                val Modifier.fooModifier3: Modifier.Element get() = TestModifier
+                val Modifier.fooModifier3: Modifier.Element get() = this.then(TestModifier)
             """
             ),
             kotlin(Stubs.Modifier)
@@ -113,7 +114,7 @@
                 val Modifier.fooModifier2: Modifier.Element get() {
                                                             ~~~
 src/androidx/compose/ui/foo/TestModifier.kt:16: Warning: Modifier factory functions should have a return type of Modifier [ModifierFactoryReturnType]
-                val Modifier.fooModifier3: Modifier.Element get() = TestModifier
+                val Modifier.fooModifier3: Modifier.Element get() = this.then(TestModifier)
                                                             ~~~
 0 errors, 3 warnings
             """
@@ -130,8 +131,8 @@
 +                 val Modifier.fooModifier2: Modifier get() {
 Fix for src/androidx/compose/ui/foo/TestModifier.kt line 16: Change return type to Modifier:
 @@ -16 +16
--                 val Modifier.fooModifier3: Modifier.Element get() = TestModifier
-+                 val Modifier.fooModifier3: Modifier get() = TestModifier
+-                 val Modifier.fooModifier3: Modifier.Element get() = this.then(TestModifier)
++                 val Modifier.fooModifier3: Modifier get() = this.then(TestModifier)
             """
             )
     }
@@ -158,7 +159,10 @@
 src/androidx/compose/ui/foo/TestModifier.kt:8: Warning: Modifier factory functions should have a return type of Modifier [ModifierFactoryReturnType]
                 fun Modifier.fooModifier() = TestModifier
                              ~~~~~~~~~~~
-0 errors, 1 warnings
+src/androidx/compose/ui/foo/TestModifier.kt:8: Error: Modifier factory functions must use the receiver Modifier instance [ModifierFactoryUnreferencedReceiver]
+                fun Modifier.fooModifier() = TestModifier
+                             ~~~~~~~~~~~
+1 errors, 1 warnings
             """
             )
             .expectFixDiffs(
@@ -193,7 +197,10 @@
 src/androidx/compose/ui/foo/TestModifier.kt:8: Warning: Modifier factory functions should have a return type of Modifier [ModifierFactoryReturnType]
                 val Modifier.fooModifier get() = TestModifier
                                          ~~~
-0 errors, 1 warnings
+src/androidx/compose/ui/foo/TestModifier.kt:8: Error: Modifier factory functions must use the receiver Modifier instance [ModifierFactoryUnreferencedReceiver]
+                val Modifier.fooModifier get() = TestModifier
+                                         ~~~
+1 errors, 1 warnings
             """
             )
             .expectFixDiffs(
@@ -218,7 +225,7 @@
                 object TestModifier : Modifier.Element
 
                 fun Modifier.fooModifier(): TestModifier {
-                    return TestModifier
+                    return this.then(TestModifier)
                 }
             """
             ),
@@ -330,15 +337,15 @@
                 object TestModifier : Modifier.Element
 
                 fun fooModifier(): Modifier {
-                    return TestModifier
+                    return this.then(TestModifier)
                 }
 
                 val fooModifier get(): Modifier {
-                    return TestModifier
+                    return this.then(TestModifier)
                 }
 
                 val fooModifier2: Modifier get() {
-                    return TestModifier
+                    return this.then(TestModifier)
                 }
 
                 val fooModifier3: Modifier get() = TestModifier
@@ -398,18 +405,18 @@
                 object TestModifier : Modifier.Element
 
                 fun TestModifier.fooModifier(): Modifier {
-                    return TestModifier
+                    return this.then(TestModifier)
                 }
 
                 val TestModifier.fooModifier get(): Modifier {
-                    return TestModifier
+                    return this.then(TestModifier)
                 }
 
                 val TestModifier.fooModifier2: Modifier get() {
-                    return TestModifier
+                    return this.then(TestModifier)
                 }
 
-                val TestModifier.fooModifier3: Modifier get() = TestModifier
+                val TestModifier.fooModifier3: Modifier get() = this.then(TestModifier)
             """
             ),
             kotlin(Stubs.Modifier)
@@ -427,7 +434,7 @@
                 val TestModifier.fooModifier2: Modifier get() {
                                                         ~~~
 src/androidx/compose/ui/foo/TestModifier.kt:20: Warning: Modifier factory functions should be extensions on Modifier [ModifierFactoryExtensionFunction]
-                val TestModifier.fooModifier3: Modifier get() = TestModifier
+                val TestModifier.fooModifier3: Modifier get() = this.then(TestModifier)
                                                         ~~~
 0 errors, 4 warnings
             """
@@ -448,8 +455,8 @@
 +                 val Modifier.fooModifier2: Modifier get() {
 Fix for src/androidx/compose/ui/foo/TestModifier.kt line 20: Change receiver to Modifier:
 @@ -20 +20
--                 val TestModifier.fooModifier3: Modifier get() = TestModifier
-+                 val Modifier.fooModifier3: Modifier get() = TestModifier
+-                 val TestModifier.fooModifier3: Modifier get() = this.then(TestModifier)
++                 val Modifier.fooModifier3: Modifier get() = this.then(TestModifier)
             """
             )
     }
@@ -472,20 +479,22 @@
                 @Composable
                 fun Modifier.fooModifier1(): Modifier {
                     val value = someComposableCall(3)
-                    return TestModifier(value)
+                    return this.then(TestModifier(value))
                 }
 
                 @Composable
-                fun Modifier.fooModifier2(): Modifier = TestModifier(someComposableCall(3))
+                fun Modifier.fooModifier2(): Modifier =
+                    this.then(TestModifier(someComposableCall(3)))
 
                 @get:Composable
                 val Modifier.fooModifier3: Modifier get() {
                     val value = someComposableCall(3)
-                    return TestModifier(value)
+                    return this.then(TestModifier(value))
                 }
 
                 @get:Composable
-                val Modifier.fooModifier4: Modifier get() = TestModifier(someComposableCall(3))
+                val Modifier.fooModifier4: Modifier get() =
+                    this.then(TestModifier(someComposableCall(3)))
             """
             ),
             kotlin(Stubs.Modifier),
@@ -498,13 +507,13 @@
                 fun Modifier.fooModifier1(): Modifier {
                              ~~~~~~~~~~~~
 src/androidx/compose/ui/foo/TestModifier.kt:19: Warning: Modifier factory functions should not be marked as @Composable, and should use composed instead [ComposableModifierFactory]
-                fun Modifier.fooModifier2(): Modifier = TestModifier(someComposableCall(3))
+                fun Modifier.fooModifier2(): Modifier =
                              ~~~~~~~~~~~~
-src/androidx/compose/ui/foo/TestModifier.kt:22: Warning: Modifier factory functions should not be marked as @Composable, and should use composed instead [ComposableModifierFactory]
+src/androidx/compose/ui/foo/TestModifier.kt:23: Warning: Modifier factory functions should not be marked as @Composable, and should use composed instead [ComposableModifierFactory]
                 val Modifier.fooModifier3: Modifier get() {
                                                     ~~~
-src/androidx/compose/ui/foo/TestModifier.kt:28: Warning: Modifier factory functions should not be marked as @Composable, and should use composed instead [ComposableModifierFactory]
-                val Modifier.fooModifier4: Modifier get() = TestModifier(someComposableCall(3))
+src/androidx/compose/ui/foo/TestModifier.kt:29: Warning: Modifier factory functions should not be marked as @Composable, and should use composed instead [ComposableModifierFactory]
+                val Modifier.fooModifier4: Modifier get() =
                                                     ~~~
 0 errors, 4 warnings
             """
@@ -517,26 +526,105 @@
 -                 fun Modifier.fooModifier1(): Modifier {
 +                 fun Modifier.fooModifier1(): Modifier = composed {
 @@ -15 +14
--                     return TestModifier(value)
-+                     TestModifier(value)
+-                     return this.then(TestModifier(value))
++                     this.then(TestModifier(value))
 Fix for src/androidx/compose/ui/foo/TestModifier.kt line 19: Replace @Composable with composed call:
 @@ -18 +18
 -                 @Composable
--                 fun Modifier.fooModifier2(): Modifier = TestModifier(someComposableCall(3))
-+                 fun Modifier.fooModifier2(): Modifier = composed { TestModifier(someComposableCall(3)) }
-Fix for src/androidx/compose/ui/foo/TestModifier.kt line 22: Replace @Composable with composed call:
-@@ -21 +21
+@@ -20 +19
+-                     this.then(TestModifier(someComposableCall(3)))
++                     composed { this.then(TestModifier(someComposableCall(3))) }
+Fix for src/androidx/compose/ui/foo/TestModifier.kt line 23: Replace @Composable with composed call:
+@@ -22 +22
 -                 @get:Composable
 -                 val Modifier.fooModifier3: Modifier get() {
 +                 val Modifier.fooModifier3: Modifier get() = composed {
-@@ -24 +23
--                     return TestModifier(value)
-+                     TestModifier(value)
-Fix for src/androidx/compose/ui/foo/TestModifier.kt line 28: Replace @Composable with composed call:
-@@ -27 +27
+@@ -25 +24
+-                     return this.then(TestModifier(value))
++                     this.then(TestModifier(value))
+Fix for src/androidx/compose/ui/foo/TestModifier.kt line 29: Replace @Composable with composed call:
+@@ -28 +28
 -                 @get:Composable
--                 val Modifier.fooModifier4: Modifier get() = TestModifier(someComposableCall(3))
-+                 val Modifier.fooModifier4: Modifier get() = composed { TestModifier(someComposableCall(3)) }
+@@ -30 +29
+-                     this.then(TestModifier(someComposableCall(3)))
++                     composed { this.then(TestModifier(someComposableCall(3))) }
+            """
+            )
+    }
+
+    @Test
+    fun unreferencedReceiver() {
+        lint().files(
+            kotlin(
+                """
+                package androidx.compose.ui.foo
+
+                import androidx.compose.ui.*
+
+                object TestModifier : Modifier.Element
+
+                // Modifier factory without a receiver - since this has no receiver it should
+                // trigger an error if this is returned inside another factory function
+                fun testModifier(): Modifier = TestModifier
+
+                interface FooInterface {
+                    fun Modifier.fooModifier(): Modifier {
+                        return TestModifier
+                    }
+                }
+
+                fun Modifier.fooModifier(): Modifier {
+                    return TestModifier
+                }
+
+                fun Modifier.fooModifier2(): Modifier {
+                    return testModifier()
+                }
+
+                fun Modifier.fooModifier3(): Modifier = TestModifier
+
+                fun Modifier.fooModifier4(): Modifier = testModifier()
+
+                fun Modifier.fooModifier5(): Modifier {
+                    return Modifier.then(TestModifier)
+                }
+
+                fun Modifier.fooModifier6(): Modifier {
+                    return Modifier.fooModifier()
+                }
+            """
+            ),
+            kotlin(Stubs.Modifier),
+            kotlin(Stubs.Composable)
+        )
+            .run()
+            .expect(
+                """
+src/androidx/compose/ui/foo/TestModifier.kt:10: Warning: Modifier factory functions should be extensions on Modifier [ModifierFactoryExtensionFunction]
+                fun testModifier(): Modifier = TestModifier
+                    ~~~~~~~~~~~~
+src/androidx/compose/ui/foo/TestModifier.kt:13: Error: Modifier factory functions must use the receiver Modifier instance [ModifierFactoryUnreferencedReceiver]
+                    fun Modifier.fooModifier(): Modifier {
+                                 ~~~~~~~~~~~
+src/androidx/compose/ui/foo/TestModifier.kt:18: Error: Modifier factory functions must use the receiver Modifier instance [ModifierFactoryUnreferencedReceiver]
+                fun Modifier.fooModifier(): Modifier {
+                             ~~~~~~~~~~~
+src/androidx/compose/ui/foo/TestModifier.kt:22: Error: Modifier factory functions must use the receiver Modifier instance [ModifierFactoryUnreferencedReceiver]
+                fun Modifier.fooModifier2(): Modifier {
+                             ~~~~~~~~~~~~
+src/androidx/compose/ui/foo/TestModifier.kt:26: Error: Modifier factory functions must use the receiver Modifier instance [ModifierFactoryUnreferencedReceiver]
+                fun Modifier.fooModifier3(): Modifier = TestModifier
+                             ~~~~~~~~~~~~
+src/androidx/compose/ui/foo/TestModifier.kt:28: Error: Modifier factory functions must use the receiver Modifier instance [ModifierFactoryUnreferencedReceiver]
+                fun Modifier.fooModifier4(): Modifier = testModifier()
+                             ~~~~~~~~~~~~
+src/androidx/compose/ui/foo/TestModifier.kt:30: Error: Modifier factory functions must use the receiver Modifier instance [ModifierFactoryUnreferencedReceiver]
+                fun Modifier.fooModifier5(): Modifier {
+                             ~~~~~~~~~~~~
+src/androidx/compose/ui/foo/TestModifier.kt:34: Error: Modifier factory functions must use the receiver Modifier instance [ModifierFactoryUnreferencedReceiver]
+                fun Modifier.fooModifier6(): Modifier {
+                             ~~~~~~~~~~~~
+7 errors, 1 warnings
             """
             )
     }
@@ -553,7 +641,15 @@
                 object TestModifier : Modifier.Element
 
                 fun Modifier.fooModifier(): Modifier {
-                    return TestModifier
+                    return this.then(TestModifier)
+                }
+
+                fun Modifier.fooModifier2(): Modifier {
+                    return then(TestModifier)
+                }
+
+                fun Modifier.fooModifier3(): Modifier {
+                    return fooModifier()
                 }
             """
             ),
diff --git a/compose/ui/ui-test-junit4/src/androidAndroidTest/AndroidManifest.xml b/compose/ui/ui-test-junit4/src/androidAndroidTest/AndroidManifest.xml
index 1318d0a..ebf98af 100644
--- a/compose/ui/ui-test-junit4/src/androidAndroidTest/AndroidManifest.xml
+++ b/compose/ui/ui-test-junit4/src/androidAndroidTest/AndroidManifest.xml
@@ -17,8 +17,6 @@
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
     package="androidx.compose.ui.test.junit4">
     <application>
-        <activity android:name="androidx.activity.ComponentActivity"
-            android:theme="@style/TestTheme" />
         <activity android:name="androidx.compose.ui.test.junit4.CustomActivity"
             android:theme="@android:style/Theme.Material.NoActionBar.Fullscreen" />
         <activity android:name="androidx.compose.ui.test.junit4.MultipleActivitiesFindTest$Activity1" />
diff --git a/compose/ui/ui-test-junit4/src/androidMain/AndroidManifest.xml b/compose/ui/ui-test-junit4/src/androidMain/AndroidManifest.xml
index 87c86b4..deb7139 100644
--- a/compose/ui/ui-test-junit4/src/androidMain/AndroidManifest.xml
+++ b/compose/ui/ui-test-junit4/src/androidMain/AndroidManifest.xml
@@ -13,12 +13,4 @@
      See the License for the specific language governing permissions and
      limitations under the License.
 -->
-<manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="androidx.compose.ui.test.junit4">
-
-    <application>
-        <activity
-            android:name="androidx.activity.ComponentActivity"
-            android:theme="@style/TestTheme" />
-    </application>
-</manifest>
+<manifest package="androidx.compose.ui.test.junit4" />
diff --git a/compose/ui/ui-test-junit4/src/androidMain/res/values/styles.xml b/compose/ui/ui-test-junit4/src/androidMain/res/values/styles.xml
deleted file mode 100644
index 43e2d55..0000000
--- a/compose/ui/ui-test-junit4/src/androidMain/res/values/styles.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-<!--
-  Copyright 2020 The Android Open Source Project
-
-  Licensed under the Apache License, Version 2.0 (the "License");
-  you may not use this file except in compliance with the License.
-  You may obtain a copy of the License at
-
-       http://www.apache.org/licenses/LICENSE-2.0
-
-  Unless required by applicable law or agreed to in writing, software
-  distributed under the License is distributed on an "AS IS" BASIS,
-  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  See the License for the specific language governing permissions and
-  limitations under the License.
-  -->
-
-<resources>
-    <style name="TestTheme" parent="@android:style/Theme.Material.Light.NoActionBar">
-        <item name="android:windowActionBar">false</item>
-        <item name="android:windowAnimationStyle">@null</item>
-        <item name="android:windowContentOverlay">@null</item>
-        <item name="android:windowFullscreen">true</item>
-        <item name="android:windowNoTitle">true</item>
-    </style>
-</resources>
\ No newline at end of file
diff --git a/compose/ui/ui-test-manifest/src/main/AndroidManifest.xml b/compose/ui/ui-test-manifest/src/main/AndroidManifest.xml
index 833d681..c694ed4 100644
--- a/compose/ui/ui-test-manifest/src/main/AndroidManifest.xml
+++ b/compose/ui/ui-test-manifest/src/main/AndroidManifest.xml
@@ -17,6 +17,6 @@
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
     package="androidx.compose.ui.test.manifest">
     <application>
-        <activity android:name="androidx.activity.ComponentActivity" />
+        <activity android:name="androidx.activity.ComponentActivity" android:exported="true" />
     </application>
 </manifest>
diff --git a/compose/ui/ui-test/src/androidAndroidTest/AndroidManifest.xml b/compose/ui/ui-test/src/androidAndroidTest/AndroidManifest.xml
index ac695d6..40c2ea8 100644
--- a/compose/ui/ui-test/src/androidAndroidTest/AndroidManifest.xml
+++ b/compose/ui/ui-test/src/androidAndroidTest/AndroidManifest.xml
@@ -17,8 +17,6 @@
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
     package="androidx.compose.ui.test">
     <application>
-        <activity android:name="androidx.activity.ComponentActivity"
-            android:theme="@style/TestTheme" />
         <activity android:name="androidx.compose.ui.test.ActivityWithActionBar" />
         <activity android:name="androidx.compose.ui.test.ClickCounterActivity" />
         <activity android:name="androidx.compose.ui.test.EmptyActivity" />
diff --git a/compose/ui/ui-test/src/androidMain/AndroidManifest.xml b/compose/ui/ui-test/src/androidMain/AndroidManifest.xml
index f5a2402..0d3bc8e 100644
--- a/compose/ui/ui-test/src/androidMain/AndroidManifest.xml
+++ b/compose/ui/ui-test/src/androidMain/AndroidManifest.xml
@@ -13,12 +13,4 @@
      See the License for the specific language governing permissions and
      limitations under the License.
 -->
-<manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="androidx.compose.ui.test">
-
-    <application>
-        <activity
-            android:name="androidx.activity.ComponentActivity"
-            android:theme="@style/TestTheme" />
-    </application>
-</manifest>
+<manifest package="androidx.compose.ui.test" />
diff --git a/compose/ui/ui-tooling-data/src/androidTest/java/AndroidManifest.xml b/compose/ui/ui-tooling-data/src/androidTest/AndroidManifest.xml
similarity index 86%
rename from compose/ui/ui-tooling-data/src/androidTest/java/AndroidManifest.xml
rename to compose/ui/ui-tooling-data/src/androidTest/AndroidManifest.xml
index 09f8b59..b2770d0 100644
--- a/compose/ui/ui-tooling-data/src/androidTest/java/AndroidManifest.xml
+++ b/compose/ui/ui-tooling-data/src/androidTest/AndroidManifest.xml
@@ -15,7 +15,7 @@
 -->
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
     package="androidx.compose.ui.tooling.data.test">
-
-    <application/>
-
+    <application>
+        <activity android:name="androidx.compose.ui.tooling.data.TestActivity" />
+    </application>
 </manifest>
\ No newline at end of file
diff --git a/compose/ui/ui-tooling-data/src/main/AndroidManifest.xml b/compose/ui/ui-tooling-data/src/main/AndroidManifest.xml
index db78381..95bc4c1 100644
--- a/compose/ui/ui-tooling-data/src/main/AndroidManifest.xml
+++ b/compose/ui/ui-tooling-data/src/main/AndroidManifest.xml
@@ -13,11 +13,4 @@
      See the License for the specific language governing permissions and
      limitations under the License.
 -->
-<manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="androidx.compose.ui.tooling.data">
-
-    <application>
-        <activity android:name=".TestActivity" />
-    </application>
-
-</manifest>
\ No newline at end of file
+<manifest package="androidx.compose.ui.tooling.data" />
\ No newline at end of file
diff --git a/compose/ui/ui-tooling/build.gradle b/compose/ui/ui-tooling/build.gradle
index ed238dd..9e636db 100644
--- a/compose/ui/ui-tooling/build.gradle
+++ b/compose/ui/ui-tooling/build.gradle
@@ -50,6 +50,7 @@
     androidTestImplementation(ANDROIDX_TEST_RULES)
     androidTestImplementation(project(":compose:foundation:foundation-layout"))
     androidTestImplementation(project(":compose:foundation:foundation"))
+    androidTestImplementation(project(":compose:test-utils"))
     androidTestImplementation(TRUTH)
     androidTestImplementation(KOTLIN_REFLECT)
     androidTestImplementation(project(":ui:ui-animation-tooling-internal"))
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/SuspendingPointerInputFilter.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/SuspendingPointerInputFilter.kt
index 5eddcf7..aef6f62 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/SuspendingPointerInputFilter.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/SuspendingPointerInputFilter.kt
@@ -130,7 +130,12 @@
  */
 // This deprecated-error function shadows the varargs overload so that the varargs version
 // is not used without key parameters.
-@Suppress("DeprecatedCallableAddReplaceWith", "UNUSED_PARAMETER", "unused")
+@Suppress(
+    "DeprecatedCallableAddReplaceWith",
+    "UNUSED_PARAMETER",
+    "unused",
+    "ModifierFactoryUnreferencedReceiver"
+)
 @Deprecated(PointerInputModifierNoParamError, level = DeprecationLevel.ERROR)
 fun Modifier.pointerInput(
     block: suspend PointerInputScope.() -> Unit
diff --git a/lifecycle/lifecycle-viewmodel-compose/build.gradle b/lifecycle/lifecycle-viewmodel-compose/build.gradle
index 1d0082c..2bdbb1c 100644
--- a/lifecycle/lifecycle-viewmodel-compose/build.gradle
+++ b/lifecycle/lifecycle-viewmodel-compose/build.gradle
@@ -39,6 +39,7 @@
     implementation(KOTLIN_STDLIB)
 
     androidTestImplementation projectOrArtifact(":compose:ui:ui-test-junit4")
+    androidTestImplementation projectOrArtifact(":compose:test-utils")
     androidTestImplementation(ANDROIDX_TEST_RULES)
     androidTestImplementation(ANDROIDX_TEST_RUNNER)
     androidTestImplementation(JUNIT)
diff --git a/room/compiler-processing-testing/build.gradle b/room/compiler-processing-testing/build.gradle
index e310d6e..41b9d08 100644
--- a/room/compiler-processing-testing/build.gradle
+++ b/room/compiler-processing-testing/build.gradle
@@ -74,7 +74,8 @@
 // in the source.
 tasks.named("compileTestKotlin", KotlinCompile.class).configure {
     it.kotlinOptions {
-        freeCompilerArgs += ["-Xopt-in=androidx.room.compiler.processing.ExperimentalProcessingApi"]
+        freeCompilerArgs += ["-Xopt-in=kotlin.RequiresOptIn",
+                             "-Xopt-in=androidx.room.compiler.processing.ExperimentalProcessingApi"]
     }
 }
 
diff --git a/room/compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/ProcessorTestExt.kt b/room/compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/ProcessorTestExt.kt
index d4115d2..e1c5fd5 100644
--- a/room/compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/ProcessorTestExt.kt
+++ b/room/compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/ProcessorTestExt.kt
@@ -28,6 +28,7 @@
 import com.google.common.truth.Truth.assertWithMessage
 import com.google.devtools.ksp.processing.SymbolProcessor
 import com.tschuchort.compiletesting.KotlinCompilation
+import com.tschuchort.compiletesting.kspArgs
 import com.tschuchort.compiletesting.symbolProcessors
 import java.io.ByteArrayOutputStream
 import java.io.File
@@ -79,12 +80,14 @@
 fun runProcessorTestWithoutKsp(
     sources: List<Source> = emptyList(),
     classpath: List<File> = emptyList(),
+    options: Map<String, String> = emptyMap(),
     handler: (XTestInvocation) -> Unit
 ) {
     runTests(
         params = TestCompilationParameters(
             sources = sources,
             classpath = classpath,
+            options = options,
             handlers = listOf(handler)
         ),
         JavacCompilationTestRunner,
@@ -111,8 +114,14 @@
 fun runProcessorTest(
     sources: List<Source> = emptyList(),
     classpath: List<File> = emptyList(),
+    options: Map<String, String> = emptyMap(),
     handler: (XTestInvocation) -> Unit
-) = runProcessorTest(sources = sources, classpath = classpath, handlers = listOf(handler))
+) = runProcessorTest(
+    sources = sources,
+    classpath = classpath,
+    options = options,
+    handlers = listOf(handler)
+)
 
 /**
  * Runs the step created by [createProcessingStep] with ksp and one of javac or kapt, depending
@@ -131,12 +140,14 @@
 fun runProcessorTest(
     sources: List<Source> = emptyList(),
     classpath: List<File> = emptyList(),
+    options: Map<String, String> = emptyMap(),
     createProcessingStep: () -> XProcessingStep,
     onCompilationResult: (CompilationResultSubject) -> Unit
 ) {
     runProcessorTest(
         sources = sources,
-        classpath = classpath
+        classpath = classpath,
+        options = options
     ) { invocation ->
         val step = createProcessingStep()
         val elements =
@@ -161,6 +172,7 @@
 fun runProcessorTest(
     sources: List<Source> = emptyList(),
     classpath: List<File> = emptyList(),
+    options: Map<String, String> = emptyMap(),
     handlers: List<(XTestInvocation) -> Unit>
 ) {
     val javaApRunner = if (sources.any { it is Source.KotlinSource }) {
@@ -172,6 +184,7 @@
         params = TestCompilationParameters(
             sources = sources,
             classpath = classpath,
+            options = options,
             handlers = handlers
         ),
         javaApRunner,
@@ -188,10 +201,12 @@
 fun runJavaProcessorTest(
     sources: List<Source>,
     classpath: List<File> = emptyList(),
+    options: Map<String, String> = emptyMap(),
     handler: (XTestInvocation) -> Unit
 ) = runJavaProcessorTest(
     sources = sources,
     classpath = classpath,
+    options = options,
     handlers = listOf(handler)
 )
 
@@ -202,12 +217,14 @@
 fun runJavaProcessorTest(
     sources: List<Source>,
     classpath: List<File> = emptyList(),
+    options: Map<String, String> = emptyMap(),
     handlers: List<(XTestInvocation) -> Unit>
 ) {
     runTests(
         params = TestCompilationParameters(
             sources = sources,
             classpath = classpath,
+            options = options,
             handlers = handlers
         ),
         JavacCompilationTestRunner
@@ -221,10 +238,12 @@
 fun runKaptTest(
     sources: List<Source>,
     classpath: List<File> = emptyList(),
+    options: Map<String, String> = emptyMap(),
     handler: (XTestInvocation) -> Unit
 ) = runKaptTest(
     sources = sources,
     classpath = classpath,
+    options = options,
     handlers = listOf(handler)
 )
 
@@ -235,12 +254,14 @@
 fun runKaptTest(
     sources: List<Source>,
     classpath: List<File> = emptyList(),
+    options: Map<String, String> = emptyMap(),
     handlers: List<(XTestInvocation) -> Unit>
 ) {
     runTests(
         params = TestCompilationParameters(
             sources = sources,
             classpath = classpath,
+            options = options,
             handlers = handlers
         ),
         KaptCompilationTestRunner
@@ -254,10 +275,12 @@
 fun runKspTest(
     sources: List<Source>,
     classpath: List<File> = emptyList(),
+    options: Map<String, String> = emptyMap(),
     handler: (XTestInvocation) -> Unit
 ) = runKspTest(
     sources = sources,
     classpath = classpath,
+    options = options,
     handlers = listOf(handler)
 )
 
@@ -268,12 +291,14 @@
 fun runKspTest(
     sources: List<Source>,
     classpath: List<File> = emptyList(),
+    options: Map<String, String> = emptyMap(),
     handlers: List<(XTestInvocation) -> Unit>
 ) {
     runTests(
         params = TestCompilationParameters(
             sources = sources,
             classpath = classpath,
+            options = options,
             handlers = handlers
         ),
         KspCompilationTestRunner
@@ -284,11 +309,13 @@
  * Compiles the given set of sources into a temporary folder and returns the output classes
  * directory.
  * @param sources The list of source files to compile
+ * @param options The annotation processor arguments
  * @param annotationProcessors The list of Java annotation processors to run with compilation
  * @param symbolProcessors The list of Kotlin symbol processors to run with compilation
  */
 fun compileFiles(
     sources: List<Source>,
+    options: Map<String, String> = emptyMap(),
     annotationProcessors: List<Processor> = emptyList(),
     symbolProcessors: List<SymbolProcessor> = emptyList()
 ): File {
@@ -297,6 +324,12 @@
         sources = sources,
         outputStream = outputStream
     )
+    if (annotationProcessors.isNotEmpty()) {
+        compilation.kaptArgs.putAll(options)
+    }
+    if (symbolProcessors.isNotEmpty()) {
+        compilation.kspArgs.putAll(options)
+    }
     compilation.annotationProcessors = annotationProcessors
     compilation.symbolProcessors = symbolProcessors
     val result = compilation.compile()
diff --git a/room/compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/runner/CompilationTestRunner.kt b/room/compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/runner/CompilationTestRunner.kt
index ba50be6..844bb20 100644
--- a/room/compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/runner/CompilationTestRunner.kt
+++ b/room/compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/runner/CompilationTestRunner.kt
@@ -39,5 +39,6 @@
 internal data class TestCompilationParameters(
     val sources: List<Source> = emptyList(),
     val classpath: List<File> = emptyList(),
+    val options: Map<String, String> = emptyMap(),
     val handlers: List<(XTestInvocation) -> Unit>
 )
diff --git a/room/compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/runner/JavacCompilationTestRunner.kt b/room/compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/runner/JavacCompilationTestRunner.kt
index 9097b02..d3bd3a8 100644
--- a/room/compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/runner/JavacCompilationTestRunner.kt
+++ b/room/compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/runner/JavacCompilationTestRunner.kt
@@ -48,10 +48,14 @@
         } else {
             params.sources
         }
+
+        val optionsArg = params.options.entries.map {
+            "-A${it.key}=${it.value}"
+        }
         val compiler = Compiler
             .javac()
             .withProcessors(syntheticJavacProcessor)
-            .withOptions("-Xlint")
+            .withOptions(optionsArg + "-Xlint")
             .let {
                 if (params.classpath.isNotEmpty()) {
                     it.withClasspath(params.classpath)
diff --git a/room/compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/runner/KaptCompilationTestRunner.kt b/room/compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/runner/KaptCompilationTestRunner.kt
index 6e1f860..651d73b 100644
--- a/room/compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/runner/KaptCompilationTestRunner.kt
+++ b/room/compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/runner/KaptCompilationTestRunner.kt
@@ -41,6 +41,7 @@
             outputStream = outputStream,
             classpaths = params.classpath
         )
+        compilation.kaptArgs.putAll(params.options)
         compilation.annotationProcessors = listOf(syntheticJavacProcessor)
         val result = compilation.compile()
         return KotlinCompileTestingCompilationResult(
diff --git a/room/compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/runner/KspCompilationTestRunner.kt b/room/compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/runner/KspCompilationTestRunner.kt
index 3ebaa0b..78308ba 100644
--- a/room/compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/runner/KspCompilationTestRunner.kt
+++ b/room/compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/runner/KspCompilationTestRunner.kt
@@ -25,6 +25,7 @@
 import androidx.room.compiler.processing.util.Source
 import com.tschuchort.compiletesting.KotlinCompilation
 import com.tschuchort.compiletesting.SourceFile
+import com.tschuchort.compiletesting.kspArgs
 import com.tschuchort.compiletesting.kspSourcesDir
 import com.tschuchort.compiletesting.symbolProcessors
 import java.io.ByteArrayOutputStream
@@ -57,6 +58,7 @@
             outputStream = combinedOutputStream,
             classpaths = params.classpath
         )
+        kspCompilation.kspArgs.putAll(params.options)
         kspCompilation.symbolProcessors = listOf(syntheticKspProcessor)
         kspCompilation.compile()
         // ignore KSP result for now because KSP stops compilation, which might create false
diff --git a/room/compiler-processing-testing/src/test/java/androidx/room/compiler/processing/util/TestRunnerTest.kt b/room/compiler-processing-testing/src/test/java/androidx/room/compiler/processing/util/TestRunnerTest.kt
index 7dcc46f..6ba44be 100644
--- a/room/compiler-processing-testing/src/test/java/androidx/room/compiler/processing/util/TestRunnerTest.kt
+++ b/room/compiler-processing-testing/src/test/java/androidx/room/compiler/processing/util/TestRunnerTest.kt
@@ -16,6 +16,7 @@
 
 package androidx.room.compiler.processing.util
 
+import androidx.room.compiler.processing.ExperimentalProcessingApi
 import com.google.common.truth.Truth.assertThat
 import com.squareup.javapoet.CodeBlock
 import com.squareup.javapoet.JavaFile
@@ -23,6 +24,7 @@
 import org.junit.Test
 import javax.tools.Diagnostic
 
+@OptIn(ExperimentalProcessingApi::class)
 class TestRunnerTest {
     @Test
     fun generatedBadCode_expected() = generatedBadCode(assertFailure = true)
@@ -30,6 +32,19 @@
     @Test(expected = AssertionError::class)
     fun generatedBadCode_unexpected() = generatedBadCode(assertFailure = false)
 
+    @Test
+    fun options() {
+        val testOptions = mapOf(
+            "a" to "b",
+            "c" to "d"
+        )
+        runProcessorTest(
+            options = testOptions
+        ) {
+            assertThat(it.processingEnv.options).containsAtLeastEntriesIn(testOptions)
+        }
+    }
+
     private fun generatedBadCode(assertFailure: Boolean) {
         runProcessorTest {
             if (it.processingEnv.findTypeElement("foo.Foo") == null) {
diff --git a/room/compiler/src/main/kotlin/androidx/room/processor/ProcessorErrors.kt b/room/compiler/src/main/kotlin/androidx/room/processor/ProcessorErrors.kt
index d22d0d2..386c3ec 100644
--- a/room/compiler/src/main/kotlin/androidx/room/processor/ProcessorErrors.kt
+++ b/room/compiler/src/main/kotlin/androidx/room/processor/ProcessorErrors.kt
@@ -721,6 +721,11 @@
     ) = "The partial entity $partialEntityName is missing the primary key fields " +
         "(${primaryKeyNames.joinToString()}) needed to perform an UPDATE."
 
+    fun noColumnsInPartialEntity(
+        partialEntityName: String
+    ) = "The partial entity $partialEntityName does not have any columns that can be used to " +
+        "perform the query."
+
     fun cannotFindPreparedQueryResultAdapter(
         returnType: String,
         type: QueryType
diff --git a/room/compiler/src/main/kotlin/androidx/room/processor/ShortcutMethodProcessor.kt b/room/compiler/src/main/kotlin/androidx/room/processor/ShortcutMethodProcessor.kt
index b6bddc9..59adf60 100644
--- a/room/compiler/src/main/kotlin/androidx/room/processor/ShortcutMethodProcessor.kt
+++ b/room/compiler/src/main/kotlin/androidx/room/processor/ShortcutMethodProcessor.kt
@@ -143,6 +143,15 @@
                                 ProcessorErrors.INVALID_RELATION_IN_PARTIAL_ENTITY
                             )
                         }
+
+                        if (pojo.fields.isEmpty()) {
+                            context.logger.e(
+                                executableElement,
+                                ProcessorErrors.noColumnsInPartialEntity(
+                                    partialEntityName = pojo.typeName.toString()
+                                )
+                            )
+                        }
                         onValidatePartialEntity(targetEntity, pojo)
                     }
                 }
diff --git a/room/compiler/src/test/kotlin/androidx/room/processor/InsertionMethodProcessorTest.kt b/room/compiler/src/test/kotlin/androidx/room/processor/InsertionMethodProcessorTest.kt
index f753d89..e22402f 100644
--- a/room/compiler/src/test/kotlin/androidx/room/processor/InsertionMethodProcessorTest.kt
+++ b/room/compiler/src/test/kotlin/androidx/room/processor/InsertionMethodProcessorTest.kt
@@ -21,17 +21,15 @@
 import androidx.room.Insert
 import androidx.room.OnConflictStrategy
 import androidx.room.compiler.processing.XTypeElement
+import androidx.room.compiler.processing.util.Source
+import androidx.room.compiler.processing.util.XTestInvocation
+import androidx.room.compiler.processing.util.runProcessorTest
 import androidx.room.ext.CommonTypeNames
 import androidx.room.ext.RxJava2TypeNames
 import androidx.room.ext.RxJava3TypeNames
 import androidx.room.solver.shortcut.result.InsertMethodAdapter
-import androidx.room.testing.TestInvocation
-import androidx.room.testing.TestProcessor
+import androidx.room.testing.context
 import androidx.room.vo.InsertionMethod
-import com.google.common.truth.Truth.assertAbout
-import com.google.testing.compile.CompileTester
-import com.google.testing.compile.JavaFileObjects
-import com.google.testing.compile.JavaSourcesSubjectFactory
 import com.squareup.javapoet.ArrayTypeName
 import com.squareup.javapoet.ClassName
 import com.squareup.javapoet.ParameterizedTypeName
@@ -43,8 +41,7 @@
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.junit.runners.JUnit4
-import toJFO
-import javax.tools.JavaFileObject
+import toSources
 
 @Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN")
 @RunWith(JUnit4::class)
@@ -70,14 +67,17 @@
                 @Insert
                 abstract public void foo();
                 """
-        ) { insertion, _ ->
+        ) { insertion, invocation ->
             assertThat(insertion.name, `is`("foo"))
             assertThat(insertion.parameters.size, `is`(0))
             assertThat(insertion.returnType.typeName, `is`(TypeName.VOID))
             assertThat(insertion.entities.size, `is`(0))
-        }.failsToCompile().withErrorContaining(
-            ProcessorErrors.INSERTION_DOES_NOT_HAVE_ANY_PARAMETERS_TO_INSERT
-        )
+            invocation.assertCompilationResult {
+                hasErrorContaining(
+                    ProcessorErrors.INSERTION_DOES_NOT_HAVE_ANY_PARAMETERS_TO_INSERT
+                )
+            }
+        }
     }
 
     @Test
@@ -99,7 +99,7 @@
                 `is`(ClassName.get("foo.bar", "User") as TypeName)
             )
             assertThat(insertion.returnType.typeName, `is`(TypeName.LONG))
-        }.compilesWithoutError()
+        }
     }
 
     @Test
@@ -109,13 +109,16 @@
                 @Insert
                 abstract public void foo(NotAnEntity notValid);
                 """
-        ) { insertion, _ ->
+        ) { insertion, invocation ->
             assertThat(insertion.name, `is`("foo"))
             assertThat(insertion.parameters.size, `is`(1))
             assertThat(insertion.entities.size, `is`(0))
-        }.failsToCompile().withErrorContaining(
-            ProcessorErrors.CANNOT_FIND_ENTITY_FOR_SHORTCUT_QUERY_PARAMETER
-        )
+            invocation.assertCompilationResult {
+                hasErrorContaining(
+                    ProcessorErrors.CANNOT_FIND_ENTITY_FOR_SHORTCUT_QUERY_PARAMETER
+                )
+            }
+        }
     }
 
     @Test
@@ -138,7 +141,7 @@
             assertThat(insertion.entities["u2"]?.pojo?.typeName, `is`(USER_TYPE_NAME))
             assertThat(insertion.parameters.map { it.name }, `is`(listOf("u1", "u2")))
             assertThat(insertion.returnType.typeName, `is`(TypeName.VOID))
-        }.compilesWithoutError()
+        }
     }
 
     @Test
@@ -173,7 +176,7 @@
                     ) as TypeName
                 )
             )
-        }.compilesWithoutError()
+        }
     }
 
     @Test
@@ -196,7 +199,7 @@
             assertThat(insertion.entities.size, `is`(1))
             assertThat(insertion.entities["users"]?.pojo?.typeName, `is`(USER_TYPE_NAME))
             assertThat(insertion.returnType.typeName, `is`(TypeName.VOID))
-        }.compilesWithoutError()
+        }
     }
 
     @Test
@@ -222,7 +225,7 @@
             assertThat(insertion.entities.size, `is`(1))
             assertThat(insertion.entities["users"]?.pojo?.typeName, `is`(USER_TYPE_NAME))
             assertThat(insertion.returnType.typeName, `is`(TypeName.VOID))
-        }.compilesWithoutError()
+        }
     }
 
     @Test
@@ -248,7 +251,7 @@
             assertThat(insertion.entities.size, `is`(1))
             assertThat(insertion.entities["users"]?.pojo?.typeName, `is`(USER_TYPE_NAME))
             assertThat(insertion.returnType.typeName, `is`(TypeName.VOID))
-        }.compilesWithoutError()
+        }
     }
 
     @Test
@@ -274,7 +277,7 @@
             assertThat(insertion.entities.size, `is`(1))
             assertThat(insertion.entities["users"]?.pojo?.typeName, `is`(USER_TYPE_NAME))
             assertThat(insertion.returnType.typeName, `is`(TypeName.VOID))
-        }.compilesWithoutError()
+        }
     }
 
     @Test
@@ -301,7 +304,7 @@
             assertThat(insertion.entities.size, `is`(1))
             assertThat(insertion.entities["users"]?.pojo?.typeName, `is`(USER_TYPE_NAME))
             assertThat(insertion.returnType.typeName, `is`(TypeName.VOID))
-        }.compilesWithoutError()
+        }
     }
 
     @Test
@@ -326,7 +329,7 @@
             assertThat(insertion.entities.size, `is`(2))
             assertThat(insertion.entities["u1"]?.pojo?.typeName, `is`(USER_TYPE_NAME))
             assertThat(insertion.entities["b1"]?.pojo?.typeName, `is`(BOOK_TYPE_NAME))
-        }.compilesWithoutError()
+        }
     }
 
     @Test
@@ -338,7 +341,7 @@
                 """
         ) { insertion, _ ->
             assertThat(insertion.onConflict, `is`(OnConflictStrategy.ABORT))
-        }.compilesWithoutError()
+        }
     }
 
     @Test
@@ -348,8 +351,13 @@
                 @Insert(onConflict = -1)
                 abstract public void foo(User user);
                 """
-        ) { _, _ ->
-        }.failsToCompile().withErrorContaining(ProcessorErrors.INVALID_ON_CONFLICT_VALUE)
+        ) { _, invocation ->
+            invocation.assertCompilationResult {
+                hasErrorContaining(
+                    ProcessorErrors.INVALID_ON_CONFLICT_VALUE
+                )
+            }
+        }
     }
 
     @Test
@@ -368,7 +376,7 @@
                 """
             ) { insertion, _ ->
                 assertThat(insertion.onConflict, `is`(pair.second))
-            }.compilesWithoutError()
+            }
         }
     }
 
@@ -388,11 +396,14 @@
                 @Insert
                 abstract public $type foo(User user);
                 """
-            ) { insertion, _ ->
+            ) { insertion, invocation ->
                 assertThat(insertion.methodBinder.adapter, `is`(nullValue()))
-            }.failsToCompile().withErrorContaining(
-                ProcessorErrors.CANNOT_FIND_INSERT_RESULT_ADAPTER
-            )
+                invocation.assertCompilationResult {
+                    hasErrorContaining(
+                        ProcessorErrors.CANNOT_FIND_INSERT_RESULT_ADAPTER
+                    )
+                }
+            }
         }
     }
 
@@ -410,11 +421,14 @@
                 @Insert
                 abstract public $type foo(User user);
                 """
-            ) { insertion, _ ->
+            ) { insertion, invocation ->
                 assertThat(insertion.methodBinder.adapter, `is`(nullValue()))
-            }.failsToCompile().withErrorContaining(
-                ProcessorErrors.CANNOT_FIND_INSERT_RESULT_ADAPTER
-            )
+                invocation.assertCompilationResult {
+                    hasErrorContaining(
+                        ProcessorErrors.CANNOT_FIND_INSERT_RESULT_ADAPTER
+                    )
+                }
+            }
         }
     }
 
@@ -431,11 +445,14 @@
                 @Insert
                 abstract public $type foo(User... user);
                 """
-            ) { insertion, _ ->
+            ) { insertion, invocation ->
                 assertThat(insertion.methodBinder.adapter, `is`(nullValue()))
-            }.failsToCompile().withErrorContaining(
-                ProcessorErrors.CANNOT_FIND_INSERT_RESULT_ADAPTER
-            )
+                invocation.assertCompilationResult {
+                    hasErrorContaining(
+                        ProcessorErrors.CANNOT_FIND_INSERT_RESULT_ADAPTER
+                    )
+                }
+            }
         }
     }
 
@@ -452,11 +469,14 @@
                 @Insert
                 abstract public $type foo(User user1, User user2);
                 """
-            ) { insertion, _ ->
+            ) { insertion, invocation ->
                 assertThat(insertion.methodBinder.adapter, `is`(nullValue()))
-            }.failsToCompile().withErrorContaining(
-                ProcessorErrors.CANNOT_FIND_INSERT_RESULT_ADAPTER
-            )
+                invocation.assertCompilationResult {
+                    hasErrorContaining(
+                        ProcessorErrors.CANNOT_FIND_INSERT_RESULT_ADAPTER
+                    )
+                }
+            }
         }
     }
 
@@ -526,13 +546,15 @@
                 """
             ) { insertion, _ ->
                 assertThat(insertion.methodBinder.adapter, `is`(notNullValue()))
-            }.compilesWithoutError()
+            }
         }
     }
 
     @Test
     fun targetEntitySingle() {
-        val usernameJfo = """
+        val usernameSource = Source.java(
+            "foo.bar.Username",
+            """
             package foo.bar;
             import androidx.room.*;
 
@@ -542,13 +564,14 @@
                 @ColumnInfo(name = "ageColumn")
                 int age;
             }
-        """.toJFO("foo.bar.Username")
+            """
+        )
         singleInsertMethod(
             """
                 @Insert(entity = User.class)
                 abstract public long foo(Username username);
-                """,
-            additionalJFOs = listOf(usernameJfo)
+            """,
+            additionalSources = listOf(usernameSource)
         ) { insertion, _ ->
             assertThat(insertion.name, `is`("foo"))
             assertThat(insertion.parameters.size, `is`(1))
@@ -559,7 +582,7 @@
             assertThat(insertion.entities["username"]?.isPartialEntity, `is`(true))
             assertThat(insertion.entities["username"]?.entityTypeName, `is`(USER_TYPE_NAME))
             assertThat(insertion.entities["username"]?.pojo?.typeName, `is`(USERNAME_TYPE_NAME))
-        }.compilesWithoutError()
+        }
     }
 
     @Test
@@ -568,14 +591,16 @@
             """
                 @Insert(entity = User.class)
                 abstract public long foo(User user);
-                """
+            """
         ) { _, _ ->
-        }.compilesWithoutError()
+        }
     }
 
     @Test
     fun targetEntityTwo() {
-        val usernameJfo = """
+        val usernameSource = Source.java(
+            "foo.bar.Username",
+            """
             package foo.bar;
             import androidx.room.*;
 
@@ -585,20 +610,23 @@
                 @ColumnInfo(name = "ageColumn")
                 int age;
             }
-        """.toJFO("foo.bar.Username")
+            """
+        )
         singleInsertMethod(
             """
                 @Insert(entity = User.class)
                 abstract public void foo(Username usernameA, Username usernameB);
-                """,
-            additionalJFOs = listOf(usernameJfo)
+            """,
+            additionalSources = listOf(usernameSource)
         ) { _, _ ->
-        }.compilesWithoutError()
+        }
     }
 
     @Test
     fun targetEntityMissingRequiredColumn() {
-        val usernameJfo = """
+        val usernameSource = Source.java(
+            "foo.bar.Username",
+            """
             package foo.bar;
             import androidx.room.*;
 
@@ -606,25 +634,31 @@
                 int uid;
                 String name;
             }
-        """.toJFO("foo.bar.Username")
+            """
+        )
         singleInsertMethod(
             """
                 @Insert(entity = User.class)
                 abstract public void foo(Username username);
-                """,
-            additionalJFOs = listOf(usernameJfo)
-        ) { _, _ ->
-        }.failsToCompile().withErrorContaining(
-            ProcessorErrors.missingRequiredColumnsInPartialEntity(
-                partialEntityName = USERNAME_TYPE_NAME.toString(),
-                missingColumnNames = listOf("ageColumn")
-            )
-        )
+            """,
+            additionalSources = listOf(usernameSource)
+        ) { _, invocation ->
+            invocation.assertCompilationResult {
+                hasErrorContaining(
+                    ProcessorErrors.missingRequiredColumnsInPartialEntity(
+                        partialEntityName = USERNAME_TYPE_NAME.toString(),
+                        missingColumnNames = listOf("ageColumn")
+                    )
+                )
+            }
+        }
     }
 
     @Test
     fun targetEntityColumnDefaultValue() {
-        val petNameJfo = """
+        val petNameSource = Source.java(
+            "foo.bar.PetName",
+            """
             package foo.bar;
             import androidx.room.*;
 
@@ -632,8 +666,11 @@
                 @ColumnInfo(name = "name")
                 String string;
             }
-        """.toJFO("foo.bar.PetName")
-        val petJfo = """
+            """
+        )
+        val petSource = Source.java(
+            "foo.bar.Pet",
+            """
             package foo.bar;
             import androidx.room.*;
 
@@ -645,20 +682,23 @@
                 @ColumnInfo(defaultValue = "0")
                 int age;
             }
-        """.toJFO("foo.bar.Pet")
+            """
+        )
         singleInsertMethod(
             """
                 @Insert(entity = Pet.class)
                 abstract public long foo(PetName petName);
-                """,
-            additionalJFOs = listOf(petNameJfo, petJfo)
+            """,
+            additionalSources = listOf(petNameSource, petSource)
         ) { _, _ ->
-        }.compilesWithoutError()
+        }
     }
 
     @Test
     fun targetEntityMissingPrimaryKey() {
-        val petNameJfo = """
+        val petNameSource = Source.java(
+            "foo.bar.PetName",
+            """
             package foo.bar;
             import androidx.room.*;
 
@@ -666,8 +706,11 @@
                 @ColumnInfo(name = "name")
                 String string;
             }
-        """.toJFO("foo.bar.PetName")
-        val petJfo = """
+            """
+        )
+        val petSource = Source.java(
+            "foo.bar.Pet",
+            """
             package foo.bar;
             import androidx.room.*;
 
@@ -677,25 +720,31 @@
                 int petId;
                 String name;
             }
-        """.toJFO("foo.bar.Pet")
+            """
+        )
         singleInsertMethod(
             """
                 @Insert(entity = Pet.class)
                 abstract public long foo(PetName petName);
-                """,
-            additionalJFOs = listOf(petNameJfo, petJfo)
-        ) { _, _ ->
-        }.failsToCompile().withErrorContaining(
-            ProcessorErrors.missingPrimaryKeysInPartialEntityForInsert(
-                partialEntityName = "foo.bar.PetName",
-                primaryKeyNames = listOf("petId")
-            )
-        )
+            """,
+            additionalSources = listOf(petNameSource, petSource)
+        ) { _, invocation ->
+            invocation.assertCompilationResult {
+                hasErrorContaining(
+                    ProcessorErrors.missingPrimaryKeysInPartialEntityForInsert(
+                        partialEntityName = "foo.bar.PetName",
+                        primaryKeyNames = listOf("petId")
+                    )
+                )
+            }
+        }
     }
 
     @Test
     fun targetEntityAutoGeneratedPrimaryKey() {
-        val petNameJfo = """
+        val petNameSource = Source.java(
+            "foo.bar.PetName",
+            """
             package foo.bar;
             import androidx.room.*;
 
@@ -703,8 +752,11 @@
                 @ColumnInfo(name = "name")
                 String string;
             }
-        """.toJFO("foo.bar.PetName")
-        val petJfo = """
+            """
+        )
+        val petSource = Source.java(
+            "foo.bar.Pet",
+            """
             package foo.bar;
             import androidx.room.*;
 
@@ -714,20 +766,23 @@
                 int petId;
                 String name;
             }
-        """.toJFO("foo.bar.Pet")
+            """
+        )
         singleInsertMethod(
             """
                 @Insert(entity = Pet.class)
                 abstract public long foo(PetName petName);
-                """,
-            additionalJFOs = listOf(petNameJfo, petJfo)
+            """,
+            additionalSources = listOf(petNameSource, petSource)
         ) { _, _ ->
-        }.compilesWithoutError()
+        }
     }
 
     @Test
     fun targetEntityExtraColumn() {
-        val usernameJfo = """
+        val usernameSource = Source.java(
+            "foo.bar.Username",
+            """
             package foo.bar;
             import androidx.room.*;
 
@@ -736,22 +791,28 @@
                 String name;
                 long extraField;
             }
-        """.toJFO("foo.bar.Username")
+            """
+        )
         singleInsertMethod(
             """
                 @Insert(entity = User.class)
                 abstract public long foo(Username username);
-                """,
-            additionalJFOs = listOf(usernameJfo)
-        ) { _, _ ->
-        }.failsToCompile().withErrorContaining(
-            ProcessorErrors.cannotFindAsEntityField("foo.bar.User")
-        )
+            """,
+            additionalSources = listOf(usernameSource)
+        ) { _, invocation ->
+            invocation.assertCompilationResult {
+                hasErrorContaining(
+                    ProcessorErrors.cannotFindAsEntityField("foo.bar.User")
+                )
+            }
+        }
     }
 
     @Test
     fun targetEntityExtraColumnIgnored() {
-        val usernameJfo = """
+        val usernameSource = Source.java(
+            "foo.bar.Username",
+            """
             package foo.bar;
             import androidx.room.*;
 
@@ -763,20 +824,23 @@
                 @Ignore
                 long extraField;
             }
-        """.toJFO("foo.bar.Username")
+            """
+        )
         singleInsertMethod(
             """
                 @Insert(entity = User.class)
                 abstract public long foo(Username username);
-                """,
-            additionalJFOs = listOf(usernameJfo)
+            """,
+            additionalSources = listOf(usernameSource)
         ) { _, _ ->
-        }.compilesWithoutError()
+        }
     }
 
     @Test
     fun targetEntityWithEmbedded() {
-        val usernameJfo = """
+        val usernameSource = Source.java(
+            "foo.bar.Username",
+            """
             package foo.bar;
             import androidx.room.*;
 
@@ -787,8 +851,11 @@
                 @ColumnInfo(name = "ageColumn")
                 int age;
             }
-        """.toJFO("foo.bar.Username")
-        val fullnameJfo = """
+            """
+        )
+        val fullnameSource = Source.java(
+            "foo.bar.Fullname",
+            """
             package foo.bar;
             import androidx.room.*;
 
@@ -797,20 +864,23 @@
                 String firstName;
                 String lastName;
             }
-        """.toJFO("foo.bar.Fullname")
+            """
+        )
         singleInsertMethod(
             """
                 @Insert(entity = User.class)
                 abstract public long foo(Username username);
-                """,
-            additionalJFOs = listOf(usernameJfo, fullnameJfo)
+            """,
+            additionalSources = listOf(usernameSource, fullnameSource)
         ) { _, _ ->
-        }.compilesWithoutError()
+        }
     }
 
     @Test
     fun targetEntityWithRelation() {
-        val userPetsJfo = """
+        val userPetsSource = Source.java(
+            "foo.bar.UserPets",
+            """
             package foo.bar;
             import androidx.room.*;
             import java.util.List;
@@ -820,8 +890,11 @@
                 @Relation(parentColumn = "uid", entityColumn = "ownerId")
                 List<Pet> pets;
             }
-        """.toJFO("foo.bar.UserPets")
-        val petJfo = """
+            """
+        )
+        val petSource = Source.java(
+            "foo.bar.Pet",
+            """
             package foo.bar;
             import androidx.room.*;
 
@@ -831,59 +904,59 @@
                 int petId;
                 int ownerId;
             }
-        """.toJFO("foo.bar.Pet")
+            """
+        )
         singleInsertMethod(
             """
                 @Insert(entity = User.class)
                 abstract public long foo(UserPets userPets);
                 """,
-            additionalJFOs = listOf(userPetsJfo, petJfo)
-        ) { _, _ ->
-        }.failsToCompile().withErrorContaining(ProcessorErrors.INVALID_RELATION_IN_PARTIAL_ENTITY)
+            additionalSources = listOf(userPetsSource, petSource)
+        ) { _, invocation ->
+            invocation.assertCompilationResult {
+                hasErrorContaining(
+                    ProcessorErrors.INVALID_RELATION_IN_PARTIAL_ENTITY
+                )
+            }
+        }
     }
 
     fun singleInsertMethod(
         vararg input: String,
-        additionalJFOs: List<JavaFileObject> = emptyList(),
-        handler: (InsertionMethod, TestInvocation) -> Unit
-    ): CompileTester {
-        return assertAbout(JavaSourcesSubjectFactory.javaSources())
-            .that(
-                listOf(
-                    JavaFileObjects.forSourceString(
-                        "foo.bar.MyClass",
-                        DAO_PREFIX + input.joinToString("\n") + DAO_SUFFIX
-                    ),
-                    COMMON.USER, COMMON.BOOK, COMMON.NOT_AN_ENTITY, COMMON.RX2_COMPLETABLE,
-                    COMMON.RX2_MAYBE, COMMON.RX2_SINGLE, COMMON.RX3_COMPLETABLE,
-                    COMMON.RX3_MAYBE, COMMON.RX3_SINGLE
-                ) + additionalJFOs
+        additionalSources: List<Source> = emptyList(),
+        handler: (InsertionMethod, XTestInvocation) -> Unit
+    ) {
+        val inputSource = Source.java(
+            "foo.bar.MyClass",
+            DAO_PREFIX + input.joinToString("\n") + DAO_SUFFIX
+        )
+        val commonSources = listOf(
+            COMMON.USER, COMMON.BOOK, COMMON.NOT_AN_ENTITY, COMMON.RX2_COMPLETABLE,
+            COMMON.RX2_MAYBE, COMMON.RX2_SINGLE, COMMON.RX3_COMPLETABLE,
+            COMMON.RX3_MAYBE, COMMON.RX3_SINGLE
+        ).toSources()
+
+        runProcessorTest(
+            sources = commonSources + additionalSources + inputSource
+        ) { invocation ->
+            val (owner, methods) = invocation.roundEnv
+                .getElementsAnnotatedWith(Dao::class.qualifiedName!!)
+                .filterIsInstance<XTypeElement>()
+                .map {
+                    Pair(
+                        it,
+                        it.getAllMethods().filter {
+                            it.hasAnnotation(Insert::class)
+                        }
+                    )
+                }.first { it.second.isNotEmpty() }
+            val processor = InsertionMethodProcessor(
+                baseContext = invocation.context,
+                containing = owner.type,
+                executableElement = methods.first()
             )
-            .processedWith(
-                TestProcessor.builder()
-                    .forAnnotations(Insert::class, Dao::class)
-                    .nextRunHandler { invocation ->
-                        val (owner, methods) = invocation.roundEnv
-                            .getElementsAnnotatedWith(Dao::class.qualifiedName!!)
-                            .filterIsInstance<XTypeElement>()
-                            .map {
-                                Pair(
-                                    it,
-                                    it.getAllMethods().filter {
-                                        it.hasAnnotation(Insert::class)
-                                    }
-                                )
-                            }.first { it.second.isNotEmpty() }
-                        val processor = InsertionMethodProcessor(
-                            baseContext = invocation.context,
-                            containing = owner.type,
-                            executableElement = methods.first()
-                        )
-                        val processed = processor.process()
-                        handler(processed, invocation)
-                        true
-                    }
-                    .build()
-            )
+            val processed = processor.process()
+            handler(processed, invocation)
+        }
     }
 }
diff --git a/room/compiler/src/test/kotlin/androidx/room/processor/RawQueryMethodProcessorTest.kt b/room/compiler/src/test/kotlin/androidx/room/processor/RawQueryMethodProcessorTest.kt
index 00b3b2b..001e186 100644
--- a/room/compiler/src/test/kotlin/androidx/room/processor/RawQueryMethodProcessorTest.kt
+++ b/room/compiler/src/test/kotlin/androidx/room/processor/RawQueryMethodProcessorTest.kt
@@ -17,32 +17,25 @@
 package androidx.room.processor
 
 import COMMON
-import androidx.room.ColumnInfo
 import androidx.room.Dao
-import androidx.room.Entity
-import androidx.room.PrimaryKey
-import androidx.room.Query
 import androidx.room.RawQuery
-import androidx.room.compiler.processing.util.runProcessorTest
 import androidx.room.compiler.processing.XTypeElement
+import androidx.room.compiler.processing.util.Source
+import androidx.room.compiler.processing.util.XTestInvocation
+import androidx.room.compiler.processing.util.runProcessorTest
 import androidx.room.ext.PagingTypeNames
 import androidx.room.ext.SupportDbTypeNames
 import androidx.room.processor.ProcessorErrors.RAW_QUERY_STRING_PARAMETER_REMOVED
-import androidx.room.testing.TestInvocation
-import androidx.room.testing.TestProcessor
 import androidx.room.testing.context
 import androidx.room.vo.RawQueryMethod
 import androidx.sqlite.db.SupportSQLiteQuery
-import com.google.common.truth.Truth
-import com.google.testing.compile.CompileTester
-import com.google.testing.compile.JavaFileObjects
-import com.google.testing.compile.JavaSourcesSubjectFactory
 import com.squareup.javapoet.ArrayTypeName
 import com.squareup.javapoet.ClassName
 import com.squareup.javapoet.TypeName
 import org.hamcrest.CoreMatchers.`is`
 import org.hamcrest.MatcherAssert.assertThat
 import org.junit.Test
+import toSources
 
 class RawQueryMethodProcessorTest {
     @Test
@@ -67,7 +60,7 @@
                 query.returnType.typeName,
                 `is`(ArrayTypeName.of(TypeName.INT) as TypeName)
             )
-        }.compilesWithoutError()
+        }
     }
 
     @Test
@@ -77,8 +70,11 @@
                 @RawQuery
                 abstract public int[] foo(String query);
                 """
-        ) { _, _ ->
-        }.failsToCompile().withErrorContaining(RAW_QUERY_STRING_PARAMETER_REMOVED)
+        ) { _, invocation ->
+            invocation.assertCompilationResult {
+                hasErrorContaining(RAW_QUERY_STRING_PARAMETER_REMOVED)
+            }
+        }
     }
 
     @Test
@@ -101,7 +97,7 @@
             )
             assertThat(query.observedTableNames.size, `is`(1))
             assertThat(query.observedTableNames, `is`(setOf("User")))
-        }.compilesWithoutError()
+        }
     }
 
     @Test
@@ -111,7 +107,7 @@
                 @RawQuery(observedEntities = {})
                 abstract public LiveData<User> foo(SupportSQLiteQuery query);
                 """
-        ) { query, _ ->
+        ) { query, invocation ->
             assertThat(query.name, `is`("foo"))
             assertThat(
                 query.runtimeQueryParam,
@@ -123,8 +119,12 @@
                 )
             )
             assertThat(query.observedTableNames, `is`(emptySet()))
-        }.failsToCompile()
-            .withErrorContaining(ProcessorErrors.OBSERVABLE_QUERY_NOTHING_TO_OBSERVE)
+            invocation.assertCompilationResult {
+                hasErrorContaining(
+                    ProcessorErrors.OBSERVABLE_QUERY_NOTHING_TO_OBSERVE
+                )
+            }
+        }
     }
 
     @Test
@@ -134,10 +134,13 @@
                 @RawQuery
                 abstract public ${PagingTypeNames.DATA_SOURCE_FACTORY}<Integer, User> getOne();
                 """
-        ) { _, _ ->
-            // do nothing
-        }.failsToCompile()
-            .withErrorContaining(ProcessorErrors.OBSERVABLE_QUERY_NOTHING_TO_OBSERVE)
+        ) { _, invocation ->
+            invocation.assertCompilationResult {
+                hasErrorContaining(
+                    ProcessorErrors.OBSERVABLE_QUERY_NOTHING_TO_OBSERVE
+                )
+            }
+        }
     }
 
     @Test
@@ -147,10 +150,13 @@
                 @RawQuery
                 abstract public ${PagingTypeNames.POSITIONAL_DATA_SOURCE}<User> getOne();
                 """
-        ) { _, _ ->
-            // do nothing
-        }.failsToCompile()
-            .withErrorContaining(ProcessorErrors.OBSERVABLE_QUERY_NOTHING_TO_OBSERVE)
+        ) { _, invocation ->
+            invocation.assertCompilationResult {
+                hasErrorContaining(
+                    ProcessorErrors.OBSERVABLE_QUERY_NOTHING_TO_OBSERVE
+                )
+            }
+        }
     }
 
     @Test
@@ -163,7 +169,7 @@
                 """
         ) { _, _ ->
             // do nothing
-        }.compilesWithoutError()
+        }
     }
 
     @Test
@@ -192,7 +198,7 @@
             )
             assertThat(query.returnType.typeName, `is`(pojo))
             assertThat(query.observedTableNames, `is`(emptySet()))
-        }.compilesWithoutError()
+        }
     }
 
     @Test
@@ -202,10 +208,13 @@
                 @RawQuery
                 abstract public void foo(SupportSQLiteQuery query);
                 """
-        ) { _, _ ->
-        }.failsToCompile().withErrorContaining(
-            ProcessorErrors.RAW_QUERY_BAD_RETURN_TYPE
-        )
+        ) { _, invocation ->
+            invocation.assertCompilationResult {
+                hasErrorContaining(
+                    ProcessorErrors.RAW_QUERY_BAD_RETURN_TYPE
+                )
+            }
+        }
     }
 
     interface RawQuerySuspendUnitDao {
@@ -237,10 +246,13 @@
                 @RawQuery
                 abstract public int[] foo();
                 """
-        ) { _, _ ->
-        }.failsToCompile().withErrorContaining(
-            ProcessorErrors.RAW_QUERY_BAD_PARAMS
-        )
+        ) { _, invocation ->
+            invocation.assertCompilationResult {
+                hasErrorContaining(
+                    ProcessorErrors.RAW_QUERY_BAD_PARAMS
+                )
+            }
+        }
     }
 
     @Test
@@ -251,10 +263,11 @@
                 abstract public int[] foo(SupportSQLiteQuery query,
                                           SupportSQLiteQuery query2);
                 """
-        ) { _, _ ->
-        }.failsToCompile().withErrorContaining(
-            ProcessorErrors.RAW_QUERY_BAD_PARAMS
-        )
+        ) { _, invocation ->
+            invocation.assertCompilationResult {
+                hasErrorContaining(ProcessorErrors.RAW_QUERY_BAD_PARAMS)
+            }
+        }
     }
 
     @Test
@@ -264,10 +277,13 @@
                 @RawQuery
                 abstract public int[] foo(SupportSQLiteQuery... query);
                 """
-        ) { _, _ ->
-        }.failsToCompile().withErrorContaining(
-            ProcessorErrors.RAW_QUERY_BAD_PARAMS
-        )
+        ) { _, invocation ->
+            invocation.assertCompilationResult {
+                hasErrorContaining(
+                    ProcessorErrors.RAW_QUERY_BAD_PARAMS
+                )
+            }
+        }
     }
 
     @Test
@@ -277,10 +293,13 @@
                 @RawQuery(observedEntities = {${COMMON.NOT_AN_ENTITY_TYPE_NAME}.class})
                 abstract public int[] foo(SupportSQLiteQuery query);
                 """
-        ) { _, _ ->
-        }.failsToCompile().withErrorContaining(
-            ProcessorErrors.rawQueryBadEntity(COMMON.NOT_AN_ENTITY_TYPE_NAME)
-        )
+        ) { _, invocation ->
+            invocation.assertCompilationResult {
+                hasErrorContaining(
+                    ProcessorErrors.rawQueryBadEntity(COMMON.NOT_AN_ENTITY_TYPE_NAME)
+                )
+            }
+        }
     }
 
     @Test
@@ -300,7 +319,7 @@
                 """
         ) { method, _ ->
             assertThat(method.observedTableNames, `is`(setOf("User")))
-        }.compilesWithoutError()
+        }
     }
 
     @Test
@@ -317,56 +336,46 @@
                 """
         ) { method, _ ->
             assertThat(method.observedTableNames, `is`(setOf("User")))
-        }.compilesWithoutError()
+        }
     }
 
     private fun singleQueryMethod(
         vararg input: String,
-        handler: (RawQueryMethod, TestInvocation) -> Unit
-    ): CompileTester {
-        return Truth.assertAbout(JavaSourcesSubjectFactory.javaSources())
-            .that(
-                listOf(
-                    JavaFileObjects.forSourceString(
-                        "foo.bar.MyClass",
-                        DAO_PREFIX +
-                            input.joinToString("\n") +
-                            DAO_SUFFIX
-                    ),
-                    COMMON.LIVE_DATA, COMMON.COMPUTABLE_LIVE_DATA, COMMON.USER,
-                    COMMON.DATA_SOURCE_FACTORY, COMMON.POSITIONAL_DATA_SOURCE,
-                    COMMON.NOT_AN_ENTITY
-                )
-            )
-            .processedWith(
-                TestProcessor.builder()
-                    .forAnnotations(
-                        Query::class, Dao::class, ColumnInfo::class,
-                        Entity::class, PrimaryKey::class, RawQuery::class
+        handler: (RawQueryMethod, XTestInvocation) -> Unit
+    ) {
+        val inputSource = Source.java(
+            "foo.bar.MyClass",
+            DAO_PREFIX +
+                input.joinToString("\n") +
+                DAO_SUFFIX
+        )
+        val commonSources = listOf(
+            COMMON.LIVE_DATA, COMMON.COMPUTABLE_LIVE_DATA, COMMON.USER,
+            COMMON.DATA_SOURCE_FACTORY, COMMON.POSITIONAL_DATA_SOURCE,
+            COMMON.NOT_AN_ENTITY
+        ).toSources()
+        runProcessorTest(
+            sources = commonSources + inputSource
+        ) { invocation ->
+            val (owner, methods) = invocation.roundEnv
+                .getElementsAnnotatedWith(Dao::class.qualifiedName!!)
+                .filterIsInstance<XTypeElement>()
+                .map {
+                    Pair(
+                        it,
+                        it.getAllMethods().filter {
+                            it.hasAnnotation(RawQuery::class)
+                        }
                     )
-                    .nextRunHandler { invocation ->
-                        val (owner, methods) = invocation.roundEnv
-                            .getElementsAnnotatedWith(Dao::class.qualifiedName!!)
-                            .filterIsInstance<XTypeElement>()
-                            .map {
-                                Pair(
-                                    it,
-                                    it.getAllMethods().filter {
-                                        it.hasAnnotation(RawQuery::class)
-                                    }
-                                )
-                            }.first { it.second.isNotEmpty() }
-                        val parser = RawQueryMethodProcessor(
-                            baseContext = invocation.context,
-                            containing = owner.type,
-                            executableElement = methods.first()
-                        )
-                        val parsedQuery = parser.process()
-                        handler(parsedQuery, invocation)
-                        true
-                    }
-                    .build()
+                }.first { it.second.isNotEmpty() }
+            val parser = RawQueryMethodProcessor(
+                baseContext = invocation.context,
+                containing = owner.type,
+                executableElement = methods.first()
             )
+            val parsedQuery = parser.process()
+            handler(parsedQuery, invocation)
+        }
     }
 
     companion object {
diff --git a/room/compiler/src/test/kotlin/androidx/room/processor/RemoveUnusedColumnsTest.kt b/room/compiler/src/test/kotlin/androidx/room/processor/RemoveUnusedColumnsTest.kt
index d030b1b..76bcdd6 100644
--- a/room/compiler/src/test/kotlin/androidx/room/processor/RemoveUnusedColumnsTest.kt
+++ b/room/compiler/src/test/kotlin/androidx/room/processor/RemoveUnusedColumnsTest.kt
@@ -17,47 +17,51 @@
 package androidx.room.processor
 
 import COMMON
+import androidx.room.DatabaseProcessingStep
 import androidx.room.RewriteQueriesToDropUnusedColumns
-import androidx.room.RoomProcessor
-import com.google.common.truth.Truth.assertAbout
-import com.google.testing.compile.CompileTester
-import com.google.testing.compile.JavaFileObjects
-import com.google.testing.compile.JavaSourcesSubjectFactory
+import androidx.room.compiler.processing.util.CompilationResultSubject
+import androidx.room.compiler.processing.util.Source
+import androidx.room.compiler.processing.util.runProcessorTest
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.junit.runners.JUnit4
-import javax.tools.JavaFileObject
-import javax.tools.StandardLocation
 
 @RunWith(JUnit4::class)
 class RemoveUnusedColumnsTest {
 
     @Test
     fun noAnnotationGivesWarning() {
-        compile()
-            .withWarningCount(1)
-            .withWarningContaining("The query returns some columns [uid, ageColumn]")
+        compile { result ->
+            result.hasWarningContaining("The query returns some columns [uid, ageColumn]")
+            result.hasWarningCount(1)
+        }
     }
 
     @Test
     fun annotateMethod() {
         compile(
             annotateMethod = true
-        ).withWarningCount(0)
+        ) { result ->
+            result.hasNoWarnings()
+        }
     }
 
     @Test
     fun annotateDao() {
         compile(
             annotateDao = true
-        ).withWarningCount(0)
+        ) { result ->
+            result.hasNoWarnings()
+        }
     }
 
     @Test
     fun annotateDb() {
         compile(
             annotateDb = true
-        ).withWarningCount(0)
+        ) { result ->
+            result.hasNoWarnings()
+        }
     }
 
     @Test
@@ -65,8 +69,10 @@
         compile(
             annotateDb = true,
             enableExpandProjection = true
-        ).withWarningCount(1)
-            .withWarningContaining(ProcessorErrors.EXPAND_PROJECTION_ALONG_WITH_REMOVE_UNUSED)
+        ) { result ->
+            result.hasWarningContaining(ProcessorErrors.EXPAND_PROJECTION_ALONG_WITH_REMOVE_UNUSED)
+            result.hasWarningCount(1)
+        }
     }
 
     @Test
@@ -74,8 +80,10 @@
         compile(
             annotateMethod = true,
             enableExpandProjection = true
-        ).withWarningCount(1)
-            .withWarningContaining(ProcessorErrors.EXPAND_PROJECTION_ALONG_WITH_REMOVE_UNUSED)
+        ) { result ->
+            result.hasWarningContaining(ProcessorErrors.EXPAND_PROJECTION_ALONG_WITH_REMOVE_UNUSED)
+            result.hasWarningCount(1)
+        }
     }
 
     @Test
@@ -83,41 +91,44 @@
         compile(
             annotateDao = true,
             enableExpandProjection = true
-        ).withWarningCount(1)
-            .withWarningContaining(ProcessorErrors.EXPAND_PROJECTION_ALONG_WITH_REMOVE_UNUSED)
+        ) { result ->
+            result.hasWarningContaining(
+                ProcessorErrors.EXPAND_PROJECTION_ALONG_WITH_REMOVE_UNUSED
+            )
+            result.hasWarningCount(1)
+        }
     }
 
     private fun compile(
         annotateDb: Boolean = false,
         annotateDao: Boolean = false,
         annotateMethod: Boolean = false,
-        enableExpandProjection: Boolean = false
-    ): CompileTester.SuccessfulCompilationClause {
-        val jfos = dao(
+        enableExpandProjection: Boolean = false,
+        validate: (CompilationResultSubject) -> Unit,
+    ) {
+        val sources = dao(
             annotateDao = annotateDao,
             annotateDb = annotateDb,
             annotateMethod = annotateMethod
-        ) + COMMON.USER
-        return assertAbout(JavaSourcesSubjectFactory.javaSources())
-            .that(jfos)
-            .withCompilerOptions("-Xlint:-processing") // remove unclaimed annotation warnings
-            .also {
-                if (enableExpandProjection) {
-                    it.withCompilerOptions("-Aroom.expandProjection=true")
-                }
-            }
-            .processedWith(RoomProcessor())
-            .compilesWithoutError()
-            .also {
-                it.and()
-                    .generatesFileNamed(
-                        StandardLocation.CLASS_OUTPUT, "foo.bar", "MyDao_Impl.class"
-                    )
-                    .and()
-                    .generatesFileNamed(
-                        StandardLocation.CLASS_OUTPUT, "foo.bar", "MyDb_Impl.class"
-                    )
-            }
+        ) + Source.fromJavaFileObject(COMMON.USER)
+
+        runProcessorTest(
+            sources = sources,
+            createProcessingStep = {
+                DatabaseProcessingStep()
+            },
+            options = mapOf(
+                "room.expandProjection" to enableExpandProjection.toString()
+            )
+        ) { result ->
+            validate(result)
+            result.generatedSourceFileWithPath(
+                "foo/bar/MyDao_Impl.java"
+            )
+            result.generatedSourceFileWithPath(
+                "foo/bar/MyDb_Impl.java"
+            )
+        }
     }
 
     companion object {
@@ -125,14 +136,14 @@
             annotateDb: Boolean,
             annotateDao: Boolean,
             annotateMethod: Boolean
-        ): List<JavaFileObject> {
+        ): List<Source> {
             fun annotationText(enabled: Boolean) = if (enabled) {
                 "@${RewriteQueriesToDropUnusedColumns::class.java.canonicalName}"
             } else {
                 ""
             }
 
-            val pojo = JavaFileObjects.forSourceString(
+            val pojo = Source.java(
                 "foo.bar.Pojo",
                 """
                     package foo.bar;
@@ -142,7 +153,7 @@
                     }
                 """.trimIndent()
             )
-            val dao = JavaFileObjects.forSourceString(
+            val dao = Source.java(
                 "foo.bar.MyDao",
                 """
                     package foo.bar;
@@ -156,7 +167,7 @@
                     }
                 """.trimIndent()
             )
-            val db = JavaFileObjects.forSourceString(
+            val db = Source.java(
                 "foo.bar.MyDb",
                 """
                     package foo.bar;
diff --git a/room/compiler/src/test/kotlin/androidx/room/processor/ShortcutMethodProcessorTest.kt b/room/compiler/src/test/kotlin/androidx/room/processor/ShortcutMethodProcessorTest.kt
index 4621ccb..a105248 100644
--- a/room/compiler/src/test/kotlin/androidx/room/processor/ShortcutMethodProcessorTest.kt
+++ b/room/compiler/src/test/kotlin/androidx/room/processor/ShortcutMethodProcessorTest.kt
@@ -18,20 +18,18 @@
 
 import COMMON
 import androidx.room.Dao
+import androidx.room.compiler.processing.XMethodElement
+import androidx.room.compiler.processing.XType
+import androidx.room.compiler.processing.XTypeElement
+import androidx.room.compiler.processing.util.Source
+import androidx.room.compiler.processing.util.XTestInvocation
+import androidx.room.compiler.processing.util.runProcessorTest
 import androidx.room.ext.CommonTypeNames
 import androidx.room.ext.GuavaUtilConcurrentTypeNames
 import androidx.room.ext.RxJava2TypeNames
 import androidx.room.ext.RxJava3TypeNames
-import androidx.room.compiler.processing.XMethodElement
-import androidx.room.compiler.processing.XType
-import androidx.room.compiler.processing.XTypeElement
-import androidx.room.testing.TestInvocation
-import androidx.room.testing.TestProcessor
+import androidx.room.testing.context
 import androidx.room.vo.ShortcutMethod
-import com.google.common.truth.Truth
-import com.google.testing.compile.CompileTester
-import com.google.testing.compile.JavaFileObjects
-import com.google.testing.compile.JavaSourcesSubjectFactory
 import com.squareup.javapoet.ArrayTypeName
 import com.squareup.javapoet.ClassName
 import com.squareup.javapoet.ParameterizedTypeName
@@ -39,8 +37,7 @@
 import org.hamcrest.CoreMatchers.`is`
 import org.hamcrest.MatcherAssert.assertThat
 import org.junit.Test
-import toJFO
-import javax.tools.JavaFileObject
+import toSources
 import kotlin.reflect.KClass
 
 /**
@@ -70,10 +67,13 @@
                 @${annotation.java.canonicalName}
                 abstract public void foo();
                 """
-        ) { shortcut, _ ->
+        ) { shortcut, invocation ->
             assertThat(shortcut.name, `is`("foo"))
             assertThat(shortcut.parameters.size, `is`(0))
-        }.failsToCompile().withErrorContaining(noParamsError())
+            invocation.assertCompilationResult {
+                hasErrorContaining(noParamsError())
+            }
+        }
     }
 
     abstract fun noParamsError(): String
@@ -94,7 +94,7 @@
             assertThat(shortcut.entities.size, `is`(1))
             assertThat(shortcut.entities["user"]?.isPartialEntity, `is`(false))
             assertThat(shortcut.entities["user"]?.pojo?.typeName, `is`(USER_TYPE_NAME))
-        }.compilesWithoutError()
+        }
     }
 
     @Test
@@ -104,13 +104,16 @@
                 @${annotation.java.canonicalName}
                 abstract public void foo(NotAnEntity notValid);
                 """
-        ) { shortcut, _ ->
+        ) { shortcut, invocation ->
             assertThat(shortcut.name, `is`("foo"))
             assertThat(shortcut.parameters.size, `is`(1))
             assertThat(shortcut.entities.size, `is`(0))
-        }.failsToCompile().withErrorContaining(
-            ProcessorErrors.CANNOT_FIND_ENTITY_FOR_SHORTCUT_QUERY_PARAMETER
-        )
+            invocation.assertCompilationResult {
+                hasErrorContaining(
+                    ProcessorErrors.CANNOT_FIND_ENTITY_FOR_SHORTCUT_QUERY_PARAMETER
+                )
+            }
+        }
     }
 
     @Test
@@ -135,7 +138,7 @@
                 shortcut.parameters.map { it.name },
                 `is`(listOf("u1", "u2"))
             )
-        }.compilesWithoutError()
+        }
     }
 
     @Test
@@ -171,7 +174,7 @@
                 assertThat(param.pojoType?.typeName, `is`(USER_TYPE_NAME))
                 assertThat(shortcut.entities.size, `is`(1))
                 assertThat(shortcut.entities["users"]?.pojo?.typeName, `is`(USER_TYPE_NAME))
-            }.compilesWithoutError()
+            }
         }
     }
 
@@ -194,7 +197,7 @@
             )
             assertThat(shortcut.entities.size, `is`(1))
             assertThat(shortcut.entities["users"]?.pojo?.typeName, `is`(USER_TYPE_NAME))
-        }.compilesWithoutError()
+        }
     }
 
     @Test
@@ -219,7 +222,7 @@
             )
             assertThat(shortcut.entities.size, `is`(1))
             assertThat(shortcut.entities["users"]?.pojo?.typeName, `is`(USER_TYPE_NAME))
-        }.compilesWithoutError()
+        }
     }
 
     @Test
@@ -244,7 +247,7 @@
             )
             assertThat(shortcut.entities.size, `is`(1))
             assertThat(shortcut.entities["users"]?.pojo?.typeName, `is`(USER_TYPE_NAME))
-        }.compilesWithoutError()
+        }
     }
 
     @Test
@@ -270,7 +273,7 @@
             )
             assertThat(shortcut.entities.size, `is`(1))
             assertThat(shortcut.entities["users"]?.pojo?.typeName, `is`(USER_TYPE_NAME))
-        }.compilesWithoutError()
+        }
     }
 
     @Test
@@ -306,7 +309,7 @@
                 assertThat(shortcut.entities.size, `is`(2))
                 assertThat(shortcut.entities["u1"]?.pojo?.typeName, `is`(USER_TYPE_NAME))
                 assertThat(shortcut.entities["b1"]?.pojo?.typeName, `is`(BOOK_TYPE_NAME))
-            }.compilesWithoutError()
+            }
         }
     }
 
@@ -331,14 +334,19 @@
                 @${annotation.java.canonicalName}
                 abstract public $type foo(User user);
                 """
-            ) { _, _ ->
-            }.failsToCompile().withErrorContaining(invalidReturnTypeError())
+            ) { _, invocation ->
+                invocation.assertCompilationResult {
+                    hasErrorContaining(invalidReturnTypeError())
+                }
+            }
         }
     }
 
     @Test
     fun targetEntity() {
-        val usernameJfo = """
+        val usernameSource = Source.java(
+            "foo.bar.Username",
+            """
             package foo.bar;
             import androidx.room.*;
 
@@ -346,13 +354,14 @@
                 int uid;
                 String name;
             }
-        """.toJFO("foo.bar.Username")
+            """
+        )
         singleShortcutMethod(
             """
                 @${annotation.java.canonicalName}(entity = User.class)
                 abstract public int foo(Username username);
                 """,
-            additionalJFOs = listOf(usernameJfo)
+            additionalSources = listOf(usernameSource)
         ) { shortcut, _ ->
             assertThat(shortcut.name, `is`("foo"))
             assertThat(shortcut.parameters.size, `is`(1))
@@ -363,7 +372,7 @@
             assertThat(shortcut.entities["username"]?.isPartialEntity, `is`(true))
             assertThat(shortcut.entities["username"]?.entityTypeName, `is`(USER_TYPE_NAME))
             assertThat(shortcut.entities["username"]?.pojo?.typeName, `is`(USERNAME_TYPE_NAME))
-        }.compilesWithoutError()
+        }
     }
 
     @Test
@@ -374,12 +383,14 @@
                 abstract public int foo(User user);
                 """
         ) { _, _ ->
-        }.compilesWithoutError()
+        }
     }
 
     @Test
     fun targetEntityExtraColumn() {
-        val usernameJfo = """
+        val usernameSource = Source.java(
+            "foo.bar.Username",
+            """
             package foo.bar;
             import androidx.room.*;
 
@@ -388,22 +399,28 @@
                 String name;
                 long extraField;
             }
-        """.toJFO("foo.bar.Username")
+            """
+        )
         singleShortcutMethod(
             """
                 @${annotation.java.canonicalName}(entity = User.class)
                 abstract public int foo(Username username);
                 """,
-            additionalJFOs = listOf(usernameJfo)
-        ) { _, _ ->
-        }.failsToCompile().withErrorContaining(
-            ProcessorErrors.cannotFindAsEntityField("foo.bar.User")
-        )
+            additionalSources = listOf(usernameSource)
+        ) { _, invocation ->
+            invocation.assertCompilationResult {
+                hasErrorContaining(
+                    ProcessorErrors.cannotFindAsEntityField("foo.bar.User")
+                )
+            }
+        }
     }
 
     @Test
     fun targetEntityExtraColumnIgnored() {
-        val usernameJfo = """
+        val usernameSource = Source.java(
+            "foo.bar.Username",
+            """
             package foo.bar;
             import androidx.room.*;
 
@@ -413,20 +430,23 @@
                 @Ignore
                 long extraField;
             }
-        """.toJFO("foo.bar.Username")
+            """
+        )
         singleShortcutMethod(
             """
                 @${annotation.java.canonicalName}(entity = User.class)
                 abstract public int foo(Username username);
                 """,
-            additionalJFOs = listOf(usernameJfo)
+            additionalSources = listOf(usernameSource)
         ) { _, _ ->
-        }.compilesWithoutError()
+        }
     }
 
     @Test
     fun targetEntityWithEmbedded() {
-        val usernameJfo = """
+        val usernameSource = Source.java(
+            "foo.bar.Username",
+            """
             package foo.bar;
             import androidx.room.*;
 
@@ -435,8 +455,11 @@
                 @Embedded
                 Fullname name;
             }
-        """.toJFO("foo.bar.Username")
-        val fullnameJfo = """
+            """
+        )
+        val fullnameSource = Source.java(
+            "foo.bar.Fullname",
+            """
             package foo.bar;
             import androidx.room.*;
 
@@ -445,20 +468,23 @@
                 String firstName;
                 String lastName;
             }
-        """.toJFO("foo.bar.Fullname")
+            """
+        )
         singleShortcutMethod(
             """
                 @${annotation.java.canonicalName}(entity = User.class)
                 abstract public int foo(Username username);
                 """,
-            additionalJFOs = listOf(usernameJfo, fullnameJfo)
+            additionalSources = listOf(usernameSource, fullnameSource)
         ) { _, _ ->
-        }.compilesWithoutError()
+        }
     }
 
     @Test
     fun targetEntityWithRelation() {
-        val userPetsJfo = """
+        val userPetsSource = Source.java(
+            "foo.bar.UserPets",
+            """
             package foo.bar;
             import androidx.room.*;
             import java.util.List;
@@ -468,8 +494,11 @@
                 @Relation(parentColumn = "uid", entityColumn = "ownerId")
                 List<Pet> pets;
             }
-        """.toJFO("foo.bar.UserPets")
-        val petJfo = """
+            """
+        )
+        val petSource = Source.java(
+            "foo.bar.Pet",
+            """
             package foo.bar;
             import androidx.room.*;
 
@@ -479,15 +508,19 @@
                 int petId;
                 int ownerId;
             }
-        """.toJFO("foo.bar.Pet")
+            """
+        )
         singleShortcutMethod(
             """
                 @${annotation.java.canonicalName}(entity = User.class)
                 abstract public int foo(UserPets userPets);
                 """,
-            additionalJFOs = listOf(userPetsJfo, petJfo)
-        ) { _, _ ->
-        }.failsToCompile().withErrorContaining(ProcessorErrors.INVALID_RELATION_IN_PARTIAL_ENTITY)
+            additionalSources = listOf(userPetsSource, petSource)
+        ) { _, invocation ->
+            invocation.assertCompilationResult {
+                hasErrorContaining(ProcessorErrors.INVALID_RELATION_IN_PARTIAL_ENTITY)
+            }
+        }
     }
 
     @Test
@@ -496,13 +529,52 @@
             """
                 @${annotation.java.canonicalName}(entity = User.class)
                 abstract public int foo(long x);
-                """
-        ) { _, _ ->
-        }.failsToCompile().withErrorContaining(
-            ProcessorErrors.shortcutMethodArgumentMustBeAClass(
-                TypeName.LONG
-            )
+                """,
+        ) { _, invocation ->
+            invocation.assertCompilationResult {
+                if (invocation.isKsp) {
+                    hasErrorContaining(
+                        ProcessorErrors.noColumnsInPartialEntity(
+                            "java.lang.Long"
+                        )
+                    )
+                } else {
+                    // javac has a different error for primitives.
+                    hasErrorContaining(
+                        ProcessorErrors.shortcutMethodArgumentMustBeAClass(
+                            TypeName.LONG
+                        )
+                    )
+                }
+            }
+        }
+    }
+
+    @Test
+    fun targetEntity_emptyClassParameter() {
+        val emptyClass = Source.java(
+            "foo.bar.EmptyClass",
+            """
+            package foo.bar;
+            public class EmptyClass {}
+            """.trimIndent()
         )
+
+        singleShortcutMethod(
+            """
+                @${annotation.java.canonicalName}(entity = User.class)
+                abstract public int foo(EmptyClass x);
+                """,
+            additionalSources = listOf(emptyClass)
+        ) { _, invocation ->
+            invocation.assertCompilationResult {
+                hasErrorContaining(
+                    ProcessorErrors.noColumnsInPartialEntity(
+                        "foo.bar.EmptyClass"
+                    )
+                )
+            }
+        }
     }
 
     abstract fun invalidReturnTypeError(): String
@@ -515,47 +587,39 @@
 
     fun singleShortcutMethod(
         vararg input: String,
-        additionalJFOs: List<JavaFileObject> = emptyList(),
-        handler: (T, TestInvocation) -> Unit
-    ):
-        CompileTester {
-            return Truth.assertAbout(JavaSourcesSubjectFactory.javaSources())
-                .that(
-                    listOf(
-                        JavaFileObjects.forSourceString(
-                            "foo.bar.MyClass",
-                            DAO_PREFIX + input.joinToString("\n") + DAO_SUFFIX
-                        ),
-                        COMMON.USER, COMMON.BOOK, COMMON.NOT_AN_ENTITY, COMMON.RX2_COMPLETABLE,
-                        COMMON.RX2_MAYBE, COMMON.RX2_SINGLE, COMMON.RX3_COMPLETABLE,
-                        COMMON.RX3_MAYBE, COMMON.RX3_SINGLE, COMMON.LISTENABLE_FUTURE,
-                        COMMON.GUAVA_ROOM
-                    ) + additionalJFOs
-                )
-                .processedWith(
-                    TestProcessor.builder()
-                        .forAnnotations(annotation, Dao::class)
-                        .nextRunHandler { invocation ->
-                            val (owner, methods) = invocation.roundEnv
-                                .getElementsAnnotatedWith(Dao::class.qualifiedName!!)
-                                .filterIsInstance<XTypeElement>()
-                                .map {
-                                    Pair(
-                                        it,
-                                        it.getAllMethods().filter {
-                                            it.hasAnnotation(annotation)
-                                        }
-                                    )
-                                }.first { it.second.isNotEmpty() }
-                            val processed = process(
-                                baseContext = invocation.context,
-                                containing = owner.type,
-                                executableElement = methods.first()
-                            )
-                            handler(processed, invocation)
-                            true
+        additionalSources: List<Source> = emptyList(),
+        handler: (T, XTestInvocation) -> Unit
+    ) {
+        val inputSource = Source.java(
+            "foo.bar.MyClass",
+            DAO_PREFIX + input.joinToString("\n") + DAO_SUFFIX
+        )
+        val commonSources = listOf(
+            COMMON.USER, COMMON.BOOK, COMMON.NOT_AN_ENTITY, COMMON.RX2_COMPLETABLE,
+            COMMON.RX2_MAYBE, COMMON.RX2_SINGLE, COMMON.RX3_COMPLETABLE,
+            COMMON.RX3_MAYBE, COMMON.RX3_SINGLE, COMMON.LISTENABLE_FUTURE,
+            COMMON.GUAVA_ROOM
+        ).toSources()
+        runProcessorTest(
+            sources = commonSources + additionalSources + inputSource
+        ) { invocation ->
+            val (owner, methods) = invocation.roundEnv
+                .getElementsAnnotatedWith(Dao::class.qualifiedName!!)
+                .filterIsInstance<XTypeElement>()
+                .map {
+                    Pair(
+                        it,
+                        it.getAllMethods().filter {
+                            it.hasAnnotation(annotation)
                         }
-                        .build()
-                )
+                    )
+                }.first { it.second.isNotEmpty() }
+            val processed = process(
+                baseContext = invocation.context,
+                containing = owner.type,
+                executableElement = methods.first()
+            )
+            handler(processed, invocation)
         }
+    }
 }
diff --git a/room/compiler/src/test/kotlin/androidx/room/processor/UpdateMethodProcessorTest.kt b/room/compiler/src/test/kotlin/androidx/room/processor/UpdateMethodProcessorTest.kt
index 88def3e..e2a264a 100644
--- a/room/compiler/src/test/kotlin/androidx/room/processor/UpdateMethodProcessorTest.kt
+++ b/room/compiler/src/test/kotlin/androidx/room/processor/UpdateMethodProcessorTest.kt
@@ -19,6 +19,7 @@
 import androidx.room.Update
 import androidx.room.compiler.processing.XMethodElement
 import androidx.room.compiler.processing.XType
+import androidx.room.compiler.processing.util.Source
 import androidx.room.processor.ProcessorErrors.CANNOT_FIND_UPDATE_RESULT_ADAPTER
 import androidx.room.processor.ProcessorErrors.UPDATE_MISSING_PARAMS
 import androidx.room.vo.UpdateMethod
@@ -27,7 +28,6 @@
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.junit.runners.JUnit4
-import toJFO
 
 @Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN")
 @RunWith(JUnit4::class)
@@ -53,7 +53,7 @@
                 """
         ) { shortcut, _ ->
             assertThat(shortcut.onConflictStrategy, `is`(OnConflictStrategy.REPLACE))
-        }.compilesWithoutError()
+        }
     }
 
     @Test
@@ -63,32 +63,41 @@
                 @Update(onConflict = -1)
                 abstract public void foo(User user);
                 """
-        ) { _, _ ->
-        }.failsToCompile().withErrorContaining(ProcessorErrors.INVALID_ON_CONFLICT_VALUE)
+        ) { _, invocation ->
+            invocation.assertCompilationResult {
+                hasErrorContaining(ProcessorErrors.INVALID_ON_CONFLICT_VALUE)
+            }
+        }
     }
 
     @Test
     fun targetEntityMissingPrimaryKey() {
-        val usernameJfo = """
+        val usernameSource = Source.java(
+            "foo.bar.Username",
+            """
             package foo.bar;
             import androidx.room.*;
 
             public class Username {
                 String name;
             }
-        """.toJFO("foo.bar.Username")
+            """
+        )
         singleShortcutMethod(
             """
                 @Update(entity = User.class)
                 abstract public int foo(Username username);
                 """,
-            additionalJFOs = listOf(usernameJfo)
-        ) { _, _ ->
-        }.failsToCompile().withErrorContaining(
-            ProcessorErrors.missingPrimaryKeysInPartialEntityForUpdate(
-                partialEntityName = "foo.bar.Username",
-                primaryKeyNames = listOf("uid")
-            )
-        )
+            additionalSources = listOf(usernameSource)
+        ) { _, invocation ->
+            invocation.assertCompilationResult {
+                hasErrorContaining(
+                    ProcessorErrors.missingPrimaryKeysInPartialEntityForUpdate(
+                        partialEntityName = "foo.bar.Username",
+                        primaryKeyNames = listOf("uid")
+                    )
+                )
+            }
+        }
     }
 }
diff --git a/wear/wear-watchface-editor/src/androidTest/java/androidx/wear/watchface/editor/EditingSessionTest.kt b/wear/wear-watchface-editor/src/androidTest/java/androidx/wear/watchface/editor/EditingSessionTest.kt
index d2c8270..72cc697 100644
--- a/wear/wear-watchface-editor/src/androidTest/java/androidx/wear/watchface/editor/EditingSessionTest.kt
+++ b/wear/wear-watchface-editor/src/androidTest/java/androidx/wear/watchface/editor/EditingSessionTest.kt
@@ -69,6 +69,7 @@
 import kotlinx.coroutines.async
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.runBlocking
+import org.junit.After
 import org.junit.Assert.assertFalse
 import org.junit.Assert.assertTrue
 import org.junit.Assert.fail
@@ -394,6 +395,11 @@
         )
     }
 
+    @After
+    public fun tearDown() {
+        WatchFace.clearAllEditorDelegates()
+    }
+
     @Test
     public fun watchFaceComponentName() {
         val scenario = createOnWatchFaceEditingTestActivity(emptyList(), emptyList())
@@ -1042,7 +1048,7 @@
     }
 
     @Test
-    public fun closeEditorSessionBeforeWatchFaceDelegateCreated() {
+    public fun closeEditorSessionBeforeInitCompleted() {
         val session: ActivityScenario<OnWatchFaceEditingTestActivity> = ActivityScenario.launch(
             WatchFaceEditorContract().createIntent(
                 ApplicationProvider.getApplicationContext<Context>(),
@@ -1060,9 +1066,9 @@
             }
         )
 
-        session.onActivity { activity ->
+        session.onActivity {
             // This shouldn't throw an exception.
-            activity.editorSession.close()
+            EditorService.globalEditorService.closeEditor()
         }
     }
 }
diff --git a/wear/wear-watchface-editor/src/main/java/androidx/wear/watchface/editor/EditorSession.kt b/wear/wear-watchface-editor/src/main/java/androidx/wear/watchface/editor/EditorSession.kt
index 7e2dffe4..ac587a4 100644
--- a/wear/wear-watchface-editor/src/main/java/androidx/wear/watchface/editor/EditorSession.kt
+++ b/wear/wear-watchface-editor/src/main/java/androidx/wear/watchface/editor/EditorSession.kt
@@ -549,7 +549,7 @@
             editorDelegate.onDestroy()
         }
         // Revert any changes to the UserStyle if needed.
-        if (!commitChangesOnClose) {
+        if (!commitChangesOnClose && this::previousWatchFaceUserStyle.isInitialized) {
             userStyle = previousWatchFaceUserStyle
         }
     }
diff --git a/wear/wear-watchface/api/restricted_current.txt b/wear/wear-watchface/api/restricted_current.txt
index 7a3ec19..928f1a8 100644
--- a/wear/wear-watchface/api/restricted_current.txt
+++ b/wear/wear-watchface/api/restricted_current.txt
@@ -238,6 +238,7 @@
   public final class WatchFace {
     ctor public WatchFace(int watchFaceType, androidx.wear.watchface.style.CurrentUserStyleRepository currentUserStyleRepository, androidx.wear.watchface.Renderer renderer, optional androidx.wear.watchface.ComplicationsManager complicationsManager);
     ctor public WatchFace(int watchFaceType, androidx.wear.watchface.style.CurrentUserStyleRepository currentUserStyleRepository, androidx.wear.watchface.Renderer renderer);
+    method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) @UiThread @VisibleForTesting public static void clearAllEditorDelegates();
     method public androidx.wear.watchface.style.CurrentUserStyleRepository getCurrentUserStyleRepository();
     method public androidx.wear.watchface.WatchFace.LegacyWatchFaceOverlayStyle getLegacyWatchFaceStyle();
     method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) @UiThread public static kotlinx.coroutines.CompletableDeferred<androidx.wear.watchface.WatchFace.EditorDelegate> getOrCreateEditorDelegate(android.content.ComponentName componentName);
@@ -255,6 +256,7 @@
   }
 
   public static final class WatchFace.Companion {
+    method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) @UiThread @VisibleForTesting public void clearAllEditorDelegates();
     method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) @UiThread public kotlinx.coroutines.CompletableDeferred<androidx.wear.watchface.WatchFace.EditorDelegate> getOrCreateEditorDelegate(android.content.ComponentName componentName);
     method public boolean isLegacyWatchFaceOverlayStyleSupported();
     method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) @UiThread public void registerEditorDelegate(android.content.ComponentName componentName, androidx.wear.watchface.WatchFace.EditorDelegate editorDelegate);
diff --git a/wear/wear-watchface/src/androidTest/java/androidx/wear/watchface/test/WatchFaceControlServiceTest.kt b/wear/wear-watchface/src/androidTest/java/androidx/wear/watchface/test/WatchFaceControlServiceTest.kt
index afdb21f..f00e321 100644
--- a/wear/wear-watchface/src/androidTest/java/androidx/wear/watchface/test/WatchFaceControlServiceTest.kt
+++ b/wear/wear-watchface/src/androidTest/java/androidx/wear/watchface/test/WatchFaceControlServiceTest.kt
@@ -19,8 +19,14 @@
 import android.content.ComponentName
 import android.content.Context
 import android.content.Intent
+import android.graphics.Canvas
 import android.graphics.Color
+import android.graphics.Rect
+import android.icu.util.Calendar
+import android.os.Handler
+import android.os.Looper
 import android.support.wearable.watchface.SharedMemoryImage
+import android.view.SurfaceHolder
 import androidx.test.core.app.ApplicationProvider
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.MediumTest
@@ -28,8 +34,14 @@
 import androidx.test.screenshot.assertAgainstGolden
 import androidx.wear.complications.data.PlainComplicationText
 import androidx.wear.complications.data.ShortTextComplicationData
+import androidx.wear.watchface.CanvasType
 import androidx.wear.watchface.DrawMode
 import androidx.wear.watchface.RenderParameters
+import androidx.wear.watchface.Renderer
+import androidx.wear.watchface.WatchFace
+import androidx.wear.watchface.WatchFaceService
+import androidx.wear.watchface.WatchFaceType
+import androidx.wear.watchface.WatchState
 import androidx.wear.watchface.control.IHeadlessWatchFace
 import androidx.wear.watchface.control.IWatchFaceControlService
 import androidx.wear.watchface.control.WatchFaceControlService
@@ -43,18 +55,50 @@
 import androidx.wear.watchface.samples.EXAMPLE_OPENGL_COMPLICATION_ID
 import androidx.wear.watchface.samples.ExampleCanvasAnalogWatchFaceService
 import androidx.wear.watchface.samples.ExampleOpenGLWatchFaceService
+import androidx.wear.watchface.style.CurrentUserStyleRepository
+import androidx.wear.watchface.style.UserStyleSchema
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.android.asCoroutineDispatcher
+import kotlinx.coroutines.withContext
 import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
 
-private const val API_VERSION = 3
+// This service constructs a WatchFace with a task that's posted on the UI thread.
+internal class AsyncInitWithUiThreadTaskWatchFace : WatchFaceService() {
+    private val mainThreadCoroutineScope = CoroutineScope(
+        Handler(Looper.getMainLooper()).asCoroutineDispatcher()
+    )
+
+    override suspend fun createWatchFace(
+        surfaceHolder: SurfaceHolder,
+        watchState: WatchState
+    ): WatchFace = withContext(mainThreadCoroutineScope.coroutineContext) {
+        val currentUserStyleRepository =
+            CurrentUserStyleRepository(UserStyleSchema(emptyList()))
+        WatchFace(
+            WatchFaceType.DIGITAL,
+            CurrentUserStyleRepository(UserStyleSchema(emptyList())),
+            object : Renderer.CanvasRenderer(
+                surfaceHolder,
+                currentUserStyleRepository,
+                watchState,
+                CanvasType.SOFTWARE,
+                16
+            ) {
+                override fun render(canvas: Canvas, bounds: Rect, calendar: Calendar) {}
+            }
+        )
+    }
+}
 
 @RunWith(AndroidJUnit4::class)
 @MediumTest
-class WatchFaceControlServiceTest {
+public class WatchFaceControlServiceTest {
 
     @get:Rule
-    val screenshotRule = AndroidXScreenshotTestRule("wear/wear-watchface")
+    internal val screenshotRule = AndroidXScreenshotTestRule("wear/wear-watchface")
 
     private fun createInstance(width: Int, height: Int): IHeadlessWatchFace {
         val instanceService = IWatchFaceControlService.Stub.asInterface(
@@ -109,7 +153,7 @@
     }
 
     @Test
-    fun createHeadlessWatchFaceInstance() {
+    public fun createHeadlessWatchFaceInstance() {
         val instance = createInstance(100, 100)
         val bitmap = SharedMemoryImage.ashmemReadImageBundle(
             instance.renderWatchFaceToBitmap(
@@ -152,7 +196,7 @@
     }
 
     @Test
-    fun createHeadlessOpenglWatchFaceInstance() {
+    public fun createHeadlessOpenglWatchFaceInstance() {
         val instance = createOpenGlInstance(400, 400)
         val bitmap = SharedMemoryImage.ashmemReadImageBundle(
             instance.renderWatchFaceToBitmap(
@@ -186,7 +230,7 @@
     }
 
     @Test
-    fun testCommandTakeComplicationScreenShot() {
+    public fun testCommandTakeComplicationScreenShot() {
         val instance = createInstance(400, 400)
         val bitmap = SharedMemoryImage.ashmemReadImageBundle(
             instance.renderComplicationToBitmap(
@@ -215,4 +259,34 @@
 
         instance.release()
     }
+
+    @Test
+    public fun asyncInitWithUiThreadTaskWatchFace() {
+        val instanceService = IWatchFaceControlService.Stub.asInterface(
+            WatchFaceControlService().apply {
+                setContext(ApplicationProvider.getApplicationContext<Context>())
+            }.onBind(
+                Intent(WatchFaceControlService.ACTION_WATCHFACE_CONTROL_SERVICE)
+            )
+        )
+        // This shouldn't hang.
+        val headlessInstance = instanceService.createHeadlessWatchFaceInstance(
+            HeadlessWatchFaceInstanceParams(
+                ComponentName(
+                    ApplicationProvider.getApplicationContext<Context>(),
+                    AsyncInitWithUiThreadTaskWatchFace::class.java
+                ),
+                DeviceConfig(
+                    false,
+                    false,
+                    0,
+                    0
+                ),
+                100,
+                100
+            )
+        )
+
+        assertThat(headlessInstance.userStyleSchema.mSchema).isEmpty()
+    }
 }
diff --git a/wear/wear-watchface/src/main/java/androidx/wear/watchface/WatchFace.kt b/wear/wear-watchface/src/main/java/androidx/wear/watchface/WatchFace.kt
index e541719..6bdb080 100644
--- a/wear/wear-watchface/src/main/java/androidx/wear/watchface/WatchFace.kt
+++ b/wear/wear-watchface/src/main/java/androidx/wear/watchface/WatchFace.kt
@@ -164,6 +164,15 @@
             componentNameToEditorDelegate.remove(componentName)
         }
 
+        /** @hide */
+        @JvmStatic
+        @UiThread
+        @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+        @VisibleForTesting
+        public fun clearAllEditorDelegates() {
+            componentNameToEditorDelegate.clear()
+        }
+
         /**
          * For use by on watch face editors.
          * @hide
diff --git a/wear/wear-watchface/src/main/java/androidx/wear/watchface/control/WatchFaceControlService.kt b/wear/wear-watchface/src/main/java/androidx/wear/watchface/control/WatchFaceControlService.kt
index d0974e9..4be3d02 100644
--- a/wear/wear-watchface/src/main/java/androidx/wear/watchface/control/WatchFaceControlService.kt
+++ b/wear/wear-watchface/src/main/java/androidx/wear/watchface/control/WatchFaceControlService.kt
@@ -33,8 +33,9 @@
 import androidx.wear.watchface.control.data.HeadlessWatchFaceInstanceParams
 import androidx.wear.watchface.control.data.WallpaperInteractiveWatchFaceInstanceParams
 import androidx.wear.watchface.editor.EditorService
-import androidx.wear.watchface.runOnHandlerWithTracing
+import kotlinx.coroutines.android.asCoroutineDispatcher
 import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.withContext
 import java.io.FileDescriptor
 import java.io.PrintWriter
 
@@ -111,13 +112,19 @@
 
     override fun createHeadlessWatchFaceInstance(
         params: HeadlessWatchFaceInstanceParams
-    ): IHeadlessWatchFace? = uiThreadHandler.runOnHandlerWithTracing(
+    ): IHeadlessWatchFace? = TraceEvent(
         "IWatchFaceInstanceServiceStub.createHeadlessWatchFaceInstance"
-    ) {
+    ).use {
         val engine = createHeadlessEngine(params.watchFaceName, context)
         engine?.let {
             // This is serviced on a background thread so it should be fine to block.
-            runBlocking { it.createHeadlessInstance(params) }
+            runBlocking {
+                // However the WatchFaceService.createWatchFace method needs to be run on a UI
+                // thread.
+                withContext(uiThreadHandler.asCoroutineDispatcher().immediate) {
+                    it.createHeadlessInstance(params)
+                }
+            }
         }
     }