Merge "Use Gradle daemons in CI." into androidx-main
diff --git a/PREUPLOAD.cfg b/PREUPLOAD.cfg
index f0ae583..619fe797 100644
--- a/PREUPLOAD.cfg
+++ b/PREUPLOAD.cfg
@@ -1,6 +1,6 @@
 [Hook Scripts]
 checkstyle_hook = ${REPO_ROOT}/prebuilts/checkstyle/checkstyle.py --sha ${PREUPLOAD_COMMIT} -c ${REPO_ROOT}/frameworks/support/development/checkstyle/config/support-lib.xml -p development/checkstyle/prebuilt/com.android.support.checkstyle.jar
-ktlint_hook = ${REPO_ROOT}/frameworks/support/gradlew -q -p ${REPO_ROOT}/frameworks/support --continue :ktlintCheckFile --configuration-cache --file=${PREUPLOAD_FILES_PREFIXED}
+ktlint_hook = ${REPO_ROOT}/frameworks/support/development/ktlint.sh --file=${PREUPLOAD_FILES_PREFIXED}
 warn_check_api = ${REPO_ROOT}/frameworks/support/development/apilint.py -f ${PREUPLOAD_FILES}
 
 [Builtin Hooks]
diff --git a/buildSrc/shared.gradle b/buildSrc/shared.gradle
index ae36e01..95df1b7 100644
--- a/buildSrc/shared.gradle
+++ b/buildSrc/shared.gradle
@@ -20,6 +20,7 @@
     // artifacts are put into these configurations than when those artifacts are put into their
     // corresponding configuration.
     cacheableApi {
+        canBeConsumed = false // not directly used to satisfy a project dependency
         attributes {
             attribute(Usage.USAGE_ATTRIBUTE, project.objects.named(Usage.class, Usage.JAVA_RUNTIME));
             attribute(Category.CATEGORY_ATTRIBUTE, project.objects.named(Category.class, Category.LIBRARY));
@@ -29,6 +30,7 @@
         }
     }
     cacheableImplementation {
+        canBeConsumed = false // not directly used to satisfy a project dependency
         attributes {
             attribute(Usage.USAGE_ATTRIBUTE, project.objects.named(Usage.class, Usage.JAVA_RUNTIME));
             attribute(Category.CATEGORY_ATTRIBUTE, project.objects.named(Category.class, Category.LIBRARY));
@@ -38,7 +40,9 @@
         }
         extendsFrom(project.configurations.cacheableApi)
     }
-    cacheableRuntimeOnly
+    cacheableRuntimeOnly {
+        canBeConsumed = false // not directly used to satisfy a project dependency
+    }
 }
 
 dependencies {
diff --git a/camera/camera-core/src/androidTest/java/androidx/camera/core/processing/DefaultSurfaceEffectTest.kt b/camera/camera-core/src/androidTest/java/androidx/camera/core/processing/DefaultSurfaceEffectTest.kt
index 76e86a3..a7a5d44 100644
--- a/camera/camera-core/src/androidTest/java/androidx/camera/core/processing/DefaultSurfaceEffectTest.kt
+++ b/camera/camera-core/src/androidTest/java/androidx/camera/core/processing/DefaultSurfaceEffectTest.kt
@@ -16,8 +16,6 @@
 
 package androidx.camera.core.processing
 
-import android.graphics.Matrix
-import android.graphics.Rect
 import android.hardware.camera2.CameraDevice.TEMPLATE_PREVIEW
 import android.media.ImageReader
 import android.os.Handler
@@ -28,7 +26,6 @@
 import androidx.camera.core.SurfaceRequest
 import androidx.camera.core.impl.DeferrableSurface
 import androidx.camera.core.impl.ImageFormatConstants
-import androidx.camera.core.impl.ImmediateSurface
 import androidx.camera.core.impl.utils.executor.CameraXExecutors
 import androidx.camera.testing.CameraUtil
 import androidx.camera.testing.HandlerUtil
@@ -110,7 +107,6 @@
     private lateinit var handler: Handler
     private lateinit var dispatcher: CoroutineDispatcher
     private val inputSurfaceRequestsToClose = mutableListOf()
-    private val settableSurfacesToClose = mutableMapOf()
     private val fakeCamera = FakeCamera()
 
     @After
@@ -128,10 +124,6 @@
         for (surfaceRequest in inputSurfaceRequestsToClose) {
             surfaceRequest.deferrableSurface.close()
         }
-        for ((settableSurface, sourceSurface) in settableSurfacesToClose) {
-            settableSurface.close()
-            sourceSurface.close()
-        }
     }
 
     @Test
@@ -193,9 +185,7 @@
     fun requestCloseAfterOnOutputSurface_closeSurfaceOutput() {
         // Arrange.
         createSurfaceEffect()
-        val sourceSurface = ImmediateSurface(mock(Surface::class.java))
-        val settableSurface = createSettableSurface(sourceSurface)
-        val surfaceOutput = createSurfaceOutput(settableSurface)
+        val surfaceOutput = createSurfaceOutput()
 
         // Act.
         surfaceEffect.onOutputSurface(surfaceOutput)
@@ -205,10 +195,6 @@
 
         // Assert.
         assertThat(surfaceOutput.isClosed).isTrue()
-
-        // Clean-up.
-        settableSurface.close()
-        sourceSurface.close()
     }
 
     @Test
@@ -332,9 +318,7 @@
                 }
             }
         }
-        val surfaceOutput = createSurfaceOutput(
-            settableSurface = createSettableSurface(ImmediateSurface(imageReader.surface))
-        )
+        val surfaceOutput = createSurfaceOutput(imageReader.surface)
         surfaceEffect.onOutputSurface(surfaceOutput)
 
         // Assert.
@@ -357,7 +341,8 @@
             TEMPLATE_PREVIEW,
             listOf(surface),
             null,
-            null)
+            null
+        )
     }
 
     private fun createSurfaceEffect(shaderProvider: ShaderProvider = ShaderProvider.DEFAULT) {
@@ -370,24 +355,14 @@
         }
     }
 
-    private fun createSurfaceOutput(settableSurface: SettableSurface = createSettableSurface()) =
-        SurfaceOutputImpl(settableSurface, IDENTITY_MATRIX)
-
-    private fun createSettableSurface(
-        source: DeferrableSurface = ImmediateSurface(mock(Surface::class.java))
-    ) = SettableSurface(
-        SurfaceEffect.PREVIEW,
-        Size(WIDTH, HEIGHT),
-        ImageFormatConstants.INTERNAL_DEFINED_IMAGE_FORMAT_PRIVATE,
-        Matrix(),
-        false,
-        Rect(0, 0, WIDTH, HEIGHT),
-        0,
-        false
-    ).apply {
-        settableSurfacesToClose[this] = source
-        setSource(source)
-    }
+    private fun createSurfaceOutput(surface: Surface = mock(Surface::class.java)) =
+        SurfaceOutputImpl(
+            surface,
+            SurfaceEffect.PREVIEW,
+            ImageFormatConstants.INTERNAL_DEFINED_IMAGE_FORMAT_PRIVATE,
+            Size(WIDTH, HEIGHT),
+            IDENTITY_MATRIX
+        )
 
     private fun getCustomFragmentShader(samplerVarName: String, fragCoordsVarName: String) =
         String.format(
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/impl/DeferrableSurface.java b/camera/camera-core/src/main/java/androidx/camera/core/impl/DeferrableSurface.java
index 1352d7f..064420e 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/impl/DeferrableSurface.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/impl/DeferrableSurface.java
@@ -256,7 +256,7 @@
      * 

This method is idempotent. Subsequent calls after the first invocation will have no * effect. */ - public final void close() { + public void close() { // If this gets set, then the surface will terminate CallbackToFutureAdapter.Completer terminationCompleter = null; synchronized (mLock) {

diff --git a/camera/camera-core/src/main/java/androidx/camera/core/processing/SettableSurface.java b/camera/camera-core/src/main/java/androidx/camera/core/processing/SettableSurface.java
index ffde0f7..7df74b7 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/processing/SettableSurface.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/processing/SettableSurface.java
@@ -16,29 +16,51 @@
 
 package androidx.camera.core.processing;
 
+import static androidx.camera.core.impl.utils.Threads.checkMainThread;
+import static androidx.camera.core.impl.utils.executor.CameraXExecutors.directExecutor;
+import static androidx.camera.core.impl.utils.executor.CameraXExecutors.mainThreadExecutor;
+
 import android.graphics.Matrix;
 import android.graphics.Rect;
+import android.media.ImageReader;
 import android.os.Build;
 import android.util.Size;
 import android.view.Surface;
 import android.view.SurfaceView;
 import android.view.TextureView;
 
+import androidx.annotation.MainThread;
 import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
 import androidx.annotation.RequiresApi;
+import androidx.camera.core.SurfaceEffect;
+import androidx.camera.core.SurfaceOutput;
 import androidx.camera.core.SurfaceRequest;
 import androidx.camera.core.UseCase;
 import androidx.camera.core.impl.CameraInternal;
 import androidx.camera.core.impl.DeferrableSurface;
 import androidx.camera.core.impl.utils.futures.Futures;
 import androidx.concurrent.futures.CallbackToFutureAdapter;
+import androidx.core.util.Preconditions;
 
 import com.google.common.util.concurrent.ListenableFuture;
 
-import java.util.concurrent.atomic.AtomicBoolean;
-
 /**
- * A {@link DeferrableSurface} with another {@link DeferrableSurface} as its source.
+ * An extended {@link DeferrableSurface} that links to external sources.
+ *
+ * 

This class is an extension of {@link DeferrableSurface} with additional info of the + * surface such as crop rect and transformation. It also provides mechanisms to link to external + * surface provider/consumer, and propagates Surface releasing/closure to linked provider/consumer. + * + *

An example of how {@link SettableSurface} connects an external surface provider to + * an external surface consumer: + *


+ * {@code PreviewView}(surface provider) <--> {@link SurfaceRequest} <--> {@link SettableSurface}
+ *     <--> {@link SurfaceOutput} --> {@link SurfaceEffect}(surface consumer)
+ * 
+ * + *

For the full workflow, please see {@code SettableSurfaceTest + * #linkBothProviderAndConsumer_surfaceAndResultsArePropagatedE2E} */ @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) public class SettableSurface extends DeferrableSurface { @@ -54,7 +76,13 @@ private final boolean mMirroring; private final int mTargets; - private final AtomicBoolean mHasSource; + // Guarded by main thread. + @Nullable + private SurfaceOutputImpl mConsumerToNotify; + // Guarded by main thread. + private boolean mHasProvider = false; + // Guarded by main thread. + private boolean mHasConsumer = false; /** * Please see the getters to understand the parameters. @@ -75,7 +103,6 @@ mCropRect = cropRect; mRotationDegrees = rotationDegrees; mMirroring = mirroring; - mHasSource = new AtomicBoolean(false); mSurfaceFuture = CallbackToFutureAdapter.getFuture( completer -> { mCompleter = completer; @@ -90,72 +117,143 @@ } /** - * Sets the internal source of this {@link SettableSurface}. + * Sets a {@link ListenableFuture} that provides the surface. * - *

This method is used to set a {@link DeferrableSurface} as the source of this - * {@link SettableSurface}. This is for organizing the pipeline internally. For example, using - * the output of one {@link UseCase} as the input of another {@link UseCase} for stream sharing. + *

{@link SettableSurface} uses the surface provided by the provider. This method is for + * organizing the pipeline internally, for example, using a {@link ImageReader} to provide + * the surface. * - *

It's also useful to link a new buffer request to an existing buffer, when the pipeline - * is rebuilt partially. Example: - * - *


-     * class NodeImplementation implements Node {
-     *   SurfaceOut apply(SurfaceIn surfaceIn) {
-     *       if (canInputBeHandledByTheCurrentBuffer(surfaceIn)) {
-     *           surfaceIn.getSurface().setSource(currentBuffer.getDeferrableSurface());
-     *           // TODO(b/234174360): need to "unset" the existing link.
-     *           return null;
-     *       }
-     *   }
-     * }
-     * 
- * - *

It throws {@link IllegalStateException} if the current {@link SettableSurface} - * already has a source. + *

It throws {@link IllegalStateException} if the current {@link SettableSurface} + * already has a provider. */ - public void setSource(@NonNull DeferrableSurface source) { - // TODO(b/234174360): propagate the #close() call to the source. Usually if the current - // DeferrableSurface is closed, the downstream one has to be closed as well. However, we - // should be able to "unset" the Surface too. e.g. when the pipeline is partially rebuilt - // during front/back camera switch VideoCapture, we don't want to propagate the close() - // call to the recording Surface. - if (mHasSource.compareAndSet(false, true)) { - Futures.propagate(source.getSurface(), mCompleter); - } else { - throw new IllegalStateException("The source has already been set."); - } + @MainThread + public void setProvider(@NonNull ListenableFuture surfaceFuture) { + checkMainThread(); + Preconditions.checkState(!mHasProvider, "Provider can only be linked once."); + mHasProvider = true; + Futures.propagate(surfaceFuture, mCompleter); } /** - * Creates a {@link SurfaceRequest} as the external source of this {@link SettableSurface}. + * Sets the {@link DeferrableSurface} that provides the surface. * - *

This method is used to request a {@link Surface} from an external source such as - * {@code PreviewView}. The {@link Surface} provided via - * {@link SurfaceRequest#provideSurface} will be used as the source of this - * {@link SettableSurface}. + *

Once connected, the parent (this {@link SettableSurface}) and the provider should be + * in sync on the following matters: 1) surface provision, 2) ref-counting, 3) closure and 4) + * termination. See the list below for details: + *

    + *
  • Surface. the provider and the parent share the same Surface object. + *
  • Ref-counting. The ref-count of the parent is managed by the surface consumer, which + * indicates whether it's safe to release the surface. The ref-count of the provider + * represents whether the parent is terminated. As long as the parent is not terminated, the + * provider cannot release the surface because someone might be accessing the surface. + *
  • Closure. When the parent is closed, if the surface is provided via {@link SurfaceOutput}, + * call {@link SurfaceOutputImpl#requestClose()} to decrease the ref-counter; if the + * surface is used by the camera-camera2, wait for the ref-counter to go to zero on its own. For + * the provider, closing after providing the surface has no effect; closing before + * providing the surface propagates the exception upstream. + *
  • Termination. On parent termination, close the provider and decrease the ref-count to + * notify that the Surface can be safely released. The provider cannot be terminated before the + * parent does. + *
* - *

-     * SurfaceOut surfaceOut = finalNode.apply(surfaceIn);
-     * SurfaceRequest surfaceRequest = surfaceOut.getSurfaces().get(0)
-     *     .createSurfaceRequestAsSource(camera, false);
-     * mSurfaceProviderExecutor.execute(
-     *     () -> surfaceProvider.onSurfaceRequested(surfaceRequest));
-     * 
+ *

This method is for organizing the pipeline internally, for example, using the output of + * one {@link UseCase} as the input of another {@link UseCase} for stream sharing. * *

It throws {@link IllegalStateException} if the current {@link SettableSurface} - * already has a source. + * already has a provider. + * + * @throws SurfaceClosedException when the provider is already closed. This should never + * happen. */ + @MainThread + public void setProvider(@NonNull DeferrableSurface provider) throws SurfaceClosedException { + checkMainThread(); + setProvider(provider.getSurface()); + provider.incrementUseCount(); + getTerminationFuture().addListener(() -> { + provider.decrementUseCount(); + provider.close(); + }, directExecutor()); + } + + /** + * Creates a {@link SurfaceRequest} that is linked to this {@link SettableSurface}. + * + *

The {@link SurfaceRequest} is for requesting a {@link Surface} from an external source + * such as {@code PreviewView} or {@code VideoCapture}. {@link SettableSurface} uses the + * {@link Surface} provided by {@link SurfaceRequest#provideSurface} as its source. For how + * the ref-counting works, please see the Javadoc of {@link #setProvider}. + * + *

It throws {@link IllegalStateException} if the current {@link SettableSurface} + * already has a provider. + */ + @MainThread @NonNull - public SurfaceRequest createSurfaceRequestAsSource(@NonNull CameraInternal cameraInternal, - boolean isRGBA8888Required) { - SurfaceRequest surfaceRequest = new SurfaceRequest(getSize(), cameraInternal, - isRGBA8888Required); - setSource(surfaceRequest.getDeferrableSurface()); + public SurfaceRequest createSurfaceRequest(@NonNull CameraInternal cameraInternal) { + checkMainThread(); + // TODO(b/238230154) figure out how to support HDR. + SurfaceRequest surfaceRequest = new SurfaceRequest(getSize(), cameraInternal, true); + try { + setProvider(surfaceRequest.getDeferrableSurface()); + } catch (SurfaceClosedException e) { + // This should never happen. We just created the SurfaceRequest. It can't be closed. + throw new AssertionError("Surface is somehow already closed", e); + } return surfaceRequest; } /** + * Creates a {@link SurfaceOutput} that is linked to this {@link SettableSurface}. + * + *

The {@link SurfaceOutput} is for providing a surface to an external target such + * as {@link SurfaceEffect}. + * + *

This method returns a {@link ListenableFuture} that completes when the + * {@link SettableSurface#getSurface()} completes. The {@link SurfaceOutput} contains the + * surface and ref-counts the {@link SettableSurface}. + * + *

Do not provide the {@link SurfaceOutput} to external target if the + * {@link ListenableFuture} fails. + */ + @MainThread + @NonNull + public ListenableFuture createSurfaceOutputFuture( + @NonNull float[] glTransformation) { + checkMainThread(); + Preconditions.checkState(!mHasConsumer, "Consumer can only be linked once."); + mHasConsumer = true; + return Futures.transformAsync(getSurface(), + surface -> { + Preconditions.checkNotNull(surface); + try { + incrementUseCount(); + } catch (SurfaceClosedException e) { + return Futures.immediateFailedFuture(e); + } + SurfaceOutputImpl surfaceOutputImpl = new SurfaceOutputImpl( + surface, getTargets(), getFormat(), getSize(), glTransformation); + surfaceOutputImpl.getCloseFuture().addListener(this::decrementUseCount, + directExecutor()); + mConsumerToNotify = surfaceOutputImpl; + return Futures.immediateFuture(surfaceOutputImpl); + }, mainThreadExecutor()); + } + + /** + * Closes the {@link DeferrableSurface} and notifies linked objects for the closure. + */ + @MainThread + @Override + public final void close() { + checkMainThread(); + super.close(); + if (mConsumerToNotify != null) { + mConsumerToNotify.requestClose(); + mConsumerToNotify = null; + } + } + + /** * This field indicates that what purpose the {@link Surface} will be used for. */ public int getTargets() {

diff --git a/camera/camera-core/src/main/java/androidx/camera/core/processing/SurfaceOutputImpl.java b/camera/camera-core/src/main/java/androidx/camera/core/processing/SurfaceOutputImpl.java
index 485eed7c..a8a91fa 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/processing/SurfaceOutputImpl.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/processing/SurfaceOutputImpl.java
@@ -29,10 +29,10 @@
 import androidx.camera.core.Logger;
 import androidx.camera.core.SurfaceEffect;
 import androidx.camera.core.SurfaceOutput;
-import androidx.camera.core.impl.DeferrableSurface;
-import androidx.core.util.Preconditions;
+import androidx.concurrent.futures.CallbackToFutureAdapter;
 
-import java.util.concurrent.ExecutionException;
+import com.google.common.util.concurrent.ListenableFuture;
+
 import java.util.concurrent.Executor;
 import java.util.concurrent.RejectedExecutionException;
 
@@ -41,7 +41,7 @@
  * A implementation of {@link SurfaceOutput} that wraps a {@link SettableSurface}.
  */
 @RequiresApi(21)
-public final class SurfaceOutputImpl implements SurfaceOutput {
+final class SurfaceOutputImpl implements SurfaceOutput {
 
     private static final String TAG = "SurfaceOutputImpl";
 
@@ -49,8 +49,10 @@
 
     @NonNull
     private final Surface mSurface;
+    private final int mTargets;
+    private final int mFormat;
     @NonNull
-    private final SettableSurface mSettableSurface;
+    private final Size mSize;
 
     @NonNull
     private final float[] mAdditionalTransform;
@@ -66,22 +68,29 @@
     @GuardedBy("mLock")
     private boolean mIsClosed = false;
 
-    /**
-     * @param settableSurface the state of settableSurface.getSurface() must be complete at the
-     *                        time of calling.
-     */
-    public SurfaceOutputImpl(
-            @NonNull SettableSurface settableSurface,
-            @NonNull float[] additionalTransform)
-            throws ExecutionException, InterruptedException,
-            DeferrableSurface.SurfaceClosedException {
-        mSettableSurface = settableSurface;
-        Preconditions.checkState(settableSurface.getSurface().isDone());
-        mSurface = settableSurface.getSurface().get();
-        mSettableSurface.incrementUseCount();
+    @NonNull
+    private final ListenableFuture mCloseFuture;
+    private CallbackToFutureAdapter.Completer mCloseFutureCompleter;
+
+    SurfaceOutputImpl(
+            @NonNull Surface surface,
+            // TODO(b/238222270): annotate targets with IntDef.
+            int targets,
+            int format,
+            @NonNull Size size,
+            @NonNull float[] additionalTransform) {
+        mSurface = surface;
+        mTargets = targets;
+        mFormat = format;
+        mSize = size;
         mAdditionalTransform = new float[16];
         System.arraycopy(additionalTransform, 0, mAdditionalTransform, 0,
                 additionalTransform.length);
+        mCloseFuture = CallbackToFutureAdapter.getFuture(
+                completer -> {
+                    mCloseFutureCompleter = completer;
+                    return "SurfaceOutputImpl close future complete";
+                });
     }
 
     /**
@@ -137,7 +146,7 @@
      */
     @Override
     public int getTargets() {
-        return mSettableSurface.getTargets();
+        return mTargets;
     }
 
     /**
@@ -146,7 +155,7 @@
     @Override
     @NonNull
     public Size getSize() {
-        return mSettableSurface.getSize();
+        return mSize;
     }
 
     /**
@@ -154,7 +163,7 @@
      */
     @Override
     public int getFormat() {
-        return mSettableSurface.getFormat();
+        return mFormat;
     }
 
     /**
@@ -166,14 +175,11 @@
     @Override
     public void close() {
         synchronized (mLock) {
-            if (mIsClosed) {
-                // Return early if it's already closed.
-                return;
-            } else {
+            if (!mIsClosed) {
                 mIsClosed = true;
             }
         }
-        mSettableSurface.decrementUseCount();
+        mCloseFutureCompleter.set(null);
     }
 
     /**
@@ -189,6 +195,14 @@
     }
 
     /**
+     * Gets a future that completes when the {@link SurfaceOutput} is closed.
+     */
+    @NonNull
+    public ListenableFuture getCloseFuture() {
+        return mCloseFuture;
+    }
+
+    /**
      * This method can be invoked by the effect implementation on any thread.
      */
     @AnyThread
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/processing/SettableSurfaceTest.kt b/camera/camera-core/src/test/java/androidx/camera/core/processing/SettableSurfaceTest.kt
index eecb931..5648bea 100644
--- a/camera/camera-core/src/test/java/androidx/camera/core/processing/SettableSurfaceTest.kt
+++ b/camera/camera-core/src/test/java/androidx/camera/core/processing/SettableSurfaceTest.kt
@@ -21,21 +21,28 @@
 import android.graphics.Rect
 import android.graphics.SurfaceTexture
 import android.os.Build
+import android.os.Looper.getMainLooper
 import android.util.Size
 import android.view.Surface
 import androidx.camera.core.SurfaceEffect
-import androidx.camera.core.impl.DeferrableSurface
-import androidx.camera.core.impl.utils.executor.CameraXExecutors
+import androidx.camera.core.SurfaceOutput
+import androidx.camera.core.SurfaceRequest
+import androidx.camera.core.SurfaceRequest.Result.RESULT_REQUEST_CANCELLED
+import androidx.camera.core.impl.DeferrableSurface.SurfaceClosedException
+import androidx.camera.core.impl.DeferrableSurface.SurfaceUnavailableException
+import androidx.camera.core.impl.ImmediateSurface
+import androidx.camera.core.impl.utils.executor.CameraXExecutors.mainThreadExecutor
+import androidx.camera.core.impl.utils.futures.FutureCallback
 import androidx.camera.core.impl.utils.futures.Futures
 import androidx.camera.testing.fakes.FakeCamera
-import com.google.common.truth.Truth
-import com.google.common.util.concurrent.ListenableFuture
-import java.util.concurrent.ExecutionException
+import androidx.concurrent.futures.CallbackToFutureAdapter
+import com.google.common.truth.Truth.assertThat
 import org.junit.After
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.robolectric.RobolectricTestRunner
+import org.robolectric.Shadows.shadowOf
 import org.robolectric.annotation.Config
 import org.robolectric.annotation.internal.DoNotInstrument
 
@@ -46,78 +53,203 @@
 @DoNotInstrument
 @Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
 class SettableSurfaceTest {
-    private lateinit var fakeDeferrableSurface: DeferrableSurface
-    private lateinit var fakeSettableSurface: SettableSurface
+
+    companion object {
+        private val IDENTITY_MATRIX = FloatArray(16).apply {
+            android.opengl.Matrix.setIdentityM(this, 0)
+        }
+    }
+
+    private lateinit var settableSurface: SettableSurface
+    private lateinit var fakeSurface: Surface
+    private lateinit var fakeSurfaceTexture: SurfaceTexture
 
     @Before
     fun setUp() {
-        fakeDeferrableSurface = createFakeDeferrableSurface()
-        fakeSettableSurface = createFakeSettableSurface()
+        settableSurface = SettableSurface(
+            SurfaceEffect.PREVIEW, Size(640, 480), ImageFormat.PRIVATE,
+            Matrix(), true, Rect(), 0, false
+        )
+        fakeSurfaceTexture = SurfaceTexture(0)
+        fakeSurface = Surface(fakeSurfaceTexture)
     }
 
     @After
     fun tearDown() {
-        fakeDeferrableSurface.close()
-        fakeSettableSurface.close()
+        settableSurface.close()
+        fakeSurfaceTexture.release()
+        fakeSurface.release()
     }
 
     @Test
-    @Throws(ExecutionException::class, InterruptedException::class)
-    fun setSource_surfaceIsPropagated() {
-        // Act.
-        fakeSettableSurface.setSource(fakeDeferrableSurface)
-        // Assert.
-        Truth.assertThat(fakeSettableSurface.surface.get())
-            .isEqualTo(fakeDeferrableSurface.surface.get())
-    }
-
-    @Test
-    @Throws(ExecutionException::class, InterruptedException::class)
-    fun createSurfaceRequestAsSource_surfaceIsPropagated() {
-        // Act.
-        val surfaceRequest = fakeSettableSurface.createSurfaceRequestAsSource(
-            FakeCamera(), false
-        )
-        surfaceRequest.provideSurface(
-            fakeDeferrableSurface.surface.get(),
-            CameraXExecutors.directExecutor()
-        ) { fakeDeferrableSurface.close() }
-        // Assert.
-        Truth.assertThat(fakeSettableSurface.surface.get())
-            .isEqualTo(fakeDeferrableSurface.surface.get())
-    }
-
-    @Test(expected = IllegalStateException::class)
-    fun setSourceTwice_throwsException() {
-        fakeSettableSurface.setSource(fakeDeferrableSurface)
-        fakeSettableSurface.setSource(fakeDeferrableSurface)
-    }
-
-    @Test(expected = IllegalStateException::class)
-    fun setSourceThenCreateSurfaceRequest_throwsException() {
-        fakeSettableSurface.setSource(fakeDeferrableSurface)
-        fakeSettableSurface.createSurfaceRequestAsSource(FakeCamera(), false)
-    }
-
-    private fun createFakeSettableSurface(): SettableSurface {
-        return SettableSurface(
-            SurfaceEffect.PREVIEW, Size(640, 480), ImageFormat.PRIVATE,
-            Matrix(), true, Rect(), 0, false
-        )
-    }
-
-    private fun createFakeDeferrableSurface(): DeferrableSurface {
-        val surfaceTexture = SurfaceTexture(0)
-        val surface = Surface(surfaceTexture)
-        val deferrableSurface = object : DeferrableSurface() {
-            override fun provideSurface(): ListenableFuture {
-                return Futures.immediateFuture(surface)
-            }
+    fun closeProviderAfterConnected_surfaceNotReleased() {
+        // Arrange.
+        val surfaceRequest = settableSurface.createSurfaceRequest(FakeCamera())
+        var result: SurfaceRequest.Result? = null
+        surfaceRequest.provideSurface(fakeSurface, mainThreadExecutor()) {
+            result = it
         }
-        deferrableSurface.terminationFuture.addListener({
-            surface.release()
-            surfaceTexture.release()
-        }, CameraXExecutors.directExecutor())
-        return deferrableSurface
+        // Act: close the provider
+        surfaceRequest.deferrableSurface.close()
+        shadowOf(getMainLooper()).idle()
+        // Assert: the surface is not released because the parent is not closed.
+        assertThat(result).isNull()
+    }
+
+    @Test(expected = SurfaceClosedException::class)
+    fun connectToClosedProvider_getsException() {
+        val closedDeferrableSurface = ImmediateSurface(fakeSurface).apply {
+            this.close()
+        }
+        settableSurface.setProvider(closedDeferrableSurface)
+    }
+
+    @Test
+    fun createSurfaceRequestAndCancel_cancellationIsPropagated() {
+        // Arrange: create a SurfaceRequest.
+        val surfaceRequest = settableSurface.createSurfaceRequest(FakeCamera())
+        var throwable: Throwable? = null
+        Futures.addCallback(settableSurface.surface, object : FutureCallback {
+            override fun onFailure(t: Throwable) {
+                throwable = t
+            }
+
+            override fun onSuccess(result: Surface?) {
+                throw IllegalStateException("Should not succeed.")
+            }
+        }, mainThreadExecutor())
+
+        // Act: set it as "will not provide".
+        surfaceRequest.willNotProvideSurface()
+        shadowOf(getMainLooper()).idle()
+
+        // Assert: the DeferrableSurface returns an error.
+        assertThat(throwable).isInstanceOf(SurfaceUnavailableException::class.java)
+    }
+
+    @Test
+    fun createSurfaceRequestWithClosedInstance_surfaceRequestCancelled() {
+        // Arrange: create a SurfaceRequest from a closed LinkableSurface
+        settableSurface.close()
+        val surfaceRequest = settableSurface.createSurfaceRequest(FakeCamera())
+
+        // Act: provide a Surface and get the result.
+        var result: SurfaceRequest.Result? = null
+        surfaceRequest.provideSurface(fakeSurface, mainThreadExecutor()) {
+            result = it
+        }
+        shadowOf(getMainLooper()).idle()
+
+        // Assert: the Surface is never used.
+        assertThat(result!!.resultCode).isEqualTo(RESULT_REQUEST_CANCELLED)
+    }
+
+    @Test
+    fun createSurfaceOutputWithClosedInstance_surfaceOutputNotCreated() {
+        // Arrange: create a SurfaceOutput future from a closed LinkableSurface
+        settableSurface.close()
+        val surfaceOutput = settableSurface.createSurfaceOutputFuture(IDENTITY_MATRIX)
+
+        // Act: wait for the SurfaceOutput to return.
+        var successful: Boolean? = null
+        Futures.addCallback(surfaceOutput, object : FutureCallback {
+            override fun onSuccess(result: SurfaceOutput?) {
+                successful = true
+            }
+
+            override fun onFailure(t: Throwable) {
+                successful = false
+            }
+        }, mainThreadExecutor())
+        shadowOf(getMainLooper()).idle()
+
+        // Assert: the SurfaceOutput is not created.
+        assertThat(successful!!).isEqualTo(false)
+    }
+
+    @Test
+    fun createSurfaceRequestAndProvide_surfaceIsPropagated() {
+        // Arrange: create a SurfaceRequest.
+        val surfaceRequest = settableSurface.createSurfaceRequest(FakeCamera())
+        // Act: provide Surface.
+        surfaceRequest.provideSurface(fakeSurface, mainThreadExecutor()) {}
+        shadowOf(getMainLooper()).idle()
+        // Assert: the surface is received.
+        assertThat(settableSurface.surface.isDone).isTrue()
+        assertThat(settableSurface.surface.get()).isEqualTo(fakeSurface)
+    }
+
+    @Test
+    fun setSourceSurfaceFutureAndProvide_surfaceIsPropagated() {
+        // Arrange: set a ListenableFuture as the source.
+        var completer: CallbackToFutureAdapter.Completer? = null
+        val surfaceFuture = CallbackToFutureAdapter.getFuture {
+            completer = it
+            return@getFuture null
+        }
+        settableSurface.setProvider(surfaceFuture)
+        // Act: provide Surface.
+        completer!!.set(fakeSurface)
+        shadowOf(getMainLooper()).idle()
+        // Assert: the surface is received.
+        assertThat(settableSurface.surface.isDone).isTrue()
+        assertThat(settableSurface.surface.get()).isEqualTo(fakeSurface)
+    }
+
+    @Test
+    fun linkBothProviderAndConsumer_surfaceAndResultsArePropagatedE2E() {
+        // Arrange: link a LinkableSurface with a SurfaceRequest and a SurfaceOutput.
+        val surfaceRequest = settableSurface.createSurfaceRequest(FakeCamera())
+        val surfaceOutputFuture = settableSurface.createSurfaceOutputFuture(IDENTITY_MATRIX)
+        var surfaceOutput: SurfaceOutput? = null
+        Futures.transform(surfaceOutputFuture, {
+            surfaceOutput = it
+        }, mainThreadExecutor())
+
+        // Act: provide a Surface via the SurfaceRequest.
+        var isSurfaceReleased = false
+        surfaceRequest.provideSurface(fakeSurface, mainThreadExecutor()) {
+            isSurfaceReleased = true
+        }
+        shadowOf(getMainLooper()).idle()
+
+        // Assert: SurfaceOutput is received and it contains the right Surface
+        assertThat(surfaceOutput).isNotNull()
+        var surfaceOutputCloseRequested = false
+        val surface = surfaceOutput!!.getSurface(mainThreadExecutor()) {
+            surfaceOutputCloseRequested = true
+        }
+        shadowOf(getMainLooper()).idle()
+        assertThat(surface).isEqualTo(fakeSurface)
+        assertThat(isSurfaceReleased).isEqualTo(false)
+
+        // Act: close the LinkableSurface, signaling the intention to close the Surface.
+        settableSurface.close()
+        shadowOf(getMainLooper()).idle()
+
+        // Assert: The close is propagated to the SurfaceRequest.
+        assertThat(surfaceOutputCloseRequested).isEqualTo(true)
+        assertThat(isSurfaceReleased).isEqualTo(false)
+
+        // Act: close the LinkableSurface, signaling it's safe to release the Surface.
+        surfaceOutput!!.close()
+        shadowOf(getMainLooper()).idle()
+
+        // Assert: The close is propagated to the SurfaceRequest.
+        assertThat(isSurfaceReleased).isEqualTo(true)
+    }
+
+    @Test(expected = IllegalStateException::class)
+    fun createSurfaceRequestTwice_throwsException() {
+        settableSurface.createSurfaceRequest(FakeCamera())
+        settableSurface.createSurfaceRequest(FakeCamera())
+        shadowOf(getMainLooper()).idle()
+    }
+
+    @Test(expected = IllegalStateException::class)
+    fun createSurfaceOutputTwice_throwsException() {
+        settableSurface.createSurfaceOutputFuture(IDENTITY_MATRIX)
+        settableSurface.createSurfaceOutputFuture(IDENTITY_MATRIX)
+        shadowOf(getMainLooper()).idle()
     }
 }
\ No newline at end of file
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/processing/SurfaceOutputImplTest.kt b/camera/camera-core/src/test/java/androidx/camera/core/processing/SurfaceOutputImplTest.kt
index b56b6f1..5e8863e 100644
--- a/camera/camera-core/src/test/java/androidx/camera/core/processing/SurfaceOutputImplTest.kt
+++ b/camera/camera-core/src/test/java/androidx/camera/core/processing/SurfaceOutputImplTest.kt
@@ -16,8 +16,7 @@
 
 package androidx.camera.core.processing
 
-import android.graphics.ImageFormat
-import android.graphics.Rect
+import android.graphics.PixelFormat
 import android.graphics.SurfaceTexture
 import android.opengl.Matrix
 import android.os.Build
@@ -49,10 +48,13 @@
             Matrix.setIdentityM(this, 0)
         }
         private const val FLOAT_TOLERANCE = 1E-4
+        private const val TARGET = SurfaceEffect.PREVIEW
+        private const val FORMAT = PixelFormat.RGBA_8888
+        private val SIZE = Size(640, 480)
     }
 
-    lateinit var fakeSurface: Surface
-    lateinit var fakeSurfaceTexture: SurfaceTexture
+    private lateinit var fakeSurface: Surface
+    private lateinit var fakeSurfaceTexture: SurfaceTexture
     private val surfacesToCleanup = mutableListOf()
     private val surfaceOutputsToCleanup = mutableListOf()
 
@@ -74,9 +76,15 @@
         }
     }
 
-    @Test(expected = java.lang.IllegalStateException::class)
-    fun createWithIncompleteFuture_throwsException() {
-        createFakeSurfaceOutputImpl(settableSurface = createIncompleteSettableSurface())
+    @Test
+    fun closeSurface_closeFutureIsDone() {
+        // Arrange.
+        val surfaceOutImpl = createFakeSurfaceOutputImpl()
+        // Act: close the SurfaceOutput.
+        surfaceOutImpl.close()
+        shadowOf(Looper.getMainLooper()).idle()
+        // Assert:
+        assertThat(surfaceOutImpl.closeFuture.isDone).isTrue()
     }
 
     @Test
@@ -147,30 +155,8 @@
             .containsExactly(floatArrayOf(2F, -1F, 0F, 1F))
     }
 
-    private fun createCompleteSettableSurface(): SettableSurface {
-        return createFakeSettableSurface(true)
-    }
-
-    private fun createIncompleteSettableSurface(): SettableSurface {
-        return createFakeSettableSurface(false)
-    }
-
-    private fun createFakeSettableSurface(setComplete: Boolean): SettableSurface {
-        val settableSurface = SettableSurface(
-            SurfaceEffect.PREVIEW, Size(640, 480), ImageFormat.PRIVATE,
-            android.graphics.Matrix(), true, Rect(), 0, false
-        )
-        if (setComplete) {
-            settableSurface.mCompleter.set(fakeSurface)
+    private fun createFakeSurfaceOutputImpl(transform: FloatArray = IDENTITY_MATRIX) =
+        SurfaceOutputImpl(fakeSurface, TARGET, FORMAT, SIZE, transform).apply {
+            surfaceOutputsToCleanup.add(this)
         }
-        surfacesToCleanup.add(settableSurface)
-        return settableSurface
-    }
-
-    private fun createFakeSurfaceOutputImpl(
-        settableSurface: SettableSurface = createCompleteSettableSurface(),
-        transform: FloatArray = IDENTITY_MATRIX
-    ) = SurfaceOutputImpl(settableSurface, transform).apply {
-        surfaceOutputsToCleanup.add(this)
-    }
 }
\ No newline at end of file
diff --git a/collection/collection/src/jvmTest/java/androidx/collection/ArrayMapTest.java b/collection/collection/src/jvmTest/java/androidx/collection/ArrayMapTest.java
deleted file mode 100644
index e61138d..0000000
--- a/collection/collection/src/jvmTest/java/androidx/collection/ArrayMapTest.java
+++ /dev/null
@@ -1,175 +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.
- */
-
-package androidx.collection;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNotSame;
-import static org.junit.Assert.assertSame;
-import static org.junit.Assert.assertTrue;
-import static org.junit.Assert.fail;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.junit.runners.JUnit4;
-
-import java.util.Arrays;
-import java.util.ConcurrentModificationException;
-import java.util.HashMap;
-import java.util.Map;
-
-@RunWith(JUnit4.class)
-public class ArrayMapTest {
-    ArrayMap mMap = new ArrayMap<>();
-
-    /**
-     * Attempt to generate a ConcurrentModificationException in ArrayMap.
-     * 

- * ArrayMap is explicitly documented to be non-thread-safe, yet it's easy to accidentally screw - * this up; ArrayMap should (in the spirit of the core Java collection types) make an effort to - * catch this and throw ConcurrentModificationException instead of crashing somewhere in its - * internals. - */ - @Test - public void testConcurrentModificationException() throws Exception { - final int testLenMs = 5000; - new Thread(new Runnable() { - @Override - public void run() { - int i = 0; - while (mMap != null) { - try { - mMap.put(String.format("key %d", i++), "B_DONT_DO_THAT"); - } catch (ArrayIndexOutOfBoundsException e) { - fail(e.getMessage()); - } catch (ClassCastException e) { - fail(e.getMessage()); - } catch (ConcurrentModificationException e) { - System.out.println("[successfully caught CME at put #" + i - + " size=" + (mMap == null ? "??" : String.valueOf(mMap.size())) - + "]"); - } - } - } - }).start(); - for (int i = 0; i < (testLenMs / 100); i++) { - try { - Thread.sleep(100); - mMap.clear(); - } catch (InterruptedException e) { - } catch (ArrayIndexOutOfBoundsException e) { - fail(e.getMessage()); - } catch (ClassCastException e) { - fail(e.getMessage()); - } catch (ConcurrentModificationException e) { - System.out.println( - "[successfully caught CME at clear #" - + i + " size=" + mMap.size() + "]"); - } - } - mMap = null; // will stop other thread - System.out.println(); - } - - /** - * Check to make sure the same operations behave as expected in a single thread. - */ - @Test - public void testNonConcurrentAccesses() throws Exception { - for (int i = 0; i < 100000; i++) { - try { - mMap.put(String.format("key %d", i++), "B_DONT_DO_THAT"); - if (i % 200 == 0) { - System.out.print("."); - } - if (i % 500 == 0) { - mMap.clear(); - System.out.print("X"); - } - } catch (ConcurrentModificationException e) { - fail(e.getMessage()); - } - } - } - - @Test - public void testIsSubclassOfSimpleArrayMap() { - Object map = new ArrayMap(); - assertTrue(map instanceof SimpleArrayMap); - } - - /** - * Regression test for ensure capacity: b/224971154 - */ - @Test - public void putAll() { - ArrayMap map = new ArrayMap<>(); - Map otherMap = new HashMap<>(); - otherMap.put("abc", "def"); - map.putAll(otherMap); - assertEquals(map.size(), 1); - assertEquals(map.keyAt(0), "abc"); - assertEquals(map.valueAt(0), "def"); - } - - @Test - public void toArray() { - Map map = new ArrayMap<>(); - map.put("a", 1); - String[] keys = map.keySet().toArray(new String[1]); - Integer[] values = map.values().toArray(new Integer[1]); - - assertEquals(Arrays.toString(keys), "[a]"); - assertEquals(Arrays.toString(values), "[1]"); - - // re-do again, it should re-use the same arrays - String[] keys2 = map.keySet().toArray(keys); - Integer[] values2 = map.values().toArray(values); - - assertSame(keys2, keys); - assertSame(values2, values); - assertEquals(Arrays.toString(keys2), "[a]"); - assertEquals(Arrays.toString(values2), "[1]"); - - // add new items - map.put("b", 2); - map.put("c", 3); - - // now it shouldn't re-use arrays because arrays are too small - String[] keys3 = map.keySet().toArray(keys); - Integer[] values3 = map.values().toArray(values); - - assertNotSame(values3, values); - assertNotSame(keys3, keys); - assertEquals(Arrays.toString(keys3), "[a, b, c]"); - assertEquals(Arrays.toString(values3), "[1, 2, 3]"); - - - map.remove("b"); - map.remove("c"); - - // arrays are big enough, re-use - String[] keys4 = map.keySet().toArray(keys3); - Integer[] values4 = map.values().toArray(values3); - - assertSame(values4, values3); - assertSame(keys4, keys3); - // https://docs.oracle.com/javase/8/docs/api/java/util/Set.html#toArray-T:A- - // only the next index is nulled in the array, per documentation. - assertEquals(Arrays.toString(keys4), "[a, null, c]"); - assertEquals(Arrays.toString(values4), "[1, null, 3]"); - } -}

diff --git a/collection/collection/src/jvmTest/kotlin/androidx/collection/ArrayMapTest.kt b/collection/collection/src/jvmTest/kotlin/androidx/collection/ArrayMapTest.kt
new file mode 100644
index 0000000..c38514e
--- /dev/null
+++ b/collection/collection/src/jvmTest/kotlin/androidx/collection/ArrayMapTest.kt
@@ -0,0 +1,167 @@
+/*
+ * 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.collection
+
+import androidx.collection.internal.Lock
+import androidx.collection.internal.synchronized
+import kotlin.random.Random
+import kotlin.test.Test
+import kotlin.test.assertContentEquals
+import kotlin.test.assertEquals
+import kotlin.test.assertTrue
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.DelicateCoroutinesApi
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.cancel
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.isActive
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.newFixedThreadPoolContext
+import kotlinx.coroutines.runBlocking
+
+internal class ArrayMapTest {
+
+    private val map = ArrayMap()
+
+    /**
+     * Attempt to generate a ConcurrentModificationException in ArrayMap.
+     *
+     * ArrayMap is explicitly documented to be non-thread-safe, yet it's easy to accidentally screw
+     * this up; ArrayMap should (in the spirit of the core Java collection types) make an effort to
+     * catch this and throw ConcurrentModificationException instead of crashing somewhere in its
+     * internals.
+     */
+    @OptIn(DelicateCoroutinesApi::class) // newFixedThreadPoolContext is delicate in jvm
+    @Test
+    fun testConcurrentModificationException() {
+        var error: Throwable? = null
+        val nThreads = 20
+        var nActiveThreads = 0
+        val lock = Lock()
+        val dispatcher = newFixedThreadPoolContext(nThreads = nThreads, name = "ArraySetTest")
+        val scope = CoroutineScope(dispatcher)
+
+        repeat(nThreads) {
+            scope.launch {
+                lock.synchronized { nActiveThreads++ }
+
+                while (isActive) {
+                    val add = Random.nextBoolean()
+                    try {
+                        if (add) {
+                            map[Random.nextLong().toString()] = "B_DONT_DO_THAT"
+                        } else {
+                            map.clear()
+                        }
+                    } catch (e: IndexOutOfBoundsException) {
+                        if (!add) {
+                            error = e
+                            throw e
+                        } // Expected exception otherwise
+                    } catch (e: ClassCastException) {
+                        error = e
+                        throw e
+                    } catch (ignored: ConcurrentModificationException) {
+                        // Expected exception
+                    }
+                }
+            }
+        }
+
+        runBlocking(Dispatchers.Default) {
+            // Wait until all worker threads are started
+            for (i in 0 until 100) {
+                if (lock.synchronized { nActiveThreads == nThreads }) {
+                    break
+                } else {
+                    delay(timeMillis = 10L)
+                }
+            }
+
+            // Allow the worker threads to run concurrently for some time
+            delay(timeMillis = 100L)
+        }
+
+        scope.cancel()
+        dispatcher.close()
+
+        error?.also { throw it }
+    }
+
+    /**
+     * Check to make sure the same operations behave as expected in a single thread.
+     */
+    @Test
+    fun testNonConcurrentAccesses() {
+        repeat(100_000) { i ->
+            map["key %i"] = "B_DONT_DO_THAT"
+            if (i % 500 == 0) {
+                map.clear()
+            }
+        }
+    }
+
+    @Test
+    fun testIsSubclassOfSimpleArrayMap() {
+        @Suppress("USELESS_IS_CHECK")
+        assertTrue(map is SimpleArrayMap<*, *>)
+    }
+
+    /**
+     * Regression test for ensure capacity: b/224971154
+     */
+    @Test
+    fun putAll() {
+        val otherMap = HashMap()
+        otherMap["abc"] = "def"
+        map.putAll(otherMap)
+        assertEquals(1, map.size)
+        assertEquals("abc", map.keyAt(0))
+        assertEquals("def", map.valueAt(0))
+    }
+
+    @Test
+    fun keys() {
+        val map = ArrayMap()
+
+        map["a"] = 1
+        assertEquals(map.keys, setOf("a"))
+
+        map["b"] = 2
+        map["c"] = 3
+        assertEquals(map.keys, setOf("a", "b", "c"))
+
+        map.remove("b")
+        map.remove("c")
+        assertEquals(map.keys, setOf("a"))
+    }
+
+    @Test
+    fun values() {
+        val map = ArrayMap()
+
+        map["a"] = 1
+        assertContentEquals(map.values, listOf(1))
+
+        map["b"] = 2
+        map["c"] = 3
+        assertContentEquals(map.values, listOf(1, 2, 3))
+
+        map.remove("b")
+        map.remove("c")
+        assertContentEquals(map.values, listOf(1))
+    }
+}
\ No newline at end of file
diff --git a/compose/foundation/foundation/api/current.txt b/compose/foundation/foundation/api/current.txt
index 199dc94..addbc86 100644
--- a/compose/foundation/foundation/api/current.txt
+++ b/compose/foundation/foundation/api/current.txt
@@ -673,6 +673,9 @@
   public final class LazyLayoutPrefetcher_androidKt {
   }
 
+  public final class LazyNearestItemsRangeKt {
+  }
+
   public final class Lazy_androidKt {
   }
 
diff --git a/compose/foundation/foundation/api/public_plus_experimental_current.txt b/compose/foundation/foundation/api/public_plus_experimental_current.txt
index e56d838..b97be26 100644
--- a/compose/foundation/foundation/api/public_plus_experimental_current.txt
+++ b/compose/foundation/foundation/api/public_plus_experimental_current.txt
@@ -779,6 +779,9 @@
   public final class LazyLayoutPrefetcher_androidKt {
   }
 
+  public final class LazyNearestItemsRangeKt {
+  }
+
   public final class Lazy_androidKt {
     method @androidx.compose.foundation.ExperimentalFoundationApi public static Object getDefaultLazyLayoutKey(int index);
   }
diff --git a/compose/foundation/foundation/api/restricted_current.txt b/compose/foundation/foundation/api/restricted_current.txt
index 199dc94..addbc86 100644
--- a/compose/foundation/foundation/api/restricted_current.txt
+++ b/compose/foundation/foundation/api/restricted_current.txt
@@ -673,6 +673,9 @@
   public final class LazyLayoutPrefetcher_androidKt {
   }
 
+  public final class LazyNearestItemsRangeKt {
+  }
+
   public final class Lazy_androidKt {
   }
 
diff --git a/compose/foundation/foundation/build.gradle b/compose/foundation/foundation/build.gradle
index bde17ce..9141f9a 100644
--- a/compose/foundation/foundation/build.gradle
+++ b/compose/foundation/foundation/build.gradle
@@ -36,7 +36,7 @@
          */
         api("androidx.annotation:annotation:1.1.0")
         api("androidx.compose.animation:animation:1.1.1")
-        api("androidx.compose.runtime:runtime:1.2.0-rc02")
+        api(project(":compose:runtime:runtime"))
         api("androidx.compose.ui:ui:1.2.0-rc02")
 
         implementation(libs.kotlinStdlibCommon)
diff --git a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/ScrollableFocusedChildDemo.kt b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/ScrollableFocusedChildDemo.kt
index bc7ba50..471a5f4 100644
--- a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/ScrollableFocusedChildDemo.kt
+++ b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/ScrollableFocusedChildDemo.kt
@@ -53,14 +53,13 @@
 import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.layout.Layout
-import androidx.compose.ui.text.font.FontStyle
 import androidx.compose.ui.tooling.preview.Preview
 import androidx.compose.ui.unit.Constraints
 import androidx.compose.ui.unit.IntOffset
 import androidx.compose.ui.unit.dp
 import kotlin.math.roundToInt
 
-@Preview
+@Preview(showBackground = true)
 @Composable
 fun ScrollableFocusedChildDemo() {
     val resizableState = remember { ResizableState() }
@@ -71,10 +70,6 @@
                 "to change its size. Try adjusting size while the box inside the resizable area " +
                 "is focused, and while it's not focused."
         )
-        Text(
-            "Resizing by dragging handle quickly is broken due to b/220119990.",
-            fontStyle = FontStyle.Italic
-        )
 
         Row {
             Button(
diff --git a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/BrushDemo.kt b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/BrushDemo.kt
index 4dcf255..1405a4a 100644
--- a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/BrushDemo.kt
+++ b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/BrushDemo.kt
@@ -177,21 +177,11 @@
     Text(
         buildAnnotatedString {
             withStyle(ParagraphStyle(textAlign = TextAlign.Right)) {
-                append(
-                    "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus condimentum" +
-                        " rhoncus est volutpat venenatis. Fusce semper, sapien ut venenatis" +
-                        " pellentesque, lorem dui aliquam sapien, non pharetra diam neque " +
-                        "id mi"
-                )
+                append(loremIpsum(wordCount = 29))
             }
 
             withStyle(ParagraphStyle(textAlign = TextAlign.Left)) {
-                append(
-                    "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus condimentum" +
-                        " rhoncus est volutpat venenatis. Fusce semper, sapien ut venenatis" +
-                        " pellentesque, lorem dui aliquam sapien, non pharetra diam neque " +
-                        "id mi"
-                )
+                append(loremIpsum(wordCount = 29))
             }
         },
         style = TextStyle(
@@ -217,10 +207,7 @@
         )
     )
     Text(
-        text = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus condimentum" +
-            " rhoncus est volutpat venenatis. Fusce semper, sapien ut venenatis" +
-            " pellentesque, lorem dui aliquam sapien, non pharetra diam neque " +
-            "id mi",
+        text = loremIpsum(wordCount = 29),
         style = TextStyle(
             brush = Brush.radialGradient(
                 *RainbowStops.zip(RainbowColors).toTypedArray(),
diff --git a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/ComposeMultiParagraph.kt b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/ComposeMultiParagraph.kt
index 2b37cd3..4405d9c 100644
--- a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/ComposeMultiParagraph.kt
+++ b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/ComposeMultiParagraph.kt
@@ -29,8 +29,7 @@
 import androidx.compose.ui.tooling.preview.Preview
 import androidx.compose.ui.unit.sp
 
-val lorem = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Maecenas fermentum non" +
-    " diam sed pretium."
+val lorem = loremIpsum(wordCount = 14)
 
 @Preview
 @Composable
diff --git a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/DrawTextDemo.kt b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/DrawTextDemo.kt
index aaae8f1..5424cce 100644
--- a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/DrawTextDemo.kt
+++ b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/DrawTextDemo.kt
@@ -130,10 +130,7 @@
 
         drawText(
             textMeasurer,
-            text = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin fringilla " +
-                "laoreet aliquam. Aliquam ut nisl aliquet, laoreet tellus quis, sagittis enim. " +
-                "Sed sem dolor, tempus blandit purus suscipit, convallis tincidunt purus. Donec " +
-                "mattis placerat arcu sed consectetur. Pellentesque eu turpis lacus.",
+            text = loremIpsum(wordCount = 41),
             topLeft = Offset(padding, padding),
             style = TextStyle(fontSize = fontSize6),
             overflow = TextOverflow.Visible,
@@ -220,7 +217,8 @@
         Canvas(
             Modifier
                 .fillMaxWidth()
-                .height(100.dp)) {
+                .height(100.dp)
+        ) {
             drawRect(brush = Brush.linearGradient(RainbowColors))
             val padding = 16.dp.toPx()
 
diff --git a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/FullScreenTextFieldDemo.kt b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/FullScreenTextFieldDemo.kt
new file mode 100644
index 0000000..ab5d1e1
--- /dev/null
+++ b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/FullScreenTextFieldDemo.kt
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.foundation.demos.text
+
+import androidx.compose.foundation.border
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.material.MaterialTheme
+import androidx.compose.material.TextField
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+
+/** Demonstrates b/237190748. */
+@Composable
+fun FullScreenTextFieldDemo() {
+    var value by remember { mutableStateOf(loremIpsum()) }
+    TextField(
+        value,
+        onValueChange = { value = it },
+        modifier = Modifier
+            .border(3.dp, MaterialTheme.colors.primary)
+            .fillMaxSize()
+    )
+}
\ No newline at end of file
diff --git a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/LoremIpsum.kt b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/LoremIpsum.kt
new file mode 100644
index 0000000..9daf80a
--- /dev/null
+++ b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/LoremIpsum.kt
@@ -0,0 +1,61 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.foundation.demos.text
+
+private val LoremIpsumWords = """
+    Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque a egestas nisi. Aenean
+    aliquam neque lacus, ac sollicitudin risus consequat in. Pellentesque habitant morbi tristique
+    senectus et netus et malesuada fames ac turpis egestas. In id nulla quam. Ut lobortis justo
+    purus, nec viverra diam gravida nec. Duis scelerisque feugiat ante, vitae semper leo. Donec
+    tempor aliquet lacus rhoncus tristique. In pulvinar sem ac velit suscipit fermentum. Donec
+    quis dolor ut enim sollicitudin laoreet. Morbi ac lectus pharetra, condimentum lectus
+    congue, euismod ante.
+
+    Phasellus vitae varius mi. Pellentesque non lacus at erat posuere pharetra a nec lorem. Vivamus
+    posuere massa sed ultricies euismod. Fusce luctus tristique diam at luctus. Nulla efficitur
+    fermentum dolor, nec hendrerit diam rhoncus ut. Donec fermentum, enim ultrices pharetra
+    suscipit, ex ligula iaculis ligula, in aliquam erat felis id magna. In est dolor, consequat
+    sed eros non, varius consequat velit. Cras blandit urna et vulputate dapibus. Donec
+    efficitur laoreet condimentum. Pellentesque habitant morbi tristique senectus et netus et
+    malesuada fames ac turpis egestas. Nullam scelerisque vitae est id pellentesque.
+
+    Aenean in ipsum rhoncus, dignissim lorem ac, consectetur quam. Phasellus id velit nec velit
+    dictum eleifend at eu nibh. Integer feugiat turpis quis dui pharetra, suscipit ornare turpis
+    aliquam. Phasellus tristique ullamcorper placerat. Aenean aliquet maximus tortor eu posuere.
+    Curabitur semper libero ut libero lacinia tempor. Duis tempor mattis nulla in malesuada.
+    Mauris rutrum commodo lacus. Integer pellentesque odio at eleifend vulputate. Donec eu erat
+    ut neque porttitor vehicula.
+
+    Duis est nisl, consequat sed ipsum eget, cursus auctor mauris. Vestibulum mattis sem et molestie
+    blandit. Donec tincidunt enim nibh, nec cursus magna feugiat id. Nulla facilisi. Suspendisse
+    ultricies lobortis ex, at vestibulum risus. Aliquam congue viverra justo vitae mollis. In
+    elementum venenatis eros non tincidunt. Pellentesque vitae sem dui. Quisque magna lacus,
+    dignissim sed viverra ut, sagittis quis turpis.
+
+    Aenean cursus id dolor sed convallis. Interdum et malesuada fames ac ante ipsum primis in
+    faucibus. Cras sodales ante et rhoncus commodo. In hac habitasse platea dictumst. Aenean
+    efficitur felis in elementum sollicitudin. In neque urna, tincidunt et lorem nec, commodo
+    maximus urna. Suspendisse tincidunt, felis ac viverra ultrices, nulla ipsum ornare nisi,
+    eleifend luctus tellus felis nec orci. Maecenas sed venenatis urna. Nulla tempor ultricies
+    lorem eget finibus. Nunc malesuada nisi id volutpat lobortis. Sed aliquet massa eu ex
+    eleifend efficitur. Curabitur accumsan vestibulum ligula sed aliquet. Nulla pretium dui id
+    nunc ultricies, id porttitor lorem pretium. Lorem ipsum dolor sit amet, consectetur
+    adipiscing elit.
+""".trimIndent().split("""\s""".toRegex())
+
+fun loremIpsum(wordCount: Int = LoremIpsumWords.size): String =
+    LoremIpsumWords.joinToString(separator = " ", limit = wordCount)
\ No newline at end of file
diff --git a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/TextDemos.kt b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/TextDemos.kt
index d510c3b..781fcef 100644
--- a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/TextDemos.kt
+++ b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/TextDemos.kt
@@ -58,7 +58,8 @@
                     DialogInputFieldDemo(onNavigateUp)
                 },
                 ComposableDemo("Inside scrollable") { TextFieldsInScrollableDemo() },
-                ComposableDemo("Cursor configuration") { TextFieldCursorBlinkingDemo() }
+                ComposableDemo("Cursor configuration") { TextFieldCursorBlinkingDemo() },
+                ComposableDemo("Full-screen field") { FullScreenTextFieldDemo() },
             )
         ),
         ComposableDemo("Text Accessibility") { TextAccessibilityDemo() }
diff --git a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/TextFieldWIthScroller.kt b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/TextFieldWIthScroller.kt
index f94fc35..7d8c550 100644
--- a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/TextFieldWIthScroller.kt
+++ b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/TextFieldWIthScroller.kt
@@ -27,9 +27,7 @@
 import androidx.compose.ui.tooling.preview.Preview
 import androidx.compose.ui.unit.dp
 
-private val LongText = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam pellentesque" +
-    " arcu quis diam malesuada pulvinar. In id condimentum metus. Suspendisse potenti. " +
-    "Praesent arcu tortor, ultrices ut vehicula sit amet, accumsan id sem."
+private val LongText = loremIpsum(wordCount = 32)
 
 @Preview
 @Composable
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/ScrollableFocusableInteractionTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/ScrollableFocusableInteractionTest.kt
index 6c7f3cb..dd85516 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/ScrollableFocusableInteractionTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/ScrollableFocusableInteractionTest.kt
@@ -30,6 +30,7 @@
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.focus.FocusRequester
 import androidx.compose.ui.focus.focusRequester
+import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.platform.isDebugInspectorInfoEnabled
 import androidx.compose.ui.platform.testTag
@@ -42,6 +43,7 @@
 import androidx.compose.ui.test.assertTopPositionInRootIsEqualTo
 import androidx.compose.ui.test.junit4.createComposeRule
 import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.performTouchInput
 import androidx.compose.ui.unit.Dp
 import androidx.compose.ui.unit.dp
 import androidx.test.filters.MediumTest
@@ -184,6 +186,146 @@
     }
 
     @Test
+    fun scrollsFocusedFocusableIntoView_whenViewportAnimatedQuickly() {
+        var viewportSize by mutableStateOf(100.dp)
+
+        rule.setContent {
+            TestScrollableColumn(size = viewportSize) {
+                // Put a focusable in the bottom of the viewport.
+                Spacer(Modifier.size(90.dp))
+                TestFocusable(size = 10.dp)
+            }
+        }
+        requestFocusAndScrollToTop()
+        rule.onNodeWithTag(focusableTag)
+            .assertScrollAxisPositionInRootIsEqualTo(90.dp)
+            .assertIsDisplayed()
+            .assertIsFocused()
+
+        // Manually execute an "animation" that shrinks the viewport by twice the focusable's size
+        // on every frame, for a few frames. The underlying bug in b/230756508 would lose track
+        // of the focusable after the second frame.
+        rule.mainClock.autoAdvance = false
+        viewportSize = 80.dp
+        rule.mainClock.advanceTimeByFrame()
+        rule.waitForIdle()
+        viewportSize = 60.dp
+        rule.mainClock.advanceTimeByFrame()
+        rule.waitForIdle()
+        viewportSize = 40.dp
+        rule.mainClock.advanceTimeByFrame()
+        rule.waitForIdle()
+
+        // Resume the clock. The scroll animation should finish.
+        rule.mainClock.autoAdvance = true
+
+        rule.onNodeWithTag(focusableTag)
+            .assertIsDisplayed()
+    }
+
+    @Test
+    fun scrollFromViewportShrink_isInterrupted_byGesture() {
+        var viewportSize by mutableStateOf(100.dp)
+
+        rule.setContent {
+            TestScrollableColumn(size = viewportSize) {
+                // Put a focusable in the bottom of the viewport.
+                Spacer(Modifier.size(90.dp))
+                TestFocusable(size = 10.dp)
+            }
+        }
+        requestFocusAndScrollToTop()
+        rule.onNodeWithTag(focusableTag)
+            .assertScrollAxisPositionInRootIsEqualTo(90.dp)
+            .assertIsDisplayed()
+            .assertIsFocused()
+
+        // Shrink the viewport to start the scroll animation.
+        rule.mainClock.autoAdvance = false
+        viewportSize = 80.dp
+        // Run the first frame of the scroll animation.
+        rule.mainClock.advanceTimeByFrame()
+        rule.waitForIdle()
+
+        // Interrupt the scroll by manually dragging.
+        rule.onNodeWithTag(scrollableAreaTag)
+            .performTouchInput {
+                down(center)
+                moveBy(Offset(viewConfiguration.touchSlop + 1, viewConfiguration.touchSlop + 1))
+                up()
+            }
+
+        // Resume the clock. The animation scroll animation should have been interrupted and not
+        // continue.
+        rule.mainClock.advanceTimeByFrame()
+        rule.mainClock.autoAdvance = true
+
+        rule.onNodeWithTag(focusableTag)
+            .assertIsNotDisplayed()
+    }
+
+    /**
+     * This test ensures that scrollable correctly cleans up its state when the scroll animation
+     * triggered by shrinking the viewport is interrupted by something other than another shrink
+     * and the focusable child does not change. If it's cleaned up correctly, expanding then re-
+     * shrinking the viewport should trigger another animation.
+     */
+    @Test
+    fun scrollsFocusedFocusableIntoView_whenViewportExpandedThenReshrunk_afterInterruption() {
+        var viewportSize by mutableStateOf(100.dp)
+
+        rule.setContent {
+            TestScrollableColumn(size = viewportSize) {
+                // Put a focusable in the bottom of the viewport.
+                Spacer(Modifier.size(90.dp))
+                TestFocusable(size = 10.dp)
+            }
+        }
+        requestFocusAndScrollToTop()
+        rule.onNodeWithTag(focusableTag)
+            .assertScrollAxisPositionInRootIsEqualTo(90.dp)
+            .assertIsDisplayed()
+            .assertIsFocused()
+
+        // Shrink the viewport to start the scroll animation.
+        rule.mainClock.autoAdvance = false
+        viewportSize = 80.dp
+        // Run the first frame of the scroll animation.
+        rule.mainClock.advanceTimeByFrame()
+        rule.waitForIdle()
+
+        // Interrupt the scroll by manually dragging.
+        rule.onNodeWithTag(scrollableAreaTag)
+            .performTouchInput {
+                down(center)
+                moveBy(Offset(viewConfiguration.touchSlop + 1, viewConfiguration.touchSlop + 1))
+                up()
+            }
+
+        // Resume the clock. The animation scroll animation should have been interrupted and not
+        // continue.
+        rule.mainClock.advanceTimeByFrame()
+        rule.mainClock.autoAdvance = true
+        rule.waitForIdle()
+        rule.onNodeWithTag(focusableTag)
+            .assertIsFocused()
+            .assertIsNotDisplayed()
+
+        // Expand the viewport back to its original size to bring the focusable back into view.
+        viewportSize = 100.dp
+        rule.waitForIdle()
+        Thread.sleep(2000)
+
+        // Shrink the viewport again, this should trigger another scroll animation to keep the
+        // scrollable in view.
+        viewportSize = 50.dp
+        rule.waitForIdle()
+
+        rule.onNodeWithTag(focusableTag)
+            .assertIsDisplayed()
+    }
+
+    @Test
     fun doesNotScrollFocusedFocusableIntoView_whenNotInViewAndViewportShrunk() {
         var viewportSize by mutableStateOf(100.dp)
 
@@ -269,6 +411,62 @@
             .assertIsDisplayed()
     }
 
+    @Test
+    fun scrollsToNewFocusable_whenFocusedChildChangesDuringAnimation() {
+        var viewportSize by mutableStateOf(100.dp)
+        val focusRequester1 = FocusRequester()
+        val focusRequester2 = FocusRequester()
+
+        rule.setContent {
+            TestScrollableColumn(size = viewportSize) {
+                Box(
+                    Modifier
+                        .size(10.dp)
+                        .background(Color.Blue)
+                        .testTag("focusable1")
+                        .focusRequester(focusRequester1)
+                        .focusable()
+                )
+                Spacer(Modifier.size(100.dp))
+                Box(
+                    Modifier
+                        .size(10.dp)
+                        .background(Color.Blue)
+                        .testTag("focusable2")
+                        .focusRequester(focusRequester2)
+                        .focusable()
+                )
+            }
+        }
+        // When focusable2 gets focus, focusable1 should be scrolled out of view.
+        rule.runOnIdle {
+            focusRequester2.requestFocus()
+        }
+        rule.onNodeWithTag("focusable1").assertIsNotDisplayed()
+        rule.onNodeWithTag("focusable2").assertIsDisplayed()
+        // Pause the clock because we need to do some work in the middle of an animation.
+        rule.mainClock.autoAdvance = false
+
+        // Shrink the viewport, which should scroll to keep focusable2 in-view.
+        rule.runOnIdle {
+            viewportSize = 20.dp
+        }
+
+        // Tick the clock forward to let the animation start and run a bit.
+        repeat(3) { rule.mainClock.advanceTimeByFrame() }
+        rule.onNodeWithTag("focusable1").assertIsNotDisplayed()
+        rule.onNodeWithTag("focusable2").assertIsNotDisplayed()
+
+        rule.runOnIdle {
+            focusRequester1.requestFocus()
+        }
+        // Resume the clock, allow animation to finish.
+        rule.mainClock.autoAdvance = true
+
+        rule.onNodeWithTag("focusable1").assertIsDisplayed()
+        rule.onNodeWithTag("focusable2").assertIsNotDisplayed()
+    }
+
     @Composable
     private fun TestScrollableColumn(
         size: Dp,
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Scrollable.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Scrollable.kt
index 0b4f00c..b66d0de 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Scrollable.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Scrollable.kt
@@ -33,10 +33,12 @@
 import androidx.compose.foundation.rememberOverscrollEffect
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.State
+import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
 import androidx.compose.runtime.rememberCoroutineScope
 import androidx.compose.runtime.rememberUpdatedState
+import androidx.compose.runtime.setValue
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.composed
 import androidx.compose.ui.geometry.Offset
@@ -66,6 +68,8 @@
 import androidx.compose.ui.util.fastForEach
 import kotlin.math.abs
 import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.NonCancellable
 import kotlinx.coroutines.launch
 
 /**
@@ -553,6 +557,12 @@
     private var coordinates: LayoutCoordinates? = null
     private var oldSize: IntSize? = null
 
+    // These properties are used to detect the case where the viewport size is animated shrinking
+    // while the scroll animation used to keep the focused child in view is still running.
+    private var focusedChildBeingAnimated: LayoutCoordinates? = null
+    private var focusTargetBounds: Rect? by mutableStateOf(null)
+    private var focusAnimationJob: Job? = null
+
     val modifier: Modifier = this
         .onFocusedBoundsChanged { focusedChild = it }
         .bringIntoViewResponder(this)
@@ -581,9 +591,36 @@
     }
 
     override suspend fun bringChildIntoView(localRect: Rect) {
-        performBringIntoView(localRect, calculateRectForParent(localRect))
+        performBringIntoView(
+            source = localRect,
+            destination = calculateRectForParent(localRect)
+        )
     }
 
+    /**
+     * Handles when the size of the scroll viewport changes by making sure any focused child is kept
+     * appropriately visible when the viewport shrinks and would otherwise hide it.
+     *
+     * One common instance of this is when a text field in a scrollable near the bottom is focused
+     * while the soft keyboard is hidden, causing the keyboard to show, and cover the field.
+     * See b/192043120 and related bugs.
+     *
+     * To future debuggers of this method, it might be helpful to add a draw modifier to the chain
+     * above to draw the focus target bounds:
+     * ```
+     * .drawWithContent {
+     *   drawContent()
+     *   focusTargetBounds?.let {
+     *     drawRect(
+     *       Color.Red,
+     *       topLeft = it.topLeft,
+     *       size = it.size,
+     *       style = Stroke(1.dp.toPx())
+     *     )
+     *   }
+     * }
+     * ```
+     */
     private fun onSizeChanged(coordinates: LayoutCoordinates, oldSize: IntSize) {
         val containerShrunk = if (orientation == Horizontal) {
             coordinates.size.width < oldSize.width
@@ -594,17 +631,75 @@
         // soon be _more_ visible, so don't scroll.
         if (!containerShrunk) return
 
-        val focusedBounds = focusedChild
-            ?.let { coordinates.localBoundingBoxOf(it, clipBounds = false) }
-            ?: return
-        val myOldBounds = Rect(Offset.Zero, oldSize.toSize())
-        val adjustedBounds = computeDestination(focusedBounds, coordinates.size)
-        val wasVisible = myOldBounds.overlaps(focusedBounds)
-        val isFocusedChildClipped = adjustedBounds != focusedBounds
+        val focusedChild = focusedChild ?: return
+        val focusedBounds = coordinates.localBoundingBoxOf(focusedChild, clipBounds = false)
 
-        if (wasVisible && isFocusedChildClipped) {
-            scope.launch {
-                performBringIntoView(focusedBounds, adjustedBounds)
+        // In order to check if we need to scroll to bring the focused child into view, it's not
+        // enough to consider where the child actually is right now. If the viewport was recently
+        // shrunk, we may have already started a scroll animation to bring it into view. In that
+        // case, we need to compare with the target of the animation, not the current position. If
+        // we don't do that, then in some cases when the viewport size is being animated (e.g. when
+        // the keyboard insets are being animated on API 30+) we might stop trying to keep the
+        // focused child in view before the viewport animation is finished, and the scroll animation
+        // will stop short and leave the focused child out of the viewport. See b/230756508.
+        val eventualFocusedBounds = if (focusedChild === focusedChildBeingAnimated) {
+            // A previous call to this method started an animation that is still running, so compare
+            // with the target of that animation.
+            checkNotNull(focusTargetBounds)
+        } else {
+            focusedBounds
+        }
+
+        val myOldBounds = Rect(Offset.Zero, oldSize.toSize())
+        if (!myOldBounds.overlaps(eventualFocusedBounds)) {
+            // The focused child was not visible before the resize, so we don't need to keep
+            // it visible.
+            return
+        }
+
+        val targetBounds = computeDestination(eventualFocusedBounds, coordinates.size)
+        if (targetBounds == eventualFocusedBounds) {
+            // The focused child is already fully visible (not clipped or hidden) after the resize,
+            // or will be after it finishes animating, so we don't need to do anything.
+            return
+        }
+
+        // If execution has gotten to this point, it means the focused child was at least partially
+        // visible before the resize, and it is either partially clipped or completely hidden after
+        // the resize, so we need to adjust scroll to keep it in view.
+        focusedChildBeingAnimated = focusedChild
+        focusTargetBounds = targetBounds
+        scope.launch(NonCancellable) {
+            val job = launch {
+                // Animate the scroll offset to keep the focused child in view. This is a suspending
+                // call that will suspend until the animation is finished, and only return if it
+                // completes. If any other scroll operations are performed after the animation starts,
+                // e.g. the viewport shrinks again or the user manually scrolls, this animation will
+                // be cancelled and this function will throw a CancellationException.
+                performBringIntoView(source = focusedBounds, destination = targetBounds)
+            }
+            focusAnimationJob = job
+
+            // If the scroll was interrupted by another viewport shrink that happens while the
+            // animation is running, we don't want to clear these fields since the later call to
+            // this onSizeChanged method will have updated the fields with its own values.
+            // If the animation completed, or was cancelled for any other reason, we need to clear
+            // them so the next viewport shrink doesn't think there's already a scroll animation in
+            // progress.
+            // Doing this wrong has a few implications:
+            // 1. If the fields are nulled out when another onSizeChange call happens, it will not
+            //    use the current animation target and viewport animations will lose track of the
+            //    focusable.
+            // 2. If the fields are not nulled out in other cases, the next viewport animation will
+            //    not keep the focusable in view if the focus hasn't changed.
+            try {
+                job.join()
+            } finally {
+                if (focusAnimationJob === job) {
+                    focusedChildBeingAnimated = null
+                    focusTargetBounds = null
+                    focusAnimationJob = null
+                }
             }
         }
     }
@@ -612,16 +707,30 @@
     /**
      * Compute the destination given the source rectangle and current bounds.
      *
-     * @param source The bounding box of the item that sent the request to be brought into view.
+     * @param childBounds The bounding box of the item that sent the request to be brought into view.
      * @return the destination rectangle.
      */
-    private fun computeDestination(source: Rect, intSize: IntSize): Rect {
-        val size = intSize.toSize()
+    private fun computeDestination(childBounds: Rect, containerSize: IntSize): Rect {
+        val size = containerSize.toSize()
         return when (orientation) {
             Vertical ->
-                source.translate(0f, relocationDistance(source.top, source.bottom, size.height))
+                childBounds.translate(
+                    translateX = 0f,
+                    translateY = -relocationDistance(
+                        childBounds.top,
+                        childBounds.bottom,
+                        size.height
+                    )
+                )
             Horizontal ->
-                source.translate(relocationDistance(source.left, source.right, size.width), 0f)
+                childBounds.translate(
+                    translateX = -relocationDistance(
+                        childBounds.left,
+                        childBounds.right,
+                        size.width
+                    ),
+                    translateY = 0f
+                )
         }
     }
 
@@ -630,8 +739,8 @@
      */
     private suspend fun performBringIntoView(source: Rect, destination: Rect) {
         val offset = when (orientation) {
-            Vertical -> source.top - destination.top
-            Horizontal -> source.left - destination.left
+            Vertical -> destination.top - source.top
+            Horizontal -> destination.left - source.left
         }
         val scrollDelta = if (reverseDirection) -offset else offset
 
@@ -654,7 +763,8 @@
             // If the item is visible but larger than the parent, we don't scroll.
             leadingEdge < 0 && trailingEdge > parentSize -> 0f
 
-            // Find the minimum scroll needed to make one of the edges coincide with the parent's edge.
+            // Find the minimum scroll needed to make one of the edges coincide with the parent's
+            // edge.
             abs(leadingEdge) < abs(trailingEdge - parentSize) -> leadingEdge
             else -> trailingEdge - parentSize
         }
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListItemProviderImpl.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListItemProviderImpl.kt
index 8c013a2..2229a03 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListItemProviderImpl.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListItemProviderImpl.kt
@@ -18,16 +18,14 @@
 
 import androidx.compose.foundation.ExperimentalFoundationApi
 import androidx.compose.foundation.lazy.layout.IntervalList
+import androidx.compose.foundation.lazy.layout.calculateNearestItemsRange
 import androidx.compose.foundation.lazy.layout.getDefaultLazyLayoutKey
 import androidx.compose.runtime.Composable
-import androidx.compose.runtime.LaunchedEffect
 import androidx.compose.runtime.State
 import androidx.compose.runtime.derivedStateOf
-import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
 import androidx.compose.runtime.rememberUpdatedState
-import androidx.compose.runtime.snapshotFlow
-import androidx.compose.runtime.snapshots.Snapshot
+import androidx.compose.runtime.structuralEqualityPolicy
 
 @ExperimentalFoundationApi
 @Composable
@@ -37,21 +35,14 @@
 ): LazyListItemProvider {
     val latestContent = rememberUpdatedState(content)
 
-    // mutableState + LaunchedEffect below are used instead of derivedStateOf to ensure that update
-    // of derivedState in return expr will only happen after the state value has been changed.
     val nearestItemsRangeState = remember(state) {
-        mutableStateOf(
-            Snapshot.withoutReadObservation {
-                // State read is observed in composition, causing it to recompose 1 additional time.
-                calculateNearestItemsRange(state.firstVisibleItemIndex)
-            }
-        )
-    }
-    LaunchedEffect(nearestItemsRangeState) {
-        snapshotFlow { calculateNearestItemsRange(state.firstVisibleItemIndex) }
-            // MutableState's SnapshotMutationPolicy will make sure the provider is only
-            // recreated when the state is updated with a new range.
-            .collect { nearestItemsRangeState.value = it }
+        derivedStateOf(structuralEqualityPolicy()) {
+            calculateNearestItemsRange(
+                slidingWindowSize = NearestItemsSlidingWindowSize,
+                extraItemCount = NearestItemsExtraItemCount,
+                firstVisibleItem = state.firstVisibleItemIndex
+            )
+        }
     }
 
     return remember(nearestItemsRangeState) {
@@ -108,7 +99,7 @@
 
     override val headerIndexes: List get() = itemsSnapshot.value.headerIndexes
 
-    override val itemCount get() = itemsSnapshot.value.itemsCount
+     override val itemCount get() = itemsSnapshot.value.itemsCount
 
     override fun getKey(index: Int) = itemsSnapshot.value.getKey(index)
 
@@ -157,26 +148,12 @@
 }
 
 /**
- * Returns a range of indexes which contains at least [ExtraItemsNearTheSlidingWindow] items near
- * the first visible item. It is optimized to return the same range for small changes in the
- * firstVisibleItem value so we do not regenerate the map on each scroll.
- */
-private fun calculateNearestItemsRange(firstVisibleItem: Int): IntRange {
-    val slidingWindowStart = VisibleItemsSlidingWindowSize *
-        (firstVisibleItem / VisibleItemsSlidingWindowSize)
-
-    val start = maxOf(slidingWindowStart - ExtraItemsNearTheSlidingWindow, 0)
-    val end = slidingWindowStart + VisibleItemsSlidingWindowSize + ExtraItemsNearTheSlidingWindow
-    return start until end
-}
-
-/**
  * We use the idea of sliding window as an optimization, so user can scroll up to this number of
  * items until we have to regenerate the key to index map.
  */
-private val VisibleItemsSlidingWindowSize = 30
+private const val NearestItemsSlidingWindowSize = 30
 
 /**
  * The minimum amount of items near the current first visible item we want to have mapping for.
  */
-private val ExtraItemsNearTheSlidingWindow = 100
+private const val NearestItemsExtraItemCount = 100
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridItemProviderImpl.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridItemProviderImpl.kt
index f81046e..9c2d01e 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridItemProviderImpl.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridItemProviderImpl.kt
@@ -18,16 +18,14 @@
 
 import androidx.compose.foundation.ExperimentalFoundationApi
 import androidx.compose.foundation.lazy.layout.IntervalList
+import androidx.compose.foundation.lazy.layout.calculateNearestItemsRange
 import androidx.compose.foundation.lazy.layout.getDefaultLazyLayoutKey
 import androidx.compose.runtime.Composable
-import androidx.compose.runtime.LaunchedEffect
 import androidx.compose.runtime.State
 import androidx.compose.runtime.derivedStateOf
-import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
 import androidx.compose.runtime.rememberUpdatedState
-import androidx.compose.runtime.snapshotFlow
-import androidx.compose.runtime.snapshots.Snapshot
+import androidx.compose.runtime.structuralEqualityPolicy
 
 @ExperimentalFoundationApi
 @Composable
@@ -36,21 +34,14 @@
     content: LazyGridScope.() -> Unit,
 ): LazyGridItemProvider {
     val latestContent = rememberUpdatedState(content)
-    // mutableState + LaunchedEffect below are used instead of derivedStateOf to ensure that update
-    // of derivedState in return expr will only happen after the state value has been changed.
     val nearestItemsRangeState = remember(state) {
-        mutableStateOf(
-            Snapshot.withoutReadObservation {
-                // State read is observed in composition, causing it to recompose 1 additional time.
-                calculateNearestItemsRange(state.firstVisibleItemIndex)
-            }
-        )
-    }
-    LaunchedEffect(nearestItemsRangeState) {
-        snapshotFlow { calculateNearestItemsRange(state.firstVisibleItemIndex) }
-            // MutableState's SnapshotMutationPolicy will make sure the provider is only
-            // recreated when the state is updated with a new range.
-            .collect { nearestItemsRangeState.value = it }
+        derivedStateOf(structuralEqualityPolicy()) {
+            calculateNearestItemsRange(
+                slidingWindowSize = NearestItemsSlidingWindowSize,
+                extraItemCount = NearestItemsExtraItemCount,
+                firstVisibleItem = state.firstVisibleItemIndex
+            )
+        }
     }
 
     return remember(nearestItemsRangeState) {
@@ -162,26 +153,12 @@
 }
 
 /**
- * Returns a range of indexes which contains at least [ExtraItemsNearTheSlidingWindow] items near
- * the first visible item. It is optimized to return the same range for small changes in the
- * firstVisibleItem value so we do not regenerate the map on each scroll.
- */
-private fun calculateNearestItemsRange(firstVisibleItem: Int): IntRange {
-    val slidingWindowStart = VisibleItemsSlidingWindowSize *
-        (firstVisibleItem / VisibleItemsSlidingWindowSize)
-
-    val start = maxOf(slidingWindowStart - ExtraItemsNearTheSlidingWindow, 0)
-    val end = slidingWindowStart + VisibleItemsSlidingWindowSize + ExtraItemsNearTheSlidingWindow
-    return start until end
-}
-
-/**
  * We use the idea of sliding window as an optimization, so user can scroll up to this number of
  * items until we have to regenerate the key to index map.
  */
-private val VisibleItemsSlidingWindowSize = 90
+private const val NearestItemsSlidingWindowSize = 90
 
 /**
  * The minimum amount of items near the current first visible item we want to have mapping for.
  */
-private val ExtraItemsNearTheSlidingWindow = 200
+private const val NearestItemsExtraItemCount = 200
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyNearestItemsRange.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyNearestItemsRange.kt
new file mode 100644
index 0000000..f34ae9f
--- /dev/null
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyNearestItemsRange.kt
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.foundation.lazy.layout
+
+/**
+ * Returns a range of indexes which contains at least [extraItemCount] items near
+ * the first visible item. It is optimized to return the same range for small changes in the
+ * [firstVisibleItem] value so we do not regenerate the map on each scroll.
+ *
+ * It uses the idea of sliding window as an optimization, so user can scroll up to this number of
+ * items until we have to regenerate the key to index map.
+ *
+ * @param firstVisibleItem currently visible item
+ * @param slidingWindowSize size of the sliding window for the nearest item calculation
+ * @param extraItemCount minimum amount of items near the first item we want to have mapping for.
+ */
+internal fun calculateNearestItemsRange(
+    firstVisibleItem: Int,
+    slidingWindowSize: Int,
+    extraItemCount: Int
+): IntRange {
+    val slidingWindowStart = slidingWindowSize * (firstVisibleItem / slidingWindowSize)
+
+    val start = maxOf(slidingWindowStart - extraItemCount, 0)
+    val end = slidingWindowStart + slidingWindowSize + extraItemCount
+    return start until end
+}
\ No newline at end of file
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/FloatingActionButtonTest.kt b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/FloatingActionButtonTest.kt
index f999349..33b1f99 100644
--- a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/FloatingActionButtonTest.kt
+++ b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/FloatingActionButtonTest.kt
@@ -420,6 +420,11 @@
 
                 assertWithinOnePixel(buttonBounds.center.y, iconBounds.center.y)
                 assertWithinOnePixel(buttonBounds.center.y, textBounds.center.y)
+
+                // Assert expanded fab icon has 16.dp of padding.
+                assertThat(iconBounds.left - buttonBounds.left)
+                    .isEqualTo(16.dp.roundToPx().toFloat())
+
                 val halfPadding = 6.dp.roundToPx().toFloat()
                 assertWithinOnePixel(
                     iconBounds.center.x + iconBounds.width / 2 + halfPadding,
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/FloatingActionButton.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/FloatingActionButton.kt
index dc56032..af1cfa4 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/FloatingActionButton.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/FloatingActionButton.kt
@@ -30,6 +30,7 @@
 import androidx.compose.foundation.interaction.InteractionSource
 import androidx.compose.foundation.interaction.MutableInteractionSource
 import androidx.compose.foundation.interaction.PressInteraction
+import androidx.compose.foundation.layout.Arrangement
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.Row
 import androidx.compose.foundation.layout.RowScope
@@ -275,7 +276,7 @@
     content: @Composable RowScope.() -> Unit,
 ) {
     FloatingActionButton(
-        modifier = modifier.sizeIn(minWidth = ExtendedFabMinimumWidth),
+        modifier = modifier,
         onClick = onClick,
         interactionSource = interactionSource,
         shape = shape,
@@ -284,7 +285,10 @@
         elevation = elevation,
     ) {
         Row(
-            modifier = Modifier.padding(horizontal = ExtendedFabTextPadding),
+            modifier = Modifier
+                .sizeIn(minWidth = ExtendedFabMinimumWidth)
+                .padding(horizontal = ExtendedFabTextPadding),
+            horizontalArrangement = Arrangement.Center,
             verticalAlignment = Alignment.CenterVertically,
             content = content,
         )
@@ -338,9 +342,7 @@
     elevation: FloatingActionButtonElevation = FloatingActionButtonDefaults.elevation(),
 ) {
     FloatingActionButton(
-        modifier = modifier.sizeIn(
-            minWidth = if (expanded) ExtendedFabMinimumWidth else FabPrimaryTokens.ContainerWidth
-        ),
+        modifier = modifier,
         onClick = onClick,
         interactionSource = interactionSource,
         shape = shape,
@@ -348,12 +350,18 @@
         contentColor = contentColor,
         elevation = elevation,
     ) {
-        val startPadding = if (expanded) ExtendedFabPrimaryTokens.IconSize / 2 else 0.dp
+        val startPadding = if (expanded) ExtendedFabStartIconPadding else 0.dp
         val endPadding = if (expanded) ExtendedFabTextPadding else 0.dp
 
         Row(
-            modifier = Modifier.padding(start = startPadding, end = endPadding),
-            verticalAlignment = Alignment.CenterVertically
+            modifier = Modifier
+                .sizeIn(
+                    minWidth = if (expanded) ExtendedFabMinimumWidth
+                    else FabPrimaryTokens.ContainerWidth
+                )
+                .padding(start = startPadding, end = endPadding),
+            verticalAlignment = Alignment.CenterVertically,
+            horizontalArrangement = if (expanded) Arrangement.Start else Arrangement.Center
         ) {
             icon()
             AnimatedVisibility(
@@ -362,7 +370,7 @@
                 exit = ExtendedFabCollapseAnimation,
             ) {
                 Row {
-                    Spacer(Modifier.width(ExtendedFabIconPadding))
+                    Spacer(Modifier.width(ExtendedFabEndIconPadding))
                     text()
                 }
             }
@@ -578,7 +586,9 @@
     }
 }
 
-private val ExtendedFabIconPadding = 12.dp
+private val ExtendedFabStartIconPadding = 16.dp
+
+private val ExtendedFabEndIconPadding = 12.dp
 
 private val ExtendedFabTextPadding = 20.dp
 
diff --git a/compose/runtime/runtime/api/current.txt b/compose/runtime/runtime/api/current.txt
index 5654983..716e0de 100644
--- a/compose/runtime/runtime/api/current.txt
+++ b/compose/runtime/runtime/api/current.txt
@@ -417,6 +417,7 @@
     method @androidx.compose.runtime.Composable public static  androidx.compose.runtime.State collectAsState(kotlinx.coroutines.flow.StateFlow, optional kotlin.coroutines.CoroutineContext context);
     method @androidx.compose.runtime.Composable public static  androidx.compose.runtime.State collectAsState(kotlinx.coroutines.flow.Flow, R? initial, optional kotlin.coroutines.CoroutineContext context);
     method public static  androidx.compose.runtime.State derivedStateOf(kotlin.jvm.functions.Function0 calculation);
+    method public static  androidx.compose.runtime.State derivedStateOf(androidx.compose.runtime.SnapshotMutationPolicy policy, kotlin.jvm.functions.Function0 calculation);
     method public static inline operator  T! getValue(androidx.compose.runtime.State, Object? thisObj, kotlin.reflect.KProperty property);
     method public static  androidx.compose.runtime.snapshots.SnapshotStateList mutableStateListOf();
     method public static  androidx.compose.runtime.snapshots.SnapshotStateList mutableStateListOf(T?... elements);
@@ -442,6 +443,7 @@
     method @androidx.compose.runtime.Composable public static  androidx.compose.runtime.State collectAsState(kotlinx.coroutines.flow.StateFlow, optional kotlin.coroutines.CoroutineContext context);
     method @androidx.compose.runtime.Composable public static  androidx.compose.runtime.State collectAsState(kotlinx.coroutines.flow.Flow, R? initial, optional kotlin.coroutines.CoroutineContext context);
     method public static  androidx.compose.runtime.State derivedStateOf(kotlin.jvm.functions.Function0 calculation);
+    method public static  androidx.compose.runtime.State derivedStateOf(androidx.compose.runtime.SnapshotMutationPolicy policy, kotlin.jvm.functions.Function0 calculation);
     method public static inline operator  T! getValue(androidx.compose.runtime.State, Object? thisObj, kotlin.reflect.KProperty property);
     method public static  androidx.compose.runtime.snapshots.SnapshotStateList mutableStateListOf();
     method public static  androidx.compose.runtime.snapshots.SnapshotStateList mutableStateListOf(T?... elements);
@@ -467,6 +469,7 @@
     method @androidx.compose.runtime.Composable public static  androidx.compose.runtime.State collectAsState(kotlinx.coroutines.flow.StateFlow, optional kotlin.coroutines.CoroutineContext context);
     method @androidx.compose.runtime.Composable public static  androidx.compose.runtime.State collectAsState(kotlinx.coroutines.flow.Flow, R? initial, optional kotlin.coroutines.CoroutineContext context);
     method public static  androidx.compose.runtime.State derivedStateOf(kotlin.jvm.functions.Function0 calculation);
+    method public static  androidx.compose.runtime.State derivedStateOf(androidx.compose.runtime.SnapshotMutationPolicy policy, kotlin.jvm.functions.Function0 calculation);
     method public static inline operator  T! getValue(androidx.compose.runtime.State, Object? thisObj, kotlin.reflect.KProperty property);
     method public static  androidx.compose.runtime.snapshots.SnapshotStateList mutableStateListOf();
     method public static  androidx.compose.runtime.snapshots.SnapshotStateList mutableStateListOf(T?... elements);
@@ -492,6 +495,7 @@
     method @androidx.compose.runtime.Composable public static  androidx.compose.runtime.State collectAsState(kotlinx.coroutines.flow.StateFlow, optional kotlin.coroutines.CoroutineContext context);
     method @androidx.compose.runtime.Composable public static  androidx.compose.runtime.State collectAsState(kotlinx.coroutines.flow.Flow, R? initial, optional kotlin.coroutines.CoroutineContext context);
     method public static  androidx.compose.runtime.State derivedStateOf(kotlin.jvm.functions.Function0 calculation);
+    method public static  androidx.compose.runtime.State derivedStateOf(androidx.compose.runtime.SnapshotMutationPolicy policy, kotlin.jvm.functions.Function0 calculation);
     method public static inline operator  T! getValue(androidx.compose.runtime.State, Object? thisObj, kotlin.reflect.KProperty property);
     method public static  androidx.compose.runtime.snapshots.SnapshotStateList mutableStateListOf();
     method public static  androidx.compose.runtime.snapshots.SnapshotStateList mutableStateListOf(T?... elements);
@@ -517,6 +521,7 @@
     method @androidx.compose.runtime.Composable public static  androidx.compose.runtime.State collectAsState(kotlinx.coroutines.flow.StateFlow, optional kotlin.coroutines.CoroutineContext context);
     method @androidx.compose.runtime.Composable public static  androidx.compose.runtime.State collectAsState(kotlinx.coroutines.flow.Flow, R? initial, optional kotlin.coroutines.CoroutineContext context);
     method public static  androidx.compose.runtime.State derivedStateOf(kotlin.jvm.functions.Function0 calculation);
+    method public static  androidx.compose.runtime.State derivedStateOf(androidx.compose.runtime.SnapshotMutationPolicy policy, kotlin.jvm.functions.Function0 calculation);
     method public static inline operator  T! getValue(androidx.compose.runtime.State, Object? thisObj, kotlin.reflect.KProperty property);
     method public static  androidx.compose.runtime.snapshots.SnapshotStateList mutableStateListOf();
     method public static  androidx.compose.runtime.snapshots.SnapshotStateList mutableStateListOf(T?... elements);
diff --git a/compose/runtime/runtime/api/public_plus_experimental_current.txt b/compose/runtime/runtime/api/public_plus_experimental_current.txt
index 75793d0..33f232d 100644
--- a/compose/runtime/runtime/api/public_plus_experimental_current.txt
+++ b/compose/runtime/runtime/api/public_plus_experimental_current.txt
@@ -465,6 +465,7 @@
     method @androidx.compose.runtime.Composable public static  androidx.compose.runtime.State collectAsState(kotlinx.coroutines.flow.StateFlow, optional kotlin.coroutines.CoroutineContext context);
     method @androidx.compose.runtime.Composable public static  androidx.compose.runtime.State collectAsState(kotlinx.coroutines.flow.Flow, R? initial, optional kotlin.coroutines.CoroutineContext context);
     method public static  androidx.compose.runtime.State derivedStateOf(kotlin.jvm.functions.Function0 calculation);
+    method public static  androidx.compose.runtime.State derivedStateOf(androidx.compose.runtime.SnapshotMutationPolicy policy, kotlin.jvm.functions.Function0 calculation);
     method public static inline operator  T! getValue(androidx.compose.runtime.State, Object? thisObj, kotlin.reflect.KProperty property);
     method public static  androidx.compose.runtime.snapshots.SnapshotStateList mutableStateListOf();
     method public static  androidx.compose.runtime.snapshots.SnapshotStateList mutableStateListOf(T?... elements);
@@ -490,6 +491,7 @@
     method @androidx.compose.runtime.Composable public static  androidx.compose.runtime.State collectAsState(kotlinx.coroutines.flow.StateFlow, optional kotlin.coroutines.CoroutineContext context);
     method @androidx.compose.runtime.Composable public static  androidx.compose.runtime.State collectAsState(kotlinx.coroutines.flow.Flow, R? initial, optional kotlin.coroutines.CoroutineContext context);
     method public static  androidx.compose.runtime.State derivedStateOf(kotlin.jvm.functions.Function0 calculation);
+    method public static  androidx.compose.runtime.State derivedStateOf(androidx.compose.runtime.SnapshotMutationPolicy policy, kotlin.jvm.functions.Function0 calculation);
     method public static inline operator  T! getValue(androidx.compose.runtime.State, Object? thisObj, kotlin.reflect.KProperty property);
     method public static  androidx.compose.runtime.snapshots.SnapshotStateList mutableStateListOf();
     method public static  androidx.compose.runtime.snapshots.SnapshotStateList mutableStateListOf(T?... elements);
@@ -515,6 +517,7 @@
     method @androidx.compose.runtime.Composable public static  androidx.compose.runtime.State collectAsState(kotlinx.coroutines.flow.StateFlow, optional kotlin.coroutines.CoroutineContext context);
     method @androidx.compose.runtime.Composable public static  androidx.compose.runtime.State collectAsState(kotlinx.coroutines.flow.Flow, R? initial, optional kotlin.coroutines.CoroutineContext context);
     method public static  androidx.compose.runtime.State derivedStateOf(kotlin.jvm.functions.Function0 calculation);
+    method public static  androidx.compose.runtime.State derivedStateOf(androidx.compose.runtime.SnapshotMutationPolicy policy, kotlin.jvm.functions.Function0 calculation);
     method public static inline operator  T! getValue(androidx.compose.runtime.State, Object? thisObj, kotlin.reflect.KProperty property);
     method public static  androidx.compose.runtime.snapshots.SnapshotStateList mutableStateListOf();
     method public static  androidx.compose.runtime.snapshots.SnapshotStateList mutableStateListOf(T?... elements);
@@ -540,6 +543,7 @@
     method @androidx.compose.runtime.Composable public static  androidx.compose.runtime.State collectAsState(kotlinx.coroutines.flow.StateFlow, optional kotlin.coroutines.CoroutineContext context);
     method @androidx.compose.runtime.Composable public static  androidx.compose.runtime.State collectAsState(kotlinx.coroutines.flow.Flow, R? initial, optional kotlin.coroutines.CoroutineContext context);
     method public static  androidx.compose.runtime.State derivedStateOf(kotlin.jvm.functions.Function0 calculation);
+    method public static  androidx.compose.runtime.State derivedStateOf(androidx.compose.runtime.SnapshotMutationPolicy policy, kotlin.jvm.functions.Function0 calculation);
     method public static inline operator  T! getValue(androidx.compose.runtime.State, Object? thisObj, kotlin.reflect.KProperty property);
     method public static  androidx.compose.runtime.snapshots.SnapshotStateList mutableStateListOf();
     method public static  androidx.compose.runtime.snapshots.SnapshotStateList mutableStateListOf(T?... elements);
@@ -565,6 +569,7 @@
     method @androidx.compose.runtime.Composable public static  androidx.compose.runtime.State collectAsState(kotlinx.coroutines.flow.StateFlow, optional kotlin.coroutines.CoroutineContext context);
     method @androidx.compose.runtime.Composable public static  androidx.compose.runtime.State collectAsState(kotlinx.coroutines.flow.Flow, R? initial, optional kotlin.coroutines.CoroutineContext context);
     method public static  androidx.compose.runtime.State derivedStateOf(kotlin.jvm.functions.Function0 calculation);
+    method public static  androidx.compose.runtime.State derivedStateOf(androidx.compose.runtime.SnapshotMutationPolicy policy, kotlin.jvm.functions.Function0 calculation);
     method public static inline operator  T! getValue(androidx.compose.runtime.State, Object? thisObj, kotlin.reflect.KProperty property);
     method public static  androidx.compose.runtime.snapshots.SnapshotStateList mutableStateListOf();
     method public static  androidx.compose.runtime.snapshots.SnapshotStateList mutableStateListOf(T?... elements);
diff --git a/compose/runtime/runtime/api/restricted_current.txt b/compose/runtime/runtime/api/restricted_current.txt
index 2643f45..8d12646 100644
--- a/compose/runtime/runtime/api/restricted_current.txt
+++ b/compose/runtime/runtime/api/restricted_current.txt
@@ -443,6 +443,7 @@
     method @androidx.compose.runtime.Composable public static  androidx.compose.runtime.State collectAsState(kotlinx.coroutines.flow.StateFlow, optional kotlin.coroutines.CoroutineContext context);
     method @androidx.compose.runtime.Composable public static  androidx.compose.runtime.State collectAsState(kotlinx.coroutines.flow.Flow, R? initial, optional kotlin.coroutines.CoroutineContext context);
     method public static  androidx.compose.runtime.State derivedStateOf(kotlin.jvm.functions.Function0 calculation);
+    method public static  androidx.compose.runtime.State derivedStateOf(androidx.compose.runtime.SnapshotMutationPolicy policy, kotlin.jvm.functions.Function0 calculation);
     method public static inline operator  T! getValue(androidx.compose.runtime.State, Object? thisObj, kotlin.reflect.KProperty property);
     method public static  androidx.compose.runtime.snapshots.SnapshotStateList mutableStateListOf();
     method public static  androidx.compose.runtime.snapshots.SnapshotStateList mutableStateListOf(T?... elements);
@@ -468,6 +469,7 @@
     method @androidx.compose.runtime.Composable public static  androidx.compose.runtime.State collectAsState(kotlinx.coroutines.flow.StateFlow, optional kotlin.coroutines.CoroutineContext context);
     method @androidx.compose.runtime.Composable public static  androidx.compose.runtime.State collectAsState(kotlinx.coroutines.flow.Flow, R? initial, optional kotlin.coroutines.CoroutineContext context);
     method public static  androidx.compose.runtime.State derivedStateOf(kotlin.jvm.functions.Function0 calculation);
+    method public static  androidx.compose.runtime.State derivedStateOf(androidx.compose.runtime.SnapshotMutationPolicy policy, kotlin.jvm.functions.Function0 calculation);
     method public static inline operator  T! getValue(androidx.compose.runtime.State, Object? thisObj, kotlin.reflect.KProperty property);
     method public static  androidx.compose.runtime.snapshots.SnapshotStateList mutableStateListOf();
     method public static  androidx.compose.runtime.snapshots.SnapshotStateList mutableStateListOf(T?... elements);
@@ -493,6 +495,7 @@
     method @androidx.compose.runtime.Composable public static  androidx.compose.runtime.State collectAsState(kotlinx.coroutines.flow.StateFlow, optional kotlin.coroutines.CoroutineContext context);
     method @androidx.compose.runtime.Composable public static  androidx.compose.runtime.State collectAsState(kotlinx.coroutines.flow.Flow, R? initial, optional kotlin.coroutines.CoroutineContext context);
     method public static  androidx.compose.runtime.State derivedStateOf(kotlin.jvm.functions.Function0 calculation);
+    method public static  androidx.compose.runtime.State derivedStateOf(androidx.compose.runtime.SnapshotMutationPolicy policy, kotlin.jvm.functions.Function0 calculation);
     method public static inline operator  T! getValue(androidx.compose.runtime.State, Object? thisObj, kotlin.reflect.KProperty property);
     method public static  androidx.compose.runtime.snapshots.SnapshotStateList mutableStateListOf();
     method public static  androidx.compose.runtime.snapshots.SnapshotStateList mutableStateListOf(T?... elements);
@@ -518,6 +521,7 @@
     method @androidx.compose.runtime.Composable public static  androidx.compose.runtime.State collectAsState(kotlinx.coroutines.flow.StateFlow, optional kotlin.coroutines.CoroutineContext context);
     method @androidx.compose.runtime.Composable public static  androidx.compose.runtime.State collectAsState(kotlinx.coroutines.flow.Flow, R? initial, optional kotlin.coroutines.CoroutineContext context);
     method public static  androidx.compose.runtime.State derivedStateOf(kotlin.jvm.functions.Function0 calculation);
+    method public static  androidx.compose.runtime.State derivedStateOf(androidx.compose.runtime.SnapshotMutationPolicy policy, kotlin.jvm.functions.Function0 calculation);
     method public static inline operator  T! getValue(androidx.compose.runtime.State, Object? thisObj, kotlin.reflect.KProperty property);
     method public static  androidx.compose.runtime.snapshots.SnapshotStateList mutableStateListOf();
     method public static  androidx.compose.runtime.snapshots.SnapshotStateList mutableStateListOf(T?... elements);
@@ -543,6 +547,7 @@
     method @androidx.compose.runtime.Composable public static  androidx.compose.runtime.State collectAsState(kotlinx.coroutines.flow.StateFlow, optional kotlin.coroutines.CoroutineContext context);
     method @androidx.compose.runtime.Composable public static  androidx.compose.runtime.State collectAsState(kotlinx.coroutines.flow.Flow, R? initial, optional kotlin.coroutines.CoroutineContext context);
     method public static  androidx.compose.runtime.State derivedStateOf(kotlin.jvm.functions.Function0 calculation);
+    method public static  androidx.compose.runtime.State derivedStateOf(androidx.compose.runtime.SnapshotMutationPolicy policy, kotlin.jvm.functions.Function0 calculation);
     method public static inline operator  T! getValue(androidx.compose.runtime.State, Object? thisObj, kotlin.reflect.KProperty property);
     method public static  androidx.compose.runtime.snapshots.SnapshotStateList mutableStateListOf();
     method public static  androidx.compose.runtime.snapshots.SnapshotStateList mutableStateListOf(T?... elements);
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/DerivedState.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/DerivedState.kt
index f1e5901..ee172c5 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/DerivedState.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/DerivedState.kt
@@ -23,14 +23,15 @@
 import androidx.compose.runtime.snapshots.Snapshot
 import androidx.compose.runtime.snapshots.StateObject
 import androidx.compose.runtime.snapshots.StateRecord
+import androidx.compose.runtime.snapshots.current
 import androidx.compose.runtime.snapshots.fastForEach
 import androidx.compose.runtime.snapshots.newWritableRecord
-import androidx.compose.runtime.snapshots.readable
 import androidx.compose.runtime.snapshots.sync
 import androidx.compose.runtime.snapshots.withCurrent
 // Explicit imports for these needed in common source sets.
 import kotlin.jvm.JvmName
 import kotlin.jvm.JvmMultifileClass
+import kotlin.math.min
 
 /**
  * A [State] that is derived from one or more other states.
@@ -50,25 +51,33 @@
      * observer set, if the state could affect value of this derived state.
      */
     val dependencies: Set
+
+    /**
+     * Mutation policy that controls how changes are handled after state dependencies update.
+     * If the policy is `null`, the derived state update is triggered regardless of the value
+     * produced and it is up to observer to invalidate it correctly.
+     */
+    val policy: SnapshotMutationPolicy?
 }
 
 private typealias DerivedStateObservers = Pair<(DerivedState<*>) -> Unit, (DerivedState<*>) -> Unit>
 
 private val derivedStateObservers = SnapshotThreadLocal>()
 
-private val isCalculationBlockRunning = SnapshotThreadLocal()
+private val calculationBlockNestedLevel = SnapshotThreadLocal()
 
 private class DerivedSnapshotState(
-    private val calculation: () -> T
+    private val calculation: () -> T,
+    override val policy: SnapshotMutationPolicy?
 ) : StateObject, DerivedState {
     private var first: ResultRecord = ResultRecord()
 
-    private class ResultRecord : StateRecord() {
+    class ResultRecord : StateRecord() {
         companion object {
             val Unset = Any()
         }
 
-        var dependencies: HashSet? = null
+        var dependencies: HashMap? = null
         var result: Any? = Unset
         var resultHash: Int = 0
 
@@ -90,9 +99,22 @@
             val dependencies = sync { dependencies }
             if (dependencies != null) {
                 notifyObservers(derivedState) {
-                    for (stateObject in dependencies) {
+                    for ((stateObject, readLevel) in dependencies.entries) {
+                        if (readLevel != 1) {
+                            continue
+                        }
+
+                        if (stateObject is DerivedSnapshotState<*>) {
+                            // eagerly access the parent derived states without recording the
+                            // read
+                            // that way we can be sure derived states in deps were recalculated,
+                            // and are updated to the last values
+                            stateObject.refresh(stateObject.firstStateRecord, snapshot)
+                        }
+
                         // Find the first record without triggering an observer read.
-                        val record = stateObject.firstStateRecord.readable(stateObject, snapshot)
+                        val record = current(stateObject.firstStateRecord, snapshot)
+
                         hash = 31 * hash + identityHashCode(record)
                         hash = 31 * hash + record.snapshotId
                     }
@@ -102,49 +124,83 @@
         }
     }
 
+    fun refresh(record: StateRecord, snapshot: Snapshot) {
+        @Suppress("UNCHECKED_CAST")
+        currentRecord(record as ResultRecord, snapshot, false, calculation)
+    }
+
     private fun currentRecord(
         readable: ResultRecord,
         snapshot: Snapshot,
+        forceDependencyReads: Boolean,
         calculation: () -> T
     ): ResultRecord {
         if (readable.isValid(this, snapshot)) {
-            @Suppress("UNCHECKED_CAST")
+            // If the dependency is not recalculated, emulate nested state reads
+            // for correct invalidation later
+            if (forceDependencyReads) {
+                notifyObservers(this) {
+                    val dependencies = readable.dependencies ?: emptyMap()
+                    val invalidationNestedLevel = calculationBlockNestedLevel.get() ?: 0
+                    for ((dependency, nestedLevel) in dependencies) {
+                        calculationBlockNestedLevel.set(nestedLevel + invalidationNestedLevel)
+                        snapshot.readObserver?.invoke(dependency)
+                    }
+                    calculationBlockNestedLevel.set(invalidationNestedLevel)
+                }
+            }
             return readable
         }
-        val nestedCalculationBlockCall = isCalculationBlockRunning.get() ?: false
+        val nestedCalculationLevel = calculationBlockNestedLevel.get() ?: 0
 
-        val newDependencies = HashSet()
+        val newDependencies = HashMap()
         val result = notifyObservers(this) {
-            if (!nestedCalculationBlockCall) {
-                isCalculationBlockRunning.set(true)
-            }
+            calculationBlockNestedLevel.set(nestedCalculationLevel + 1)
+
             val result = Snapshot.observe(
                 {
                     if (it === this)
                         error("A derived state calculation cannot read itself")
-                    if (it is StateObject) newDependencies.add(it)
+                    if (it is StateObject) {
+                        val readNestedLevel = calculationBlockNestedLevel.get()!!
+                        newDependencies[it] = min(
+                            readNestedLevel - nestedCalculationLevel,
+                            newDependencies[it] ?: Int.MAX_VALUE
+                        )
+                    }
                 },
                 null, calculation
             )
-            if (!nestedCalculationBlockCall) {
-                isCalculationBlockRunning.set(false)
-            }
+
+            calculationBlockNestedLevel.set(nestedCalculationLevel)
             result
         }
 
-        val written = sync {
-            val writeSnapshot = Snapshot.current
-            val writable = first.newWritableRecord(this, writeSnapshot)
-            writable.dependencies = newDependencies
-            writable.resultHash = writable.readableHash(this, writeSnapshot)
-            writable.result = result
-            writable
+        val record = sync {
+            val currentSnapshot = Snapshot.current
+
+            if (
+                readable.result !== ResultRecord.Unset &&
+                @Suppress("UNCHECKED_CAST")
+                policy?.equivalent(result, readable.result as T) == true
+            ) {
+                readable.dependencies = newDependencies
+                readable.resultHash = readable.readableHash(this, currentSnapshot)
+                readable
+            } else {
+                val writable = first.newWritableRecord(this, currentSnapshot)
+                writable.dependencies = newDependencies
+                writable.resultHash = writable.readableHash(this, currentSnapshot)
+                writable.result = result
+                writable
+            }
         }
-        if (!nestedCalculationBlockCall) {
+
+        if (nestedCalculationLevel == 0) {
             Snapshot.notifyObjectsInitialized()
         }
 
-        return written
+        return record
     }
 
     override val firstStateRecord: StateRecord get() = first
@@ -168,12 +224,12 @@
     override val currentValue: T
         get() = first.withCurrent {
             @Suppress("UNCHECKED_CAST")
-            currentRecord(it, Snapshot.current, calculation).result as T
+            currentRecord(it, Snapshot.current, true, calculation).result as T
         }
 
     override val dependencies: Set
         get() = first.withCurrent {
-            currentRecord(it, Snapshot.current, calculation).dependencies ?: emptySet()
+            currentRecord(it, Snapshot.current, false, calculation).dependencies?.keys ?: emptySet()
         }
 
     override fun toString(): String = first.withCurrent {
@@ -221,12 +277,35 @@
  * objects that got read during the [calculation] to be read in the current [Snapshot], meaning
  * that this will correctly subscribe to the derived state objects if the value is being read in
  * an observed context such as a [Composable] function.
+ * Derived states without mutation policy trigger updates on each dependency change. To avoid
+ * invalidation on update, provide suitable [SnapshotMutationPolicy] through [derivedStateOf]
+ * overload.
  *
  * @sample androidx.compose.runtime.samples.DerivedStateSample
  *
  * @param calculation the calculation to create the value this state object represents.
  */
-fun  derivedStateOf(calculation: () -> T): State = DerivedSnapshotState(calculation)
+fun  derivedStateOf(
+    calculation: () -> T,
+): State = DerivedSnapshotState(calculation, null)
+
+/**
+ * Creates a [State] object whose [State.value] is the result of [calculation]. The result of
+ * calculation will be cached in such a way that calling [State.value] repeatedly will not cause
+ * [calculation] to be executed multiple times, but reading [State.value] will cause all [State]
+ * objects that got read during the [calculation] to be read in the current [Snapshot], meaning
+ * that this will correctly subscribe to the derived state objects if the value is being read in
+ * an observed context such as a [Composable] function.
+ *
+ * @sample androidx.compose.runtime.samples.DerivedStateSample
+ *
+ * @param policy mutation policy to control when changes to the [calculation] result trigger update.
+ * @param calculation the calculation to create the value this state object represents.
+ */
+fun  derivedStateOf(
+    policy: SnapshotMutationPolicy,
+    calculation: () -> T,
+): State = DerivedSnapshotState(calculation, policy)
 
 /**
  * Observe the recalculations performed by any derived state that is recalculated during the
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 4914cce..2de14b5 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
@@ -261,8 +261,12 @@
         if (
             instances.isNotEmpty() &&
             instances.all { instance ->
-                instance is DerivedState<*> &&
-                    trackedDependencies[instance] == instance.currentValue
+                instance is DerivedState<*> && instance.let {
+                    @Suppress("UNCHECKED_CAST")
+                    it as DerivedState
+                    val policy = it.policy ?: structuralEqualityPolicy()
+                    policy.equivalent(it.currentValue, trackedDependencies[it])
+                }
             }
         )
             return false
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/snapshots/Snapshot.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/snapshots/Snapshot.kt
index 2e5d0da..9ee164d 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/snapshots/Snapshot.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/snapshots/Snapshot.kt
@@ -215,8 +215,8 @@
 
     /**
      * Notify the snapshot that all objects created in this snapshot to this point should be
-     * considered initialized. If any state object is are modified passed this point it will
-     * appear as modified in the snapshot and any applicable snapshot write observer will be
+     * considered initialized. If any state object is modified after this point it will
+     * appear as modified in the snapshot. Any applicable snapshot write observer will be
      * called for the object and the object will be part of the a set of mutated objects sent to
      * any applicable snapshot apply observer.
      *
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/snapshots/SnapshotStateObserver.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/snapshots/SnapshotStateObserver.kt
index 382f190..55240ef 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/snapshots/SnapshotStateObserver.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/snapshots/SnapshotStateObserver.kt
@@ -16,11 +16,14 @@
 
 package androidx.compose.runtime.snapshots
 
+import androidx.compose.runtime.DerivedState
 import androidx.compose.runtime.TestOnly
 import androidx.compose.runtime.collection.IdentityArrayMap
 import androidx.compose.runtime.collection.IdentityArraySet
 import androidx.compose.runtime.collection.IdentityScopeMap
 import androidx.compose.runtime.collection.mutableVectorOf
+import androidx.compose.runtime.observeDerivedStateRecalculations
+import androidx.compose.runtime.structuralEqualityPolicy
 
 /**
  * Helper class to efficiently observe snapshot state reads. See [observeReads] for more details.
@@ -121,7 +124,12 @@
             currentMap = scopeMap
             scopeMap.currentScope = scope
 
-            Snapshot.observe(readObserver, null, block)
+            observeDerivedStateRecalculations(
+                start = { scopeMap.deriveStateScopeCount++ },
+                done = { scopeMap.deriveStateScopeCount-- },
+            ) {
+                Snapshot.observe(readObserver, null, block)
+            }
         } finally {
             scopeMap.currentScope = oldScope
             currentMap = oldMap
@@ -229,6 +237,8 @@
          */
         var currentScope: Any? = null
 
+        var deriveStateScopeCount = 0
+
         /**
          * Values that have been read during the scope's [SnapshotStateObserver.observeReads].
          */
@@ -245,16 +255,34 @@
          */
         private val invalidated = hashSetOf()
 
+        private val dependencyToDerivedStates = IdentityScopeMap>()
+
+        private val derivedStateToValue = HashMap, Any?>()
+
         /**
          * Record that [value] was read in [currentScope].
          */
         fun recordRead(value: Any) {
+            if (deriveStateScopeCount > 0) {
+                // Reads coming from derivedStateOf block
+                return
+            }
+
             val scope = currentScope!!
             valueToScopes.add(value, scope)
+
             val recordedValues = scopeToValues[scope]
                 ?: IdentityArraySet().also { scopeToValues[scope] = it }
-
             recordedValues.add(value)
+
+            if (value is DerivedState<*>) {
+                dependencyToDerivedStates.removeScope(value)
+                val dependencies = value.dependencies
+                for (dependency in dependencies) {
+                    dependencyToDerivedStates.add(dependency, value)
+                }
+                derivedStateToValue[value] = value.currentValue
+            }
         }
 
         /**
@@ -263,7 +291,7 @@
         fun clearScopeObservations(scope: Any) {
             val recordedValues = scopeToValues[scope] ?: return
             recordedValues.fastForEach {
-                valueToScopes.remove(it, scope)
+                removeObservation(scope, it)
             }
             scopeToValues.remove(scope)
         }
@@ -276,19 +304,27 @@
                 val willRemove = predicate(scope)
                 if (willRemove) {
                     valueSet.fastForEach {
-                        valueToScopes.remove(it, scope)
+                        removeObservation(scope, it)
                     }
                 }
                 willRemove
             }
         }
 
+        private fun removeObservation(scope: Any, value: Any) {
+            valueToScopes.remove(value, scope)
+            if (value is DerivedState<*>) {
+                dependencyToDerivedStates.removeScope(value)
+            }
+        }
+
         /**
          * Clear all observations.
          */
         fun clear() {
             valueToScopes.clear()
             scopeToValues.clear()
+            dependencyToDerivedStates.clear()
         }
 
         /**
@@ -298,6 +334,23 @@
         fun recordInvalidation(changes: Set): Boolean {
             var hasValues = false
             for (value in changes) {
+                if (value in dependencyToDerivedStates) {
+                    // Find derived state that is invalidated by this change
+                    dependencyToDerivedStates.forEachScopeOf(value) { derivedState ->
+                        derivedState as DerivedState
+                        val previousValue = derivedStateToValue[derivedState]
+                        val policy = derivedState.policy ?: structuralEqualityPolicy()
+
+                        // Invalidate only if currentValue is different than observed on read
+                        if (!policy.equivalent(derivedState.currentValue, previousValue)) {
+                            valueToScopes.forEachScopeOf(derivedState) { scope ->
+                                invalidated += scope
+                                hasValues = true
+                            }
+                        }
+                    }
+                }
+
                 valueToScopes.forEachScopeOf(value) { scope ->
                     invalidated += scope
                     hasValues = true
diff --git a/compose/runtime/runtime/src/commonTest/kotlin/androidx/compose/runtime/CompositionAndDerivedStateTests.kt b/compose/runtime/runtime/src/commonTest/kotlin/androidx/compose/runtime/CompositionAndDerivedStateTests.kt
index 78cf889..0ed6a87 100644
--- a/compose/runtime/runtime/src/commonTest/kotlin/androidx/compose/runtime/CompositionAndDerivedStateTests.kt
+++ b/compose/runtime/runtime/src/commonTest/kotlin/androidx/compose/runtime/CompositionAndDerivedStateTests.kt
@@ -104,6 +104,37 @@
     }
 
     @Test
+    fun onlyInvalidatesIfResultIsStructurallyDifferent() = compositionTest {
+        var a by mutableStateOf(mutableListOf(32), referentialEqualityPolicy())
+        var b by mutableStateOf(mutableListOf(10), referentialEqualityPolicy())
+        val answer by derivedStateOf { a + b }
+
+        compose {
+            Text("The answer is $answer")
+        }
+
+        validate {
+            Text("The answer is ${a + b}")
+        }
+
+        // A snapshot is necessary here otherwise the ui thread might see one changed but not
+        // the other. A snapshot ensures that both modifications will be seen together.
+        Snapshot.withMutableSnapshot {
+            a = mutableListOf(32)
+            b = mutableListOf(10)
+        }
+
+        // Change both to different instance should not result in a change.
+        expectNoChanges()
+        revalidate()
+
+        a = mutableListOf(32)
+        // Change just one should not result in a change.
+        expectNoChanges()
+        revalidate()
+    }
+
+    @Test
     fun onlyEvaluateDerivedStatesThatAreLive() = compositionTest {
         var a by mutableStateOf(11)
 
@@ -533,6 +564,134 @@
 
         assertEquals(0, conditionalScopes.count { it.isConditional })
     }
+
+    @Test
+    fun derivedStateOfNestedChangesInvalidate() = compositionTest {
+        var a by mutableStateOf(31)
+        var b by mutableStateOf(10)
+        val transient by derivedStateOf { a + b }
+        val answer by derivedStateOf { transient - 1 }
+
+        compose {
+            Text("The answer is $answer")
+        }
+
+        validate {
+            Text("The answer is ${a + b - 1}")
+        }
+
+        a++
+        expectChanges()
+
+        b++
+        expectChanges()
+
+        revalidate()
+    }
+
+    @Test
+    fun derivedStateOfMutationPolicyDoesNotInvalidateNestedStates() = compositionTest {
+        var a by mutableStateOf(30)
+        var b by mutableStateOf(10)
+        val transient by derivedStateOf(structuralEqualityPolicy()) { (a + b) / 10 }
+        var invalidateCount = 0
+        val answer by derivedStateOf {
+            invalidateCount++
+            transient
+        }
+
+        compose {
+            Text("The answer is $answer")
+        }
+
+        validate {
+            Text("The answer is ${(a + b) / 10}")
+        }
+
+        assertEquals(1, invalidateCount)
+
+        a += 10
+        expectChanges()
+        assertEquals(2, invalidateCount)
+
+        b++
+        expectNoChanges()
+        assertEquals(2, invalidateCount)
+
+        revalidate()
+    }
+
+    @Test
+    fun derivedStateOfStructuralMutationPolicyDoesntRecompose() = compositionTest {
+        var a by mutableStateOf(30)
+        var b by mutableStateOf(10)
+        val answer by derivedStateOf(structuralEqualityPolicy()) {
+            listOf(a >= 30, b >= 10)
+        }
+        var compositionCount = 0
+
+        compose {
+            val remembered = rememberUpdatedState(answer)
+            Linear {
+                compositionCount++
+                Text("The answer is ${remembered.value}")
+            }
+        }
+
+        validate {
+            Linear {
+                Text("The answer is ${listOf(true, true)}")
+            }
+        }
+
+        assertEquals(1, compositionCount)
+
+        a++
+        expectNoChanges() // the equivalent list doesn't cause changes
+        assertEquals(1, compositionCount)
+
+        b++
+        expectNoChanges() // the equivalent list doesn't cause changes
+        assertEquals(1, compositionCount)
+
+        revalidate()
+    }
+
+    @Test
+    fun derivedStateOfReferencialMutationPolicyRecomposes() = compositionTest {
+        var a by mutableStateOf(30)
+        var b by mutableStateOf(10)
+        val answer by derivedStateOf(referentialEqualityPolicy()) {
+            listOf(a >= 30, b >= 10)
+        }
+        var compositionCount = 0
+
+        compose {
+            val remembered = rememberUpdatedState(answer)
+            Linear {
+                compositionCount++
+                Text("The answer is ${remembered.value}")
+            }
+        }
+
+        validate {
+            Linear {
+                Text("The answer is ${listOf(true, true)}")
+            }
+        }
+
+        assertEquals(1, compositionCount)
+
+        a++
+        expectNoChanges() // the equivalent list doesn't cause changes
+        assertEquals(2, compositionCount)
+
+        b++
+        expectNoChanges() // the equivalent list doesn't cause changes
+        assertEquals(3, compositionCount)
+
+        revalidate()
+    }
 }
 
 @Composable
diff --git a/compose/runtime/runtime/src/commonTest/kotlin/androidx/compose/runtime/snapshots/DerivedSnapshotStateTests.kt b/compose/runtime/runtime/src/commonTest/kotlin/androidx/compose/runtime/snapshots/DerivedSnapshotStateTests.kt
index 45676b0..d7ec4ee 100644
--- a/compose/runtime/runtime/src/commonTest/kotlin/androidx/compose/runtime/snapshots/DerivedSnapshotStateTests.kt
+++ b/compose/runtime/runtime/src/commonTest/kotlin/androidx/compose/runtime/snapshots/DerivedSnapshotStateTests.kt
@@ -201,9 +201,8 @@
             assertEquals(0, result)
             // 1 for derived, 1 for state
             assertEquals(2, readStates.size)
-            // NOTE: the first calculation will cause two reads of all dependencies: one for the
-            // calculation, and one for the hash calculation, 3 reads total
-            assertEquals(3, readCount)
+            // 1 for derived, 1 for state
+            assertEquals(2, readCount)
             assertEquals(true, readStates.contains(state))
             assertEquals(true, readStates.contains(derived))
         } finally {
diff --git a/compose/runtime/runtime/src/commonTest/kotlin/androidx/compose/runtime/snapshots/SnapshotStateObserverTestsCommon.kt b/compose/runtime/runtime/src/commonTest/kotlin/androidx/compose/runtime/snapshots/SnapshotStateObserverTestsCommon.kt
index 8dc0656..6b9bd48 100644
--- a/compose/runtime/runtime/src/commonTest/kotlin/androidx/compose/runtime/snapshots/SnapshotStateObserverTestsCommon.kt
+++ b/compose/runtime/runtime/src/commonTest/kotlin/androidx/compose/runtime/snapshots/SnapshotStateObserverTestsCommon.kt
@@ -17,7 +17,10 @@
 package androidx.compose.runtime.snapshots
 
 import androidx.compose.runtime.MutableState
+import androidx.compose.runtime.derivedStateOf
 import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.referentialEqualityPolicy
+import androidx.compose.runtime.structuralEqualityPolicy
 import kotlin.test.Test
 import kotlin.test.assertEquals
 
@@ -437,6 +440,108 @@
         assertEquals(0, changes)
     }
 
+    @Test
+    fun derivedStateOfInvalidatesObserver() {
+        var changes = 0
+
+        runSimpleTest { stateObserver, state ->
+            val derivedState = derivedStateOf { state.value }
+
+            stateObserver.observeReads("scope", { changes++ }) {
+                // read
+                derivedState.value
+            }
+        }
+        assertEquals(1, changes)
+    }
+
+    @Test
+    fun derivedStateOfReferentialChangeDoesNotInvalidateObserver() {
+        var changes = 0
+
+        runSimpleTest { stateObserver, _ ->
+            val state = mutableStateOf(mutableListOf(42), referentialEqualityPolicy())
+            val derivedState = derivedStateOf { state.value }
+
+            stateObserver.observeReads("scope", { changes++ }) {
+                // read
+                derivedState.value
+            }
+
+            state.value = mutableListOf(42)
+        }
+        assertEquals(0, changes)
+    }
+
+    @Test
+    fun nestedDerivedStateOfInvalidatesObserver() {
+        var changes = 0
+
+        runSimpleTest { stateObserver, state ->
+            val derivedState = derivedStateOf { state.value }
+            val derivedState2 = derivedStateOf { derivedState.value }
+
+            stateObserver.observeReads("scope", { changes++ }) {
+                // read
+                derivedState2.value
+            }
+        }
+        assertEquals(1, changes)
+    }
+
+    @Test
+    fun derivedStateOfWithReferentialMutationPolicy() {
+        var changes = 0
+
+        runSimpleTest { stateObserver, _ ->
+            val state = mutableStateOf(mutableListOf(1), referentialEqualityPolicy())
+            val derivedState = derivedStateOf(referentialEqualityPolicy()) { state.value }
+
+            stateObserver.observeReads("scope", { changes++ }) {
+                // read
+                derivedState.value
+            }
+
+            state.value = mutableListOf(1)
+        }
+        assertEquals(1, changes)
+    }
+
+    @Test
+    fun derivedStateOfWithStructuralMutationPolicy() {
+        var changes = 0
+
+        runSimpleTest { stateObserver, _ ->
+            val state = mutableStateOf(mutableListOf(1), referentialEqualityPolicy())
+            val derivedState = derivedStateOf(structuralEqualityPolicy()) { state.value }
+
+            stateObserver.observeReads("scope", { changes++ }) {
+                // read
+                derivedState.value
+            }
+
+            state.value = mutableListOf(1)
+        }
+        assertEquals(0, changes)
+    }
+
+    @Test
+    fun readingDerivedStateAndDependencyInvalidates() {
+        var changes = 0
+
+        runSimpleTest { stateObserver, state ->
+            val derivedState = derivedStateOf { state.value >= 0 }
+
+            stateObserver.observeReads("scope", { changes++ }) {
+                // read derived state
+                derivedState.value
+                // read dependency
+                state.value
+            }
+        }
+        assertEquals(1, changes)
+    }
+
     private fun runSimpleTest(
         block: (modelObserver: SnapshotStateObserver, data: MutableState) -> Unit
     ) {
diff --git a/compose/runtime/runtime/src/jvmTest/kotlin/androidx/compose/runtime/snapshots/SnapshotStateObserverTestsJvm.kt b/compose/runtime/runtime/src/jvmTest/kotlin/androidx/compose/runtime/snapshots/SnapshotStateObserverTestsJvm.kt
index 380f3ab..02d3ff8 100644
--- a/compose/runtime/runtime/src/jvmTest/kotlin/androidx/compose/runtime/snapshots/SnapshotStateObserverTestsJvm.kt
+++ b/compose/runtime/runtime/src/jvmTest/kotlin/androidx/compose/runtime/snapshots/SnapshotStateObserverTestsJvm.kt
@@ -178,6 +178,7 @@
         val stateObserver = SnapshotStateObserver { it() }
         try {
             stateObserver.start()
+            Snapshot.notifyObjectsInitialized()
 
             val observer = object : (Any) -> Unit {
                 override fun invoke(affected: Any) {
@@ -194,11 +195,10 @@
                     }
                 }
             }
-
-            state.value++
+            // read with 0
             observer.readWithObservation()
-
-            Snapshot.notifyObjectsInitialized()
+            // increase to 1
+            state.value++
             Snapshot.sendApplyNotifications()
 
             assertEquals(1, changes)
diff --git a/core/core-google-shortcuts/build.gradle b/core/core-google-shortcuts/build.gradle
index 2896ff4..d1da793 100644
--- a/core/core-google-shortcuts/build.gradle
+++ b/core/core-google-shortcuts/build.gradle
@@ -37,7 +37,7 @@
 dependencies {
     api("androidx.core:core:1.7.0")
 
-    implementation("com.google.firebase:firebase-appindexing:20.0.0")
+    implementation "com.google.android.gms:play-services-appindex:16.1.0"
     implementation("com.google.crypto.tink:tink-android:1.5.0")
     implementation projectOrArtifact(":appsearch:appsearch")
     implementation projectOrArtifact(":appsearch:appsearch-builtin-types")
diff --git a/core/core-google-shortcuts/src/androidTest/java/androidx/core/google/shortcuts/ShortcutInfoChangeListenerImplTest.java b/core/core-google-shortcuts/src/androidTest/java/androidx/core/google/shortcuts/ShortcutInfoChangeListenerImplTest.java
index 40dc180..3fa18b1 100644
--- a/core/core-google-shortcuts/src/androidTest/java/androidx/core/google/shortcuts/ShortcutInfoChangeListenerImplTest.java
+++ b/core/core-google-shortcuts/src/androidTest/java/androidx/core/google/shortcuts/ShortcutInfoChangeListenerImplTest.java
@@ -39,11 +39,11 @@
 import androidx.test.filters.SdkSuppress;
 import androidx.test.filters.SmallTest;
 
+import com.google.android.gms.appindex.Action;
+import com.google.android.gms.appindex.AppIndex;
+import com.google.android.gms.appindex.Indexable;
+import com.google.android.gms.appindex.UserActions;
 import com.google.common.collect.ImmutableList;
-import com.google.firebase.appindexing.Action;
-import com.google.firebase.appindexing.FirebaseAppIndex;
-import com.google.firebase.appindexing.FirebaseUserActions;
-import com.google.firebase.appindexing.Indexable;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -58,15 +58,15 @@
 @RunWith(AndroidJUnit4.class)
 @SdkSuppress(minSdkVersion = 21) // This module should only be called for version 21+.
 public class ShortcutInfoChangeListenerImplTest {
-    private FirebaseAppIndex mFirebaseAppIndex;
-    private FirebaseUserActions mFirebaseUserActions;
+    private AppIndex mFirebaseAppIndex;
+    private UserActions mFirebaseUserActions;
     private Context mContext;
     private ShortcutInfoChangeListenerImpl mShortcutInfoChangeListener;
 
     @Before
     public void setUp() {
-        mFirebaseAppIndex = mock(FirebaseAppIndex.class);
-        mFirebaseUserActions = mock(FirebaseUserActions.class);
+        mFirebaseAppIndex = mock(AppIndex.class);
+        mFirebaseUserActions = mock(UserActions.class);
         mContext = ApplicationProvider.getApplicationContext();
         mShortcutInfoChangeListener = new ShortcutInfoChangeListenerImpl(
                 mContext, mFirebaseAppIndex, mFirebaseUserActions, null);
@@ -307,11 +307,9 @@
         verify(mFirebaseUserActions, times(2)).end(actionCaptor.capture());
         Action expectedAction1 = new Action.Builder(Action.Builder.VIEW_ACTION)
                 .setObject("", ShortcutUtils.getIndexableUrl(mContext, "id1"))
-                .setMetadata(new Action.Metadata.Builder().setUpload(false))
                 .build();
         Action expectedAction2 = new Action.Builder(Action.Builder.VIEW_ACTION)
                 .setObject("", ShortcutUtils.getIndexableUrl(mContext, "id2"))
-                .setMetadata(new Action.Metadata.Builder().setUpload(false))
                 .build();
         // Action has no equals comparator, so instead we compare their string forms.
         assertThat(convertActionsToString(actionCaptor.getAllValues())).containsExactly(
@@ -404,19 +402,15 @@
 
         Action expectedAction1 = new Action.Builder(Action.Builder.VIEW_ACTION)
                 .setObject("", ShortcutUtils.getIndexableUrl(mContext, "item1"))
-                .setMetadata(new Action.Metadata.Builder().setUpload(false))
                 .build();
         Action expectedAction2 = new Action.Builder(Action.Builder.VIEW_ACTION)
                 .setObject("", ShortcutUtils.getIndexableUrl(mContext, "item2"))
-                .setMetadata(new Action.Metadata.Builder().setUpload(false))
                 .build();
         Action expectedAction3 = new Action.Builder(Action.Builder.VIEW_ACTION)
                 .setObject("", ShortcutUtils.getIndexableUrl(mContext, "restaurant1"))
-                .setMetadata(new Action.Metadata.Builder().setUpload(false))
                 .build();
         Action expectedAction4 = new Action.Builder(Action.Builder.VIEW_ACTION)
                 .setObject("", ShortcutUtils.getIndexableUrl(mContext, "restaurant2"))
-                .setMetadata(new Action.Metadata.Builder().setUpload(false))
                 .build();
 
 
diff --git a/core/core-google-shortcuts/src/androidTest/java/androidx/core/google/shortcuts/builders/ShortcutBuilderTest.java b/core/core-google-shortcuts/src/androidTest/java/androidx/core/google/shortcuts/builders/ShortcutBuilderTest.java
index 6b4763f..92efab5 100644
--- a/core/core-google-shortcuts/src/androidTest/java/androidx/core/google/shortcuts/builders/ShortcutBuilderTest.java
+++ b/core/core-google-shortcuts/src/androidTest/java/androidx/core/google/shortcuts/builders/ShortcutBuilderTest.java
@@ -22,7 +22,7 @@
 import androidx.test.filters.SdkSuppress;
 import androidx.test.filters.SmallTest;
 
-import com.google.firebase.appindexing.Indexable;
+import com.google.android.gms.appindex.Indexable;
 
 import org.junit.Test;
 import org.junit.runner.RunWith;
diff --git a/core/core-google-shortcuts/src/androidTest/java/androidx/core/google/shortcuts/converters/AlarmConverterTest.java b/core/core-google-shortcuts/src/androidTest/java/androidx/core/google/shortcuts/converters/AlarmConverterTest.java
index 073e4d8..8c2575e 100644
--- a/core/core-google-shortcuts/src/androidTest/java/androidx/core/google/shortcuts/converters/AlarmConverterTest.java
+++ b/core/core-google-shortcuts/src/androidTest/java/androidx/core/google/shortcuts/converters/AlarmConverterTest.java
@@ -27,7 +27,7 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
 
-import com.google.firebase.appindexing.Indexable;
+import com.google.android.gms.appindex.Indexable;
 
 import org.junit.Test;
 import org.junit.runner.RunWith;
diff --git a/core/core-google-shortcuts/src/androidTest/java/androidx/core/google/shortcuts/converters/AlarmInstanceConverterTest.java b/core/core-google-shortcuts/src/androidTest/java/androidx/core/google/shortcuts/converters/AlarmInstanceConverterTest.java
index fbc23b5..2d60607 100644
--- a/core/core-google-shortcuts/src/androidTest/java/androidx/core/google/shortcuts/converters/AlarmInstanceConverterTest.java
+++ b/core/core-google-shortcuts/src/androidTest/java/androidx/core/google/shortcuts/converters/AlarmInstanceConverterTest.java
@@ -26,7 +26,7 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
 
-import com.google.firebase.appindexing.Indexable;
+import com.google.android.gms.appindex.Indexable;
 
 import org.junit.Test;
 import org.junit.runner.RunWith;
diff --git a/core/core-google-shortcuts/src/androidTest/java/androidx/core/google/shortcuts/converters/GenericDocumentConverterTest.java b/core/core-google-shortcuts/src/androidTest/java/androidx/core/google/shortcuts/converters/GenericDocumentConverterTest.java
index d6b73b17..50dcec2a 100644
--- a/core/core-google-shortcuts/src/androidTest/java/androidx/core/google/shortcuts/converters/GenericDocumentConverterTest.java
+++ b/core/core-google-shortcuts/src/androidTest/java/androidx/core/google/shortcuts/converters/GenericDocumentConverterTest.java
@@ -25,7 +25,7 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
 
-import com.google.firebase.appindexing.Indexable;
+import com.google.android.gms.appindex.Indexable;
 
 import org.junit.Test;
 import org.junit.runner.RunWith;
diff --git a/core/core-google-shortcuts/src/androidTest/java/androidx/core/google/shortcuts/converters/TimerConverterTest.java b/core/core-google-shortcuts/src/androidTest/java/androidx/core/google/shortcuts/converters/TimerConverterTest.java
index 1c8d69b..f805be4 100644
--- a/core/core-google-shortcuts/src/androidTest/java/androidx/core/google/shortcuts/converters/TimerConverterTest.java
+++ b/core/core-google-shortcuts/src/androidTest/java/androidx/core/google/shortcuts/converters/TimerConverterTest.java
@@ -29,7 +29,7 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
 
-import com.google.firebase.appindexing.Indexable;
+import com.google.android.gms.appindex.Indexable;
 
 import org.junit.Before;
 import org.junit.Test;
diff --git a/core/core-google-shortcuts/src/androidTest/java/androidx/core/google/shortcuts/utils/ConverterUtilsTest.java b/core/core-google-shortcuts/src/androidTest/java/androidx/core/google/shortcuts/utils/ConverterUtilsTest.java
index 3feec3a..85a57e7 100644
--- a/core/core-google-shortcuts/src/androidTest/java/androidx/core/google/shortcuts/utils/ConverterUtilsTest.java
+++ b/core/core-google-shortcuts/src/androidTest/java/androidx/core/google/shortcuts/utils/ConverterUtilsTest.java
@@ -26,7 +26,7 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
 
-import com.google.firebase.appindexing.Indexable;
+import com.google.android.gms.appindex.Indexable;
 
 import org.junit.Test;
 import org.junit.runner.RunWith;
diff --git a/core/core-google-shortcuts/src/main/java/androidx/core/google/shortcuts/ShortcutInfoChangeListenerImpl.java b/core/core-google-shortcuts/src/main/java/androidx/core/google/shortcuts/ShortcutInfoChangeListenerImpl.java
index a312b72..2067550 100644
--- a/core/core-google-shortcuts/src/main/java/androidx/core/google/shortcuts/ShortcutInfoChangeListenerImpl.java
+++ b/core/core-google-shortcuts/src/main/java/androidx/core/google/shortcuts/ShortcutInfoChangeListenerImpl.java
@@ -42,11 +42,11 @@
 import androidx.core.google.shortcuts.utils.ShortcutUtils;
 import androidx.core.graphics.drawable.IconCompat;
 
+import com.google.android.gms.appindex.Action;
+import com.google.android.gms.appindex.AppIndex;
+import com.google.android.gms.appindex.Indexable;
+import com.google.android.gms.appindex.UserActions;
 import com.google.crypto.tink.KeysetHandle;
-import com.google.firebase.appindexing.Action;
-import com.google.firebase.appindexing.FirebaseAppIndex;
-import com.google.firebase.appindexing.FirebaseUserActions;
-import com.google.firebase.appindexing.Indexable;
 
 import java.util.ArrayList;
 import java.util.List;
@@ -59,8 +59,8 @@
 @RestrictTo(LIBRARY_GROUP)
 public class ShortcutInfoChangeListenerImpl extends ShortcutInfoChangeListener {
     private final Context mContext;
-    private final FirebaseAppIndex mFirebaseAppIndex;
-    private final FirebaseUserActions mFirebaseUserActions;
+    private final AppIndex mFirebaseAppIndex;
+    private final UserActions mFirebaseUserActions;
     @Nullable private final KeysetHandle mKeysetHandle;
 
     /**
@@ -71,14 +71,14 @@
      */
     @NonNull
     public static ShortcutInfoChangeListenerImpl getInstance(@NonNull Context context) {
-        return new ShortcutInfoChangeListenerImpl(context, FirebaseAppIndex.getInstance(context),
-                FirebaseUserActions.getInstance(context),
+        return new ShortcutInfoChangeListenerImpl(context, AppIndex.getInstance(context),
+                UserActions.getInstance(context),
                 ShortcutUtils.getOrCreateShortcutKeysetHandle(context));
     }
 
     @VisibleForTesting
-    ShortcutInfoChangeListenerImpl(Context context, FirebaseAppIndex firebaseAppIndex,
-            FirebaseUserActions firebaseUserActions, @Nullable KeysetHandle keysetHandle) {
+    ShortcutInfoChangeListenerImpl(Context context, AppIndex firebaseAppIndex,
+            UserActions firebaseUserActions, @Nullable KeysetHandle keysetHandle) {
         mContext = context;
         mFirebaseAppIndex = firebaseAppIndex;
         mFirebaseUserActions = firebaseUserActions;
@@ -206,12 +206,9 @@
 
     @NonNull
     private Action buildAction(@NonNull String url) {
-        // The reported action isn't uploaded to the server.
-        Action.Metadata.Builder metadataBuilder = new Action.Metadata.Builder().setUpload(false);
         return new Action.Builder(Action.Builder.VIEW_ACTION)
                 // Empty label as placeholder.
                 .setObject("", url)
-                .setMetadata(metadataBuilder)
                 .build();
     }
 
diff --git a/core/core-google-shortcuts/src/main/java/androidx/core/google/shortcuts/builders/CapabilityBuilder.java b/core/core-google-shortcuts/src/main/java/androidx/core/google/shortcuts/builders/CapabilityBuilder.java
index c95e092..5414efd 100644
--- a/core/core-google-shortcuts/src/main/java/androidx/core/google/shortcuts/builders/CapabilityBuilder.java
+++ b/core/core-google-shortcuts/src/main/java/androidx/core/google/shortcuts/builders/CapabilityBuilder.java
@@ -24,7 +24,7 @@
 import androidx.annotation.Nullable;
 import androidx.annotation.RestrictTo;
 
-import com.google.firebase.appindexing.builders.IndexableBuilder;
+import com.google.android.gms.appindex.builders.IndexableBuilder;
 
 /**
  * Builder for the Capability section in the Shortcut Corpus.
diff --git a/core/core-google-shortcuts/src/main/java/androidx/core/google/shortcuts/builders/ParameterBuilder.java b/core/core-google-shortcuts/src/main/java/androidx/core/google/shortcuts/builders/ParameterBuilder.java
index 0bb0078..f6751d2 100644
--- a/core/core-google-shortcuts/src/main/java/androidx/core/google/shortcuts/builders/ParameterBuilder.java
+++ b/core/core-google-shortcuts/src/main/java/androidx/core/google/shortcuts/builders/ParameterBuilder.java
@@ -24,7 +24,7 @@
 import androidx.annotation.Nullable;
 import androidx.annotation.RestrictTo;
 
-import com.google.firebase.appindexing.builders.IndexableBuilder;
+import com.google.android.gms.appindex.builders.IndexableBuilder;
 
 /**
  * Builder for the Parameter section in the Shortcut Corpus.
diff --git a/core/core-google-shortcuts/src/main/java/androidx/core/google/shortcuts/builders/ShortcutBuilder.java b/core/core-google-shortcuts/src/main/java/androidx/core/google/shortcuts/builders/ShortcutBuilder.java
index d8317cb..07086ec 100644
--- a/core/core-google-shortcuts/src/main/java/androidx/core/google/shortcuts/builders/ShortcutBuilder.java
+++ b/core/core-google-shortcuts/src/main/java/androidx/core/google/shortcuts/builders/ShortcutBuilder.java
@@ -27,7 +27,7 @@
 import androidx.annotation.Nullable;
 import androidx.annotation.RestrictTo;
 
-import com.google.firebase.appindexing.builders.IndexableBuilder;
+import com.google.android.gms.appindex.builders.IndexableBuilder;
 
 /**
  * Builder for the Shortcut Corpus.
diff --git a/core/core-google-shortcuts/src/main/java/androidx/core/google/shortcuts/converters/AlarmConverter.java b/core/core-google-shortcuts/src/main/java/androidx/core/google/shortcuts/converters/AlarmConverter.java
index 789e54a..e265f8b 100644
--- a/core/core-google-shortcuts/src/main/java/androidx/core/google/shortcuts/converters/AlarmConverter.java
+++ b/core/core-google-shortcuts/src/main/java/androidx/core/google/shortcuts/converters/AlarmConverter.java
@@ -27,8 +27,8 @@
 import androidx.core.google.shortcuts.utils.ConverterUtils;
 import androidx.core.util.Preconditions;
 
-import com.google.firebase.appindexing.FirebaseAppIndexingInvalidArgumentException;
-import com.google.firebase.appindexing.Indexable;
+import com.google.android.gms.appindex.AppIndexInvalidArgumentException;
+import com.google.android.gms.appindex.Indexable;
 
 import java.util.ArrayList;
 import java.util.Calendar;
@@ -152,7 +152,7 @@
         if (!alarmInstances.isEmpty()) {
             try {
                 indexableBuilder.put(ALARM_INSTANCES_KEY, alarmInstances.toArray(new Indexable[0]));
-            } catch (FirebaseAppIndexingInvalidArgumentException e) {
+            } catch (AppIndexInvalidArgumentException e) {
                 Log.e(TAG, "Failed to add AlarmInstances to Alarm.", e);
             }
         }
diff --git a/core/core-google-shortcuts/src/main/java/androidx/core/google/shortcuts/converters/AlarmInstanceConverter.java b/core/core-google-shortcuts/src/main/java/androidx/core/google/shortcuts/converters/AlarmInstanceConverter.java
index 1fa4520..bbeab4c 100644
--- a/core/core-google-shortcuts/src/main/java/androidx/core/google/shortcuts/converters/AlarmInstanceConverter.java
+++ b/core/core-google-shortcuts/src/main/java/androidx/core/google/shortcuts/converters/AlarmInstanceConverter.java
@@ -28,7 +28,7 @@
 import androidx.core.google.shortcuts.utils.ConverterUtils;
 import androidx.core.util.Preconditions;
 
-import com.google.firebase.appindexing.Indexable;
+import com.google.android.gms.appindex.Indexable;
 
 import java.text.DateFormat;
 import java.text.ParseException;
diff --git a/core/core-google-shortcuts/src/main/java/androidx/core/google/shortcuts/converters/AppSearchDocumentConverter.java b/core/core-google-shortcuts/src/main/java/androidx/core/google/shortcuts/converters/AppSearchDocumentConverter.java
index 54a1464..b63851e 100644
--- a/core/core-google-shortcuts/src/main/java/androidx/core/google/shortcuts/converters/AppSearchDocumentConverter.java
+++ b/core/core-google-shortcuts/src/main/java/androidx/core/google/shortcuts/converters/AppSearchDocumentConverter.java
@@ -24,7 +24,7 @@
 import androidx.annotation.RestrictTo;
 import androidx.appsearch.app.GenericDocument;
 
-import com.google.firebase.appindexing.Indexable;
+import com.google.android.gms.appindex.Indexable;
 
 /**
  * Interface for a converter that will convert an AppSearch Document into {@link Indexable}.
diff --git a/core/core-google-shortcuts/src/main/java/androidx/core/google/shortcuts/converters/GenericDocumentConverter.java b/core/core-google-shortcuts/src/main/java/androidx/core/google/shortcuts/converters/GenericDocumentConverter.java
index 0c25217..a079c9a 100644
--- a/core/core-google-shortcuts/src/main/java/androidx/core/google/shortcuts/converters/GenericDocumentConverter.java
+++ b/core/core-google-shortcuts/src/main/java/androidx/core/google/shortcuts/converters/GenericDocumentConverter.java
@@ -27,8 +27,8 @@
 import androidx.core.google.shortcuts.utils.ConverterUtils;
 import androidx.core.util.Preconditions;
 
-import com.google.firebase.appindexing.FirebaseAppIndexingInvalidArgumentException;
-import com.google.firebase.appindexing.Indexable;
+import com.google.android.gms.appindex.AppIndexInvalidArgumentException;
+import com.google.android.gms.appindex.Indexable;
 
 /**
  * Default converter for all {@link GenericDocument}. This converter will map each property into
@@ -70,7 +70,7 @@
                 try {
                     indexableBuilder.put(property,
                             convertGenericDocuments(context, (GenericDocument[]) rawProperty));
-                } catch (FirebaseAppIndexingInvalidArgumentException e) {
+                } catch (AppIndexInvalidArgumentException e) {
                     Log.e(TAG, "Cannot convert GenericDocument for property " + property);
                 }
             } else {
diff --git a/core/core-google-shortcuts/src/main/java/androidx/core/google/shortcuts/converters/TimerConverter.java b/core/core-google-shortcuts/src/main/java/androidx/core/google/shortcuts/converters/TimerConverter.java
index 779d764..db5f88a 100644
--- a/core/core-google-shortcuts/src/main/java/androidx/core/google/shortcuts/converters/TimerConverter.java
+++ b/core/core-google-shortcuts/src/main/java/androidx/core/google/shortcuts/converters/TimerConverter.java
@@ -29,7 +29,7 @@
 import androidx.core.google.shortcuts.utils.ConverterUtils;
 import androidx.core.util.Preconditions;
 
-import com.google.firebase.appindexing.Indexable;
+import com.google.android.gms.appindex.Indexable;
 
 /**
  * Convert for the {@link Timer} built-in type.
diff --git a/core/core-google-shortcuts/src/main/java/androidx/core/google/shortcuts/utils/ConverterUtils.java b/core/core-google-shortcuts/src/main/java/androidx/core/google/shortcuts/utils/ConverterUtils.java
index cedac51..bfd4a4f 100644
--- a/core/core-google-shortcuts/src/main/java/androidx/core/google/shortcuts/utils/ConverterUtils.java
+++ b/core/core-google-shortcuts/src/main/java/androidx/core/google/shortcuts/utils/ConverterUtils.java
@@ -28,7 +28,7 @@
 import androidx.core.google.shortcuts.converters.IndexableKeys;
 import androidx.core.util.Preconditions;
 
-import com.google.firebase.appindexing.Indexable;
+import com.google.android.gms.appindex.Indexable;
 
 import java.text.DateFormat;
 import java.text.SimpleDateFormat;
diff --git a/core/settings.gradle b/core/settings.gradle
index aa80db7..ffa5f61 100644
--- a/core/settings.gradle
+++ b/core/settings.gradle
@@ -13,6 +13,7 @@
     selectProjectsFromAndroidX({ name ->
         if (name.startsWith(":core")) return true
         if (name == ":internal-testutils-mockito") return true
+        if (name == ":internal-testutils-fonts") return true
         if (name == ":internal-testutils-runtime") return true
         if (name == ":internal-testutils-truth") return true
         if (name == ":annotation:annotation-sampled") return true
diff --git a/development/ktlint.sh b/development/ktlint.sh
new file mode 100755
index 0000000..754bfaf
--- /dev/null
+++ b/development/ktlint.sh
@@ -0,0 +1,18 @@
+#!/bin/bash
+
+function usage() {
+  echo "usage: $0 "
+  echo
+  echo "Runs the ktlint Gradle task with the provided gradle_arguments, if any of the files to check with .kt(s). Specify files to check using --file= arguments."
+  exit 1
+}
+
+if [ "$1" == "" ]; then
+  usage
+fi
+
+PROJECT_ROOT=$(dirname "$0")/..
+
+if echo "$@" | tr ' ' '\n' | grep -q "^--file=.\+\.kts\?$"; then
+  exec "$PROJECT_ROOT"/gradlew -q -p "$PROJECT_ROOT" --continue :ktlintCheckFile --configuration-cache "$@"
+fi
diff --git a/development/update_studio.sh b/development/update_studio.sh
index 473cc42..a3cb5fb 100755
--- a/development/update_studio.sh
+++ b/development/update_studio.sh
@@ -1,7 +1,7 @@
 #!/bin/bash
 # Get versions
-AGP_VERSION=${1:-7.4.0-alpha07}
-STUDIO_VERSION_STRING=${2:-"Android Studio Electric Eel (2022.1.1) Canary 7"}
+AGP_VERSION=${1:-7.4.0-alpha08}
+STUDIO_VERSION_STRING=${2:-"Android Studio Electric Eel (2022.1.1) Canary 8"}
 STUDIO_IFRAME_LINK=`curl "https://developer.android.com/studio/archive.html" | grep iframe | sed "s/.*src=\"\([a-zA-Z0-9\/\._]*\)\".*/https:\/\/android-dot-devsite-v2-prod.appspot.com\1/g"`
 STUDIO_LINK=`curl -s $STUDIO_IFRAME_LINK | grep -C30 "$STUDIO_VERSION_STRING" | grep Linux | tail -n 1 | sed 's/.*a href="\(.*\).*"/\1/g'`
 STUDIO_VERSION=`echo $STUDIO_LINK | sed "s/.*ide-zips\/\(.*\)\/android-studio-.*/\1/g"`
@@ -43,4 +43,4 @@
 ARTIFACTS_TO_DOWNLOAD+="com.google.testing.platform:core:$ATP_VERSION"
 
 # Download all the artifacts
-./development/importMaven/importMaven.sh import-artifact --artifacts "$ARTIFACTS_TO_DOWNLOAD"
\ No newline at end of file
+./development/importMaven/importMaven.sh "$ARTIFACTS_TO_DOWNLOAD"
\ No newline at end of file
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 1095b04..630cc0f 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -2,13 +2,13 @@
 # -----------------------------------------------------------------------------
 # All of the following should be updated in sync.
 # -----------------------------------------------------------------------------
-androidGradlePlugin = "7.4.0-alpha07"
+androidGradlePlugin = "7.4.0-alpha08"
 # NOTE: When updating the lint version we also need to update the `api` version
 # supported by `IssueRegistry`'s.' For e.g. r.android.com/1331903
-androidLint = "30.4.0-alpha07"
+androidLint = "30.4.0-alpha08"
 # Once you have a chosen version of AGP to upgrade to, go to
 # https://developer.android.com/studio/archive and find the matching version of Studio.
-androidStudio = "2022.1.1.7"
+androidStudio = "2022.1.1.8"
 # -----------------------------------------------------------------------------
 
 androidGradlePluginMin = "7.0.4"
diff --git a/metrics/metrics-performance/src/androidTest/java/androidx/metrics/performance/test/JankStatsTest.kt b/metrics/metrics-performance/src/androidTest/java/androidx/metrics/performance/test/JankStatsTest.kt
index 7582c07..389b651 100644
--- a/metrics/metrics-performance/src/androidTest/java/androidx/metrics/performance/test/JankStatsTest.kt
+++ b/metrics/metrics-performance/src/androidTest/java/androidx/metrics/performance/test/JankStatsTest.kt
@@ -20,12 +20,12 @@
 import android.os.Build.VERSION_CODES.JELLY_BEAN
 import android.view.Choreographer
 import androidx.annotation.RequiresApi
-import androidx.metrics.performance.PerformanceMetricsState
 import androidx.metrics.performance.FrameData
 import androidx.metrics.performance.FrameDataApi24
 import androidx.metrics.performance.FrameDataApi31
 import androidx.metrics.performance.JankStats
 import androidx.metrics.performance.JankStats.OnFrameListener
+import androidx.metrics.performance.PerformanceMetricsState
 import androidx.metrics.performance.StateInfo
 import androidx.test.annotation.UiThreadTest
 import androidx.test.ext.junit.rules.ActivityScenarioRule
@@ -34,10 +34,11 @@
 import androidx.test.filters.SdkSuppress
 import java.util.concurrent.CountDownLatch
 import java.util.concurrent.TimeUnit
+import kotlin.math.max
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.asExecutor
-import org.hamcrest.Matchers
 import org.hamcrest.MatcherAssert.assertThat
+import org.hamcrest.Matchers
 import org.junit.Assert.assertEquals
 import org.junit.Assert.assertFalse
 import org.junit.Assert.assertNotEquals
@@ -74,6 +75,7 @@
             frameInit = FrameInitCompat(this)
         }
     }
+
     @Rule
     @JvmField
     var delayedActivityRule: ActivityScenarioRule =
@@ -87,8 +89,11 @@
             delayedView = delayedActivity.findViewById(R.id.delayedView)
             latchedListener = LatchedListener()
             latchedListener.latch = CountDownLatch(1)
-            jankStats = JankStats.createAndTrack(delayedActivity.window,
-                Dispatchers.Default.asExecutor(), latchedListener)
+            jankStats = JankStats.createAndTrack(
+                delayedActivity.window,
+                Dispatchers.Default.asExecutor(),
+                latchedListener
+            )
             metricsState = PerformanceMetricsState.getForHierarchy(delayedView).state!!
         }
     }
@@ -99,8 +104,35 @@
         assert(PerformanceMetricsState.getForHierarchy(delayedView).state == metricsState)
     }
 
+    /**
+     * Test adding/removing listeners while inside the listener callback (on the same thread)
+     */
     @Test
-    @UiThreadTest
+    fun testConcurrentListenerModifications() {
+        lateinit var jsSecond: JankStats
+        lateinit var jsThird: JankStats
+
+        // We add two more JankStats object with another listener which will add/remove
+        // in the callback. One more listener will not necessarily trigger the issue, because if
+        // that listener is at the end of the internal list of listeners, then the iterator will
+        // not try to find another one after it has been removed. So we make the list large enough
+        // that we will be removing a listener in the middle.
+        val secondListener = OnFrameListener {
+            jsSecond.isTrackingEnabled = !jsSecond.isTrackingEnabled
+            jsThird.isTrackingEnabled = !jsThird.isTrackingEnabled
+        }
+        delayedActivityRule.scenario.onActivity { _ ->
+            // To repro, must add/remove on the same thread as the one used to call the listeners.
+            // That thread is determined by the Executor. Here's a dirty hack to force the executor
+            // to run on the same thread as the one iterating through the listeners.
+            val executor = Runnable::run
+            jsSecond = JankStats.createAndTrack(delayedActivity.window, executor, secondListener)
+            jsThird = JankStats.createAndTrack(delayedActivity.window, executor, secondListener)
+        }
+        runDelayTest(frameDelay = 0, NUM_FRAMES, latchedListener)
+    }
+
+    @Test
     fun testEnable() {
         assertTrue(jankStats.isTrackingEnabled)
         jankStats.isTrackingEnabled = false
@@ -192,8 +224,7 @@
         val scenario = delayedActivityRule.scenario
         scenario.onActivity { _ ->
             jankStats2 = JankStats.createAndTrack(
-                delayedActivity.window,
-                Dispatchers.Default.asExecutor(), secondListener
+                delayedActivity.window, Dispatchers.Default.asExecutor(), secondListener
             )
         }
         val testState = StateInfo("Testing State", "sampleState")
@@ -237,9 +268,9 @@
             }
         }
         listenerPostingThread.start()
-        runDelayTest(frameDelay, NUM_FRAMES * 100, latchedListener)
         // add listeners concurrently - no asserts here, just testing whether we
         // avoid any concurrency issues with adding and using multiple listeners
+        runDelayTest(frameDelay, NUM_FRAMES * 100, latchedListener)
     }
 
     @SdkSuppress(minSdkVersion = JELLY_BEAN)
@@ -510,7 +541,7 @@
             listener.numFrames = 0
         }
         try {
-            latch.await(frameDelay * numFrames + 1000L, TimeUnit.MILLISECONDS)
+            latch.await(max(frameDelay, 1) * numFrames + 1000L, TimeUnit.MILLISECONDS)
         } catch (e: InterruptedException) {
             assert(false)
         }
diff --git a/metrics/metrics-performance/src/main/java/androidx/metrics/performance/JankStatsApi16Impl.kt b/metrics/metrics-performance/src/main/java/androidx/metrics/performance/JankStatsApi16Impl.kt
index c325df5e..e0774cc 100644
--- a/metrics/metrics-performance/src/main/java/androidx/metrics/performance/JankStatsApi16Impl.kt
+++ b/metrics/metrics-performance/src/main/java/androidx/metrics/performance/JankStatsApi16Impl.kt
@@ -64,13 +64,11 @@
     override fun setupFrameTimer(enable: Boolean) {
         val decorView = decorViewRef.get()
         decorView?.let {
-            withDelegates(decorView) {
-                if (enable) {
-                    val delegates = decorView.getOrCreateOnPreDrawListenerDelegates()
-                    delegates.add(onFrameListenerDelegate)
-                } else {
-                    decorView.removeOnPreDrawListenerDelegate(onFrameListenerDelegate)
-                }
+            if (enable) {
+                val delegates = decorView.getOrCreateOnPreDrawListenerDelegator()
+                delegates.add(onFrameListenerDelegate)
+            } else {
+                decorView.removeOnPreDrawListenerDelegate(onFrameListenerDelegate)
             }
         }
     }
@@ -89,13 +87,7 @@
 
     private fun View.removeOnPreDrawListenerDelegate(delegate: OnFrameListenerDelegate) {
         val delegator = getTag(R.id.metricsDelegator) as DelegatingOnPreDrawListener?
-        with(delegator?.delegates) {
-            this?.remove(delegate)
-            if (this?.size == 0) {
-                viewTreeObserver.removeOnPreDrawListener(delegator)
-                setTag(R.id.metricsDelegator, null)
-            }
-        }
+        delegator?.remove(delegate)
     }
 
     /**
@@ -103,8 +95,7 @@
      * If no such list exists, it will create it and add a root listener that
      * delegates to that list.
      */
-    private fun View.getOrCreateOnPreDrawListenerDelegates():
-        MutableList {
+    private fun View.getOrCreateOnPreDrawListenerDelegator(): DelegatingOnPreDrawListener {
         var delegator = getTag(R.id.metricsDelegator) as DelegatingOnPreDrawListener?
         if (delegator == null) {
             val delegates = mutableListOf()
@@ -112,7 +103,7 @@
             viewTreeObserver.addOnPreDrawListener(delegator)
             setTag(R.id.metricsDelegator, delegator)
         }
-        return delegator.delegates
+        return delegator
     }
 
     internal open fun createDelegatingOnDrawListener(
@@ -131,22 +122,6 @@
     fun getExpectedFrameDuration(view: View?): Long {
         return DelegatingOnPreDrawListener.getExpectedFrameDuration(view)
     }
-
-    companion object {
-
-        /**
-         * Anything dealing with the delegates list must go through this function, to avoid the list
-         * changing while being used on a different thread. It synchronizes on the DecorView object
-         * to guarantee that all delegate access is similarly synchronized.
-         */
-        fun withDelegates(decorView: View, delegateAction: () -> Unit) {
-            // prevent concurrent modification of delegates list by synchronizing on
-            // DecorView, which holds the delegator object
-            synchronized(decorView) {
-                delegateAction()
-            }
-        }
-    }
 }
 
 /**
@@ -169,9 +144,32 @@
     val delegates: MutableList
 ) : ViewTreeObserver.OnPreDrawListener {
 
+    // Track whether the delegate list is being iterated, used to prevent concurrent modification
+    var iterating = false
+
+    // These lists cache add/remove requests to be handled after the current iteration loop
+    val toBeAdded = mutableListOf()
+    val toBeRemoved = mutableListOf()
+
     val decorViewRef = WeakReference(decorView)
     val metricsStateHolder = PerformanceMetricsState.getForHierarchy(decorView)
 
+    /**
+     * It is possible for the delegates list to be modified concurrently (adding/removing items
+     * while also iterating through the list). To prevent this, we synchronize on this instance.
+     * It is also possible for the same thread to do both operations, causing reentrance into
+     * that synchronization block. However, the only way that should happen is if the list is
+     * being iterated on (which is called from the UI thread) and, in any of those delegate
+     * listeners, the delegates list is modified
+     * (by calling JankStats.isTrackingEnabled()). In this case, we cache the request in one of the
+     * toBeAdded/Removed lists and return. When iteration is complete, we handle those requests.
+     * This would not be sufficient if those operations could happen randomly on the same thread,
+     * but the order should also be as described above (with add/remove nested inside iteration).
+     *
+     * Iteration and add/remove could also happen randomly and concurrently on different threads,
+     * but in that case the synchronization block around both accesses should suffice.
+     */
+
     override fun onPreDraw(): Boolean {
         val decorView = decorViewRef.get()
         decorView?.let {
@@ -180,10 +178,27 @@
                 handler.sendMessage(Message.obtain(handler) {
                     val now = System.nanoTime()
                     val expectedDuration = getExpectedFrameDuration(decorView)
-                    JankStatsApi16Impl.withDelegates(decorView) {
+                    // prevent concurrent modification of delegates list by synchronizing on
+                    // this delegator object while iterating and modifying
+                    synchronized(this@DelegatingOnPreDrawListener) {
+                        iterating = true
                         for (delegate in delegates) {
                             delegate.onFrame(frameStart, now - frameStart, expectedDuration)
                         }
+                        for (delegate in toBeAdded) {
+                            delegates.add(delegate)
+                        }
+                        toBeAdded.clear()
+                        for (delegate in toBeRemoved) {
+                            delegates.remove(delegate)
+                        }
+                        toBeRemoved.clear()
+                        if (delegates.size == 0) {
+                            viewTreeObserver.removeOnPreDrawListener(
+                                this@DelegatingOnPreDrawListener)
+                            setTag(R.id.metricsDelegator, null)
+                        }
+                        iterating = false
                     }
                     metricsStateHolder.state?.cleanupSingleFrameStates()
                 }.apply {
@@ -194,6 +209,29 @@
         return true
     }
 
+    fun add(delegate: OnFrameListenerDelegate) {
+        // prevent concurrent modification of delegates list by synchronizing on
+        // this delegator object while iterating and modifying
+        synchronized(this) {
+            if (iterating) {
+                toBeAdded.add(delegate)
+            } else {
+                delegates.add(delegate)
+            }
+        }
+    }
+
+    fun remove(delegate: OnFrameListenerDelegate) {
+        // prevent concurrent modification of delegates list by synchronizing on
+        // this delegator object while iterating and modifying
+        synchronized(this) {
+            if (iterating) {
+                toBeRemoved.add(delegate)
+            } else {
+                delegates.remove(delegate)
+            }
+        }
+    }
     private fun getFrameStartTime(): Long {
         return choreographerLastFrameTimeField.get(choreographer) as Long
     }
diff --git a/metrics/metrics-performance/src/main/java/androidx/metrics/performance/JankStatsApi24Impl.kt b/metrics/metrics-performance/src/main/java/androidx/metrics/performance/JankStatsApi24Impl.kt
index 7c331e7..f8658c9 100644
--- a/metrics/metrics-performance/src/main/java/androidx/metrics/performance/JankStatsApi24Impl.kt
+++ b/metrics/metrics-performance/src/main/java/androidx/metrics/performance/JankStatsApi24Impl.kt
@@ -93,20 +93,18 @@
     }
 
     override fun setupFrameTimer(enable: Boolean) {
-        window.let {
-            // prevent concurrent modification of delegates list by synchronizing on
-            // Window, whose DecorView holds the delegator object
-            synchronized(window) {
-                if (enable) {
-                    if (listenerAddedTime == 0L) {
-                        val delegates = window.getOrCreateFrameMetricsListenerDelegates()
-                        delegates.add(frameMetricsAvailableListenerDelegate)
-                        listenerAddedTime = System.nanoTime()
-                    }
-                } else {
-                    window.removeFrameMetricsListenerDelegate(frameMetricsAvailableListenerDelegate)
-                    listenerAddedTime = 0
+        // prevent concurrent modification of delegates list by synchronizing on
+        // Window, whose DecorView holds the delegator object
+        synchronized(window) {
+            if (enable) {
+                if (listenerAddedTime == 0L) {
+                    val delegates = window.getOrCreateFrameMetricsListenerDelegator()
+                    delegates.add(frameMetricsAvailableListenerDelegate)
+                    listenerAddedTime = System.nanoTime()
                 }
+            } else {
+                window.removeFrameMetricsListenerDelegate(frameMetricsAvailableListenerDelegate)
+                listenerAddedTime = 0
             }
         }
     }
@@ -123,9 +121,9 @@
     ) {
         val delegator = decorView.getTag(R.id.metricsDelegator) as
             DelegatingFrameMetricsListener?
-        with(delegator?.delegates) {
+        with(delegator) {
             this?.remove(delegate)
-            if (this?.size == 0) {
+            if (this?.delegates?.size == 0) {
                 removeOnFrameMetricsAvailableListener(delegator)
                 decorView.setTag(R.id.metricsDelegator, null)
             }
@@ -138,8 +136,8 @@
      * delegates to that list.
      */
     @RequiresApi(24)
-    private fun Window.getOrCreateFrameMetricsListenerDelegates():
-        MutableList {
+    private fun Window.getOrCreateFrameMetricsListenerDelegator():
+        DelegatingFrameMetricsListener {
         var delegator = decorView.getTag(R.id.metricsDelegator) as
             DelegatingFrameMetricsListener?
         if (delegator == null) {
@@ -155,7 +153,7 @@
             addOnFrameMetricsAvailableListener(delegator, frameMetricsHandler)
             decorView.setTag(R.id.metricsDelegator, delegator)
         }
-        return delegator.delegates
+        return delegator
     }
 }
 
@@ -169,35 +167,88 @@
 private class DelegatingFrameMetricsListener(
     val delegates: MutableList
 ) : Window.OnFrameMetricsAvailableListener {
+
+    // Track whether the delegate list is being iterated, used to prevent concurrent modification
+    var iterating = false
+
+    // These lists cache add/remove requests to be handled after the current iteration loop
+    val toBeAdded = mutableListOf()
+    val toBeRemoved = mutableListOf()
+
+    /**
+     * It is possible for the delegates list to be modified concurrently (adding/removing items
+     * while also iterating through the list). To prevent this, we synchronize on this instance.
+     * It is also possible for the same thread to do both operations, causing reentrance into
+     * that synchronization block. However, the only way that should happen is if the list is
+     * being iterated on (which is called from the FrameMetrics thread, not accessible to the
+     * JankStats client) and, in any of those delegate listeners, the delegates list is modified
+     * (by calling JankStats.isTrackingEnabled()). In this case, we cache the request in one of the
+     * toBeAdded/Removed lists and return. When iteration is complete, we handle those requests.
+     * This would not be sufficient if those operations could happen randomly on the same thread,
+     * but the order should also be as described above (with add/remove nested inside iteration).
+     *
+     * Iteration and add/remove could also happen randomly and concurrently on different threads,
+     * but in that case the synchronization block around both accesses should suffice.
+     */
+
     override fun onFrameMetricsAvailable(
         window: Window?,
         frameMetrics: FrameMetrics?,
         dropCount: Int
     ) {
-        if (window != null) {
-            // prevent concurrent modification of delegates list by synchronizing on
-            // Window, whose DecorView holds the delegator object
-            window.withDelegates() {
-                for (delegate in delegates) {
-                    delegate.onFrameMetricsAvailable(window, frameMetrics, dropCount)
-                }
+        // prevent concurrent modification of delegates list by synchronizing on
+        // this delegator object while iterating and modifying
+        synchronized(this) {
+            iterating = true
+            for (delegate in delegates) {
+                delegate.onFrameMetricsAvailable(window, frameMetrics, dropCount)
             }
+            if (toBeAdded.isNotEmpty()) {
+                for (delegate in toBeAdded) {
+                    delegates.add(delegate)
+                }
+                toBeAdded.clear()
+            }
+            if (toBeRemoved.isNotEmpty()) {
+                for (delegate in toBeRemoved) {
+                    delegates.remove(delegate)
+                }
+                toBeRemoved.clear()
+            }
+            if (delegates.size == 0) {
+                window?.removeOnFrameMetricsAvailableListener(this)
+                window?.decorView?.setTag(R.id.metricsDelegator, null)
+            }
+            iterating = false
+        }
+        if (window != null) {
             // Remove singleFrame states now that we are done processing this frame
             val holder = PerformanceMetricsState.getForHierarchy(window.decorView)
             holder.state?.cleanupSingleFrameStates()
         }
     }
 
-    /**
-     * Anything dealing with the delegates list must go through this function, to avoid the list
-     * changing while being used on a different thread. It synchronizes on the Window object
-     * to guarantee that all delegate access is similarly synchronized.
-     */
-    fun Window.withDelegates(delegateAction: () -> Unit) {
+    fun add(delegate: Window.OnFrameMetricsAvailableListener) {
         // prevent concurrent modification of delegates list by synchronizing on
-        // DecorView, which holds the delegator object
+        // this delegator object while iterating and modifying
         synchronized(this) {
-            delegateAction()
+            if (iterating) {
+                toBeAdded.add(delegate)
+            } else {
+                delegates.add(delegate)
+            }
+        }
+    }
+
+    fun remove(delegate: Window.OnFrameMetricsAvailableListener) {
+        // prevent concurrent modification of delegates list by synchronizing on
+        // this delegator object while iterating and modifying
+        synchronized(this) {
+            if (iterating) {
+                toBeRemoved.add(delegate)
+            } else {
+                delegates.remove(delegate)
+            }
         }
     }
 }
diff --git a/settings.gradle b/settings.gradle
index 60f1d67..58fa787 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -91,6 +91,7 @@
         value("androidx.compose.multiplatformEnabled", isMultiplatformEnabled().toString())
         value("androidx.projects", getRequestedProjectSubsetName() ?: "Unset")
         value("androidx.useMaxDepVersions", providers.gradleProperty("androidx.useMaxDepVersions").isPresent().toString())
+        value("androidx.disallowTaskExecution", providers.environmentVariable("DISALLOW_TASK_EXECUTION").isPresent().toString())
 
         publishAlways()
         publishIfAuthenticated()
@@ -189,7 +190,8 @@
     WEAR,
     GLANCE,
     TOOLS,
-    KMP
+    KMP,
+    CAMERA
 }
 
 private String getRequestedProjectSubsetName() {
@@ -243,7 +245,10 @@
                 break
             case "KMP":
                 filter.add(BuildType.KMP)
-                break;
+                break
+            case "CAMERA":
+                filter.add(BuildType.CAMERA)
+                break
             case "ALL":
                 // Return null so that no filtering is done
                 return null
@@ -253,6 +258,7 @@
                         "We only support the following:\n" +
                         "ALL     - all androidx projects\n" +
                         "COMPOSE - compose projects\n" +
+                        "CAMERA  - camera projects\n" +
                         "MAIN    - androidx projects that are not compose\n" +
                         "FLAN    - fragment, lifecycle, activity, and navigation projects\n" +
                         "MEDIA   - media, media2, and mediarouter projects\n" +
@@ -393,30 +399,30 @@
     includeProject(":buildSrc-tests:max-dep-versions:buildSrc-tests-max-dep-versions-main", [BuildType.MAIN])
 }
 includeProject(":buildSrc-tests:project-subsets", [BuildType.MAIN])
-includeProject(":camera:camera-camera2", [BuildType.MAIN])
-includeProject(":camera:camera-camera2-pipe", [BuildType.MAIN])
-includeProject(":camera:camera-camera2-pipe-integration", [BuildType.MAIN])
-includeProject(":camera:camera-camera2-pipe-testing", [BuildType.MAIN])
-includeProject(":camera:camera-core", [BuildType.MAIN])
-includeProject(":camera:camera-effects", [BuildType.MAIN])
-includeProject(":camera:camera-extensions", [BuildType.MAIN])
-includeProject(":camera:camera-extensions-stub", [BuildType.MAIN])
-includeProject(":camera:camera-lifecycle", [BuildType.MAIN])
-includeProject(":camera:camera-mlkit-vision", [BuildType.MAIN])
-includeProject(":camera:camera-viewfinder", [BuildType.MAIN])
-includeProject(":camera:camera-testing", [BuildType.MAIN])
-includeProject(":camera:camera-video", [BuildType.MAIN])
-includeProject(":camera:camera-view", [BuildType.MAIN])
-includeProject(":camera:integration-tests:camera-testapp-avsync", "camera/integration-tests/avsynctestapp", [BuildType.MAIN])
-includeProject(":camera:integration-tests:camera-testapp-camera2-pipe", "camera/integration-tests/camerapipetestapp", [BuildType.MAIN])
-includeProject(":camera:integration-tests:camera-testapp-core", "camera/integration-tests/coretestapp", [BuildType.MAIN])
-includeProject(":camera:integration-tests:camera-testapp-diagnose", "camera/integration-tests/diagnosetestapp", [BuildType.MAIN])
-includeProject(":camera:integration-tests:camera-testapp-extensions", "camera/integration-tests/extensionstestapp", [BuildType.MAIN])
-includeProject(":camera:integration-tests:camera-testapp-viewfinder", "camera/integration-tests/viewfindertestapp", [BuildType.MAIN])
-includeProject(":camera:integration-tests:camera-testapp-timing", "camera/integration-tests/timingtestapp", [BuildType.MAIN])
-includeProject(":camera:integration-tests:camera-testapp-uiwidgets", "camera/integration-tests/uiwidgetstestapp", [BuildType.MAIN])
-includeProject(":camera:integration-tests:camera-testapp-view", "camera/integration-tests/viewtestapp", [BuildType.MAIN])
-includeProject(":camera:camera-testlib-extensions", [BuildType.MAIN])
+includeProject(":camera:camera-camera2", [BuildType.CAMERA])
+includeProject(":camera:camera-camera2-pipe", [BuildType.CAMERA])
+includeProject(":camera:camera-camera2-pipe-integration", [BuildType.CAMERA])
+includeProject(":camera:camera-camera2-pipe-testing", [BuildType.CAMERA])
+includeProject(":camera:camera-core", [BuildType.CAMERA])
+includeProject(":camera:camera-effects", [BuildType.CAMERA])
+includeProject(":camera:camera-extensions", [BuildType.CAMERA])
+includeProject(":camera:camera-extensions-stub", [BuildType.CAMERA])
+includeProject(":camera:camera-lifecycle", [BuildType.CAMERA])
+includeProject(":camera:camera-mlkit-vision", [BuildType.CAMERA])
+includeProject(":camera:camera-viewfinder", [BuildType.CAMERA])
+includeProject(":camera:camera-testing", [BuildType.CAMERA])
+includeProject(":camera:camera-video", [BuildType.CAMERA])
+includeProject(":camera:camera-view", [BuildType.CAMERA])
+includeProject(":camera:integration-tests:camera-testapp-avsync", "camera/integration-tests/avsynctestapp", [BuildType.CAMERA])
+includeProject(":camera:integration-tests:camera-testapp-camera2-pipe", "camera/integration-tests/camerapipetestapp", [BuildType.CAMERA])
+includeProject(":camera:integration-tests:camera-testapp-core", "camera/integration-tests/coretestapp", [BuildType.CAMERA])
+includeProject(":camera:integration-tests:camera-testapp-diagnose", "camera/integration-tests/diagnosetestapp", [BuildType.CAMERA])
+includeProject(":camera:integration-tests:camera-testapp-extensions", "camera/integration-tests/extensionstestapp", [BuildType.CAMERA])
+includeProject(":camera:integration-tests:camera-testapp-viewfinder", "camera/integration-tests/viewfindertestapp", [BuildType.CAMERA])
+includeProject(":camera:integration-tests:camera-testapp-timing", "camera/integration-tests/timingtestapp", [BuildType.CAMERA])
+includeProject(":camera:integration-tests:camera-testapp-uiwidgets", "camera/integration-tests/uiwidgetstestapp", [BuildType.CAMERA])
+includeProject(":camera:integration-tests:camera-testapp-view", "camera/integration-tests/viewtestapp", [BuildType.CAMERA])
+includeProject(":camera:camera-testlib-extensions", [BuildType.CAMERA])
 includeProject(":car:app:app", [BuildType.MAIN])
 includeProject(":car:app:app-automotive", [BuildType.MAIN])
 includeProject(":car:app:app-projected", [BuildType.MAIN])
@@ -454,9 +460,9 @@
 includeProject(":compose:animation:animation-graphics:animation-graphics-samples", "compose/animation/animation-graphics/samples", [BuildType.COMPOSE])
 includeProject(":compose:benchmark-utils", [BuildType.COMPOSE])
 includeProject(":compose:benchmark-utils:benchmark-utils-benchmark", "compose/benchmark-utils/benchmark", [BuildType.COMPOSE])
-includeProject(":compose:compiler:compiler", [BuildType.COMPOSE, BuildType.MAIN])
+includeProject(":compose:compiler:compiler", [BuildType.COMPOSE, BuildType.CAMERA])
 includeProject(":compose:compiler:compiler:integration-tests", [BuildType.COMPOSE])
-includeProject(":compose:compiler:compiler-hosted", [BuildType.COMPOSE, BuildType.MAIN])
+includeProject(":compose:compiler:compiler-hosted", [BuildType.COMPOSE, BuildType.CAMERA])
 includeProject(":compose:compiler:compiler-hosted:integration-tests", [BuildType.COMPOSE])
 includeProject(":compose:compiler:compiler-hosted:integration-tests:kotlin-compiler-repackaged", [BuildType.COMPOSE])
 includeProject(":compose:compiler:compiler-daemon", [BuildType.COMPOSE])
@@ -484,10 +490,10 @@
 includeProject(":compose:integration-tests:macrobenchmark", [BuildType.COMPOSE])
 includeProject(":compose:integration-tests:macrobenchmark-target", [BuildType.COMPOSE])
 includeProject(":compose:integration-tests:material-catalog", [BuildType.COMPOSE])
-includeProject(":compose:lint", [BuildType.COMPOSE])
-includeProject(":compose:lint:internal-lint-checks", [BuildType.COMPOSE, BuildType.MAIN])
-includeProject(":compose:lint:common", [BuildType.COMPOSE, BuildType.MAIN])
-includeProject(":compose:lint:common-test", [BuildType.COMPOSE, BuildType.MAIN])
+includeProject(":compose:lint", [BuildType.COMPOSE, BuildType.CAMERA])
+includeProject(":compose:lint:internal-lint-checks", [BuildType.COMPOSE, BuildType.CAMERA])
+includeProject(":compose:lint:common", [BuildType.COMPOSE, BuildType.CAMERA])
+includeProject(":compose:lint:common-test", [BuildType.COMPOSE, BuildType.CAMERA])
 includeProject(":compose:material", [BuildType.COMPOSE])
 includeProject(":compose:material3:material3", [BuildType.COMPOSE])
 includeProject(":compose:material3:material3-lint", [BuildType.COMPOSE])
@@ -512,9 +518,9 @@
 includeProject(":compose:material3:material3:integration-tests:material3-catalog", [BuildType.COMPOSE])
 includeProject(":compose:material:material:material-samples", "compose/material/material/samples", [BuildType.COMPOSE])
 includeProject(":compose:material3:material3:material3-samples", "compose/material3/material3/samples", [BuildType.COMPOSE])
-includeProject(":compose:runtime", [BuildType.COMPOSE, BuildType.MAIN])
-includeProject(":compose:runtime:runtime", [BuildType.COMPOSE, BuildType.MAIN])
-includeProject(":compose:runtime:runtime-lint", [BuildType.COMPOSE, BuildType.MAIN])
+includeProject(":compose:runtime", [BuildType.COMPOSE, BuildType.CAMERA])
+includeProject(":compose:runtime:runtime", [BuildType.COMPOSE, BuildType.CAMERA])
+includeProject(":compose:runtime:runtime-lint", [BuildType.COMPOSE, BuildType.CAMERA])
 includeProject(":compose:runtime:runtime-livedata", [BuildType.COMPOSE])
 includeProject(":compose:runtime:runtime-livedata:runtime-livedata-samples", "compose/runtime/runtime-livedata/samples", [BuildType.COMPOSE])
 includeProject(":compose:runtime:runtime-tracing", [BuildType.COMPOSE])
@@ -527,7 +533,7 @@
 includeProject(":compose:runtime:runtime-saveable:runtime-saveable-samples", "compose/runtime/runtime-saveable/samples", [BuildType.COMPOSE])
 includeProject(":compose:runtime:runtime:benchmark", "compose/runtime/runtime/compose-runtime-benchmark", [BuildType.COMPOSE])
 includeProject(":compose:runtime:runtime:integration-tests", [BuildType.COMPOSE])
-includeProject(":compose:runtime:runtime:runtime-samples", "compose/runtime/runtime/samples", [BuildType.MAIN, BuildType.COMPOSE])
+includeProject(":compose:runtime:runtime:runtime-samples", "compose/runtime/runtime/samples", [BuildType.COMPOSE, BuildType.CAMERA])
 includeProject(":compose:test-utils", [BuildType.COMPOSE])
 includeProject(":compose:ui", [BuildType.COMPOSE])
 includeProject(":compose:ui:ui", [BuildType.COMPOSE])
@@ -561,8 +567,8 @@
 includeProject(":compose:ui:ui-viewbinding:ui-viewbinding-samples", "compose/ui/ui-viewbinding/samples", [BuildType.COMPOSE])
 includeProject(":compose:ui:ui:integration-tests:ui-demos", [BuildType.COMPOSE])
 includeProject(":compose:ui:ui:ui-samples", "compose/ui/ui/samples", [BuildType.COMPOSE])
-includeProject(":concurrent:concurrent-futures", [BuildType.MAIN])
-includeProject(":concurrent:concurrent-futures-ktx", [BuildType.MAIN])
+includeProject(":concurrent:concurrent-futures", [BuildType.MAIN, BuildType.CAMERA])
+includeProject(":concurrent:concurrent-futures-ktx", [BuildType.MAIN, BuildType.CAMERA])
 includeProject(":contentpager:contentpager", [BuildType.MAIN])
 includeProject(":coordinatorlayout:coordinatorlayout", [BuildType.MAIN])
 includeProject(":core:core", [BuildType.MAIN, BuildType.GLANCE, BuildType.MEDIA, BuildType.FLAN, BuildType.COMPOSE, BuildType.WEAR])
@@ -631,16 +637,16 @@
 includeProject(":fragment:fragment-testing-lint", [BuildType.MAIN, BuildType.FLAN])
 includeProject(":fragment:fragment-truth", [BuildType.MAIN, BuildType.FLAN])
 includeProject(":fragment:integration-tests:testapp", [BuildType.MAIN, BuildType.FLAN])
-includeProject(":glance:glance", [BuildType.MAIN, BuildType.GLANCE])
-includeProject(":glance:glance-appwidget", [BuildType.MAIN, BuildType.GLANCE])
-includeProject(":glance:glance-appwidget-preview", [BuildType.MAIN, BuildType.GLANCE])
-includeProject(":glance:glance-appwidget-proto", [BuildType.MAIN, BuildType.GLANCE])
-includeProject(":glance:glance-appwidget:integration-tests:demos", [BuildType.MAIN, BuildType.GLANCE])
-includeProject(":glance:glance-appwidget:integration-tests:template-demos", [BuildType.MAIN, BuildType.GLANCE])
-includeProject(":glance:glance-appwidget:glance-layout-generator", [BuildType.MAIN, BuildType.GLANCE])
-includeProject(":glance:glance-wear-tiles:integration-tests:demos", [BuildType.MAIN, BuildType.GLANCE])
-includeProject(":glance:glance-wear-tiles:integration-tests:template-demos", [BuildType.MAIN, BuildType.GLANCE])
-includeProject(":glance:glance-wear-tiles", [BuildType.MAIN, BuildType.GLANCE])
+includeProject(":glance:glance", [BuildType.GLANCE])
+includeProject(":glance:glance-appwidget", [BuildType.GLANCE])
+includeProject(":glance:glance-appwidget-preview", [BuildType.GLANCE])
+includeProject(":glance:glance-appwidget-proto", [BuildType.GLANCE])
+includeProject(":glance:glance-appwidget:integration-tests:demos", [BuildType.GLANCE])
+includeProject(":glance:glance-appwidget:integration-tests:template-demos", [BuildType.GLANCE])
+includeProject(":glance:glance-appwidget:glance-layout-generator", [BuildType.GLANCE])
+includeProject(":glance:glance-wear-tiles:integration-tests:demos", [BuildType.GLANCE])
+includeProject(":glance:glance-wear-tiles:integration-tests:template-demos", [BuildType.GLANCE])
+includeProject(":glance:glance-wear-tiles", [BuildType.GLANCE])
 includeProject(":gridlayout:gridlayout", [BuildType.MAIN])
 includeProject(":health:health-connect-client", [BuildType.MAIN])
 includeProject(":health:health-connect-client-proto", [BuildType.MAIN])
@@ -668,8 +674,8 @@
 includeProject(":lifecycle:integration-tests:incrementality", [BuildType.MAIN, BuildType.FLAN])
 includeProject(":lifecycle:integration-tests:lifecycle-testapp", "lifecycle/integration-tests/testapp", [BuildType.MAIN, BuildType.FLAN])
 includeProject(":lifecycle:integration-tests:lifecycle-testapp-kotlin", "lifecycle/integration-tests/kotlintestapp", [BuildType.MAIN, BuildType.FLAN])
-includeProject(":lifecycle:lifecycle-common", [BuildType.MAIN, BuildType.FLAN, BuildType.WEAR, BuildType.COMPOSE])
-includeProject(":lifecycle:lifecycle-common-java8", [BuildType.MAIN, BuildType.FLAN, BuildType.WEAR, BuildType.COMPOSE])
+includeProject(":lifecycle:lifecycle-common", [BuildType.MAIN, BuildType.FLAN, BuildType.WEAR, BuildType.COMPOSE, BuildType.CAMERA])
+includeProject(":lifecycle:lifecycle-common-java8", [BuildType.MAIN, BuildType.FLAN, BuildType.WEAR, BuildType.COMPOSE, BuildType.CAMERA])
 includeProject(":lifecycle:lifecycle-compiler", [BuildType.MAIN, BuildType.FLAN])
 includeProject(":lifecycle:lifecycle-extensions", [BuildType.MAIN, BuildType.FLAN])
 includeProject(":lifecycle:lifecycle-livedata", [BuildType.MAIN, BuildType.FLAN])
@@ -895,15 +901,15 @@
 includeProject(":wear:watchface:watchface-style", [BuildType.MAIN, BuildType.WEAR])
 includeProject(":webkit:integration-tests:testapp", [BuildType.MAIN])
 includeProject(":webkit:webkit", [BuildType.MAIN])
-includeProject(":window:window", [BuildType.MAIN, BuildType.COMPOSE, BuildType.FLAN])
-includeProject(":window:extensions:extensions", [BuildType.MAIN, BuildType.COMPOSE, BuildType.FLAN])
+includeProject(":window:window", [BuildType.MAIN, BuildType.COMPOSE, BuildType.FLAN, BuildType.CAMERA])
+includeProject(":window:extensions:extensions", [BuildType.MAIN, BuildType.COMPOSE, BuildType.FLAN, BuildType.CAMERA])
 includeProject(":window:integration-tests:configuration-change-tests", [BuildType.MAIN])
-includeProject(":window:sidecar:sidecar", [BuildType.MAIN, BuildType.COMPOSE, BuildType.FLAN])
-includeProject(":window:window-java", [BuildType.MAIN])
+includeProject(":window:sidecar:sidecar", [BuildType.MAIN, BuildType.COMPOSE, BuildType.FLAN, BuildType.CAMERA])
+includeProject(":window:window-java", [BuildType.MAIN, BuildType.CAMERA])
 includeProject(":window:window-rxjava2", [BuildType.MAIN])
 includeProject(":window:window-rxjava3", [BuildType.MAIN])
-includeProject(":window:window-samples", [BuildType.MAIN, BuildType.COMPOSE, BuildType.FLAN])
-includeProject(":window:window-testing", [BuildType.MAIN, BuildType.COMPOSE, BuildType.FLAN])
+includeProject(":window:window-samples", [BuildType.MAIN, BuildType.COMPOSE, BuildType.FLAN, BuildType.CAMERA])
+includeProject(":window:window-testing", [BuildType.MAIN, BuildType.COMPOSE, BuildType.FLAN, BuildType.CAMERA])
 includeProject(":work:integration-tests:testapp", [BuildType.MAIN])
 includeProject(":work:work-benchmark", [BuildType.MAIN])
 includeProject(":work:work-gcm", [BuildType.MAIN])
@@ -946,11 +952,11 @@
 
 includeProject(":internal-testutils-common", "testutils/testutils-common", [BuildType.MAIN, BuildType.COMPOSE])
 includeProject(":internal-testutils-datastore", "testutils/testutils-datastore", [BuildType.MAIN])
-includeProject(":internal-testutils-runtime", "testutils/testutils-runtime", [BuildType.MAIN, BuildType.FLAN, BuildType.COMPOSE, BuildType.MEDIA, BuildType.WEAR])
+includeProject(":internal-testutils-runtime", "testutils/testutils-runtime", [BuildType.MAIN, BuildType.FLAN, BuildType.COMPOSE, BuildType.MEDIA, BuildType.WEAR, BuildType.CAMERA])
 includeProject(":internal-testutils-appcompat", "testutils/testutils-appcompat", [BuildType.MAIN])
 includeProject(":internal-testutils-espresso", "testutils/testutils-espresso", [BuildType.MAIN, BuildType.COMPOSE])
 includeProject(":internal-testutils-fonts", "testutils/testutils-fonts", [BuildType.MAIN, BuildType.GLANCE, BuildType.MEDIA, BuildType.FLAN, BuildType.COMPOSE, BuildType.WEAR])
-includeProject(":internal-testutils-truth", "testutils/testutils-truth", [BuildType.MAIN, BuildType.GLANCE, BuildType.MEDIA, BuildType.FLAN, BuildType.COMPOSE, BuildType.WEAR, BuildType.KMP])
+includeProject(":internal-testutils-truth", "testutils/testutils-truth", [BuildType.MAIN, BuildType.GLANCE, BuildType.MEDIA, BuildType.FLAN, BuildType.COMPOSE, BuildType.WEAR, BuildType.KMP, BuildType.CAMERA])
 includeProject(":internal-testutils-ktx", "testutils/testutils-ktx")
 includeProject(":internal-testutils-kmp", "testutils/testutils-kmp", [BuildType.MAIN, BuildType.KMP])
 includeProject(":internal-testutils-macrobenchmark", "testutils/testutils-macrobenchmark", [BuildType.MAIN, BuildType.COMPOSE])
@@ -995,7 +1001,7 @@
 
 includeProject(":icing", new File(externalRoot, "icing"), [BuildType.MAIN])
 includeProject(":icing:nativeLib", new File(externalRoot, "icing/nativeLib"), [BuildType.MAIN])
-includeProject(":external:libyuv", [BuildType.MAIN])
+includeProject(":external:libyuv", [BuildType.CAMERA])
 includeProject(":noto-emoji-compat-font", new File(externalRoot, "noto-fonts/emoji-compat"), [BuildType.MAIN])
 includeProject(":noto-emoji-compat-flatbuffers", new File(externalRoot, "noto-fonts/emoji-compat-flatbuffers"), [BuildType.MAIN])
 
diff --git a/studiow b/studiow
index a642819..9ff41c2 100755
--- a/studiow
+++ b/studiow
@@ -25,6 +25,9 @@
   echo " c, compose"
   echo "  Open the project subset compose"
   echo
+  echo " ca, camera"
+  echo "  Open the project subset camera"
+  echo
   echo " f, flan"
   echo "  Open the project subset flan: Fragment, Lifecycle, Activity, and Navigation"
   echo
@@ -86,6 +89,9 @@
   if [ "$subsetArg" == "c" -o "$subsetArg" == "compose" ]; then
     newSubset=compose
   fi
+  if [ "$subsetArg" == "ca" -o "$subsetArg" == "camera" ]; then
+    newSubset=camera
+  fi
   if [ "$subsetArg" == "f" -o "$subsetArg" == "flan" ]; then
     newSubset=flan
   fi
diff --git a/test/uiautomator/uiautomator/api/current.txt b/test/uiautomator/uiautomator/api/current.txt
index 6b15918..90bec63 100644
--- a/test/uiautomator/uiautomator/api/current.txt
+++ b/test/uiautomator/uiautomator/api/current.txt
@@ -320,32 +320,32 @@
   }
 
   public class UiScrollable extends androidx.test.uiautomator.UiCollection {
-    ctor public UiScrollable(androidx.test.uiautomator.UiSelector!);
-    method protected boolean exists(androidx.test.uiautomator.UiSelector!);
+    ctor public UiScrollable(androidx.test.uiautomator.UiSelector);
+    method protected boolean exists(androidx.test.uiautomator.UiSelector);
     method public boolean flingBackward() throws androidx.test.uiautomator.UiObjectNotFoundException;
     method public boolean flingForward() throws androidx.test.uiautomator.UiObjectNotFoundException;
     method public boolean flingToBeginning(int) throws androidx.test.uiautomator.UiObjectNotFoundException;
     method public boolean flingToEnd(int) throws androidx.test.uiautomator.UiObjectNotFoundException;
-    method public androidx.test.uiautomator.UiObject! getChildByDescription(androidx.test.uiautomator.UiSelector!, String!, boolean) throws androidx.test.uiautomator.UiObjectNotFoundException;
-    method public androidx.test.uiautomator.UiObject! getChildByText(androidx.test.uiautomator.UiSelector!, String!, boolean) throws androidx.test.uiautomator.UiObjectNotFoundException;
+    method public androidx.test.uiautomator.UiObject getChildByDescription(androidx.test.uiautomator.UiSelector, String, boolean) throws androidx.test.uiautomator.UiObjectNotFoundException;
+    method public androidx.test.uiautomator.UiObject getChildByText(androidx.test.uiautomator.UiSelector, String, boolean) throws androidx.test.uiautomator.UiObjectNotFoundException;
     method public int getMaxSearchSwipes();
     method public double getSwipeDeadZonePercentage();
     method public boolean scrollBackward() throws androidx.test.uiautomator.UiObjectNotFoundException;
     method public boolean scrollBackward(int) throws androidx.test.uiautomator.UiObjectNotFoundException;
-    method public boolean scrollDescriptionIntoView(String!) throws androidx.test.uiautomator.UiObjectNotFoundException;
+    method public boolean scrollDescriptionIntoView(String) throws androidx.test.uiautomator.UiObjectNotFoundException;
     method public boolean scrollForward() throws androidx.test.uiautomator.UiObjectNotFoundException;
     method public boolean scrollForward(int) throws androidx.test.uiautomator.UiObjectNotFoundException;
-    method public boolean scrollIntoView(androidx.test.uiautomator.UiObject!) throws androidx.test.uiautomator.UiObjectNotFoundException;
-    method public boolean scrollIntoView(androidx.test.uiautomator.UiSelector!) throws androidx.test.uiautomator.UiObjectNotFoundException;
-    method public boolean scrollTextIntoView(String!) throws androidx.test.uiautomator.UiObjectNotFoundException;
+    method public boolean scrollIntoView(androidx.test.uiautomator.UiObject) throws androidx.test.uiautomator.UiObjectNotFoundException;
+    method public boolean scrollIntoView(androidx.test.uiautomator.UiSelector) throws androidx.test.uiautomator.UiObjectNotFoundException;
+    method public boolean scrollTextIntoView(String) throws androidx.test.uiautomator.UiObjectNotFoundException;
     method public boolean scrollToBeginning(int, int) throws androidx.test.uiautomator.UiObjectNotFoundException;
     method public boolean scrollToBeginning(int) throws androidx.test.uiautomator.UiObjectNotFoundException;
     method public boolean scrollToEnd(int, int) throws androidx.test.uiautomator.UiObjectNotFoundException;
     method public boolean scrollToEnd(int) throws androidx.test.uiautomator.UiObjectNotFoundException;
-    method public androidx.test.uiautomator.UiScrollable! setAsHorizontalList();
-    method public androidx.test.uiautomator.UiScrollable! setAsVerticalList();
-    method public androidx.test.uiautomator.UiScrollable! setMaxSearchSwipes(int);
-    method public androidx.test.uiautomator.UiScrollable! setSwipeDeadZonePercentage(double);
+    method public androidx.test.uiautomator.UiScrollable setAsHorizontalList();
+    method public androidx.test.uiautomator.UiScrollable setAsVerticalList();
+    method public androidx.test.uiautomator.UiScrollable setMaxSearchSwipes(int);
+    method public androidx.test.uiautomator.UiScrollable setSwipeDeadZonePercentage(double);
   }
 
   public class UiSelector {
diff --git a/test/uiautomator/uiautomator/api/public_plus_experimental_current.txt b/test/uiautomator/uiautomator/api/public_plus_experimental_current.txt
index 6b15918..90bec63 100644
--- a/test/uiautomator/uiautomator/api/public_plus_experimental_current.txt
+++ b/test/uiautomator/uiautomator/api/public_plus_experimental_current.txt
@@ -320,32 +320,32 @@
   }
 
   public class UiScrollable extends androidx.test.uiautomator.UiCollection {
-    ctor public UiScrollable(androidx.test.uiautomator.UiSelector!);
-    method protected boolean exists(androidx.test.uiautomator.UiSelector!);
+    ctor public UiScrollable(androidx.test.uiautomator.UiSelector);
+    method protected boolean exists(androidx.test.uiautomator.UiSelector);
     method public boolean flingBackward() throws androidx.test.uiautomator.UiObjectNotFoundException;
     method public boolean flingForward() throws androidx.test.uiautomator.UiObjectNotFoundException;
     method public boolean flingToBeginning(int) throws androidx.test.uiautomator.UiObjectNotFoundException;
     method public boolean flingToEnd(int) throws androidx.test.uiautomator.UiObjectNotFoundException;
-    method public androidx.test.uiautomator.UiObject! getChildByDescription(androidx.test.uiautomator.UiSelector!, String!, boolean) throws androidx.test.uiautomator.UiObjectNotFoundException;
-    method public androidx.test.uiautomator.UiObject! getChildByText(androidx.test.uiautomator.UiSelector!, String!, boolean) throws androidx.test.uiautomator.UiObjectNotFoundException;
+    method public androidx.test.uiautomator.UiObject getChildByDescription(androidx.test.uiautomator.UiSelector, String, boolean) throws androidx.test.uiautomator.UiObjectNotFoundException;
+    method public androidx.test.uiautomator.UiObject getChildByText(androidx.test.uiautomator.UiSelector, String, boolean) throws androidx.test.uiautomator.UiObjectNotFoundException;
     method public int getMaxSearchSwipes();
     method public double getSwipeDeadZonePercentage();
     method public boolean scrollBackward() throws androidx.test.uiautomator.UiObjectNotFoundException;
     method public boolean scrollBackward(int) throws androidx.test.uiautomator.UiObjectNotFoundException;
-    method public boolean scrollDescriptionIntoView(String!) throws androidx.test.uiautomator.UiObjectNotFoundException;
+    method public boolean scrollDescriptionIntoView(String) throws androidx.test.uiautomator.UiObjectNotFoundException;
     method public boolean scrollForward() throws androidx.test.uiautomator.UiObjectNotFoundException;
     method public boolean scrollForward(int) throws androidx.test.uiautomator.UiObjectNotFoundException;
-    method public boolean scrollIntoView(androidx.test.uiautomator.UiObject!) throws androidx.test.uiautomator.UiObjectNotFoundException;
-    method public boolean scrollIntoView(androidx.test.uiautomator.UiSelector!) throws androidx.test.uiautomator.UiObjectNotFoundException;
-    method public boolean scrollTextIntoView(String!) throws androidx.test.uiautomator.UiObjectNotFoundException;
+    method public boolean scrollIntoView(androidx.test.uiautomator.UiObject) throws androidx.test.uiautomator.UiObjectNotFoundException;
+    method public boolean scrollIntoView(androidx.test.uiautomator.UiSelector) throws androidx.test.uiautomator.UiObjectNotFoundException;
+    method public boolean scrollTextIntoView(String) throws androidx.test.uiautomator.UiObjectNotFoundException;
     method public boolean scrollToBeginning(int, int) throws androidx.test.uiautomator.UiObjectNotFoundException;
     method public boolean scrollToBeginning(int) throws androidx.test.uiautomator.UiObjectNotFoundException;
     method public boolean scrollToEnd(int, int) throws androidx.test.uiautomator.UiObjectNotFoundException;
     method public boolean scrollToEnd(int) throws androidx.test.uiautomator.UiObjectNotFoundException;
-    method public androidx.test.uiautomator.UiScrollable! setAsHorizontalList();
-    method public androidx.test.uiautomator.UiScrollable! setAsVerticalList();
-    method public androidx.test.uiautomator.UiScrollable! setMaxSearchSwipes(int);
-    method public androidx.test.uiautomator.UiScrollable! setSwipeDeadZonePercentage(double);
+    method public androidx.test.uiautomator.UiScrollable setAsHorizontalList();
+    method public androidx.test.uiautomator.UiScrollable setAsVerticalList();
+    method public androidx.test.uiautomator.UiScrollable setMaxSearchSwipes(int);
+    method public androidx.test.uiautomator.UiScrollable setSwipeDeadZonePercentage(double);
   }
 
   public class UiSelector {
diff --git a/test/uiautomator/uiautomator/api/restricted_current.txt b/test/uiautomator/uiautomator/api/restricted_current.txt
index 6b15918..90bec63 100644
--- a/test/uiautomator/uiautomator/api/restricted_current.txt
+++ b/test/uiautomator/uiautomator/api/restricted_current.txt
@@ -320,32 +320,32 @@
   }
 
   public class UiScrollable extends androidx.test.uiautomator.UiCollection {
-    ctor public UiScrollable(androidx.test.uiautomator.UiSelector!);
-    method protected boolean exists(androidx.test.uiautomator.UiSelector!);
+    ctor public UiScrollable(androidx.test.uiautomator.UiSelector);
+    method protected boolean exists(androidx.test.uiautomator.UiSelector);
     method public boolean flingBackward() throws androidx.test.uiautomator.UiObjectNotFoundException;
     method public boolean flingForward() throws androidx.test.uiautomator.UiObjectNotFoundException;
     method public boolean flingToBeginning(int) throws androidx.test.uiautomator.UiObjectNotFoundException;
     method public boolean flingToEnd(int) throws androidx.test.uiautomator.UiObjectNotFoundException;
-    method public androidx.test.uiautomator.UiObject! getChildByDescription(androidx.test.uiautomator.UiSelector!, String!, boolean) throws androidx.test.uiautomator.UiObjectNotFoundException;
-    method public androidx.test.uiautomator.UiObject! getChildByText(androidx.test.uiautomator.UiSelector!, String!, boolean) throws androidx.test.uiautomator.UiObjectNotFoundException;
+    method public androidx.test.uiautomator.UiObject getChildByDescription(androidx.test.uiautomator.UiSelector, String, boolean) throws androidx.test.uiautomator.UiObjectNotFoundException;
+    method public androidx.test.uiautomator.UiObject getChildByText(androidx.test.uiautomator.UiSelector, String, boolean) throws androidx.test.uiautomator.UiObjectNotFoundException;
     method public int getMaxSearchSwipes();
     method public double getSwipeDeadZonePercentage();
     method public boolean scrollBackward() throws androidx.test.uiautomator.UiObjectNotFoundException;
     method public boolean scrollBackward(int) throws androidx.test.uiautomator.UiObjectNotFoundException;
-    method public boolean scrollDescriptionIntoView(String!) throws androidx.test.uiautomator.UiObjectNotFoundException;
+    method public boolean scrollDescriptionIntoView(String) throws androidx.test.uiautomator.UiObjectNotFoundException;
     method public boolean scrollForward() throws androidx.test.uiautomator.UiObjectNotFoundException;
     method public boolean scrollForward(int) throws androidx.test.uiautomator.UiObjectNotFoundException;
-    method public boolean scrollIntoView(androidx.test.uiautomator.UiObject!) throws androidx.test.uiautomator.UiObjectNotFoundException;
-    method public boolean scrollIntoView(androidx.test.uiautomator.UiSelector!) throws androidx.test.uiautomator.UiObjectNotFoundException;
-    method public boolean scrollTextIntoView(String!) throws androidx.test.uiautomator.UiObjectNotFoundException;
+    method public boolean scrollIntoView(androidx.test.uiautomator.UiObject) throws androidx.test.uiautomator.UiObjectNotFoundException;
+    method public boolean scrollIntoView(androidx.test.uiautomator.UiSelector) throws androidx.test.uiautomator.UiObjectNotFoundException;
+    method public boolean scrollTextIntoView(String) throws androidx.test.uiautomator.UiObjectNotFoundException;
     method public boolean scrollToBeginning(int, int) throws androidx.test.uiautomator.UiObjectNotFoundException;
     method public boolean scrollToBeginning(int) throws androidx.test.uiautomator.UiObjectNotFoundException;
     method public boolean scrollToEnd(int, int) throws androidx.test.uiautomator.UiObjectNotFoundException;
     method public boolean scrollToEnd(int) throws androidx.test.uiautomator.UiObjectNotFoundException;
-    method public androidx.test.uiautomator.UiScrollable! setAsHorizontalList();
-    method public androidx.test.uiautomator.UiScrollable! setAsVerticalList();
-    method public androidx.test.uiautomator.UiScrollable! setMaxSearchSwipes(int);
-    method public androidx.test.uiautomator.UiScrollable! setSwipeDeadZonePercentage(double);
+    method public androidx.test.uiautomator.UiScrollable setAsHorizontalList();
+    method public androidx.test.uiautomator.UiScrollable setAsVerticalList();
+    method public androidx.test.uiautomator.UiScrollable setMaxSearchSwipes(int);
+    method public androidx.test.uiautomator.UiScrollable setSwipeDeadZonePercentage(double);
   }
 
   public class UiSelector {
diff --git a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/UiScrollable.java b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/UiScrollable.java
index 72381f1..8983175 100644
--- a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/UiScrollable.java
+++ b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/UiScrollable.java
@@ -16,11 +16,14 @@
 
 package androidx.test.uiautomator;
 
+import static androidx.annotation.RestrictTo.Scope.LIBRARY;
+
 import android.graphics.Rect;
 import android.util.Log;
 import android.view.accessibility.AccessibilityNodeInfo;
 
 import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
 
 /**
  * UiScrollable is a {@link UiCollection} and provides support for searching
@@ -54,7 +57,7 @@
      *     layout element.
      * @since API Level 16
      */
-    public UiScrollable(UiSelector container) {
+    public UiScrollable(@NonNull UiSelector container) {
         // wrap the container selector with container so that QueryController can handle
         // this type of enumeration search accordingly
         super(container);
@@ -65,6 +68,7 @@
      * @return reference to itself
      * @since API Level 16
      */
+    @NonNull
     public UiScrollable setAsVerticalList() {
         Tracer.trace();
         mIsVerticalList = true;
@@ -76,6 +80,7 @@
      * @return reference to itself
      * @since API Level 16
      */
+    @NonNull
     public UiScrollable setAsHorizontalList() {
         Tracer.trace();
         mIsVerticalList = false;
@@ -90,7 +95,7 @@
      * @return true if found else false
      * @since API Level 16
      */
-    protected boolean exists(UiSelector selector) {
+    protected boolean exists(@NonNull UiSelector selector) {
         if(getQueryController().findAccessibilityNodeInfo(selector) != null) {
             return true;
         }
@@ -139,7 +144,8 @@
      * @throws UiObjectNotFoundException
      * @since API Level 16
      */
-    public UiObject getChildByDescription(UiSelector childPattern, String text,
+    @NonNull
+    public UiObject getChildByDescription(@NonNull UiSelector childPattern, @NonNull String text,
             boolean allowScrollSearch) throws UiObjectNotFoundException {
         Tracer.trace(childPattern, text, allowScrollSearch);
         if (text != null) {
@@ -164,7 +170,7 @@
      */
     @NonNull
     @Override
-    public UiObject getChildByInstance(UiSelector childPattern, int instance)
+    public UiObject getChildByInstance(@NonNull UiSelector childPattern, int instance)
             throws UiObjectNotFoundException {
         Tracer.trace(childPattern, instance);
         UiSelector patternSelector = UiSelector.patternBuilder(getSelector(),
@@ -211,8 +217,10 @@
      * @throws UiObjectNotFoundException
      * @since API Level 16
      */
-    public UiObject getChildByText(UiSelector childPattern, String text, boolean allowScrollSearch)
-            throws UiObjectNotFoundException {
+    @NonNull
+    public UiObject getChildByText(@NonNull UiSelector childPattern,
+            @NonNull String text,
+            boolean allowScrollSearch) throws UiObjectNotFoundException {
         Tracer.trace(childPattern, text, allowScrollSearch);
         if (text != null) {
             if (allowScrollSearch) {
@@ -228,11 +236,13 @@
      * the content-description is found, or until swipe attempts have been exhausted.
      * See {@link #setMaxSearchSwipes(int)}
      *
-     * @param text content-description to find within the contents of this scrollable layout element.
+     * @param text content-description to find within the contents of this scrollable layout
+     * element.
      * @return true if item is found; else, false
      * @since API Level 16
      */
-    public boolean scrollDescriptionIntoView(String text) throws UiObjectNotFoundException {
+    public boolean scrollDescriptionIntoView(@NonNull String text)
+            throws UiObjectNotFoundException {
         Tracer.trace(text);
         return scrollIntoView(new UiSelector().description(text));
     }
@@ -245,7 +255,7 @@
      * @return true if the item was found and now is in view else false
      * @since API Level 16
      */
-    public boolean scrollIntoView(UiObject obj) throws UiObjectNotFoundException {
+    public boolean scrollIntoView(@NonNull UiObject obj) throws UiObjectNotFoundException {
         Tracer.trace(obj.getSelector());
         return scrollIntoView(obj.getSelector());
     }
@@ -260,7 +270,7 @@
      * @return true if the item was found and now is in view; else, false
      * @since API Level 16
      */
-    public boolean scrollIntoView(UiSelector selector) throws UiObjectNotFoundException {
+    public boolean scrollIntoView(@NonNull UiSelector selector) throws UiObjectNotFoundException {
         Tracer.trace(selector);
         // if we happen to be on top of the text we want then return here
         UiSelector childSelector = getSelector().childSelector(selector);
@@ -296,7 +306,9 @@
      * @throws UiObjectNotFoundException
      * @hide
      */
-    public boolean ensureFullyVisible(UiObject childObject) throws UiObjectNotFoundException {
+    @RestrictTo(LIBRARY)
+    public boolean ensureFullyVisible(@NonNull UiObject childObject)
+            throws UiObjectNotFoundException {
         Rect actual = childObject.getBounds();
         Rect visible = childObject.getVisibleBounds();
         if (visible.width() * visible.height() == actual.width() * actual.height()) {
@@ -337,7 +349,7 @@
      * @return true if item is found; else, false
      * @since API Level 16
      */
-    public boolean scrollTextIntoView(String text) throws UiObjectNotFoundException {
+    public boolean scrollTextIntoView(@NonNull String text) throws UiObjectNotFoundException {
         Tracer.trace(text);
         return scrollIntoView(new UiSelector().text(text));
     }
@@ -352,6 +364,7 @@
      * @return reference to itself
      * @since API Level 16
      */
+    @NonNull
     public UiScrollable setMaxSearchSwipes(int swipes) {
         Tracer.trace(swipes);
         mMaxSearchSwipes = swipes;
@@ -513,7 +526,7 @@
         // set otherwise by setAsHorizontalContainer()
         if(mIsVerticalList) {
             int swipeAreaAdjust = (int)(rect.height() * getSwipeDeadZonePercentage());
-            Log.d(LOG_TAG, "scrollToBegining() using vertical scroll");
+            Log.d(LOG_TAG, "scrollToBeginning() using vertical scroll");
             // scroll vertically: swipe up -> down
             downX = rect.centerX();
             downY = rect.top + swipeAreaAdjust;
@@ -521,7 +534,7 @@
             upY = rect.bottom - swipeAreaAdjust;
         } else {
             int swipeAreaAdjust = (int)(rect.width() * getSwipeDeadZonePercentage());
-            Log.d(LOG_TAG, "scrollToBegining() using hotizontal scroll");
+            Log.d(LOG_TAG, "scrollToBeginning() using hotizontal scroll");
             // scroll horizontally: swipe left -> right
             // TODO: Assuming device is not in right to left language
             downX = rect.left + swipeAreaAdjust;
@@ -664,6 +677,7 @@
      * @return reference to itself
      * @since API Level 16
      */
+    @NonNull
     public UiScrollable setSwipeDeadZonePercentage(double swipeDeadZonePercentage) {
         Tracer.trace(swipeDeadZonePercentage);
         mSwipeDeadZonePercentage = swipeDeadZonePercentage;
diff --git a/work/work-runtime/src/androidTest/java/androidx/work/WorkUpdateTest.kt b/work/work-runtime/src/androidTest/java/androidx/work/WorkUpdateTest.kt
index a1ff449..c90bcc8 100644
--- a/work/work-runtime/src/androidTest/java/androidx/work/WorkUpdateTest.kt
+++ b/work/work-runtime/src/androidTest/java/androidx/work/WorkUpdateTest.kt
@@ -424,6 +424,28 @@
         assertThat(worker2.runAttemptCount).isEqualTo(1)
     }
 
+    @MediumTest
+    @Test
+    fun updateCorrectNextRunTime() {
+        val request = OneTimeWorkRequest.Builder(TestWorker::class.java)
+            .setInitialDelay(10, TimeUnit.MINUTES).build()
+        val enqueueTime = System.currentTimeMillis()
+        workManager.enqueue(request).result.get()
+        workManager.workDatabase.workSpecDao().setLastEnqueuedTime(request.stringId,
+            enqueueTime - TimeUnit.MINUTES.toMillis(5))
+        val updated = OneTimeWorkRequest.Builder(TestWorker::class.java)
+            .setInitialDelay(20, TimeUnit.MINUTES)
+            .setId(request.id)
+            .build()
+        workManager.updateWork(updated).get()
+        val workSpec = workManager.workDatabase.workSpecDao().getWorkSpec(request.stringId)!!
+        val delta = workSpec.calculateNextRunTime() - enqueueTime
+        // enqueue time isn't very accurate but delta should be about 15 minutes, because
+        // enqueueTime was rewound 5 minutes back.
+        assertThat(delta).isGreaterThan(TimeUnit.MINUTES.toMillis(14))
+        assertThat(delta).isLessThan(TimeUnit.MINUTES.toMillis(16))
+    }
+
     @After
     fun tearDown() {
         workManager.cancelAllWork()
diff --git a/work/work-runtime/src/main/java/androidx/work/impl/WorkerUpdater.kt b/work/work-runtime/src/main/java/androidx/work/impl/WorkerUpdater.kt
index ea6012d..236e52c 100644
--- a/work/work-runtime/src/main/java/androidx/work/impl/WorkerUpdater.kt
+++ b/work/work-runtime/src/main/java/androidx/work/impl/WorkerUpdater.kt
@@ -54,7 +54,8 @@
         // preserving run attempt count, to calculate back off correctly
         val updatedSpec = newWorkSpec.copy(
             state = oldWorkSpec.state,
-            runAttemptCount = oldWorkSpec.runAttemptCount
+            runAttemptCount = oldWorkSpec.runAttemptCount,
+            lastEnqueueTime = oldWorkSpec.lastEnqueueTime,
         )
         workSpecDao.updateWorkSpec(updatedSpec)
         workTagDao.deleteByWorkSpecId(workSpecId)