Merge "Fix split-screen tests in API 34" into androidx-main
diff --git a/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/AppSearchImplTest.java b/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/AppSearchImplTest.java
index d972613..4fef3f4 100644
--- a/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/AppSearchImplTest.java
+++ b/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/AppSearchImplTest.java
@@ -32,6 +32,7 @@
 import androidx.appsearch.app.GenericDocument;
 import androidx.appsearch.app.GetSchemaResponse;
 import androidx.appsearch.app.InternalSetSchemaResponse;
+import androidx.appsearch.app.JoinSpec;
 import androidx.appsearch.app.PackageIdentifier;
 import androidx.appsearch.app.SearchResult;
 import androidx.appsearch.app.SearchResultPage;
@@ -3323,6 +3324,18 @@
     }
 
     @Test
+    public void testRemoveByQuery_withJoinSpec_throwsException() {
+        Exception e = assertThrows(IllegalArgumentException.class,
+                () -> mAppSearchImpl.removeByQuery("", "", "",
+                        new SearchSpec.Builder()
+                                .setJoinSpec(new JoinSpec.Builder("childProp").build())
+                                .build(),
+                        null));
+        assertThat(e.getMessage()).isEqualTo(
+                "JoinSpec not allowed in removeByQuery, but JoinSpec was provided");
+    }
+
+    @Test
     public void testLimitConfig_Replace() throws Exception {
         // Create a new mAppSearchImpl with a lower limit
         mAppSearchImpl.close();
diff --git a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/AppSearchImpl.java b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/AppSearchImpl.java
index 2307cde..f445af7 100644
--- a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/AppSearchImpl.java
+++ b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/AppSearchImpl.java
@@ -43,6 +43,7 @@
 import androidx.appsearch.app.GetByDocumentIdRequest;
 import androidx.appsearch.app.GetSchemaResponse;
 import androidx.appsearch.app.InternalSetSchemaResponse;
+import androidx.appsearch.app.JoinSpec;
 import androidx.appsearch.app.PackageIdentifier;
 import androidx.appsearch.app.SearchResultPage;
 import androidx.appsearch.app.SearchSpec;
@@ -1825,18 +1826,26 @@
      *
      * <p>This method belongs to mutate group.
      *
+     * <p> {@link SearchSpec} objects containing a {@link JoinSpec} are not allowed here.
+     *
      * @param packageName        The package name that owns the documents.
      * @param databaseName       The databaseName the document is in.
      * @param queryExpression    Query String to search.
      * @param searchSpec         Defines what and how to remove
      * @param removeStatsBuilder builder for {@link RemoveStats} to hold stats for remove
      * @throws AppSearchException on IcingSearchEngine error.
+     * @throws IllegalArgumentException if the {@link SearchSpec} contains a {@link JoinSpec}.
      */
     public void removeByQuery(@NonNull String packageName, @NonNull String databaseName,
             @NonNull String queryExpression,
             @NonNull SearchSpec searchSpec,
             @Nullable RemoveStats.Builder removeStatsBuilder)
             throws AppSearchException {
+        if (searchSpec.getJoinSpec() != null) {
+            throw new IllegalArgumentException("JoinSpec not allowed in removeByQuery, but "
+                    + "JoinSpec was provided");
+        }
+
         long totalLatencyStartTimeMillis = SystemClock.elapsedRealtime();
         mReadWriteLock.writeLock().lock();
         try {
diff --git a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/SearchSessionImpl.java b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/SearchSessionImpl.java
index f66b650..36644b4 100644
--- a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/SearchSessionImpl.java
+++ b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/SearchSessionImpl.java
@@ -508,6 +508,12 @@
             @NonNull String queryExpression, @NonNull SearchSpec searchSpec) {
         Preconditions.checkNotNull(queryExpression);
         Preconditions.checkNotNull(searchSpec);
+
+        if (searchSpec.getJoinSpec() != null) {
+            throw new IllegalArgumentException("JoinSpec not allowed in removeByQuery, but "
+                    + "JoinSpec was provided.");
+        }
+
         Preconditions.checkState(!mIsClosed, "AppSearchSession has already been closed");
         ListenableFuture<Void> future = execute(() -> {
             RemoveStats.Builder removeStatsBuilder = null;
diff --git a/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/SearchSessionImpl.java b/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/SearchSessionImpl.java
index 76e6450..a4621c2 100644
--- a/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/SearchSessionImpl.java
+++ b/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/SearchSessionImpl.java
@@ -204,6 +204,11 @@
         Preconditions.checkNotNull(searchSpec);
         ResolvableFuture<Void> future = ResolvableFuture.create();
 
+        if (searchSpec.getJoinSpec() != null) {
+            throw new IllegalArgumentException("JoinSpec not allowed in removeByQuery, but "
+                    + "JoinSpec was provided.");
+        }
+
         if (!BuildCompat.isAtLeastT() && !searchSpec.getFilterNamespaces().isEmpty()) {
             // This is a patch for b/197361770, framework-appsearch in Android S will
             // disable the given namespace filter if it is not empty and none of given namespaces
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/AppSearchSessionCtsTestBase.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/AppSearchSessionCtsTestBase.java
index bac049f..fb31d2e 100644
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/AppSearchSessionCtsTestBase.java
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/AppSearchSessionCtsTestBase.java
@@ -3220,6 +3220,19 @@
     }
 
     @Test
+    public void testRemoveQueryWithJoinSpecThrowsException() {
+        assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.JOIN_SPEC_AND_QUALIFIED_ID));
+
+        IllegalArgumentException e = assertThrows(IllegalArgumentException.class,
+                () -> mDb2.removeAsync("",
+                new SearchSpec.Builder()
+                        .setJoinSpec(new JoinSpec.Builder("entityId").build())
+                        .build()));
+        assertThat(e.getMessage()).isEqualTo("JoinSpec not allowed in removeByQuery, "
+                + "but JoinSpec was provided.");
+    }
+
+    @Test
     public void testCloseAndReopen() throws Exception {
         // Schema registration
         mDb1.setSchemaAsync(
@@ -3483,20 +3496,20 @@
     public void testIndexNestedDocuments() throws Exception {
         // Schema registration
         mDb1.setSchemaAsync(new SetSchemaRequest.Builder()
-                .addSchemas(AppSearchEmail.SCHEMA)
-                .addSchemas(new AppSearchSchema.Builder("YesNestedIndex")
-                        .addProperty(new AppSearchSchema.DocumentPropertyConfig.Builder(
-                                "prop", AppSearchEmail.SCHEMA_TYPE)
-                                .setShouldIndexNestedProperties(true)
+                        .addSchemas(AppSearchEmail.SCHEMA)
+                        .addSchemas(new AppSearchSchema.Builder("YesNestedIndex")
+                                .addProperty(new AppSearchSchema.DocumentPropertyConfig.Builder(
+                                        "prop", AppSearchEmail.SCHEMA_TYPE)
+                                        .setShouldIndexNestedProperties(true)
+                                        .build())
+                                .build())
+                        .addSchemas(new AppSearchSchema.Builder("NoNestedIndex")
+                                .addProperty(new AppSearchSchema.DocumentPropertyConfig.Builder(
+                                        "prop", AppSearchEmail.SCHEMA_TYPE)
+                                        .setShouldIndexNestedProperties(false)
+                                        .build())
                                 .build())
                         .build())
-                .addSchemas(new AppSearchSchema.Builder("NoNestedIndex")
-                        .addProperty(new AppSearchSchema.DocumentPropertyConfig.Builder(
-                                "prop", AppSearchEmail.SCHEMA_TYPE)
-                                .setShouldIndexNestedProperties(false)
-                                .build())
-                        .build())
-                .build())
                 .get();
 
         // Index the documents.
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/AppSearchSession.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/AppSearchSession.java
index 06a0d6b..16098ebc 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/AppSearchSession.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/AppSearchSession.java
@@ -312,6 +312,9 @@
      *                        indicates how document will be removed. All specific about how to
      *                        scoring, ordering, snippeting and resulting will be ignored.
      * @return The pending result of performing this operation.
+     * @throws IllegalArgumentException if the {@link SearchSpec} contains a {@link JoinSpec}.
+     * {@link JoinSpec} lets you join docs that are not owned by the caller, so the semantics of
+     * failures from this method would be complex.
      */
     @NonNull
     ListenableFuture<Void> removeAsync(@NonNull String queryExpression,
diff --git a/arch/core/core-common/api/2.2.0-beta01.txt b/arch/core/core-common/api/2.2.0-beta01.txt
new file mode 100644
index 0000000..43568b1
--- /dev/null
+++ b/arch/core/core-common/api/2.2.0-beta01.txt
@@ -0,0 +1,9 @@
+// Signature format: 4.0
+package androidx.arch.core.util {
+
+  public interface Function<I, O> {
+    method public O! apply(I!);
+  }
+
+}
+
diff --git a/arch/core/core-common/api/public_plus_experimental_2.2.0-beta01.txt b/arch/core/core-common/api/public_plus_experimental_2.2.0-beta01.txt
new file mode 100644
index 0000000..43568b1
--- /dev/null
+++ b/arch/core/core-common/api/public_plus_experimental_2.2.0-beta01.txt
@@ -0,0 +1,9 @@
+// Signature format: 4.0
+package androidx.arch.core.util {
+
+  public interface Function<I, O> {
+    method public O! apply(I!);
+  }
+
+}
+
diff --git a/arch/core/core-common/api/restricted_2.2.0-beta01.txt b/arch/core/core-common/api/restricted_2.2.0-beta01.txt
new file mode 100644
index 0000000..4fbc435
--- /dev/null
+++ b/arch/core/core-common/api/restricted_2.2.0-beta01.txt
@@ -0,0 +1,41 @@
+// Signature format: 4.0
+package androidx.arch.core.internal {
+
+  @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public class FastSafeIterableMap<K, V> extends androidx.arch.core.internal.SafeIterableMap<K,V> {
+    ctor public FastSafeIterableMap();
+    method public java.util.Map.Entry<K!,V!>? ceil(K!);
+    method public boolean contains(K!);
+  }
+
+  @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public class SafeIterableMap<K, V> implements java.lang.Iterable<java.util.Map.Entry<K,V>> {
+    ctor public SafeIterableMap();
+    method public java.util.Iterator<java.util.Map.Entry<K!,V!>!> descendingIterator();
+    method public java.util.Map.Entry<K!,V!>? eldest();
+    method protected androidx.arch.core.internal.SafeIterableMap.Entry<K!,V!>? get(K!);
+    method public java.util.Iterator<java.util.Map.Entry<K!,V!>!> iterator();
+    method public androidx.arch.core.internal.SafeIterableMap.IteratorWithAdditions iteratorWithAdditions();
+    method public java.util.Map.Entry<K!,V!>? newest();
+    method public V! putIfAbsent(K, V);
+    method public V! remove(K);
+    method public int size();
+  }
+
+  @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public class SafeIterableMap.IteratorWithAdditions extends androidx.arch.core.internal.SafeIterableMap.SupportRemove<K,V> implements java.util.Iterator<java.util.Map.Entry<K,V>> {
+    method public boolean hasNext();
+    method public java.util.Map.Entry<K!,V!>! next();
+  }
+
+  @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public abstract static class SafeIterableMap.SupportRemove<K, V> {
+    ctor public SafeIterableMap.SupportRemove();
+  }
+
+}
+
+package androidx.arch.core.util {
+
+  public interface Function<I, O> {
+    method public O! apply(I!);
+  }
+
+}
+
diff --git a/arch/core/core-runtime/api/2.2.0-beta01.txt b/arch/core/core-runtime/api/2.2.0-beta01.txt
new file mode 100644
index 0000000..e6f50d0
--- /dev/null
+++ b/arch/core/core-runtime/api/2.2.0-beta01.txt
@@ -0,0 +1 @@
+// Signature format: 4.0
diff --git a/arch/core/core-runtime/api/public_plus_experimental_2.2.0-beta01.txt b/arch/core/core-runtime/api/public_plus_experimental_2.2.0-beta01.txt
new file mode 100644
index 0000000..e6f50d0
--- /dev/null
+++ b/arch/core/core-runtime/api/public_plus_experimental_2.2.0-beta01.txt
@@ -0,0 +1 @@
+// Signature format: 4.0
diff --git a/arch/core/core-runtime/api/res-2.2.0-beta01.txt b/arch/core/core-runtime/api/res-2.2.0-beta01.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/arch/core/core-runtime/api/res-2.2.0-beta01.txt
diff --git a/arch/core/core-runtime/api/restricted_2.2.0-beta01.txt b/arch/core/core-runtime/api/restricted_2.2.0-beta01.txt
new file mode 100644
index 0000000..9958884
--- /dev/null
+++ b/arch/core/core-runtime/api/restricted_2.2.0-beta01.txt
@@ -0,0 +1,30 @@
+// Signature format: 4.0
+package androidx.arch.core.executor {
+
+  @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public class ArchTaskExecutor extends androidx.arch.core.executor.TaskExecutor {
+    method public void executeOnDiskIO(Runnable);
+    method public static java.util.concurrent.Executor getIOThreadExecutor();
+    method public static androidx.arch.core.executor.ArchTaskExecutor getInstance();
+    method public static java.util.concurrent.Executor getMainThreadExecutor();
+    method public boolean isMainThread();
+    method public void postToMainThread(Runnable);
+    method public void setDelegate(androidx.arch.core.executor.TaskExecutor?);
+  }
+
+  @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public class DefaultTaskExecutor extends androidx.arch.core.executor.TaskExecutor {
+    ctor public DefaultTaskExecutor();
+    method public void executeOnDiskIO(Runnable);
+    method public boolean isMainThread();
+    method public void postToMainThread(Runnable);
+  }
+
+  @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public abstract class TaskExecutor {
+    ctor public TaskExecutor();
+    method public abstract void executeOnDiskIO(Runnable);
+    method public void executeOnMainThread(Runnable);
+    method public abstract boolean isMainThread();
+    method public abstract void postToMainThread(Runnable);
+  }
+
+}
+
diff --git a/arch/core/core-testing/api/2.2.0-beta01.txt b/arch/core/core-testing/api/2.2.0-beta01.txt
new file mode 100644
index 0000000..0303b8a
--- /dev/null
+++ b/arch/core/core-testing/api/2.2.0-beta01.txt
@@ -0,0 +1,16 @@
+// Signature format: 4.0
+package androidx.arch.core.executor.testing {
+
+  public class CountingTaskExecutorRule extends org.junit.rules.TestWatcher {
+    ctor public CountingTaskExecutorRule();
+    method public void drainTasks(int, java.util.concurrent.TimeUnit) throws java.lang.InterruptedException, java.util.concurrent.TimeoutException;
+    method public boolean isIdle();
+    method protected void onIdle();
+  }
+
+  public class InstantTaskExecutorRule extends org.junit.rules.TestWatcher {
+    ctor public InstantTaskExecutorRule();
+  }
+
+}
+
diff --git a/arch/core/core-testing/api/public_plus_experimental_2.2.0-beta01.txt b/arch/core/core-testing/api/public_plus_experimental_2.2.0-beta01.txt
new file mode 100644
index 0000000..0303b8a
--- /dev/null
+++ b/arch/core/core-testing/api/public_plus_experimental_2.2.0-beta01.txt
@@ -0,0 +1,16 @@
+// Signature format: 4.0
+package androidx.arch.core.executor.testing {
+
+  public class CountingTaskExecutorRule extends org.junit.rules.TestWatcher {
+    ctor public CountingTaskExecutorRule();
+    method public void drainTasks(int, java.util.concurrent.TimeUnit) throws java.lang.InterruptedException, java.util.concurrent.TimeoutException;
+    method public boolean isIdle();
+    method protected void onIdle();
+  }
+
+  public class InstantTaskExecutorRule extends org.junit.rules.TestWatcher {
+    ctor public InstantTaskExecutorRule();
+  }
+
+}
+
diff --git a/arch/core/core-testing/api/res-2.2.0-beta01.txt b/arch/core/core-testing/api/res-2.2.0-beta01.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/arch/core/core-testing/api/res-2.2.0-beta01.txt
diff --git a/arch/core/core-testing/api/restricted_2.2.0-beta01.txt b/arch/core/core-testing/api/restricted_2.2.0-beta01.txt
new file mode 100644
index 0000000..8113a1d
--- /dev/null
+++ b/arch/core/core-testing/api/restricted_2.2.0-beta01.txt
@@ -0,0 +1,35 @@
+// Signature format: 4.0
+package androidx.arch.core.executor {
+
+  @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public class JunitTaskExecutorRule implements org.junit.rules.TestRule {
+    ctor public JunitTaskExecutorRule(int, boolean);
+    method public org.junit.runners.model.Statement apply(org.junit.runners.model.Statement, org.junit.runner.Description);
+    method public void drainTasks(int) throws java.lang.InterruptedException;
+    method public androidx.arch.core.executor.TaskExecutor getTaskExecutor();
+  }
+
+  @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public class TaskExecutorWithFakeMainThread extends androidx.arch.core.executor.TaskExecutor {
+    ctor public TaskExecutorWithFakeMainThread(int);
+    method public void drainTasks(int) throws java.lang.InterruptedException;
+    method public void executeOnDiskIO(Runnable);
+    method public boolean isMainThread();
+    method public void postToMainThread(Runnable);
+  }
+
+}
+
+package androidx.arch.core.executor.testing {
+
+  public class CountingTaskExecutorRule extends org.junit.rules.TestWatcher {
+    ctor public CountingTaskExecutorRule();
+    method public void drainTasks(int, java.util.concurrent.TimeUnit) throws java.lang.InterruptedException, java.util.concurrent.TimeoutException;
+    method public boolean isIdle();
+    method protected void onIdle();
+  }
+
+  public class InstantTaskExecutorRule extends org.junit.rules.TestWatcher {
+    ctor public InstantTaskExecutorRule();
+  }
+
+}
+
diff --git a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/CameraStateMachineTest.kt b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/CameraStateMachineTest.kt
index b8d9b49..12ffb92 100644
--- a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/CameraStateMachineTest.kt
+++ b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/CameraStateMachineTest.kt
@@ -171,10 +171,8 @@
         private val states = mutableListOf<CameraState>()
         private var index = 0
 
-        override fun onChanged(state: CameraState?) {
-            if (state != null) {
-                states.add(state)
-            }
+        override fun onChanged(value: CameraState) {
+            states.add(value)
         }
 
         fun assertHasState(expectedState: CameraState): StateObserver {
diff --git a/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/internal/sessionprocessor/BasicExtenderSessionProcessorTest.kt b/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/internal/sessionprocessor/BasicExtenderSessionProcessorTest.kt
index fddf7c4..46e04fa 100644
--- a/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/internal/sessionprocessor/BasicExtenderSessionProcessorTest.kt
+++ b/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/internal/sessionprocessor/BasicExtenderSessionProcessorTest.kt
@@ -299,8 +299,8 @@
         val cameraClosedLatch = CountDownLatch(1)
         withContext(Dispatchers.Main) {
             camera.cameraInfo.cameraState.observeForever(object : Observer<CameraState?> {
-                override fun onChanged(cameraState: CameraState?) {
-                    if (cameraState?.type == CameraState.Type.CLOSED) {
+                override fun onChanged(value: CameraState?) {
+                    if (value?.type == CameraState.Type.CLOSED) {
                         cameraClosedLatch.countDown()
                         camera.cameraInfo.cameraState.removeObserver(this)
                     }
diff --git a/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeCameraDeviceSurfaceManager.java b/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeCameraDeviceSurfaceManager.java
index 2f630d2..d8201f8 100644
--- a/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeCameraDeviceSurfaceManager.java
+++ b/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeCameraDeviceSurfaceManager.java
@@ -16,6 +16,13 @@
 
 package androidx.camera.testing.fakes;
 
+import static android.graphics.ImageFormat.JPEG;
+import static android.graphics.ImageFormat.YUV_420_888;
+
+import static androidx.camera.core.impl.ImageFormatConstants.INTERNAL_DEFINED_IMAGE_FORMAT_PRIVATE;
+
+import static com.google.common.primitives.Ints.asList;
+
 import android.util.Size;
 
 import androidx.annotation.NonNull;
@@ -26,9 +33,12 @@
 import androidx.camera.core.impl.SurfaceConfig;
 import androidx.camera.core.impl.UseCaseConfig;
 
+import java.util.ArrayList;
 import java.util.HashMap;
+import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
 
 /** A CameraDeviceSurfaceManager which has no supported SurfaceConfigs. */
 @RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
@@ -39,6 +49,8 @@
     private final Map<String, Map<Class<? extends UseCaseConfig<?>>, Size>> mDefinedResolutions =
             new HashMap<>();
 
+    private final Set<List<Integer>> mValidSurfaceCombos = createDefaultValidSurfaceCombos();
+
     /**
      * Sets the given suggested resolutions for the specified camera Id and use case type.
      */
@@ -77,6 +89,7 @@
             @NonNull String cameraId,
             @NonNull List<AttachedSurfaceInfo> existingSurfaces,
             @NonNull List<UseCaseConfig<?>> newUseCaseConfigs) {
+        checkSurfaceCombo(existingSurfaces, newUseCaseConfigs);
         Map<UseCaseConfig<?>, Size> suggestedSizes = new HashMap<>();
         for (UseCaseConfig<?> useCaseConfig : newUseCaseConfigs) {
             Size resolution = MAX_OUTPUT_SIZE;
@@ -94,4 +107,54 @@
 
         return suggestedSizes;
     }
+
+    /**
+     * Checks if the surface combinations is supported.
+     *
+     * <p> Throws {@link IllegalArgumentException} if not supported.
+     */
+    private void checkSurfaceCombo(List<AttachedSurfaceInfo> existingSurfaceInfos,
+            @NonNull List<UseCaseConfig<?>> newSurfaceConfigs) {
+        // Combine existing Surface with new Surface
+        List<Integer> currentCombo = new ArrayList<>();
+        for (UseCaseConfig<?> useCaseConfig : newSurfaceConfigs) {
+            currentCombo.add(useCaseConfig.getInputFormat());
+        }
+        for (AttachedSurfaceInfo surfaceInfo : existingSurfaceInfos) {
+            currentCombo.add(surfaceInfo.getImageFormat());
+        }
+        // Loop through valid combinations and return early if the combo is supported.
+        for (List<Integer> validCombo : mValidSurfaceCombos) {
+            if (isComboSupported(currentCombo, validCombo)) {
+                return;
+            }
+        }
+        // Throw IAE if none of the valid combos supports the current combo.
+        throw new IllegalArgumentException("Surface combo not supported");
+    }
+
+    /**
+     * Checks if the app combination in covered by the given valid combination.
+     */
+    private boolean isComboSupported(@NonNull List<Integer> appCombo,
+            @NonNull List<Integer> validCombo) {
+        List<Integer> combo = new ArrayList<>(validCombo);
+        for (Integer format : appCombo) {
+            if (!combo.remove(format)) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    /**
+     * The default combination is similar to LEGACY level devices.
+     */
+    private static Set<List<Integer>> createDefaultValidSurfaceCombos() {
+        Set<List<Integer>> validCombos = new HashSet<>();
+        validCombos.add(asList(INTERNAL_DEFINED_IMAGE_FORMAT_PRIVATE, YUV_420_888, JPEG));
+        validCombos.add(asList(INTERNAL_DEFINED_IMAGE_FORMAT_PRIVATE,
+                INTERNAL_DEFINED_IMAGE_FORMAT_PRIVATE));
+        return validCombos;
+    }
 }
diff --git a/camera/camera-testing/src/test/java/androidx/camera/testing/fakes/FakeCameraDeviceSurfaceManagerTest.java b/camera/camera-testing/src/test/java/androidx/camera/testing/fakes/FakeCameraDeviceSurfaceManagerTest.java
index 07528fb..c6b7329 100644
--- a/camera/camera-testing/src/test/java/androidx/camera/testing/fakes/FakeCameraDeviceSurfaceManagerTest.java
+++ b/camera/camera-testing/src/test/java/androidx/camera/testing/fakes/FakeCameraDeviceSurfaceManagerTest.java
@@ -16,13 +16,24 @@
 
 package androidx.camera.testing.fakes;
 
+import static android.graphics.ImageFormat.YUV_420_888;
+
+import static androidx.camera.core.impl.SurfaceConfig.ConfigSize.PREVIEW;
+import static androidx.camera.core.impl.SurfaceConfig.ConfigType.YUV;
+
 import static com.google.common.truth.Truth.assertThat;
 
-import static org.mockito.Mockito.mock;
+import static java.util.Arrays.asList;
+import static java.util.Collections.emptyList;
+import static java.util.Collections.singletonList;
 
 import android.os.Build;
+import android.util.Range;
 import android.util.Size;
 
+import androidx.camera.core.ImageAnalysis;
+import androidx.camera.core.impl.AttachedSurfaceInfo;
+import androidx.camera.core.impl.SurfaceConfig;
 import androidx.camera.core.impl.UseCaseConfig;
 
 import org.junit.Before;
@@ -36,6 +47,9 @@
 import java.util.List;
 import java.util.Map;
 
+/**
+ * Unit tests for {@link FakeCameraDeviceSurfaceManager}.
+ */
 @RunWith(RobolectricTestRunner.class)
 @DoNotInstrument
 @Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
@@ -59,14 +73,44 @@
     @Before
     public void setUp() {
         mFakeCameraDeviceSurfaceManager = new FakeCameraDeviceSurfaceManager();
-        mFakeUseCaseConfig = mock(FakeUseCaseConfig.class);
+        mFakeUseCaseConfig = new FakeUseCaseConfig.Builder().getUseCaseConfig();
 
         mFakeCameraDeviceSurfaceManager.setSuggestedResolution(FAKE_CAMERA_ID0,
                 mFakeUseCaseConfig.getClass(), new Size(FAKE_WIDTH0, FAKE_HEIGHT0));
         mFakeCameraDeviceSurfaceManager.setSuggestedResolution(FAKE_CAMERA_ID1,
                 mFakeUseCaseConfig.getClass(), new Size(FAKE_WIDTH1, FAKE_HEIGHT1));
 
-        mUseCaseConfigList = Collections.singletonList((UseCaseConfig<?>) mFakeUseCaseConfig);
+        mUseCaseConfigList = singletonList(mFakeUseCaseConfig);
+    }
+
+    @Test
+    public void validSurfaceCombination_noException() {
+        UseCaseConfig<?> preview = new FakeUseCaseConfig.Builder().getUseCaseConfig();
+        UseCaseConfig<?> analysis = new ImageAnalysis.Builder().getUseCaseConfig();
+        mFakeCameraDeviceSurfaceManager.getSuggestedResolutions(FAKE_CAMERA_ID0,
+                emptyList(), asList(preview, analysis));
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void invalidSurfaceAndConfigCombination_throwException() {
+        UseCaseConfig<?> preview = new FakeUseCaseConfig.Builder().getUseCaseConfig();
+        UseCaseConfig<?> video = new FakeUseCaseConfig.Builder().getUseCaseConfig();
+        AttachedSurfaceInfo analysis = AttachedSurfaceInfo.create(
+                        SurfaceConfig.create(YUV, PREVIEW),
+                        YUV_420_888,
+                        new Size(1, 1),
+                        new Range<>(30, 30));
+        mFakeCameraDeviceSurfaceManager.getSuggestedResolutions(FAKE_CAMERA_ID0,
+                singletonList(analysis), asList(preview, video));
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void invalidConfigCombination_throwException() {
+        UseCaseConfig<?> preview = new FakeUseCaseConfig.Builder().getUseCaseConfig();
+        UseCaseConfig<?> video = new FakeUseCaseConfig.Builder().getUseCaseConfig();
+        UseCaseConfig<?> analysis = new ImageAnalysis.Builder().getUseCaseConfig();
+        mFakeCameraDeviceSurfaceManager.getSuggestedResolutions(FAKE_CAMERA_ID0,
+                Collections.emptyList(), asList(preview, video, analysis));
     }
 
     @Test
@@ -82,7 +126,6 @@
                 new Size(FAKE_WIDTH0, FAKE_HEIGHT0));
         assertThat(suggestedSizesCamera1.get(mFakeUseCaseConfig)).isEqualTo(
                 new Size(FAKE_WIDTH1, FAKE_HEIGHT1));
-
     }
 
 }
diff --git a/compose/foundation/foundation-newtext/src/commonMain/kotlin/androidx/compose/foundation/newtext/text/TextUsingModifier.kt b/compose/foundation/foundation-newtext/src/commonMain/kotlin/androidx/compose/foundation/newtext/text/TextUsingModifier.kt
index 819e0e1..ca96202 100644
--- a/compose/foundation/foundation-newtext/src/commonMain/kotlin/androidx/compose/foundation/newtext/text/TextUsingModifier.kt
+++ b/compose/foundation/foundation-newtext/src/commonMain/kotlin/androidx/compose/foundation/newtext/text/TextUsingModifier.kt
@@ -21,6 +21,7 @@
 import androidx.compose.foundation.newtext.text.modifiers.SelectableTextAnnotatedStringElement
 import androidx.compose.foundation.newtext.text.modifiers.TextAnnotatedStringElement
 import androidx.compose.foundation.newtext.text.modifiers.SelectionController
+import androidx.compose.foundation.newtext.text.modifiers.TextStringSimpleElement
 import androidx.compose.foundation.newtext.text.modifiers.validateMinMaxLines
 import androidx.compose.foundation.text.InlineTextContent
 import androidx.compose.runtime.Composable
@@ -52,13 +53,14 @@
 /**
  * Rewrite of BasicText
  */
+@OptIn(ExperimentalComposeUiApi::class)
 @ExperimentalTextApi
 @Composable
 fun TextUsingModifier(
     text: String,
     modifier: Modifier = Modifier,
     style: TextStyle = TextStyle.Default,
-    onTextLayout: (TextLayoutResult) -> Unit = {},
+    onTextLayout: ((TextLayoutResult) -> Unit)? = null,
     overflow: TextOverflow = TextOverflow.Clip,
     softWrap: Boolean = true,
     maxLines: Int = Int.MAX_VALUE,
@@ -77,8 +79,8 @@
     } else {
         null
     }
-    Layout(
-        modifier = modifier.textModifier(
+    val finalModifier = if (selectionController != null || onTextLayout != null) {
+        modifier.textModifier(
             AnnotatedString(text),
             style = style,
             onTextLayout = onTextLayout,
@@ -90,9 +92,19 @@
             placeholders = null,
             onPlaceholderLayout = null,
             selectionController = selectionController
-        ),
-        EmptyMeasurePolicy
-    )
+        )
+    } else {
+        modifier then TextStringSimpleElement(
+            text,
+            style,
+            LocalFontFamilyResolver.current,
+            overflow,
+            softWrap,
+            maxLines,
+            minLines
+        )
+    }
+    Layout(finalModifier, EmptyMeasurePolicy)
 }
 
 /**
diff --git a/compose/foundation/foundation-newtext/src/commonMain/kotlin/androidx/compose/foundation/newtext/text/modifiers/ParagraphLayoutCache.kt b/compose/foundation/foundation-newtext/src/commonMain/kotlin/androidx/compose/foundation/newtext/text/modifiers/ParagraphLayoutCache.kt
new file mode 100644
index 0000000..45330d0
--- /dev/null
+++ b/compose/foundation/foundation-newtext/src/commonMain/kotlin/androidx/compose/foundation/newtext/text/modifiers/ParagraphLayoutCache.kt
@@ -0,0 +1,389 @@
+/*
+ * Copyright 2023 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.newtext.text.modifiers
+
+import androidx.compose.foundation.newtext.text.DefaultMinLines
+import androidx.compose.foundation.newtext.text.ceilToIntPx
+import androidx.compose.ui.text.AnnotatedString
+import androidx.compose.ui.text.MultiParagraph
+import androidx.compose.ui.text.MultiParagraphIntrinsics
+import androidx.compose.ui.text.Paragraph
+import androidx.compose.ui.text.ParagraphIntrinsics
+import androidx.compose.ui.text.TextLayoutInput
+import androidx.compose.ui.text.TextLayoutResult
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.font.FontFamily
+import androidx.compose.ui.text.resolveDefaults
+import androidx.compose.ui.text.style.LineBreak
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.unit.Constraints
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.IntSize
+import androidx.compose.ui.unit.LayoutDirection
+import androidx.compose.ui.unit.constrain
+
+internal class ParagraphLayoutCache(
+    private var text: String,
+    private var style: TextStyle,
+    private var fontFamilyResolver: FontFamily.Resolver,
+    private var overflow: TextOverflow = TextOverflow.Clip,
+    private var softWrap: Boolean = true,
+    private var maxLines: Int = Int.MAX_VALUE,
+    private var minLines: Int = DefaultMinLines,
+) {
+    internal var density: Density? = null
+        set(value) {
+            val localField = field
+            if (localField == null) {
+                field = value
+                return
+            }
+
+            if (value == null) {
+                field = value
+                markDirty()
+                return
+            }
+
+            if (localField.density != value.density || localField.fontScale != value.fontScale) {
+                field = value
+                // none of our results are correct if density changed
+                markDirty()
+            }
+        }
+    internal val observeFontChanges: Unit
+        get() {
+            paragraphIntrinsics?.hasStaleResolvedFonts
+        }
+
+    internal var paragraph: Paragraph? = null
+    internal var didOverflow: Boolean = false
+    internal var layoutSize: IntSize = IntSize(0, 0)
+
+    private var minMaxLinesCoercer: MinMaxLinesCoercer? = null
+    private var paragraphIntrinsics: ParagraphIntrinsics? = null
+
+    private var intrinsicsLayoutDirection: LayoutDirection? = null
+    private var prevConstraints: Constraints = Constraints.fixed(0, 0)
+
+    private var cachedIntrinsicWidth: Int = -1
+    private var cachedIntrinsicHeight: Int = -1
+
+    private val nonNullIntrinsics: ParagraphIntrinsics
+        get() = paragraphIntrinsics ?: throw IllegalStateException(
+            "MeasureScope.measure() must be called first to query text intrinsics"
+        )
+
+    /**
+     * The width for text if all soft wrap opportunities were taken.
+     *
+     * Valid only after [layoutWithConstraints] has been called.
+     */
+    val minIntrinsicWidth: Int get() = nonNullIntrinsics.minIntrinsicWidth.ceilToIntPx()
+
+    /**
+     * The width at which increasing the width of the text no lonfger decreases the height.
+     *
+     * Valid only after [layoutWithConstraints] has been called.
+     */
+    val maxIntrinsicWidth: Int get() = nonNullIntrinsics.maxIntrinsicWidth.ceilToIntPx()
+
+    /**
+     * Update layout constraints for this text
+     *
+     * @return true if constraints caused a text layout invalidation
+     */
+    fun layoutWithConstraints(
+        constraints: Constraints,
+        layoutDirection: LayoutDirection
+    ): Boolean {
+        val finalConstraints = if (maxLines != Int.MAX_VALUE || minLines > 1) {
+            val localMinMax = MinMaxLinesCoercer.from(
+                minMaxLinesCoercer,
+                layoutDirection,
+                style,
+                density!!,
+                fontFamilyResolver
+            ).also {
+                minMaxLinesCoercer = it
+            }
+            localMinMax.coerceMaxMinLines(
+                inConstraints = constraints,
+                minLines = minLines,
+                maxLines = maxLines
+            )
+        } else {
+            constraints
+        }
+        if (!newLayoutWillBeDifferent(finalConstraints, layoutDirection)) {
+            return false
+        }
+        paragraph = layoutText(finalConstraints, layoutDirection).also {
+            prevConstraints = finalConstraints
+            val localSize = finalConstraints.constrain(
+                IntSize(
+                    it.width.ceilToIntPx(),
+                    it.height.ceilToIntPx()
+                )
+            )
+            layoutSize = localSize
+            didOverflow = overflow != TextOverflow.Visible &&
+                (localSize.width < it.width || localSize.height < it.height)
+        }
+        return true
+    }
+
+    fun intrinsicHeightAt(width: Int, layoutDirection: LayoutDirection): Int {
+        val localWidth = cachedIntrinsicWidth
+        val localHeght = cachedIntrinsicHeight
+        if (width == localWidth && localWidth != -1) return localHeght
+        val result = layoutText(
+            Constraints(0, width, 0, Constraints.Infinity),
+            layoutDirection
+        ).height.ceilToIntPx()
+
+        cachedIntrinsicWidth = width
+        cachedIntrinsicHeight = result
+        return result
+    }
+
+    fun update(
+        text: String,
+        style: TextStyle,
+        fontFamilyResolver: FontFamily.Resolver,
+        overflow: TextOverflow,
+        softWrap: Boolean,
+        maxLines: Int,
+        minLines: Int
+    ) {
+        this.text = text
+        this.style = style
+        this.fontFamilyResolver = fontFamilyResolver
+        this.overflow = overflow
+        this.softWrap = softWrap
+        this.maxLines = maxLines
+        this.minLines = minLines
+        markDirty()
+    }
+
+    private fun setLayoutDirection(layoutDirection: LayoutDirection) {
+        val localIntrinsics = paragraphIntrinsics
+        val intrinsics = if (
+            localIntrinsics == null ||
+            layoutDirection != intrinsicsLayoutDirection ||
+            localIntrinsics.hasStaleResolvedFonts
+        ) {
+            intrinsicsLayoutDirection = layoutDirection
+            ParagraphIntrinsics(
+                text = text,
+                style = resolveDefaults(style, layoutDirection),
+                density = density!!,
+                fontFamilyResolver = fontFamilyResolver
+            )
+        } else {
+            localIntrinsics
+        }
+        paragraphIntrinsics = intrinsics
+    }
+
+    /**
+     * Computes the visual position of the glyphs for painting the text.
+     *
+     * The text will layout with a width that's as close to its max intrinsic width as possible
+     * while still being greater than or equal to `minWidth` and less than or equal to `maxWidth`.
+     */
+    private fun layoutText(
+        constraints: Constraints,
+        layoutDirection: LayoutDirection
+    ): Paragraph {
+        setLayoutDirection(layoutDirection)
+
+        val minWidth = constraints.minWidth
+        val widthMatters = softWrap || overflow == TextOverflow.Ellipsis
+        val maxWidth = if (widthMatters && constraints.hasBoundedWidth) {
+            constraints.maxWidth
+        } else {
+            Constraints.Infinity
+        }
+
+        // This is a fallback behavior because native text layout doesn't support multiple
+        // ellipsis in one text layout.
+        // When softWrap is turned off and overflow is ellipsis, it's expected that each line
+        // that exceeds maxWidth will be ellipsized.
+        // For example,
+        // input text:
+        //     "AAAA\nAAAA"
+        // maxWidth:
+        //     3 * fontSize that only allow 3 characters to be displayed each line.
+        // expected output:
+        //     AA…
+        //     AA…
+        // Here we assume there won't be any '\n' character when softWrap is false. And make
+        // maxLines 1 to implement the similar behavior.
+        val overwriteMaxLines = !softWrap && overflow == TextOverflow.Ellipsis
+        val finalMaxLines = if (overwriteMaxLines) 1 else maxLines.coerceAtLeast(1)
+
+        // if minWidth == maxWidth the width is fixed.
+        //    therefore we can pass that value to our paragraph and use it
+        // if minWidth != maxWidth there is a range
+        //    then we should check if the max intrinsic width is in this range to decide the
+        //    width to be passed to Paragraph
+        //        if max intrinsic width is between minWidth and maxWidth
+        //           we can use it to layout
+        //        else if max intrinsic width is greater than maxWidth, we can only use maxWidth
+        //        else if max intrinsic width is less than minWidth, we should use minWidth
+        val width = if (minWidth == maxWidth) {
+            maxWidth
+        } else {
+            maxIntrinsicWidth.coerceIn(minWidth, maxWidth)
+        }
+
+        val finalConstraints = Constraints(maxWidth = width, maxHeight = constraints.maxHeight)
+        return Paragraph(
+            paragraphIntrinsics = nonNullIntrinsics,
+            constraints = finalConstraints,
+            // This is a fallback behavior for ellipsis. Native
+            maxLines = finalMaxLines,
+            ellipsis = overflow == TextOverflow.Ellipsis
+        )
+    }
+
+    private fun newLayoutWillBeDifferent(
+        constraints: Constraints,
+        layoutDirection: LayoutDirection
+    ): Boolean {
+        val localParagraph = paragraph ?: return true
+        val localParagraphIntrinsics = paragraphIntrinsics ?: return true
+        // no layout yet
+
+        // async typeface changes
+        if (localParagraphIntrinsics.hasStaleResolvedFonts) return true
+
+        // layout direction changed
+        if (layoutDirection != intrinsicsLayoutDirection) return true
+
+        // if we were passed identical constraints just skip more work
+        if (constraints == prevConstraints) return false
+
+        // only be clever if we can predict line break behavior exactly, which is only possible with
+        // simple geometry math for the greedy layout case
+        if (style.lineBreak != LineBreak.Simple) {
+            return true
+        }
+
+        // see if width would produce the same wraps (greedy wraps only)
+        val canWrap = softWrap && maxLines > 1
+        if (canWrap && layoutSize.width != localParagraph.maxIntrinsicWidth.ceilToIntPx()) {
+            // some soft wrapping happened, check to see if we're between the previous measure and
+            // the next wrap
+            val prevActualMaxWidth = maxWidth(prevConstraints)
+            val newMaxWidth = maxWidth(constraints)
+            if (newMaxWidth > prevActualMaxWidth) {
+                // we've grown the potential layout area, and may break longer lines
+                return true
+            }
+            if (newMaxWidth <= layoutSize.width) {
+                // it's possible to shrink this text (possible opt: check minIntrinsicWidth
+                return true
+            }
+        }
+
+        // check any constraint width changes for single line text
+        if (!canWrap &&
+            (constraints.maxWidth != prevConstraints.maxWidth ||
+                (constraints.minWidth != prevConstraints.minWidth))) {
+            // no soft wrap and width is different, always invalidate
+            return true
+        }
+
+        // if we get here width won't change, height may be clipped
+        if (constraints.maxHeight < localParagraph.height) {
+            // vertical clip changes
+            return true
+        }
+
+        // breaks can't change, height can't change
+        return false
+    }
+
+    private fun maxWidth(constraints: Constraints): Int {
+        val minWidth = constraints.minWidth
+        val widthMatters = softWrap || overflow == TextOverflow.Ellipsis
+        val maxWidth = if (widthMatters && constraints.hasBoundedWidth) {
+            constraints.maxWidth
+        } else {
+            Constraints.Infinity
+        }
+        return if (minWidth == maxWidth) {
+            maxWidth
+        } else {
+            maxIntrinsicWidth.coerceIn(minWidth, maxWidth)
+        }
+    }
+
+    private fun markDirty() {
+        paragraph = null
+        paragraphIntrinsics = null
+        intrinsicsLayoutDirection = null
+        cachedIntrinsicWidth = -1
+        cachedIntrinsicHeight = -1
+        prevConstraints = Constraints.fixed(0, 0)
+        layoutSize = IntSize(0, 0)
+        didOverflow = false
+    }
+
+    /**
+     * This does an entire Text layout to produce the result, it is slow
+     */
+    fun slowCreateTextLayoutResultOrNull(): TextLayoutResult? {
+        // make sure we're in a valid place
+        val localLayoutDirection = intrinsicsLayoutDirection ?: return null
+        val localDensity = density ?: return null
+        val annotatedString = AnnotatedString(text)
+        paragraph ?: return null
+        paragraphIntrinsics ?: return null
+
+        // and redo layout with MultiParagraph
+        return TextLayoutResult(
+            TextLayoutInput(
+                annotatedString,
+                style,
+                emptyList(),
+                maxLines,
+                softWrap,
+                overflow,
+                localDensity,
+                localLayoutDirection,
+                fontFamilyResolver,
+                prevConstraints
+            ),
+            MultiParagraph(
+                MultiParagraphIntrinsics(
+                    annotatedString = annotatedString,
+                    style = style,
+                    placeholders = emptyList(),
+                    density = localDensity,
+                    fontFamilyResolver = fontFamilyResolver
+                ),
+                prevConstraints,
+                maxLines,
+                overflow == TextOverflow.Ellipsis
+            ),
+            layoutSize
+        )
+    }
+}
\ No newline at end of file
diff --git a/compose/foundation/foundation-newtext/src/commonMain/kotlin/androidx/compose/foundation/newtext/text/modifiers/TextStringSimpleElement.kt b/compose/foundation/foundation-newtext/src/commonMain/kotlin/androidx/compose/foundation/newtext/text/modifiers/TextStringSimpleElement.kt
new file mode 100644
index 0000000..2e2ed43
--- /dev/null
+++ b/compose/foundation/foundation-newtext/src/commonMain/kotlin/androidx/compose/foundation/newtext/text/modifiers/TextStringSimpleElement.kt
@@ -0,0 +1,94 @@
+/*
+ * Copyright 2023 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.newtext.text.modifiers
+
+import androidx.compose.foundation.newtext.text.DefaultMinLines
+import androidx.compose.ui.ExperimentalComposeUiApi
+import androidx.compose.ui.node.ModifierNodeElement
+import androidx.compose.ui.platform.debugInspectorInfo
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.font.FontFamily
+import androidx.compose.ui.text.style.TextOverflow
+
+@ExperimentalComposeUiApi
+internal class TextStringSimpleElement(
+    private val text: String,
+    private val style: TextStyle,
+    private val fontFamilyResolver: FontFamily.Resolver,
+    private val overflow: TextOverflow = TextOverflow.Clip,
+    private val softWrap: Boolean = true,
+    private val maxLines: Int = Int.MAX_VALUE,
+    private val minLines: Int = DefaultMinLines,
+) : ModifierNodeElement<TextStringSimpleNode>(inspectorInfo = debugInspectorInfo { }) {
+    override fun create(): TextStringSimpleNode = TextStringSimpleNode(
+        text,
+        style,
+        fontFamilyResolver,
+        overflow,
+        softWrap,
+        maxLines,
+        minLines
+    )
+
+    override fun update(node: TextStringSimpleNode): TextStringSimpleNode {
+        node.doInvalidations(
+            textChanged = node.updateText(
+                text = text
+            ),
+            layoutChanged = node.updateLayoutRelatedArgs(
+                style = style,
+                minLines = minLines,
+                maxLines = maxLines,
+                softWrap = softWrap,
+                fontFamilyResolver = fontFamilyResolver,
+                overflow = overflow
+            )
+        )
+        return node
+    }
+
+    override fun equals(other: Any?): Boolean {
+        if (this === other) return true
+
+        if (other !is TextStringSimpleElement) return false
+
+        // these three are most likely to actually change
+        if (text != other.text) return false
+        if (style != other.style) return false
+
+        // these are equally unlikely to change
+        if (fontFamilyResolver != other.fontFamilyResolver) return false
+        if (overflow != other.overflow) return false
+        if (softWrap != other.softWrap) return false
+        if (maxLines != other.maxLines) return false
+        if (minLines != other.minLines) return false
+
+        return true
+    }
+
+    override fun hashCode(): Int {
+        var result = super.hashCode()
+        result = 31 * result + text.hashCode()
+        result = 31 * result + style.hashCode()
+        result = 31 * result + fontFamilyResolver.hashCode()
+        result = 31 * result + overflow.hashCode()
+        result = 31 * result + softWrap.hashCode()
+        result = 31 * result + maxLines
+        result = 31 * result + minLines
+        return result
+    }
+}
\ No newline at end of file
diff --git a/compose/foundation/foundation-newtext/src/commonMain/kotlin/androidx/compose/foundation/newtext/text/modifiers/TextStringSimpleNode.kt b/compose/foundation/foundation-newtext/src/commonMain/kotlin/androidx/compose/foundation/newtext/text/modifiers/TextStringSimpleNode.kt
new file mode 100644
index 0000000..a90ff59
--- /dev/null
+++ b/compose/foundation/foundation-newtext/src/commonMain/kotlin/androidx/compose/foundation/newtext/text/modifiers/TextStringSimpleNode.kt
@@ -0,0 +1,301 @@
+/*
+ * Copyright 2023 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.newtext.text.modifiers
+
+import androidx.compose.foundation.newtext.text.DefaultMinLines
+import androidx.compose.ui.ExperimentalComposeUiApi
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.geometry.Rect
+import androidx.compose.ui.geometry.Size
+import androidx.compose.ui.graphics.Shadow
+import androidx.compose.ui.graphics.drawscope.ContentDrawScope
+import androidx.compose.ui.graphics.drawscope.Fill
+import androidx.compose.ui.graphics.drawscope.drawIntoCanvas
+import androidx.compose.ui.layout.AlignmentLine
+import androidx.compose.ui.layout.FirstBaseline
+import androidx.compose.ui.layout.IntrinsicMeasurable
+import androidx.compose.ui.layout.IntrinsicMeasureScope
+import androidx.compose.ui.layout.LastBaseline
+import androidx.compose.ui.layout.Measurable
+import androidx.compose.ui.layout.MeasureResult
+import androidx.compose.ui.layout.MeasureScope
+import androidx.compose.ui.node.DrawModifierNode
+import androidx.compose.ui.node.LayoutModifierNode
+import androidx.compose.ui.node.SemanticsModifierNode
+import androidx.compose.ui.node.invalidateDraw
+import androidx.compose.ui.node.invalidateLayer
+import androidx.compose.ui.node.invalidateMeasurements
+import androidx.compose.ui.node.invalidateSemantics
+import androidx.compose.ui.semantics.SemanticsConfiguration
+import androidx.compose.ui.semantics.getTextLayoutResult
+import androidx.compose.ui.semantics.text
+import androidx.compose.ui.text.AnnotatedString
+import androidx.compose.ui.text.ExperimentalTextApi
+import androidx.compose.ui.text.TextLayoutResult
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.font.FontFamily
+import androidx.compose.ui.text.style.TextDecoration
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.unit.Constraints
+import androidx.compose.ui.unit.Density
+import kotlin.math.roundToInt
+
+@OptIn(ExperimentalComposeUiApi::class)
+internal class TextStringSimpleNode(
+    private var text: String,
+    private var style: TextStyle,
+    private var fontFamilyResolver: FontFamily.Resolver,
+    private var overflow: TextOverflow = TextOverflow.Clip,
+    private var softWrap: Boolean = true,
+    private var maxLines: Int = Int.MAX_VALUE,
+    private var minLines: Int = DefaultMinLines
+) : Modifier.Node(), LayoutModifierNode, DrawModifierNode, SemanticsModifierNode {
+    private var baselineCache: Map<AlignmentLine, Int>? = null
+
+    private var _layoutCache: ParagraphLayoutCache? = null
+    private val layoutCache: ParagraphLayoutCache
+        get() {
+            if (_layoutCache == null) {
+                _layoutCache = ParagraphLayoutCache(
+                    text,
+                    style,
+                    fontFamilyResolver,
+                    overflow,
+                    softWrap,
+                    maxLines,
+                    minLines,
+                )
+            }
+            return _layoutCache!!
+        }
+
+    private fun getLayoutCache(density: Density): ParagraphLayoutCache {
+        return layoutCache.also { it.density = density }
+    }
+
+    fun updateText(text: String): Boolean {
+        if (this.text == text) return false
+        this.text = text
+        return true
+    }
+
+    fun updateLayoutRelatedArgs(
+        style: TextStyle,
+        minLines: Int,
+        maxLines: Int,
+        softWrap: Boolean,
+        fontFamilyResolver: FontFamily.Resolver,
+        overflow: TextOverflow
+    ): Boolean {
+        var changed = false
+        if (this.style != style) {
+            this.style = style
+            changed = true
+        }
+
+        if (this.minLines != minLines) {
+            this.minLines = minLines
+            changed = true
+        }
+
+        if (this.maxLines != maxLines) {
+            this.maxLines != maxLines
+            changed = true
+        }
+
+        if (this.softWrap != softWrap) {
+            this.softWrap = softWrap
+            changed = true
+        }
+
+        if (this.fontFamilyResolver != fontFamilyResolver) {
+            this.fontFamilyResolver = fontFamilyResolver
+            changed = true
+        }
+
+        if (this.overflow != overflow) {
+            this.overflow = overflow
+            changed = true
+        }
+
+        return changed
+    }
+
+    fun doInvalidations(
+        textChanged: Boolean,
+        layoutChanged: Boolean
+    ) {
+        if (textChanged) {
+            _semanticsConfiguration = null
+            invalidateSemantics()
+        }
+
+        if (textChanged || layoutChanged) {
+            layoutCache.update(
+                text = text,
+                style = style,
+                fontFamilyResolver = fontFamilyResolver,
+                overflow = overflow,
+                softWrap = softWrap,
+                maxLines = maxLines,
+                minLines = minLines
+            )
+            invalidateMeasurements()
+            invalidateDraw()
+        }
+    }
+
+    private var _semanticsConfiguration: SemanticsConfiguration? = null
+
+    private var semanticsTextLayoutResult: ((MutableList<TextLayoutResult>) -> Boolean)? = null
+
+    private fun generateSemantics(text: String): SemanticsConfiguration {
+        var localSemanticsTextLayoutResult = semanticsTextLayoutResult
+        if (localSemanticsTextLayoutResult == null) {
+            localSemanticsTextLayoutResult = { textLayoutResult ->
+                val layout = layoutCache.slowCreateTextLayoutResultOrNull()?.also {
+                    textLayoutResult.add(it)
+                }
+                layout != null
+                false
+            }
+            semanticsTextLayoutResult = localSemanticsTextLayoutResult
+        }
+        return SemanticsConfiguration().also {
+            it.isMergingSemanticsOfDescendants = false
+            it.isClearingSemantics = false
+            it.text = AnnotatedString(text)
+            it.getTextLayoutResult(action = localSemanticsTextLayoutResult)
+        }
+    }
+
+    override val semanticsConfiguration: SemanticsConfiguration
+        get() {
+            var localSemantics = _semanticsConfiguration
+            if (localSemantics == null) {
+                localSemantics = generateSemantics(text)
+                _semanticsConfiguration = localSemantics
+            }
+            return localSemantics
+        }
+
+    override fun MeasureScope.measure(
+        measurable: Measurable,
+        constraints: Constraints
+    ): MeasureResult {
+        val layoutCache = getLayoutCache(this)
+
+        val didChangeLayout = layoutCache.layoutWithConstraints(constraints, layoutDirection)
+        // ensure measure restarts when hasStaleResolvedFonts by reading in measure
+        layoutCache.observeFontChanges
+        val paragraph = layoutCache.paragraph!!
+        val layoutSize = layoutCache.layoutSize
+
+        if (didChangeLayout) {
+            invalidateLayer()
+            baselineCache = mapOf(
+                FirstBaseline to paragraph.firstBaseline.roundToInt(),
+                LastBaseline to paragraph.lastBaseline.roundToInt()
+            )
+        }
+
+        // then allow children to measure _inside_ our final box, with the above placeholders
+        val placeable = measurable.measure(
+            Constraints.fixed(
+                layoutSize.width,
+                layoutSize.height
+            )
+        )
+
+        return layout(
+            layoutSize.width,
+            layoutSize.height,
+            baselineCache!!
+        ) {
+            // this is basically a graphicsLayer
+            placeable.place(0, 0)
+        }
+    }
+
+    override fun IntrinsicMeasureScope.minIntrinsicWidth(
+        measurable: IntrinsicMeasurable,
+        height: Int
+    ): Int {
+        return getLayoutCache(this).minIntrinsicWidth
+    }
+
+    override fun IntrinsicMeasureScope.minIntrinsicHeight(
+        measurable: IntrinsicMeasurable,
+        width: Int
+    ): Int = getLayoutCache(this).intrinsicHeightAt(width, layoutDirection)
+
+    override fun IntrinsicMeasureScope.maxIntrinsicWidth(
+        measurable: IntrinsicMeasurable,
+        height: Int
+    ): Int = getLayoutCache(this).maxIntrinsicWidth
+
+    override fun IntrinsicMeasureScope.maxIntrinsicHeight(
+        measurable: IntrinsicMeasurable,
+        width: Int
+    ): Int = getLayoutCache(this).intrinsicHeightAt(width, layoutDirection)
+
+    @OptIn(ExperimentalTextApi::class)
+    override fun ContentDrawScope.draw() {
+        val localParagraph = requireNotNull(layoutCache.paragraph)
+        drawIntoCanvas { canvas ->
+            val willClip = layoutCache.didOverflow
+            if (willClip) {
+                val width = layoutCache.layoutSize.width.toFloat()
+                val height = layoutCache.layoutSize.height.toFloat()
+                val bounds = Rect(Offset.Zero, Size(width, height))
+                canvas.save()
+                canvas.clipRect(bounds)
+            }
+            try {
+                val textDecoration = style.textDecoration ?: TextDecoration.None
+                val shadow = style.shadow ?: Shadow.None
+                val drawStyle = style.drawStyle ?: Fill
+                val brush = style.brush
+                if (brush != null) {
+                    val alpha = style.alpha
+                    localParagraph.paint(
+                        canvas = canvas,
+                        brush = brush,
+                        alpha = alpha,
+                        shadow = shadow,
+                        drawStyle = drawStyle,
+                        textDecoration = textDecoration
+                    )
+                } else {
+                    val color = style.color
+                    localParagraph.paint(
+                        canvas = canvas,
+                        color = color,
+                        shadow = shadow,
+                        textDecoration = textDecoration,
+                        drawStyle = drawStyle
+                    )
+                }
+            } finally {
+                if (willClip) {
+                    canvas.restore()
+                }
+            }
+        }
+    }
+}
diff --git a/compose/foundation/foundation/api/current.txt b/compose/foundation/foundation/api/current.txt
index 1ccb795..d087b0b 100644
--- a/compose/foundation/foundation/api/current.txt
+++ b/compose/foundation/foundation/api/current.txt
@@ -505,9 +505,6 @@
   public final class LazyListMeasureKt {
   }
 
-  public final class LazyListPinnableContainerProviderKt {
-  }
-
   @androidx.compose.foundation.lazy.LazyScopeMarker @kotlin.jvm.JvmDefaultWithCompatibility public interface LazyListScope {
     method public default void item(optional Object? key, optional Object? contentType, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.lazy.LazyItemScope,kotlin.Unit> content);
     method @Deprecated public void item(optional Object? key, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.lazy.LazyItemScope,? extends kotlin.Unit> content);
@@ -718,6 +715,9 @@
   public final class LazyNearestItemsRangeKt {
   }
 
+  public final class LazyPinnableContainerProviderKt {
+  }
+
   public final class LazySaveableStateHolderKt {
   }
 
diff --git a/compose/foundation/foundation/api/public_plus_experimental_current.txt b/compose/foundation/foundation/api/public_plus_experimental_current.txt
index 272d94d..8b4e97f 100644
--- a/compose/foundation/foundation/api/public_plus_experimental_current.txt
+++ b/compose/foundation/foundation/api/public_plus_experimental_current.txt
@@ -599,9 +599,6 @@
   public final class LazyListMeasureKt {
   }
 
-  public final class LazyListPinnableContainerProviderKt {
-  }
-
   @androidx.compose.foundation.lazy.LazyScopeMarker @kotlin.jvm.JvmDefaultWithCompatibility public interface LazyListScope {
     method public default void item(optional Object? key, optional Object? contentType, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.lazy.LazyItemScope,kotlin.Unit> content);
     method @Deprecated public void item(optional Object? key, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.lazy.LazyItemScope,? extends kotlin.Unit> content);
@@ -874,6 +871,9 @@
     method @androidx.compose.foundation.ExperimentalFoundationApi @androidx.compose.runtime.Composable public static androidx.compose.runtime.State<kotlin.ranges.IntRange> rememberLazyNearestItemsRangeState(kotlin.jvm.functions.Function0<java.lang.Integer> firstVisibleItemIndex, kotlin.jvm.functions.Function0<java.lang.Integer> slidingWindowSize, kotlin.jvm.functions.Function0<java.lang.Integer> extraItemCount);
   }
 
+  public final class LazyPinnableContainerProviderKt {
+  }
+
   public final class LazySaveableStateHolderKt {
   }
 
diff --git a/compose/foundation/foundation/api/restricted_current.txt b/compose/foundation/foundation/api/restricted_current.txt
index 1ccb795..d087b0b 100644
--- a/compose/foundation/foundation/api/restricted_current.txt
+++ b/compose/foundation/foundation/api/restricted_current.txt
@@ -505,9 +505,6 @@
   public final class LazyListMeasureKt {
   }
 
-  public final class LazyListPinnableContainerProviderKt {
-  }
-
   @androidx.compose.foundation.lazy.LazyScopeMarker @kotlin.jvm.JvmDefaultWithCompatibility public interface LazyListScope {
     method public default void item(optional Object? key, optional Object? contentType, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.lazy.LazyItemScope,kotlin.Unit> content);
     method @Deprecated public void item(optional Object? key, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.lazy.LazyItemScope,? extends kotlin.Unit> content);
@@ -718,6 +715,9 @@
   public final class LazyNearestItemsRangeKt {
   }
 
+  public final class LazyPinnableContainerProviderKt {
+  }
+
   public final class LazySaveableStateHolderKt {
   }
 
diff --git a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/MemoryAllocs.kt b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/MemoryAllocs.kt
index bfbb956..b191349a 100644
--- a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/MemoryAllocs.kt
+++ b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/MemoryAllocs.kt
@@ -16,12 +16,21 @@
 
 package androidx.compose.foundation.demos.text
 
+import androidx.compose.foundation.background
 import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material.Divider
 import androidx.compose.material.Text
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.State
 import androidx.compose.runtime.produceState
 import androidx.compose.runtime.withFrameMillis
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.text.font.FontFamily
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
 
 /**
  * These demos are for using the memory profiler to observe initial compo and recompo memory
@@ -32,8 +41,12 @@
 @Composable
 fun MemoryAllocsSetText() {
     Column {
-        Text("Run in memory profiler to emulate setting text value when observable loads")
-        Text("This is designed to be used in the Android Studio memory profiler")
+        Preamble("""
+            @Composable
+            fun SetText(text: State<String>) {
+                Text(text.value)
+            }""".trimIndent()
+        )
         SetText(textToggler())
     }
 }
@@ -47,13 +60,37 @@
 @Composable
 fun MemoryAllocsIfNotEmptyText() {
     Column {
-        Text("Run in memory profiler to emulate calling Text after an observable loads")
-        Text("This is designed to be used in the Android Studio memory profiler")
+        Preamble("""
+            @Composable
+            fun IfNotEmptyText(text: State<String>) {
+                if (text.value.isNotEmpty()) {
+                    Text(text.value)
+                }
+            }""".trimIndent()
+        )
         IfNotEmptyText(textToggler())
     }
 }
 
 @Composable
+fun Preamble(sourceCode: String) {
+    Text("Run in memory profiler to emulate text behavior during observable loads")
+    Text(text = sourceCode,
+        modifier = Modifier
+            .fillMaxWidth()
+            .background(Color(220, 230, 240)),
+        fontFamily = FontFamily.Monospace,
+        color = Color(41, 17, 27),
+        fontSize = 10.sp
+    )
+    Divider(
+        Modifier
+            .fillMaxWidth()
+            .padding(vertical = 8.dp))
+    Text("\uD83D\uDC47 running here \uD83D\uDC47")
+}
+
+@Composable
 fun IfNotEmptyText(text: State<String>) {
     if (text.value.isNotEmpty()) {
         Text(text.value)
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 62e4dbf..258991f 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
@@ -117,10 +117,10 @@
             )
         ),
         DemoCategory(
-            "⚠️️ Memory benchmark ⚠️️",
+            "\uD83D\uDD75️️️ Memory allocs",
             listOf(
-                ComposableDemo("SetText") { MemoryAllocsSetText() },
-                ComposableDemo("IfNotEmptyText") { MemoryAllocsIfNotEmptyText() }
+                ComposableDemo("\uD83D\uDD75️ SetText") { MemoryAllocsSetText() },
+                ComposableDemo("\uD83D\uDD75️ IfNotEmptyText") { MemoryAllocsIfNotEmptyText() }
             )
         )
     )
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/BasicMarqueeTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/BasicMarqueeTest.kt
index 8f5638a..37cd6eb 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/BasicMarqueeTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/BasicMarqueeTest.kt
@@ -25,7 +25,6 @@
 import androidx.compose.foundation.layout.width
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.CompositionLocalProvider
-import androidx.compose.runtime.LaunchedEffect
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.setValue
@@ -43,6 +42,7 @@
 import androidx.compose.ui.graphics.toPixelMap
 import androidx.compose.ui.platform.LocalFocusManager
 import androidx.compose.ui.platform.LocalLayoutDirection
+import androidx.compose.ui.test.ExperimentalTestApi
 import androidx.compose.ui.test.captureToImage
 import androidx.compose.ui.test.junit4.createComposeRule
 import androidx.compose.ui.test.onRoot
@@ -55,7 +55,6 @@
 import com.google.common.truth.Truth.assertThat
 import kotlin.math.roundToInt
 import org.junit.Before
-import org.junit.Ignore
 import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -73,8 +72,13 @@
         private val BackgroundColor = Color.White
     }
 
+    private val motionDurationScale = object : MotionDurationScale {
+        override var scaleFactor: Float by mutableStateOf(1f)
+    }
+
+    @OptIn(ExperimentalTestApi::class)
     @get:Rule
-    val rule = createComposeRule()
+    val rule = createComposeRule(effectContext = motionDurationScale)
 
     /**
      * Converts pxPerFrame to dps per second. The frame delay is 16ms, which means there are
@@ -119,14 +123,12 @@
         }
     }
 
-    @Ignore("b/265177763: Not currently possible to inject a MotionDurationScale in tests.")
+    @Suppress("UnnecessaryOptInAnnotation")
+    @OptIn(ExperimentalTestApi::class)
     @Test fun animates_whenAnimationsDisabledBySystem() {
-        // TODO(b/265177763) Inject 0 duration scale.
+        motionDurationScale.scaleFactor = 0f
 
         rule.setContent {
-            LaunchedEffect(Unit) {
-                assertThat(coroutineContext[MotionDurationScale]?.scaleFactor).isEqualTo(0f)
-            }
             TestMarqueeContent(
                 Modifier.basicMarqueeWithTestParams(
                     iterations = 1,
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/grid/LazyGridPinnableContainerTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/grid/LazyGridPinnableContainerTest.kt
new file mode 100644
index 0000000..91e6a5b
--- /dev/null
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/grid/LazyGridPinnableContainerTest.kt
@@ -0,0 +1,666 @@
+/*
+ * 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.grid
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.lazy.list.assertIsNotPlaced
+import androidx.compose.foundation.lazy.list.assertIsPlaced
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.layout.LocalPinnableContainer
+import androidx.compose.ui.layout.PinnableContainer
+import androidx.compose.ui.layout.PinnableContainer.PinnedHandle
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.assertIsNotDisplayed
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.unit.Dp
+import androidx.test.filters.MediumTest
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.runBlocking
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+
+@MediumTest
+class LazyGridPinnableContainerTest {
+
+    @get:Rule
+    val rule = createComposeRule()
+
+    private var pinnableContainer: PinnableContainer? = null
+
+    private val itemSizePx = 10
+    private var itemSize = Dp.Unspecified
+
+    private val composed = mutableSetOf<Int>()
+
+    @Before
+    fun setup() {
+        itemSize = with(rule.density) { itemSizePx.toDp() }
+    }
+
+    @Composable
+    fun Item(index: Int) {
+        Box(
+            Modifier
+                .size(itemSize)
+                .testTag("$index")
+        )
+        DisposableEffect(index) {
+            composed.add(index)
+            onDispose {
+                composed.remove(index)
+            }
+        }
+    }
+
+    @Test
+    fun pinnedItemIsComposedAndPlacedWhenScrolledOut() {
+        val state = LazyGridState()
+        // Arrange.
+        rule.setContent {
+            LazyVerticalGrid(
+                columns = GridCells.Fixed(1),
+                modifier = Modifier.size(itemSize * 2),
+                state = state
+            ) {
+                items(100) { index ->
+                    if (index == 1) {
+                        pinnableContainer = LocalPinnableContainer.current
+                    }
+                    Item(index)
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            requireNotNull(pinnableContainer).pin()
+        }
+
+        rule.runOnIdle {
+            assertThat(composed).contains(1)
+            runBlocking {
+                state.scrollToItem(3)
+            }
+        }
+
+        rule.waitUntil {
+            // not visible items were disposed
+            !composed.contains(0)
+        }
+
+        rule.runOnIdle {
+            // item 1 is still pinned
+            assertThat(composed).contains(1)
+        }
+
+        rule.onNodeWithTag("1")
+            .assertExists()
+            .assertIsNotDisplayed()
+            .assertIsPlaced()
+    }
+
+    @Test
+    fun itemsBetweenPinnedAndCurrentVisibleAreNotComposed() {
+        val state = LazyGridState()
+        // Arrange.
+        rule.setContent {
+            LazyVerticalGrid(
+                columns = GridCells.Fixed(1),
+                modifier = Modifier.size(itemSize * 2),
+                state = state
+            ) {
+                items(100) { index ->
+                    if (index == 1) {
+                        pinnableContainer = LocalPinnableContainer.current
+                    }
+                    Item(index)
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            requireNotNull(pinnableContainer).pin()
+        }
+
+        rule.runOnIdle {
+            runBlocking {
+                state.scrollToItem(4)
+            }
+        }
+
+        rule.waitUntil {
+            // not visible items were disposed
+            !composed.contains(0)
+        }
+
+        rule.runOnIdle {
+            assertThat(composed).doesNotContain(0)
+            assertThat(composed).contains(1)
+            assertThat(composed).doesNotContain(2)
+            assertThat(composed).doesNotContain(3)
+            assertThat(composed).contains(4)
+        }
+    }
+
+    @Test
+    fun pinnedItemAfterVisibleOnesIsComposedAndPlacedWhenScrolledOut() {
+        val state = LazyGridState()
+        // Arrange.
+        rule.setContent {
+            LazyVerticalGrid(
+                columns = GridCells.Fixed(1),
+                modifier = Modifier.size(itemSize * 2),
+                state = state
+            ) {
+                items(100) { index ->
+                    if (index == 4) {
+                        pinnableContainer = LocalPinnableContainer.current
+                    }
+                    Item(index)
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            runBlocking {
+                state.scrollToItem(4)
+            }
+        }
+
+        rule.waitUntil {
+            // wait for not visible items to be disposed
+            !composed.contains(1)
+        }
+
+        rule.runOnIdle {
+            requireNotNull(pinnableContainer).pin()
+            assertThat(composed).contains(5)
+        }
+
+        rule.runOnIdle {
+            runBlocking {
+                state.scrollToItem(0)
+            }
+        }
+
+        rule.waitUntil {
+            // wait for not visible items to be disposed
+            !composed.contains(5)
+        }
+
+        rule.runOnIdle {
+            assertThat(composed).contains(0)
+            assertThat(composed).contains(1)
+            assertThat(composed).doesNotContain(2)
+            assertThat(composed).doesNotContain(3)
+            assertThat(composed).contains(4)
+            assertThat(composed).doesNotContain(5)
+        }
+    }
+
+    @Test
+    fun pinnedItemCanBeUnpinned() {
+        val state = LazyGridState()
+        // Arrange.
+        rule.setContent {
+            LazyVerticalGrid(
+                columns = GridCells.Fixed(1),
+                modifier = Modifier.size(itemSize * 2),
+                state = state
+            ) {
+                items(100) { index ->
+                    if (index == 1) {
+                        pinnableContainer = LocalPinnableContainer.current
+                    }
+                    Item(index)
+                }
+            }
+        }
+
+        val handle = rule.runOnIdle {
+            requireNotNull(pinnableContainer).pin()
+        }
+
+        rule.runOnIdle {
+            runBlocking {
+                state.scrollToItem(3)
+            }
+        }
+
+        rule.waitUntil {
+            // wait for not visible items to be disposed
+            !composed.contains(0)
+        }
+
+        rule.runOnIdle {
+            handle.unpin()
+        }
+
+        rule.waitUntil {
+            // wait for unpinned item to be disposed
+            !composed.contains(1)
+        }
+
+        rule.onNodeWithTag("1")
+            .assertIsNotPlaced()
+    }
+
+    @Test
+    fun pinnedItemIsStillPinnedWhenReorderedAndNotVisibleAnymore() {
+        val state = LazyGridState()
+        var list by mutableStateOf(listOf(0, 1, 2, 3, 4))
+        // Arrange.
+        rule.setContent {
+            LazyVerticalGrid(
+                columns = GridCells.Fixed(1),
+                modifier = Modifier.size(itemSize * 3),
+                state = state
+            ) {
+                items(list, key = { it }) { index ->
+                    if (index == 2) {
+                        pinnableContainer = LocalPinnableContainer.current
+                    }
+                    Item(index)
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            assertThat(composed).containsExactly(0, 1, 2)
+            requireNotNull(pinnableContainer).pin()
+        }
+
+        rule.runOnIdle {
+            list = listOf(0, 3, 4, 1, 2)
+        }
+
+        rule.waitUntil {
+            // wait for not visible item to be disposed
+            !composed.contains(1)
+        }
+
+        rule.runOnIdle {
+            assertThat(composed).containsExactly(0, 3, 4, 2) // 2 is pinned
+        }
+
+        rule.onNodeWithTag("2")
+            .assertIsPlaced()
+    }
+
+    @Test
+    fun unpinnedWhenLazyGridStateChanges() {
+        var state by mutableStateOf(LazyGridState(firstVisibleItemIndex = 2))
+        // Arrange.
+        rule.setContent {
+            LazyVerticalGrid(
+                columns = GridCells.Fixed(1),
+                modifier = Modifier.size(itemSize * 2),
+                state = state
+            ) {
+                items(100) { index ->
+                    if (index == 2) {
+                        pinnableContainer = LocalPinnableContainer.current
+                    }
+                    Item(index)
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            requireNotNull(pinnableContainer).pin()
+        }
+
+        rule.runOnIdle {
+            assertThat(composed).contains(3)
+            runBlocking {
+                state.scrollToItem(0)
+            }
+        }
+
+        rule.waitUntil {
+            // wait for not visible item to be disposed
+            !composed.contains(3)
+        }
+
+        rule.runOnIdle {
+            assertThat(composed).contains(2)
+            state = LazyGridState()
+        }
+
+        rule.waitUntil {
+            // wait for pinned item to be disposed
+            !composed.contains(2)
+        }
+
+        rule.onNodeWithTag("2")
+            .assertIsNotPlaced()
+    }
+
+    @Test
+    fun pinAfterLazyGridStateChange() {
+        var state by mutableStateOf(LazyGridState())
+        // Arrange.
+        rule.setContent {
+            LazyVerticalGrid(
+                columns = GridCells.Fixed(1),
+                modifier = Modifier.size(itemSize * 2),
+                state = state
+            ) {
+                items(100) { index ->
+                    if (index == 0) {
+                        pinnableContainer = LocalPinnableContainer.current
+                    }
+                    Item(index)
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            state = LazyGridState()
+        }
+
+        rule.runOnIdle {
+            requireNotNull(pinnableContainer).pin()
+        }
+
+        rule.runOnIdle {
+            assertThat(composed).contains(1)
+            runBlocking {
+                state.scrollToItem(2)
+            }
+        }
+
+        rule.waitUntil {
+            // wait for not visible item to be disposed
+            !composed.contains(1)
+        }
+
+        rule.runOnIdle {
+            assertThat(composed).contains(0)
+        }
+    }
+
+    @Test
+    fun itemsArePinnedBasedOnGlobalIndexes() {
+        val state = LazyGridState(firstVisibleItemIndex = 3)
+        // Arrange.
+        rule.setContent {
+            LazyVerticalGrid(
+                columns = GridCells.Fixed(1),
+                modifier = Modifier.size(itemSize * 2),
+                state = state
+            ) {
+                repeat(100) { index ->
+                    item {
+                        if (index == 3) {
+                            pinnableContainer = LocalPinnableContainer.current
+                        }
+                        Item(index)
+                    }
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            requireNotNull(pinnableContainer).pin()
+        }
+
+        rule.runOnIdle {
+            assertThat(composed).contains(4)
+            runBlocking {
+                state.scrollToItem(6)
+            }
+        }
+
+        rule.waitUntil {
+            // wait for not visible item to be disposed
+            !composed.contains(4)
+        }
+
+        rule.runOnIdle {
+            assertThat(composed).contains(3)
+        }
+
+        rule.onNodeWithTag("3")
+            .assertExists()
+            .assertIsNotDisplayed()
+            .assertIsPlaced()
+    }
+
+    @Test
+    fun pinnedItemIsRemovedWhenNotVisible() {
+        val state = LazyGridState(3)
+        var itemCount by mutableStateOf(10)
+        // Arrange.
+        rule.setContent {
+            LazyVerticalGrid(
+                columns = GridCells.Fixed(1),
+                modifier = Modifier.size(itemSize * 2),
+                state = state
+            ) {
+                items(itemCount) { index ->
+                    if (index == 3) {
+                        pinnableContainer = LocalPinnableContainer.current
+                    }
+                    Item(index)
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            requireNotNull(pinnableContainer).pin()
+            assertThat(composed).contains(4)
+            runBlocking {
+                state.scrollToItem(0)
+            }
+        }
+
+        rule.waitUntil {
+            // wait for not visible item to be disposed
+            !composed.contains(4)
+        }
+
+        rule.runOnIdle {
+            itemCount = 3
+        }
+
+        rule.waitUntil {
+            // wait for pinned item to be disposed
+            !composed.contains(3)
+        }
+
+        rule.onNodeWithTag("3")
+            .assertIsNotPlaced()
+    }
+
+    @Test
+    fun pinnedItemIsRemovedWhenVisible() {
+        val state = LazyGridState(0)
+        var items by mutableStateOf(listOf(0, 1, 2))
+        // Arrange.
+        rule.setContent {
+            LazyVerticalGrid(
+                columns = GridCells.Fixed(1),
+                modifier = Modifier.size(itemSize * 2),
+                state = state
+            ) {
+                items(items) { index ->
+                    if (index == 1) {
+                        pinnableContainer = LocalPinnableContainer.current
+                    }
+                    Item(index)
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            requireNotNull(pinnableContainer).pin()
+        }
+
+        rule.runOnIdle {
+            items = listOf(0, 2)
+        }
+
+        rule.waitUntil {
+            // wait for pinned item to be disposed
+            !composed.contains(1)
+        }
+
+        rule.onNodeWithTag("1")
+            .assertIsNotPlaced()
+    }
+
+    @Test
+    fun pinnedMultipleTimes() {
+        val state = LazyGridState(0)
+        // Arrange.
+        rule.setContent {
+            LazyVerticalGrid(
+                columns = GridCells.Fixed(1),
+                modifier = Modifier.size(itemSize * 2),
+                state = state
+            ) {
+                items(100) { index ->
+                    if (index == 1) {
+                        pinnableContainer = LocalPinnableContainer.current
+                    }
+                    Item(index)
+                }
+            }
+        }
+
+        val handles = mutableListOf<PinnedHandle>()
+        rule.runOnIdle {
+            handles.add(requireNotNull(pinnableContainer).pin())
+            handles.add(requireNotNull(pinnableContainer).pin())
+        }
+
+        rule.runOnIdle {
+            // pinned 3 times in total
+            handles.add(requireNotNull(pinnableContainer).pin())
+            assertThat(composed).contains(0)
+            runBlocking {
+                state.scrollToItem(3)
+            }
+        }
+
+        rule.waitUntil {
+            // wait for not visible item to be disposed
+            !composed.contains(0)
+        }
+
+        while (handles.isNotEmpty()) {
+            rule.runOnIdle {
+                assertThat(composed).contains(1)
+                handles.removeFirst().unpin()
+            }
+        }
+
+        rule.waitUntil {
+            // wait for pinned item to be disposed
+            !composed.contains(1)
+        }
+    }
+
+    @Test
+    fun pinningIsPropagatedToParentContainer() {
+        var parentPinned = false
+        val parentContainer = object : PinnableContainer {
+            override fun pin(): PinnedHandle {
+                parentPinned = true
+                return PinnedHandle { parentPinned = false }
+            }
+        }
+        // Arrange.
+        rule.setContent {
+            CompositionLocalProvider(LocalPinnableContainer provides parentContainer) {
+                LazyVerticalGrid(GridCells.Fixed(1)) {
+                    item {
+                        pinnableContainer = LocalPinnableContainer.current
+                        Box(Modifier.size(itemSize))
+                    }
+                }
+            }
+        }
+
+        val handle = rule.runOnIdle {
+            requireNotNull(pinnableContainer).pin()
+        }
+
+        rule.runOnIdle {
+            assertThat(parentPinned).isTrue()
+            handle.unpin()
+        }
+
+        rule.runOnIdle {
+            assertThat(parentPinned).isFalse()
+        }
+    }
+
+    @Test
+    fun parentContainerChange_pinningIsMaintained() {
+        var parent1Pinned = false
+        val parent1Container = object : PinnableContainer {
+            override fun pin(): PinnedHandle {
+                parent1Pinned = true
+                return PinnedHandle { parent1Pinned = false }
+            }
+        }
+        var parent2Pinned = false
+        val parent2Container = object : PinnableContainer {
+            override fun pin(): PinnedHandle {
+                parent2Pinned = true
+                return PinnedHandle { parent2Pinned = false }
+            }
+        }
+        var parentContainer by mutableStateOf<PinnableContainer>(parent1Container)
+        // Arrange.
+        rule.setContent {
+            CompositionLocalProvider(LocalPinnableContainer provides parentContainer) {
+                LazyVerticalGrid(GridCells.Fixed(1)) {
+                    item {
+                        pinnableContainer = LocalPinnableContainer.current
+                        Box(Modifier.size(itemSize))
+                    }
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            requireNotNull(pinnableContainer).pin()
+        }
+
+        rule.runOnIdle {
+            assertThat(parent1Pinned).isTrue()
+            assertThat(parent2Pinned).isFalse()
+            parentContainer = parent2Container
+        }
+
+        rule.runOnIdle {
+            assertThat(parent1Pinned).isFalse()
+            assertThat(parent2Pinned).isTrue()
+        }
+    }
+}
\ No newline at end of file
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridPinnableContainerTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridPinnableContainerTest.kt
new file mode 100644
index 0000000..7d688a4
--- /dev/null
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridPinnableContainerTest.kt
@@ -0,0 +1,668 @@
+/*
+ * 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.staggeredgrid
+
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.lazy.list.assertIsNotPlaced
+import androidx.compose.foundation.lazy.list.assertIsPlaced
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.layout.LocalPinnableContainer
+import androidx.compose.ui.layout.PinnableContainer
+import androidx.compose.ui.layout.PinnableContainer.PinnedHandle
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.assertIsNotDisplayed
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.unit.Dp
+import androidx.test.filters.MediumTest
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.runBlocking
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+
+@OptIn(ExperimentalFoundationApi::class)
+@MediumTest
+class LazyStaggeredGridPinnableContainerTest {
+
+    @get:Rule
+    val rule = createComposeRule()
+
+    private var pinnableContainer: PinnableContainer? = null
+
+    private val itemSizePx = 10
+    private var itemSize = Dp.Unspecified
+
+    private val composed = mutableSetOf<Int>()
+
+    @Before
+    fun setup() {
+        itemSize = with(rule.density) { itemSizePx.toDp() }
+    }
+
+    @Composable
+    fun Item(index: Int) {
+        Box(
+            Modifier
+                .size(itemSize)
+                .testTag("$index")
+        )
+        DisposableEffect(index) {
+            composed.add(index)
+            onDispose {
+                composed.remove(index)
+            }
+        }
+    }
+
+    @Test
+    fun pinnedItemIsComposedAndPlacedWhenScrolledOut() {
+        val state = LazyStaggeredGridState()
+        // Arrange.
+        rule.setContent {
+            LazyVerticalStaggeredGrid(
+                columns = StaggeredGridCells.Fixed(1),
+                modifier = Modifier.size(itemSize * 2),
+                state = state
+            ) {
+                items(100) { index ->
+                    if (index == 1) {
+                        pinnableContainer = LocalPinnableContainer.current
+                    }
+                    Item(index)
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            requireNotNull(pinnableContainer).pin()
+        }
+
+        rule.runOnIdle {
+            assertThat(composed).contains(1)
+            runBlocking {
+                state.scrollToItem(3)
+            }
+        }
+
+        rule.waitUntil {
+            // not visible items were disposed
+            !composed.contains(0)
+        }
+
+        rule.runOnIdle {
+            // item 1 is still pinned
+            assertThat(composed).contains(1)
+        }
+
+        rule.onNodeWithTag("1")
+            .assertExists()
+            .assertIsNotDisplayed()
+            .assertIsPlaced()
+    }
+
+    @Test
+    fun itemsBetweenPinnedAndCurrentVisibleAreNotComposed() {
+        val state = LazyStaggeredGridState()
+        // Arrange.
+        rule.setContent {
+            LazyVerticalStaggeredGrid(
+                columns = StaggeredGridCells.Fixed(1),
+                modifier = Modifier.size(itemSize * 2),
+                state = state
+            ) {
+                items(100) { index ->
+                    if (index == 1) {
+                        pinnableContainer = LocalPinnableContainer.current
+                    }
+                    Item(index)
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            requireNotNull(pinnableContainer).pin()
+        }
+
+        rule.runOnIdle {
+            runBlocking {
+                state.scrollToItem(4)
+            }
+        }
+
+        rule.waitUntil {
+            // not visible items were disposed
+            !composed.contains(0)
+        }
+
+        rule.runOnIdle {
+            assertThat(composed).doesNotContain(0)
+            assertThat(composed).contains(1)
+            assertThat(composed).doesNotContain(2)
+            assertThat(composed).doesNotContain(3)
+            assertThat(composed).contains(4)
+        }
+    }
+
+    @Test
+    fun pinnedItemAfterVisibleOnesIsComposedAndPlacedWhenScrolledOut() {
+        val state = LazyStaggeredGridState()
+        // Arrange.
+        rule.setContent {
+            LazyVerticalStaggeredGrid(
+                columns = StaggeredGridCells.Fixed(1),
+                modifier = Modifier.size(itemSize * 2),
+                state = state
+            ) {
+                items(100) { index ->
+                    if (index == 4) {
+                        pinnableContainer = LocalPinnableContainer.current
+                    }
+                    Item(index)
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            runBlocking {
+                state.scrollToItem(4)
+            }
+        }
+
+        rule.waitUntil {
+            // wait for not visible items to be disposed
+            !composed.contains(1)
+        }
+
+        rule.runOnIdle {
+            requireNotNull(pinnableContainer).pin()
+            assertThat(composed).contains(5)
+        }
+
+        rule.runOnIdle {
+            runBlocking {
+                state.scrollToItem(0)
+            }
+        }
+
+        rule.waitUntil {
+            // wait for not visible items to be disposed
+            !composed.contains(5)
+        }
+
+        rule.runOnIdle {
+            assertThat(composed).contains(0)
+            assertThat(composed).contains(1)
+            assertThat(composed).doesNotContain(2)
+            assertThat(composed).doesNotContain(3)
+            assertThat(composed).contains(4)
+            assertThat(composed).doesNotContain(5)
+        }
+    }
+
+    @Test
+    fun pinnedItemCanBeUnpinned() {
+        val state = LazyStaggeredGridState()
+        // Arrange.
+        rule.setContent {
+            LazyVerticalStaggeredGrid(
+                columns = StaggeredGridCells.Fixed(1),
+                modifier = Modifier.size(itemSize * 2),
+                state = state
+            ) {
+                items(100) { index ->
+                    if (index == 1) {
+                        pinnableContainer = LocalPinnableContainer.current
+                    }
+                    Item(index)
+                }
+            }
+        }
+
+        val handle = rule.runOnIdle {
+            requireNotNull(pinnableContainer).pin()
+        }
+
+        rule.runOnIdle {
+            runBlocking {
+                state.scrollToItem(3)
+            }
+        }
+
+        rule.waitUntil {
+            // wait for not visible items to be disposed
+            !composed.contains(0)
+        }
+
+        rule.runOnIdle {
+            handle.unpin()
+        }
+
+        rule.waitUntil {
+            // wait for unpinned item to be disposed
+            !composed.contains(1)
+        }
+
+        rule.onNodeWithTag("1")
+            .assertIsNotPlaced()
+    }
+
+    @Test
+    fun pinnedItemIsStillPinnedWhenReorderedAndNotVisibleAnymore() {
+        val state = LazyStaggeredGridState()
+        var list by mutableStateOf(listOf(0, 1, 2, 3, 4))
+        // Arrange.
+        rule.setContent {
+            LazyVerticalStaggeredGrid(
+                columns = StaggeredGridCells.Fixed(1),
+                modifier = Modifier.size(itemSize * 3),
+                state = state
+            ) {
+                items(list, key = { it }) { index ->
+                    if (index == 2) {
+                        pinnableContainer = LocalPinnableContainer.current
+                    }
+                    Item(index)
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            assertThat(composed).containsExactly(0, 1, 2)
+            requireNotNull(pinnableContainer).pin()
+        }
+
+        rule.runOnIdle {
+            list = listOf(0, 3, 4, 1, 2)
+        }
+
+        rule.waitUntil {
+            // wait for not visible item to be disposed
+            !composed.contains(1)
+        }
+
+        rule.runOnIdle {
+            assertThat(composed).containsExactly(0, 3, 4, 2) // 2 is pinned
+        }
+
+        rule.onNodeWithTag("2")
+            .assertIsPlaced()
+    }
+
+    @Test
+    fun unpinnedWhenLazyStaggeredGridStateChanges() {
+        var state by mutableStateOf(LazyStaggeredGridState(initialFirstVisibleItemIndex = 2))
+        // Arrange.
+        rule.setContent {
+            LazyVerticalStaggeredGrid(
+                columns = StaggeredGridCells.Fixed(1),
+                modifier = Modifier.size(itemSize * 2),
+                state = state
+            ) {
+                items(100) { index ->
+                    if (index == 2) {
+                        pinnableContainer = LocalPinnableContainer.current
+                    }
+                    Item(index)
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            requireNotNull(pinnableContainer).pin()
+        }
+
+        rule.runOnIdle {
+            assertThat(composed).contains(3)
+            runBlocking {
+                state.scrollToItem(0)
+            }
+        }
+
+        rule.waitUntil {
+            // wait for not visible item to be disposed
+            !composed.contains(3)
+        }
+
+        rule.runOnIdle {
+            assertThat(composed).contains(2)
+            state = LazyStaggeredGridState()
+        }
+
+        rule.waitUntil {
+            // wait for pinned item to be disposed
+            !composed.contains(2)
+        }
+
+        rule.onNodeWithTag("2")
+            .assertIsNotPlaced()
+    }
+
+    @Test
+    fun pinAfterLazyStaggeredGridStateChange() {
+        var state by mutableStateOf(LazyStaggeredGridState())
+        // Arrange.
+        rule.setContent {
+            LazyVerticalStaggeredGrid(
+                columns = StaggeredGridCells.Fixed(1),
+                modifier = Modifier.size(itemSize * 2),
+                state = state
+            ) {
+                items(100) { index ->
+                    if (index == 0) {
+                        pinnableContainer = LocalPinnableContainer.current
+                    }
+                    Item(index)
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            state = LazyStaggeredGridState()
+        }
+
+        rule.runOnIdle {
+            requireNotNull(pinnableContainer).pin()
+        }
+
+        rule.runOnIdle {
+            assertThat(composed).contains(1)
+            runBlocking {
+                state.scrollToItem(2)
+            }
+        }
+
+        rule.waitUntil {
+            // wait for not visible item to be disposed
+            !composed.contains(1)
+        }
+
+        rule.runOnIdle {
+            assertThat(composed).contains(0)
+        }
+    }
+
+    @Test
+    fun itemsArePinnedBasedOnGlobalIndexes() {
+        val state = LazyStaggeredGridState(initialFirstVisibleItemIndex = 3)
+        // Arrange.
+        rule.setContent {
+            LazyVerticalStaggeredGrid(
+                columns = StaggeredGridCells.Fixed(1),
+                modifier = Modifier.size(itemSize * 2),
+                state = state
+            ) {
+                repeat(100) { index ->
+                    item {
+                        if (index == 3) {
+                            pinnableContainer = LocalPinnableContainer.current
+                        }
+                        Item(index)
+                    }
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            requireNotNull(pinnableContainer).pin()
+        }
+
+        rule.runOnIdle {
+            assertThat(composed).contains(4)
+            runBlocking {
+                state.scrollToItem(6)
+            }
+        }
+
+        rule.waitUntil {
+            // wait for not visible item to be disposed
+            !composed.contains(4)
+        }
+
+        rule.runOnIdle {
+            assertThat(composed).contains(3)
+        }
+
+        rule.onNodeWithTag("3")
+            .assertExists()
+            .assertIsNotDisplayed()
+            .assertIsPlaced()
+    }
+
+    @Test
+    fun pinnedItemIsRemovedWhenNotVisible() {
+        val state = LazyStaggeredGridState(3)
+        var itemCount by mutableStateOf(10)
+        // Arrange.
+        rule.setContent {
+            LazyVerticalStaggeredGrid(
+                columns = StaggeredGridCells.Fixed(1),
+                modifier = Modifier.size(itemSize * 2),
+                state = state
+            ) {
+                items(itemCount) { index ->
+                    if (index == 3) {
+                        pinnableContainer = LocalPinnableContainer.current
+                    }
+                    Item(index)
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            requireNotNull(pinnableContainer).pin()
+            assertThat(composed).contains(4)
+            runBlocking {
+                state.scrollToItem(0)
+            }
+        }
+
+        rule.waitUntil {
+            // wait for not visible item to be disposed
+            !composed.contains(4)
+        }
+
+        rule.runOnIdle {
+            itemCount = 3
+        }
+
+        rule.waitUntil {
+            // wait for pinned item to be disposed
+            !composed.contains(3)
+        }
+
+        rule.onNodeWithTag("3")
+            .assertIsNotPlaced()
+    }
+
+    @Test
+    fun pinnedItemIsRemovedWhenVisible() {
+        val state = LazyStaggeredGridState(0)
+        var items by mutableStateOf(listOf(0, 1, 2))
+        // Arrange.
+        rule.setContent {
+            LazyVerticalStaggeredGrid(
+                columns = StaggeredGridCells.Fixed(1),
+                modifier = Modifier.size(itemSize * 2),
+                state = state
+            ) {
+                items(items) { index ->
+                    if (index == 1) {
+                        pinnableContainer = LocalPinnableContainer.current
+                    }
+                    Item(index)
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            requireNotNull(pinnableContainer).pin()
+        }
+
+        rule.runOnIdle {
+            items = listOf(0, 2)
+        }
+
+        rule.waitUntil {
+            // wait for pinned item to be disposed
+            !composed.contains(1)
+        }
+
+        rule.onNodeWithTag("1")
+            .assertIsNotPlaced()
+    }
+
+    @Test
+    fun pinnedMultipleTimes() {
+        val state = LazyStaggeredGridState(0)
+        // Arrange.
+        rule.setContent {
+            LazyVerticalStaggeredGrid(
+                columns = StaggeredGridCells.Fixed(1),
+                modifier = Modifier.size(itemSize * 2),
+                state = state
+            ) {
+                items(100) { index ->
+                    if (index == 1) {
+                        pinnableContainer = LocalPinnableContainer.current
+                    }
+                    Item(index)
+                }
+            }
+        }
+
+        val handles = mutableListOf<PinnedHandle>()
+        rule.runOnIdle {
+            handles.add(requireNotNull(pinnableContainer).pin())
+            handles.add(requireNotNull(pinnableContainer).pin())
+        }
+
+        rule.runOnIdle {
+            // pinned 3 times in total
+            handles.add(requireNotNull(pinnableContainer).pin())
+            assertThat(composed).contains(0)
+            runBlocking {
+                state.scrollToItem(3)
+            }
+        }
+
+        rule.waitUntil {
+            // wait for not visible item to be disposed
+            !composed.contains(0)
+        }
+
+        while (handles.isNotEmpty()) {
+            rule.runOnIdle {
+                assertThat(composed).contains(1)
+                handles.removeFirst().unpin()
+            }
+        }
+
+        rule.waitUntil {
+            // wait for pinned item to be disposed
+            !composed.contains(1)
+        }
+    }
+
+    @Test
+    fun pinningIsPropagatedToParentContainer() {
+        var parentPinned = false
+        val parentContainer = object : PinnableContainer {
+            override fun pin(): PinnedHandle {
+                parentPinned = true
+                return PinnedHandle { parentPinned = false }
+            }
+        }
+        // Arrange.
+        rule.setContent {
+            CompositionLocalProvider(LocalPinnableContainer provides parentContainer) {
+                LazyVerticalStaggeredGrid(StaggeredGridCells.Fixed(1)) {
+                    item {
+                        pinnableContainer = LocalPinnableContainer.current
+                        Box(Modifier.size(itemSize))
+                    }
+                }
+            }
+        }
+
+        val handle = rule.runOnIdle {
+            requireNotNull(pinnableContainer).pin()
+        }
+
+        rule.runOnIdle {
+            assertThat(parentPinned).isTrue()
+            handle.unpin()
+        }
+
+        rule.runOnIdle {
+            assertThat(parentPinned).isFalse()
+        }
+    }
+
+    @Test
+    fun parentContainerChange_pinningIsMaintained() {
+        var parent1Pinned = false
+        val parent1Container = object : PinnableContainer {
+            override fun pin(): PinnedHandle {
+                parent1Pinned = true
+                return PinnedHandle { parent1Pinned = false }
+            }
+        }
+        var parent2Pinned = false
+        val parent2Container = object : PinnableContainer {
+            override fun pin(): PinnedHandle {
+                parent2Pinned = true
+                return PinnedHandle { parent2Pinned = false }
+            }
+        }
+        var parentContainer by mutableStateOf<PinnableContainer>(parent1Container)
+        // Arrange.
+        rule.setContent {
+            CompositionLocalProvider(LocalPinnableContainer provides parentContainer) {
+                LazyVerticalStaggeredGrid(StaggeredGridCells.Fixed(1)) {
+                    item {
+                        pinnableContainer = LocalPinnableContainer.current
+                        Box(Modifier.size(itemSize))
+                    }
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            requireNotNull(pinnableContainer).pin()
+        }
+
+        rule.runOnIdle {
+            assertThat(parent1Pinned).isTrue()
+            assertThat(parent2Pinned).isFalse()
+            parentContainer = parent2Container
+        }
+
+        rule.runOnIdle {
+            assertThat(parent1Pinned).isFalse()
+            assertThat(parent2Pinned).isTrue()
+        }
+    }
+}
\ No newline at end of file
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/textfield/TextFieldCursorTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/textfield/TextFieldCursorTest.kt
index 3e2c1f1..c3a20e6e 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/textfield/TextFieldCursorTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/textfield/TextFieldCursorTest.kt
@@ -30,6 +30,7 @@
 import androidx.compose.testutils.assertPixels
 import androidx.compose.testutils.assertShape
 import androidx.compose.ui.Modifier
+import androidx.compose.ui.MotionDurationScale
 import androidx.compose.ui.focus.onFocusChanged
 import androidx.compose.ui.geometry.Rect
 import androidx.compose.ui.graphics.Brush
@@ -38,6 +39,7 @@
 import androidx.compose.ui.graphics.RectangleShape
 import androidx.compose.ui.graphics.SolidColor
 import androidx.compose.ui.graphics.toPixelMap
+import androidx.compose.ui.test.ExperimentalTestApi
 import androidx.compose.ui.test.captureToImage
 import androidx.compose.ui.test.hasSetTextAction
 import androidx.compose.ui.test.junit4.createComposeRule
@@ -58,15 +60,19 @@
 import com.google.common.truth.Truth.assertThat
 import kotlin.math.ceil
 import kotlin.math.floor
-import org.junit.Ignore
 import org.junit.Rule
 import org.junit.Test
 
 @LargeTest
 class TextFieldCursorTest {
 
+    private val motionDurationScale = object : MotionDurationScale {
+        override var scaleFactor: Float by mutableStateOf(1f)
+    }
+
+    @OptIn(ExperimentalTestApi::class)
     @get:Rule
-    val rule = createComposeRule().also {
+    val rule = createComposeRule(effectContext = motionDurationScale).also {
         it.mainClock.autoAdvance = false
     }
 
@@ -87,8 +93,11 @@
     private val bgModifier = Modifier.background(textFieldBgColor)
     private val focusModifier = Modifier.onFocusChanged { if (it.isFocused) isFocused = true }
     private val sizeModifier = Modifier.size(textFieldWidth, textFieldHeight)
+
     // default TextFieldModifier
-    private val textFieldModifier = sizeModifier.then(bgModifier).then(focusModifier)
+    private val textFieldModifier = sizeModifier
+        .then(bgModifier)
+        .then(focusModifier)
 
     // default onTextLayout to capture cursor boundaries.
     private val onTextLayout: (TextLayoutResult) -> Unit = { cursorRect = it.getCursorRect(0) }
@@ -206,11 +215,13 @@
             )
     }
 
-    // TODO(b/265177763) Update this test to set MotionDurationScale to 0 when that's supported.
-    @Ignore("b/265177763")
+    @Suppress("UnnecessaryOptInAnnotation")
+    @OptIn(ExperimentalTestApi::class)
     @Test
     @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
     fun cursorBlinkingAnimation_whenSystemDisablesAnimations() = with(rule.density) {
+        motionDurationScale.scaleFactor = 0f
+
         rule.setContent {
             // The padding helps if the test is run accidentally in landscape. Landscape makes
             // the cursor to be next to the navigation bar which affects the red color to be a bit
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListItemProvider.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListItemProvider.kt
index ae6229e..f2b57ef 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListItemProvider.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListItemProvider.kt
@@ -20,6 +20,7 @@
 import androidx.compose.foundation.lazy.layout.DelegatingLazyLayoutItemProvider
 import androidx.compose.foundation.lazy.layout.IntervalList
 import androidx.compose.foundation.lazy.layout.LazyLayoutItemProvider
+import androidx.compose.foundation.lazy.layout.LazyPinnableContainerProvider
 import androidx.compose.foundation.lazy.layout.rememberLazyNearestItemsRangeState
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.derivedStateOf
@@ -79,7 +80,7 @@
         intervals = intervals,
         nearestItemsRange = nearestItemsRange,
         itemContent = { interval, index ->
-            LazyListPinnableContainerProvider(state, index) {
+            LazyPinnableContainerProvider(state.pinnedItems, index) {
                 interval.value.item.invoke(itemScope, index - interval.startIndex)
             }
         }
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListMeasure.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListMeasure.kt
index 104832d7..5c9ea54 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListMeasure.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListMeasure.kt
@@ -19,6 +19,7 @@
 import androidx.compose.foundation.fastFilter
 import androidx.compose.foundation.gestures.Orientation
 import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.lazy.layout.LazyPinnedItem
 import androidx.compose.ui.layout.MeasureResult
 import androidx.compose.ui.layout.Placeable
 import androidx.compose.ui.unit.Constraints
@@ -56,7 +57,7 @@
     placementAnimator: LazyListItemPlacementAnimator,
     beyondBoundsInfo: LazyListBeyondBoundsInfo,
     beyondBoundsItemCount: Int,
-    pinnedItems: List<LazyListPinnedItem>,
+    pinnedItems: List<LazyPinnedItem>,
     layout: (Int, Int, Placeable.PlacementScope.() -> Unit) -> MeasureResult
 ): LazyListMeasureResult {
     require(beforeContentPadding >= 0)
@@ -337,7 +338,7 @@
     itemProvider: LazyMeasuredItemProvider,
     itemsCount: Int,
     beyondBoundsItemCount: Int,
-    pinnedItems: List<LazyListPinnedItem>
+    pinnedItems: List<LazyPinnedItem>
 ): List<LazyMeasuredItem> {
     fun LazyListBeyondBoundsInfo.endIndex() = min(end, itemsCount - 1)
 
@@ -377,7 +378,7 @@
     itemProvider: LazyMeasuredItemProvider,
     itemsCount: Int,
     beyondBoundsItemCount: Int,
-    pinnedItems: List<LazyListPinnedItem>
+    pinnedItems: List<LazyPinnedItem>
 ): List<LazyMeasuredItem> {
     fun LazyListBeyondBoundsInfo.startIndex() = min(start, itemsCount - 1)
 
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListState.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListState.kt
index 906b051..d1507e2 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListState.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListState.kt
@@ -24,11 +24,11 @@
 import androidx.compose.foundation.interaction.InteractionSource
 import androidx.compose.foundation.interaction.MutableInteractionSource
 import androidx.compose.foundation.lazy.layout.LazyLayoutPrefetchState
+import androidx.compose.foundation.lazy.layout.LazyPinnedItemContainer
 import androidx.compose.foundation.lazy.layout.animateScrollToItem
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.Stable
 import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateListOf
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.saveable.Saver
 import androidx.compose.runtime.saveable.listSaver
@@ -224,7 +224,7 @@
     /**
      * List of extra items to compose during the measure pass.
      */
-    internal val pinnedItems = mutableStateListOf<LazyListPinnedItem>()
+    internal val pinnedItems = LazyPinnedItemContainer()
 
     /**
      * Instantly brings the item at [index] to the top of the viewport, offset by [scrollOffset]
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGrid.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGrid.kt
index e643447..7ecc15a 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGrid.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGrid.kt
@@ -261,7 +261,7 @@
             this,
             spaceBetweenLines
         ) { index, key, crossAxisSize, mainAxisSpacing, placeables ->
-            LazyMeasuredItem(
+            LazyGridMeasuredItem(
                 index = index,
                 key = key,
                 isVertical = isVertical,
@@ -285,7 +285,7 @@
             measuredItemProvider,
             spanLayoutProvider
         ) { index, items, spans, mainAxisSpacing ->
-            LazyMeasuredLine(
+            LazyGridMeasuredLine(
                 index = index,
                 items = items,
                 spans = spans,
@@ -345,6 +345,7 @@
             density = this,
             placementAnimator = placementAnimator,
             spanLayoutProvider = spanLayoutProvider,
+            pinnedItems = state.pinnedItems,
             layout = { width, height, placement ->
                 layout(
                     containerConstraints.constrainWidth(width + totalHorizontalPadding),
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridItemPlacementAnimator.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridItemPlacementAnimator.kt
index f7a2499..1e5f46c 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridItemPlacementAnimator.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridItemPlacementAnimator.kt
@@ -58,8 +58,8 @@
     private val movingAwayKeys = LinkedHashSet<Any>()
     private val movingInFromStartBound = mutableListOf<LazyGridPositionedItem>()
     private val movingInFromEndBound = mutableListOf<LazyGridPositionedItem>()
-    private val movingAwayToStartBound = mutableListOf<LazyMeasuredItem>()
-    private val movingAwayToEndBound = mutableListOf<LazyMeasuredItem>()
+    private val movingAwayToStartBound = mutableListOf<LazyGridMeasuredItem>()
+    private val movingAwayToEndBound = mutableListOf<LazyGridMeasuredItem>()
 
     /**
      * Should be called after the measuring so we can detect position changes and start animations.
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridItemProvider.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridItemProvider.kt
index 57d3aed..562daad 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridItemProvider.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridItemProvider.kt
@@ -20,6 +20,7 @@
 import androidx.compose.foundation.lazy.layout.DelegatingLazyLayoutItemProvider
 import androidx.compose.foundation.lazy.layout.IntervalList
 import androidx.compose.foundation.lazy.layout.LazyLayoutItemProvider
+import androidx.compose.foundation.lazy.layout.LazyPinnableContainerProvider
 import androidx.compose.foundation.lazy.layout.rememberLazyNearestItemsRangeState
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.State
@@ -56,7 +57,8 @@
             LazyGridItemProviderImpl(
                 gridScope.intervals,
                 gridScope.hasCustomSpans,
-                nearestItemsRangeState.value
+                state,
+                nearestItemsRangeState.value,
             )
         }
 
@@ -80,12 +82,15 @@
 private class LazyGridItemProviderImpl(
     private val intervals: IntervalList<LazyGridIntervalContent>,
     override val hasCustomSpans: Boolean,
+    state: LazyGridState,
     nearestItemsRange: IntRange
 ) : LazyGridItemProvider, LazyLayoutItemProvider by LazyLayoutItemProvider(
     intervals = intervals,
     nearestItemsRange = nearestItemsRange,
     itemContent = { interval, index ->
-        interval.value.item.invoke(LazyGridItemScopeImpl, index - interval.startIndex)
+        LazyPinnableContainerProvider(state.pinnedItems, index) {
+            interval.value.item.invoke(LazyGridItemScopeImpl, index - interval.startIndex)
+        }
     }
 ) {
     override val spanLayoutProvider: LazyGridSpanLayoutProvider =
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridMeasure.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridMeasure.kt
index 7f4538a..12c9769 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridMeasure.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridMeasure.kt
@@ -16,9 +16,10 @@
 
 package androidx.compose.foundation.lazy.grid
 
-import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.fastFilter
 import androidx.compose.foundation.gestures.Orientation
 import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.lazy.layout.LazyPinnedItem
 import androidx.compose.ui.layout.MeasureResult
 import androidx.compose.ui.layout.Placeable
 import androidx.compose.ui.unit.Constraints
@@ -56,6 +57,7 @@
     density: Density,
     placementAnimator: LazyGridItemPlacementAnimator,
     spanLayoutProvider: LazyGridSpanLayoutProvider,
+    pinnedItems: List<LazyPinnedItem>,
     layout: (Int, Int, Placeable.PlacementScope.() -> Unit) -> MeasureResult
 ): LazyGridMeasureResult {
     require(beforeContentPadding >= 0)
@@ -94,7 +96,7 @@
         }
 
         // this will contain all the MeasuredItems representing the visible lines
-        val visibleLines = mutableListOf<LazyMeasuredLine>()
+        val visibleLines = mutableListOf<LazyGridMeasuredLine>()
 
         // define min and max offsets
         val minOffset = -beforeContentPadding + if (spaceBetweenLines < 0) spaceBetweenLines else 0
@@ -201,6 +203,22 @@
         val visibleLinesScrollOffset = -currentFirstLineScrollOffset
         var firstLine = visibleLines.first()
 
+        val firstItemIndex = firstLine.items.firstOrNull()?.index?.value ?: 0
+        val lastItemIndex = visibleLines.lastOrNull()?.items?.lastOrNull()?.index?.value ?: 0
+        val extraItemsBefore = calculateExtraItems(
+            pinnedItems,
+            measuredItemProvider,
+            itemConstraints = { measuredLineProvider.itemConstraints(it) },
+            filter = { it in 0 until firstItemIndex }
+        )
+
+        val extraItemsAfter = calculateExtraItems(
+            pinnedItems,
+            measuredItemProvider,
+            itemConstraints = { measuredLineProvider.itemConstraints(it) },
+            filter = { it in (lastItemIndex + 1) until itemsCount }
+        )
+
         // even if we compose lines to fill before content padding we should ignore lines fully
         // located there for the state's scroll position calculation (first line + first offset)
         if (beforeContentPadding > 0 || spaceBetweenLines < 0) {
@@ -229,6 +247,8 @@
 
         val positionedItems = calculateItemsOffsets(
             lines = visibleLines,
+            itemsBefore = extraItemsBefore,
+            itemsAfter = extraItemsAfter,
             layoutWidth = layoutWidth,
             layoutHeight = layoutHeight,
             finalMainAxisOffset = currentMainAxisOffset,
@@ -250,19 +270,24 @@
             spanLayoutProvider = spanLayoutProvider
         )
 
-        val lastVisibleItemIndex = visibleLines.lastOrNull()?.items?.lastOrNull()?.index?.value ?: 0
         return LazyGridMeasureResult(
             firstVisibleLine = firstLine,
             firstVisibleLineScrollOffset = currentFirstLineScrollOffset,
             canScrollForward =
-                lastVisibleItemIndex != itemsCount - 1 || currentMainAxisOffset > maxOffset,
+                lastItemIndex != itemsCount - 1 || currentMainAxisOffset > maxOffset,
             consumedScroll = consumedScroll,
             measureResult = layout(layoutWidth, layoutHeight) {
                 positionedItems.fastForEach { it.place(this) }
             },
             viewportStartOffset = -beforeContentPadding,
             viewportEndOffset = mainAxisAvailableSize + afterContentPadding,
-            visibleItemsInfo = positionedItems,
+            visibleItemsInfo = if (extraItemsBefore.isEmpty() && extraItemsAfter.isEmpty()) {
+                positionedItems
+            } else {
+                positionedItems.fastFilter {
+                    it.index in firstItemIndex..lastItemIndex
+                }
+            },
             totalItemsCount = itemsCount,
             reverseLayout = reverseLayout,
             orientation = if (isVertical) Orientation.Vertical else Orientation.Horizontal,
@@ -271,12 +296,36 @@
     }
 }
 
+private inline fun calculateExtraItems(
+    pinnedItems: List<LazyPinnedItem>,
+    itemProvider: LazyMeasuredItemProvider,
+    itemConstraints: (ItemIndex) -> Constraints,
+    filter: (Int) -> Boolean
+): List<LazyGridMeasuredItem> {
+    var items: MutableList<LazyGridMeasuredItem>? = null
+
+    pinnedItems.fastForEach {
+        val index = ItemIndex(it.index)
+        if (filter(it.index)) {
+            val constraints = itemConstraints(index)
+            val item = itemProvider.getAndMeasure(index, constraints = constraints)
+            if (items == null) {
+                items = mutableListOf()
+            }
+            items?.add(item)
+        }
+    }
+
+    return items ?: emptyList()
+}
+
 /**
- * Calculates [LazyMeasuredLine]s offsets.
+ * Calculates [LazyGridMeasuredLine]s offsets.
  */
-@OptIn(ExperimentalFoundationApi::class)
 private fun calculateItemsOffsets(
-    lines: List<LazyMeasuredLine>,
+    lines: List<LazyGridMeasuredLine>,
+    itemsBefore: List<LazyGridMeasuredItem>,
+    itemsAfter: List<LazyGridMeasuredItem>,
     layoutWidth: Int,
     layoutHeight: Int,
     finalMainAxisOffset: Int,
@@ -297,6 +346,7 @@
     val positionedItems = ArrayList<LazyGridPositionedItem>(lines.fastSumBy { it.items.size })
 
     if (hasSpareSpace) {
+        require(itemsBefore.isEmpty() && itemsAfter.isEmpty())
         val linesCount = lines.size
         fun Int.reverseAware() =
             if (!reverseLayout) this else linesCount - this - 1
@@ -335,10 +385,36 @@
         }
     } else {
         var currentMainAxis = firstLineScrollOffset
+
+        itemsBefore.fastForEach {
+            currentMainAxis -= it.mainAxisSizeWithSpacings
+            positionedItems.add(it.positionExtraItem(currentMainAxis, layoutWidth, layoutHeight))
+        }
+
+        currentMainAxis = firstLineScrollOffset
         lines.fastForEach {
             positionedItems.addAll(it.position(currentMainAxis, layoutWidth, layoutHeight))
             currentMainAxis += it.mainAxisSizeWithSpacings
         }
+
+        itemsAfter.fastForEach {
+            positionedItems.add(it.positionExtraItem(currentMainAxis, layoutWidth, layoutHeight))
+            currentMainAxis += it.mainAxisSizeWithSpacings
+        }
     }
     return positionedItems
 }
+
+private fun LazyGridMeasuredItem.positionExtraItem(
+    mainAxisOffset: Int,
+    layoutWidth: Int,
+    layoutHeight: Int
+): LazyGridPositionedItem =
+    position(
+        mainAxisOffset = mainAxisOffset,
+        crossAxisOffset = 0,
+        layoutWidth = layoutWidth,
+        layoutHeight = layoutHeight,
+        row = 0,
+        column = 0
+    )
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridMeasureResult.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridMeasureResult.kt
index 8218fac..769a7da 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridMeasureResult.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridMeasureResult.kt
@@ -28,7 +28,7 @@
 internal class LazyGridMeasureResult(
     // properties defining the scroll position:
     /** The new first visible line of items.*/
-    val firstVisibleLine: LazyMeasuredLine?,
+    val firstVisibleLine: LazyGridMeasuredLine?,
     /** The new value for [LazyGridState.firstVisibleItemScrollOffset].*/
     val firstVisibleLineScrollOffset: Int,
     /** True if there is some space available to continue scrolling in the forward direction.*/
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyMeasuredItem.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridMeasuredItem.kt
similarity index 99%
rename from compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyMeasuredItem.kt
rename to compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridMeasuredItem.kt
index dcf4450..eff4278 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyMeasuredItem.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridMeasuredItem.kt
@@ -29,7 +29,7 @@
  * if the user emit multiple layout nodes in the item callback.
  */
 @OptIn(ExperimentalFoundationApi::class)
-internal class LazyMeasuredItem(
+internal class LazyGridMeasuredItem(
     val index: ItemIndex,
     val key: Any,
     private val isVertical: Boolean,
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyMeasuredLine.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridMeasuredLine.kt
similarity index 96%
rename from compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyMeasuredLine.kt
rename to compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridMeasuredLine.kt
index 2c9279f..efc095e 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyMeasuredLine.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridMeasuredLine.kt
@@ -24,9 +24,9 @@
  * multiple placeables if the user emit multiple layout nodes in the item callback.
  */
 @OptIn(ExperimentalFoundationApi::class)
-internal class LazyMeasuredLine constructor(
+internal class LazyGridMeasuredLine constructor(
     val index: LineIndex,
-    val items: Array<LazyMeasuredItem>,
+    val items: Array<LazyGridMeasuredItem>,
     private val spans: List<GridItemSpan>,
     private val isVertical: Boolean,
     private val slotsPerLine: Int,
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridSpanLayoutProvider.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridSpanLayoutProvider.kt
index f7d777b..e4d0e7b 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridSpanLayoutProvider.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridSpanLayoutProvider.kt
@@ -210,7 +210,7 @@
         return LineIndex(currentLine)
     }
 
-    private fun spanOf(itemIndex: Int, maxSpan: Int) = with(itemProvider) {
+    fun spanOf(itemIndex: Int, maxSpan: Int) = with(itemProvider) {
         with(LazyGridItemSpanScopeImpl) {
             maxCurrentLineSpan = maxSpan
             maxLineSpan = slotsPerLine
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridState.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridState.kt
index a19e3d2..89ac78a 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridState.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridState.kt
@@ -25,6 +25,7 @@
 import androidx.compose.foundation.interaction.MutableInteractionSource
 import androidx.compose.foundation.lazy.AwaitFirstLayoutModifier
 import androidx.compose.foundation.lazy.layout.LazyLayoutPrefetchState
+import androidx.compose.foundation.lazy.layout.LazyPinnedItemContainer
 import androidx.compose.foundation.lazy.layout.animateScrollToItem
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.Stable
@@ -81,6 +82,7 @@
     firstVisibleItemIndex: Int = 0,
     firstVisibleItemScrollOffset: Int = 0
 ) : ScrollableState {
+
     /**
      * The holder class for the current scroll position.
      */
@@ -226,6 +228,11 @@
     private val animateScrollScope = LazyGridAnimateScrollScope(this)
 
     /**
+     * Pinned items are measured and placed even when they are beyond bounds of lazy layout.
+     */
+    internal val pinnedItems = LazyPinnedItemContainer()
+
+    /**
      * Instantly brings the item at [index] to the top of the viewport, offset by [scrollOffset]
      * pixels.
      *
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyMeasuredItemProvider.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyMeasuredItemProvider.kt
index 68d6504..651aa4f 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyMeasuredItemProvider.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyMeasuredItemProvider.kt
@@ -33,13 +33,13 @@
 ) {
     /**
      * Used to subcompose individual items of lazy grids. Composed placeables will be measured
-     * with the provided [constraints] and wrapped into [LazyMeasuredItem].
+     * with the provided [constraints] and wrapped into [LazyGridMeasuredItem].
      */
     fun getAndMeasure(
         index: ItemIndex,
         mainAxisSpacing: Int = defaultMainAxisSpacing,
         constraints: Constraints
-    ): LazyMeasuredItem {
+    ): LazyGridMeasuredItem {
         val key = itemProvider.getKey(index.value)
         val placeables = measureScope.measure(index.value, constraints)
         val crossAxisSize = if (constraints.hasFixedWidth) {
@@ -72,5 +72,5 @@
         crossAxisSize: Int,
         mainAxisSpacing: Int,
         placeables: List<Placeable>
-    ): LazyMeasuredItem
+    ): LazyGridMeasuredItem
 }
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyMeasuredLineProvider.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyMeasuredLineProvider.kt
index 588ae51..0b7296e 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyMeasuredLineProvider.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyMeasuredLineProvider.kt
@@ -46,11 +46,19 @@
         }
     }
 
+    fun itemConstraints(itemIndex: ItemIndex): Constraints {
+        val span = spanLayoutProvider.spanOf(
+            itemIndex.value,
+            spanLayoutProvider.slotsPerLine
+        )
+        return childConstraints(0, span)
+    }
+
     /**
      * Used to subcompose items on lines of lazy grids. Composed placeables will be measured
-     * with the correct constraints and wrapped into [LazyMeasuredLine].
+     * with the correct constraints and wrapped into [LazyGridMeasuredLine].
      */
-    fun getAndMeasure(lineIndex: LineIndex): LazyMeasuredLine {
+    fun getAndMeasure(lineIndex: LineIndex): LazyGridMeasuredLine {
         val lineConfiguration = spanLayoutProvider.getLineConfiguration(lineIndex.value)
         val lineItemsCount = lineConfiguration.spans.size
 
@@ -93,8 +101,8 @@
 internal fun interface MeasuredLineFactory {
     fun createLine(
         index: LineIndex,
-        items: Array<LazyMeasuredItem>,
+        items: Array<LazyGridMeasuredItem>,
         spans: List<GridItemSpan>,
         mainAxisSpacing: Int
-    ): LazyMeasuredLine
+    ): LazyGridMeasuredLine
 }
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListPinnableContainerProvider.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyPinnableContainerProvider.kt
similarity index 78%
rename from compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListPinnableContainerProvider.kt
rename to compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyPinnableContainerProvider.kt
index 6d8ffb8..a3e794a 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListPinnableContainerProvider.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyPinnableContainerProvider.kt
@@ -1,5 +1,5 @@
 /*
- * Copyright 2022 The Android Open Source Project
+ * Copyright 2023 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package androidx.compose.foundation.lazy
+package androidx.compose.foundation.lazy.layout
 
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.CompositionLocalProvider
@@ -24,20 +24,33 @@
 import androidx.compose.runtime.remember
 import androidx.compose.runtime.setValue
 import androidx.compose.runtime.snapshots.Snapshot
+import androidx.compose.runtime.snapshots.SnapshotStateList
 import androidx.compose.ui.layout.LocalPinnableContainer
 import androidx.compose.ui.layout.PinnableContainer
 
-internal interface LazyListPinnedItem {
+internal interface LazyPinnedItem {
     val index: Int
 }
 
+internal class LazyPinnedItemContainer(
+    private val pinnedItems: MutableList<LazyPinnedItem> = SnapshotStateList()
+) : List<LazyPinnedItem> by pinnedItems {
+    fun pin(item: LazyPinnedItem) {
+        pinnedItems.add(item)
+    }
+
+    fun unpin(item: LazyPinnedItem) {
+        pinnedItems.remove(item)
+    }
+}
+
 @Composable
-internal fun LazyListPinnableContainerProvider(
-    state: LazyListState,
+internal fun LazyPinnableContainerProvider(
+    owner: LazyPinnedItemContainer,
     index: Int,
     content: @Composable () -> Unit
 ) {
-    val pinnableItem = remember(state) { LazyListPinnableItem(state) }
+    val pinnableItem = remember(owner) { LazyPinnableItem(owner) }
     pinnableItem.index = index
     pinnableItem.parentPinnableContainer = LocalPinnableContainer.current
     DisposableEffect(pinnableItem) { onDispose { pinnableItem.onDisposed() } }
@@ -46,9 +59,9 @@
     )
 }
 
-private class LazyListPinnableItem(
-    private val state: LazyListState,
-) : PinnableContainer, PinnableContainer.PinnedHandle, LazyListPinnedItem {
+private class LazyPinnableItem(
+    private val owner: LazyPinnedItemContainer,
+) : PinnableContainer, PinnableContainer.PinnedHandle, LazyPinnedItem {
     /**
      * Current index associated with this item.
      */
@@ -86,7 +99,7 @@
 
     override fun pin(): PinnableContainer.PinnedHandle {
         if (pinsCount == 0) {
-            state.pinnedItems.add(this)
+            owner.pin(this)
             parentHandle = parentPinnableContainer?.pin()
         }
         pinsCount++
@@ -97,7 +110,7 @@
         check(pinsCount > 0) { "Unpin should only be called once" }
         pinsCount--
         if (pinsCount == 0) {
-            state.pinnedItems.remove(this)
+            owner.unpin(this)
             parentHandle?.unpin()
             parentHandle = null
         }
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridItemProvider.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridItemProvider.kt
index be07e927..7ce5cff 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridItemProvider.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridItemProvider.kt
@@ -19,6 +19,7 @@
 import androidx.compose.foundation.ExperimentalFoundationApi
 import androidx.compose.foundation.lazy.layout.DelegatingLazyLayoutItemProvider
 import androidx.compose.foundation.lazy.layout.LazyLayoutItemProvider
+import androidx.compose.foundation.lazy.layout.LazyPinnableContainerProvider
 import androidx.compose.foundation.lazy.layout.rememberLazyNearestItemsRangeState
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.derivedStateOf
@@ -48,11 +49,13 @@
             object : LazyLayoutItemProvider by LazyLayoutItemProvider(
                 scope.intervals,
                 nearestItemsRangeState.value,
-                { interval, index ->
-                    interval.value.item.invoke(
-                        LazyStaggeredGridItemScopeImpl,
-                        index - interval.startIndex
-                    )
+                itemContent = { interval, index ->
+                    LazyPinnableContainerProvider(state.pinnedItems, index) {
+                        interval.value.item.invoke(
+                            LazyStaggeredGridItemScopeImpl,
+                            index - interval.startIndex
+                        )
+                    }
                 }
             ), LazyStaggeredGridItemProvider {
                 override val spanProvider = LazyStaggeredGridSpanProvider(scope.intervals)
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridMeasure.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridMeasure.kt
index 28210ac..c24b674 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridMeasure.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridMeasure.kt
@@ -21,9 +21,9 @@
 import androidx.compose.foundation.fastMaxOfOrNull
 import androidx.compose.foundation.lazy.layout.LazyLayoutItemProvider
 import androidx.compose.foundation.lazy.layout.LazyLayoutMeasureScope
+import androidx.compose.foundation.lazy.layout.LazyPinnedItem
 import androidx.compose.foundation.lazy.staggeredgrid.LazyStaggeredGridLaneInfo.Companion.FullSpan
 import androidx.compose.foundation.lazy.staggeredgrid.LazyStaggeredGridLaneInfo.Companion.Unset
-import androidx.compose.runtime.collection.MutableVector
 import androidx.compose.runtime.snapshots.Snapshot
 import androidx.compose.ui.layout.Placeable
 import androidx.compose.ui.unit.Constraints
@@ -636,6 +636,65 @@
 
         // end measure
 
+        var extraItemOffset = itemScrollOffsets[0]
+        val extraItemsBefore = calculateExtraItems(
+            state.pinnedItems,
+            position = {
+                extraItemOffset -= it.sizeWithSpacings
+                it.position(0, extraItemOffset, 0)
+            }
+        ) { itemIndex ->
+            val lane = laneInfo.getLane(itemIndex)
+            when (lane) {
+                Unset, FullSpan -> {
+                    firstItemIndices.all { it > itemIndex }
+                }
+                else -> {
+                    firstItemIndices[lane] > itemIndex
+                }
+            }
+        }
+
+        val positionedItems = calculatePositionedItems(
+            measuredItems,
+            itemScrollOffsets
+        )
+
+        extraItemOffset = itemScrollOffsets[0]
+        val extraItemsAfter = calculateExtraItems(
+            state.pinnedItems,
+            position = {
+                val positionedItem = it.position(0, extraItemOffset, 0)
+                extraItemOffset += it.sizeWithSpacings
+                positionedItem
+            }
+        ) { itemIndex ->
+            if (itemIndex >= itemCount) {
+                return@calculateExtraItems false
+            }
+            val lane = laneInfo.getLane(itemIndex)
+            when (lane) {
+                Unset, FullSpan -> {
+                    currentItemIndices.all { it < itemIndex }
+                }
+                else -> {
+                    currentItemIndices[lane] < itemIndex
+                }
+            }
+        }
+
+        debugLog {
+            @Suppress("ListIterator")
+            "| positioned: ${positionedItems.map { "${it.index} at ${it.offset}" }.toList()}"
+        }
+        debugLog {
+            "========== MEASURE COMPLETED ==========="
+        }
+
+        // todo: reverse layout support
+
+        // End placement
+
         val layoutWidth = if (isVertical) {
             constraints.maxWidth
         } else {
@@ -646,54 +705,6 @@
         } else {
             constraints.maxHeight
         }
-
-        // Placement
-        val positionedItems = MutableVector<LazyStaggeredGridPositionedItem>(
-            capacity = measuredItems.sumOf { it.size }
-        )
-        while (measuredItems.any { it.isNotEmpty() }) {
-            // find the next item to position
-            val laneIndex = measuredItems.indexOfMinBy {
-                it.firstOrNull()?.index ?: Int.MAX_VALUE
-            }
-            val item = measuredItems[laneIndex].removeFirst()
-
-            if (item.lane != laneIndex) {
-                continue
-            }
-
-            // todo(b/182882362): arrangement support
-            val spanRange = SpanRange(item.lane, item.span)
-            val mainAxisOffset = itemScrollOffsets.maxInRange(spanRange)
-            val crossAxisOffset =
-                if (laneIndex == 0) {
-                    0
-                } else {
-                    resolvedSlotSums[laneIndex - 1] + crossAxisSpacing * laneIndex
-                }
-
-            if (item.placeables.isEmpty()) {
-                // nothing to place, ignore spacings
-                continue
-            }
-
-            positionedItems += item.position(laneIndex, mainAxisOffset, crossAxisOffset)
-            spanRange.forEach { lane ->
-                itemScrollOffsets[lane] = mainAxisOffset + item.sizeWithSpacings
-            }
-        }
-
-        debugLog {
-            "| positioned: ${positionedItems.map { "${it.index} at ${it.offset}" }.toList()}"
-        }
-        debugLog {
-            "========== MEASURE DONE ==========="
-        }
-
-        // todo: reverse layout support
-
-        // End placement
-
         // only scroll backward if the first item is not on screen or fully visible
         val canScrollBackward = !(firstItemIndices[0] == 0 && firstItemOffsets[0] <= 0)
         // only scroll forward if the last item is not on screen or fully visible
@@ -706,14 +717,22 @@
             firstVisibleItemScrollOffsets = firstItemOffsets,
             consumedScroll = consumedScroll,
             measureResult = layout(layoutWidth, layoutHeight) {
-                positionedItems.forEach { item ->
+                extraItemsBefore.fastForEach { item ->
+                    item.place(this)
+                }
+
+                positionedItems.fastForEach { item ->
+                    item.place(this)
+                }
+
+                extraItemsAfter.fastForEach { item ->
                     item.place(this)
                 }
             },
             canScrollForward = canScrollForward,
             canScrollBackward = canScrollBackward,
             isVertical = isVertical,
-            visibleItemsInfo = positionedItems.asMutableList(),
+            visibleItemsInfo = positionedItems,
             totalItemsCount = itemCount,
             viewportSize = IntSize(layoutWidth, layoutHeight),
             viewportStartOffset = minOffset,
@@ -724,6 +743,68 @@
     }
 }
 
+private fun LazyStaggeredGridMeasureContext.calculatePositionedItems(
+    measuredItems: Array<ArrayDeque<LazyStaggeredGridMeasuredItem>>,
+    itemScrollOffsets: IntArray,
+): List<LazyStaggeredGridPositionedItem> {
+    val positionedItems = ArrayList<LazyStaggeredGridPositionedItem>(
+        measuredItems.sumOf { it.size }
+    )
+    while (measuredItems.any { it.isNotEmpty() }) {
+        // find the next item to position
+        val laneIndex = measuredItems.indexOfMinBy {
+            it.firstOrNull()?.index ?: Int.MAX_VALUE
+        }
+        val item = measuredItems[laneIndex].removeFirst()
+
+        if (item.lane != laneIndex) {
+            continue
+        }
+
+        // todo(b/182882362): arrangement support
+        val spanRange = SpanRange(item.lane, item.span)
+        val mainAxisOffset = itemScrollOffsets.maxInRange(spanRange)
+        val crossAxisOffset =
+            if (laneIndex == 0) {
+                0
+            } else {
+                resolvedSlotSums[laneIndex - 1] + crossAxisSpacing * laneIndex
+            }
+
+        if (item.placeables.isEmpty()) {
+            // nothing to place, ignore spacings
+            continue
+        }
+
+        positionedItems += item.position(laneIndex, mainAxisOffset, crossAxisOffset)
+        spanRange.forEach { lane ->
+            itemScrollOffsets[lane] = mainAxisOffset + item.sizeWithSpacings
+        }
+    }
+    return positionedItems
+}
+
+private inline fun LazyStaggeredGridMeasureContext.calculateExtraItems(
+    pinnedItems: List<LazyPinnedItem>,
+    position: (LazyStaggeredGridMeasuredItem) -> LazyStaggeredGridPositionedItem,
+    filter: (itemIndex: Int) -> Boolean
+): List<LazyStaggeredGridPositionedItem> {
+    var result: MutableList<LazyStaggeredGridPositionedItem>? = null
+
+    pinnedItems.fastForEach {
+        if (filter(it.index)) {
+            val spanRange = itemProvider.getSpanRange(it.index, 0)
+            if (result == null) {
+                result = mutableListOf()
+            }
+            val measuredItem = measuredItemProvider.getAndMeasure(it.index, spanRange)
+            result?.add(position(measuredItem))
+        }
+    }
+
+    return result ?: emptyList()
+}
+
 @JvmInline
 private value class SpanRange private constructor(val packedValue: Long) {
     constructor(lane: Int, span: Int) : this(packInts(lane, lane + span))
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridState.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridState.kt
index bf95c2e..82c3e4b 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridState.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridState.kt
@@ -26,6 +26,7 @@
 import androidx.compose.foundation.lazy.layout.LazyLayoutItemProvider
 import androidx.compose.foundation.lazy.layout.LazyLayoutPrefetchState
 import androidx.compose.foundation.lazy.layout.LazyLayoutPrefetchState.PrefetchHandle
+import androidx.compose.foundation.lazy.layout.LazyPinnedItemContainer
 import androidx.compose.foundation.lazy.layout.animateScrollToItem
 import androidx.compose.foundation.lazy.staggeredgrid.LazyStaggeredGridLaneInfo.Companion.Unset
 import androidx.compose.runtime.Composable
@@ -206,6 +207,11 @@
     internal val mutableInteractionSource = MutableInteractionSource()
 
     /**
+     * List of extra items to compose during the measure pass.
+     */
+    internal val pinnedItems = LazyPinnedItemContainer()
+
+    /**
      * Call this function to take control of scrolling and gain the ability to send scroll events
      * via [ScrollScope.scrollBy]. All actions that change the logical scroll position must be
      * performed within a [scroll] block (even if they don't call any other methods on this
diff --git a/compose/integration-tests/demos/src/main/java/androidx/compose/integration/demos/DemoActivity.kt b/compose/integration-tests/demos/src/main/java/androidx/compose/integration/demos/DemoActivity.kt
index 42befbe..d891805 100644
--- a/compose/integration-tests/demos/src/main/java/androidx/compose/integration/demos/DemoActivity.kt
+++ b/compose/integration-tests/demos/src/main/java/androidx/compose/integration/demos/DemoActivity.kt
@@ -139,7 +139,7 @@
         const val DEMO_NAME = "demoname"
 
         internal fun requireDemo(demoName: String, demo: Demo?) = requireNotNull(demo) {
-            "No demo called \"$demoName\" could be found."
+            "No demo called \"$demoName\" could be found. Note substring matches are allowed."
         }
     }
 }
@@ -239,20 +239,26 @@
             restore = { restored ->
                 require(restored.isNotEmpty())
                 val backStack = restored.mapTo(mutableListOf()) {
-                    requireNotNull(findDemo(rootDemo, it))
+                    requireNotNull(findDemo(rootDemo, it, exact = true))
                 }
                 val initial = backStack.removeAt(backStack.lastIndex)
                 Navigator(backDispatcher, launchActivityDemo, rootDemo, initial, backStack)
             }
         )
 
-        fun findDemo(demo: Demo, title: String): Demo? {
-            if (demo.title == title) {
-                return demo
+        fun findDemo(demo: Demo, title: String, exact: Boolean = false): Demo? {
+            if (exact) {
+                if (demo.title == title) {
+                    return demo
+                }
+            } else {
+                if (demo.title.contains(title)) {
+                    return demo
+                }
             }
             if (demo is DemoCategory) {
                 demo.demos.forEach { child ->
-                    findDemo(child, title)
+                    findDemo(child, title, exact)
                         ?.let { return it }
                 }
             }
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/DatePickerTest.kt b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/DatePickerTest.kt
index c17e736..a22a0342 100644
--- a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/DatePickerTest.kt
+++ b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/DatePickerTest.kt
@@ -16,10 +16,15 @@
 
 package androidx.compose.material3
 
+import androidx.compose.ui.semantics.Role
+import androidx.compose.ui.semantics.SemanticsProperties
+import androidx.compose.ui.test.SemanticsMatcher.Companion.expectValue
+import androidx.compose.ui.test.assert
+import androidx.compose.ui.test.assertContentDescriptionEquals
 import androidx.compose.ui.test.assertIsEnabled
 import androidx.compose.ui.test.assertIsNotEnabled
-import androidx.compose.ui.test.assertIsOff
-import androidx.compose.ui.test.assertIsOn
+import androidx.compose.ui.test.assertIsNotSelected
+import androidx.compose.ui.test.assertIsSelected
 import androidx.compose.ui.test.junit4.StateRestorationTester
 import androidx.compose.ui.test.junit4.createComposeRule
 import androidx.compose.ui.test.onAllNodesWithText
@@ -31,6 +36,7 @@
 import androidx.test.filters.MediumTest
 import com.google.common.truth.Truth.assertThat
 import java.util.Calendar
+import java.util.Locale
 import java.util.TimeZone
 import org.junit.Rule
 import org.junit.Test
@@ -58,7 +64,7 @@
         }
 
         // Select the 11th day of the displayed month is selected.
-        rule.onNodeWithText("11").assertIsOn()
+        rule.onNodeWithText("11").assertIsSelected()
         rule.onNodeWithText("May 11, 2010").assertExists()
     }
 
@@ -78,7 +84,7 @@
         rule.onNodeWithText(defaultHeadline).assertExists()
 
         // Select the 27th day of the displayed month.
-        rule.onNodeWithText("27").assertIsOff()
+        rule.onNodeWithText("27").assertIsNotSelected()
         rule.onNodeWithText("27").performClick()
 
         rule.runOnIdle {
@@ -93,7 +99,7 @@
 
         rule.onNodeWithText(defaultHeadline).assertDoesNotExist()
         rule.onNodeWithText("Jan 27, 2019").assertExists()
-        rule.onNodeWithText("27").assertIsOn()
+        rule.onNodeWithText("27").assertIsSelected()
     }
 
     @Test
@@ -136,7 +142,7 @@
         }
 
         rule.onNodeWithText("January 2019").performClick()
-        rule.onNodeWithText("2019").assertIsOn()
+        rule.onNodeWithText("2019").assertIsSelected()
         rule.onNodeWithText("2020").performClick()
         // Select the 15th day of the displayed month in 2020.
         rule.onAllNodesWithText("15").onFirst().performClick()
@@ -153,8 +159,8 @@
 
         // Check that if the years are opened again, the last selected year is still marked as such
         rule.onNodeWithText("January 2020").performClick()
-        rule.onNodeWithText("2019").assertIsOff()
-        rule.onNodeWithText("2020").assertIsOn()
+        rule.onNodeWithText("2019").assertIsNotSelected()
+        rule.onNodeWithText("2020").assertIsSelected()
     }
 
     @Test
@@ -366,6 +372,43 @@
         }
     }
 
+    @Test
+    fun defaultSemantics() {
+        val selectedDateInUtcMillis = dayInUtcMilliseconds(year = 2010, month = 5, dayOfMonth = 11)
+        val monthInUtcMillis = dayInUtcMilliseconds(year = 2010, month = 5, dayOfMonth = 1)
+        lateinit var expectedHeadlineStringFormat: String
+        rule.setMaterialContent(lightColorScheme()) {
+            // e.g. "Current selection: %1$s"
+            expectedHeadlineStringFormat = getString(Strings.DatePickerHeadlineDescription)
+            DatePicker(
+                datePickerState = rememberDatePickerState(
+                    initialSelectedDateMillis = selectedDateInUtcMillis,
+                    initialDisplayedMonthMillis = monthInUtcMillis
+                )
+            )
+        }
+
+        val fullDateDescription = formatWithSkeleton(
+            selectedDateInUtcMillis,
+            DatePickerDefaults.YearMonthWeekdayDaySkeleton,
+            Locale.US
+        )
+
+        rule.onNodeWithContentDescription(label = "next", substring = true, ignoreCase = true)
+            .assert(expectValue(SemanticsProperties.Role, Role.Button))
+        rule.onNodeWithContentDescription(label = "previous", substring = true, ignoreCase = true)
+            .assert(expectValue(SemanticsProperties.Role, Role.Button))
+        rule.onNodeWithText("May 2010")
+            .assert(expectValue(SemanticsProperties.Role, Role.Button))
+        rule.onNodeWithText("11")
+            .assert(expectValue(SemanticsProperties.Role, Role.Button))
+            .assertContentDescriptionEquals(fullDateDescription)
+        rule.onNodeWithText("May 11, 2010")
+            .assertContentDescriptionEquals(
+                expectedHeadlineStringFormat.format(fullDateDescription)
+            )
+    }
+
     // Returns the given date's day as milliseconds from epoch. The returned value is for the day's
     // start on midnight.
     private fun dayInUtcMilliseconds(year: Int, month: Int, dayOfMonth: Int): Long {
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/TooltipTest.kt b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/TooltipTest.kt
index cc8b1ea..ae58d4b 100644
--- a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/TooltipTest.kt
+++ b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/TooltipTest.kt
@@ -35,9 +35,9 @@
 import androidx.compose.ui.test.performTouchInput
 import androidx.compose.ui.unit.dp
 import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.test.filters.FlakyTest
 import androidx.test.filters.MediumTest
 import kotlinx.coroutines.launch
+import org.junit.Ignore
 import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -75,7 +75,7 @@
             .assertWidthIsEqualTo(customWidth)
     }
 
-    @FlakyTest(bugId = 264907895)
+    @Ignore // b/264907895
     @Test
     fun plainTooltip_content_padding() {
         rule.setMaterialContent(lightColorScheme()) {
@@ -94,7 +94,7 @@
             .assertTopPositionInRootIsEqualTo(4.dp)
     }
 
-    @FlakyTest(bugId = 264887805)
+    @Ignore // b/264887805
     @Test
     fun plainTooltip_behavior() {
         rule.setMaterialContent(lightColorScheme()) {
diff --git a/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/Strings.android.kt b/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/Strings.android.kt
index f3ca92b..9059aad 100644
--- a/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/Strings.android.kt
+++ b/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/Strings.android.kt
@@ -50,6 +50,9 @@
         Strings.DatePickerHeadline -> resources.getString(
             androidx.compose.material3.R.string.date_picker_headline
         )
+        Strings.DatePickerYearPickerPaneTitle -> resources.getString(
+            androidx.compose.material3.R.string.date_picker_year_picker_pane_title
+        )
         Strings.DatePickerSwitchToYearSelection -> resources.getString(
             androidx.compose.material3.R.string.date_picker_switch_to_year_selection
         )
@@ -62,6 +65,15 @@
         Strings.DatePickerSwitchToPreviousMonth -> resources.getString(
             androidx.compose.material3.R.string.date_picker_switch_to_previous_month
         )
+        Strings.DatePickerNavigateToYearDescription -> resources.getString(
+            androidx.compose.material3.R.string.date_picker_navigate_to_year_description
+        )
+        Strings.DatePickerHeadlineDescription -> resources.getString(
+            androidx.compose.material3.R.string.date_picker_headline_description
+        )
+        Strings.DatePickerNoSelectionDescription -> resources.getString(
+            androidx.compose.material3.R.string.date_picker_no_selection_description
+        )
         else -> ""
     }
 }
diff --git a/compose/material3/material3/src/androidMain/res/values/strings.xml b/compose/material3/material3/src/androidMain/res/values/strings.xml
index 9284a12..6949cde 100644
--- a/compose/material3/material3/src/androidMain/res/values/strings.xml
+++ b/compose/material3/material3/src/androidMain/res/values/strings.xml
@@ -32,7 +32,13 @@
     <string name="date_picker_title">"Select date"</string>
     <string name="date_picker_headline">"Selected date"</string>
     <string name="date_picker_switch_to_year_selection">"Switch to selecting a year"</string>
-    <string name="date_picker_switch_to_day_selection">"Switch to selecting a day"</string>
+    <string name="date_picker_switch_to_day_selection">
+        "Swipe to select a year, or tap to switch back to selecting a day"
+    </string>
     <string name="date_picker_switch_to_next_month">"Change to next month"</string>
     <string name="date_picker_switch_to_previous_month">"Change to previous month"</string>
+    <string name="date_picker_navigate_to_year_description">Navigate to year %1$s</string>
+    <string name="date_picker_headline_description">Current selection: %1$s</string>
+    <string name="date_picker_no_selection_description">None</string>
+    <string name="date_picker_year_picker_pane_title">Year picker visible</string>
 </resources>
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/DatePicker.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/DatePicker.kt
index 602bf70..850dbfc9 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/DatePicker.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/DatePicker.kt
@@ -82,8 +82,17 @@
 import androidx.compose.ui.graphics.Shape
 import androidx.compose.ui.platform.LocalDensity
 import androidx.compose.ui.platform.LocalLayoutDirection
+import androidx.compose.ui.semantics.LiveRegionMode
+import androidx.compose.ui.semantics.Role
+import androidx.compose.ui.semantics.ScrollAxisRange
 import androidx.compose.ui.semantics.clearAndSetSemantics
 import androidx.compose.ui.semantics.contentDescription
+import androidx.compose.ui.semantics.horizontalScrollAxisRange
+import androidx.compose.ui.semantics.liveRegion
+import androidx.compose.ui.semantics.paneTitle
+import androidx.compose.ui.semantics.role
+import androidx.compose.ui.semantics.semantics
+import androidx.compose.ui.semantics.verticalScrollAxisRange
 import androidx.compose.ui.text.style.TextAlign
 import androidx.compose.ui.unit.Dp
 import androidx.compose.ui.unit.LayoutDirection
@@ -385,11 +394,24 @@
             date = state.selectedDate,
             calendarModel = state.calendarModel
         )
-        if (formattedDate == null) {
-            Text(getString(string = Strings.DatePickerHeadline), maxLines = 1)
-        } else {
-            Text(formattedDate, maxLines = 1)
-        }
+        val verboseDateDescription = dateFormatter.formatDate(
+            date = state.selectedDate,
+            calendarModel = state.calendarModel,
+            forContentDescription = true
+        ) ?: getString(Strings.DatePickerNoSelectionDescription)
+
+        val headlineText = formattedDate ?: getString(string = Strings.DatePickerHeadline)
+        val headlineDescription =
+            getString(Strings.DatePickerHeadlineDescription).format(verboseDateDescription)
+
+        Text(
+            text = headlineText,
+            modifier = Modifier.semantics {
+                liveRegion = LiveRegionMode.Polite
+                contentDescription = headlineDescription
+            },
+            maxLines = 1
+        )
     }
 
     /**
@@ -645,10 +667,18 @@
     internal fun formatDate(
         date: CalendarDate?,
         calendarModel: CalendarModel,
+        forContentDescription: Boolean = false,
         locale: Locale = Locale.getDefault()
     ): String? {
         if (date == null) return null
-        return calendarModel.formatWithSkeleton(date, selectedDateSkeleton, locale)
+        return calendarModel.formatWithSkeleton(
+            date, if (forContentDescription) {
+                selectedDateDescriptionSkeleton
+            } else {
+                selectedDateSkeleton
+            },
+            locale
+        )
     }
 
     override fun equals(other: Any?): Boolean {
@@ -737,6 +767,7 @@
                     onDateSelected = onDateSelected,
                     datePickerState = datePickerState,
                     lazyListState = monthsListState,
+                    dateFormatter = dateFormatter,
                     dateValidator = dateValidator,
                     colors = colors
                 )
@@ -747,7 +778,12 @@
                 enter = expandVertically() + fadeIn(initialAlpha = 0.6f),
                 exit = shrinkVertically() + fadeOut()
             ) {
-                Column {
+                // Apply a paneTitle to make the screen reader focus on a relevant node after this
+                // column is hidden and disposed.
+                // TODO(b/186443263): Have the screen reader focus on a year in the list when the
+                //  list is revealed.
+                val yearsPaneTitle = getString(Strings.DatePickerYearPickerPaneTitle)
+                Column(modifier = Modifier.semantics { paneTitle = yearsPaneTitle }) {
                     YearPicker(
                         // Keep the height the same as the monthly calendar + weekdays height, and
                         // take into account the thickness of the divider that will be composed
@@ -830,6 +866,7 @@
     onDateSelected: (dateInMillis: Long) -> Unit,
     datePickerState: DatePickerState,
     lazyListState: LazyListState,
+    dateFormatter: DatePickerFormatter,
     dateValidator: (Long) -> Boolean,
     colors: DatePickerColors,
 ) {
@@ -841,6 +878,12 @@
         )
     }
     LazyRow(
+        // Apply this to prevent the screen reader from scrolling to the next or previous month, and
+        // instead, traverse outside the Month composable when swiping from a focused first or last
+        // day of the month.
+        modifier = Modifier.semantics {
+            horizontalScrollAxisRange = ScrollAxisRange(value = { 0f }, maxValue = { 0f })
+        },
         state = lazyListState,
         // TODO(b/264687693): replace with the framework's rememberSnapFlingBehavior(lazyListState)
         //  when promoted to stable
@@ -860,6 +903,7 @@
                     onDateSelected = onDateSelected,
                     today = today,
                     selectedDate = datePickerState.selectedDate,
+                    dateFormatter = dateFormatter,
                     dateValidator = dateValidator,
                     colors = colors
                 )
@@ -946,6 +990,7 @@
     today: CalendarDate,
     selectedDate: CalendarDate?,
     dateValidator: (Long) -> Boolean,
+    dateFormatter: DatePickerFormatter,
     colors: DatePickerColors
 ) {
     ProvideTextStyle(
@@ -982,16 +1027,29 @@
                             val dateInMillis = month.startUtcTimeMillis +
                                 (dayNumber * MillisecondsIn24Hours)
                             Day(
-                                checked = dateInMillis == selectedDate?.utcTimeMillis,
-                                onCheckedChange = { onDateSelected(dateInMillis) },
+                                modifier = Modifier.semantics { role = Role.Button },
+                                selected = dateInMillis == selectedDate?.utcTimeMillis,
+                                onClick = { onDateSelected(dateInMillis) },
                                 animateChecked = true,
                                 enabled = remember(dateInMillis) {
                                     dateValidator.invoke(dateInMillis)
                                 },
                                 today = dateInMillis == today.utcTimeMillis,
-                                text = (dayNumber + 1).toLocalString(),
                                 colors = colors
-                            )
+                            ) {
+                                Text(
+                                    text = (dayNumber + 1).toLocalString(),
+                                    modifier = Modifier.semantics {
+                                        contentDescription =
+                                            formatWithSkeleton(
+                                                dateInMillis,
+                                                dateFormatter.selectedDateDescriptionSkeleton,
+                                                Locale.getDefault()
+                                            )
+                                    },
+                                    textAlign = TextAlign.Center
+                                )
+                            }
                         }
                         cellsCount++
                     }
@@ -1004,18 +1062,19 @@
 @OptIn(ExperimentalMaterial3Api::class)
 @Composable
 private fun Day(
-    checked: Boolean,
-    onCheckedChange: (Boolean) -> Unit,
+    modifier: Modifier,
+    selected: Boolean,
+    onClick: () -> Unit,
     animateChecked: Boolean,
     enabled: Boolean,
     today: Boolean,
-    text: String,
-    colors: DatePickerColors
+    colors: DatePickerColors,
+    content: @Composable () -> Unit
 ) {
     Surface(
-        checked = checked,
-        onCheckedChange = onCheckedChange,
-        modifier = Modifier
+        selected = selected,
+        onClick = onClick,
+        modifier = modifier
             .minimumInteractiveComponentSize()
             .requiredSize(
                 DatePickerModalTokens.DateStateLayerWidth,
@@ -1024,16 +1083,16 @@
         enabled = enabled,
         shape = DatePickerModalTokens.DateContainerShape.toShape(),
         color = colors.dayContainerColor(
-            selected = checked,
+            selected = selected,
             enabled = enabled,
             animate = animateChecked
         ).value,
         contentColor = colors.dayContentColor(
             today = today,
-            selected = checked,
+            selected = selected,
             enabled = enabled,
         ).value,
-        border = if (today && !checked) {
+        border = if (today && !selected) {
             BorderStroke(
                 DatePickerModalTokens.DateTodayContainerOutlineWidth,
                 colors.todayDateBorderColor
@@ -1043,10 +1102,7 @@
         }
     ) {
         Box(contentAlignment = Alignment.Center) {
-            Text(
-                text = text,
-                textAlign = TextAlign.Center
-            )
+            content()
         }
     }
 }
@@ -1080,7 +1136,13 @@
         }
         LazyVerticalGrid(
             columns = GridCells.Fixed(YearsInRow),
-            modifier = modifier.background(containerColor),
+            modifier = modifier
+                .background(containerColor)
+                // Apply this to have the screen reader traverse outside the visible list of years
+                // and not scroll them by default.
+                .semantics {
+                    verticalScrollAxisRange = ScrollAxisRange(value = { 0f }, maxValue = { 0f })
+                },
             state = lazyGridState,
             horizontalArrangement = Arrangement.SpaceEvenly,
             verticalArrangement = Arrangement.spacedBy(YearsVerticalPadding)
@@ -1088,21 +1150,27 @@
             items(datePickerState.yearRange.count()) {
                 val selectedYear = it + datePickerState.yearRange.first
                 Year(
-                    checked = selectedYear == displayedYear,
+                    modifier = Modifier
+                        .requiredSize(
+                            width = DatePickerModalTokens.SelectionYearContainerWidth,
+                            height = DatePickerModalTokens.SelectionYearContainerHeight
+                        )
+                        .semantics {
+                            role = Role.Button
+                        },
+                    selected = selectedYear == displayedYear,
                     currentYear = selectedYear == currentYear,
-                    onCheckedChange = { checked ->
-                        if (checked) {
-                            onYearSelected(selectedYear)
-                        }
-                    },
-                    modifier = Modifier.requiredSize(
-                        width = DatePickerModalTokens.SelectionYearContainerWidth,
-                        height = DatePickerModalTokens.SelectionYearContainerHeight
-                    ),
+                    onClick = { onYearSelected(selectedYear) },
                     colors = colors
                 ) {
+                    val localizedYear = selectedYear.toLocalString()
+                    val description =
+                        getString(Strings.DatePickerNavigateToYearDescription).format(localizedYear)
                     Text(
-                        text = selectedYear.toLocalString(),
+                        text = localizedYear,
+                        modifier = Modifier.semantics {
+                            contentDescription = description
+                        },
                         textAlign = TextAlign.Center
                     )
                 }
@@ -1114,15 +1182,15 @@
 @OptIn(ExperimentalMaterial3Api::class)
 @Composable
 private fun Year(
-    checked: Boolean,
-    currentYear: Boolean,
-    onCheckedChange: (Boolean) -> Unit,
     modifier: Modifier,
+    selected: Boolean,
+    currentYear: Boolean,
+    onClick: () -> Unit,
     colors: DatePickerColors,
     content: @Composable () -> Unit
 ) {
-    val border = remember(currentYear, checked) {
-        if (currentYear && !checked) {
+    val border = remember(currentYear, selected) {
+        if (currentYear && !selected) {
             // Use the day's spec to draw a border around the current year.
             BorderStroke(
                 DatePickerModalTokens.DateTodayContainerOutlineWidth,
@@ -1133,14 +1201,14 @@
         }
     }
     Surface(
-        checked = checked,
-        onCheckedChange = onCheckedChange,
+        selected = selected,
+        onClick = onClick,
         modifier = modifier,
         shape = DatePickerModalTokens.SelectionYearStateLayerShape.toShape(),
-        color = colors.yearContainerColor(selected = checked).value,
+        color = colors.yearContainerColor(selected = selected).value,
         contentColor = colors.yearContentColor(
             currentYear = currentYear,
-            selected = checked
+            selected = selected
         ).value,
         border = border
     ) {
@@ -1180,7 +1248,13 @@
             onClick = onYearPickerButtonClicked,
             expanded = yearPickerVisible
         ) {
-            Text(yearPickerText)
+            Text(text = yearPickerText,
+                modifier = Modifier.semantics {
+                    // Make the screen reader read out updates to the menu button text as the user
+                    // navigates the arrows or scrolls to change the displayed month.
+                    liveRegion = LiveRegionMode.Polite
+                    contentDescription = yearPickerText
+                })
         }
         // Show arrows for traversing months (only visible when the year selection is off)
         if (!yearPickerVisible) {
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Strings.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Strings.kt
index b4a1da0..0d58497 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Strings.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Strings.kt
@@ -38,10 +38,14 @@
         val SuggestionsAvailable = Strings(12)
         val DatePickerTitle = Strings(13)
         val DatePickerHeadline = Strings(14)
-        val DatePickerSwitchToYearSelection = Strings(15)
-        val DatePickerSwitchToDaySelection = Strings(16)
-        val DatePickerSwitchToNextMonth = Strings(17)
-        val DatePickerSwitchToPreviousMonth = Strings(18)
+        val DatePickerYearPickerPaneTitle = Strings(15)
+        val DatePickerSwitchToYearSelection = Strings(16)
+        val DatePickerSwitchToDaySelection = Strings(17)
+        val DatePickerSwitchToNextMonth = Strings(18)
+        val DatePickerSwitchToPreviousMonth = Strings(19)
+        val DatePickerNavigateToYearDescription = Strings(20)
+        val DatePickerHeadlineDescription = Strings(21)
+        val DatePickerNoSelectionDescription = Strings(22)
     }
 }
 
diff --git a/compose/material3/material3/src/desktopMain/kotlin/androidx/compose/material/Strings.desktop.kt b/compose/material3/material3/src/desktopMain/kotlin/androidx/compose/material/Strings.desktop.kt
index e2d8ba4..398da25 100644
--- a/compose/material3/material3/src/desktopMain/kotlin/androidx/compose/material/Strings.desktop.kt
+++ b/compose/material3/material3/src/desktopMain/kotlin/androidx/compose/material/Strings.desktop.kt
@@ -35,10 +35,15 @@
         Strings.SuggestionsAvailable -> "Suggestions below"
         Strings.DatePickerTitle -> "Select date"
         Strings.DatePickerHeadline -> "Selected date"
+        Strings.DatePickerYearPickerPaneTitle -> "Year picker visible"
         Strings.DatePickerSwitchToYearSelection -> "Switch to selecting a year"
-        Strings.DatePickerSwitchToDaySelection -> "Switch to selecting a day"
+        Strings.DatePickerSwitchToDaySelection -> "Swipe to select a year, or tap to switch " +
+            "back to selecting a day"
         Strings.DatePickerSwitchToNextMonth -> "Change to next month"
         Strings.DatePickerSwitchToPreviousMonth -> "Change to previous month"
+        Strings.DatePickerNavigateToYearDescription -> "Navigate to year %1$"
+        Strings.DatePickerHeadlineDescription -> "Current selection: %1$"
+        Strings.DatePickerNoSelectionDescription -> "None"
         else -> ""
     }
 }
diff --git a/compose/ui/ui-test-junit4/api/public_plus_experimental_current.txt b/compose/ui/ui-test-junit4/api/public_plus_experimental_current.txt
index f9e2d8a..4d3f223 100644
--- a/compose/ui/ui-test-junit4/api/public_plus_experimental_current.txt
+++ b/compose/ui/ui-test-junit4/api/public_plus_experimental_current.txt
@@ -7,7 +7,7 @@
   }
 
   @androidx.compose.ui.test.ExperimentalTestApi public abstract class AndroidComposeUiTestEnvironment<A extends androidx.activity.ComponentActivity> {
-    ctor public AndroidComposeUiTestEnvironment();
+    ctor public AndroidComposeUiTestEnvironment(optional kotlin.coroutines.CoroutineContext effectContext);
     method protected abstract A? getActivity();
     method public final androidx.compose.ui.test.AndroidComposeUiTest<A> getTest();
     method public final <R> R! runTest(kotlin.jvm.functions.Function1<? super androidx.compose.ui.test.AndroidComposeUiTest<A>,? extends R> block);
@@ -31,14 +31,14 @@
   }
 
   public final class ComposeUiTestKt {
-    method @androidx.compose.ui.test.ExperimentalTestApi public static void runComposeUiTest(kotlin.jvm.functions.Function1<? super androidx.compose.ui.test.ComposeUiTest,? extends kotlin.Unit> block);
+    method @androidx.compose.ui.test.ExperimentalTestApi public static void runComposeUiTest(optional kotlin.coroutines.CoroutineContext effectContext, kotlin.jvm.functions.Function1<? super androidx.compose.ui.test.ComposeUiTest,? extends kotlin.Unit> block);
   }
 
   public final class ComposeUiTest_androidKt {
-    method @androidx.compose.ui.test.ExperimentalTestApi public static inline <A extends androidx.activity.ComponentActivity> androidx.compose.ui.test.AndroidComposeUiTestEnvironment<A> AndroidComposeUiTestEnvironment(kotlin.jvm.functions.Function0<? extends A> activityProvider);
-    method @androidx.compose.ui.test.ExperimentalTestApi public static <A extends androidx.activity.ComponentActivity> void runAndroidComposeUiTest(Class<A> activityClass, kotlin.jvm.functions.Function1<? super androidx.compose.ui.test.AndroidComposeUiTest<A>,kotlin.Unit> block);
-    method @androidx.compose.ui.test.ExperimentalTestApi public static inline <reified A extends androidx.activity.ComponentActivity> void runAndroidComposeUiTest(kotlin.jvm.functions.Function1<? super androidx.compose.ui.test.AndroidComposeUiTest<A>,? extends kotlin.Unit> block);
-    method @androidx.compose.ui.test.ExperimentalTestApi public static void runComposeUiTest(kotlin.jvm.functions.Function1<? super androidx.compose.ui.test.ComposeUiTest,kotlin.Unit> block);
+    method @androidx.compose.ui.test.ExperimentalTestApi public static inline <A extends androidx.activity.ComponentActivity> androidx.compose.ui.test.AndroidComposeUiTestEnvironment<A> AndroidComposeUiTestEnvironment(optional kotlin.coroutines.CoroutineContext effectContext, kotlin.jvm.functions.Function0<? extends A> activityProvider);
+    method @androidx.compose.ui.test.ExperimentalTestApi public static <A extends androidx.activity.ComponentActivity> void runAndroidComposeUiTest(Class<A> activityClass, optional kotlin.coroutines.CoroutineContext effectContext, kotlin.jvm.functions.Function1<? super androidx.compose.ui.test.AndroidComposeUiTest<A>,kotlin.Unit> block);
+    method @androidx.compose.ui.test.ExperimentalTestApi public static inline <reified A extends androidx.activity.ComponentActivity> void runAndroidComposeUiTest(optional kotlin.coroutines.CoroutineContext effectContext, kotlin.jvm.functions.Function1<? super androidx.compose.ui.test.AndroidComposeUiTest<A>,? extends kotlin.Unit> block);
+    method @androidx.compose.ui.test.ExperimentalTestApi public static void runComposeUiTest(kotlin.coroutines.CoroutineContext effectContext, kotlin.jvm.functions.Function1<? super androidx.compose.ui.test.ComposeUiTest,kotlin.Unit> block);
     method @androidx.compose.ui.test.ExperimentalTestApi public static void runEmptyComposeUiTest(kotlin.jvm.functions.Function1<? super androidx.compose.ui.test.ComposeUiTest,kotlin.Unit> block);
   }
 
@@ -54,6 +54,7 @@
 
   public final class AndroidComposeTestRule<R extends org.junit.rules.TestRule, A extends androidx.activity.ComponentActivity> implements androidx.compose.ui.test.junit4.ComposeContentTestRule {
     ctor public AndroidComposeTestRule(R activityRule, kotlin.jvm.functions.Function1<? super R,? extends A> activityProvider);
+    ctor @androidx.compose.ui.test.ExperimentalTestApi public AndroidComposeTestRule(R activityRule, optional kotlin.coroutines.CoroutineContext effectContext, kotlin.jvm.functions.Function1<? super R,? extends A> activityProvider);
     method public org.junit.runners.model.Statement apply(org.junit.runners.model.Statement base, org.junit.runner.Description description);
     method public suspend Object? awaitIdle(kotlin.coroutines.Continuation<? super kotlin.Unit>);
     method public A getActivity();
@@ -82,9 +83,13 @@
 
   public final class AndroidComposeTestRule_androidKt {
     method public static <A extends androidx.activity.ComponentActivity> androidx.compose.ui.test.junit4.AndroidComposeTestRule<androidx.test.ext.junit.rules.ActivityScenarioRule<A>,A> createAndroidComposeRule(Class<A> activityClass);
+    method @androidx.compose.ui.test.ExperimentalTestApi public static <A extends androidx.activity.ComponentActivity> androidx.compose.ui.test.junit4.AndroidComposeTestRule<androidx.test.ext.junit.rules.ActivityScenarioRule<A>,A> createAndroidComposeRule(Class<A> activityClass, optional kotlin.coroutines.CoroutineContext effectContext);
     method public static inline <reified A extends androidx.activity.ComponentActivity> androidx.compose.ui.test.junit4.AndroidComposeTestRule<androidx.test.ext.junit.rules.ActivityScenarioRule<A>,A> createAndroidComposeRule();
+    method @androidx.compose.ui.test.ExperimentalTestApi public static inline <reified A extends androidx.activity.ComponentActivity> androidx.compose.ui.test.junit4.AndroidComposeTestRule<androidx.test.ext.junit.rules.ActivityScenarioRule<A>,A> createAndroidComposeRule(optional kotlin.coroutines.CoroutineContext effectContext);
     method public static androidx.compose.ui.test.junit4.ComposeContentTestRule createComposeRule();
+    method @androidx.compose.ui.test.ExperimentalTestApi public static androidx.compose.ui.test.junit4.ComposeContentTestRule createComposeRule(kotlin.coroutines.CoroutineContext effectContext);
     method public static androidx.compose.ui.test.junit4.ComposeTestRule createEmptyComposeRule();
+    method @androidx.compose.ui.test.ExperimentalTestApi public static androidx.compose.ui.test.junit4.ComposeTestRule createEmptyComposeRule(optional kotlin.coroutines.CoroutineContext effectContext);
   }
 
   public final class AndroidSynchronization_androidKt {
diff --git a/compose/ui/ui-test-junit4/build.gradle b/compose/ui/ui-test-junit4/build.gradle
index d4b8514..5e4501d 100644
--- a/compose/ui/ui-test-junit4/build.gradle
+++ b/compose/ui/ui-test-junit4/build.gradle
@@ -141,6 +141,15 @@
                 implementation(libs.truth)
                 implementation(libs.skiko)
             }
+
+            desktopTest.dependencies {
+                implementation(libs.truth)
+                implementation(libs.junit)
+                implementation(libs.kotlinTest)
+                implementation(libs.skikoCurrentOs)
+                implementation(project(":compose:foundation:foundation"))
+                implementation(project(":compose:ui:ui-test-junit4"))
+            }
         }
     }
 
diff --git a/compose/ui/ui-test-junit4/src/androidAndroidTest/kotlin/androidx/compose/ui/test/ComposeUiTestTest.kt b/compose/ui/ui-test-junit4/src/androidAndroidTest/kotlin/androidx/compose/ui/test/ComposeUiTestTest.kt
index 8d2b530..f820217 100644
--- a/compose/ui/ui-test-junit4/src/androidAndroidTest/kotlin/androidx/compose/ui/test/ComposeUiTestTest.kt
+++ b/compose/ui/ui-test-junit4/src/androidAndroidTest/kotlin/androidx/compose/ui/test/ComposeUiTestTest.kt
@@ -17,11 +17,12 @@
 package androidx.compose.ui.test
 
 import androidx.activity.ComponentActivity
+import androidx.activity.compose.setContent
 import androidx.compose.animation.core.animateFloatAsState
 import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.LocalOverscrollConfiguration
 import androidx.compose.foundation.ScrollState
 import androidx.compose.foundation.gestures.FlingBehavior
-import androidx.compose.foundation.LocalOverscrollConfiguration
 import androidx.compose.foundation.gestures.ScrollScope
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.Column
@@ -41,22 +42,34 @@
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
 import androidx.compose.runtime.setValue
 import androidx.compose.runtime.withFrameNanos
 import androidx.compose.testutils.WithTouchSlop
 import androidx.compose.ui.Modifier
+import androidx.compose.ui.MotionDurationScale
 import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.junit4.createAndroidComposeRule
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.junit4.createEmptyComposeRule
 import androidx.compose.ui.unit.dp
 import androidx.test.espresso.IdlingPolicies
 import androidx.test.espresso.IdlingPolicy
+import androidx.test.ext.junit.rules.ActivityScenarioRule
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.LargeTest
 import com.google.common.truth.Truth.assertThat
+import kotlin.coroutines.CoroutineContext
 import kotlin.math.roundToInt
+import kotlinx.coroutines.CoroutineScope
 import org.junit.After
 import org.junit.Before
+import org.junit.Rule
 import org.junit.Test
+import org.junit.rules.TestWatcher
+import org.junit.runner.Description
 import org.junit.runner.RunWith
+import org.junit.runners.model.Statement
 
 @LargeTest
 @RunWith(AndroidJUnit4::class)
@@ -64,6 +77,18 @@
 class ComposeUiTestTest {
 
     private var idlingPolicy: IdlingPolicy? = null
+    private lateinit var testDescription: Description
+
+    /**
+     * Records the current [testDescription] for tests that need to invoke the compose test rule
+     * directly.
+     */
+    @get:Rule
+    val testWatcher = object : TestWatcher() {
+        override fun starting(description: Description) {
+            testDescription = description
+        }
+    }
 
     @Before
     fun setup() {
@@ -130,7 +155,12 @@
         setContent {
             val offset = animateFloatAsState(target)
             Box(Modifier.fillMaxSize()) {
-                Box(Modifier.size(10.dp).offset(x = offset.value.dp).testTag("box"))
+                Box(
+                    Modifier
+                        .size(10.dp)
+                        .offset(x = offset.value.dp)
+                        .testTag("box")
+                )
             }
         }
         onNodeWithTag("box").assertLeftPositionInRootIsEqualTo(0.dp)
@@ -158,13 +188,20 @@
                 CompositionLocalProvider(LocalOverscrollConfiguration provides null) {
                     Box(Modifier.fillMaxSize()) {
                         Column(
-                            Modifier.requiredSize(200.dp).verticalScroll(
-                                scrollState,
-                                flingBehavior = flingBehavior
-                            ).testTag("list")
+                            Modifier
+                                .requiredSize(200.dp)
+                                .verticalScroll(
+                                    scrollState,
+                                    flingBehavior = flingBehavior
+                                )
+                                .testTag("list")
                         ) {
                             repeat(n) {
-                                Spacer(Modifier.fillMaxWidth().height(30.dp))
+                                Spacer(
+                                    Modifier
+                                        .fillMaxWidth()
+                                        .height(30.dp)
+                                )
                             }
                         }
                     }
@@ -217,4 +254,124 @@
     fun getActivityTest() = runAndroidComposeUiTest<ComponentActivity> {
         assertThat(activity).isNotNull()
     }
+
+    @Test
+    fun effectContextPropagatedToComposition_runComposeUiTest() {
+        val testElement = TestCoroutineContextElement()
+        runComposeUiTest(effectContext = testElement) {
+            lateinit var compositionScope: CoroutineScope
+            setContent {
+                compositionScope = rememberCoroutineScope()
+            }
+
+            runOnIdle {
+                val elementFromComposition =
+                    compositionScope.coroutineContext[TestCoroutineContextElement]
+                assertThat(elementFromComposition).isSameInstanceAs(testElement)
+            }
+        }
+    }
+
+    @Test
+    fun effectContextPropagatedToComposition_createComposeRule() {
+        val testElement = TestCoroutineContextElement()
+        lateinit var compositionScope: CoroutineScope
+        val rule = createComposeRule(testElement)
+        val baseStatement = object : Statement() {
+            override fun evaluate() {
+                rule.setContent {
+                    compositionScope = rememberCoroutineScope()
+                }
+                rule.waitForIdle()
+            }
+        }
+        rule.apply(baseStatement, testDescription)
+            .evaluate()
+
+        val elementFromComposition =
+            compositionScope.coroutineContext[TestCoroutineContextElement]
+        assertThat(elementFromComposition).isSameInstanceAs(testElement)
+    }
+
+    @Test
+    fun effectContextPropagatedToComposition_createAndroidComposeRule() {
+        val testElement = TestCoroutineContextElement()
+        lateinit var compositionScope: CoroutineScope
+        val rule = createAndroidComposeRule<ComponentActivity>(testElement)
+        val baseStatement = object : Statement() {
+            override fun evaluate() {
+                rule.setContent {
+                    compositionScope = rememberCoroutineScope()
+                }
+                rule.waitForIdle()
+            }
+        }
+        rule.apply(baseStatement, testDescription)
+            .evaluate()
+
+        val elementFromComposition =
+            compositionScope.coroutineContext[TestCoroutineContextElement]
+        assertThat(elementFromComposition).isSameInstanceAs(testElement)
+    }
+
+    @Test
+    fun effectContextPropagatedToComposition_createEmptyComposeRule() {
+        val testElement = TestCoroutineContextElement()
+        lateinit var compositionScope: CoroutineScope
+        val composeRule = createEmptyComposeRule(testElement)
+        val activityRule = ActivityScenarioRule(ComponentActivity::class.java)
+        val baseStatement = object : Statement() {
+            override fun evaluate() {
+                activityRule.scenario.onActivity {
+                    it.setContent {
+                        compositionScope = rememberCoroutineScope()
+                    }
+                }
+                composeRule.waitForIdle()
+            }
+        }
+        activityRule.apply(composeRule.apply(baseStatement, testDescription), testDescription)
+            .evaluate()
+
+        val elementFromComposition =
+            compositionScope.coroutineContext[TestCoroutineContextElement]
+        assertThat(elementFromComposition).isSameInstanceAs(testElement)
+    }
+
+    @Test
+    fun motionDurationScale_defaultValue() = runComposeUiTest {
+        var lastRecordedMotionDurationScale: Float? = null
+        setContent {
+            val context = rememberCoroutineScope().coroutineContext
+            lastRecordedMotionDurationScale = context[MotionDurationScale]?.scaleFactor
+        }
+
+        runOnIdle {
+            assertThat(lastRecordedMotionDurationScale).isNull()
+        }
+    }
+
+    @Test
+    fun motionDurationScale_propagatedToCoroutines() {
+        val motionDurationScale = object : MotionDurationScale {
+            override val scaleFactor: Float get() = 0f
+        }
+        runComposeUiTest(effectContext = motionDurationScale) {
+            var lastRecordedMotionDurationScale: Float? = null
+            setContent {
+                val context = rememberCoroutineScope().coroutineContext
+                lastRecordedMotionDurationScale = context[MotionDurationScale]?.scaleFactor
+            }
+
+            runOnIdle {
+                assertThat(lastRecordedMotionDurationScale).isEqualTo(0f)
+            }
+        }
+    }
+
+    private class TestCoroutineContextElement : CoroutineContext.Element {
+        override val key: CoroutineContext.Key<*> get() = Key
+
+        companion object Key : CoroutineContext.Key<TestCoroutineContextElement>
+    }
 }
diff --git a/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/ComposeUiTest.android.kt b/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/ComposeUiTest.android.kt
index 5453449..7b88012 100644
--- a/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/ComposeUiTest.android.kt
+++ b/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/ComposeUiTest.android.kt
@@ -48,6 +48,8 @@
 import androidx.compose.ui.unit.Density
 import androidx.test.core.app.ActivityScenario
 import androidx.test.core.app.ApplicationProvider
+import kotlin.coroutines.CoroutineContext
+import kotlin.coroutines.EmptyCoroutineContext
 import kotlinx.coroutines.CancellationException
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -59,8 +61,8 @@
 import kotlinx.coroutines.test.runTest
 
 @ExperimentalTestApi
-actual fun runComposeUiTest(block: ComposeUiTest.() -> Unit) {
-    runAndroidComposeUiTest(ComponentActivity::class.java, block)
+actual fun runComposeUiTest(effectContext: CoroutineContext, block: ComposeUiTest.() -> Unit) {
+    runAndroidComposeUiTest(ComponentActivity::class.java, effectContext, block)
 }
 
 /**
@@ -71,12 +73,16 @@
  *
  * @param A The Activity type to be launched, which typically (but not necessarily) hosts the
  * Compose content
+ * @param effectContext The [CoroutineContext] used to run the composition. The context for
+ * `LaunchedEffect`s and `rememberCoroutineScope` will be derived from this context.
+ * @param block The test function.
  */
 @ExperimentalTestApi
 inline fun <reified A : ComponentActivity> runAndroidComposeUiTest(
+    effectContext: CoroutineContext = EmptyCoroutineContext,
     noinline block: AndroidComposeUiTest<A>.() -> Unit
 ) {
-    runAndroidComposeUiTest(A::class.java, block)
+    runAndroidComposeUiTest(A::class.java, effectContext, block)
 }
 
 /**
@@ -87,16 +93,21 @@
  *
  * @param A The Activity type to be launched, which typically (but not necessarily) hosts the
  * Compose content
+ * @param activityClass The [Class] of the Activity type to be launched, corresponding to [A].
+ * @param effectContext The [CoroutineContext] used to run the composition. The context for
+ * `LaunchedEffect`s and `rememberCoroutineScope` will be derived from this context.
+ * @param block The test function.
  */
 @ExperimentalTestApi
 fun <A : ComponentActivity> runAndroidComposeUiTest(
     activityClass: Class<A>,
+    effectContext: CoroutineContext = EmptyCoroutineContext,
     block: AndroidComposeUiTest<A>.() -> Unit
 ) {
     // Don't start the scenario now, wait until we're inside runTest { },
     // in case the Activity's onCreate/Start/Resume calls setContent
     var scenario: ActivityScenario<A>? = null
-    val environment = AndroidComposeUiTestEnvironment {
+    val environment = AndroidComposeUiTestEnvironment(effectContext) {
         requireNotNull(scenario) {
             "ActivityScenario has not yet been launched, or has already finished. Make sure that " +
                 "any call to ComposeUiTest.setContent() and AndroidComposeUiTest.getActivity() " +
@@ -195,13 +206,16 @@
  * @param activityProvider A lambda that should return the current Activity instance of type [A],
  * if it is available. If it is not available, it should return `null`.
  * @param A The Activity type to be interacted with, which typically (but not necessarily) is the
- * activity that was launched and hosts the Compose content
+ * activity that was launched and hosts the Compose content.
+ * @param effectContext The [CoroutineContext] used to run the composition. The context for
+ * `LaunchedEffect`s and `rememberCoroutineScope` will be derived from this context.
  */
 @ExperimentalTestApi
 inline fun <A : ComponentActivity> AndroidComposeUiTestEnvironment(
+    effectContext: CoroutineContext = EmptyCoroutineContext,
     crossinline activityProvider: () -> A?
 ): AndroidComposeUiTestEnvironment<A> {
-    return object : AndroidComposeUiTestEnvironment<A>() {
+    return object : AndroidComposeUiTestEnvironment<A>(effectContext) {
         override val activity: A?
             get() = activityProvider.invoke()
     }
@@ -213,11 +227,15 @@
  * as they require that the environment has been set up.
  *
  * @param A The Activity type to be interacted with, which typically (but not necessarily) is the
- * activity that was launched and hosts the Compose content
+ * activity that was launched and hosts the Compose content.
+ * @param effectContext The [CoroutineContext] used to run the composition. The context for
+ * `LaunchedEffect`s and `rememberCoroutineScope` will be derived from this context.
  */
 @ExperimentalTestApi
 @OptIn(InternalTestApi::class, ExperimentalCoroutinesApi::class, ExperimentalComposeUiApi::class)
-abstract class AndroidComposeUiTestEnvironment<A : ComponentActivity> {
+abstract class AndroidComposeUiTestEnvironment<A : ComponentActivity>(
+    effectContext: CoroutineContext = EmptyCoroutineContext
+) {
     private val idlingResourceRegistry = IdlingResourceRegistry()
 
     internal val composeRootRegistry = ComposeRootRegistry()
@@ -259,8 +277,12 @@
             }
         }
         recomposerCoroutineScope = CoroutineScope(
-            recomposerContinuationInterceptor + frameClock + infiniteAnimationPolicy +
-                coroutineExceptionHandler + Job()
+            effectContext +
+                recomposerContinuationInterceptor +
+                frameClock +
+                infiniteAnimationPolicy +
+                coroutineExceptionHandler +
+                Job()
         )
         recomposer = Recomposer(recomposerCoroutineScope.coroutineContext)
         composeIdlingResource = ComposeIdlingResource(
diff --git a/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/AndroidComposeTestRule.android.kt b/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/AndroidComposeTestRule.android.kt
index 887eb84..ec2ea21 100644
--- a/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/AndroidComposeTestRule.android.kt
+++ b/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/AndroidComposeTestRule.android.kt
@@ -27,6 +27,8 @@
 import androidx.compose.ui.test.SemanticsNodeInteractionCollection
 import androidx.compose.ui.unit.Density
 import androidx.test.ext.junit.rules.ActivityScenarioRule
+import kotlin.coroutines.CoroutineContext
+import kotlin.coroutines.EmptyCoroutineContext
 import org.junit.rules.TestRule
 import org.junit.runner.Description
 import org.junit.runners.model.Statement
@@ -34,6 +36,10 @@
 actual fun createComposeRule(): ComposeContentTestRule =
     createAndroidComposeRule<ComponentActivity>()
 
+@ExperimentalTestApi
+actual fun createComposeRule(effectContext: CoroutineContext): ComposeContentTestRule =
+    createAndroidComposeRule<ComponentActivity>(effectContext)
+
 /**
  * Factory method to provide android specific implementation of [createComposeRule], for a given
  * activity class type [A].
@@ -51,12 +57,38 @@
  */
 inline fun <reified A : ComponentActivity> createAndroidComposeRule():
     AndroidComposeTestRule<ActivityScenarioRule<A>, A> {
-        // TODO(b/138993381): By launching custom activities we are losing control over what content is
-        //  already there. This is issue in case the user already set some compose content and decides
-        //  to set it again via our API. In such case we won't be able to dispose the old composition.
-        //  Other option would be to provide a smaller interface that does not expose these methods.
-        return createAndroidComposeRule(A::class.java)
-    }
+    // TODO(b/138993381): By launching custom activities we are losing control over what content is
+    //  already there. This is issue in case the user already set some compose content and decides
+    //  to set it again via our API. In such case we won't be able to dispose the old composition.
+    //  Other option would be to provide a smaller interface that does not expose these methods.
+    return createAndroidComposeRule(A::class.java)
+}
+
+/**
+ * Factory method to provide android specific implementation of [createComposeRule], for a given
+ * activity class type [A].
+ *
+ * This method is useful for tests that require a custom Activity. This is usually the case for
+ * tests where the compose content is set by that Activity, instead of via the test rule's
+ * [setContent][ComposeContentTestRule.setContent]. Make sure that you add the provided activity
+ * into your app's manifest file (usually in main/AndroidManifest.xml).
+ *
+ * This creates a test rule that is using [ActivityScenarioRule] as the activity launcher. If you
+ * would like to use a different one you can create [AndroidComposeTestRule] directly and supply
+ * it with your own launcher.
+ *
+ * If your test doesn't require a specific Activity, use [createComposeRule] instead.
+ */
+@ExperimentalTestApi
+inline fun <reified A : ComponentActivity> createAndroidComposeRule(
+    effectContext: CoroutineContext = EmptyCoroutineContext
+): AndroidComposeTestRule<ActivityScenarioRule<A>, A> {
+    // TODO(b/138993381): By launching custom activities we are losing control over what content is
+    //  already there. This is issue in case the user already set some compose content and decides
+    //  to set it again via our API. In such case we won't be able to dispose the old composition.
+    //  Other option would be to provide a smaller interface that does not expose these methods.
+    return createAndroidComposeRule(A::class.java, effectContext)
+}
 
 /**
  * Factory method to provide android specific implementation of [createComposeRule], for a given
@@ -81,6 +113,31 @@
 )
 
 /**
+ * Factory method to provide android specific implementation of [createComposeRule], for a given
+ * [activityClass].
+ *
+ * This method is useful for tests that require a custom Activity. This is usually the case for
+ * tests where the compose content is set by that Activity, instead of via the test rule's
+ * [setContent][ComposeContentTestRule.setContent]. Make sure that you add the provided activity
+ * into your app's manifest file (usually in main/AndroidManifest.xml).
+ *
+ * This creates a test rule that is using [ActivityScenarioRule] as the activity launcher. If you
+ * would like to use a different one you can create [AndroidComposeTestRule] directly and supply
+ * it with your own launcher.
+ *
+ * If your test doesn't require a specific Activity, use [createComposeRule] instead.
+ */
+@ExperimentalTestApi
+fun <A : ComponentActivity> createAndroidComposeRule(
+    activityClass: Class<A>,
+    effectContext: CoroutineContext = EmptyCoroutineContext
+): AndroidComposeTestRule<ActivityScenarioRule<A>, A> = AndroidComposeTestRule(
+    activityRule = ActivityScenarioRule(activityClass),
+    activityProvider = ::getActivityFromTestRule,
+    effectContext = effectContext
+)
+
+/**
  * Factory method to provide an implementation of [ComposeTestRule] that doesn't create a compose
  * host for you in which you can set content.
  *
@@ -103,6 +160,35 @@
         }
     )
 
+/**
+ * Factory method to provide an implementation of [ComposeTestRule] that doesn't create a compose
+ * host for you in which you can set content.
+ *
+ * This method is useful for tests that need to create their own compose host during the test.
+ * The returned test rule will not create a host, and consequently does not provide a
+ * `setContent` method. To set content in tests using this rule, use the appropriate `setContent`
+ * methods from your compose host.
+ *
+ * A typical use case on Android is when the test needs to launch an Activity (the compose host)
+ * after one or more dependencies have been injected.
+ *
+ * @param effectContext The [CoroutineContext] used to run the composition. The context for
+ * `LaunchedEffect`s and `rememberCoroutineScope` will be derived from this context.
+ */
+@ExperimentalTestApi
+fun createEmptyComposeRule(
+    effectContext: CoroutineContext = EmptyCoroutineContext
+): ComposeTestRule = AndroidComposeTestRule<TestRule, ComponentActivity>(
+    activityRule = TestRule { base, _ -> base },
+    effectContext = effectContext,
+    activityProvider = {
+        error(
+            "createEmptyComposeRule() does not provide an Activity to set Compose content in." +
+                " Launch and use the Activity yourself, or use createAndroidComposeRule()."
+        )
+    }
+)
+
 @OptIn(ExperimentalTestApi::class)
 class AndroidComposeTestRule<R : TestRule, A : ComponentActivity> private constructor(
     val activityRule: R,
@@ -128,9 +214,43 @@
      * @param activityRule Test rule to use to launch the Activity.
      * @param activityProvider Function to retrieve the Activity from the given [activityRule].
      */
-    constructor(activityRule: R, activityProvider: (R) -> A) : this(
+    constructor(
+        activityRule: R,
+        activityProvider: (R) -> A
+    ) : this(
+        activityRule = activityRule,
+        effectContext = EmptyCoroutineContext,
+        activityProvider = activityProvider,
+    )
+
+    /**
+     * Android specific implementation of [ComposeContentTestRule], where compose content is hosted
+     * by an Activity.
+     *
+     * The Activity is normally launched by the given [activityRule] before the test starts, but it
+     * is possible to pass a test rule that chooses to launch an Activity on a later time. The
+     * Activity is retrieved from the [activityRule] by means of the [activityProvider], which can be
+     * thought of as a getter for the Activity on the [activityRule]. If you use an [activityRule]
+     * that launches an Activity on a later time, you should make sure that the Activity is launched
+     * by the time or while the [activityProvider] is called.
+     *
+     * The [AndroidComposeTestRule] wraps around the given [activityRule] to make sure the Activity
+     * is launched _after_ the [AndroidComposeTestRule] has completed all necessary steps to control
+     * and monitor the compose content.
+     *
+     * @param activityRule Test rule to use to launch the Activity.
+     * @param effectContext The [CoroutineContext] used to run the composition. The context for
+     * `LaunchedEffect`s and `rememberCoroutineScope` will be derived from this context.
+     * @param activityProvider Function to retrieve the Activity from the given [activityRule].
+     */
+    @ExperimentalTestApi
+    constructor(
+        activityRule: R,
+        effectContext: CoroutineContext = EmptyCoroutineContext,
+        activityProvider: (R) -> A,
+    ) : this(
         activityRule,
-        AndroidComposeUiTestEnvironment { activityProvider(activityRule) }
+        AndroidComposeUiTestEnvironment(effectContext) { activityProvider(activityRule) },
     )
 
     /**
diff --git a/compose/ui/ui-test-junit4/src/commonMain/kotlin/androidx/compose/ui/test/ComposeUiTest.kt b/compose/ui/ui-test-junit4/src/commonMain/kotlin/androidx/compose/ui/test/ComposeUiTest.kt
index 7fd77b0..e7900ea 100644
--- a/compose/ui/ui-test-junit4/src/commonMain/kotlin/androidx/compose/ui/test/ComposeUiTest.kt
+++ b/compose/ui/ui-test-junit4/src/commonMain/kotlin/androidx/compose/ui/test/ComposeUiTest.kt
@@ -18,6 +18,8 @@
 
 import androidx.compose.runtime.Composable
 import androidx.compose.ui.unit.Density
+import kotlin.coroutines.CoroutineContext
+import kotlin.coroutines.EmptyCoroutineContext
 
 /**
  * Sets up the test environment, runs the given [test][block] and then tears down the test
@@ -34,9 +36,16 @@
  * launch the host from within the test lambda as well depends on the platform.
  *
  * Keeping a reference to the [ComposeUiTest] outside of this function is an error.
+ *
+ * @param effectContext The [CoroutineContext] used to run the composition. The context for
+ * `LaunchedEffect`s and `rememberCoroutineScope` will be derived from this context.
+ * @param block The test function.
  */
 @ExperimentalTestApi
-expect fun runComposeUiTest(block: ComposeUiTest.() -> Unit)
+expect fun runComposeUiTest(
+    effectContext: CoroutineContext = EmptyCoroutineContext,
+    block: ComposeUiTest.() -> Unit
+)
 
 /**
  * A test environment that allows you to test and control composables, either in isolation or in
diff --git a/compose/ui/ui-test-junit4/src/desktopMain/kotlin/androidx/compose/ui/test/ComposeUiTest.desktop.kt b/compose/ui/ui-test-junit4/src/desktopMain/kotlin/androidx/compose/ui/test/ComposeUiTest.desktop.kt
index 84471eb..159e7d1 100644
--- a/compose/ui/ui-test-junit4/src/desktopMain/kotlin/androidx/compose/ui/test/ComposeUiTest.desktop.kt
+++ b/compose/ui/ui-test-junit4/src/desktopMain/kotlin/androidx/compose/ui/test/ComposeUiTest.desktop.kt
@@ -30,6 +30,8 @@
 import androidx.compose.ui.text.input.ImeAction
 import androidx.compose.ui.unit.Constraints
 import androidx.compose.ui.unit.Density
+import kotlin.coroutines.CoroutineContext
+import kotlin.coroutines.EmptyCoroutineContext
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.test.TestScope
 import kotlinx.coroutines.test.UnconfinedTestDispatcher
@@ -40,14 +42,20 @@
 
 @ExperimentalTestApi
 @OptIn(InternalTestApi::class)
-actual fun runComposeUiTest(block: ComposeUiTest.() -> Unit) {
-    DesktopComposeUiTest().runTest(block)
+actual fun runComposeUiTest(effectContext: CoroutineContext, block: ComposeUiTest.() -> Unit) {
+    DesktopComposeUiTest(effectContext).runTest(block)
 }
 
+/**
+ * @param effectContext The [CoroutineContext] used to run the composition. The context for
+ * `LaunchedEffect`s and `rememberCoroutineScope` will be derived from this context.
+ */
 @InternalTestApi
 @ExperimentalTestApi
 @OptIn(ExperimentalComposeUiApi::class, ExperimentalCoroutinesApi::class)
-class DesktopComposeUiTest : ComposeUiTest {
+class DesktopComposeUiTest(
+    effectContext: CoroutineContext = EmptyCoroutineContext
+) : ComposeUiTest {
 
     override val density = Density(1f, 1f)
 
@@ -65,7 +73,11 @@
         }
     }
     private val coroutineContext =
-        coroutineDispatcher + uncaughtExceptionHandler + infiniteAnimationPolicy
+        effectContext +
+            coroutineDispatcher +
+            uncaughtExceptionHandler +
+            infiniteAnimationPolicy
+
     private val surface = Surface.makeRasterN32Premul(1024, 768)
 
     lateinit var scene: ComposeScene
diff --git a/compose/ui/ui-test-junit4/src/desktopMain/kotlin/androidx/compose/ui/test/junit4/DesktopComposeTestRule.desktop.kt b/compose/ui/ui-test-junit4/src/desktopMain/kotlin/androidx/compose/ui/test/junit4/DesktopComposeTestRule.desktop.kt
index e9933a0..f9ca8ad 100644
--- a/compose/ui/ui-test-junit4/src/desktopMain/kotlin/androidx/compose/ui/test/junit4/DesktopComposeTestRule.desktop.kt
+++ b/compose/ui/ui-test-junit4/src/desktopMain/kotlin/androidx/compose/ui/test/junit4/DesktopComposeTestRule.desktop.kt
@@ -27,12 +27,19 @@
 import androidx.compose.ui.test.SemanticsNodeInteraction
 import androidx.compose.ui.test.SemanticsNodeInteractionCollection
 import androidx.compose.ui.unit.Density
+import kotlin.coroutines.CoroutineContext
+import kotlin.coroutines.EmptyCoroutineContext
 import org.junit.runner.Description
 import org.junit.runners.model.Statement
 
 @OptIn(InternalTestApi::class)
 actual fun createComposeRule(): ComposeContentTestRule = DesktopComposeTestRule()
 
+@ExperimentalTestApi
+@OptIn(InternalTestApi::class)
+actual fun createComposeRule(effectContext: CoroutineContext): ComposeContentTestRule =
+    DesktopComposeTestRule(effectContext)
+
 @InternalTestApi
 @OptIn(ExperimentalTestApi::class)
 class DesktopComposeTestRule private constructor(
@@ -41,6 +48,11 @@
 
     constructor() : this(DesktopComposeUiTest())
 
+    @ExperimentalTestApi
+    constructor(
+        effectContext: CoroutineContext = EmptyCoroutineContext
+    ) : this(DesktopComposeUiTest(effectContext))
+
     var scene: ComposeScene
         get() = composeTest.scene
         set(value) {
diff --git a/compose/ui/ui-test-junit4/src/desktopTest/kotlin/androidx/compose/ui/test/ComposeUiTestTest.kt b/compose/ui/ui-test-junit4/src/desktopTest/kotlin/androidx/compose/ui/test/ComposeUiTestTest.kt
new file mode 100644
index 0000000..aef2e84
--- /dev/null
+++ b/compose/ui/ui-test-junit4/src/desktopTest/kotlin/androidx/compose/ui/test/ComposeUiTestTest.kt
@@ -0,0 +1,124 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.ui.test
+
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.ui.MotionDurationScale
+import androidx.compose.ui.test.junit4.createComposeRule
+import com.google.common.truth.Truth.assertThat
+import kotlin.coroutines.CoroutineContext
+import kotlinx.coroutines.CoroutineScope
+import org.junit.Rule
+import org.junit.Test
+import org.junit.rules.TestWatcher
+import org.junit.runner.Description
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import org.junit.runners.model.Statement
+
+@RunWith(JUnit4::class)
+@OptIn(ExperimentalTestApi::class)
+class ComposeUiTestTest {
+
+    private lateinit var testDescription: Description
+
+    /**
+     * Records the current [testDescription] for tests that need to invoke the compose test rule
+     * directly.
+     */
+    @get:Rule
+    val testWatcher = object : TestWatcher() {
+        override fun starting(description: Description) {
+            testDescription = description
+        }
+    }
+
+    @Test
+    fun effectContextPropagatedToComposition_runComposeUiTest() {
+        val testElement = TestCoroutineContextElement()
+        runComposeUiTest(effectContext = testElement) {
+            lateinit var compositionScope: CoroutineScope
+            setContent {
+                compositionScope = rememberCoroutineScope()
+            }
+
+            runOnIdle {
+                val elementFromComposition =
+                    compositionScope.coroutineContext[TestCoroutineContextElement]
+                assertThat(elementFromComposition).isSameInstanceAs(testElement)
+            }
+        }
+    }
+
+    @Test
+    fun effectContextPropagatedToComposition_createComposeRule() {
+        val testElement = TestCoroutineContextElement()
+        lateinit var compositionScope: CoroutineScope
+        val rule = createComposeRule(testElement)
+        val baseStatement = object : Statement() {
+            override fun evaluate() {
+                rule.setContent {
+                    compositionScope = rememberCoroutineScope()
+                }
+                rule.waitForIdle()
+            }
+        }
+        rule.apply(baseStatement, testDescription)
+            .evaluate()
+
+        val elementFromComposition =
+            compositionScope.coroutineContext[TestCoroutineContextElement]
+        assertThat(elementFromComposition).isSameInstanceAs(testElement)
+    }
+
+    @Test
+    fun motionDurationScale_defaultValue() = runComposeUiTest {
+        var lastRecordedMotionDurationScale: Float? = null
+        setContent {
+            val context = rememberCoroutineScope().coroutineContext
+            lastRecordedMotionDurationScale = context[MotionDurationScale]?.scaleFactor
+        }
+
+        runOnIdle {
+            assertThat(lastRecordedMotionDurationScale).isNull()
+        }
+    }
+
+    @Test
+    fun motionDurationScale_propagatedToCoroutines() {
+        val motionDurationScale = object : MotionDurationScale {
+            override val scaleFactor: Float get() = 0f
+        }
+        runComposeUiTest(effectContext = motionDurationScale) {
+            var lastRecordedMotionDurationScale: Float? = null
+            setContent {
+                val context = rememberCoroutineScope().coroutineContext
+                lastRecordedMotionDurationScale = context[MotionDurationScale]?.scaleFactor
+            }
+
+            runOnIdle {
+                assertThat(lastRecordedMotionDurationScale).isEqualTo(0f)
+            }
+        }
+    }
+
+    private class TestCoroutineContextElement : CoroutineContext.Element {
+        override val key: CoroutineContext.Key<*> get() = Key
+
+        companion object Key : CoroutineContext.Key<TestCoroutineContextElement>
+    }
+}
\ No newline at end of file
diff --git a/compose/ui/ui-test-junit4/src/jvmMain/kotlin/androidx/compose/ui/test/junit4/ComposeTestRule.jvm.kt b/compose/ui/ui-test-junit4/src/jvmMain/kotlin/androidx/compose/ui/test/junit4/ComposeTestRule.jvm.kt
index 54e9aa7..b0d353a 100644
--- a/compose/ui/ui-test-junit4/src/jvmMain/kotlin/androidx/compose/ui/test/junit4/ComposeTestRule.jvm.kt
+++ b/compose/ui/ui-test-junit4/src/jvmMain/kotlin/androidx/compose/ui/test/junit4/ComposeTestRule.jvm.kt
@@ -18,12 +18,14 @@
 
 import androidx.compose.runtime.Composable
 import androidx.compose.ui.test.ComposeTimeoutException
+import androidx.compose.ui.test.ExperimentalTestApi
 import androidx.compose.ui.test.IdlingResource
 import androidx.compose.ui.test.MainTestClock
 import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
 import androidx.compose.ui.unit.Density
+import kotlin.coroutines.CoroutineContext
+import kotlin.coroutines.EmptyCoroutineContext
 import org.junit.rules.TestRule
-import kotlin.jvm.JvmDefaultWithCompatibility
 
 /**
  * A [TestRule] that allows you to test and control composables and applications using Compose.
@@ -179,3 +181,24 @@
  * launched, see [createAndroidComposeRule].
  */
 expect fun createComposeRule(): ComposeContentTestRule
+
+/**
+ * Factory method to provide an implementation of [ComposeContentTestRule].
+ *
+ * This method is useful for tests in compose libraries where it is irrelevant where the compose
+ * content is hosted (e.g. an Activity on Android). Such tests typically set compose content
+ * themselves via [setContent][ComposeContentTestRule.setContent] and only instrument and assert
+ * that content.
+ *
+ * For Android this will use the default Activity (android.app.Activity). You need to add a
+ * reference to this activity into the manifest file of the corresponding tests (usually in
+ * androidTest/AndroidManifest.xml). If your Android test requires a specific Activity to be
+ * launched, see [createAndroidComposeRule].
+ *
+ * @param effectContext The [CoroutineContext] used to run the composition. The context for
+ * `LaunchedEffect`s and `rememberCoroutineScope` will be derived from this context.
+ */
+@ExperimentalTestApi
+expect fun createComposeRule(
+    effectContext: CoroutineContext = EmptyCoroutineContext
+): ComposeContentTestRule
diff --git a/compose/ui/ui-text/api/current.txt b/compose/ui/ui-text/api/current.txt
index df13e31..c265249 100644
--- a/compose/ui/ui-text/api/current.txt
+++ b/compose/ui/ui-text/api/current.txt
@@ -307,7 +307,6 @@
     method public int getEmojiSupportMatch();
     method @Deprecated public boolean getIncludeFontPadding();
     method public androidx.compose.ui.text.PlatformParagraphStyle merge(androidx.compose.ui.text.PlatformParagraphStyle? other);
-    method public void setEmojiSupportMatch(int);
     property public final int emojiSupportMatch;
     property @Deprecated public final boolean includeFontPadding;
     field public static final androidx.compose.ui.text.PlatformParagraphStyle.Companion Companion;
diff --git a/compose/ui/ui-text/api/public_plus_experimental_current.txt b/compose/ui/ui-text/api/public_plus_experimental_current.txt
index b27caf4..04f88e9 100644
--- a/compose/ui/ui-text/api/public_plus_experimental_current.txt
+++ b/compose/ui/ui-text/api/public_plus_experimental_current.txt
@@ -328,7 +328,6 @@
     method public int getEmojiSupportMatch();
     method @Deprecated public boolean getIncludeFontPadding();
     method public androidx.compose.ui.text.PlatformParagraphStyle merge(androidx.compose.ui.text.PlatformParagraphStyle? other);
-    method public void setEmojiSupportMatch(int);
     property public final int emojiSupportMatch;
     property @Deprecated public final boolean includeFontPadding;
     field public static final androidx.compose.ui.text.PlatformParagraphStyle.Companion Companion;
diff --git a/compose/ui/ui-text/api/restricted_current.txt b/compose/ui/ui-text/api/restricted_current.txt
index df13e31..c265249 100644
--- a/compose/ui/ui-text/api/restricted_current.txt
+++ b/compose/ui/ui-text/api/restricted_current.txt
@@ -307,7 +307,6 @@
     method public int getEmojiSupportMatch();
     method @Deprecated public boolean getIncludeFontPadding();
     method public androidx.compose.ui.text.PlatformParagraphStyle merge(androidx.compose.ui.text.PlatformParagraphStyle? other);
-    method public void setEmojiSupportMatch(int);
     property public final int emojiSupportMatch;
     property @Deprecated public final boolean includeFontPadding;
     field public static final androidx.compose.ui.text.PlatformParagraphStyle.Companion Companion;
diff --git a/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/AndroidTextStyle.android.kt b/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/AndroidTextStyle.android.kt
index 9e35c4f..88efda3 100644
--- a/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/AndroidTextStyle.android.kt
+++ b/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/AndroidTextStyle.android.kt
@@ -32,6 +32,12 @@
      */
     actual val paragraphStyle: PlatformParagraphStyle?
 
+    /**
+     * Convenience constructor for when you already have a [spanStyle] and [paragraphStyle].
+     *
+     * @param spanStyle platform specific span styling
+     * @param paragraphStyle platform specific paragraph styling
+     */
     constructor(
         spanStyle: PlatformSpanStyle?,
         paragraphStyle: PlatformParagraphStyle?
@@ -70,7 +76,11 @@
     )
 
     /**
-     * Set EmojiMatchSupport on [PlatformParagraphStyle]
+     * [EmojiSupportMatch] allows you to control emoji support replacement behavior.
+     *
+     * You can disable emoji support matches by passing [EmojiSupportMatch.None]
+     *
+     * @param emojiSupportMatch configuration for emoji support match and replacement
      */
     constructor(
         emojiSupportMatch: EmojiSupportMatch
@@ -118,17 +128,46 @@
             PlatformParagraphStyle()
     }
 
+    /**
+     * Include extra space beyond font ascent and descent.
+     *
+     * Enables turning on and off for Android [includeFontPadding](https://developer.android.com/reference/android/text/StaticLayout.Builder#setIncludePad(boolean)).
+     *
+     * includeFontPadding was added to Android in order to prevent clipping issues on tall scripts.
+     * However that issue has been fixed since Android 28. Jetpack Compose backports the fix for
+     * Android versions prior to Android 28. Therefore the original reason why includeFontPadding
+     * was needed in invalid on Compose.
+     *
+     * This configuration was added for migration of the apps in case some code or design  was
+     * relying includeFontPadding=true behavior and will be removed.
+     */
     @Deprecated("Sets includeFontPadding parameter for transitioning. Will be removed.")
     val includeFontPadding: Boolean
 
-    var emojiSupportMatch: EmojiSupportMatch
+    /**
+     * When to replace emoji with support emoji using androidx.emoji2.
+     *
+     * This is only available on Android.
+     */
+    val emojiSupportMatch: EmojiSupportMatch
 
+    /**
+     * Represents platform specific text flags
+     *
+     * @param includeFontPadding Set whether to include extra space beyond font ascent and descent.
+     */
     @Deprecated("Provides configuration options for behavior compatibility.")
     constructor(includeFontPadding: Boolean = DefaultIncludeFontPadding) {
         this.includeFontPadding = includeFontPadding
         this.emojiSupportMatch = EmojiSupportMatch.Default
     }
 
+    /**
+     * Represents platform specific text flags
+     *
+     * @param emojiSupportMatch control emoji support matches on Android
+     * @param includeFontPadding Set whether to include extra space beyond font ascent and descent.
+     */
     @Deprecated("Provides configuration options for behavior compatibility.")
     constructor(
         emojiSupportMatch: EmojiSupportMatch = EmojiSupportMatch.Default,
@@ -138,11 +177,19 @@
         this.emojiSupportMatch = emojiSupportMatch
     }
 
+    /**
+     * Represents platform specific text flags.
+     *
+     * @param emojiSupportMatch control emoji support matches on Android
+     */
     constructor(emojiSupportMatch: EmojiSupportMatch = EmojiSupportMatch.Default) {
         this.includeFontPadding = DefaultIncludeFontPadding
         this.emojiSupportMatch = emojiSupportMatch
     }
 
+    /**
+     * Default platform paragraph style
+     */
     constructor() : this(
         includeFontPadding = DefaultIncludeFontPadding,
         emojiSupportMatch = EmojiSupportMatch.Default
diff --git a/compose/ui/ui/build.gradle b/compose/ui/ui/build.gradle
index db53f5c..fb3978f 100644
--- a/compose/ui/ui/build.gradle
+++ b/compose/ui/ui/build.gradle
@@ -291,6 +291,9 @@
     sourceSets.androidTest.assets.srcDirs +=
             project.rootDir.absolutePath + "/../../golden/compose/ui/ui"
     namespace "androidx.compose.ui"
+    // namespace has to be unique, but default androidx.compose.ui.test package is taken by
+    // the androidx.compose.ui:ui-test library
+    testNamespace "androidx.compose.ui.tests"
 }
 
 // Diagnostics for b/188565660
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/graphics/vector/VectorInvalidationTestCase.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/graphics/vector/VectorInvalidationTestCase.kt
index b4aa669..6696cb1 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/graphics/vector/VectorInvalidationTestCase.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/graphics/vector/VectorInvalidationTestCase.kt
@@ -22,12 +22,12 @@
 import androidx.compose.runtime.remember
 import androidx.compose.ui.AtLeastSize
 import androidx.compose.ui.Modifier
-import androidx.compose.ui.draw.paint
 import androidx.compose.ui.draw.drawBehind
+import androidx.compose.ui.draw.paint
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.platform.LocalDensity
 import androidx.compose.ui.res.painterResource
-import androidx.compose.ui.test.R
+import androidx.compose.ui.tests.R
 
 class VectorInvalidationTestCase() {
 
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/graphics/vector/VectorTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/graphics/vector/VectorTest.kt
index 8208a50..eff49cc 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/graphics/vector/VectorTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/graphics/vector/VectorTest.kt
@@ -48,10 +48,10 @@
 import androidx.compose.ui.graphics.ImageBitmap
 import androidx.compose.ui.graphics.SolidColor
 import androidx.compose.ui.graphics.asAndroidBitmap
-import androidx.compose.ui.graphics.toArgb
-import androidx.compose.ui.graphics.toPixelMap
 import androidx.compose.ui.graphics.drawscope.CanvasDrawScope
 import androidx.compose.ui.graphics.painter.BitmapPainter
+import androidx.compose.ui.graphics.toArgb
+import androidx.compose.ui.graphics.toPixelMap
 import androidx.compose.ui.layout.ContentScale
 import androidx.compose.ui.platform.LocalContext
 import androidx.compose.ui.platform.LocalDensity
@@ -59,20 +59,21 @@
 import androidx.compose.ui.platform.LocalLayoutDirection
 import androidx.compose.ui.platform.testTag
 import androidx.compose.ui.res.ImageVectorCache
-import androidx.compose.ui.unit.Density
-import androidx.compose.ui.unit.LayoutDirection
-
 import androidx.compose.ui.res.painterResource
 import androidx.compose.ui.test.captureToImage
+import androidx.compose.ui.test.junit4.createAndroidComposeRule
 import androidx.compose.ui.test.onNodeWithTag
 import androidx.compose.ui.test.onRoot
 import androidx.compose.ui.test.performClick
-import androidx.compose.ui.test.R
-import androidx.compose.ui.test.junit4.createAndroidComposeRule
+import androidx.compose.ui.tests.R
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.LayoutDirection
 import androidx.compose.ui.unit.dp
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.MediumTest
 import androidx.test.filters.SdkSuppress
+import java.util.concurrent.CountDownLatch
+import java.util.concurrent.TimeUnit
 import org.junit.Assert
 import org.junit.Assert.assertEquals
 import org.junit.Assert.assertTrue
@@ -80,8 +81,6 @@
 import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
-import java.util.concurrent.CountDownLatch
-import java.util.concurrent.TimeUnit
 
 @MediumTest
 @RunWith(AndroidJUnit4::class)
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/graphics/vector/compat/XmlVectorParserTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/graphics/vector/compat/XmlVectorParserTest.kt
index 01c9518..4a724a3 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/graphics/vector/compat/XmlVectorParserTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/graphics/vector/compat/XmlVectorParserTest.kt
@@ -26,7 +26,7 @@
 import androidx.compose.ui.graphics.vector.VectorNode
 import androidx.compose.ui.graphics.vector.VectorPath
 import androidx.compose.ui.res.vectorResource
-import androidx.compose.ui.test.R
+import androidx.compose.ui.tests.R
 import androidx.compose.ui.unit.dp
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/platform/ComposeViewTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/platform/ComposeViewTest.kt
index 2779885..1b702f2 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/platform/ComposeViewTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/platform/ComposeViewTest.kt
@@ -17,9 +17,9 @@
 package androidx.compose.ui.platform
 
 import android.view.ViewGroup
-import androidx.compose.ui.test.R
 import androidx.compose.ui.test.TestActivity
 import androidx.compose.ui.test.junit4.createAndroidComposeRule
+import androidx.compose.ui.tests.R
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.MediumTest
 import org.junit.Assert.assertFalse
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/res/ColorResourcesTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/res/ColorResourcesTest.kt
index e0ec921..cca5c26 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/res/ColorResourcesTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/res/ColorResourcesTest.kt
@@ -19,8 +19,8 @@
 import androidx.compose.runtime.CompositionLocalProvider
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.platform.LocalContext
-import androidx.compose.ui.test.R
 import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.tests.R
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import androidx.test.platform.app.InstrumentationRegistry
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/res/PrimitiveResourcesTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/res/PrimitiveResourcesTest.kt
index 3703ff2..a503656 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/res/PrimitiveResourcesTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/res/PrimitiveResourcesTest.kt
@@ -18,8 +18,8 @@
 
 import androidx.compose.runtime.CompositionLocalProvider
 import androidx.compose.ui.platform.LocalContext
-import androidx.compose.ui.test.R
 import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.tests.R
 import androidx.compose.ui.unit.dp
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.MediumTest
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/res/StringResourcesTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/res/StringResourcesTest.kt
index 4b67de4e..9751449 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/res/StringResourcesTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/res/StringResourcesTest.kt
@@ -19,8 +19,8 @@
 import androidx.compose.runtime.CompositionLocalProvider
 import androidx.compose.ui.ExperimentalComposeUiApi
 import androidx.compose.ui.platform.LocalContext
-import androidx.compose.ui.test.R
 import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.tests.R
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.MediumTest
 import androidx.test.platform.app.InstrumentationRegistry
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/viewinterop/AndroidViewTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/viewinterop/AndroidViewTest.kt
index 98804c8..49fc678f 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/viewinterop/AndroidViewTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/viewinterop/AndroidViewTest.kt
@@ -61,11 +61,11 @@
 import androidx.compose.ui.platform.ViewCompositionStrategy
 import androidx.compose.ui.platform.findViewTreeCompositionContext
 import androidx.compose.ui.platform.testTag
-import androidx.compose.ui.test.R
 import androidx.compose.ui.test.TestActivity
 import androidx.compose.ui.test.captureToImage
 import androidx.compose.ui.test.junit4.createAndroidComposeRule
 import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.tests.R
 import androidx.compose.ui.unit.Density
 import androidx.compose.ui.unit.Dp
 import androidx.compose.ui.unit.IntSize
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/viewinterop/ComposeViewTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/viewinterop/ComposeViewTest.kt
index b203206..0fe1ac4 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/viewinterop/ComposeViewTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/viewinterop/ComposeViewTest.kt
@@ -53,12 +53,12 @@
 import androidx.compose.ui.platform.LocalView
 import androidx.compose.ui.platform.ViewCompositionStrategy
 import androidx.compose.ui.platform.testTag
-import androidx.compose.ui.test.R
 import androidx.compose.ui.test.assertTextEquals
 import androidx.compose.ui.test.junit4.createAndroidComposeRule
 import androidx.compose.ui.test.onNodeWithTag
 import androidx.compose.ui.test.performScrollTo
 import androidx.compose.ui.test.performTouchInput
+import androidx.compose.ui.tests.R
 import androidx.compose.ui.unit.Dp
 import androidx.compose.ui.unit.IntOffset
 import androidx.compose.ui.unit.IntRect
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/viewinterop/NestedScrollInteropConnectionTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/viewinterop/NestedScrollInteropConnectionTest.kt
index 73c9f5e..ea089f4 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/viewinterop/NestedScrollInteropConnectionTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/viewinterop/NestedScrollInteropConnectionTest.kt
@@ -33,7 +33,6 @@
 import androidx.compose.ui.input.nestedscroll.nestedScroll
 import androidx.compose.ui.modifier.modifierLocalProvider
 import androidx.compose.ui.platform.testTag
-import androidx.compose.ui.test.R
 import androidx.compose.ui.test.assertIsDisplayed
 import androidx.compose.ui.test.junit4.createAndroidComposeRule
 import androidx.compose.ui.test.onNodeWithTag
@@ -41,6 +40,7 @@
 import androidx.compose.ui.test.swipeDown
 import androidx.compose.ui.test.swipeUp
 import androidx.compose.ui.test.swipeWithVelocity
+import androidx.compose.ui.tests.R
 import androidx.compose.ui.unit.dp
 import androidx.compose.ui.unit.round
 import androidx.test.espresso.Espresso.onView
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/viewinterop/NestedScrollInteropTestHelper.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/viewinterop/NestedScrollInteropTestHelper.kt
index 755df89..5392884 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/viewinterop/NestedScrollInteropTestHelper.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/viewinterop/NestedScrollInteropTestHelper.kt
@@ -44,7 +44,7 @@
 import androidx.compose.ui.platform.ComposeView
 import androidx.compose.ui.platform.rememberNestedScrollInteropConnection
 import androidx.compose.ui.platform.testTag
-import androidx.compose.ui.test.R
+import androidx.compose.ui.tests.R
 import androidx.compose.ui.unit.Velocity
 import androidx.compose.ui.unit.dp
 import androidx.coordinatorlayout.widget.CoordinatorLayout
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/viewinterop/NestedScrollInteropThreeFoldTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/viewinterop/NestedScrollInteropThreeFoldTest.kt
index 2ea1689..67a83e3f 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/viewinterop/NestedScrollInteropThreeFoldTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/viewinterop/NestedScrollInteropThreeFoldTest.kt
@@ -24,11 +24,11 @@
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.input.nestedscroll.nestedScroll
-import androidx.compose.ui.test.R
 import androidx.compose.ui.test.junit4.createAndroidComposeRule
 import androidx.compose.ui.test.onNodeWithTag
 import androidx.compose.ui.test.performTouchInput
 import androidx.compose.ui.test.swipeUp
+import androidx.compose.ui.tests.R
 import androidx.compose.ui.unit.round
 import androidx.test.espresso.Espresso.onView
 import androidx.test.espresso.assertion.ViewAssertions.matches
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/viewinterop/NestedScrollInteropViewHolderTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/viewinterop/NestedScrollInteropViewHolderTest.kt
index 78bd15c..40f2199 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/viewinterop/NestedScrollInteropViewHolderTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/viewinterop/NestedScrollInteropViewHolderTest.kt
@@ -25,8 +25,8 @@
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.input.nestedscroll.nestedScroll
-import androidx.compose.ui.test.R
 import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.tests.R
 import androidx.compose.ui.unit.Velocity
 import androidx.test.espresso.Espresso
 import androidx.test.espresso.Espresso.onView
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/viewinterop/PoolingContainerComposeTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/viewinterop/PoolingContainerComposeTest.kt
index 878ccca..a9b7904 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/viewinterop/PoolingContainerComposeTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/viewinterop/PoolingContainerComposeTest.kt
@@ -23,7 +23,7 @@
 import androidx.compose.runtime.DisposableEffect
 import androidx.compose.ui.platform.AbstractComposeView
 import androidx.compose.ui.platform.ViewCompositionStrategy
-import androidx.compose.ui.test.R
+import androidx.compose.ui.tests.R
 import androidx.customview.poolingcontainer.callPoolingContainerOnRelease
 import androidx.customview.poolingcontainer.isPoolingContainer
 import androidx.test.ext.junit.rules.ActivityScenarioRule
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/MotionDurationScale.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/MotionDurationScale.kt
index 40cfb6e..2fe23c8 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/MotionDurationScale.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/MotionDurationScale.kt
@@ -24,6 +24,12 @@
  * the motion will end in the next frame callback. Otherwise, the duration [scaleFactor] will be
  * used as a multiplier to scale the duration of the motion. The larger the scale, the longer the
  * motion will take to finish, and therefore the slower it will be perceived.
+ *
+ * ## Testing
+ *
+ * To control the motion duration scale in tests, create an implementation of this interface and
+ * pass it to the `effectContext` parameter either where you call `runComposeUiTest` or where you
+ * create your test rule.
  */
 @Stable
 interface MotionDurationScale : CoroutineContext.Element {
diff --git a/constraintlayout/constraintlayout-compose/api/current.txt b/constraintlayout/constraintlayout-compose/api/current.txt
index 0182e04..f80b976 100644
--- a/constraintlayout/constraintlayout-compose/api/current.txt
+++ b/constraintlayout/constraintlayout-compose/api/current.txt
@@ -411,9 +411,6 @@
   public final class MotionDragHandlerKt {
   }
 
-  @kotlin.DslMarker public @interface MotionDslScope {
-  }
-
   public interface MotionItemsProvider {
     method public int count();
     method public kotlin.jvm.functions.Function0<kotlin.Unit> getContent(int index);
diff --git a/constraintlayout/constraintlayout-compose/api/public_plus_experimental_current.txt b/constraintlayout/constraintlayout-compose/api/public_plus_experimental_current.txt
index 05f48b7..b9c3d1e 100644
--- a/constraintlayout/constraintlayout-compose/api/public_plus_experimental_current.txt
+++ b/constraintlayout/constraintlayout-compose/api/public_plus_experimental_current.txt
@@ -586,9 +586,6 @@
   public final class MotionDragHandlerKt {
   }
 
-  @kotlin.DslMarker public @interface MotionDslScope {
-  }
-
   public interface MotionItemsProvider {
     method public int count();
     method public kotlin.jvm.functions.Function0<kotlin.Unit> getContent(int index);
@@ -597,9 +594,6 @@
   }
 
   public final class MotionKt {
-    method @androidx.compose.runtime.Composable @androidx.constraintlayout.compose.ExperimentalMotionApi public static void Motion(optional androidx.compose.ui.Modifier modifier, kotlin.jvm.functions.Function1<? super androidx.constraintlayout.compose.MotionScope,kotlin.Unit> content);
-    method @androidx.compose.runtime.Composable @androidx.constraintlayout.compose.ExperimentalMotionApi public static kotlin.jvm.functions.Function1<androidx.constraintlayout.compose.MotionScope,kotlin.Unit> rememberMotionContent(kotlin.jvm.functions.Function1<? super androidx.constraintlayout.compose.MotionScope,kotlin.Unit> content);
-    method @androidx.compose.runtime.Composable @androidx.constraintlayout.compose.ExperimentalMotionApi public static java.util.List<kotlin.jvm.functions.Function2<androidx.constraintlayout.compose.MotionScope,java.lang.Integer,kotlin.Unit>> rememberMotionListItems(int count, kotlin.jvm.functions.Function2<? super androidx.constraintlayout.compose.MotionScope,? super java.lang.Integer,kotlin.Unit> content);
   }
 
   public enum MotionLayoutDebugFlags {
@@ -734,12 +728,6 @@
     method @androidx.constraintlayout.compose.ExperimentalMotionApi public static androidx.constraintlayout.compose.MotionScene MotionScene(kotlin.jvm.functions.Function1<? super androidx.constraintlayout.compose.MotionSceneScope,kotlin.Unit> motionSceneContent);
   }
 
-  @androidx.constraintlayout.compose.ExperimentalMotionApi @androidx.constraintlayout.compose.MotionDslScope public final class MotionScope implements androidx.compose.ui.layout.LookaheadLayoutScope {
-    ctor public MotionScope(androidx.compose.ui.layout.LookaheadLayoutScope lookaheadLayoutScope);
-    method @androidx.compose.runtime.Composable public void emit(java.util.List<? extends kotlin.jvm.functions.Function2<? super androidx.constraintlayout.compose.MotionScope,? super java.lang.Integer,kotlin.Unit>>);
-    method public androidx.compose.ui.Modifier motion(androidx.compose.ui.Modifier, optional androidx.compose.animation.core.AnimationSpec<java.lang.Float> animationSpec, optional boolean ignoreAxisChanges, optional kotlin.jvm.functions.Function1<? super androidx.constraintlayout.compose.MotionModifierScope,kotlin.Unit> motionDescription);
-  }
-
   @androidx.constraintlayout.compose.ExperimentalMotionApi public final class OnSwipe {
     ctor public OnSwipe(androidx.constraintlayout.compose.ConstrainedLayoutReference anchor, androidx.constraintlayout.compose.SwipeSide side, androidx.constraintlayout.compose.SwipeDirection direction, optional float dragScale, optional float dragThreshold, optional androidx.constraintlayout.compose.ConstrainedLayoutReference? dragAround, optional androidx.constraintlayout.compose.ConstrainedLayoutReference? limitBoundsTo, optional androidx.constraintlayout.compose.SwipeTouchUp onTouchUp, optional androidx.constraintlayout.compose.SwipeMode mode);
     method public androidx.constraintlayout.compose.ConstrainedLayoutReference component1();
diff --git a/constraintlayout/constraintlayout-compose/api/restricted_current.txt b/constraintlayout/constraintlayout-compose/api/restricted_current.txt
index d7d773c..c7ccd73 100644
--- a/constraintlayout/constraintlayout-compose/api/restricted_current.txt
+++ b/constraintlayout/constraintlayout-compose/api/restricted_current.txt
@@ -528,9 +528,6 @@
     method public androidx.constraintlayout.compose.MotionDragState onDragEnd(long velocity);
   }
 
-  @kotlin.DslMarker public @interface MotionDslScope {
-  }
-
   public interface MotionItemsProvider {
     method public int count();
     method public kotlin.jvm.functions.Function0<kotlin.Unit> getContent(int index);
diff --git a/constraintlayout/constraintlayout-compose/src/androidMain/kotlin/androidx/constraintlayout/compose/Motion.kt b/constraintlayout/constraintlayout-compose/src/androidMain/kotlin/androidx/constraintlayout/compose/Motion.kt
index 686ac0b3..662566b 100644
--- a/constraintlayout/constraintlayout-compose/src/androidMain/kotlin/androidx/constraintlayout/compose/Motion.kt
+++ b/constraintlayout/constraintlayout-compose/src/androidMain/kotlin/androidx/constraintlayout/compose/Motion.kt
@@ -93,7 +93,7 @@
 @ExperimentalMotionApi
 @OptIn(ExperimentalComposeUiApi::class)
 @Composable
-fun Motion(
+private fun Motion(
     modifier: Modifier = Modifier,
     content: @Composable MotionScope.() -> Unit
 ) {
@@ -125,7 +125,7 @@
 }
 
 @DslMarker
-annotation class MotionDslScope
+private annotation class MotionDslScope
 
 /**
  * Scope for the [Motion] Composable.
@@ -136,7 +136,7 @@
 @ExperimentalMotionApi
 @MotionDslScope
 @OptIn(ExperimentalComposeUiApi::class)
-class MotionScope(
+private class MotionScope(
     lookaheadLayoutScope: LookaheadLayoutScope
 ) : LookaheadLayoutScope by lookaheadLayoutScope {
     private var nextId: Int = 1000
@@ -360,7 +360,7 @@
  */
 @ExperimentalMotionApi
 @Composable
-fun rememberMotionContent(content: @Composable MotionScope.() -> Unit):
+private fun rememberMotionContent(content: @Composable MotionScope.() -> Unit):
     @Composable MotionScope.() -> Unit {
     return remember {
         movableContentOf(content)
@@ -376,7 +376,7 @@
  */
 @ExperimentalMotionApi
 @Composable
-fun rememberMotionListItems(
+private fun rememberMotionListItems(
     count: Int,
     content: @Composable MotionScope.(index: Int) -> Unit
 ): List<@Composable MotionScope.(index: Int) -> Unit> {
diff --git a/constraintlayout/constraintlayout-core/src/main/java/androidx/constraintlayout/core/state/ConstraintSetParser.java b/constraintlayout/constraintlayout-core/src/main/java/androidx/constraintlayout/core/state/ConstraintSetParser.java
index 2cdabc5..72db469 100644
--- a/constraintlayout/constraintlayout-core/src/main/java/androidx/constraintlayout/core/state/ConstraintSetParser.java
+++ b/constraintlayout/constraintlayout-core/src/main/java/androidx/constraintlayout/core/state/ConstraintSetParser.java
@@ -1094,31 +1094,16 @@
                     flow.setWrapMode(State.Wrap.getValueByString(wrapValue));
                     break;
                 case "vGap":
-                    String vGapValue = element.get(param).content();
-                    try {
-                        int value = Integer.parseInt(vGapValue);
-                        flow.setVerticalGap(value);
-                    } catch(NumberFormatException e) {
-
-                    }
+                    int vGapValue = element.get(param).getInt();
+                    flow.setVerticalGap(vGapValue);
                     break;
                 case "hGap":
-                    String hGapValue = element.get(param).content();
-                    try {
-                        int value = Integer.parseInt(hGapValue);
-                        flow.setHorizontalGap(value);
-                    } catch(NumberFormatException e) {
-
-                    }
+                    int hGapValue = element.get(param).getInt();
+                    flow.setHorizontalGap(hGapValue);
                     break;
                 case "maxElement":
-                    String maxElementValue = element.get(param).content();
-                    try {
-                        int value = Integer.parseInt(maxElementValue);
-                        flow.setMaxElementsWrap(value);
-                    } catch(NumberFormatException e) {
-
-                    }
+                    int maxElementValue = element.get(param).getInt();
+                    flow.setMaxElementsWrap(maxElementValue);
                     break;
                 case "padding":
                     CLElement paddingObject = element.get(param);
diff --git a/constraintlayout/constraintlayout/api/current.txt b/constraintlayout/constraintlayout/api/current.txt
index 469ddb0..6e30c9f 100644
--- a/constraintlayout/constraintlayout/api/current.txt
+++ b/constraintlayout/constraintlayout/api/current.txt
@@ -120,6 +120,20 @@
     field protected float mComputedMinY;
   }
 
+  public class LogJson extends androidx.constraintlayout.widget.ConstraintHelper {
+    ctor public LogJson(android.content.Context);
+    ctor public LogJson(android.content.Context, android.util.AttributeSet?);
+    ctor public LogJson(android.content.Context, android.util.AttributeSet?, int);
+    method public void periodicStart();
+    method public void periodicStop();
+    method public void setDelay(int);
+    method public void writeLog();
+    field public static final int LOG_API = 4; // 0x4
+    field public static final int LOG_DELAYED = 2; // 0x2
+    field public static final int LOG_LAYOUT = 3; // 0x3
+    field public static final int LOG_PERIODIC = 1; // 0x1
+  }
+
   public class MotionEffect extends androidx.constraintlayout.motion.widget.MotionHelper {
     ctor public MotionEffect(android.content.Context!);
     ctor public MotionEffect(android.content.Context!, android.util.AttributeSet!);
diff --git a/constraintlayout/constraintlayout/api/public_plus_experimental_current.txt b/constraintlayout/constraintlayout/api/public_plus_experimental_current.txt
index 469ddb0..6e30c9f 100644
--- a/constraintlayout/constraintlayout/api/public_plus_experimental_current.txt
+++ b/constraintlayout/constraintlayout/api/public_plus_experimental_current.txt
@@ -120,6 +120,20 @@
     field protected float mComputedMinY;
   }
 
+  public class LogJson extends androidx.constraintlayout.widget.ConstraintHelper {
+    ctor public LogJson(android.content.Context);
+    ctor public LogJson(android.content.Context, android.util.AttributeSet?);
+    ctor public LogJson(android.content.Context, android.util.AttributeSet?, int);
+    method public void periodicStart();
+    method public void periodicStop();
+    method public void setDelay(int);
+    method public void writeLog();
+    field public static final int LOG_API = 4; // 0x4
+    field public static final int LOG_DELAYED = 2; // 0x2
+    field public static final int LOG_LAYOUT = 3; // 0x3
+    field public static final int LOG_PERIODIC = 1; // 0x1
+  }
+
   public class MotionEffect extends androidx.constraintlayout.motion.widget.MotionHelper {
     ctor public MotionEffect(android.content.Context!);
     ctor public MotionEffect(android.content.Context!, android.util.AttributeSet!);
diff --git a/constraintlayout/constraintlayout/api/restricted_current.txt b/constraintlayout/constraintlayout/api/restricted_current.txt
index 469ddb0..6e30c9f 100644
--- a/constraintlayout/constraintlayout/api/restricted_current.txt
+++ b/constraintlayout/constraintlayout/api/restricted_current.txt
@@ -120,6 +120,20 @@
     field protected float mComputedMinY;
   }
 
+  public class LogJson extends androidx.constraintlayout.widget.ConstraintHelper {
+    ctor public LogJson(android.content.Context);
+    ctor public LogJson(android.content.Context, android.util.AttributeSet?);
+    ctor public LogJson(android.content.Context, android.util.AttributeSet?, int);
+    method public void periodicStart();
+    method public void periodicStop();
+    method public void setDelay(int);
+    method public void writeLog();
+    field public static final int LOG_API = 4; // 0x4
+    field public static final int LOG_DELAYED = 2; // 0x2
+    field public static final int LOG_LAYOUT = 3; // 0x3
+    field public static final int LOG_PERIODIC = 1; // 0x1
+  }
+
   public class MotionEffect extends androidx.constraintlayout.motion.widget.MotionHelper {
     ctor public MotionEffect(android.content.Context!);
     ctor public MotionEffect(android.content.Context!, android.util.AttributeSet!);
diff --git a/constraintlayout/constraintlayout/src/main/java/androidx/constraintlayout/helper/widget/LogJson.java b/constraintlayout/constraintlayout/src/main/java/androidx/constraintlayout/helper/widget/LogJson.java
new file mode 100644
index 0000000..6b1c9db
--- /dev/null
+++ b/constraintlayout/constraintlayout/src/main/java/androidx/constraintlayout/helper/widget/LogJson.java
@@ -0,0 +1,772 @@
+/*

+ * Copyright (C) 2023 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.constraintlayout.helper.widget;

+

+import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;

+import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT;

+

+import static androidx.constraintlayout.widget.ConstraintSet.Layout.UNSET_GONE_MARGIN;

+

+import android.annotation.SuppressLint;

+import android.content.Context;

+import android.content.res.TypedArray;

+import android.os.Environment;

+import android.util.AttributeSet;

+import android.util.Log;

+import android.util.TypedValue;

+import android.view.View;

+import android.view.ViewGroup;

+import android.widget.Button;

+import android.widget.TextView;

+

+import androidx.annotation.RequiresApi;

+import androidx.constraintlayout.motion.widget.Debug;

+import androidx.constraintlayout.widget.ConstraintAttribute;

+import androidx.constraintlayout.widget.ConstraintHelper;

+import androidx.constraintlayout.widget.ConstraintLayout;

+import androidx.constraintlayout.widget.ConstraintSet;

+import androidx.constraintlayout.widget.R;

+

+import java.io.File;

+import java.io.FileOutputStream;

+import java.io.IOException;

+import java.io.StringWriter;

+import java.io.Writer;

+import java.util.HashMap;

+import java.util.concurrent.atomic.AtomicInteger;

+

+/**

+ * This is a class is a debugging/logging utility to write out the constraints in JSON

+ * This is used for debugging purposes

+ * <ul>

+ *     <li>logJsonTo - defines the output log console or "fileName"</li>

+ *     <li>logJsonMode - mode one of:

+ *     <b>periodic</b>, <b>delayed</b>, <b>layout</b> or <b>api</b></li>

+ *     <li>logJsonDelay - the duration of the delay or the delay between repeated logs</li>

+ * </ul>

+ * logJsonTo supports:

+ * <ul>

+ *     <li>log - logs using log.v("JSON5", ...)</li>

+ *     <li>console - logs using System.out.println(...)</li>

+ *     <li>[fileName] - will write to /storage/emulated/0/Download/[fileName].json5</li>

+ * </ul>

+ * logJsonMode modes are:

+ * <ul>

+ *     <li>periodic - after window is attached will log every delay ms</li>

+ *     <li>delayed - log once after delay ms</li>

+ *     <li>layout - log every time there is a layout call</li>

+ *     <li>api - do not automatically log developer will call writeLog</li>

+ * </ul>

+ *

+ * The defaults are:

+ * <ul>

+ *     <li>logJsonTo="log"</li>

+ *     <li>logJsonMode="delayed"</li>

+ *     <li>logJsonDelay="1000"</li>

+ * </ul>

+ *  Usage:

+ *  <p></p>

+ *  <pre>

+ *  {@code

+ *      <androidx.constraintlayout.helper.widget.LogJson

+ *         android:layout_width="0dp"

+ *         android:layout_height="0dp"

+ *         android:visibility="gone"

+ *         app:logJsonTo="log"

+ *         app:logJsonMode="delayed"

+ *         app:logJsonDelay="1000"

+ *         />

+ *  }

+ * </pre>

+ * </p>

+ */

+public class LogJson extends ConstraintHelper {

+    private static final String TAG = "JSON5";

+    private int mDelay = 1000;

+    private int mMode = LOG_DELAYED;

+    private String mLogToFile = null;

+    private boolean mLogConsole = true;

+

+    public static final int LOG_PERIODIC = 1;

+    public static final int LOG_DELAYED = 2;

+    public static final int LOG_LAYOUT = 3;

+    public static final int LOG_API = 4;

+    private boolean mPeriodic = false;

+

+    public LogJson(@androidx.annotation.NonNull Context context) {

+        super(context);

+    }

+

+    public LogJson(@androidx.annotation.NonNull Context context,

+                   @androidx.annotation.Nullable AttributeSet attrs) {

+        super(context, attrs);

+        initLogJson(attrs);

+

+    }

+

+    public LogJson(@androidx.annotation.NonNull Context context,

+                   @androidx.annotation.Nullable AttributeSet attrs, int defStyleAttr) {

+        super(context, attrs, defStyleAttr);

+        initLogJson(attrs);

+    }

+

+    private void initLogJson(AttributeSet attrs) {

+

+        if (attrs != null) {

+            TypedArray a = getContext().obtainStyledAttributes(attrs,

+                    R.styleable.LogJson);

+            final int count = a.getIndexCount();

+            for (int i = 0; i < count; i++) {

+                int attr = a.getIndex(i);

+                if (attr == R.styleable.LogJson_logJsonDelay) {

+                    mDelay = a.getInt(attr, mDelay);

+                } else if (attr == R.styleable.LogJson_logJsonMode) {

+                    mMode = a.getInt(attr, mMode);

+                } else if (attr == R.styleable.LogJson_logJsonTo) {

+                    TypedValue v = a.peekValue(attr);

+                    if (v.type == TypedValue.TYPE_STRING) {

+                        mLogToFile = a.getString(attr);

+                    } else {

+                        int value = a.getInt(attr, 0);

+                        mLogConsole = value == 2;

+                    }

+                }

+            }

+            a.recycle();

+        }

+        setVisibility(GONE);

+    }

+

+    @Override

+    protected void onAttachedToWindow() {

+        super.onAttachedToWindow();

+        switch (mMode) {

+            case LOG_PERIODIC:

+                mPeriodic = true;

+                this.postDelayed(this::periodic, mDelay);

+                break;

+            case LOG_DELAYED:

+                this.postDelayed(this::writeLog, mDelay);

+                break;

+            case LOG_LAYOUT:

+                ConstraintLayout cl = (ConstraintLayout) getParent();

+                cl.addOnLayoutChangeListener((v, a, b, c, d, e, f, g, h) -> logOnLayout());

+        }

+    }

+

+    private void logOnLayout() {

+        if (mMode == LOG_LAYOUT) {

+            writeLog();

+        }

+    }

+

+    /**

+     * Set the duration of periodic logging of constraints

+     *

+     * @param duration the time in ms between writing files

+     */

+    public void setDelay(int duration) {

+        mDelay = duration;

+    }

+

+    /**

+     * Start periodic sampling

+     */

+    public void periodicStart() {

+        if (mPeriodic) {

+            return;

+        }

+        mPeriodic = true;

+        this.postDelayed(this::periodic, mDelay);

+    }

+

+    /**

+     * Stop periodic sampling

+     */

+    public void periodicStop() {

+        mPeriodic = false;

+    }

+

+    private void periodic() {

+        if (mPeriodic) {

+            writeLog();

+            this.postDelayed(this::periodic, mDelay);

+        }

+    }

+

+    /**

+     * This writes a JSON5 representation of the constraintSet

+     */

+    public void writeLog() {

+        String str = asString((ConstraintLayout) this.getParent());

+        if (mLogToFile == null) {

+            if (mLogConsole) {

+                System.out.println(str);

+            } else {

+                logBigString(str);

+            }

+        } else {

+            String name = toFile(str, mLogToFile);

+            Log.v("JSON", "\"" + name + "\" written!");

+        }

+    }

+

+    /**

+     * This writes the JSON5 description of the constraintLayout to a file named fileName.json5

+     * in the download directory which can be pulled with:

+     * "adb pull "/storage/emulated/0/Download/" ."

+     *

+     * @param str      String to write as a file

+     * @param fileName file name

+     * @return full path name of file

+     */

+    private static String toFile(String str, String fileName) {

+        FileOutputStream outputStream;

+        if (!fileName.endsWith(".json5")) {

+            fileName += ".json5";

+        }

+        try {

+            File down =

+                    Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS);

+            File file = new File(down, fileName);

+            outputStream = new FileOutputStream(file);

+            outputStream.write(str.getBytes());

+            outputStream.close();

+            return file.getCanonicalPath();

+        } catch (IOException e) {

+            return e.toString();

+        }

+    }

+

+    @SuppressLint("LogConditional")

+    private void logBigString(String str) {

+        int len = str.length();

+        for (int i = 0; i < len; i++) {

+            int k = str.indexOf("\n", i);

+            if (k == -1) {

+                Log.v(TAG, str.substring(i));

+                break;

+            }

+            Log.v(TAG, str.substring(i, k));

+            i = k;

+        }

+    }

+

+    /**

+     * Get a JSON5 String that represents the Constraints in a running ConstraintLayout

+     *

+     * @param constraintLayout its constraints are converted to a string

+     * @return JSON5 string

+     */

+    private static String asString(ConstraintLayout constraintLayout) {

+        JsonWriter c = new JsonWriter();

+        return c.constraintLayoutToJson(constraintLayout);

+    }

+

+    // ================================== JSON writer==============================================

+

+    private static class JsonWriter {

+        public static final int UNSET = ConstraintLayout.LayoutParams.UNSET;

+        ConstraintSet mSet;

+        Writer mWriter;

+        Context mContext;

+        int mUnknownCount = 0;

+        final String mLEFT = "left";

+        final String mRIGHT = "right";

+        final String mBASELINE = "baseline";

+        final String mBOTTOM = "bottom";

+        final String mTOP = "top";

+        final String mSTART = "start";

+        final String mEND = "end";

+        private static final String INDENT = "    ";

+        private static final String SMALL_INDENT = "  ";

+        HashMap<Integer, String> mIdMap = new HashMap<>();

+        private static final String LOG_JSON = LogJson.class.getSimpleName();

+        private static final AtomicInteger sNextGeneratedId = new AtomicInteger(1);

+        HashMap<Integer, String> mNames = new HashMap<>();

+

+        private static int generateViewId() {

+            final int max_id = 0x00FFFFFF;

+            for (;;) {

+                final int result = sNextGeneratedId.get();

+                int newValue = result + 1;

+                if (newValue > max_id) {

+                    newValue = 1;

+                }

+                if (sNextGeneratedId.compareAndSet(result, newValue)) {

+                    return result;

+                }

+            }

+        }

+

+        @RequiresApi(17)

+        private static class JellyBean {

+            static int generateViewId() {

+                return View.generateViewId();

+            }

+        }

+

+        String constraintLayoutToJson(ConstraintLayout constraintLayout) {

+            StringWriter writer = new StringWriter();

+

+            int count = constraintLayout.getChildCount();

+            for (int i = 0; i < count; i++) {

+                View v = constraintLayout.getChildAt(i);

+                String name = v.getClass().getSimpleName();

+                int id = v.getId();

+                if (id == -1) {

+                    if (android.os.Build.VERSION.SDK_INT

+                            >= android.os.Build.VERSION_CODES.JELLY_BEAN_MR1) {

+                        id = JellyBean.generateViewId();

+                    } else {

+                        id = generateViewId();

+                    }

+                    v.setId(id);

+                    if (!LOG_JSON.equals(name)) {

+                        name = "noid_" + name;

+                    }

+                    mNames.put(id, name);

+                } else if (LOG_JSON.equals(name)) {

+                    mNames.put(id, name);

+                }

+            }

+            writer.append("{\n");

+

+            writeWidgets(writer, constraintLayout);

+            writer.append("  ConstraintSet:{\n");

+            ConstraintSet set = new ConstraintSet();

+            set.clone(constraintLayout);

+            String name =

+                    (constraintLayout.getId() == -1) ? "cset" : Debug.getName(constraintLayout);

+            try {

+                writer.append(name + ":");

+                setup(writer, set, constraintLayout);

+                writeLayout();

+                writer.append("\n");

+            } catch (IOException e) {

+                throw new RuntimeException(e);

+            }

+            writer.append("  }\n");

+            writer.append("}\n");

+            return writer.toString();

+        }

+

+        private void writeWidgets(StringWriter writer, ConstraintLayout constraintLayout) {

+            writer.append("Widgets:{\n");

+            int count = constraintLayout.getChildCount();

+

+            for (int i = -1; i < count; i++) {

+                View v = (i == -1) ? constraintLayout : constraintLayout.getChildAt(i);

+                int id = v.getId();

+                if (LOG_JSON.equals(v.getClass().getSimpleName())) {

+                    continue;

+                }

+                String name = mNames.containsKey(id) ? mNames.get(id)

+                        : ((i == -1) ? "parent" : Debug.getName(v));

+                String cname = v.getClass().getSimpleName();

+                String bounds = ", bounds: [" + v.getLeft() + ", " + v.getTop()

+                        + ", " + v.getRight() + ", " + v.getBottom() + "]},\n";

+                writer.append("  " + name + ": { ");

+                if (i == -1) {

+                    writer.append("type: '" + v.getClass().getSimpleName() + "' , ");

+

+                    try {

+                        ViewGroup.LayoutParams p = (ViewGroup.LayoutParams) v.getLayoutParams();

+                        String wrap = "'WRAP_CONTENT'";

+                        String match = "'MATCH_PARENT'";

+                        String w = p.width == MATCH_PARENT ? match :

+                                (p.width == WRAP_CONTENT) ? wrap : p.width + "";

+                        writer.append("width: " + w + ", ");

+                        String h = p.height == MATCH_PARENT ? match :

+                                (p.height == WRAP_CONTENT) ? wrap : p.height + "";

+                        writer.append("height: ").append(h);

+                    } catch (Exception e) {

+                    }

+                } else if (cname.contains("Text")) {

+                    if (v instanceof TextView) {

+                        writer.append("type: 'Text', label: '"

+                                + escape(((TextView) v).getText().toString()) + "'");

+                    } else {

+                        writer.append("type: 'Text' },\n");

+                    }

+                } else if (cname.contains("Button")) {

+                    if (v instanceof Button) {

+                        writer.append("type: 'Button', label: '" + ((Button) v).getText() + "'");

+                    } else

+                        writer.append("type: 'Button'");

+                } else if (cname.contains("Image")) {

+                    writer.append("type: 'Image'");

+                } else if (cname.contains("View")) {

+                    writer.append("type: 'Box'");

+                } else {

+                    writer.append("type: '" + v.getClass().getSimpleName() + "'");

+                }

+                writer.append(bounds);

+            }

+            writer.append("},\n");

+        }

+

+        private static String escape(String str) {

+            return str.replaceAll("'", "\\'");

+        }

+

+        JsonWriter() {

+        }

+

+        void setup(Writer writer,

+                   ConstraintSet set,

+                   ConstraintLayout layout) throws IOException {

+            this.mWriter = writer;

+            this.mContext = layout.getContext();

+            this.mSet = set;

+            set.getConstraint(2);

+        }

+

+        private int[] getIDs() {

+            return mSet.getKnownIds();

+        }

+

+        private ConstraintSet.Constraint getConstraint(int id) {

+            return mSet.getConstraint(id);

+        }

+

+        private void writeLayout() throws IOException {

+            mWriter.write("{\n");

+            for (Integer id : getIDs()) {

+                ConstraintSet.Constraint c = getConstraint(id);

+                String idName = getSimpleName(id);

+                if (LOG_JSON.equals(idName)) { // skip LogJson it is for used to log

+                    continue;

+                }

+                mWriter.write(SMALL_INDENT + idName + ":{\n");

+                ConstraintSet.Layout l = c.layout;

+                if (l.mReferenceIds != null) {

+                    StringBuilder ref =

+                            new StringBuilder("type: '_" + idName + "_' , contains: [");

+                    for (int r = 0; r < l.mReferenceIds.length; r++) {

+                        int rid = l.mReferenceIds[r];

+                        ref.append((r == 0) ? "" : ", ").append(getName(rid));

+                    }

+                    mWriter.write(ref + "]\n");

+                }

+                if (l.mReferenceIdString != null) {

+                    StringBuilder ref =

+                            new StringBuilder(SMALL_INDENT + "type: '???' , contains: [");

+                    String[] rids = l.mReferenceIdString.split(",");

+                    for (int r = 0; r < rids.length; r++) {

+                        String rid = rids[r];

+                        ref.append((r == 0) ? "" : ", ").append("`").append(rid).append("`");

+                    }

+                    mWriter.write(ref + "]\n");

+                }

+                writeDimension("height", l.mHeight, l.heightDefault, l.heightPercent,

+                        l.heightMin, l.heightMax, l.constrainedHeight);

+                writeDimension("width", l.mWidth, l.widthDefault, l.widthPercent,

+                        l.widthMin, l.widthMax, l.constrainedWidth);

+

+                writeConstraint(mLEFT, l.leftToLeft, mLEFT, l.leftMargin, l.goneLeftMargin);

+                writeConstraint(mLEFT, l.leftToRight, mRIGHT, l.leftMargin, l.goneLeftMargin);

+                writeConstraint(mRIGHT, l.rightToLeft, mLEFT, l.rightMargin, l.goneRightMargin);

+                writeConstraint(mRIGHT, l.rightToRight, mRIGHT, l.rightMargin, l.goneRightMargin);

+                writeConstraint(mBASELINE, l.baselineToBaseline, mBASELINE, UNSET,

+                        l.goneBaselineMargin);

+                writeConstraint(mBASELINE, l.baselineToTop, mTOP, UNSET, l.goneBaselineMargin);

+                writeConstraint(mBASELINE, l.baselineToBottom,

+                        mBOTTOM, UNSET, l.goneBaselineMargin);

+

+                writeConstraint(mTOP, l.topToBottom, mBOTTOM, l.topMargin, l.goneTopMargin);

+                writeConstraint(mTOP, l.topToTop, mTOP, l.topMargin, l.goneTopMargin);

+                writeConstraint(mBOTTOM, l.bottomToBottom, mBOTTOM, l.bottomMargin,

+                        l.goneBottomMargin);

+                writeConstraint(mBOTTOM, l.bottomToTop, mTOP, l.bottomMargin, l.goneBottomMargin);

+                writeConstraint(mSTART, l.startToStart, mSTART, l.startMargin, l.goneStartMargin);

+                writeConstraint(mSTART, l.startToEnd, mEND, l.startMargin, l.goneStartMargin);

+                writeConstraint(mEND, l.endToStart, mSTART, l.endMargin, l.goneEndMargin);

+                writeConstraint(mEND, l.endToEnd, mEND, l.endMargin, l.goneEndMargin);

+

+                writeVariable("horizontalBias", l.horizontalBias, 0.5f);

+                writeVariable("verticalBias", l.verticalBias, 0.5f);

+

+                writeCircle(l.circleConstraint, l.circleAngle, l.circleRadius);

+

+                writeGuideline(l.orientation, l.guideBegin, l.guideEnd, l.guidePercent);

+                writeVariable("dimensionRatio", l.dimensionRatio);

+                writeVariable("barrierMargin", l.mBarrierMargin);

+                writeVariable("type", l.mHelperType);

+                writeVariable("ReferenceId", l.mReferenceIdString);

+                writeVariable("mBarrierAllowsGoneWidgets",

+                        l.mBarrierAllowsGoneWidgets, true);

+                writeVariable("WrapBehavior", l.mWrapBehavior);

+

+                writeVariable("verticalWeight", l.verticalWeight);

+                writeVariable("horizontalWeight", l.horizontalWeight);

+                writeVariable("horizontalChainStyle", l.horizontalChainStyle);

+                writeVariable("verticalChainStyle", l.verticalChainStyle);

+                writeVariable("barrierDirection", l.mBarrierDirection);

+                if (l.mReferenceIds != null) {

+                    writeVariable("ReferenceIds", l.mReferenceIds);

+                }

+                writeTransform(c.transform);

+                writeCustom(c.mCustomConstraints);

+

+                mWriter.write("  },\n");

+            }

+            mWriter.write("},\n");

+        }

+

+        private void writeTransform(ConstraintSet.Transform transform) throws IOException {

+            if (transform.applyElevation) {

+                writeVariable("elevation", transform.elevation);

+            }

+            writeVariable("rotationX", transform.rotationX, 0);

+            writeVariable("rotationY", transform.rotationY, 0);

+            writeVariable("rotationZ", transform.rotation, 0);

+            writeVariable("scaleX", transform.scaleX, 1);

+            writeVariable("scaleY", transform.scaleY, 1);

+            writeVariable("translationX", transform.translationX, 0);

+            writeVariable("translationY", transform.translationY, 0);

+            writeVariable("translationZ", transform.translationZ, 0);

+        }

+

+        private void writeCustom(HashMap<String, ConstraintAttribute> cset) throws IOException {

+            if (cset != null && cset.size() > 0) {

+                mWriter.write(INDENT + "custom: {\n");

+                for (String s : cset.keySet()) {

+                    ConstraintAttribute attr = cset.get(s);

+                    if (attr == null) {

+                        continue;

+                    }

+                    String custom = INDENT + SMALL_INDENT + attr.getName() + ": ";

+                    switch (attr.getType()) {

+                        case INT_TYPE:

+                            custom += attr.getIntegerValue();

+                            break;

+                        case COLOR_TYPE:

+                            custom += colorString(attr.getColorValue());

+                            break;

+                        case FLOAT_TYPE:

+                            custom += attr.getFloatValue();

+                            break;

+                        case STRING_TYPE:

+                            custom += "'" + attr.getStringValue() + "'";

+                            break;

+                        case DIMENSION_TYPE:

+                            custom = custom + attr.getFloatValue();

+                            break;

+                        case REFERENCE_TYPE:

+                        case COLOR_DRAWABLE_TYPE:

+                        case BOOLEAN_TYPE:

+                            custom = null;

+                    }

+                    if (custom != null) {

+                        mWriter.write(custom + ",\n");

+                    }

+                }

+                mWriter.write(SMALL_INDENT + "   } \n");

+            }

+        }

+

+        private static String colorString(int v) {

+            String str = "00000000" + Integer.toHexString(v);

+            return "#" + str.substring(str.length() - 8);

+        }

+

+        private void writeGuideline(int orientation,

+                                    int guideBegin,

+                                    int guideEnd,

+                                    float guidePercent) throws IOException {

+            writeVariable("orientation", orientation);

+            writeVariable("guideBegin", guideBegin);

+            writeVariable("guideEnd", guideEnd);

+            writeVariable("guidePercent", guidePercent);

+        }

+

+        private void writeDimension(String dimString,

+                                    int dim,

+                                    int dimDefault,

+                                    float dimPercent,

+                                    int dimMin,

+                                    int dimMax,

+                                    boolean unusedConstrainedDim) throws IOException {

+            if (dim == 0) {

+                if (dimMax != UNSET || dimMin != UNSET) {

+                    String s = "-----";

+                    switch (dimDefault) {

+                        case 0: // spread

+                            s = INDENT + dimString + ": {value:'spread'";

+                            break;

+                        case 1: //  wrap

+                            s = INDENT + dimString + ": {value:'wrap'";

+                            break;

+                        case 2: // percent

+                            s = INDENT + dimString + ": {value: '" + dimPercent + "%'";

+                            break;

+                    }

+                    if (dimMax != UNSET) {

+                        s += ", max: " + dimMax;

+                    }

+                    if (dimMax != UNSET) {

+                        s += ", min: " + dimMin;

+                    }

+                    s += "},\n";

+                    mWriter.write(s);

+                    return;

+                }

+

+                switch (dimDefault) {

+                    case 0: // spread is the default

+                        break;

+                    case 1: //  wrap

+                        mWriter.write(INDENT + dimString + ": '???????????',\n");

+                        return;

+                    case 2: // percent

+                        mWriter.write(INDENT + dimString + ": '" + dimPercent + "%',\n");

+                }

+

+            } else if (dim == -2) {

+                mWriter.write(INDENT + dimString + ": 'wrap',\n");

+            } else if (dim == -1) {

+                mWriter.write(INDENT + dimString + ": 'parent',\n");

+            } else {

+                mWriter.write(INDENT + dimString + ": " + dim + ",\n");

+            }

+        }

+

+        private String getSimpleName(int id) {

+            if (mIdMap.containsKey(id)) {

+                return "" + mIdMap.get(id);

+            }

+            if (id == 0) {

+                return "parent";

+            }

+            String name = lookup(id);

+            mIdMap.put(id, name);

+            return "" + name + "";

+        }

+

+        private String getName(int id) {

+            return "'" + getSimpleName(id) + "'";

+        }

+

+        private String lookup(int id) {

+            try {

+                if (mNames.containsKey(id)) {

+                    return mNames.get(id);

+                }

+                if (id != -1) {

+                    return mContext.getResources().getResourceEntryName(id);

+                } else {

+                    return "unknown" + ++mUnknownCount;

+                }

+            } catch (Exception ex) {

+                return "unknown" + ++mUnknownCount;

+            }

+        }

+

+        private void writeConstraint(String my,

+                                     int constraint,

+                                     String other,

+                                     int margin,

+                                     int goneMargin) throws IOException {

+            if (constraint == UNSET) {

+                return;

+            }

+            mWriter.write(INDENT + my);

+            mWriter.write(":[");

+            mWriter.write(getName(constraint));

+            mWriter.write(", ");

+            mWriter.write("'" + other + "'");

+            if (margin != 0 || goneMargin != UNSET_GONE_MARGIN) {

+                mWriter.write(", " + margin);

+                if (goneMargin != UNSET_GONE_MARGIN) {

+                    mWriter.write(", " + goneMargin);

+                }

+            }

+            mWriter.write("],\n");

+        }

+

+        private void writeCircle(int circleConstraint,

+                                 float circleAngle,

+                                 int circleRadius) throws IOException {

+            if (circleConstraint == UNSET) {

+                return;

+            }

+            mWriter.write(INDENT + "circle");

+            mWriter.write(":[");

+            mWriter.write(getName(circleConstraint));

+            mWriter.write(", " + circleAngle);

+            mWriter.write(circleRadius + "],\n");

+        }

+

+        private void writeVariable(String name, int value) throws IOException {

+            if (value == 0 || value == -1) {

+                return;

+            }

+            mWriter.write(INDENT + name);

+            mWriter.write(": " + value);

+            mWriter.write(",\n");

+        }

+

+        private void writeVariable(String name, float value) throws IOException {

+            if (value == UNSET) {

+                return;

+            }

+            mWriter.write(INDENT + name);

+            mWriter.write(": " + value);

+            mWriter.write(",\n");

+        }

+

+        private void writeVariable(String name, float value, float def) throws IOException {

+            if (value == def) {

+                return;

+            }

+            mWriter.write(INDENT + name);

+            mWriter.write(": " + value);

+            mWriter.write(",\n");

+        }

+

+        private void writeVariable(String name, boolean value, boolean def) throws IOException {

+            if (value == def) {

+                return;

+            }

+            mWriter.write(INDENT + name);

+            mWriter.write(": " + value);

+            mWriter.write(",\n");

+        }

+

+        private void writeVariable(String name, int[] value) throws IOException {

+            if (value == null) {

+                return;

+            }

+            mWriter.write(INDENT + name);

+            mWriter.write(": ");

+            for (int i = 0; i < value.length; i++) {

+                mWriter.write(((i == 0) ? "[" : ", ") + getName(value[i]));

+            }

+            mWriter.write("],\n");

+        }

+

+        private void writeVariable(String name, String value) throws IOException {

+            if (value == null) {

+                return;

+            }

+            mWriter.write(INDENT + name);

+            mWriter.write(": '" + value);

+            mWriter.write("',\n");

+        }

+    }

+}

diff --git a/constraintlayout/constraintlayout/src/main/res/values/attrs.xml b/constraintlayout/constraintlayout/src/main/res/values/attrs.xml
index d9029b17..aba4fb6 100644
--- a/constraintlayout/constraintlayout/src/main/res/values/attrs.xml
+++ b/constraintlayout/constraintlayout/src/main/res/values/attrs.xml
@@ -816,6 +816,21 @@
 
     </declare-styleable>
 
+    <!--    This is the api for logging constraints in JSON5 format -->
+    <declare-styleable name="LogJson">
+        <attr name="logJsonTo" format="enum|string">
+            <enum name="log" value="1" />
+            <enum name="console" value="2" />
+        </attr>
+        <attr name="logJsonMode" format="enum">
+            <enum name="periodic" value="1" />
+            <enum name="delayed" value="2" />
+            <enum name="layout" value="3" />
+            <enum name="api" value="4" />
+        </attr>
+        <attr name="logJsonDelay" format="integer" />
+    </declare-styleable>
+
     <declare-styleable name="Constraint">
         <attr name="android:orientation" />
         <attr name="android:id" />
diff --git a/core/core/api/current.txt b/core/core/api/current.txt
index aa9867b..3f5d92d 100644
--- a/core/core/api/current.txt
+++ b/core/core/api/current.txt
@@ -1804,6 +1804,7 @@
     method @Deprecated @ChecksSdkIntAtLeast(api=android.os.Build.VERSION_CODES.Q) public static boolean isAtLeastQ();
     method @Deprecated @ChecksSdkIntAtLeast(api=android.os.Build.VERSION_CODES.R) public static boolean isAtLeastR();
     method @Deprecated @ChecksSdkIntAtLeast(api=31, codename="S") public static boolean isAtLeastS();
+    field @ChecksSdkIntAtLeast(extension=android.os.ext.SdkExtensions.AD_SERVICES) public static final int AD_SERVICES_EXTENSION_INT;
     field @ChecksSdkIntAtLeast(extension=android.os.Build.VERSION_CODES.R) public static final int R_EXTENSION_INT;
     field @ChecksSdkIntAtLeast(extension=android.os.Build.VERSION_CODES.S) public static final int S_EXTENSION_INT;
     field @ChecksSdkIntAtLeast(extension=android.os.Build.VERSION_CODES.TIRAMISU) public static final int T_EXTENSION_INT;
@@ -3135,8 +3136,9 @@
     method public void setSystemBarsBehavior(int);
     method public void show(int);
     method @Deprecated @RequiresApi(30) public static androidx.core.view.WindowInsetsControllerCompat toWindowInsetsControllerCompat(android.view.WindowInsetsController);
-    field public static final int BEHAVIOR_SHOW_BARS_BY_SWIPE = 1; // 0x1
-    field public static final int BEHAVIOR_SHOW_BARS_BY_TOUCH = 0; // 0x0
+    field public static final int BEHAVIOR_DEFAULT = 1; // 0x1
+    field @Deprecated public static final int BEHAVIOR_SHOW_BARS_BY_SWIPE = 1; // 0x1
+    field @Deprecated public static final int BEHAVIOR_SHOW_BARS_BY_TOUCH = 0; // 0x0
     field public static final int BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE = 2; // 0x2
   }
 
diff --git a/core/core/api/public_plus_experimental_current.txt b/core/core/api/public_plus_experimental_current.txt
index 7960fa9..9df9106 100644
--- a/core/core/api/public_plus_experimental_current.txt
+++ b/core/core/api/public_plus_experimental_current.txt
@@ -1807,6 +1807,7 @@
     method @Deprecated @ChecksSdkIntAtLeast(api=32, codename="Sv2") @androidx.core.os.BuildCompat.PrereleaseSdkCheck public static boolean isAtLeastSv2();
     method @ChecksSdkIntAtLeast(api=33, codename="Tiramisu") @androidx.core.os.BuildCompat.PrereleaseSdkCheck public static boolean isAtLeastT();
     method @ChecksSdkIntAtLeast(codename="UpsideDownCake") @androidx.core.os.BuildCompat.PrereleaseSdkCheck public static boolean isAtLeastU();
+    field @ChecksSdkIntAtLeast(extension=android.os.ext.SdkExtensions.AD_SERVICES) public static final int AD_SERVICES_EXTENSION_INT;
     field @ChecksSdkIntAtLeast(extension=android.os.Build.VERSION_CODES.R) public static final int R_EXTENSION_INT;
     field @ChecksSdkIntAtLeast(extension=android.os.Build.VERSION_CODES.S) public static final int S_EXTENSION_INT;
     field @ChecksSdkIntAtLeast(extension=android.os.Build.VERSION_CODES.TIRAMISU) public static final int T_EXTENSION_INT;
@@ -3141,8 +3142,9 @@
     method public void setSystemBarsBehavior(int);
     method public void show(int);
     method @Deprecated @RequiresApi(30) public static androidx.core.view.WindowInsetsControllerCompat toWindowInsetsControllerCompat(android.view.WindowInsetsController);
-    field public static final int BEHAVIOR_SHOW_BARS_BY_SWIPE = 1; // 0x1
-    field public static final int BEHAVIOR_SHOW_BARS_BY_TOUCH = 0; // 0x0
+    field public static final int BEHAVIOR_DEFAULT = 1; // 0x1
+    field @Deprecated public static final int BEHAVIOR_SHOW_BARS_BY_SWIPE = 1; // 0x1
+    field @Deprecated public static final int BEHAVIOR_SHOW_BARS_BY_TOUCH = 0; // 0x0
     field public static final int BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE = 2; // 0x2
   }
 
diff --git a/core/core/api/restricted_current.txt b/core/core/api/restricted_current.txt
index 63e99d4..51353767 100644
--- a/core/core/api/restricted_current.txt
+++ b/core/core/api/restricted_current.txt
@@ -2144,6 +2144,7 @@
     method @Deprecated @ChecksSdkIntAtLeast(api=android.os.Build.VERSION_CODES.Q) public static boolean isAtLeastQ();
     method @Deprecated @ChecksSdkIntAtLeast(api=android.os.Build.VERSION_CODES.R) public static boolean isAtLeastR();
     method @Deprecated @ChecksSdkIntAtLeast(api=31, codename="S") public static boolean isAtLeastS();
+    field @ChecksSdkIntAtLeast(extension=android.os.ext.SdkExtensions.AD_SERVICES) public static final int AD_SERVICES_EXTENSION_INT;
     field @ChecksSdkIntAtLeast(extension=android.os.Build.VERSION_CODES.R) public static final int R_EXTENSION_INT;
     field @ChecksSdkIntAtLeast(extension=android.os.Build.VERSION_CODES.S) public static final int S_EXTENSION_INT;
     field @ChecksSdkIntAtLeast(extension=android.os.Build.VERSION_CODES.TIRAMISU) public static final int T_EXTENSION_INT;
@@ -3595,8 +3596,9 @@
     method public void setSystemBarsBehavior(int);
     method public void show(@androidx.core.view.WindowInsetsCompat.Type.InsetsType int);
     method @Deprecated @RequiresApi(30) public static androidx.core.view.WindowInsetsControllerCompat toWindowInsetsControllerCompat(android.view.WindowInsetsController);
-    field public static final int BEHAVIOR_SHOW_BARS_BY_SWIPE = 1; // 0x1
-    field public static final int BEHAVIOR_SHOW_BARS_BY_TOUCH = 0; // 0x0
+    field public static final int BEHAVIOR_DEFAULT = 1; // 0x1
+    field @Deprecated public static final int BEHAVIOR_SHOW_BARS_BY_SWIPE = 1; // 0x1
+    field @Deprecated public static final int BEHAVIOR_SHOW_BARS_BY_TOUCH = 0; // 0x0
     field public static final int BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE = 2; // 0x2
   }
 
diff --git a/core/core/src/androidTest/java/androidx/core/view/WindowInsetsControllerCompatActivityTest.kt b/core/core/src/androidTest/java/androidx/core/view/WindowInsetsControllerCompatActivityTest.kt
index b7bb15f..6a40e06 100644
--- a/core/core/src/androidTest/java/androidx/core/view/WindowInsetsControllerCompatActivityTest.kt
+++ b/core/core/src/androidTest/java/androidx/core/view/WindowInsetsControllerCompatActivityTest.kt
@@ -376,12 +376,31 @@
     }
 
     @Test
-    // minSdkVersion = 21 due to b/189492236
-    @SdkSuppress(minSdkVersion = 21, maxSdkVersion = 29) // Flag deprecated in 30+
-    public fun systemBarsBehavior_swipe() {
+    @SdkSuppress(minSdkVersion = 31) // Older APIs does not support getSystemBarsBehavior
+    fun systemBarsBehavior() {
         scenario.onActivity {
             windowInsetsController.systemBarsBehavior =
-                WindowInsetsControllerCompat.BEHAVIOR_SHOW_BARS_BY_SWIPE
+                WindowInsetsControllerCompat.BEHAVIOR_DEFAULT
+            assertEquals(
+                WindowInsetsControllerCompat.BEHAVIOR_DEFAULT,
+                windowInsetsController.systemBarsBehavior
+            )
+            windowInsetsController.systemBarsBehavior =
+                WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
+            assertEquals(
+                WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE,
+                windowInsetsController.systemBarsBehavior
+            )
+        }
+    }
+
+    @Test
+    // minSdkVersion = 21 due to b/189492236
+    @SdkSuppress(minSdkVersion = 21, maxSdkVersion = 29) // Flag deprecated in 30+
+    public fun systemBarsBehavior_default() {
+        scenario.onActivity {
+            windowInsetsController.systemBarsBehavior =
+                WindowInsetsControllerCompat.BEHAVIOR_DEFAULT
         }
         val decorView = scenario.withActivity { window.decorView }
         val sysUiVis = decorView.systemUiVisibility
@@ -409,23 +428,6 @@
         assertEquals(0, sysUiVis and View.SYSTEM_UI_FLAG_IMMERSIVE)
     }
 
-    @Test
-    public fun systemBarsBehavior_touch() {
-        scenario.onActivity {
-            windowInsetsController.systemBarsBehavior =
-                WindowInsetsControllerCompat.BEHAVIOR_SHOW_BARS_BY_TOUCH
-        }
-        val decorView = scenario.withActivity { window.decorView }
-        val sysUiVis = decorView.systemUiVisibility
-        assertEquals(
-            0,
-            sysUiVis and (
-                View.SYSTEM_UI_FLAG_IMMERSIVE
-                    or View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
-                )
-        )
-    }
-
     private fun assumeNotCuttlefish() {
         // TODO: remove this if b/159103848 is resolved
         assumeFalse(
diff --git a/core/core/src/main/java/androidx/core/os/BuildCompat.java b/core/core/src/main/java/androidx/core/os/BuildCompat.java
index e50f09e..223fba1 100644
--- a/core/core/src/main/java/androidx/core/os/BuildCompat.java
+++ b/core/core/src/main/java/androidx/core/os/BuildCompat.java
@@ -21,6 +21,7 @@
 import android.annotation.SuppressLint;
 import android.os.Build;
 import android.os.Build.VERSION;
+import android.os.ext.SdkExtensions;
 
 import androidx.annotation.ChecksSdkIntAtLeast;
 import androidx.annotation.NonNull;
@@ -281,6 +282,21 @@
     @SuppressLint("CompileTimeConstant")
     public static final int T_EXTENSION_INT = VERSION.SDK_INT >= 30 ? Extensions30Impl.TIRAMISU : 0;
 
+    /**
+     * The value of {@code SdkExtensions.getExtensionVersion(AD_SERVICES)}. This is a convenience
+     * constant which provides the extension version in a similar style to
+     * {@code Build.VERSION.SDK_INT}.
+     * <p>
+     * Compared to calling {@code getExtensionVersion} directly, using this constant has the
+     * benefit of not having to verify the {@code getExtensionVersion} method is available.
+     *
+     * @return the version of the AdServices extension, if it exists. 0 otherwise.
+     */
+    @ChecksSdkIntAtLeast(extension = SdkExtensions.AD_SERVICES)
+    @SuppressLint("CompileTimeConstant")
+    public static final int AD_SERVICES_EXTENSION_INT =
+            VERSION.SDK_INT >= 30 ? Extensions30Impl.AD_SERVICES : 0;
+
     @SuppressLint("ClassVerificationFailure") // Remove when SDK including b/206996004 is imported
     @RequiresApi(30)
     private static final class Extensions30Impl {
@@ -290,6 +306,8 @@
         static final int S = getExtensionVersion(Build.VERSION_CODES.S);
         @SuppressLint("NewApi") // Remove when SDK including b/206996004 is imported
         static final int TIRAMISU = getExtensionVersion(Build.VERSION_CODES.TIRAMISU);
+        @SuppressLint("NewApi") // Remove when SDK including b/206996004 is imported
+        static final int AD_SERVICES = getExtensionVersion(SdkExtensions.AD_SERVICES);
     }
 
 }
diff --git a/core/core/src/main/java/androidx/core/view/WindowInsetsControllerCompat.java b/core/core/src/main/java/androidx/core/view/WindowInsetsControllerCompat.java
index 9a49066..bff7344 100644
--- a/core/core/src/main/java/androidx/core/view/WindowInsetsControllerCompat.java
+++ b/core/core/src/main/java/androidx/core/view/WindowInsetsControllerCompat.java
@@ -54,22 +54,43 @@
 public final class WindowInsetsControllerCompat {
 
     /**
-     * The default option for {@link #setSystemBarsBehavior(int)}. System bars will be forcibly
-     * shown on any user interaction on the corresponding display if navigation bars are hidden
-     * by {@link #hide(int)} or
+     * Option for {@link #setSystemBarsBehavior(int)}. System bars will be forcibly shown on any
+     * user interaction on the corresponding display if navigation bars are hidden by
+     * {@link #hide(int)} or
      * {@link WindowInsetsAnimationControllerCompat#setInsetsAndAlpha(Insets, float, float)}.
+     *
+     * @deprecated This is not supported on Android {@link android.os.Build.VERSION_CODES#S} and
+     * later. Use {@link #BEHAVIOR_DEFAULT} or {@link #BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE}
+     * instead.
      */
+    @Deprecated
     public static final int BEHAVIOR_SHOW_BARS_BY_TOUCH = 0;
 
     /**
+     * The default option for {@link #setSystemBarsBehavior(int)}: Window would like to remain
+     * interactive when hiding navigation bars by calling {@link #hide(int)} or
+     * {@link WindowInsetsAnimationControllerCompat#setInsetsAndAlpha(Insets, float, float)}.
+     *
+     * <p>When system bars are hidden in this mode, they can be revealed with system gestures, such
+     * as swiping from the edge of the screen where the bar is hidden from.</p>
+     *
+     * <p>When the gesture navigation is enabled, the system gestures can be triggered regardless
+     * the visibility of system bars.</p>
+     */
+    public static final int BEHAVIOR_DEFAULT = 1;
+
+    /**
      * Option for {@link #setSystemBarsBehavior(int)}: Window would like to remain interactive
      * when hiding navigation bars by calling {@link #hide(int)} or
      * {@link WindowInsetsAnimationControllerCompat#setInsetsAndAlpha(Insets, float, float)}.
      * <p>
      * When system bars are hidden in this mode, they can be revealed with system
      * gestures, such as swiping from the edge of the screen where the bar is hidden from.
+     *
+     * @deprecated Use {@link #BEHAVIOR_DEFAULT} instead.
      */
-    public static final int BEHAVIOR_SHOW_BARS_BY_SWIPE = 1;
+    @Deprecated
+    public static final int BEHAVIOR_SHOW_BARS_BY_SWIPE = BEHAVIOR_DEFAULT;
 
     /**
      * Option for {@link #setSystemBarsBehavior(int)}: Window would like to remain
@@ -135,8 +156,7 @@
      */
     @RestrictTo(RestrictTo.Scope.LIBRARY)
     @Retention(RetentionPolicy.SOURCE)
-    @IntDef(value = {BEHAVIOR_SHOW_BARS_BY_TOUCH, BEHAVIOR_SHOW_BARS_BY_SWIPE,
-            BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE})
+    @IntDef(value = {BEHAVIOR_DEFAULT, BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE})
     @interface Behavior {
     }
 
@@ -515,7 +535,7 @@
         @Override
         void setSystemBarsBehavior(int behavior) {
             switch (behavior) {
-                case BEHAVIOR_SHOW_BARS_BY_SWIPE:
+                case BEHAVIOR_DEFAULT:
                     unsetSystemUiFlag(View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY);
                     setSystemUiFlag(View.SYSTEM_UI_FLAG_IMMERSIVE);
                     break;
diff --git a/emoji2/emoji2-bundled/api/1.3.0-beta01.txt b/emoji2/emoji2-bundled/api/1.3.0-beta01.txt
new file mode 100644
index 0000000..8749c28
--- /dev/null
+++ b/emoji2/emoji2-bundled/api/1.3.0-beta01.txt
@@ -0,0 +1,9 @@
+// Signature format: 4.0
+package androidx.emoji2.bundled {
+
+  public class BundledEmojiCompatConfig extends androidx.emoji2.text.EmojiCompat.Config {
+    ctor public BundledEmojiCompatConfig(android.content.Context);
+  }
+
+}
+
diff --git a/emoji2/emoji2-bundled/api/public_plus_experimental_1.3.0-beta01.txt b/emoji2/emoji2-bundled/api/public_plus_experimental_1.3.0-beta01.txt
new file mode 100644
index 0000000..8749c28
--- /dev/null
+++ b/emoji2/emoji2-bundled/api/public_plus_experimental_1.3.0-beta01.txt
@@ -0,0 +1,9 @@
+// Signature format: 4.0
+package androidx.emoji2.bundled {
+
+  public class BundledEmojiCompatConfig extends androidx.emoji2.text.EmojiCompat.Config {
+    ctor public BundledEmojiCompatConfig(android.content.Context);
+  }
+
+}
+
diff --git a/emoji2/emoji2-bundled/api/res-1.3.0-beta01.txt b/emoji2/emoji2-bundled/api/res-1.3.0-beta01.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/emoji2/emoji2-bundled/api/res-1.3.0-beta01.txt
diff --git a/emoji2/emoji2-bundled/api/restricted_1.3.0-beta01.txt b/emoji2/emoji2-bundled/api/restricted_1.3.0-beta01.txt
new file mode 100644
index 0000000..8749c28
--- /dev/null
+++ b/emoji2/emoji2-bundled/api/restricted_1.3.0-beta01.txt
@@ -0,0 +1,9 @@
+// Signature format: 4.0
+package androidx.emoji2.bundled {
+
+  public class BundledEmojiCompatConfig extends androidx.emoji2.text.EmojiCompat.Config {
+    ctor public BundledEmojiCompatConfig(android.content.Context);
+  }
+
+}
+
diff --git a/emoji2/emoji2-bundled/src/androidTest/java/androidx/emoji2/bundled/AllEmojisTest.java b/emoji2/emoji2-bundled/src/androidTest/java/androidx/emoji2/bundled/AllEmojisTest.java
index 929dbc8..1cf822f 100644
--- a/emoji2/emoji2-bundled/src/androidTest/java/androidx/emoji2/bundled/AllEmojisTest.java
+++ b/emoji2/emoji2-bundled/src/androidTest/java/androidx/emoji2/bundled/AllEmojisTest.java
@@ -22,6 +22,8 @@
 import android.content.Context;
 import android.graphics.Paint;
 import android.text.Spanned;
+import android.text.StaticLayout;
+import android.text.TextPaint;
 
 import androidx.core.graphics.PaintCompat;
 import androidx.emoji2.bundled.util.EmojiMatcher;
@@ -135,6 +137,23 @@
                 EmojiCompat.get().hasEmojiGlyph(mString, Integer.MAX_VALUE));
     }
 
+    @Test
+    public void emoji_hasDesiredWidth() {
+        TextPaint tp = new TextPaint();
+        // spanned, test fails, width == 0
+        CharSequence processedText = EmojiCompat.get()
+                .process(
+                        mString,
+                        /* start= */ 0,
+                        /* end= */ mString.length(),
+                        /* maxEmojiCount= */ mString.length(),
+                        EmojiCompat.REPLACE_STRATEGY_ALL);
+        float emojiCompatWidth = StaticLayout.getDesiredWidth(processedText, tp);
+        assertTrue("emoji " + mString + " with codepoints " + mCodepoints + "has "
+                + "desired width " + emojiCompatWidth,
+                emojiCompatWidth > 0);
+    }
+
     private void assertSpanCanRenderEmoji(final String str) {
         final Spanned spanned = (Spanned) EmojiCompat.get().process(new TestString(str).toString());
         final EmojiSpan[] spans = spanned.getSpans(0, spanned.length(), EmojiSpan.class);
diff --git a/emoji2/emoji2-emojipicker/build.gradle b/emoji2/emoji2-emojipicker/build.gradle
index be8163d..3ea643f 100644
--- a/emoji2/emoji2-emojipicker/build.gradle
+++ b/emoji2/emoji2-emojipicker/build.gradle
@@ -60,6 +60,7 @@
 androidx {
     name = "androidx.emoji2:emoji2-emojipicker"
     type = LibraryType.PUBLISHED_LIBRARY
+    mavenVersion = LibraryVersions.EMOJI2_QUARANTINE
     inceptionYear = "2022"
     description = "This library provides the latest emoji support and emoji picker UI to input " +
             "emoji in current and older devices"
diff --git a/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiPickerConstants.kt b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiPickerConstants.kt
index 989a8e3..479e1ff 100644
--- a/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiPickerConstants.kt
+++ b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiPickerConstants.kt
@@ -30,4 +30,9 @@
 
     // The max pool size of the Emoji ItemType in RecyclerViewPool.
     const val EMOJI_VIEW_POOL_SIZE = 100
+
+    const val ADD_VIEW_EXCEPTION_MESSAGE = "Adding views to the EmojiPickerView is unsupported"
+
+    const val REMOVE_VIEW_EXCEPTION_MESSAGE =
+        "Removing views from the EmojiPickerView is unsupported"
 }
\ No newline at end of file
diff --git a/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiPickerView.kt b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiPickerView.kt
index ef30c3b..79ce7be 100644
--- a/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiPickerView.kt
+++ b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiPickerView.kt
@@ -20,6 +20,7 @@
 import android.content.res.TypedArray
 import android.util.AttributeSet
 import android.view.View
+import android.view.ViewGroup
 import android.widget.FrameLayout
 import androidx.core.util.Consumer
 import androidx.core.view.ViewCompat
@@ -46,9 +47,10 @@
     /**
      * The number of rows of the emoji picker.
      *
-     * Default value will be used if emojiGridRows is set to non-positive value. Float value
-     * indicates that we will display partial of the last row and have content down, so the users
-     * get the idea that they can scroll down for more contents.
+     * Default value([EmojiPickerConstants.DEFAULT_BODY_ROWS]: 7.5) will be used if emojiGridRows
+     * is set to non-positive value. Float value indicates that we will display partial of the last
+     * row and have content down, so the users get the idea that they can scroll down for more
+     * contents.
      * @attr ref androidx.emoji2.emojipicker.R.styleable.EmojiPickerView_emojiGridRows
      */
     var emojiGridRows: Float = EmojiPickerConstants.DEFAULT_BODY_ROWS
@@ -63,7 +65,8 @@
     /**
      * The number of columns of the emoji picker.
      *
-     * Default value will be used if emojiGridColumns is set to non-positive value.
+     * Default value([EmojiPickerConstants.DEFAULT_BODY_COLUMNS]: 9) will be used if
+     * emojiGridColumns is set to non-positive value.
      * @attr ref androidx.emoji2.emojipicker.R.styleable.EmojiPickerView_emojiGridColumns
      */
     var emojiGridColumns: Int = EmojiPickerConstants.DEFAULT_BODY_COLUMNS
@@ -180,7 +183,7 @@
             })
 
         // clear view's children in case of resetting layout
-        removeAllViews()
+        super.removeAllViews()
         with(inflate(context, R.layout.emoji_picker, this)) {
             // set headerView
             ViewCompat.requireViewById<RecyclerView>(this, R.id.emoji_picker_header).apply {
@@ -259,14 +262,110 @@
     }
 
     /**
-     * Disallow clients to add view to the EmojiPickerView
+     * The following functions disallow clients to add view to the EmojiPickerView
      *
      * @param child the child view to be added
      * @throws UnsupportedOperationException
      */
     override fun addView(child: View?) {
-        throw UnsupportedOperationException(
-            "Adding views to the EmojiPickerView is unsupported"
-        )
+        if (childCount > 0)
+            throw UnsupportedOperationException(EmojiPickerConstants.ADD_VIEW_EXCEPTION_MESSAGE)
+        else super.addView(child)
+    }
+
+    /**
+     * @param child
+     * @param params
+     * @throws UnsupportedOperationException
+     */
+    override fun addView(child: View?, params: ViewGroup.LayoutParams?) {
+        if (childCount > 0)
+            throw UnsupportedOperationException(EmojiPickerConstants.ADD_VIEW_EXCEPTION_MESSAGE)
+        else super.addView(child, params)
+    }
+
+    /**
+     * @param child
+     * @param index
+     * @throws UnsupportedOperationException
+     */
+    override fun addView(child: View?, index: Int) {
+        if (childCount > 0)
+            throw UnsupportedOperationException(EmojiPickerConstants.ADD_VIEW_EXCEPTION_MESSAGE)
+        else super.addView(child, index)
+    }
+
+    /**
+     * @param child
+     * @param index
+     * @param params
+     * @throws UnsupportedOperationException
+     */
+    override fun addView(child: View?, index: Int, params: ViewGroup.LayoutParams?) {
+        if (childCount > 0)
+            throw UnsupportedOperationException(EmojiPickerConstants.ADD_VIEW_EXCEPTION_MESSAGE)
+        else super.addView(child, index, params)
+    }
+
+    /**
+     * @param child
+     * @param width
+     * @param height
+     * @throws UnsupportedOperationException
+     */
+    override fun addView(child: View?, width: Int, height: Int) {
+        if (childCount > 0)
+            throw UnsupportedOperationException(EmojiPickerConstants.ADD_VIEW_EXCEPTION_MESSAGE)
+        else super.addView(child, width, height)
+    }
+
+    /**
+     * The following functions disallow clients to remove view from the EmojiPickerView
+     * @throws UnsupportedOperationException
+     */
+    override fun removeAllViews() {
+        throw UnsupportedOperationException(EmojiPickerConstants.REMOVE_VIEW_EXCEPTION_MESSAGE)
+    }
+
+    /**
+     * @param child
+     * @throws UnsupportedOperationException
+     */
+    override fun removeView(child: View?) {
+        throw UnsupportedOperationException(EmojiPickerConstants.REMOVE_VIEW_EXCEPTION_MESSAGE)
+    }
+
+    /**
+     * @param index
+     * @throws UnsupportedOperationException
+     */
+    override fun removeViewAt(index: Int) {
+        throw UnsupportedOperationException(EmojiPickerConstants.REMOVE_VIEW_EXCEPTION_MESSAGE)
+    }
+
+    /**
+     * @param child
+     * @throws UnsupportedOperationException
+     */
+    override fun removeViewInLayout(child: View?) {
+        throw UnsupportedOperationException(EmojiPickerConstants.REMOVE_VIEW_EXCEPTION_MESSAGE)
+    }
+
+    /**
+     * @param start
+     * @param count
+     * @throws UnsupportedOperationException
+     */
+    override fun removeViews(start: Int, count: Int) {
+        throw UnsupportedOperationException(EmojiPickerConstants.REMOVE_VIEW_EXCEPTION_MESSAGE)
+    }
+
+    /**
+     * @param start
+     * @param count
+     * @throws UnsupportedOperationException
+     */
+    override fun removeViewsInLayout(start: Int, count: Int) {
+        throw UnsupportedOperationException(EmojiPickerConstants.REMOVE_VIEW_EXCEPTION_MESSAGE)
     }
 }
diff --git a/emoji2/emoji2-views-helper/api/1.3.0-beta01.txt b/emoji2/emoji2-views-helper/api/1.3.0-beta01.txt
new file mode 100644
index 0000000..30a6feb
--- /dev/null
+++ b/emoji2/emoji2-views-helper/api/1.3.0-beta01.txt
@@ -0,0 +1,27 @@
+// Signature format: 4.0
+package androidx.emoji2.viewsintegration {
+
+  public final class EmojiEditTextHelper {
+    ctor public EmojiEditTextHelper(android.widget.EditText);
+    ctor public EmojiEditTextHelper(android.widget.EditText, boolean);
+    method public android.text.method.KeyListener? getKeyListener(android.text.method.KeyListener?);
+    method public int getMaxEmojiCount();
+    method public boolean isEnabled();
+    method public android.view.inputmethod.InputConnection? onCreateInputConnection(android.view.inputmethod.InputConnection?, android.view.inputmethod.EditorInfo);
+    method public void setEnabled(boolean);
+    method public void setMaxEmojiCount(@IntRange(from=0) int);
+  }
+
+  public final class EmojiTextViewHelper {
+    ctor public EmojiTextViewHelper(android.widget.TextView);
+    ctor public EmojiTextViewHelper(android.widget.TextView, boolean);
+    method public android.text.InputFilter![] getFilters(android.text.InputFilter![]);
+    method public boolean isEnabled();
+    method public void setAllCaps(boolean);
+    method public void setEnabled(boolean);
+    method public void updateTransformationMethod();
+    method public android.text.method.TransformationMethod? wrapTransformationMethod(android.text.method.TransformationMethod?);
+  }
+
+}
+
diff --git a/emoji2/emoji2-views-helper/api/public_plus_experimental_1.3.0-beta01.txt b/emoji2/emoji2-views-helper/api/public_plus_experimental_1.3.0-beta01.txt
new file mode 100644
index 0000000..30a6feb
--- /dev/null
+++ b/emoji2/emoji2-views-helper/api/public_plus_experimental_1.3.0-beta01.txt
@@ -0,0 +1,27 @@
+// Signature format: 4.0
+package androidx.emoji2.viewsintegration {
+
+  public final class EmojiEditTextHelper {
+    ctor public EmojiEditTextHelper(android.widget.EditText);
+    ctor public EmojiEditTextHelper(android.widget.EditText, boolean);
+    method public android.text.method.KeyListener? getKeyListener(android.text.method.KeyListener?);
+    method public int getMaxEmojiCount();
+    method public boolean isEnabled();
+    method public android.view.inputmethod.InputConnection? onCreateInputConnection(android.view.inputmethod.InputConnection?, android.view.inputmethod.EditorInfo);
+    method public void setEnabled(boolean);
+    method public void setMaxEmojiCount(@IntRange(from=0) int);
+  }
+
+  public final class EmojiTextViewHelper {
+    ctor public EmojiTextViewHelper(android.widget.TextView);
+    ctor public EmojiTextViewHelper(android.widget.TextView, boolean);
+    method public android.text.InputFilter![] getFilters(android.text.InputFilter![]);
+    method public boolean isEnabled();
+    method public void setAllCaps(boolean);
+    method public void setEnabled(boolean);
+    method public void updateTransformationMethod();
+    method public android.text.method.TransformationMethod? wrapTransformationMethod(android.text.method.TransformationMethod?);
+  }
+
+}
+
diff --git a/emoji2/emoji2-views-helper/api/res-1.3.0-beta01.txt b/emoji2/emoji2-views-helper/api/res-1.3.0-beta01.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/emoji2/emoji2-views-helper/api/res-1.3.0-beta01.txt
diff --git a/emoji2/emoji2-views-helper/api/restricted_1.3.0-beta01.txt b/emoji2/emoji2-views-helper/api/restricted_1.3.0-beta01.txt
new file mode 100644
index 0000000..30a6feb
--- /dev/null
+++ b/emoji2/emoji2-views-helper/api/restricted_1.3.0-beta01.txt
@@ -0,0 +1,27 @@
+// Signature format: 4.0
+package androidx.emoji2.viewsintegration {
+
+  public final class EmojiEditTextHelper {
+    ctor public EmojiEditTextHelper(android.widget.EditText);
+    ctor public EmojiEditTextHelper(android.widget.EditText, boolean);
+    method public android.text.method.KeyListener? getKeyListener(android.text.method.KeyListener?);
+    method public int getMaxEmojiCount();
+    method public boolean isEnabled();
+    method public android.view.inputmethod.InputConnection? onCreateInputConnection(android.view.inputmethod.InputConnection?, android.view.inputmethod.EditorInfo);
+    method public void setEnabled(boolean);
+    method public void setMaxEmojiCount(@IntRange(from=0) int);
+  }
+
+  public final class EmojiTextViewHelper {
+    ctor public EmojiTextViewHelper(android.widget.TextView);
+    ctor public EmojiTextViewHelper(android.widget.TextView, boolean);
+    method public android.text.InputFilter![] getFilters(android.text.InputFilter![]);
+    method public boolean isEnabled();
+    method public void setAllCaps(boolean);
+    method public void setEnabled(boolean);
+    method public void updateTransformationMethod();
+    method public android.text.method.TransformationMethod? wrapTransformationMethod(android.text.method.TransformationMethod?);
+  }
+
+}
+
diff --git a/emoji2/emoji2-views/api/1.3.0-beta01.txt b/emoji2/emoji2-views/api/1.3.0-beta01.txt
new file mode 100644
index 0000000..879b30e
--- /dev/null
+++ b/emoji2/emoji2-views/api/1.3.0-beta01.txt
@@ -0,0 +1,34 @@
+// Signature format: 4.0
+package androidx.emoji2.widget {
+
+  public class EmojiButton extends android.widget.Button {
+    ctor public EmojiButton(android.content.Context);
+    ctor public EmojiButton(android.content.Context, android.util.AttributeSet?);
+    ctor public EmojiButton(android.content.Context, android.util.AttributeSet?, int);
+  }
+
+  public class EmojiEditText extends android.widget.EditText {
+    ctor public EmojiEditText(android.content.Context);
+    ctor public EmojiEditText(android.content.Context, android.util.AttributeSet?);
+    ctor public EmojiEditText(android.content.Context, android.util.AttributeSet?, int);
+    method public int getMaxEmojiCount();
+    method public void setMaxEmojiCount(@IntRange(from=0) int);
+  }
+
+  public class EmojiExtractTextLayout extends android.widget.LinearLayout {
+    ctor public EmojiExtractTextLayout(android.content.Context);
+    ctor public EmojiExtractTextLayout(android.content.Context, android.util.AttributeSet?);
+    ctor public EmojiExtractTextLayout(android.content.Context, android.util.AttributeSet?, int);
+    method public int getEmojiReplaceStrategy();
+    method public void onUpdateExtractingViews(android.inputmethodservice.InputMethodService, android.view.inputmethod.EditorInfo);
+    method public void setEmojiReplaceStrategy(int);
+  }
+
+  public class EmojiTextView extends android.widget.TextView {
+    ctor public EmojiTextView(android.content.Context);
+    ctor public EmojiTextView(android.content.Context, android.util.AttributeSet?);
+    ctor public EmojiTextView(android.content.Context, android.util.AttributeSet?, int);
+  }
+
+}
+
diff --git a/emoji2/emoji2-views/api/public_plus_experimental_1.3.0-beta01.txt b/emoji2/emoji2-views/api/public_plus_experimental_1.3.0-beta01.txt
new file mode 100644
index 0000000..879b30e
--- /dev/null
+++ b/emoji2/emoji2-views/api/public_plus_experimental_1.3.0-beta01.txt
@@ -0,0 +1,34 @@
+// Signature format: 4.0
+package androidx.emoji2.widget {
+
+  public class EmojiButton extends android.widget.Button {
+    ctor public EmojiButton(android.content.Context);
+    ctor public EmojiButton(android.content.Context, android.util.AttributeSet?);
+    ctor public EmojiButton(android.content.Context, android.util.AttributeSet?, int);
+  }
+
+  public class EmojiEditText extends android.widget.EditText {
+    ctor public EmojiEditText(android.content.Context);
+    ctor public EmojiEditText(android.content.Context, android.util.AttributeSet?);
+    ctor public EmojiEditText(android.content.Context, android.util.AttributeSet?, int);
+    method public int getMaxEmojiCount();
+    method public void setMaxEmojiCount(@IntRange(from=0) int);
+  }
+
+  public class EmojiExtractTextLayout extends android.widget.LinearLayout {
+    ctor public EmojiExtractTextLayout(android.content.Context);
+    ctor public EmojiExtractTextLayout(android.content.Context, android.util.AttributeSet?);
+    ctor public EmojiExtractTextLayout(android.content.Context, android.util.AttributeSet?, int);
+    method public int getEmojiReplaceStrategy();
+    method public void onUpdateExtractingViews(android.inputmethodservice.InputMethodService, android.view.inputmethod.EditorInfo);
+    method public void setEmojiReplaceStrategy(int);
+  }
+
+  public class EmojiTextView extends android.widget.TextView {
+    ctor public EmojiTextView(android.content.Context);
+    ctor public EmojiTextView(android.content.Context, android.util.AttributeSet?);
+    ctor public EmojiTextView(android.content.Context, android.util.AttributeSet?, int);
+  }
+
+}
+
diff --git a/emoji2/emoji2-views/api/res-1.3.0-beta01.txt b/emoji2/emoji2-views/api/res-1.3.0-beta01.txt
new file mode 100644
index 0000000..8bc8423
--- /dev/null
+++ b/emoji2/emoji2-views/api/res-1.3.0-beta01.txt
@@ -0,0 +1,2 @@
+attr emojiReplaceStrategy
+attr maxEmojiCount
diff --git a/emoji2/emoji2-views/api/restricted_1.3.0-beta01.txt b/emoji2/emoji2-views/api/restricted_1.3.0-beta01.txt
new file mode 100644
index 0000000..879b30e
--- /dev/null
+++ b/emoji2/emoji2-views/api/restricted_1.3.0-beta01.txt
@@ -0,0 +1,34 @@
+// Signature format: 4.0
+package androidx.emoji2.widget {
+
+  public class EmojiButton extends android.widget.Button {
+    ctor public EmojiButton(android.content.Context);
+    ctor public EmojiButton(android.content.Context, android.util.AttributeSet?);
+    ctor public EmojiButton(android.content.Context, android.util.AttributeSet?, int);
+  }
+
+  public class EmojiEditText extends android.widget.EditText {
+    ctor public EmojiEditText(android.content.Context);
+    ctor public EmojiEditText(android.content.Context, android.util.AttributeSet?);
+    ctor public EmojiEditText(android.content.Context, android.util.AttributeSet?, int);
+    method public int getMaxEmojiCount();
+    method public void setMaxEmojiCount(@IntRange(from=0) int);
+  }
+
+  public class EmojiExtractTextLayout extends android.widget.LinearLayout {
+    ctor public EmojiExtractTextLayout(android.content.Context);
+    ctor public EmojiExtractTextLayout(android.content.Context, android.util.AttributeSet?);
+    ctor public EmojiExtractTextLayout(android.content.Context, android.util.AttributeSet?, int);
+    method public int getEmojiReplaceStrategy();
+    method public void onUpdateExtractingViews(android.inputmethodservice.InputMethodService, android.view.inputmethod.EditorInfo);
+    method public void setEmojiReplaceStrategy(int);
+  }
+
+  public class EmojiTextView extends android.widget.TextView {
+    ctor public EmojiTextView(android.content.Context);
+    ctor public EmojiTextView(android.content.Context, android.util.AttributeSet?);
+    ctor public EmojiTextView(android.content.Context, android.util.AttributeSet?, int);
+  }
+
+}
+
diff --git a/emoji2/emoji2/api/1.3.0-beta01.txt b/emoji2/emoji2/api/1.3.0-beta01.txt
new file mode 100644
index 0000000..11d9335
--- /dev/null
+++ b/emoji2/emoji2/api/1.3.0-beta01.txt
@@ -0,0 +1,131 @@
+// Signature format: 4.0
+package androidx.emoji2.text {
+
+  public final class DefaultEmojiCompatConfig {
+    method public static androidx.emoji2.text.FontRequestEmojiCompatConfig? create(android.content.Context);
+  }
+
+  @AnyThread public class EmojiCompat {
+    method public static androidx.emoji2.text.EmojiCompat get();
+    method public String getAssetSignature();
+    method public int getEmojiEnd(CharSequence, @IntRange(from=0) int);
+    method public int getEmojiMatch(CharSequence, @IntRange(from=0) int);
+    method public int getEmojiStart(CharSequence, @IntRange(from=0) int);
+    method public int getLoadState();
+    method public static boolean handleDeleteSurroundingText(android.view.inputmethod.InputConnection, android.text.Editable, @IntRange(from=0) int, @IntRange(from=0) int, boolean);
+    method public static boolean handleOnKeyDown(android.text.Editable, int, android.view.KeyEvent);
+    method @Deprecated public boolean hasEmojiGlyph(CharSequence);
+    method @Deprecated public boolean hasEmojiGlyph(CharSequence, @IntRange(from=0) int);
+    method public static androidx.emoji2.text.EmojiCompat? init(android.content.Context);
+    method public static androidx.emoji2.text.EmojiCompat init(androidx.emoji2.text.EmojiCompat.Config);
+    method public static boolean isConfigured();
+    method public void load();
+    method @CheckResult public CharSequence? process(CharSequence?);
+    method @CheckResult public CharSequence? process(CharSequence?, @IntRange(from=0) int, @IntRange(from=0) int);
+    method @CheckResult public CharSequence? process(CharSequence?, @IntRange(from=0) int, @IntRange(from=0) int, @IntRange(from=0) int);
+    method @CheckResult public CharSequence? process(CharSequence?, @IntRange(from=0) int, @IntRange(from=0) int, @IntRange(from=0) int, int);
+    method public void registerInitCallback(androidx.emoji2.text.EmojiCompat.InitCallback);
+    method public void unregisterInitCallback(androidx.emoji2.text.EmojiCompat.InitCallback);
+    method public void updateEditorInfo(android.view.inputmethod.EditorInfo);
+    field public static final String EDITOR_INFO_METAVERSION_KEY = "android.support.text.emoji.emojiCompat_metadataVersion";
+    field public static final String EDITOR_INFO_REPLACE_ALL_KEY = "android.support.text.emoji.emojiCompat_replaceAll";
+    field public static final int EMOJI_FALLBACK = 2; // 0x2
+    field public static final int EMOJI_SUPPORTED = 1; // 0x1
+    field public static final int EMOJI_UNSUPPORTED = 0; // 0x0
+    field public static final int LOAD_STATE_DEFAULT = 3; // 0x3
+    field public static final int LOAD_STATE_FAILED = 2; // 0x2
+    field public static final int LOAD_STATE_LOADING = 0; // 0x0
+    field public static final int LOAD_STATE_SUCCEEDED = 1; // 0x1
+    field public static final int LOAD_STRATEGY_DEFAULT = 0; // 0x0
+    field public static final int LOAD_STRATEGY_MANUAL = 1; // 0x1
+    field public static final int REPLACE_STRATEGY_ALL = 1; // 0x1
+    field public static final int REPLACE_STRATEGY_DEFAULT = 0; // 0x0
+    field public static final int REPLACE_STRATEGY_NON_EXISTENT = 2; // 0x2
+  }
+
+  public abstract static class EmojiCompat.Config {
+    ctor protected EmojiCompat.Config(androidx.emoji2.text.EmojiCompat.MetadataRepoLoader);
+    method protected final androidx.emoji2.text.EmojiCompat.MetadataRepoLoader getMetadataRepoLoader();
+    method public androidx.emoji2.text.EmojiCompat.Config registerInitCallback(androidx.emoji2.text.EmojiCompat.InitCallback);
+    method public androidx.emoji2.text.EmojiCompat.Config setEmojiSpanIndicatorColor(@ColorInt int);
+    method public androidx.emoji2.text.EmojiCompat.Config setEmojiSpanIndicatorEnabled(boolean);
+    method public androidx.emoji2.text.EmojiCompat.Config setGlyphChecker(androidx.emoji2.text.EmojiCompat.GlyphChecker);
+    method public androidx.emoji2.text.EmojiCompat.Config setMetadataLoadStrategy(int);
+    method public androidx.emoji2.text.EmojiCompat.Config setReplaceAll(boolean);
+    method public androidx.emoji2.text.EmojiCompat.Config setSpanFactory(androidx.emoji2.text.EmojiCompat.SpanFactory);
+    method public androidx.emoji2.text.EmojiCompat.Config setUseEmojiAsDefaultStyle(boolean);
+    method public androidx.emoji2.text.EmojiCompat.Config setUseEmojiAsDefaultStyle(boolean, java.util.List<java.lang.Integer!>?);
+    method public androidx.emoji2.text.EmojiCompat.Config unregisterInitCallback(androidx.emoji2.text.EmojiCompat.InitCallback);
+  }
+
+  public static interface EmojiCompat.GlyphChecker {
+    method public boolean hasGlyph(CharSequence, @IntRange(from=0) int, @IntRange(from=0) int, @IntRange(from=0) int);
+  }
+
+  public abstract static class EmojiCompat.InitCallback {
+    ctor public EmojiCompat.InitCallback();
+    method public void onFailed(Throwable?);
+    method public void onInitialized();
+  }
+
+  public static interface EmojiCompat.MetadataRepoLoader {
+    method public void load(androidx.emoji2.text.EmojiCompat.MetadataRepoLoaderCallback);
+  }
+
+  public abstract static class EmojiCompat.MetadataRepoLoaderCallback {
+    ctor public EmojiCompat.MetadataRepoLoaderCallback();
+    method public abstract void onFailed(Throwable?);
+    method public abstract void onLoaded(androidx.emoji2.text.MetadataRepo);
+  }
+
+  public static interface EmojiCompat.SpanFactory {
+    method @RequiresApi(19) public androidx.emoji2.text.EmojiSpan createSpan(androidx.emoji2.text.TypefaceEmojiRasterizer);
+  }
+
+  public class EmojiCompatInitializer implements androidx.startup.Initializer<java.lang.Boolean> {
+    ctor public EmojiCompatInitializer();
+    method public Boolean create(android.content.Context);
+    method public java.util.List<java.lang.Class<? extends androidx.startup.Initializer<?>>!> dependencies();
+  }
+
+  @RequiresApi(19) public abstract class EmojiSpan extends android.text.style.ReplacementSpan {
+    method public int getSize(android.graphics.Paint, CharSequence!, int, int, android.graphics.Paint.FontMetricsInt?);
+    method public final androidx.emoji2.text.TypefaceEmojiRasterizer getTypefaceRasterizer();
+  }
+
+  public class FontRequestEmojiCompatConfig extends androidx.emoji2.text.EmojiCompat.Config {
+    ctor public FontRequestEmojiCompatConfig(android.content.Context, androidx.core.provider.FontRequest);
+    method @Deprecated public androidx.emoji2.text.FontRequestEmojiCompatConfig setHandler(android.os.Handler?);
+    method public androidx.emoji2.text.FontRequestEmojiCompatConfig setLoadingExecutor(java.util.concurrent.Executor);
+    method public androidx.emoji2.text.FontRequestEmojiCompatConfig setRetryPolicy(androidx.emoji2.text.FontRequestEmojiCompatConfig.RetryPolicy?);
+  }
+
+  public static class FontRequestEmojiCompatConfig.ExponentialBackoffRetryPolicy extends androidx.emoji2.text.FontRequestEmojiCompatConfig.RetryPolicy {
+    ctor public FontRequestEmojiCompatConfig.ExponentialBackoffRetryPolicy(long);
+    method public long getRetryDelay();
+  }
+
+  public abstract static class FontRequestEmojiCompatConfig.RetryPolicy {
+    ctor public FontRequestEmojiCompatConfig.RetryPolicy();
+    method public abstract long getRetryDelay();
+  }
+
+  @AnyThread @RequiresApi(19) public final class MetadataRepo {
+    method public static androidx.emoji2.text.MetadataRepo create(android.graphics.Typeface, java.io.InputStream) throws java.io.IOException;
+    method public static androidx.emoji2.text.MetadataRepo create(android.graphics.Typeface, java.nio.ByteBuffer) throws java.io.IOException;
+    method public static androidx.emoji2.text.MetadataRepo create(android.content.res.AssetManager, String) throws java.io.IOException;
+  }
+
+  @AnyThread @RequiresApi(19) public class TypefaceEmojiRasterizer {
+    method public void draw(android.graphics.Canvas, float, float, android.graphics.Paint);
+    method public int getCodepointAt(int);
+    method public int getCodepointsLength();
+    method public int getHeight();
+    method public android.graphics.Typeface getTypeface();
+    method public int getWidth();
+    method public boolean isDefaultEmoji();
+    method public boolean isPreferredSystemRender();
+  }
+
+}
+
diff --git a/emoji2/emoji2/api/public_plus_experimental_1.3.0-beta01.txt b/emoji2/emoji2/api/public_plus_experimental_1.3.0-beta01.txt
new file mode 100644
index 0000000..11d9335
--- /dev/null
+++ b/emoji2/emoji2/api/public_plus_experimental_1.3.0-beta01.txt
@@ -0,0 +1,131 @@
+// Signature format: 4.0
+package androidx.emoji2.text {
+
+  public final class DefaultEmojiCompatConfig {
+    method public static androidx.emoji2.text.FontRequestEmojiCompatConfig? create(android.content.Context);
+  }
+
+  @AnyThread public class EmojiCompat {
+    method public static androidx.emoji2.text.EmojiCompat get();
+    method public String getAssetSignature();
+    method public int getEmojiEnd(CharSequence, @IntRange(from=0) int);
+    method public int getEmojiMatch(CharSequence, @IntRange(from=0) int);
+    method public int getEmojiStart(CharSequence, @IntRange(from=0) int);
+    method public int getLoadState();
+    method public static boolean handleDeleteSurroundingText(android.view.inputmethod.InputConnection, android.text.Editable, @IntRange(from=0) int, @IntRange(from=0) int, boolean);
+    method public static boolean handleOnKeyDown(android.text.Editable, int, android.view.KeyEvent);
+    method @Deprecated public boolean hasEmojiGlyph(CharSequence);
+    method @Deprecated public boolean hasEmojiGlyph(CharSequence, @IntRange(from=0) int);
+    method public static androidx.emoji2.text.EmojiCompat? init(android.content.Context);
+    method public static androidx.emoji2.text.EmojiCompat init(androidx.emoji2.text.EmojiCompat.Config);
+    method public static boolean isConfigured();
+    method public void load();
+    method @CheckResult public CharSequence? process(CharSequence?);
+    method @CheckResult public CharSequence? process(CharSequence?, @IntRange(from=0) int, @IntRange(from=0) int);
+    method @CheckResult public CharSequence? process(CharSequence?, @IntRange(from=0) int, @IntRange(from=0) int, @IntRange(from=0) int);
+    method @CheckResult public CharSequence? process(CharSequence?, @IntRange(from=0) int, @IntRange(from=0) int, @IntRange(from=0) int, int);
+    method public void registerInitCallback(androidx.emoji2.text.EmojiCompat.InitCallback);
+    method public void unregisterInitCallback(androidx.emoji2.text.EmojiCompat.InitCallback);
+    method public void updateEditorInfo(android.view.inputmethod.EditorInfo);
+    field public static final String EDITOR_INFO_METAVERSION_KEY = "android.support.text.emoji.emojiCompat_metadataVersion";
+    field public static final String EDITOR_INFO_REPLACE_ALL_KEY = "android.support.text.emoji.emojiCompat_replaceAll";
+    field public static final int EMOJI_FALLBACK = 2; // 0x2
+    field public static final int EMOJI_SUPPORTED = 1; // 0x1
+    field public static final int EMOJI_UNSUPPORTED = 0; // 0x0
+    field public static final int LOAD_STATE_DEFAULT = 3; // 0x3
+    field public static final int LOAD_STATE_FAILED = 2; // 0x2
+    field public static final int LOAD_STATE_LOADING = 0; // 0x0
+    field public static final int LOAD_STATE_SUCCEEDED = 1; // 0x1
+    field public static final int LOAD_STRATEGY_DEFAULT = 0; // 0x0
+    field public static final int LOAD_STRATEGY_MANUAL = 1; // 0x1
+    field public static final int REPLACE_STRATEGY_ALL = 1; // 0x1
+    field public static final int REPLACE_STRATEGY_DEFAULT = 0; // 0x0
+    field public static final int REPLACE_STRATEGY_NON_EXISTENT = 2; // 0x2
+  }
+
+  public abstract static class EmojiCompat.Config {
+    ctor protected EmojiCompat.Config(androidx.emoji2.text.EmojiCompat.MetadataRepoLoader);
+    method protected final androidx.emoji2.text.EmojiCompat.MetadataRepoLoader getMetadataRepoLoader();
+    method public androidx.emoji2.text.EmojiCompat.Config registerInitCallback(androidx.emoji2.text.EmojiCompat.InitCallback);
+    method public androidx.emoji2.text.EmojiCompat.Config setEmojiSpanIndicatorColor(@ColorInt int);
+    method public androidx.emoji2.text.EmojiCompat.Config setEmojiSpanIndicatorEnabled(boolean);
+    method public androidx.emoji2.text.EmojiCompat.Config setGlyphChecker(androidx.emoji2.text.EmojiCompat.GlyphChecker);
+    method public androidx.emoji2.text.EmojiCompat.Config setMetadataLoadStrategy(int);
+    method public androidx.emoji2.text.EmojiCompat.Config setReplaceAll(boolean);
+    method public androidx.emoji2.text.EmojiCompat.Config setSpanFactory(androidx.emoji2.text.EmojiCompat.SpanFactory);
+    method public androidx.emoji2.text.EmojiCompat.Config setUseEmojiAsDefaultStyle(boolean);
+    method public androidx.emoji2.text.EmojiCompat.Config setUseEmojiAsDefaultStyle(boolean, java.util.List<java.lang.Integer!>?);
+    method public androidx.emoji2.text.EmojiCompat.Config unregisterInitCallback(androidx.emoji2.text.EmojiCompat.InitCallback);
+  }
+
+  public static interface EmojiCompat.GlyphChecker {
+    method public boolean hasGlyph(CharSequence, @IntRange(from=0) int, @IntRange(from=0) int, @IntRange(from=0) int);
+  }
+
+  public abstract static class EmojiCompat.InitCallback {
+    ctor public EmojiCompat.InitCallback();
+    method public void onFailed(Throwable?);
+    method public void onInitialized();
+  }
+
+  public static interface EmojiCompat.MetadataRepoLoader {
+    method public void load(androidx.emoji2.text.EmojiCompat.MetadataRepoLoaderCallback);
+  }
+
+  public abstract static class EmojiCompat.MetadataRepoLoaderCallback {
+    ctor public EmojiCompat.MetadataRepoLoaderCallback();
+    method public abstract void onFailed(Throwable?);
+    method public abstract void onLoaded(androidx.emoji2.text.MetadataRepo);
+  }
+
+  public static interface EmojiCompat.SpanFactory {
+    method @RequiresApi(19) public androidx.emoji2.text.EmojiSpan createSpan(androidx.emoji2.text.TypefaceEmojiRasterizer);
+  }
+
+  public class EmojiCompatInitializer implements androidx.startup.Initializer<java.lang.Boolean> {
+    ctor public EmojiCompatInitializer();
+    method public Boolean create(android.content.Context);
+    method public java.util.List<java.lang.Class<? extends androidx.startup.Initializer<?>>!> dependencies();
+  }
+
+  @RequiresApi(19) public abstract class EmojiSpan extends android.text.style.ReplacementSpan {
+    method public int getSize(android.graphics.Paint, CharSequence!, int, int, android.graphics.Paint.FontMetricsInt?);
+    method public final androidx.emoji2.text.TypefaceEmojiRasterizer getTypefaceRasterizer();
+  }
+
+  public class FontRequestEmojiCompatConfig extends androidx.emoji2.text.EmojiCompat.Config {
+    ctor public FontRequestEmojiCompatConfig(android.content.Context, androidx.core.provider.FontRequest);
+    method @Deprecated public androidx.emoji2.text.FontRequestEmojiCompatConfig setHandler(android.os.Handler?);
+    method public androidx.emoji2.text.FontRequestEmojiCompatConfig setLoadingExecutor(java.util.concurrent.Executor);
+    method public androidx.emoji2.text.FontRequestEmojiCompatConfig setRetryPolicy(androidx.emoji2.text.FontRequestEmojiCompatConfig.RetryPolicy?);
+  }
+
+  public static class FontRequestEmojiCompatConfig.ExponentialBackoffRetryPolicy extends androidx.emoji2.text.FontRequestEmojiCompatConfig.RetryPolicy {
+    ctor public FontRequestEmojiCompatConfig.ExponentialBackoffRetryPolicy(long);
+    method public long getRetryDelay();
+  }
+
+  public abstract static class FontRequestEmojiCompatConfig.RetryPolicy {
+    ctor public FontRequestEmojiCompatConfig.RetryPolicy();
+    method public abstract long getRetryDelay();
+  }
+
+  @AnyThread @RequiresApi(19) public final class MetadataRepo {
+    method public static androidx.emoji2.text.MetadataRepo create(android.graphics.Typeface, java.io.InputStream) throws java.io.IOException;
+    method public static androidx.emoji2.text.MetadataRepo create(android.graphics.Typeface, java.nio.ByteBuffer) throws java.io.IOException;
+    method public static androidx.emoji2.text.MetadataRepo create(android.content.res.AssetManager, String) throws java.io.IOException;
+  }
+
+  @AnyThread @RequiresApi(19) public class TypefaceEmojiRasterizer {
+    method public void draw(android.graphics.Canvas, float, float, android.graphics.Paint);
+    method public int getCodepointAt(int);
+    method public int getCodepointsLength();
+    method public int getHeight();
+    method public android.graphics.Typeface getTypeface();
+    method public int getWidth();
+    method public boolean isDefaultEmoji();
+    method public boolean isPreferredSystemRender();
+  }
+
+}
+
diff --git a/emoji2/emoji2/api/res-1.3.0-beta01.txt b/emoji2/emoji2/api/res-1.3.0-beta01.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/emoji2/emoji2/api/res-1.3.0-beta01.txt
diff --git a/emoji2/emoji2/api/restricted_1.3.0-beta01.txt b/emoji2/emoji2/api/restricted_1.3.0-beta01.txt
new file mode 100644
index 0000000..11d9335
--- /dev/null
+++ b/emoji2/emoji2/api/restricted_1.3.0-beta01.txt
@@ -0,0 +1,131 @@
+// Signature format: 4.0
+package androidx.emoji2.text {
+
+  public final class DefaultEmojiCompatConfig {
+    method public static androidx.emoji2.text.FontRequestEmojiCompatConfig? create(android.content.Context);
+  }
+
+  @AnyThread public class EmojiCompat {
+    method public static androidx.emoji2.text.EmojiCompat get();
+    method public String getAssetSignature();
+    method public int getEmojiEnd(CharSequence, @IntRange(from=0) int);
+    method public int getEmojiMatch(CharSequence, @IntRange(from=0) int);
+    method public int getEmojiStart(CharSequence, @IntRange(from=0) int);
+    method public int getLoadState();
+    method public static boolean handleDeleteSurroundingText(android.view.inputmethod.InputConnection, android.text.Editable, @IntRange(from=0) int, @IntRange(from=0) int, boolean);
+    method public static boolean handleOnKeyDown(android.text.Editable, int, android.view.KeyEvent);
+    method @Deprecated public boolean hasEmojiGlyph(CharSequence);
+    method @Deprecated public boolean hasEmojiGlyph(CharSequence, @IntRange(from=0) int);
+    method public static androidx.emoji2.text.EmojiCompat? init(android.content.Context);
+    method public static androidx.emoji2.text.EmojiCompat init(androidx.emoji2.text.EmojiCompat.Config);
+    method public static boolean isConfigured();
+    method public void load();
+    method @CheckResult public CharSequence? process(CharSequence?);
+    method @CheckResult public CharSequence? process(CharSequence?, @IntRange(from=0) int, @IntRange(from=0) int);
+    method @CheckResult public CharSequence? process(CharSequence?, @IntRange(from=0) int, @IntRange(from=0) int, @IntRange(from=0) int);
+    method @CheckResult public CharSequence? process(CharSequence?, @IntRange(from=0) int, @IntRange(from=0) int, @IntRange(from=0) int, int);
+    method public void registerInitCallback(androidx.emoji2.text.EmojiCompat.InitCallback);
+    method public void unregisterInitCallback(androidx.emoji2.text.EmojiCompat.InitCallback);
+    method public void updateEditorInfo(android.view.inputmethod.EditorInfo);
+    field public static final String EDITOR_INFO_METAVERSION_KEY = "android.support.text.emoji.emojiCompat_metadataVersion";
+    field public static final String EDITOR_INFO_REPLACE_ALL_KEY = "android.support.text.emoji.emojiCompat_replaceAll";
+    field public static final int EMOJI_FALLBACK = 2; // 0x2
+    field public static final int EMOJI_SUPPORTED = 1; // 0x1
+    field public static final int EMOJI_UNSUPPORTED = 0; // 0x0
+    field public static final int LOAD_STATE_DEFAULT = 3; // 0x3
+    field public static final int LOAD_STATE_FAILED = 2; // 0x2
+    field public static final int LOAD_STATE_LOADING = 0; // 0x0
+    field public static final int LOAD_STATE_SUCCEEDED = 1; // 0x1
+    field public static final int LOAD_STRATEGY_DEFAULT = 0; // 0x0
+    field public static final int LOAD_STRATEGY_MANUAL = 1; // 0x1
+    field public static final int REPLACE_STRATEGY_ALL = 1; // 0x1
+    field public static final int REPLACE_STRATEGY_DEFAULT = 0; // 0x0
+    field public static final int REPLACE_STRATEGY_NON_EXISTENT = 2; // 0x2
+  }
+
+  public abstract static class EmojiCompat.Config {
+    ctor protected EmojiCompat.Config(androidx.emoji2.text.EmojiCompat.MetadataRepoLoader);
+    method protected final androidx.emoji2.text.EmojiCompat.MetadataRepoLoader getMetadataRepoLoader();
+    method public androidx.emoji2.text.EmojiCompat.Config registerInitCallback(androidx.emoji2.text.EmojiCompat.InitCallback);
+    method public androidx.emoji2.text.EmojiCompat.Config setEmojiSpanIndicatorColor(@ColorInt int);
+    method public androidx.emoji2.text.EmojiCompat.Config setEmojiSpanIndicatorEnabled(boolean);
+    method public androidx.emoji2.text.EmojiCompat.Config setGlyphChecker(androidx.emoji2.text.EmojiCompat.GlyphChecker);
+    method public androidx.emoji2.text.EmojiCompat.Config setMetadataLoadStrategy(int);
+    method public androidx.emoji2.text.EmojiCompat.Config setReplaceAll(boolean);
+    method public androidx.emoji2.text.EmojiCompat.Config setSpanFactory(androidx.emoji2.text.EmojiCompat.SpanFactory);
+    method public androidx.emoji2.text.EmojiCompat.Config setUseEmojiAsDefaultStyle(boolean);
+    method public androidx.emoji2.text.EmojiCompat.Config setUseEmojiAsDefaultStyle(boolean, java.util.List<java.lang.Integer!>?);
+    method public androidx.emoji2.text.EmojiCompat.Config unregisterInitCallback(androidx.emoji2.text.EmojiCompat.InitCallback);
+  }
+
+  public static interface EmojiCompat.GlyphChecker {
+    method public boolean hasGlyph(CharSequence, @IntRange(from=0) int, @IntRange(from=0) int, @IntRange(from=0) int);
+  }
+
+  public abstract static class EmojiCompat.InitCallback {
+    ctor public EmojiCompat.InitCallback();
+    method public void onFailed(Throwable?);
+    method public void onInitialized();
+  }
+
+  public static interface EmojiCompat.MetadataRepoLoader {
+    method public void load(androidx.emoji2.text.EmojiCompat.MetadataRepoLoaderCallback);
+  }
+
+  public abstract static class EmojiCompat.MetadataRepoLoaderCallback {
+    ctor public EmojiCompat.MetadataRepoLoaderCallback();
+    method public abstract void onFailed(Throwable?);
+    method public abstract void onLoaded(androidx.emoji2.text.MetadataRepo);
+  }
+
+  public static interface EmojiCompat.SpanFactory {
+    method @RequiresApi(19) public androidx.emoji2.text.EmojiSpan createSpan(androidx.emoji2.text.TypefaceEmojiRasterizer);
+  }
+
+  public class EmojiCompatInitializer implements androidx.startup.Initializer<java.lang.Boolean> {
+    ctor public EmojiCompatInitializer();
+    method public Boolean create(android.content.Context);
+    method public java.util.List<java.lang.Class<? extends androidx.startup.Initializer<?>>!> dependencies();
+  }
+
+  @RequiresApi(19) public abstract class EmojiSpan extends android.text.style.ReplacementSpan {
+    method public int getSize(android.graphics.Paint, CharSequence!, int, int, android.graphics.Paint.FontMetricsInt?);
+    method public final androidx.emoji2.text.TypefaceEmojiRasterizer getTypefaceRasterizer();
+  }
+
+  public class FontRequestEmojiCompatConfig extends androidx.emoji2.text.EmojiCompat.Config {
+    ctor public FontRequestEmojiCompatConfig(android.content.Context, androidx.core.provider.FontRequest);
+    method @Deprecated public androidx.emoji2.text.FontRequestEmojiCompatConfig setHandler(android.os.Handler?);
+    method public androidx.emoji2.text.FontRequestEmojiCompatConfig setLoadingExecutor(java.util.concurrent.Executor);
+    method public androidx.emoji2.text.FontRequestEmojiCompatConfig setRetryPolicy(androidx.emoji2.text.FontRequestEmojiCompatConfig.RetryPolicy?);
+  }
+
+  public static class FontRequestEmojiCompatConfig.ExponentialBackoffRetryPolicy extends androidx.emoji2.text.FontRequestEmojiCompatConfig.RetryPolicy {
+    ctor public FontRequestEmojiCompatConfig.ExponentialBackoffRetryPolicy(long);
+    method public long getRetryDelay();
+  }
+
+  public abstract static class FontRequestEmojiCompatConfig.RetryPolicy {
+    ctor public FontRequestEmojiCompatConfig.RetryPolicy();
+    method public abstract long getRetryDelay();
+  }
+
+  @AnyThread @RequiresApi(19) public final class MetadataRepo {
+    method public static androidx.emoji2.text.MetadataRepo create(android.graphics.Typeface, java.io.InputStream) throws java.io.IOException;
+    method public static androidx.emoji2.text.MetadataRepo create(android.graphics.Typeface, java.nio.ByteBuffer) throws java.io.IOException;
+    method public static androidx.emoji2.text.MetadataRepo create(android.content.res.AssetManager, String) throws java.io.IOException;
+  }
+
+  @AnyThread @RequiresApi(19) public class TypefaceEmojiRasterizer {
+    method public void draw(android.graphics.Canvas, float, float, android.graphics.Paint);
+    method public int getCodepointAt(int);
+    method public int getCodepointsLength();
+    method public int getHeight();
+    method public android.graphics.Typeface getTypeface();
+    method public int getWidth();
+    method public boolean isDefaultEmoji();
+    method public boolean isPreferredSystemRender();
+  }
+
+}
+
diff --git a/libraryversions.toml b/libraryversions.toml
index 9160d928..46498e4 100644
--- a/libraryversions.toml
+++ b/libraryversions.toml
@@ -6,7 +6,7 @@
 APPACTIONS_INTERACTION = "1.0.0-alpha01"
 APPCOMPAT = "1.7.0-alpha02"
 APPSEARCH = "1.1.0-alpha03"
-ARCH_CORE = "2.2.0-alpha02"
+ARCH_CORE = "2.2.0-beta01"
 ASYNCLAYOUTINFLATER = "1.1.0-alpha02"
 AUTOFILL = "1.2.0-beta02"
 BENCHMARK = "1.2.0-alpha10"
@@ -53,7 +53,8 @@
 DYNAMICANIMATION = "1.1.0-alpha04"
 DYNAMICANIMATION_KTX = "1.0.0-alpha04"
 EMOJI = "1.2.0-alpha03"
-EMOJI2 = "1.3.0-alpha02"
+EMOJI2 = "1.3.0-beta01"
+EMOJI2_QUARANTINE = "1.0.0-alpha01"
 ENTERPRISE = "1.1.0-rc01"
 EXIFINTERFACE = "1.4.0-alpha01"
 FRAGMENT = "1.6.0-alpha05"
diff --git a/lifecycle/lifecycle-livedata-core/api/current.txt b/lifecycle/lifecycle-livedata-core/api/current.txt
index f4df726..f528b4e 100644
--- a/lifecycle/lifecycle-livedata-core/api/current.txt
+++ b/lifecycle/lifecycle-livedata-core/api/current.txt
@@ -25,8 +25,8 @@
     method public void setValue(T!);
   }
 
-  public interface Observer<T> {
-    method public void onChanged(T!);
+  public fun interface Observer<T> {
+    method public void onChanged(T? value);
   }
 
 }
diff --git a/lifecycle/lifecycle-livedata-core/api/public_plus_experimental_current.txt b/lifecycle/lifecycle-livedata-core/api/public_plus_experimental_current.txt
index f4df726..f528b4e 100644
--- a/lifecycle/lifecycle-livedata-core/api/public_plus_experimental_current.txt
+++ b/lifecycle/lifecycle-livedata-core/api/public_plus_experimental_current.txt
@@ -25,8 +25,8 @@
     method public void setValue(T!);
   }
 
-  public interface Observer<T> {
-    method public void onChanged(T!);
+  public fun interface Observer<T> {
+    method public void onChanged(T? value);
   }
 
 }
diff --git a/lifecycle/lifecycle-livedata-core/api/restricted_current.txt b/lifecycle/lifecycle-livedata-core/api/restricted_current.txt
index f4df726..f528b4e 100644
--- a/lifecycle/lifecycle-livedata-core/api/restricted_current.txt
+++ b/lifecycle/lifecycle-livedata-core/api/restricted_current.txt
@@ -25,8 +25,8 @@
     method public void setValue(T!);
   }
 
-  public interface Observer<T> {
-    method public void onChanged(T!);
+  public fun interface Observer<T> {
+    method public void onChanged(T? value);
   }
 
 }
diff --git a/lifecycle/lifecycle-livedata-core/build.gradle b/lifecycle/lifecycle-livedata-core/build.gradle
index be5361f..fba1c86 100644
--- a/lifecycle/lifecycle-livedata-core/build.gradle
+++ b/lifecycle/lifecycle-livedata-core/build.gradle
@@ -19,9 +19,11 @@
 plugins {
     id("AndroidXPlugin")
     id("com.android.library")
+    id("org.jetbrains.kotlin.android")
 }
 
 dependencies {
+    api(libs.kotlinStdlib)
     implementation("androidx.arch.core:core-common:2.1.0")
     implementation("androidx.arch.core:core-runtime:2.1.0")
     api(project(":lifecycle:lifecycle-common"))
diff --git a/lifecycle/lifecycle-livedata-core/src/main/java/androidx/lifecycle/Observer.java b/lifecycle/lifecycle-livedata-core/src/main/java/androidx/lifecycle/Observer.kt
similarity index 72%
rename from lifecycle/lifecycle-livedata-core/src/main/java/androidx/lifecycle/Observer.java
rename to lifecycle/lifecycle-livedata-core/src/main/java/androidx/lifecycle/Observer.kt
index 30afeedf..2df6295 100644
--- a/lifecycle/lifecycle-livedata-core/src/main/java/androidx/lifecycle/Observer.java
+++ b/lifecycle/lifecycle-livedata-core/src/main/java/androidx/lifecycle/Observer.kt
@@ -13,20 +13,17 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-
-package androidx.lifecycle;
+package androidx.lifecycle
 
 /**
- * A simple callback that can receive from {@link LiveData}.
- *
- * @param <T> The type of the parameter
+ * A simple callback that can receive from [LiveData].
  *
  * @see LiveData LiveData - for a usage description.
- */
-public interface Observer<T> {
+*/
+fun interface Observer<T> {
+
     /**
-     * Called when the data is changed.
-     * @param t  The new data
+     * Called when the data is changed is changed to [value].
      */
-    void onChanged(T t);
-}
+    fun onChanged(value: T)
+}
\ No newline at end of file
diff --git a/lifecycle/lifecycle-livedata-core/src/test/java/androidx/lifecycle/LiveDataTest.java b/lifecycle/lifecycle-livedata-core/src/test/java/androidx/lifecycle/LiveDataTest.java
index 85834a3..874db97 100644
--- a/lifecycle/lifecycle-livedata-core/src/test/java/androidx/lifecycle/LiveDataTest.java
+++ b/lifecycle/lifecycle-livedata-core/src/test/java/androidx/lifecycle/LiveDataTest.java
@@ -954,7 +954,7 @@
 
     private class FailReentranceObserver<T> implements Observer<T> {
         @Override
-        public void onChanged(@Nullable T t) {
+        public void onChanged(@Nullable T value) {
             assertThat(mInObserver, is(false));
         }
     }
diff --git a/lifecycle/lifecycle-livedata-ktx/src/test/java/androidx/lifecycle/ScopesRule.kt b/lifecycle/lifecycle-livedata-ktx/src/test/java/androidx/lifecycle/ScopesRule.kt
index 7791be2..d1193af 100644
--- a/lifecycle/lifecycle-livedata-ktx/src/test/java/androidx/lifecycle/ScopesRule.kt
+++ b/lifecycle/lifecycle-livedata-ktx/src/test/java/androidx/lifecycle/ScopesRule.kt
@@ -104,8 +104,8 @@
     private val scopes: ScopesRule
 ) : Observer<T> {
     private var items = mutableListOf<T>()
-    override fun onChanged(t: T) {
-        items.add(t)
+    override fun onChanged(value: T) {
+        items.add(value)
     }
 
     fun assertItems(vararg expected: T) {
diff --git a/lifecycle/lifecycle-livedata/src/main/java/androidx/lifecycle/Transformations.kt b/lifecycle/lifecycle-livedata/src/main/java/androidx/lifecycle/Transformations.kt
index 7c8af62..26d7c36 100644
--- a/lifecycle/lifecycle-livedata/src/main/java/androidx/lifecycle/Transformations.kt
+++ b/lifecycle/lifecycle-livedata/src/main/java/androidx/lifecycle/Transformations.kt
@@ -120,8 +120,8 @@
     result.addSource(this, object : Observer<X> {
         var liveData: LiveData<Y>? = null
 
-        override fun onChanged(x: X) {
-            val newLiveData = transform(x)
+        override fun onChanged(value: X) {
+            val newLiveData = transform(value)
             if (liveData === newLiveData) {
                 return
             }
@@ -149,8 +149,8 @@
     result.addSource(this, object : Observer<X> {
         var liveData: LiveData<Y>? = null
 
-        override fun onChanged(x: X) {
-            val newLiveData = switchMapFunction.apply(x)
+        override fun onChanged(value: X) {
+            val newLiveData = switchMapFunction.apply(value)
             if (liveData === newLiveData) {
                 return
             }
@@ -180,14 +180,14 @@
     outputLiveData.addSource(this, object : Observer<X> {
         var firstTime = true
 
-        override fun onChanged(currentValue: X) {
+        override fun onChanged(value: X) {
             val previousValue = outputLiveData.value
             if (firstTime ||
-                previousValue == null && currentValue != null ||
-                previousValue != null && previousValue != currentValue
+                previousValue == null && value != null ||
+                previousValue != null && previousValue != value
             ) {
                 firstTime = false
-                outputLiveData.value = currentValue
+                outputLiveData.value = value
             }
         }
     })
diff --git a/lifecycle/lifecycle-livedata/src/test/java/androidx/lifecycle/TransformationsTest.java b/lifecycle/lifecycle-livedata/src/test/java/androidx/lifecycle/TransformationsTest.java
index e77c136..4b1a493 100644
--- a/lifecycle/lifecycle-livedata/src/test/java/androidx/lifecycle/TransformationsTest.java
+++ b/lifecycle/lifecycle-livedata/src/test/java/androidx/lifecycle/TransformationsTest.java
@@ -238,7 +238,7 @@
         int mTimesUpdated;
 
         @Override
-        public void onChanged(@Nullable T t) {
+        public void onChanged(@Nullable T value) {
             ++mTimesUpdated;
         }
     }
diff --git a/lifecycle/lifecycle-reactivestreams/src/main/java/androidx/lifecycle/LiveDataReactiveStreams.kt b/lifecycle/lifecycle-reactivestreams/src/main/java/androidx/lifecycle/LiveDataReactiveStreams.kt
index dbf4021..5f09eb2 100644
--- a/lifecycle/lifecycle-reactivestreams/src/main/java/androidx/lifecycle/LiveDataReactiveStreams.kt
+++ b/lifecycle/lifecycle-reactivestreams/src/main/java/androidx/lifecycle/LiveDataReactiveStreams.kt
@@ -75,7 +75,7 @@
         val subscriber: Subscriber<in T>,
         val lifecycle: LifecycleOwner,
         val liveData: LiveData<T>
-    ) : Subscription, Observer<T> {
+    ) : Subscription, Observer<T?> {
         @Volatile
         var canceled = false
 
@@ -86,18 +86,18 @@
         // used on main thread only
         var latest: T? = null
 
-        override fun onChanged(t: T?) {
+        override fun onChanged(value: T?) {
             if (canceled) {
                 return
             }
             if (requested > 0) {
                 latest = null
-                subscriber.onNext(t)
+                subscriber.onNext(value)
                 if (requested != Long.MAX_VALUE) {
                     requested--
                 }
             } else {
-                latest = t
+                latest = value
             }
         }
 
diff --git a/lifecycle/lifecycle-viewmodel-savedstate/src/main/java/androidx/lifecycle/AbstractSavedStateViewModelFactory.kt b/lifecycle/lifecycle-viewmodel-savedstate/src/main/java/androidx/lifecycle/AbstractSavedStateViewModelFactory.kt
index 6561594..3e09793 100644
--- a/lifecycle/lifecycle-viewmodel-savedstate/src/main/java/androidx/lifecycle/AbstractSavedStateViewModelFactory.kt
+++ b/lifecycle/lifecycle-viewmodel-savedstate/src/main/java/androidx/lifecycle/AbstractSavedStateViewModelFactory.kt
@@ -93,7 +93,7 @@
 
     private fun <T : ViewModel> create(key: String, modelClass: Class<T>): T {
         val controller = LegacySavedStateHandleController
-            .create(savedStateRegistry, lifecycle, key, defaultArgs)
+            .create(savedStateRegistry!!, lifecycle!!, key, defaultArgs)
         val viewModel = create(key, modelClass, controller.handle)
         viewModel.setTagIfAbsent(TAG_SAVED_STATE_HANDLE_CONTROLLER, controller)
         return viewModel
@@ -148,7 +148,7 @@
     override fun onRequery(viewModel: ViewModel) {
         // is need only for legacy path
         if (savedStateRegistry != null) {
-            attachHandleIfNeeded(viewModel, savedStateRegistry, lifecycle)
+            attachHandleIfNeeded(viewModel, savedStateRegistry!!, lifecycle!!)
         }
     }
 
diff --git a/lifecycle/lifecycle-viewmodel-savedstate/src/main/java/androidx/lifecycle/LegacySavedStateHandleController.java b/lifecycle/lifecycle-viewmodel-savedstate/src/main/java/androidx/lifecycle/LegacySavedStateHandleController.java
deleted file mode 100644
index f7f9033..0000000
--- a/lifecycle/lifecycle-viewmodel-savedstate/src/main/java/androidx/lifecycle/LegacySavedStateHandleController.java
+++ /dev/null
@@ -1,92 +0,0 @@
-/*
- * Copyright 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.lifecycle;
-
-import static androidx.lifecycle.Lifecycle.State.INITIALIZED;
-import static androidx.lifecycle.Lifecycle.State.STARTED;
-
-import android.os.Bundle;
-
-import androidx.annotation.NonNull;
-import androidx.savedstate.SavedStateRegistry;
-import androidx.savedstate.SavedStateRegistryOwner;
-
-class LegacySavedStateHandleController {
-
-    private LegacySavedStateHandleController() {}
-
-    static final String TAG_SAVED_STATE_HANDLE_CONTROLLER = "androidx.lifecycle.savedstate.vm.tag";
-
-    static SavedStateHandleController create(SavedStateRegistry registry, Lifecycle lifecycle,
-            String key, Bundle defaultArgs) {
-        Bundle restoredState = registry.consumeRestoredStateForKey(key);
-        SavedStateHandle handle = SavedStateHandle.createHandle(restoredState, defaultArgs);
-        SavedStateHandleController controller = new SavedStateHandleController(key, handle);
-        controller.attachToLifecycle(registry, lifecycle);
-        tryToAddRecreator(registry, lifecycle);
-        return controller;
-    }
-
-    static final class OnRecreation implements SavedStateRegistry.AutoRecreated {
-
-        @Override
-        public void onRecreated(@NonNull SavedStateRegistryOwner owner) {
-            if (!(owner instanceof ViewModelStoreOwner)) {
-                throw new IllegalStateException(
-                        "Internal error: OnRecreation should be registered only on components"
-                                + " that implement ViewModelStoreOwner");
-            }
-            ViewModelStore viewModelStore = ((ViewModelStoreOwner) owner).getViewModelStore();
-            SavedStateRegistry savedStateRegistry = owner.getSavedStateRegistry();
-            for (String key : viewModelStore.keys()) {
-                ViewModel viewModel = viewModelStore.get(key);
-                attachHandleIfNeeded(viewModel, savedStateRegistry, owner.getLifecycle());
-            }
-            if (!viewModelStore.keys().isEmpty()) {
-                savedStateRegistry.runOnNextRecreation(OnRecreation.class);
-            }
-        }
-    }
-
-    static void attachHandleIfNeeded(ViewModel viewModel, SavedStateRegistry registry,
-            Lifecycle lifecycle) {
-        SavedStateHandleController controller = viewModel.getTag(
-                TAG_SAVED_STATE_HANDLE_CONTROLLER);
-        if (controller != null && !controller.isAttached()) {
-            controller.attachToLifecycle(registry, lifecycle);
-            tryToAddRecreator(registry, lifecycle);
-        }
-    }
-
-    private static void tryToAddRecreator(SavedStateRegistry registry, Lifecycle lifecycle) {
-        Lifecycle.State currentState = lifecycle.getCurrentState();
-        if (currentState == INITIALIZED || currentState.isAtLeast(STARTED)) {
-            registry.runOnNextRecreation(OnRecreation.class);
-        } else {
-            lifecycle.addObserver(new LifecycleEventObserver() {
-                @Override
-                public void onStateChanged(@NonNull LifecycleOwner source,
-                        @NonNull Lifecycle.Event event) {
-                    if (event == Lifecycle.Event.ON_START) {
-                        lifecycle.removeObserver(this);
-                        registry.runOnNextRecreation(OnRecreation.class);
-                    }
-                }
-            });
-        }
-    }
-}
diff --git a/lifecycle/lifecycle-viewmodel-savedstate/src/main/java/androidx/lifecycle/LegacySavedStateHandleController.kt b/lifecycle/lifecycle-viewmodel-savedstate/src/main/java/androidx/lifecycle/LegacySavedStateHandleController.kt
new file mode 100644
index 0000000..2f3de01
--- /dev/null
+++ b/lifecycle/lifecycle-viewmodel-savedstate/src/main/java/androidx/lifecycle/LegacySavedStateHandleController.kt
@@ -0,0 +1,93 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package androidx.lifecycle
+
+import android.os.Bundle
+import androidx.lifecycle.SavedStateHandle.Companion.createHandle
+import androidx.savedstate.SavedStateRegistry
+import androidx.savedstate.SavedStateRegistryOwner
+
+internal object LegacySavedStateHandleController {
+    const val TAG_SAVED_STATE_HANDLE_CONTROLLER = "androidx.lifecycle.savedstate.vm.tag"
+
+    @JvmStatic
+    fun create(
+        registry: SavedStateRegistry,
+        lifecycle: Lifecycle,
+        key: String?,
+        defaultArgs: Bundle?
+    ): SavedStateHandleController {
+        val restoredState = registry.consumeRestoredStateForKey(key!!)
+        val handle = createHandle(restoredState, defaultArgs)
+        val controller = SavedStateHandleController(key, handle)
+        controller.attachToLifecycle(registry, lifecycle)
+        tryToAddRecreator(registry, lifecycle)
+        return controller
+    }
+
+    @JvmStatic
+    fun attachHandleIfNeeded(
+        viewModel: ViewModel,
+        registry: SavedStateRegistry,
+        lifecycle: Lifecycle
+    ) {
+        val controller = viewModel.getTag<SavedStateHandleController>(
+            TAG_SAVED_STATE_HANDLE_CONTROLLER
+        )
+        if (controller != null && !controller.isAttached) {
+            controller.attachToLifecycle(registry, lifecycle)
+            tryToAddRecreator(registry, lifecycle)
+        }
+    }
+
+    private fun tryToAddRecreator(registry: SavedStateRegistry, lifecycle: Lifecycle) {
+        val currentState = lifecycle.currentState
+        if (currentState === Lifecycle.State.INITIALIZED ||
+            currentState.isAtLeast(Lifecycle.State.STARTED)) {
+            registry.runOnNextRecreation(OnRecreation::class.java)
+        } else {
+            lifecycle.addObserver(object : LifecycleEventObserver {
+                override fun onStateChanged(
+                    source: LifecycleOwner,
+                    event: Lifecycle.Event
+                ) {
+                    if (event === Lifecycle.Event.ON_START) {
+                        lifecycle.removeObserver(this)
+                        registry.runOnNextRecreation(OnRecreation::class.java)
+                    }
+                }
+            })
+        }
+    }
+
+    internal class OnRecreation : SavedStateRegistry.AutoRecreated {
+        override fun onRecreated(owner: SavedStateRegistryOwner) {
+            check(owner is ViewModelStoreOwner) {
+                ("Internal error: OnRecreation should be registered only on components " +
+                    "that implement ViewModelStoreOwner")
+            }
+            val viewModelStore = (owner as ViewModelStoreOwner).viewModelStore
+            val savedStateRegistry = owner.savedStateRegistry
+            for (key in viewModelStore.keys()) {
+                val viewModel = viewModelStore[key]
+                attachHandleIfNeeded(viewModel!!, savedStateRegistry, owner.lifecycle)
+            }
+            if (viewModelStore.keys().isNotEmpty()) {
+                savedStateRegistry.runOnNextRecreation(OnRecreation::class.java)
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/lifecycle/lifecycle-viewmodel-savedstate/src/main/java/androidx/lifecycle/SavedStateViewModelFactory.kt b/lifecycle/lifecycle-viewmodel-savedstate/src/main/java/androidx/lifecycle/SavedStateViewModelFactory.kt
index 2e22611..aa24f21 100644
--- a/lifecycle/lifecycle-viewmodel-savedstate/src/main/java/androidx/lifecycle/SavedStateViewModelFactory.kt
+++ b/lifecycle/lifecycle-viewmodel-savedstate/src/main/java/androidx/lifecycle/SavedStateViewModelFactory.kt
@@ -156,12 +156,11 @@
      */
     fun <T : ViewModel> create(key: String, modelClass: Class<T>): T {
         // empty constructor was called.
-        if (lifecycle == null) {
-            throw UnsupportedOperationException(
+        val lifecycle = lifecycle
+            ?: throw UnsupportedOperationException(
                 "SavedStateViewModelFactory constructed with empty constructor supports only " +
                     "calls to create(modelClass: Class<T>, extras: CreationExtras)."
             )
-        }
         val isAndroidViewModel = AndroidViewModel::class.java.isAssignableFrom(modelClass)
         val constructor: Constructor<T>? = if (isAndroidViewModel && application != null) {
             findMatchingConstructor(modelClass, ANDROID_VIEWMODEL_SIGNATURE)
@@ -169,14 +168,13 @@
             findMatchingConstructor(modelClass, VIEWMODEL_SIGNATURE)
         }
         // doesn't need SavedStateHandle
-        if (constructor == null) {
-            // If you are using a stateful constructor and no application is available, we
+        constructor
+            ?: // If you are using a stateful constructor and no application is available, we
             // use an instance factory instead.
             return if (application != null) factory.create(modelClass)
-                else instance.create(modelClass)
-        }
+            else instance.create(modelClass)
         val controller = LegacySavedStateHandleController.create(
-            savedStateRegistry, lifecycle, key, defaultArgs
+            savedStateRegistry!!, lifecycle, key, defaultArgs
         )
         val viewModel: T = if (isAndroidViewModel && application != null) {
             newInstance(modelClass, constructor, application!!, controller.handle)
@@ -212,8 +210,8 @@
         if (lifecycle != null) {
             LegacySavedStateHandleController.attachHandleIfNeeded(
                 viewModel,
-                savedStateRegistry,
-                lifecycle
+                savedStateRegistry!!,
+                lifecycle!!
             )
         }
     }
diff --git a/mediarouter/mediarouter/src/main/java/androidx/mediarouter/app/SystemOutputSwitcherDialogController.java b/mediarouter/mediarouter/src/main/java/androidx/mediarouter/app/SystemOutputSwitcherDialogController.java
index ad6bb9d..3d85b4c 100644
--- a/mediarouter/mediarouter/src/main/java/androidx/mediarouter/app/SystemOutputSwitcherDialogController.java
+++ b/mediarouter/mediarouter/src/main/java/androidx/mediarouter/app/SystemOutputSwitcherDialogController.java
@@ -63,6 +63,10 @@
     /**
      * Shows the system output switcher dialog.
      *
+     * <p>See
+     * <a href="https://developer.android.com/guide/topics/media/media-routing#output-switcher">
+     * Output Switcher documentation</a> for more details.
+     *
      * @param context Android context
      * @return {@code true} if the dialog was shown successfully and {@code false} otherwise
      */
diff --git a/navigation/navigation-dynamic-features-fragment/src/androidTest/java/androidx/navigation/dynamicfeatures/fragment/ui/DefaultProgressFragmentTest.kt b/navigation/navigation-dynamic-features-fragment/src/androidTest/java/androidx/navigation/dynamicfeatures/fragment/ui/DefaultProgressFragmentTest.kt
index 57d781b..99c67e7 100644
--- a/navigation/navigation-dynamic-features-fragment/src/androidTest/java/androidx/navigation/dynamicfeatures/fragment/ui/DefaultProgressFragmentTest.kt
+++ b/navigation/navigation-dynamic-features-fragment/src/androidTest/java/androidx/navigation/dynamicfeatures/fragment/ui/DefaultProgressFragmentTest.kt
@@ -72,8 +72,8 @@
                 // it to fail before we check for test failure.
                 val liveData = viewModel.installMonitor!!.status
                 val observer = object : Observer<SplitInstallSessionState> {
-                    override fun onChanged(state: SplitInstallSessionState) {
-                        if (state.status() == SplitInstallSessionStatus.FAILED) {
+                    override fun onChanged(value: SplitInstallSessionState) {
+                        if (value.status() == SplitInstallSessionStatus.FAILED) {
                             liveData.removeObserver(this)
                             failureCountdownLatch.countDown()
                         }
diff --git a/navigation/navigation-dynamic-features-fragment/src/main/java/androidx/navigation/dynamicfeatures/fragment/ui/AbstractProgressFragment.kt b/navigation/navigation-dynamic-features-fragment/src/main/java/androidx/navigation/dynamicfeatures/fragment/ui/AbstractProgressFragment.kt
index c550abe..e0caf39 100644
--- a/navigation/navigation-dynamic-features-fragment/src/main/java/androidx/navigation/dynamicfeatures/fragment/ui/AbstractProgressFragment.kt
+++ b/navigation/navigation-dynamic-features-fragment/src/main/java/androidx/navigation/dynamicfeatures/fragment/ui/AbstractProgressFragment.kt
@@ -126,59 +126,60 @@
     private inner class StateObserver constructor(private val monitor: DynamicInstallMonitor) :
         Observer<SplitInstallSessionState> {
 
-        override fun onChanged(sessionState: SplitInstallSessionState?) {
-            if (sessionState != null) {
-                if (sessionState.hasTerminalStatus()) {
-                    monitor.status.removeObserver(this)
+        override fun onChanged(
+            @Suppress("PARAMETER_NAME_CHANGED_ON_OVERRIDE")
+            sessionState: SplitInstallSessionState
+        ) {
+            if (sessionState.hasTerminalStatus()) {
+                monitor.status.removeObserver(this)
+            }
+            when (sessionState.status()) {
+                SplitInstallSessionStatus.INSTALLED -> {
+                    onInstalled()
+                    navigate()
                 }
-                when (sessionState.status()) {
-                    SplitInstallSessionStatus.INSTALLED -> {
-                        onInstalled()
-                        navigate()
-                    }
-                    SplitInstallSessionStatus.REQUIRES_USER_CONFIRMATION ->
-                        try {
-                            val splitInstallManager = monitor.splitInstallManager
-                            if (splitInstallManager == null) {
-                                onFailed(SplitInstallErrorCode.INTERNAL_ERROR)
-                                return
-                            }
-                            splitInstallManager.startConfirmationDialogForResult(
-                                sessionState,
-                                IntentSenderForResultStarter { intent,
-                                    _,
-                                    fillInIntent,
-                                    flagsMask,
-                                    flagsValues,
-                                    _,
-                                    _ ->
-                                    intentSenderLauncher.launch(
-                                        IntentSenderRequest.Builder(intent)
-                                            .setFillInIntent(fillInIntent)
-                                            .setFlags(flagsValues, flagsMask)
-                                            .build()
-                                    )
-                                },
-                                INSTALL_REQUEST_CODE
-                            )
-                        } catch (e: IntentSender.SendIntentException) {
+                SplitInstallSessionStatus.REQUIRES_USER_CONFIRMATION ->
+                    try {
+                        val splitInstallManager = monitor.splitInstallManager
+                        if (splitInstallManager == null) {
                             onFailed(SplitInstallErrorCode.INTERNAL_ERROR)
+                            return
                         }
-                    SplitInstallSessionStatus.CANCELED -> onCancelled()
-                    SplitInstallSessionStatus.FAILED -> onFailed(sessionState.errorCode())
-                    SplitInstallSessionStatus.UNKNOWN ->
-                        onFailed(SplitInstallErrorCode.INTERNAL_ERROR)
-                    SplitInstallSessionStatus.CANCELING,
-                    SplitInstallSessionStatus.DOWNLOADED,
-                    SplitInstallSessionStatus.DOWNLOADING,
-                    SplitInstallSessionStatus.INSTALLING,
-                    SplitInstallSessionStatus.PENDING -> {
-                        onProgress(
-                            sessionState.status(),
-                            sessionState.bytesDownloaded(),
-                            sessionState.totalBytesToDownload()
+                        splitInstallManager.startConfirmationDialogForResult(
+                            sessionState,
+                            IntentSenderForResultStarter { intent,
+                                _,
+                                fillInIntent,
+                                flagsMask,
+                                flagsValues,
+                                _,
+                                _ ->
+                                intentSenderLauncher.launch(
+                                    IntentSenderRequest.Builder(intent)
+                                        .setFillInIntent(fillInIntent)
+                                        .setFlags(flagsValues, flagsMask)
+                                        .build()
+                                )
+                            },
+                            INSTALL_REQUEST_CODE
                         )
+                    } catch (e: IntentSender.SendIntentException) {
+                        onFailed(SplitInstallErrorCode.INTERNAL_ERROR)
                     }
+                SplitInstallSessionStatus.CANCELED -> onCancelled()
+                SplitInstallSessionStatus.FAILED -> onFailed(sessionState.errorCode())
+                SplitInstallSessionStatus.UNKNOWN ->
+                    onFailed(SplitInstallErrorCode.INTERNAL_ERROR)
+                SplitInstallSessionStatus.CANCELING,
+                SplitInstallSessionStatus.DOWNLOADED,
+                SplitInstallSessionStatus.DOWNLOADING,
+                SplitInstallSessionStatus.INSTALLING,
+                SplitInstallSessionStatus.PENDING -> {
+                    onProgress(
+                        sessionState.status(),
+                        sessionState.bytesDownloaded(),
+                        sessionState.totalBytesToDownload()
+                    )
                 }
             }
         }
diff --git a/navigation/navigation-runtime/src/androidTest/java/androidx/navigation/NavControllerRouteTest.kt b/navigation/navigation-runtime/src/androidTest/java/androidx/navigation/NavControllerRouteTest.kt
index 523d14e..168482b 100644
--- a/navigation/navigation-runtime/src/androidTest/java/androidx/navigation/NavControllerRouteTest.kt
+++ b/navigation/navigation-runtime/src/androidTest/java/androidx/navigation/NavControllerRouteTest.kt
@@ -591,6 +591,189 @@
 
     @UiThreadTest
     @Test
+    fun testPopBackStack() {
+        val navController = createNavController()
+        navController.graph =
+            navController.createGraph(route = "nav_root", startDestination = "start_test") {
+                test("start_test")
+                test("second_test/{arg}") {
+                    argument("arg") { type = NavType.StringType }
+                }
+            }
+
+        // first nav with arg filed in
+        val deepLink = Uri.parse("android-app://androidx.navigation/second_test/13")
+        navController.navigate(deepLink)
+
+        // second nav with arg filled in
+        val deepLink2 = Uri.parse("android-app://androidx.navigation/second_test/18")
+        navController.navigate(deepLink2)
+
+        val navigator = navController.navigatorProvider.getNavigator(TestNavigator::class.java)
+        // ["start_test", "second_test/13", "second_test/18"]
+        assertThat(navigator.backStack.size).isEqualTo(3)
+
+        val popped = navController.popBackStack("second_test/{arg}", true)
+        assertThat(popped).isTrue()
+        // only last entry with "second_test/{arg}" has been popped
+        assertThat(navigator.backStack.size).isEqualTo(2)
+    }
+
+    @UiThreadTest
+    @Test
+    fun testPopBackStackWithExactRoute() {
+        val navController = createNavController()
+        navController.graph =
+            navController.createGraph(route = "nav_root", startDestination = "start_test") {
+                test("start_test")
+                test("second_test/{arg}") {
+                    argument("arg") { type = NavType.StringType }
+                }
+            }
+
+        // first nav with arg filed in
+        val deepLink = Uri.parse("android-app://androidx.navigation/second_test/13")
+        navController.navigate(deepLink)
+
+        // second nav with arg filled in
+        val deepLink2 = Uri.parse("android-app://androidx.navigation/second_test/18")
+        navController.navigate(deepLink2)
+
+        val navigator = navController.navigatorProvider.getNavigator(TestNavigator::class.java)
+        // ["start_test", "second_test/13", "second_test/18"]
+        assertThat(navigator.backStack.size).isEqualTo(3)
+
+        val popped = navController.popBackStack("second_test/13", true)
+        assertThat(popped).isTrue()
+        assertThat(navigator.backStack.size).isEqualTo(1)
+    }
+
+    @UiThreadTest
+    @Test
+    fun testPopBackStackWithExactRoute_multiArgs() {
+        val navController = createNavController()
+        navController.graph =
+            navController.createGraph(route = "nav_root", startDestination = "start_test") {
+                test("start_test")
+                test("second_test/{arg}/{arg2}") {
+                    argument("arg") { type = NavType.StringType }
+                    argument("arg2") { type = NavType.StringType }
+                }
+            }
+
+        val deepLink = Uri.parse("android-app://androidx.navigation/second_test/13/18")
+        navController.navigate(deepLink)
+
+        val navigator = navController.navigatorProvider.getNavigator(TestNavigator::class.java)
+        // ["start_test", "second_test/13/18"]
+        assertThat(navigator.backStack.size).isEqualTo(2)
+
+        val popped = navController.popBackStack("second_test/13/18", true)
+        assertThat(popped).isTrue()
+        assertThat(navigator.backStack.size).isEqualTo(1)
+    }
+
+    @UiThreadTest
+    @Test
+    fun testPopBackStackWithPartialExactRoute() {
+        val navController = createNavController()
+        navController.graph =
+            navController.createGraph(route = "nav_root", startDestination = "start_test") {
+                test("start_test")
+                test("second_test/{arg}/{arg2}") {
+                    argument("arg") { type = NavType.StringType }
+                    argument("arg2") { type = NavType.StringType }
+                }
+            }
+
+        val deepLink = Uri.parse("android-app://androidx.navigation/second_test/13/{arg2}")
+        navController.navigate(deepLink)
+
+        val navigator = navController.navigatorProvider.getNavigator(TestNavigator::class.java)
+        // ["start_test", "second_test/13/{arg2}"]
+        assertThat(navigator.backStack.size).isEqualTo(2)
+
+        val popped = navController.popBackStack("second_test/13/{arg2}", true)
+        assertThat(popped).isTrue()
+        assertThat(navigator.backStack.size).isEqualTo(1)
+    }
+
+    @UiThreadTest
+    @Test
+    fun testPopBackStackWithIncorrectExactRoute() {
+        val navController = createNavController()
+        navController.graph =
+            navController.createGraph(route = "nav_root", startDestination = "start_test") {
+                test("start_test")
+                test("second_test/{arg}/{arg2}") {
+                    argument("arg") { type = NavType.StringType }
+                    argument("arg2") { type = NavType.StringType }
+                }
+            }
+
+        val deepLink = Uri.parse("android-app://androidx.navigation/second_test/13/18")
+        navController.navigate(deepLink)
+
+        val navigator = navController.navigatorProvider.getNavigator(TestNavigator::class.java)
+        // ["start_test", "second_test/13/18"]
+        assertThat(navigator.backStack.size).isEqualTo(2)
+
+        val popped = navController.popBackStack("second_test/13/19", true)
+        assertThat(popped).isFalse()
+        assertThat(navigator.backStack.size).isEqualTo(2)
+    }
+    @UiThreadTest
+    @Test
+    fun testPopBackStackWithAdditionalPartialArgs() {
+        val navController = createNavController()
+        navController.graph =
+            navController.createGraph(route = "nav_root", startDestination = "start_test") {
+                test("start_test")
+                test("second_test/{arg}/{arg2}") {
+                    argument("arg") { type = NavType.StringType }
+                    argument("arg2") { type = NavType.StringType }
+                }
+            }
+
+        val deepLink = Uri.parse("android-app://androidx.navigation/second_test/13/{arg2}")
+        navController.navigate(deepLink)
+
+        val navigator = navController.navigatorProvider.getNavigator(TestNavigator::class.java)
+        // ["start_test", "second_test/13/{arg2}"]
+        assertThat(navigator.backStack.size).isEqualTo(2)
+
+        val popped = navController.popBackStack("second_test/13/18", true)
+        assertThat(popped).isFalse()
+        assertThat(navigator.backStack.size).isEqualTo(2)
+    }
+
+    @UiThreadTest
+    @Test
+    fun testPopBackStackWithMissingPartialArgs() {
+        val navController = createNavController()
+        navController.graph =
+            navController.createGraph(route = "nav_root", startDestination = "start_test") {
+                test("start_test")
+                test("second_test/{arg}/{arg2}") {
+                    argument("arg") { type = NavType.StringType }
+                    argument("arg2") { type = NavType.StringType }
+                }
+            }
+
+        val deepLink = Uri.parse("android-app://androidx.navigation/second_test/13/18")
+        navController.navigate(deepLink)
+
+        val navigator = navController.navigatorProvider.getNavigator(TestNavigator::class.java)
+        // ["start_test", "second_test/13/18"]
+        assertThat(navigator.backStack.size).isEqualTo(2)
+
+        val popped = navController.popBackStack("second_test/13/{arg2}", true)
+        assertThat(popped).isFalse()
+        assertThat(navigator.backStack.size).isEqualTo(2)
+    }
+
+    @UiThreadTest
+    @Test
     fun testNavigateViaDeepLinkDefaultArgs() {
         val navController = createNavController()
         navController.graph = nav_simple_route_graph
diff --git a/navigation/navigation-runtime/src/main/java/androidx/navigation/NavController.kt b/navigation/navigation-runtime/src/main/java/androidx/navigation/NavController.kt
index 126bf8f..d7d03d6 100644
--- a/navigation/navigation-runtime/src/main/java/androidx/navigation/NavController.kt
+++ b/navigation/navigation-runtime/src/main/java/androidx/navigation/NavController.kt
@@ -506,7 +506,12 @@
         route: String,
         inclusive: Boolean,
         saveState: Boolean = false
-    ): Boolean = popBackStack(createRoute(route).hashCode(), inclusive, saveState)
+    ): Boolean {
+        val popped = popBackStackInternal(route, inclusive, saveState)
+        // Only return true if the pop succeeded and we've dispatched
+        // the change to a new destination
+        return popped && dispatchOnDestinationChanged()
+    }
 
     /**
      * Attempts to pop the controller's back stack back to a specific destination. This does
@@ -564,6 +569,55 @@
         return executePopOperations(popOperations, foundDestination, inclusive, saveState)
     }
 
+    /**
+     * Attempts to pop the controller's back stack back to a specific destination. This does
+     * **not** handle calling [dispatchOnDestinationChanged]
+     *
+     * @param route The topmost destination with this route to retain
+     * @param inclusive Whether the given destination should also be popped.
+     * @param saveState Whether the back stack and the state of all destinations between the
+     * current destination and the destination with [route] should be saved for later to be
+     * restored via [NavOptions.Builder.setRestoreState] or the `restoreState` attribute using
+     * the [NavDestination.id] of the destination with this route (note: this matching ID
+     * is true whether [inclusive] is true or false).
+     *
+     * @return true if the stack was popped at least once, false otherwise
+     */
+    private fun popBackStackInternal(
+        route: String,
+        inclusive: Boolean,
+        saveState: Boolean,
+    ): Boolean {
+        if (backQueue.isEmpty()) {
+            // Nothing to pop if the back stack is empty
+            return false
+        }
+
+        val popOperations = mutableListOf<Navigator<*>>()
+        val foundDestination = backQueue.lastOrNull { entry ->
+            val hasRoute = entry.destination.hasRoute(route, entry.arguments)
+            if (inclusive || !hasRoute) {
+                val navigator = _navigatorProvider.getNavigator<Navigator<*>>(
+                    entry.destination.navigatorName
+                )
+                popOperations.add(navigator)
+            }
+            hasRoute
+        }?.destination
+
+        if (foundDestination == null) {
+            // We were passed a route that doesn't exist on our back stack.
+            // Better to ignore the popBackStack than accidentally popping the entire stack
+            Log.i(
+                TAG,
+                "Ignoring popBackStack to route $route as it was not found " +
+                    "on the current back stack"
+            )
+            return false
+        }
+        return executePopOperations(popOperations, foundDestination, inclusive, saveState)
+    }
+
     private fun executePopOperations(
         popOperations: List<Navigator<*>>,
         foundDestination: NavDestination,
diff --git a/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/parser/ApiParser.kt b/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/parser/ApiParser.kt
index 5782e08..19c669b 100644
--- a/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/parser/ApiParser.kt
+++ b/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/parser/ApiParser.kt
@@ -40,7 +40,7 @@
 /** Top-level entry point to parse a complete user-defined sandbox SDK API into a [ParsedApi]. */
 class ApiParser(private val resolver: Resolver, private val logger: KSPLogger) {
     private val typeParser = TypeParser(logger)
-    private val interfaceParser = InterfaceParser(logger, typeParser)
+    private val interfaceParser = InterfaceParser(logger, typeParser, resolver)
     private val valueParser = ValueParser(logger, typeParser)
 
     fun parseApi(): ParsedApi {
diff --git a/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/parser/InterfaceParser.kt b/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/parser/InterfaceParser.kt
index cb2b9d0..7455049 100644
--- a/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/parser/InterfaceParser.kt
+++ b/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/parser/InterfaceParser.kt
@@ -23,13 +23,18 @@
 import com.google.devtools.ksp.getDeclaredProperties
 import com.google.devtools.ksp.isPublic
 import com.google.devtools.ksp.processing.KSPLogger
+import com.google.devtools.ksp.processing.Resolver
 import com.google.devtools.ksp.symbol.ClassKind
 import com.google.devtools.ksp.symbol.KSClassDeclaration
 import com.google.devtools.ksp.symbol.KSFunctionDeclaration
 import com.google.devtools.ksp.symbol.KSValueParameter
 import com.google.devtools.ksp.symbol.Modifier
 
-internal class InterfaceParser(private val logger: KSPLogger, private val typeParser: TypeParser) {
+internal class InterfaceParser(
+    private val logger: KSPLogger,
+    private val typeParser: TypeParser,
+    private val resolver: Resolver,
+) {
     private val validInterfaceModifiers = setOf(Modifier.PUBLIC)
     private val validMethodModifiers = setOf(Modifier.PUBLIC, Modifier.SUSPEND)
 
@@ -71,6 +76,17 @@
                 })."
             )
         }
+        if (interfaceDeclaration.superTypes.singleOrNull()?.resolve()
+            != resolver.builtIns.anyType
+        ) {
+            logger.error(
+                "Error in $name: annotated interface inherits prohibited types (${
+                    interfaceDeclaration.superTypes.map {
+                        it.resolve().declaration.simpleName.getShortName()
+                    }.sorted().joinToString(limit = 3)
+                })."
+            )
+        }
 
         val methods = interfaceDeclaration.getDeclaredFunctions().map(::parseMethod).toList()
         return AnnotatedInterface(
diff --git a/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/TestUtils.kt b/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/TestUtils.kt
index c6be61c..e473360 100644
--- a/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/TestUtils.kt
+++ b/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/TestUtils.kt
@@ -28,7 +28,7 @@
  */
 fun compileWithPrivacySandboxKspCompiler(
     sources: List<Source>,
-    addSdkRuntimeLibraryStubs: Boolean = true,
+    addLibraryStubs: Boolean = true,
     extraProcessorOptions: Map<String, String> = mapOf(),
 ): TestCompilationResult {
     val provider = PrivacySandboxKspCompiler.Provider()
@@ -41,7 +41,8 @@
     }
 
     return CompilationTestHelper.compileAll(
-        if (addSdkRuntimeLibraryStubs) sources + syntheticSdkRuntimeLibraryStubs else sources,
+        if (addLibraryStubs) sources + syntheticSdkRuntimeLibraryStubs + syntheticUiLibraryStubs
+        else sources,
         symbolProcessorProviders = listOf(provider),
         processorOptions = processorOptions,
     )
@@ -88,5 +89,57 @@
         |   ): View
         |}
         |""".trimMargin()
-    )
+    ),
+)
+
+private val syntheticUiLibraryStubs = listOf(
+    Source.kotlin(
+        "androidx/privacysandbox/ui/core/SandboxedUiAdapter.kt", """
+        |package androidx.privacysandbox.ui.core
+        |
+        |import android.content.Context
+        |import android.view.View
+        |import java.util.concurrent.Executor
+        |
+        |interface SandboxedUiAdapter {
+        |  fun openSession(
+        |      context: Context,
+        |      initialWidth: Int,
+        |      initialHeight: Int,
+        |      isZOrderOnTop: Boolean,
+        |      clientExecutor: Executor,
+        |      client: SessionClient
+        |  )
+        |
+        |
+        |  interface Session {
+        |    fun close()
+        |    val view: View
+        |  }
+        |
+        |  interface SessionClient {
+        |    fun onSessionError(throwable: Throwable);
+        |    fun onSessionOpened(session: Session);
+        |  }
+        |}
+        |""".trimMargin()
+    ),
+    Source.kotlin(
+        "androidx/privacysandbox/ui/core/SdkRuntimeUiLibVersions.kt", """
+        |package androidx.privacysandbox.ui.core
+        |
+        |import androidx.annotation.RestrictTo
+        |
+        |object SdkRuntimeUiLibVersions {
+        |    var clientVersion: Int = -1
+        |        /**
+        |         * @hide
+        |         */
+        |        @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+        |        set
+        |
+        |    const val apiVersion: Int = 1
+        |}
+        |""".trimMargin()
+    ),
 )
diff --git a/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/WithoutRuntimeLibrarySdkTest.kt b/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/WithoutRuntimeLibrarySdkTest.kt
index aeeca05..388e130 100644
--- a/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/WithoutRuntimeLibrarySdkTest.kt
+++ b/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/WithoutRuntimeLibrarySdkTest.kt
@@ -34,7 +34,7 @@
 
         val result = compileWithPrivacySandboxKspCompiler(
             inputSources,
-            addSdkRuntimeLibraryStubs = false,
+            addLibraryStubs = false,
             extraProcessorOptions = mapOf("skip_sdk_runtime_compat_library" to "true")
         )
         assertThat(result).succeeds()
diff --git a/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/parser/InterfaceParserTest.kt b/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/parser/InterfaceParserTest.kt
index 93b2d53..776d7b3 100644
--- a/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/parser/InterfaceParserTest.kt
+++ b/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/parser/InterfaceParserTest.kt
@@ -202,6 +202,49 @@
     }
 
     @Test
+    fun interfaceInheritance_fails() {
+        val source = Source.kotlin(
+            "com/mysdk/MySdk.kt", """
+                    package com.mysdk
+                    import androidx.privacysandbox.tools.PrivacySandboxService
+
+                    interface FooInterface {}
+
+                    @PrivacySandboxService
+                    interface MySdk : FooInterface {
+                        suspend fun foo(): Int
+                    }"""
+        )
+        checkSourceFails(source).containsExactlyErrors(
+            "Error in com.mysdk.MySdk: annotated interface inherits prohibited types (" +
+                "FooInterface)."
+        )
+    }
+
+    @Test
+    fun interfaceInheritsManyInterfaces_fails() {
+        val source = Source.kotlin(
+            "com/mysdk/MySdk.kt", """
+                    package com.mysdk
+                    import androidx.privacysandbox.tools.PrivacySandboxService
+
+                    interface A {}
+                    interface B {}
+                    interface C {}
+                    interface D {}
+
+                    @PrivacySandboxService
+                    interface MySdk : B, C, D, A {
+                        suspend fun foo(): Int
+                    }"""
+        )
+        checkSourceFails(source).containsExactlyErrors(
+            "Error in com.mysdk.MySdk: annotated interface inherits prohibited types (A, B, C, " +
+                "...)."
+        )
+    }
+
+    @Test
     fun methodWithImplementation_fails() {
         checkSourceFails(serviceMethod("suspend fun foo(): Int = 1")).containsExactlyErrors(
             "Error in com.mysdk.MySdk.foo: method cannot have default implementation."
@@ -223,7 +266,7 @@
     }
 
     @Test
-    fun parameterWitDefaultValue_fails() {
+    fun parameterWithDefaultValue_fails() {
         checkSourceFails(serviceMethod("suspend fun foo(x: Int = 5)")).containsExactlyErrors(
             "Error in com.mysdk.MySdk.foo: parameters cannot have default values."
         )
diff --git a/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/LiveDataTestUtil.kt b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/LiveDataTestUtil.kt
index aabc29a..98b9f79 100644
--- a/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/LiveDataTestUtil.kt
+++ b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/LiveDataTestUtil.kt
@@ -29,8 +29,8 @@
         val latch = CountDownLatch(1)
         var data: T? = null
         val observer = object : Observer<T> {
-            override fun onChanged(o: T?) {
-                data = o
+            override fun onChanged(value: T) {
+                data = value
                 liveData.removeObserver(this)
                 latch.countDown()
             }
diff --git a/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/testutil/TestObserver.kt b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/testutil/TestObserver.kt
index c3005bf..2b89222 100644
--- a/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/testutil/TestObserver.kt
+++ b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/testutil/TestObserver.kt
@@ -27,8 +27,8 @@
         mLastData = null
     }
 
-    override fun onChanged(o: T?) {
-        mLastData = o
+    override fun onChanged(value: T) {
+        mLastData = value
         mHasValue = true
     }
 
diff --git a/samples/Support4Demos/src/main/java/com/example/android/supportv4/view/WindowInsetsControllerPlayground.kt b/samples/Support4Demos/src/main/java/com/example/android/supportv4/view/WindowInsetsControllerPlayground.kt
index 63cf794..67fee43 100644
--- a/samples/Support4Demos/src/main/java/com/example/android/supportv4/view/WindowInsetsControllerPlayground.kt
+++ b/samples/Support4Demos/src/main/java/com/example/android/supportv4/view/WindowInsetsControllerPlayground.kt
@@ -487,9 +487,10 @@
 
     private fun setupBehaviorSpinner() {
         val types = mapOf(
-            "BY TOUCH" to WindowInsetsControllerCompat.BEHAVIOR_SHOW_BARS_BY_TOUCH,
-            "BY SWIPE" to WindowInsetsControllerCompat.BEHAVIOR_SHOW_BARS_BY_SWIPE,
+            "DEFAULT" to WindowInsetsControllerCompat.BEHAVIOR_DEFAULT,
             "TRANSIENT" to WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE,
+            "BY TOUCH (Deprecated)" to WindowInsetsControllerCompat.BEHAVIOR_SHOW_BARS_BY_TOUCH,
+            "BY SWIPE (Deprecated)" to WindowInsetsControllerCompat.BEHAVIOR_SHOW_BARS_BY_SWIPE,
         )
         findViewById<Spinner>(R.id.spn_behavior).apply {
             adapter = ArrayAdapter(
diff --git a/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/grid/LazyScrollTest.kt b/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/grid/LazyScrollTest.kt
index 7c3174f..b14edf9 100644
--- a/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/grid/LazyScrollTest.kt
+++ b/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/grid/LazyScrollTest.kt
@@ -38,7 +38,6 @@
 import kotlinx.coroutines.runBlocking
 import kotlinx.coroutines.withContext
 import org.junit.Before
-import org.junit.Ignore
 import org.junit.Rule
 import org.junit.Test
 
@@ -251,7 +250,6 @@
         assertThat(state.canScrollBackward).isFalse()
     }
 
-    @Ignore("b/259608530")
     @Test
     fun canScrollBackward() = runBlocking {
         withContext(Dispatchers.Main + AutoTestFrameClock()) {
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyGridMeasure.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyGridMeasure.kt
index 93719d4..1d2de6dc 100644
--- a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyGridMeasure.kt
+++ b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyGridMeasure.kt
@@ -250,10 +250,12 @@
             spanLayoutProvider = spanLayoutProvider
         )
 
+        val lastVisibleItemIndex = visibleLines.lastOrNull()?.items?.lastOrNull()?.index?.value ?: 0
         return TvLazyGridMeasureResult(
             firstVisibleLine = firstLine,
             firstVisibleLineScrollOffset = currentFirstLineScrollOffset,
-            canScrollForward = index.value < itemsCount || currentMainAxisOffset > maxOffset,
+            canScrollForward =
+            lastVisibleItemIndex != itemsCount - 1 || currentMainAxisOffset > maxOffset,
             consumedScroll = consumedScroll,
             measureResult = layout(layoutWidth, layoutHeight) {
                 positionedItems.fastForEach { it.place(this) }
diff --git a/tv/tv-material/src/androidTest/java/androidx/tv/material3/CarouselTest.kt b/tv/tv-material/src/androidTest/java/androidx/tv/material3/CarouselTest.kt
index 6e600c6..adb434b 100644
--- a/tv/tv-material/src/androidTest/java/androidx/tv/material3/CarouselTest.kt
+++ b/tv/tv-material/src/androidTest/java/androidx/tv/material3/CarouselTest.kt
@@ -16,6 +16,8 @@
 
 package androidx.tv.material3
 
+import android.os.SystemClock
+import android.view.KeyEvent
 import androidx.compose.foundation.background
 import androidx.compose.foundation.border
 import androidx.compose.foundation.focusable
@@ -57,6 +59,7 @@
 import androidx.compose.ui.test.onNodeWithText
 import androidx.compose.ui.test.onParent
 import androidx.compose.ui.test.onRoot
+import androidx.compose.ui.test.performKeyPress
 import androidx.compose.ui.test.performSemanticsAction
 import androidx.compose.ui.unit.LayoutDirection
 import androidx.compose.ui.unit.dp
@@ -616,6 +619,50 @@
         rule.onNodeWithText("Play ${finalSlide + 3}").assertIsFocused()
     }
 
+    fun carousel_manualScrolling_onDpadLongPress() {
+        rule.setContent {
+            SampleCarousel(slideCount = 6) { index ->
+                SampleButton("Button ${index + 1}")
+            }
+        }
+
+        // Request focus for Carousel on start
+        rule.mainClock.autoAdvance = false
+        rule.onNodeWithTag("pager")
+            .performSemanticsAction(SemanticsActions.RequestFocus)
+
+        // Trigger recomposition after requesting focus
+        rule.mainClock.advanceTimeByFrame()
+        rule.waitForIdle()
+
+        // Assert that Button 1 from first slide is focused
+        rule.onNodeWithText("Button 1").assertIsFocused()
+
+        // Trigger dpad right key long press
+        performLongKeyPress(rule, NativeKeyEvent.KEYCODE_DPAD_RIGHT)
+
+        // Advance time and trigger recomposition to switch to next slide
+        rule.mainClock.advanceTimeByFrame()
+        rule.waitForIdle()
+        rule.mainClock.advanceTimeBy(delayBetweenSlides, false)
+        rule.waitForIdle()
+
+        // Assert that Button 2 from second slide is focused
+        rule.onNodeWithText("Button 2").assertIsFocused()
+
+        // Trigger dpad left key long press
+        performLongKeyPress(rule, NativeKeyEvent.KEYCODE_DPAD_LEFT)
+
+        // Advance time and trigger recomposition to switch to previous slide
+        rule.mainClock.advanceTimeBy(delayBetweenSlides, false)
+        rule.waitForIdle()
+        rule.mainClock.advanceTimeByFrame()
+        rule.waitForIdle()
+
+        // Assert that Button 1 from first slide is focused
+        rule.onNodeWithText("Button 1").assertIsFocused()
+    }
+
     @Test
     fun carousel_manualScrolling_ltr() {
         rule.setContent {
@@ -804,3 +851,35 @@
         afterEachPress()
     }
 }
+
+private fun performLongKeyPress(
+    rule: ComposeContentTestRule,
+    keyCode: Int,
+    count: Int = 1
+) {
+    repeat(count) {
+        // Trigger the first key down event to simulate key press
+        val firstKeyDownEvent = KeyEvent(
+            SystemClock.uptimeMillis(), SystemClock.uptimeMillis(),
+            KeyEvent.ACTION_DOWN, keyCode, 0, 0, 0, 0
+        )
+        rule.onRoot().performKeyPress(androidx.compose.ui.input.key.KeyEvent(firstKeyDownEvent))
+        rule.waitForIdle()
+
+        // Trigger multiple key down events with repeat count (>0) to simulate key long press
+        val repeatedKeyDownEvent = KeyEvent(
+            SystemClock.uptimeMillis(), SystemClock.uptimeMillis(),
+            KeyEvent.ACTION_DOWN, keyCode, 5, 0, 0, 0
+        )
+        rule.onRoot().performKeyPress(androidx.compose.ui.input.key.KeyEvent(repeatedKeyDownEvent))
+        rule.waitForIdle()
+
+        // Trigger the final key up event to simulate key release
+        val keyUpEvent = KeyEvent(
+            SystemClock.uptimeMillis(), SystemClock.uptimeMillis(),
+            KeyEvent.ACTION_UP, keyCode, 0, 0, 0, 0
+        )
+        rule.onRoot().performKeyPress(androidx.compose.ui.input.key.KeyEvent(keyUpEvent))
+        rule.waitForIdle()
+    }
+}
diff --git a/tv/tv-material/src/main/java/androidx/tv/material3/Carousel.kt b/tv/tv-material/src/main/java/androidx/tv/material3/Carousel.kt
index ffcef53..70c5480 100644
--- a/tv/tv-material/src/main/java/androidx/tv/material3/Carousel.kt
+++ b/tv/tv-material/src/main/java/androidx/tv/material3/Carousel.kt
@@ -241,16 +241,30 @@
             KeyEventPropagation.ContinuePropagation
         }
 
-        KEYCODE_DPAD_LEFT -> if (isLtr) {
-            showPreviousSlideAndGetKeyEventPropagation()
-        } else {
-            showNextSlideAndGetKeyEventPropagation()
+        KEYCODE_DPAD_LEFT -> {
+            // Ignore long press key event for manual scrolling
+            if (it.nativeKeyEvent.repeatCount > 0) {
+                return@onKeyEvent KeyEventPropagation.StopPropagation
+            }
+
+            if (isLtr) {
+                showPreviousSlideAndGetKeyEventPropagation()
+            } else {
+                showNextSlideAndGetKeyEventPropagation()
+            }
         }
 
-        KEYCODE_DPAD_RIGHT -> if (isLtr) {
-            showNextSlideAndGetKeyEventPropagation()
-        } else {
-            showPreviousSlideAndGetKeyEventPropagation()
+        KEYCODE_DPAD_RIGHT -> {
+            // Ignore long press key event for manual scrolling
+            if (it.nativeKeyEvent.repeatCount > 0) {
+                return@onKeyEvent KeyEventPropagation.StopPropagation
+            }
+
+            if (isLtr) {
+                showNextSlideAndGetKeyEventPropagation()
+            } else {
+                showPreviousSlideAndGetKeyEventPropagation()
+            }
         }
 
         else -> KeyEventPropagation.ContinuePropagation
diff --git a/wear/watchface/watchface-style/src/androidTest/java/androidx/wear/watchface/style/UserStyleSettingWithStringResourcesTest.kt b/wear/watchface/watchface-style/src/androidTest/java/androidx/wear/watchface/style/UserStyleSettingWithStringResourcesTest.kt
index b88eadc..4fd0c50 100644
--- a/wear/watchface/watchface-style/src/androidTest/java/androidx/wear/watchface/style/UserStyleSettingWithStringResourcesTest.kt
+++ b/wear/watchface/watchface-style/src/androidTest/java/androidx/wear/watchface/style/UserStyleSettingWithStringResourcesTest.kt
@@ -241,6 +241,56 @@
     }
 
     @Test
+    public fun complicationSettingsWithIndices() {
+        val one = UserStyleSetting.Id("one")
+        val two = UserStyleSetting.Id("two")
+        val schema = UserStyleSchema(
+            listOf(
+                ListUserStyleSetting(
+                    one,
+                    context.resources,
+                    R.string.ith_style,
+                    R.string.ith_style_screen_reader_name,
+                    icon = null,
+                    options = listOf(
+                        ListOption(
+                            UserStyleSetting.Option.Id("one"),
+                            context.resources,
+                            R.string.ith_option,
+                            R.string.ith_option_screen_reader_name,
+                            icon = null
+                        )
+                    ),
+                    listOf(WatchFaceLayer.BASE, WatchFaceLayer.COMPLICATIONS_OVERLAY)
+                ),
+                ComplicationSlotsUserStyleSetting(
+                    two,
+                    context.resources,
+                    R.string.ith_style,
+                    R.string.ith_style_screen_reader_name,
+                    icon = null,
+                    complicationConfig = listOf(
+                        ComplicationSlotsOption(
+                            UserStyleSetting.Option.Id("one"),
+                            context.resources,
+                            R.string.ith_option,
+                            R.string.ith_option_screen_reader_name,
+                            icon = null,
+                            emptyList()
+                        )
+                    ),
+                    listOf(WatchFaceLayer.COMPLICATIONS)
+                )
+            )
+        )
+
+        Truth.assertThat(schema[one]!!.displayName).isEqualTo("1st style")
+        Truth.assertThat(schema[one]!!.description).isEqualTo("1st style setting")
+        Truth.assertThat(schema[two]!!.displayName).isEqualTo("2nd style")
+        Truth.assertThat(schema[two]!!.description).isEqualTo("2nd style setting")
+    }
+
+    @Test
     @Suppress("deprecation")
     public fun
     complicationsUserStyleSettingWireFormatRoundTrip_noScreenReaderName_filledByDisplayName() {
diff --git a/wear/watchface/watchface-style/src/androidTest/res/values/strings.xml b/wear/watchface/watchface-style/src/androidTest/res/values/strings.xml
index 3133a45..aa37205 100644
--- a/wear/watchface/watchface-style/src/androidTest/res/values/strings.xml
+++ b/wear/watchface/watchface-style/src/androidTest/res/values/strings.xml
@@ -36,6 +36,9 @@
     <string name="ith_option" translatable="false">%1$s option</string>
     <string name="ith_option_screen_reader_name" translatable="false">%1$s list option</string>
 
+    <string name="ith_style" translatable="false">%1$s style</string>
+    <string name="ith_style_screen_reader_name" translatable="false">%1$s style setting</string>
+
     <!-- An option within the watch face color style theme settings [CHAR LIMIT=20] -->
     <string name="colors_style_red">Red</string>
 
diff --git a/wear/watchface/watchface-style/src/main/java/androidx/wear/watchface/style/CurrentUserStyleRepository.kt b/wear/watchface/watchface-style/src/main/java/androidx/wear/watchface/style/CurrentUserStyleRepository.kt
index d5c8fff..fe8e80f 100644
--- a/wear/watchface/watchface-style/src/main/java/androidx/wear/watchface/style/CurrentUserStyleRepository.kt
+++ b/wear/watchface/watchface-style/src/main/java/androidx/wear/watchface/style/CurrentUserStyleRepository.kt
@@ -432,7 +432,6 @@
 public class UserStyleSchema constructor(
     userStyleSettings: List<UserStyleSetting>
 ) {
-
     public val userStyleSettings = userStyleSettings
         @Deprecated("use rootUserStyleSettings instead")
         get
@@ -503,7 +502,11 @@
     init {
         var complicationSlotsUserStyleSettingCount = 0
         var customValueUserStyleSettingCount = 0
+        var displayNameIndex = 1
         for (setting in userStyleSettings) {
+            // Provide the ordinal used by fallback descriptions for each setting.
+            setting.setDisplayNameIndex(displayNameIndex++)
+
             when (setting) {
                 is UserStyleSetting.ComplicationSlotsUserStyleSetting ->
                     complicationSlotsUserStyleSettingCount++
diff --git a/wear/watchface/watchface-style/src/main/java/androidx/wear/watchface/style/UserStyleSetting.kt b/wear/watchface/watchface-style/src/main/java/androidx/wear/watchface/style/UserStyleSetting.kt
index 483ff43..097a0b2e 100644
--- a/wear/watchface/watchface-style/src/main/java/androidx/wear/watchface/style/UserStyleSetting.kt
+++ b/wear/watchface/watchface-style/src/main/java/androidx/wear/watchface/style/UserStyleSetting.kt
@@ -177,6 +177,16 @@
         }
     }
 
+    internal fun setDisplayNameIndex(index: Int) {
+        if (displayNameInternal is DisplayText.ResourceDisplayTextWithIndex) {
+            displayNameInternal.setIndex(index)
+        }
+
+        if (descriptionInternal is DisplayText.ResourceDisplayTextWithIndex) {
+            descriptionInternal.setIndex(index)
+        }
+    }
+
     /**
      * Optional data for an on watch face editor (not the companion editor).
      *
@@ -788,8 +798,8 @@
             watchFaceEditorData: WatchFaceEditorData? = null
         ) : super(
             id,
-            DisplayText.ResourceDisplayText(resources, displayNameResourceId),
-            DisplayText.ResourceDisplayText(resources, descriptionResourceId),
+            DisplayText.ResourceDisplayTextWithIndex(resources, displayNameResourceId),
+            DisplayText.ResourceDisplayTextWithIndex(resources, descriptionResourceId),
             icon,
             watchFaceEditorData,
             listOf(BooleanOption.TRUE, BooleanOption.FALSE),
@@ -1351,8 +1361,8 @@
             watchFaceEditorData: WatchFaceEditorData? = null
         ) : this(
             id,
-            DisplayText.ResourceDisplayText(resources, displayNameResourceId),
-            DisplayText.ResourceDisplayText(resources, descriptionResourceId),
+            DisplayText.ResourceDisplayTextWithIndex(resources, displayNameResourceId),
+            DisplayText.ResourceDisplayTextWithIndex(resources, descriptionResourceId),
             icon,
             watchFaceEditorData,
             complicationConfig,
@@ -1952,8 +1962,8 @@
             watchFaceEditorData: WatchFaceEditorData? = null
         ) : super(
             id,
-            DisplayText.ResourceDisplayText(resources, displayNameResourceId),
-            DisplayText.ResourceDisplayText(resources, descriptionResourceId),
+            DisplayText.ResourceDisplayTextWithIndex(resources, displayNameResourceId),
+            DisplayText.ResourceDisplayTextWithIndex(resources, descriptionResourceId),
             icon,
             watchFaceEditorData,
             createOptionsList(minimumValue, maximumValue, defaultValue),
@@ -2155,8 +2165,8 @@
             watchFaceEditorData: WatchFaceEditorData? = null
         ) : super(
             id,
-            DisplayText.ResourceDisplayText(resources, displayNameResourceId),
-            DisplayText.ResourceDisplayText(resources, descriptionResourceId),
+            DisplayText.ResourceDisplayTextWithIndex(resources, displayNameResourceId),
+            DisplayText.ResourceDisplayTextWithIndex(resources, descriptionResourceId),
             icon,
             watchFaceEditorData,
             options,
@@ -2739,8 +2749,8 @@
             watchFaceEditorData: WatchFaceEditorData? = null
         ) : super(
             id,
-            DisplayText.ResourceDisplayText(resources, displayNameResourceId),
-            DisplayText.ResourceDisplayText(resources, descriptionResourceId),
+            DisplayText.ResourceDisplayTextWithIndex(resources, displayNameResourceId),
+            DisplayText.ResourceDisplayTextWithIndex(resources, descriptionResourceId),
             icon,
             watchFaceEditorData,
             createOptionsList(minimumValue, maximumValue, defaultValue),
diff --git a/wear/watchface/watchface/src/main/java/androidx/wear/watchface/WatchFace.kt b/wear/watchface/watchface/src/main/java/androidx/wear/watchface/WatchFace.kt
index 541c0da..6ed0000 100644
--- a/wear/watchface/watchface/src/main/java/androidx/wear/watchface/WatchFace.kt
+++ b/wear/watchface/watchface/src/main/java/androidx/wear/watchface/WatchFace.kt
@@ -44,6 +44,7 @@
 import androidx.wear.watchface.complications.SystemDataSources
 import androidx.wear.watchface.complications.data.ComplicationData
 import androidx.wear.watchface.complications.data.toApiComplicationData
+import androidx.wear.watchface.control.HeadlessWatchFaceImpl
 import androidx.wear.watchface.control.data.ComplicationRenderParams
 import androidx.wear.watchface.control.data.HeadlessWatchFaceInstanceParams
 import androidx.wear.watchface.control.data.WatchFaceRenderParams
@@ -207,8 +208,8 @@
                 watchFaceService.setContext(context)
                 val engine = watchFaceService.createHeadlessEngine() as
                     WatchFaceService.EngineWrapper
-                engine.createHeadlessInstance(params)
-                return engine.deferredWatchFaceImpl.await().WFEditorDelegate()
+                val headlessWatchFaceImpl = engine.createHeadlessInstance(params)
+                return engine.deferredWatchFaceImpl.await().WFEditorDelegate(headlessWatchFaceImpl)
             }
         }
     }
@@ -727,7 +728,10 @@
         }
 
         if (!watchState.isHeadless) {
-            WatchFace.registerEditorDelegate(componentName, WFEditorDelegate())
+            WatchFace.registerEditorDelegate(
+                componentName,
+                WFEditorDelegate(headlessWatchFaceImpl = null)
+            )
             registerReceivers()
         }
 
@@ -778,7 +782,9 @@
         }
     }
 
-    internal inner class WFEditorDelegate : WatchFace.EditorDelegate {
+    internal inner class WFEditorDelegate(
+        private val headlessWatchFaceImpl: HeadlessWatchFaceImpl?
+    ) : WatchFace.EditorDelegate {
         override val userStyleSchema
             get() = currentUserStyleRepository.schema
 
@@ -849,8 +855,10 @@
             complicationSlotsManager.configExtrasChangeCallback = callback
         }
 
+        @SuppressLint("NewApi") // release
         override fun onDestroy(): Unit = TraceEvent("WFEditorDelegate.onDestroy").use {
             if (watchState.isHeadless) {
+                headlessWatchFaceImpl!!.release()
                 [email protected]()
             }
         }
diff --git a/wear/watchface/watchface/src/main/java/androidx/wear/watchface/control/HeadlessWatchFaceImpl.kt b/wear/watchface/watchface/src/main/java/androidx/wear/watchface/control/HeadlessWatchFaceImpl.kt
index a5f3897..2c2540b 100644
--- a/wear/watchface/watchface/src/main/java/androidx/wear/watchface/control/HeadlessWatchFaceImpl.kt
+++ b/wear/watchface/watchface/src/main/java/androidx/wear/watchface/control/HeadlessWatchFaceImpl.kt
@@ -55,7 +55,7 @@
             indentingPrintWriter.decreaseIndent()
         }
 
-        private val headlessInstances = HashSet<HeadlessWatchFaceImpl>()
+        internal val headlessInstances = HashSet<HeadlessWatchFaceImpl>()
     }
 
     init {
diff --git a/wear/watchface/watchface/src/test/java/androidx/wear/watchface/WatchFaceServiceTest.kt b/wear/watchface/watchface/src/test/java/androidx/wear/watchface/WatchFaceServiceTest.kt
index fd9bfe2..48c3f56 100644
--- a/wear/watchface/watchface/src/test/java/androidx/wear/watchface/WatchFaceServiceTest.kt
+++ b/wear/watchface/watchface/src/test/java/androidx/wear/watchface/WatchFaceServiceTest.kt
@@ -18,6 +18,7 @@
 
 import android.support.wearable.complications.ComplicationData as WireComplicationData
 import android.support.wearable.complications.ComplicationText as WireComplicationText
+import android.annotation.SuppressLint
 import android.app.NotificationManager
 import android.app.PendingIntent
 import android.content.ComponentName
@@ -69,6 +70,7 @@
 import androidx.wear.watchface.complications.data.TimeDifferenceStyle
 import androidx.wear.watchface.complications.rendering.CanvasComplicationDrawable
 import androidx.wear.watchface.complications.rendering.ComplicationDrawable
+import androidx.wear.watchface.control.HeadlessWatchFaceImpl
 import androidx.wear.watchface.control.IInteractiveWatchFace
 import androidx.wear.watchface.control.IPendingInteractiveWatchFace
 import androidx.wear.watchface.control.IWatchfaceListener
@@ -6578,6 +6580,42 @@
             .isInstanceOf(ShortTextComplicationData::class.java)
     }
 
+    @SuppressLint("NewApi")
+    @Test
+    public fun createHeadlessSessionDelegate_onDestroy() {
+        val context = ApplicationProvider.getApplicationContext<Context>()
+        val componentName = ComponentName(context, TestNopCanvasWatchFaceService::class.java)
+        lateinit var delegate: WatchFace.EditorDelegate
+
+        // Allows us to programmatically control tasks.
+        TestNopCanvasWatchFaceService.handler = this.handler
+
+        CoroutineScope(handler.asCoroutineDispatcher().immediate).launch {
+            delegate = WatchFace.createHeadlessSessionDelegate(
+                componentName,
+                HeadlessWatchFaceInstanceParams(
+                    componentName,
+                    DeviceConfig(false, false, 100, 200),
+                    100,
+                    100,
+                    null
+                ),
+                context
+            )
+        }
+
+        // Run all pending tasks.
+        while (pendingTasks.isNotEmpty()) {
+            pendingTasks.remove().runnable.run()
+        }
+
+        assertThat(HeadlessWatchFaceImpl.headlessInstances).isNotEmpty()
+        delegate.onDestroy()
+
+        // The headlessInstances should become empty, otherwise there's a leak.
+        assertThat(HeadlessWatchFaceImpl.headlessInstances).isEmpty()
+    }
+
     private fun getLeftShortTextComplicationDataText(): CharSequence {
         val complication = complicationSlotsManager[
             LEFT_COMPLICATION_ID
@@ -6612,3 +6650,50 @@
     private suspend fun Flow<ComplicationData>.firstNonEmpty(): ComplicationData =
         withTimeout(1000) { dropWhile { it is NoDataComplicationData }.first() }
 }
+
+class TestNopCanvasWatchFaceService : WatchFaceService() {
+    companion object {
+        lateinit var handler: Handler
+    }
+
+    override fun getUiThreadHandlerImpl() = handler
+
+    // To make unit tests simpler and non-flaky we run background tasks and ui tasks on the same
+    // handler.
+    override fun getBackgroundThreadHandlerImpl() = handler
+
+    override suspend fun createWatchFace(
+        surfaceHolder: SurfaceHolder,
+        watchState: WatchState,
+        complicationSlotsManager: ComplicationSlotsManager,
+        currentUserStyleRepository: CurrentUserStyleRepository
+    ) = WatchFace(
+        WatchFaceType.DIGITAL,
+        @Suppress("deprecation")
+        object : Renderer.CanvasRenderer(
+            surfaceHolder,
+            currentUserStyleRepository,
+            watchState,
+            CanvasType.HARDWARE,
+            16
+        ) {
+            override fun render(canvas: Canvas, bounds: Rect, zonedDateTime: ZonedDateTime) {
+                // Intentionally empty.
+            }
+
+            override fun renderHighlightLayer(
+                canvas: Canvas,
+                bounds: Rect,
+                zonedDateTime: ZonedDateTime
+            ) {
+                // Intentionally empty.
+            }
+        }
+    )
+
+    override fun getSystemTimeProvider() = object : SystemTimeProvider {
+        override fun getSystemTimeMillis() = 123456789L
+
+        override fun getSystemTimeZoneId() = ZoneId.of("UTC")
+    }
+}
\ No newline at end of file
diff --git a/work/work-inspection/src/main/java/androidx/work/inspection/WorkManagerInspector.kt b/work/work-inspection/src/main/java/androidx/work/inspection/WorkManagerInspector.kt
index 66a0dd2..f51e4cf 100644
--- a/work/work-inspection/src/main/java/androidx/work/inspection/WorkManagerInspector.kt
+++ b/work/work-inspection/src/main/java/androidx/work/inspection/WorkManagerInspector.kt
@@ -133,13 +133,13 @@
                 owner,
                 object : Observer<T> {
                     private var lastValue: T? = null
-                    override fun onChanged(t: T) {
-                        if (t == null) {
+                    override fun onChanged(value: T) {
+                        if (value == null) {
                             removeObserver(this)
                         } else {
                             executor.execute {
-                                listener(lastValue, t)
-                                lastValue = t
+                                listener(lastValue, value)
+                                lastValue = value
                             }
                         }
                     }
diff --git a/work/work-runtime/src/androidTest/java/androidx/work/impl/utils/LiveDataUtilsTest.java b/work/work-runtime/src/androidTest/java/androidx/work/impl/utils/LiveDataUtilsTest.java
index bd21bae..c186329 100644
--- a/work/work-runtime/src/androidTest/java/androidx/work/impl/utils/LiveDataUtilsTest.java
+++ b/work/work-runtime/src/androidTest/java/androidx/work/impl/utils/LiveDataUtilsTest.java
@@ -124,7 +124,7 @@
         int mTimesUpdated;
 
         @Override
-        public void onChanged(@Nullable T t) {
+        public void onChanged(@Nullable T value) {
             ++mTimesUpdated;
         }
     }