Merge "Annotate flaky tests" into androidx-main
diff --git a/annotation/annotation-experimental-lint/integration-tests/lint-baseline.xml b/annotation/annotation-experimental-lint/integration-tests/lint-baseline.xml
index 1822def..76be17e 100644
--- a/annotation/annotation-experimental-lint/integration-tests/lint-baseline.xml
+++ b/annotation/annotation-experimental-lint/integration-tests/lint-baseline.xml
@@ -1126,4 +1126,13 @@
             file="src/main/java/sample/optin/UseKtExperimentalFromJava.java"/>
     </issue>
 
+    <issue
+        id="WrongRequiresOptIn"
+        message="Experimental annotation should use kotlin.RequiresOptIn"
+        errorLine1="annotation class ExperimentalKotlinAnnotationWrongAnnotation"
+        errorLine2="                 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="src/main/java/sample/kotlin/ExperimentalKotlinAnnotationWrongAnnotation.kt"/>
+    </issue>
+
 </issues>
diff --git a/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/Compiler.kt b/annotation/annotation-experimental-lint/integration-tests/src/main/java/sample/kotlin/ExperimentalKotlinAnnotationWrongAnnotation.kt
similarity index 71%
copy from privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/Compiler.kt
copy to annotation/annotation-experimental-lint/integration-tests/src/main/java/sample/kotlin/ExperimentalKotlinAnnotationWrongAnnotation.kt
index 4f92192..722bfcb 100644
--- a/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/Compiler.kt
+++ b/annotation/annotation-experimental-lint/integration-tests/src/main/java/sample/kotlin/ExperimentalKotlinAnnotationWrongAnnotation.kt
@@ -14,4 +14,9 @@
  * limitations under the License.
  */
 
-package androidx.privacysandbox.tools.apicompiler
\ No newline at end of file
+package sample.kotlin
+
+@Retention(AnnotationRetention.BINARY)
+@Suppress("unused")
+@androidx.annotation.RequiresOptIn(level = androidx.annotation.RequiresOptIn.Level.ERROR)
+annotation class ExperimentalKotlinAnnotationWrongAnnotation
diff --git a/annotation/annotation-experimental-lint/src/main/java/androidx/annotation/experimental/lint/AnnotationRetentionDetector.kt b/annotation/annotation-experimental-lint/src/main/java/androidx/annotation/experimental/lint/AnnotationRetentionDetector.kt
index 94ec534..50643b2 100644
--- a/annotation/annotation-experimental-lint/src/main/java/androidx/annotation/experimental/lint/AnnotationRetentionDetector.kt
+++ b/annotation/annotation-experimental-lint/src/main/java/androidx/annotation/experimental/lint/AnnotationRetentionDetector.kt
@@ -48,12 +48,18 @@
 
     private inner class AnnotationChecker(val context: JavaContext) : UElementHandler() {
         override fun visitAnnotation(node: UAnnotation) {
-            when (node.qualifiedName) {
+            val annotated = node.uastParent as? UAnnotated ?: return
+            val isKotlin = isKotlin(annotated.sourcePsi)
+            val qualifiedName = node.qualifiedName
+
+            if (isKotlin && qualifiedName == JAVA_REQUIRES_OPT_IN_ANNOTATION) {
+                reportKotlinUsage(annotated)
+            }
+
+            when (qualifiedName) {
                 KOTLIN_EXPERIMENTAL_ANNOTATION, KOTLIN_REQUIRES_OPT_IN_ANNOTATION,
                 JAVA_EXPERIMENTAL_ANNOTATION, JAVA_REQUIRES_OPT_IN_ANNOTATION -> {
-                    (node.uastParent as? UAnnotated)?.let { annotated ->
-                        validateAnnotationRetention(annotated)
-                    }
+                    validateAnnotationRetention(annotated)
                 }
             }
         }
@@ -88,7 +94,7 @@
             }?.extractAttribute(context, "value") ?: defaultRetention
 
             if (expectedRetention != actualRetention) {
-                report(
+                reportRetention(
                     annotated,
                     formatRetention(expectedRetention, defaultRetention),
                     formatRetention(actualRetention, defaultRetention),
@@ -108,16 +114,39 @@
          * Reports an issue with the [annotated] element where the [expected] retention does not
          * match the [actual] retention.
          */
-        private fun report(annotated: UAnnotated, expected: String, actual: String) {
+        private fun reportRetention(annotated: UAnnotated, expected: String, actual: String) {
             context.report(
-                ISSUE, annotated, context.getNameLocation(annotated),
+                ISSUE_RETENTION, annotated, context.getNameLocation(annotated),
                 "Experimental annotation has $actual retention, should use $expected"
             )
         }
+
+        /**
+         * Reports an issue with the [annotated] element where the experimental meta-annotation
+         * should be changed to `kotlin.RequiresOptIn`.
+         */
+        private fun reportKotlinUsage(annotated: UAnnotated) {
+            context.report(
+                ISSUE_KOTLIN_USAGE, annotated, context.getNameLocation(annotated),
+                "Experimental annotation should use kotlin.RequiresOptIn"
+            )
+        }
     }
 
     companion object {
-        val ISSUE = Issue.create(
+        val ISSUE_KOTLIN_USAGE = Issue.create(
+            "WrongRequiresOptIn",
+            "Experimental annotations defined in Kotlin must use kotlin.RequiresOptIn",
+            """
+            Experimental features defined in Kotlin source code must be annotated with the Kotlin
+            `@RequiresOptIn` annotation. Using `androidx.annotation.RequiresOptIn` will prevent the
+            Kotlin compiler from enforcing its opt-in policies.
+            """,
+            Category.CORRECTNESS, 4, Severity.ERROR,
+            Implementation(AnnotationRetentionDetector::class.java, Scope.JAVA_FILE_SCOPE)
+        )
+
+        val ISSUE_RETENTION = Issue.create(
             "ExperimentalAnnotationRetention",
             "Experimental annotation with incorrect retention",
             "Experimental annotations defined in Java source should use default " +
@@ -126,6 +155,11 @@
             Category.CORRECTNESS, 5, Severity.ERROR,
             Implementation(AnnotationRetentionDetector::class.java, Scope.JAVA_FILE_SCOPE)
         )
+
+        val ISSUES = listOf(
+            ISSUE_RETENTION,
+            ISSUE_KOTLIN_USAGE
+        )
     }
 }
 
diff --git a/annotation/annotation-experimental-lint/src/main/java/androidx/annotation/experimental/lint/ExperimentalIssueRegistry.kt b/annotation/annotation-experimental-lint/src/main/java/androidx/annotation/experimental/lint/ExperimentalIssueRegistry.kt
index 83bc43f..0f6ff59 100644
--- a/annotation/annotation-experimental-lint/src/main/java/androidx/annotation/experimental/lint/ExperimentalIssueRegistry.kt
+++ b/annotation/annotation-experimental-lint/src/main/java/androidx/annotation/experimental/lint/ExperimentalIssueRegistry.kt
@@ -24,7 +24,7 @@
 class ExperimentalIssueRegistry : IssueRegistry() {
     override val minApi = CURRENT_API
     override val api = 13
-    override val issues get() = ExperimentalDetector.ISSUES + AnnotationRetentionDetector.ISSUE
+    override val issues get() = ExperimentalDetector.ISSUES + AnnotationRetentionDetector.ISSUES
     override val vendor = Vendor(
         feedbackUrl = "https://issuetracker.google.com/issues/new?component=459778",
         identifier = "androidx.annotation.experimental",
diff --git a/annotation/annotation-experimental-lint/src/test/kotlin/androidx/annotation/experimental/lint/AnnotationRetentionDetectorTest.kt b/annotation/annotation-experimental-lint/src/test/kotlin/androidx/annotation/experimental/lint/AnnotationRetentionDetectorTest.kt
index d08a07d..f6ee76c 100644
--- a/annotation/annotation-experimental-lint/src/test/kotlin/androidx/annotation/experimental/lint/AnnotationRetentionDetectorTest.kt
+++ b/annotation/annotation-experimental-lint/src/test/kotlin/androidx/annotation/experimental/lint/AnnotationRetentionDetectorTest.kt
@@ -35,7 +35,7 @@
                 RequiresOptInDetectorTest.ANDROIDX_OPT_IN_KT,
                 *testFiles
             )
-            .issues(AnnotationRetentionDetector.ISSUE)
+            .issues(*AnnotationRetentionDetector.ISSUES.toTypedArray())
             .run()
     }
 
@@ -78,4 +78,26 @@
 
         check(*input).expect(expected)
     }
+
+    /**
+     * Test for lint check that discourages the use of Java-style opt-in on Kotlin-sourced
+     * annotations.
+     */
+    @Test
+    fun wrongRequiresOptInAnnotation() {
+        val input = arrayOf(
+            ktSample("sample.kotlin.ExperimentalKotlinAnnotationWrongAnnotation"),
+        )
+
+        /* ktlint-disable max-line-length */
+        val expected = """
+src/sample/kotlin/ExperimentalKotlinAnnotationWrongAnnotation.kt:22: Error: Experimental annotation should use kotlin.RequiresOptIn [WrongRequiresOptIn]
+annotation class ExperimentalKotlinAnnotationWrongAnnotation
+                 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+1 errors, 0 warnings
+        """.trimIndent()
+        /* ktlint-enable max-line-length */
+
+        check(*input).expect(expected)
+    }
 }
\ No newline at end of file
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/processing/Edge.java b/camera/camera-core/src/main/java/androidx/camera/core/processing/Edge.java
new file mode 100644
index 0000000..e061f95
--- /dev/null
+++ b/camera/camera-core/src/main/java/androidx/camera/core/processing/Edge.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.core.processing;
+
+import android.media.ImageWriter;
+import android.view.Surface;
+
+/**
+ * A handler to a publisher/subscriber pair.
+ *
+ * <p>Upstream units publish the frames to the edge, which will be delivered to the downstream
+ * subscribers. Edge usually contains both the image buffer and the specifications about the
+ * image, such as the size and format of the image buffer.
+ *
+ * <p>One example is Android’s {@link Surface} API. Surface is a handler to a {@code BufferQueue}.
+ * The publisher, e.g. camera, sends images to the {@link Surface} by using APIs such as
+ * {@link ImageWriter}, OpenGL and/or NDK. Subscribers can get the frames by using APIs such as
+ * {@code ImageReader.OnImageAvailableListener} or {@code SurfaceTexture.OnFrameAvailableListener}.
+ */
+public interface Edge {
+}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/processing/Node.java b/camera/camera-core/src/main/java/androidx/camera/core/processing/Node.java
index 34a1f12..ae4f055 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/processing/Node.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/processing/Node.java
@@ -24,9 +24,7 @@
 import androidx.camera.core.impl.CameraCaptureResult;
 
 /**
- * Base unit for CameraX post-processing.
- *
- * <p>All CameraX post-processing should be wrapped by this interface to explicitly define the I/O.
+ * A pub/sub based unit for CameraX post-processing.
  *
  * <p>Both {@link I} and {@link O} should include handlers to buffers that
  * contain camera frames, as well as the callbacks to notify when the frames are updated. One
@@ -64,11 +62,6 @@
      *
      * <p>This method will be invoked in {@code UseCase#createPipeline}. For now, {@code
      * #createPipeline}s are called on the main thread.
-     *
-     * <p> Returns {@code null} if the input does not change the current state of the
-     * {@link Node}. This usually happens when the input specification can be handled by the
-     * previously allocated buffer, thus no new buffer needs to be allocated. The node will
-     * provide the existing buffer for the upstream node to write to.
      */
     @NonNull
     @MainThread
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/processing/Processor.java b/camera/camera-core/src/main/java/androidx/camera/core/processing/Processor.java
new file mode 100644
index 0000000..759708c
--- /dev/null
+++ b/camera/camera-core/src/main/java/androidx/camera/core/processing/Processor.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.core.processing;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.WorkerThread;
+import androidx.camera.core.ImageCaptureException;
+import androidx.camera.core.impl.utils.executor.CameraXExecutors;
+
+/**
+ * Class for processing a single image frame.
+ *
+ * <p>Both {@link I} and {@link O} contain one or many camera frames and their metadata such as
+ * dimension, format and Exif.
+ *
+ * <p>This is a syntax sugar for the {@link Node} class. The purpose is for building a pipeline
+ * intuitively without using {@link Node}'s publisher/subscriber model.
+ *
+ * @param <I> input image frame
+ * @param <O> output image frame.
+ */
+public interface Processor<I, O> {
+
+    /**
+     * Processes an input frame and produces an output frame.
+     *
+     * <p>The implementation of method performs the image processing operations that usually
+     * blocks the current thread. It must be invoked on a non-blocking thread. e.g.
+     * {@link CameraXExecutors#ioExecutor()}.
+     */
+    @NonNull
+    @WorkerThread
+    O process(@NonNull I i) throws ImageCaptureException;
+}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/processing/SurfaceEdge.java b/camera/camera-core/src/main/java/androidx/camera/core/processing/SurfaceEdge.java
index 20bab64..b2af040 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/processing/SurfaceEdge.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/processing/SurfaceEdge.java
@@ -28,7 +28,7 @@
  * A data class represents a {@link Node} output that is based on {@link Surface}s.
  */
 @AutoValue
-public abstract class SurfaceEdge {
+public abstract class SurfaceEdge implements Edge {
 
     /**
      * Gets output surfaces.
diff --git a/camera/camera-mlkit-vision/src/main/java/androidx/camera/mlkit/vision/MlKitAnalyzer.java b/camera/camera-mlkit-vision/src/main/java/androidx/camera/mlkit/vision/MlKitAnalyzer.java
index 52b5f08..55b0cfd 100644
--- a/camera/camera-mlkit-vision/src/main/java/androidx/camera/mlkit/vision/MlKitAnalyzer.java
+++ b/camera/camera-mlkit-vision/src/main/java/androidx/camera/mlkit/vision/MlKitAnalyzer.java
@@ -51,18 +51,18 @@
 import java.util.concurrent.Executor;
 
 /**
- * An implementation of {@link ImageAnalysis.Analyzer} with MLKit libraries.
+ * An implementation of {@link ImageAnalysis.Analyzer} with ML Kit libraries.
  *
- * <p> This class is a wrapper of one or many MLKit {@code Detector}s. It forwards
+ * <p> This class is a wrapper of one or many ML Kit {@code Detector}s. It forwards
  * {@link ImageAnalysis} frames to all the {@code Detector}s sequentially. Once all the
  * {@code Detector}s finish analyzing the frame, {@link Consumer#accept} will be
  * invoked with the aggregated analysis results.
  *
- * <p> This class handles the coordinates transformation between MLKit output and the target
- * coordinate system. Based the {@code targetCoordinateSystem} set in the constructor, it
+ * <p> This class handles the coordinate transformation between ML Kit output and the target
+ * coordinate system. Using the {@code targetCoordinateSystem} set in the constructor, it
  * calculates the {@link Matrix} with the value provided by CameraX via
- * {@link ImageAnalysis.Analyzer#updateTransform} and forward it to the MLKit {@code Detector}. The
- * coordinates returned by MLKit will be in the desired coordinate system.
+ * {@link ImageAnalysis.Analyzer#updateTransform} and forwards it to the ML Kit {@code Detector}. The
+ * coordinates returned by MLKit will be in the specified coordinate system.
  *
  * <p> This class is designed to work seamlessly with the {@code CameraController} class in
  * camera-view. When used with {@link ImageAnalysis} in camera-core, the following scenarios may
@@ -79,7 +79,7 @@
  *  cameraController.setImageAnalysisAnalyzer(executor,
  *       new MlKitAnalyzer(List.of(barcodeScanner), COORDINATE_SYSTEM_VIEW_REFERENCED,
  *       executor, result -> {
- *    // The value of result.getResult(barcodeScanner) can be used directly for drawying UI layover.
+ *    // The value of result.getResult(barcodeScanner) can be used directly for drawing UI overlay.
  *  });
  * </pre></code>
  *
@@ -109,23 +109,23 @@
     /**
      * Constructor of {@link MlKitAnalyzer}.
      *
-     * <p>The list detectors will be invoked sequentially in order.
+     * <p>The list of detectors will be invoked sequentially in order.
      *
      * <p>When the targetCoordinateSystem is {@link ImageAnalysis#COORDINATE_SYSTEM_ORIGINAL}, the
-     * output coordinate system is defined by MLKit, which is the buffer with rotation applied. For
+     * output coordinate system is defined by ML Kit, which is the buffer with rotation applied. For
      * example, if {@link ImageProxy#getHeight()} is {@code h} and the rotation is 90°, (0, 0) in
      * the result maps to the pixel (0, h) in the original buffer.
      *
      * <p>The constructor throws {@link IllegalArgumentException} if
      * {@code Detector#getDetectorType()} is TYPE_SEGMENTATION and {@code targetCoordinateSystem}
-     * is COORDINATE_SYSTEM_ORIGINAL. Currently MLKit does not support transformation with
+     * is COORDINATE_SYSTEM_ORIGINAL. Currently ML Kit does not support transformation with
      * segmentation.
      *
-     * @param detectors              list of MLKit {@link Detector}.
+     * @param detectors              list of ML Kit {@link Detector}.
      * @param targetCoordinateSystem e.g. {@link ImageAnalysis#COORDINATE_SYSTEM_ORIGINAL}
-     *                               the coordinates in MLKit output will be based on this value.
+     *                               the coordinates in ML Kit output will be based on this value.
      * @param executor               on which the consumer is invoked.
-     * @param consumer               invoked when there is new MLKit result.
+     * @param consumer               invoked when there is a new ML Kit result.
      */
     @OptIn(markerClass = TransformExperimental.class)
     public MlKitAnalyzer(
@@ -262,7 +262,7 @@
     /**
      * Gets the recommended resolution for the given {@code Detector} type.
      *
-     * <p> The resolution can be found on MLKit's DAC page.
+     * <p> The resolution can be found on ML Kit's DAC page.
      */
     @NonNull
     private Size getTargetResolution(int detectorType) {
@@ -314,7 +314,7 @@
         }
 
         /**
-         * Get the analysis result for the given MLKit {@code Detector}.
+         * Get the analysis result for the given ML Kit {@code Detector}.
          *
          * <p>Returns {@code null} if the detection is unsuccessful.
          *
diff --git a/camera/camera-mlkit-vision/src/main/java/androidx/camera/mlkit/vision/camera-mlkit-vision-documentation.md b/camera/camera-mlkit-vision/src/main/java/androidx/camera/mlkit/vision/camera-mlkit-vision-documentation.md
index dea5c58..d7bbf31 100644
--- a/camera/camera-mlkit-vision/src/main/java/androidx/camera/mlkit/vision/camera-mlkit-vision-documentation.md
+++ b/camera/camera-mlkit-vision/src/main/java/androidx/camera/mlkit/vision/camera-mlkit-vision-documentation.md
@@ -1,8 +1,8 @@
 # Module root
 
-CameraX MLKit Vision
+CameraX ML Kit Vision
 
 # Package androidx.camera.mlkit.vision
 
-A library providing a seamless integration between CameraX and Google's MLKit library.
+A library providing a seamless integration between CameraX and Google's ML Kit library.
 
diff --git a/car/app/app-automotive/src/main/java/androidx/car/app/activity/CarAppViewModelFactory.java b/car/app/app-automotive/src/main/java/androidx/car/app/activity/CarAppViewModelFactory.java
index 4ce892c..dd8bdef 100644
--- a/car/app/app-automotive/src/main/java/androidx/car/app/activity/CarAppViewModelFactory.java
+++ b/car/app/app-automotive/src/main/java/androidx/car/app/activity/CarAppViewModelFactory.java
@@ -24,6 +24,7 @@
 import androidx.annotation.NonNull;
 import androidx.annotation.RestrictTo;
 import androidx.car.app.SessionInfo;
+import androidx.core.util.Pair;
 import androidx.lifecycle.ViewModel;
 import androidx.lifecycle.ViewModelProvider;
 
@@ -31,13 +32,15 @@
 import java.util.Map;
 
 /**
- * A factory to provide a unique {@link CarAppViewModel} for each given {@link ComponentName}.
+ * A factory to provide a unique {@link CarAppViewModel} for each pair of {@link ComponentName} and
+ * {@link SessionInfo}.
  *
  * @hide
  */
 @RestrictTo(LIBRARY)
 class CarAppViewModelFactory implements ViewModelProvider.Factory {
-    private static final Map<ComponentName, CarAppViewModelFactory> sInstances = new HashMap<>();
+    private static final Map<Pair<ComponentName, SessionInfo>, CarAppViewModelFactory> sInstances =
+            new HashMap<>();
 
     Application mApplication;
     ComponentName mComponentName;
@@ -51,17 +54,19 @@
     }
 
     /**
-     * Retrieve a singleton instance of CarAppViewModelFactory for the given key.
+     * Retrieve a singleton instance of CarAppViewModelFactory for the given
+     * {@link ComponentName} and {@link SessionInfo}.
      *
      * @return A valid {@link CarAppViewModelFactory}
      */
     @NonNull
     static CarAppViewModelFactory getInstance(Application application,
             ComponentName componentName, SessionInfo sessionInfo) {
-        CarAppViewModelFactory instance = sInstances.get(componentName);
+        Pair<ComponentName, SessionInfo> instanceCacheKey = new Pair<>(componentName, sessionInfo);
+        CarAppViewModelFactory instance = sInstances.get(instanceCacheKey);
         if (instance == null) {
             instance = new CarAppViewModelFactory(componentName, application, sessionInfo);
-            sInstances.put(componentName, instance);
+            sInstances.put(instanceCacheKey, instance);
         }
         return instance;
     }
diff --git a/car/app/app-automotive/src/test/java/androidx/car/app/activity/CarAppViewModelFactoryTest.java b/car/app/app-automotive/src/test/java/androidx/car/app/activity/CarAppViewModelFactoryTest.java
index 8625a22..fc228fe 100644
--- a/car/app/app-automotive/src/test/java/androidx/car/app/activity/CarAppViewModelFactoryTest.java
+++ b/car/app/app-automotive/src/test/java/androidx/car/app/activity/CarAppViewModelFactoryTest.java
@@ -17,12 +17,14 @@
 package androidx.car.app.activity;
 
 import static androidx.car.app.SessionInfo.DEFAULT_SESSION_INFO;
+import static androidx.car.app.SessionInfo.DISPLAY_TYPE_MAIN;
 
 import static com.google.common.truth.Truth.assertThat;
 
 import android.app.Application;
 import android.content.ComponentName;
 
+import androidx.car.app.SessionInfo;
 import androidx.test.core.app.ApplicationProvider;
 
 import org.junit.Test;
@@ -49,18 +51,29 @@
         CarAppViewModelFactory factory2 = CarAppViewModelFactory.getInstance(mApplication,
                 TEST_COMPONENT_NAME_1, DEFAULT_SESSION_INFO);
 
-        assertThat(factory1).isEqualTo(factory2);
+        assertThat(factory1).isSameInstanceAs(factory2);
     }
 
     @Test
-    public void getInstance_differentKeys_returnsDifferent() {
+    public void getInstance_differentComponentNames_returnsDifferent() {
         CarAppViewModelFactory factory1 = CarAppViewModelFactory.getInstance(mApplication,
                 TEST_COMPONENT_NAME_1, DEFAULT_SESSION_INFO);
 
         CarAppViewModelFactory factory2 = CarAppViewModelFactory.getInstance(mApplication,
                 TEST_COMPONENT_NAME_2, DEFAULT_SESSION_INFO);
 
-        assertThat(factory1).isNotEqualTo(factory2);
+        assertThat(factory1).isNotSameInstanceAs(factory2);
+    }
+
+    @Test
+    public void getInstance_differentSessionInfos_returnsDifferent() {
+        CarAppViewModelFactory factory1 = CarAppViewModelFactory.getInstance(mApplication,
+                TEST_COMPONENT_NAME_1, DEFAULT_SESSION_INFO);
+
+        CarAppViewModelFactory factory2 = CarAppViewModelFactory.getInstance(mApplication,
+                TEST_COMPONENT_NAME_1, new SessionInfo(DISPLAY_TYPE_MAIN, "a session id"));
+
+        assertThat(factory1).isNotSameInstanceAs(factory2);
     }
 
     @Test
diff --git a/car/app/app/api/current.txt b/car/app/app/api/current.txt
index cc6101d..919b746 100644
--- a/car/app/app/api/current.txt
+++ b/car/app/app/api/current.txt
@@ -980,6 +980,7 @@
     method public java.util.List<androidx.car.app.model.Action!> getActions();
     method public androidx.car.app.model.CarIcon? getImage();
     method public androidx.car.app.model.Metadata? getMetadata();
+    method public int getNumericDecoration();
     method public androidx.car.app.model.OnClickDelegate? getOnClickDelegate();
     method public int getRowImageType();
     method public java.util.List<androidx.car.app.model.CarText!> getTexts();
@@ -992,6 +993,7 @@
     field public static final int IMAGE_TYPE_ICON = 4; // 0x4
     field public static final int IMAGE_TYPE_LARGE = 2; // 0x2
     field public static final int IMAGE_TYPE_SMALL = 1; // 0x1
+    field public static final int NO_DECORATION = -1; // 0xffffffff
   }
 
   public static final class Row.Builder {
@@ -1005,6 +1007,7 @@
     method public androidx.car.app.model.Row.Builder setImage(androidx.car.app.model.CarIcon);
     method public androidx.car.app.model.Row.Builder setImage(androidx.car.app.model.CarIcon, int);
     method public androidx.car.app.model.Row.Builder setMetadata(androidx.car.app.model.Metadata);
+    method public androidx.car.app.model.Row.Builder setNumericDecoration(int);
     method public androidx.car.app.model.Row.Builder setOnClickListener(androidx.car.app.model.OnClickListener);
     method public androidx.car.app.model.Row.Builder setTitle(CharSequence);
     method public androidx.car.app.model.Row.Builder setTitle(androidx.car.app.model.CarText);
diff --git a/car/app/app/api/public_plus_experimental_current.txt b/car/app/app/api/public_plus_experimental_current.txt
index a040961..41b5994 100644
--- a/car/app/app/api/public_plus_experimental_current.txt
+++ b/car/app/app/api/public_plus_experimental_current.txt
@@ -1330,6 +1330,7 @@
     method public java.util.List<androidx.car.app.model.Action!> getActions();
     method public androidx.car.app.model.CarIcon? getImage();
     method public androidx.car.app.model.Metadata? getMetadata();
+    method public int getNumericDecoration();
     method public androidx.car.app.model.OnClickDelegate? getOnClickDelegate();
     method public int getRowImageType();
     method public java.util.List<androidx.car.app.model.CarText!> getTexts();
@@ -1342,6 +1343,7 @@
     field public static final int IMAGE_TYPE_ICON = 4; // 0x4
     field public static final int IMAGE_TYPE_LARGE = 2; // 0x2
     field public static final int IMAGE_TYPE_SMALL = 1; // 0x1
+    field public static final int NO_DECORATION = -1; // 0xffffffff
   }
 
   public static final class Row.Builder {
@@ -1355,6 +1357,7 @@
     method public androidx.car.app.model.Row.Builder setImage(androidx.car.app.model.CarIcon);
     method public androidx.car.app.model.Row.Builder setImage(androidx.car.app.model.CarIcon, int);
     method public androidx.car.app.model.Row.Builder setMetadata(androidx.car.app.model.Metadata);
+    method public androidx.car.app.model.Row.Builder setNumericDecoration(int);
     method public androidx.car.app.model.Row.Builder setOnClickListener(androidx.car.app.model.OnClickListener);
     method public androidx.car.app.model.Row.Builder setTitle(CharSequence);
     method public androidx.car.app.model.Row.Builder setTitle(androidx.car.app.model.CarText);
diff --git a/car/app/app/api/restricted_current.txt b/car/app/app/api/restricted_current.txt
index cc6101d..919b746 100644
--- a/car/app/app/api/restricted_current.txt
+++ b/car/app/app/api/restricted_current.txt
@@ -980,6 +980,7 @@
     method public java.util.List<androidx.car.app.model.Action!> getActions();
     method public androidx.car.app.model.CarIcon? getImage();
     method public androidx.car.app.model.Metadata? getMetadata();
+    method public int getNumericDecoration();
     method public androidx.car.app.model.OnClickDelegate? getOnClickDelegate();
     method public int getRowImageType();
     method public java.util.List<androidx.car.app.model.CarText!> getTexts();
@@ -992,6 +993,7 @@
     field public static final int IMAGE_TYPE_ICON = 4; // 0x4
     field public static final int IMAGE_TYPE_LARGE = 2; // 0x2
     field public static final int IMAGE_TYPE_SMALL = 1; // 0x1
+    field public static final int NO_DECORATION = -1; // 0xffffffff
   }
 
   public static final class Row.Builder {
@@ -1005,6 +1007,7 @@
     method public androidx.car.app.model.Row.Builder setImage(androidx.car.app.model.CarIcon);
     method public androidx.car.app.model.Row.Builder setImage(androidx.car.app.model.CarIcon, int);
     method public androidx.car.app.model.Row.Builder setMetadata(androidx.car.app.model.Metadata);
+    method public androidx.car.app.model.Row.Builder setNumericDecoration(int);
     method public androidx.car.app.model.Row.Builder setOnClickListener(androidx.car.app.model.OnClickListener);
     method public androidx.car.app.model.Row.Builder setTitle(CharSequence);
     method public androidx.car.app.model.Row.Builder setTitle(androidx.car.app.model.CarText);
diff --git a/car/app/app/src/main/java/androidx/car/app/model/Row.java b/car/app/app/src/main/java/androidx/car/app/model/Row.java
index 8987cfa..09805cc 100644
--- a/car/app/app/src/main/java/androidx/car/app/model/Row.java
+++ b/car/app/app/src/main/java/androidx/car/app/model/Row.java
@@ -52,6 +52,9 @@
     /** A boat that belongs to you. */
     private static final String YOUR_BOAT = "\uD83D\uDEA3"; // 🚣
 
+    /** An integer value indicating no decoration should be shown. */
+    public static final int NO_DECORATION = -1;
+
     /**
      * The type of images supported within rows.
      *
@@ -106,6 +109,8 @@
     @Keep
     private final List<Action> mActions;
     @Keep
+    private final int mDecoration;
+    @Keep
     @Nullable
     private final Toggle mToggle;
     @Keep
@@ -168,6 +173,19 @@
     }
 
     /**
+     * Returns the numeric decoration.
+     *
+     * <p> Numeric decorations are displayed at the end of the row, but before any actions.
+     *
+     * <p> {@link Row#NO_DECORATION} will be returned if the row does not contain a decoration.
+     *
+     * @see Builder#setNumericDecoration(int)
+     */
+    public int getNumericDecoration() {
+        return mDecoration;
+    }
+
+    /**
      * Returns the {@link Toggle} in the row or {@code null} if the row does not contain a
      * toggle.
      *
@@ -292,6 +310,7 @@
         mTexts = CollectionUtils.unmodifiableCopy(builder.mTexts);
         mImage = builder.mImage;
         mActions = CollectionUtils.unmodifiableCopy(builder.mActions);
+        mDecoration = builder.mDecoration;
         mToggle = builder.mToggle;
         mOnClickDelegate = builder.mOnClickDelegate;
         mMetadata = builder.mMetadata;
@@ -306,6 +325,7 @@
         mTexts = Collections.emptyList();
         mImage = null;
         mActions = Collections.emptyList();
+        mDecoration = NO_DECORATION;
         mToggle = null;
         mOnClickDelegate = null;
         mMetadata = EMPTY_METADATA;
@@ -323,6 +343,7 @@
         @Nullable
         CarIcon mImage;
         final List<Action> mActions = new ArrayList<>();
+        int mDecoration;
         @Nullable
         Toggle mToggle;
         @Nullable
@@ -522,6 +543,35 @@
         }
 
         /**
+         * Sets a numeric decoration to display in the row.
+         *
+         * <p> Numeric decorations are displayed at the end of the row, but before any actions.
+         *
+         * <p> Numeric decorations typically represent a quantity of unseen content. For example, a
+         * decoration might represent a number of missed notifications, or a number of unread
+         * messages in a conversation.
+         *
+         * @param decoration the {@code int} to display. Must be positive, zero, or equal to
+         * {@link Row#NO_DECORATION}.
+         * @throws IllegalArgumentException if {@code decoration} is invalid
+         */
+        @NonNull
+        public Builder setNumericDecoration(int decoration) {
+            if (decoration < 0 && decoration != NO_DECORATION) {
+                throw new IllegalArgumentException(
+                        String.format(
+                                "Decoration should be positive, zero, or equal to NO_DECORATION. "
+                                        + "Instead, was %d",
+                                decoration
+                        )
+                );
+            }
+
+            mDecoration = decoration;
+            return this;
+        }
+
+        /**
          * Sets a {@link Toggle} to show in the row.
          *
          * @throws NullPointerException if {@code toggle} is {@code null}
diff --git a/car/app/app/src/test/java/androidx/car/app/model/RowTest.java b/car/app/app/src/test/java/androidx/car/app/model/RowTest.java
index d64eac7..c745f1a 100644
--- a/car/app/app/src/test/java/androidx/car/app/model/RowTest.java
+++ b/car/app/app/src/test/java/androidx/car/app/model/RowTest.java
@@ -132,6 +132,38 @@
     }
 
     @Test
+    public void setDecoration_positiveValue() {
+        int decoration = 5;
+        Row row = new Row.Builder().setTitle("Title").setNumericDecoration(decoration).build();
+        assertThat(decoration).isEqualTo(row.getNumericDecoration());
+    }
+
+    @Test
+    public void setDecoration_zero() {
+        int decoration = 0;
+        Row row = new Row.Builder().setTitle("Title").setNumericDecoration(decoration).build();
+        assertThat(decoration).isEqualTo(row.getNumericDecoration());
+    }
+
+    @Test
+    public void setDecoration_noDecoration() {
+        int decoration = Row.NO_DECORATION;
+        Row row = new Row.Builder().setTitle("Title").setNumericDecoration(decoration).build();
+        assertThat(decoration).isEqualTo(row.getNumericDecoration());
+    }
+
+    @Test
+    public void setDecoration_negative_throws() {
+        int decoration = -123;
+        Row.Builder rowBuilder =
+                new Row.Builder().setTitle("Title");
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> rowBuilder.setNumericDecoration(decoration)
+        );
+    }
+
+    @Test
     public void setToggle() {
         Toggle toggle1 = new Toggle.Builder(isChecked -> {
         }).build();
diff --git a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/VersionChecker.kt b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/VersionChecker.kt
index 7a4a17c..9a50c15 100644
--- a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/VersionChecker.kt
+++ b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/VersionChecker.kt
@@ -26,10 +26,11 @@
 
     companion object {
         /**
-         * A table of runtime version ints to version strings. This should be
-         * updated every time ComposeVersion.kt is updated.
+         * A table of runtime version ints to version strings for compose-runtime.
+         * This should be updated every time a new version of the Compose Runtime is released.
+         * Typically updated via update_versions_for_release.py
          */
-        private val versionTable = mapOf(
+        private val runtimeVersionToMavenVersionTable = mapOf(
             1600 to "0.1.0-dev16",
             1700 to "1.0.0-alpha06",
             1800 to "1.0.0-alpha07",
@@ -103,7 +104,7 @@
          */
         const val compilerVersion: String = "1.3.1"
         private val minimumRuntimeVersion: String
-            get() = versionTable[minimumRuntimeVersionInt] ?: "unknown"
+            get() = runtimeVersionToMavenVersionTable[minimumRuntimeVersionInt] ?: "unknown"
     }
 
     fun check() {
@@ -137,7 +138,7 @@
         }
         val versionInt = versionExpr.value as Int
         if (versionInt < minimumRuntimeVersionInt) {
-            outdatedRuntime(versionTable[versionInt] ?: "<unknown>")
+            outdatedRuntime(runtimeVersionToMavenVersionTable[versionInt] ?: "<unknown>")
         }
         // success. We are compatible with this runtime version!
     }
diff --git a/compose/foundation/foundation/api/current.txt b/compose/foundation/foundation/api/current.txt
index 3feb40c..963ea75 100644
--- a/compose/foundation/foundation/api/current.txt
+++ b/compose/foundation/foundation/api/current.txt
@@ -708,6 +708,22 @@
 
 }
 
+package androidx.compose.foundation.lazy.staggeredgrid {
+
+  public final class LazyStaggeredGridDslKt {
+  }
+
+  public final class LazyStaggeredGridItemProviderKt {
+  }
+
+  public final class LazyStaggeredGridKt {
+  }
+
+  public final class LazyStaggeredGridMeasureKt {
+  }
+
+}
+
 package androidx.compose.foundation.relocation {
 
   public final class BringIntoViewKt {
diff --git a/compose/foundation/foundation/api/public_plus_experimental_current.txt b/compose/foundation/foundation/api/public_plus_experimental_current.txt
index 6f14566..93e6607 100644
--- a/compose/foundation/foundation/api/public_plus_experimental_current.txt
+++ b/compose/foundation/foundation/api/public_plus_experimental_current.txt
@@ -853,6 +853,22 @@
 
 }
 
+package androidx.compose.foundation.lazy.staggeredgrid {
+
+  public final class LazyStaggeredGridDslKt {
+  }
+
+  public final class LazyStaggeredGridItemProviderKt {
+  }
+
+  public final class LazyStaggeredGridKt {
+  }
+
+  public final class LazyStaggeredGridMeasureKt {
+  }
+
+}
+
 package androidx.compose.foundation.relocation {
 
   public final class BringIntoViewKt {
diff --git a/compose/foundation/foundation/api/restricted_current.txt b/compose/foundation/foundation/api/restricted_current.txt
index 3feb40c..963ea75 100644
--- a/compose/foundation/foundation/api/restricted_current.txt
+++ b/compose/foundation/foundation/api/restricted_current.txt
@@ -708,6 +708,22 @@
 
 }
 
+package androidx.compose.foundation.lazy.staggeredgrid {
+
+  public final class LazyStaggeredGridDslKt {
+  }
+
+  public final class LazyStaggeredGridItemProviderKt {
+  }
+
+  public final class LazyStaggeredGridKt {
+  }
+
+  public final class LazyStaggeredGridMeasureKt {
+  }
+
+}
+
 package androidx.compose.foundation.relocation {
 
   public final class BringIntoViewKt {
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/BaseLazyLayoutTestWithOrientation.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/BaseLazyLayoutTestWithOrientation.kt
new file mode 100644
index 0000000..179dd3a
--- /dev/null
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/BaseLazyLayoutTestWithOrientation.kt
@@ -0,0 +1,151 @@
+/*
+ * 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
+
+import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.lazy.grid.scrollBy
+import androidx.compose.runtime.Stable
+import androidx.compose.testutils.assertIsEqualTo
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.test.SemanticsNodeInteraction
+import androidx.compose.ui.test.assertHeightIsEqualTo
+import androidx.compose.ui.test.assertLeftPositionInRootIsEqualTo
+import androidx.compose.ui.test.assertTopPositionInRootIsEqualTo
+import androidx.compose.ui.test.assertWidthIsEqualTo
+import androidx.compose.ui.test.getUnclippedBoundsInRoot
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+import org.junit.Rule
+
+open class BaseLazyLayoutTestWithOrientation(private val orientation: Orientation) {
+    @get:Rule
+    val rule = createComposeRule()
+
+    val vertical: Boolean
+        get() = orientation == Orientation.Vertical
+
+    @Stable
+    fun Modifier.crossAxisSize(size: Dp) =
+        if (vertical) {
+            this.width(size)
+        } else {
+            this.height(size)
+        }
+
+    @Stable
+    fun Modifier.mainAxisSize(size: Dp) =
+        if (vertical) {
+            this.height(size)
+        } else {
+            this.width(size)
+        }
+
+    @Stable
+    fun Modifier.axisSize(crossAxis: Dp, mainAxis: Dp) =
+        if (vertical) {
+            this.size(crossAxis, mainAxis)
+        } else {
+            this.size(mainAxis, crossAxis)
+        }
+
+    fun SemanticsNodeInteraction.scrollMainAxisBy(distance: Dp) {
+        if (vertical) {
+            this.scrollBy(y = distance, density = rule.density)
+        } else {
+            this.scrollBy(x = distance, density = rule.density)
+        }
+    }
+
+    fun SemanticsNodeInteraction.assertMainAxisSizeIsEqualTo(expectedSize: Dp) =
+        if (vertical) {
+            assertHeightIsEqualTo(expectedSize)
+        } else {
+            assertWidthIsEqualTo(expectedSize)
+        }
+
+    fun SemanticsNodeInteraction.assertCrossAxisSizeIsEqualTo(expectedSize: Dp) =
+        if (vertical) {
+            assertWidthIsEqualTo(expectedSize)
+        } else {
+            assertHeightIsEqualTo(expectedSize)
+        }
+
+    fun SemanticsNodeInteraction.assertStartPositionIsAlmost(expected: Dp) {
+        val position = if (vertical) {
+            getUnclippedBoundsInRoot().top
+        } else {
+            getUnclippedBoundsInRoot().left
+        }
+        position.assertIsEqualTo(expected, tolerance = 1.dp)
+    }
+
+    fun SemanticsNodeInteraction.assertMainAxisStartPositionInRootIsEqualTo(expectedStart: Dp) =
+        if (vertical) {
+            assertTopPositionInRootIsEqualTo(expectedStart)
+        } else {
+            assertLeftPositionInRootIsEqualTo(expectedStart)
+        }
+
+    fun SemanticsNodeInteraction.assertStartPositionInRootIsEqualTo(expectedStart: Dp) =
+        if (vertical) {
+            assertTopPositionInRootIsEqualTo(expectedStart)
+        } else {
+            assertLeftPositionInRootIsEqualTo(expectedStart)
+        }
+
+    fun SemanticsNodeInteraction.assertCrossAxisStartPositionInRootIsEqualTo(expectedStart: Dp) =
+        if (vertical) {
+            assertLeftPositionInRootIsEqualTo(expectedStart)
+        } else {
+            assertTopPositionInRootIsEqualTo(expectedStart)
+        }
+
+    fun PaddingValues(
+        mainAxis: Dp = 0.dp,
+        crossAxis: Dp = 0.dp
+    ) = PaddingValues(
+        beforeContent = mainAxis,
+        afterContent = mainAxis,
+        beforeContentCrossAxis = crossAxis,
+        afterContentCrossAxis = crossAxis
+    )
+
+    fun PaddingValues(
+        beforeContent: Dp = 0.dp,
+        afterContent: Dp = 0.dp,
+        beforeContentCrossAxis: Dp = 0.dp,
+        afterContentCrossAxis: Dp = 0.dp,
+    ) = if (vertical) {
+        androidx.compose.foundation.layout.PaddingValues(
+            start = beforeContentCrossAxis,
+            top = beforeContent,
+            end = afterContentCrossAxis,
+            bottom = afterContent
+        )
+    } else {
+        androidx.compose.foundation.layout.PaddingValues(
+            start = beforeContent,
+            top = beforeContentCrossAxis,
+            end = afterContent,
+            bottom = afterContentCrossAxis
+        )
+    }
+}
\ No newline at end of file
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/grid/BaseLazyGridTestWithOrientation.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/grid/BaseLazyGridTestWithOrientation.kt
index 16b0e12..dcb34e1 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/grid/BaseLazyGridTestWithOrientation.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/grid/BaseLazyGridTestWithOrientation.kt
@@ -18,139 +18,24 @@
 
 import androidx.compose.animation.core.snap
 import androidx.compose.foundation.AutoTestFrameClock
+import androidx.compose.foundation.BaseLazyLayoutTestWithOrientation
 import androidx.compose.foundation.gestures.FlingBehavior
 import androidx.compose.foundation.gestures.Orientation
 import androidx.compose.foundation.gestures.ScrollableDefaults
 import androidx.compose.foundation.gestures.animateScrollBy
 import androidx.compose.foundation.layout.Arrangement
 import androidx.compose.foundation.layout.PaddingValues
-import androidx.compose.foundation.layout.height
-import androidx.compose.foundation.layout.size
-import androidx.compose.foundation.layout.width
 import androidx.compose.runtime.Composable
-import androidx.compose.runtime.Stable
-import androidx.compose.testutils.assertIsEqualTo
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.test.SemanticsNodeInteraction
-import androidx.compose.ui.test.assertHeightIsEqualTo
-import androidx.compose.ui.test.assertLeftPositionInRootIsEqualTo
-import androidx.compose.ui.test.assertTopPositionInRootIsEqualTo
-import androidx.compose.ui.test.assertWidthIsEqualTo
-import androidx.compose.ui.test.getUnclippedBoundsInRoot
-import androidx.compose.ui.test.junit4.createComposeRule
 import androidx.compose.ui.unit.Dp
 import androidx.compose.ui.unit.dp
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.runBlocking
-import org.junit.Rule
 
-open class BaseLazyGridTestWithOrientation(private val orientation: Orientation) {
-
-    @get:Rule
-    val rule = createComposeRule()
-
-    val vertical: Boolean
-        get() = orientation == Orientation.Vertical
-
-    @Stable
-    fun Modifier.crossAxisSize(size: Dp) =
-        if (vertical) {
-            this.width(size)
-        } else {
-            this.height(size)
-        }
-
-    @Stable
-    fun Modifier.mainAxisSize(size: Dp) =
-        if (vertical) {
-            this.height(size)
-        } else {
-            this.width(size)
-        }
-
-    @Stable
-    fun Modifier.axisSize(crossAxis: Dp, mainAxis: Dp) =
-        if (vertical) {
-            this.size(crossAxis, mainAxis)
-        } else {
-            this.size(mainAxis, crossAxis)
-        }
-
-    fun SemanticsNodeInteraction.scrollMainAxisBy(distance: Dp) {
-        if (vertical) {
-            this.scrollBy(y = distance, density = rule.density)
-        } else {
-            this.scrollBy(x = distance, density = rule.density)
-        }
-    }
-
-    fun SemanticsNodeInteraction.assertMainAxisSizeIsEqualTo(expectedSize: Dp) =
-        if (vertical) {
-            assertHeightIsEqualTo(expectedSize)
-        } else {
-            assertWidthIsEqualTo(expectedSize)
-        }
-
-    fun SemanticsNodeInteraction.assertCrossAxisSizeIsEqualTo(expectedSize: Dp) =
-        if (vertical) {
-            assertWidthIsEqualTo(expectedSize)
-        } else {
-            assertHeightIsEqualTo(expectedSize)
-        }
-
-    fun SemanticsNodeInteraction.assertStartPositionIsAlmost(expected: Dp) {
-        val position = if (vertical) {
-            getUnclippedBoundsInRoot().top
-        } else {
-            getUnclippedBoundsInRoot().left
-        }
-        position.assertIsEqualTo(expected, tolerance = 1.dp)
-    }
-
-    fun SemanticsNodeInteraction.assertMainAxisStartPositionInRootIsEqualTo(expectedStart: Dp) =
-        if (vertical) {
-            assertTopPositionInRootIsEqualTo(expectedStart)
-        } else {
-            assertLeftPositionInRootIsEqualTo(expectedStart)
-        }
-
-    fun SemanticsNodeInteraction.assertCrossAxisStartPositionInRootIsEqualTo(expectedStart: Dp) =
-        if (vertical) {
-            assertLeftPositionInRootIsEqualTo(expectedStart)
-        } else {
-            assertTopPositionInRootIsEqualTo(expectedStart)
-        }
-
-    fun PaddingValues(
-        mainAxis: Dp = 0.dp,
-        crossAxis: Dp = 0.dp
-    ) = PaddingValues(
-        beforeContent = mainAxis,
-        afterContent = mainAxis,
-        beforeContentCrossAxis = crossAxis,
-        afterContentCrossAxis = crossAxis
-    )
-
-    fun PaddingValues(
-        beforeContent: Dp = 0.dp,
-        afterContent: Dp = 0.dp,
-        beforeContentCrossAxis: Dp = 0.dp,
-        afterContentCrossAxis: Dp = 0.dp,
-    ) = if (vertical) {
-        PaddingValues(
-            start = beforeContentCrossAxis,
-            top = beforeContent,
-            end = afterContentCrossAxis,
-            bottom = afterContent
-        )
-    } else {
-        PaddingValues(
-            start = beforeContent,
-            top = beforeContentCrossAxis,
-            end = afterContent,
-            bottom = afterContentCrossAxis
-        )
-    }
+open class BaseLazyGridTestWithOrientation(
+    orientation: Orientation
+) : BaseLazyLayoutTestWithOrientation(orientation) {
 
     fun LazyGridState.scrollBy(offset: Dp) {
         runBlocking(Dispatchers.Main + AutoTestFrameClock()) {
@@ -164,11 +49,7 @@
         }
     }
 
-    fun SemanticsNodeInteraction.scrollBy(offset: Dp) = scrollBy(
-        x = if (vertical) 0.dp else offset,
-        y = if (!vertical) 0.dp else offset,
-        density = rule.density
-    )
+    fun SemanticsNodeInteraction.scrollBy(offset: Dp) = scrollMainAxisBy(offset)
 
     @Composable
     fun LazyGrid(
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/list/BaseLazyListTestWithOrientation.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/list/BaseLazyListTestWithOrientation.kt
index 1919263..44c5abf 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/list/BaseLazyListTestWithOrientation.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/list/BaseLazyListTestWithOrientation.kt
@@ -18,6 +18,7 @@
 
 import androidx.compose.animation.core.snap
 import androidx.compose.foundation.AutoTestFrameClock
+import androidx.compose.foundation.BaseLazyLayoutTestWithOrientation
 import androidx.compose.foundation.composeViewSwipeDown
 import androidx.compose.foundation.composeViewSwipeLeft
 import androidx.compose.foundation.composeViewSwipeRight
@@ -30,8 +31,6 @@
 import androidx.compose.foundation.layout.PaddingValues
 import androidx.compose.foundation.layout.fillMaxHeight
 import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.height
-import androidx.compose.foundation.layout.width
 import androidx.compose.foundation.lazy.LazyColumn
 import androidx.compose.foundation.lazy.LazyItemScope
 import androidx.compose.foundation.lazy.LazyListScope
@@ -39,42 +38,16 @@
 import androidx.compose.foundation.lazy.LazyRow
 import androidx.compose.foundation.lazy.rememberLazyListState
 import androidx.compose.runtime.Composable
-import androidx.compose.testutils.assertIsEqualTo
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.test.SemanticsNodeInteraction
-import androidx.compose.ui.test.assertHeightIsEqualTo
-import androidx.compose.ui.test.assertLeftPositionInRootIsEqualTo
-import androidx.compose.ui.test.assertTopPositionInRootIsEqualTo
-import androidx.compose.ui.test.assertWidthIsEqualTo
-import androidx.compose.ui.test.getUnclippedBoundsInRoot
-import androidx.compose.ui.test.junit4.createComposeRule
 import androidx.compose.ui.unit.Dp
 import androidx.compose.ui.unit.dp
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.runBlocking
-import org.junit.Rule
 
-open class BaseLazyListTestWithOrientation(private val orientation: Orientation) {
-
-    @get:Rule
-    val rule = createComposeRule()
-
-    val vertical: Boolean
-        get() = orientation == Orientation.Vertical
-
-    fun Modifier.mainAxisSize(size: Dp) =
-        if (vertical) {
-            this.height(size)
-        } else {
-            this.width(size)
-        }
-
-    fun Modifier.crossAxisSize(size: Dp) =
-        if (vertical) {
-            this.width(size)
-        } else {
-            this.height(size)
-        }
+open class BaseLazyListTestWithOrientation(
+    private val orientation: Orientation
+) : BaseLazyLayoutTestWithOrientation(orientation) {
 
     fun Modifier.fillMaxCrossAxis() =
         if (vertical) {
@@ -90,82 +63,6 @@
             Modifier.fillParentMaxHeight()
         }
 
-    fun SemanticsNodeInteraction.scrollMainAxisBy(distance: Dp) {
-        if (vertical) {
-            this.scrollBy(y = distance, density = rule.density)
-        } else {
-            this.scrollBy(x = distance, density = rule.density)
-        }
-    }
-
-    fun SemanticsNodeInteraction.assertMainAxisSizeIsEqualTo(expectedSize: Dp) =
-        if (vertical) {
-            assertHeightIsEqualTo(expectedSize)
-        } else {
-            assertWidthIsEqualTo(expectedSize)
-        }
-
-    fun SemanticsNodeInteraction.assertCrossAxisSizeIsEqualTo(expectedSize: Dp) =
-        if (vertical) {
-            assertWidthIsEqualTo(expectedSize)
-        } else {
-            assertHeightIsEqualTo(expectedSize)
-        }
-
-    fun SemanticsNodeInteraction.assertStartPositionIsAlmost(expected: Dp) {
-        val position = if (vertical) {
-            getUnclippedBoundsInRoot().top
-        } else {
-            getUnclippedBoundsInRoot().left
-        }
-        position.assertIsEqualTo(expected, tolerance = 1.dp)
-    }
-
-    fun SemanticsNodeInteraction.assertStartPositionInRootIsEqualTo(expectedStart: Dp) =
-        if (vertical) {
-            assertTopPositionInRootIsEqualTo(expectedStart)
-        } else {
-            assertLeftPositionInRootIsEqualTo(expectedStart)
-        }
-
-    fun SemanticsNodeInteraction.assertCrossAxisStartPositionInRootIsEqualTo(expectedStart: Dp) =
-        if (vertical) {
-            assertLeftPositionInRootIsEqualTo(expectedStart)
-        } else {
-            assertTopPositionInRootIsEqualTo(expectedStart)
-        }
-
-    fun PaddingValues(
-        mainAxis: Dp = 0.dp,
-        crossAxis: Dp = 0.dp
-    ) = PaddingValues(
-        beforeContent = mainAxis,
-        afterContent = mainAxis,
-        beforeContentCrossAxis = crossAxis,
-        afterContentCrossAxis = crossAxis
-    )
-
-    fun PaddingValues(
-        beforeContent: Dp = 0.dp,
-        afterContent: Dp = 0.dp,
-        beforeContentCrossAxis: Dp = 0.dp,
-        afterContentCrossAxis: Dp = 0.dp,
-    ) = if (vertical) {
-        PaddingValues(
-            start = beforeContentCrossAxis,
-            top = beforeContent,
-            end = afterContentCrossAxis,
-            bottom = afterContent
-        )
-    } else {
-        PaddingValues(
-            start = beforeContent,
-            top = beforeContentCrossAxis,
-            end = afterContent,
-            bottom = afterContentCrossAxis
-        )
-    }
-
     fun LazyListState.scrollBy(offset: Dp) {
         runBlocking(Dispatchers.Main + AutoTestFrameClock()) {
             animateScrollBy(with(rule.density) { offset.roundToPx().toFloat() }, snap())
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/BaseLazyStaggeredGridWithOrientation.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/BaseLazyStaggeredGridWithOrientation.kt
new file mode 100644
index 0000000..b865bdb
--- /dev/null
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/BaseLazyStaggeredGridWithOrientation.kt
@@ -0,0 +1,72 @@
+/*
+ * 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.animation.core.snap
+import androidx.compose.foundation.AutoTestFrameClock
+import androidx.compose.foundation.BaseLazyLayoutTestWithOrientation
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.foundation.gestures.animateScrollBy
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.Dp
+import kotlin.math.roundToInt
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.runBlocking
+
+@OptIn(ExperimentalFoundationApi::class)
+open class BaseLazyStaggeredGridWithOrientation(
+    private val orientation: Orientation
+) : BaseLazyLayoutTestWithOrientation(orientation) {
+
+    internal fun LazyStaggeredGridState.scrollBy(offset: Dp) {
+        runBlocking(Dispatchers.Main + AutoTestFrameClock()) {
+            animateScrollBy(with(rule.density) { offset.roundToPx().toFloat() }, snap())
+        }
+    }
+
+    @Composable
+    internal fun LazyStaggeredGrid(
+        lanes: Int,
+        modifier: Modifier = Modifier,
+        state: LazyStaggeredGridState = remember { LazyStaggeredGridState() },
+        content: LazyStaggeredGridScope.() -> Unit,
+    ) {
+        LazyStaggeredGrid(
+            state = state,
+            modifier = modifier,
+            orientation = orientation,
+            userScrollEnabled = true,
+            verticalArrangement = Arrangement.Top,
+            horizontalArrangement = Arrangement.Start,
+            slotSizesSums = { constraints ->
+                val crossAxisSize = if (orientation == Orientation.Vertical) {
+                    constraints.maxWidth
+                } else {
+                    constraints.maxHeight
+                }
+                IntArray(lanes) {
+                    (crossAxisSize / lanes.toDouble() * (it + 1)).roundToInt()
+                }
+            },
+            content = content
+        )
+    }
+}
\ No newline at end of file
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridTest.kt
new file mode 100644
index 0000000..df3d0f3
--- /dev/null
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridTest.kt
@@ -0,0 +1,342 @@
+/*
+ * 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.gestures.Orientation
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.size
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.composed
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.assertIsDisplayed
+import androidx.compose.ui.test.assertIsNotDisplayed
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+import androidx.test.filters.MediumTest
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+
+@OptIn(ExperimentalFoundationApi::class)
+@MediumTest
+@RunWith(Parameterized::class)
+class LazyStaggeredGridTest(
+    private val orientation: Orientation
+) : BaseLazyStaggeredGridWithOrientation(orientation) {
+    private val LazyStaggeredGridTag = "LazyStaggeredGridTag"
+
+    companion object {
+        @JvmStatic
+        @Parameterized.Parameters(name = "{0}")
+        fun initParameters(): Array<Any> = arrayOf(
+            Orientation.Vertical,
+            Orientation.Horizontal,
+        )
+    }
+
+    private var itemSizeDp: Dp = Dp.Unspecified
+    private val itemSizePx: Int = 50
+
+    @Before
+    fun setUp() {
+        with(rule.density) {
+            itemSizeDp = itemSizePx.toDp()
+        }
+    }
+
+    @Test
+    fun showsOneItem() {
+        val itemTestTag = "itemTestTag"
+
+        rule.setContent {
+            LazyStaggeredGrid(
+                lanes = 3,
+            ) {
+                item {
+                    Spacer(
+                        Modifier.size(itemSizeDp).testTag(itemTestTag)
+                    )
+                }
+            }
+        }
+
+        rule.onNodeWithTag(itemTestTag)
+            .assertIsDisplayed()
+    }
+
+    @Test
+    fun distributesSingleLine() {
+        rule.setContent {
+            LazyStaggeredGrid(
+                lanes = 3,
+                modifier = Modifier.crossAxisSize(itemSizeDp * 3),
+            ) {
+                items(3) {
+                    Spacer(
+                        Modifier.size(itemSizeDp).testTag("$it")
+                    )
+                }
+            }
+        }
+
+        rule.onNodeWithTag("0")
+            .assertIsDisplayed()
+            .assertMainAxisStartPositionInRootIsEqualTo(0.dp)
+            .assertCrossAxisStartPositionInRootIsEqualTo(0.dp)
+
+        rule.onNodeWithTag("1")
+            .assertIsDisplayed()
+            .assertMainAxisStartPositionInRootIsEqualTo(0.dp)
+            .assertCrossAxisStartPositionInRootIsEqualTo(itemSizeDp)
+
+        rule.onNodeWithTag("2")
+            .assertIsDisplayed()
+            .assertMainAxisStartPositionInRootIsEqualTo(0.dp)
+            .assertCrossAxisStartPositionInRootIsEqualTo(itemSizeDp * 2)
+    }
+
+    @Test
+    fun distributesTwoLines() {
+        rule.setContent {
+            LazyStaggeredGrid(
+                lanes = 3,
+                modifier = Modifier.crossAxisSize(itemSizeDp * 3),
+            ) {
+                items(6) {
+                    Spacer(
+                        Modifier.axisSize(
+                            crossAxis = itemSizeDp,
+                            mainAxis = itemSizeDp * (it + 1)
+                        ).testTag("$it")
+                    )
+                }
+            }
+        }
+
+        rule.onNodeWithTag("0")
+            .assertIsDisplayed()
+            .assertMainAxisStartPositionInRootIsEqualTo(0.dp)
+            .assertCrossAxisStartPositionInRootIsEqualTo(0.dp)
+
+        // [item, 0, 0]
+        rule.onNodeWithTag("1")
+            .assertIsDisplayed()
+            .assertMainAxisStartPositionInRootIsEqualTo(0.dp)
+            .assertCrossAxisStartPositionInRootIsEqualTo(itemSizeDp)
+
+        // [item, item x 2, 0]
+        rule.onNodeWithTag("2")
+            .assertIsDisplayed()
+            .assertMainAxisStartPositionInRootIsEqualTo(0.dp)
+            .assertCrossAxisStartPositionInRootIsEqualTo(itemSizeDp * 2)
+
+        // [item, item x 2, item x 3]
+        rule.onNodeWithTag("3")
+            .assertIsDisplayed()
+            .assertMainAxisStartPositionInRootIsEqualTo(itemSizeDp)
+            .assertCrossAxisStartPositionInRootIsEqualTo(0.dp)
+
+        // [item x 4, item x 2, item x 3]
+        rule.onNodeWithTag("4")
+            .assertIsDisplayed()
+            .assertMainAxisStartPositionInRootIsEqualTo(itemSizeDp * 2)
+            .assertCrossAxisStartPositionInRootIsEqualTo(itemSizeDp)
+
+        // [item x 4, item x 7, item x 3]
+        rule.onNodeWithTag("5")
+            .assertIsDisplayed()
+            .assertMainAxisStartPositionInRootIsEqualTo(itemSizeDp * 3)
+            .assertCrossAxisStartPositionInRootIsEqualTo(itemSizeDp * 2)
+
+        // [item x 4, item x 7, item x 9]
+    }
+
+    @Test
+    fun moreItemsDisplayedOnScroll() {
+        val state = LazyStaggeredGridState()
+        rule.setContent {
+            LazyStaggeredGrid(
+                lanes = 3,
+                state = state,
+                modifier = Modifier.axisSize(itemSizeDp * 3, itemSizeDp),
+            ) {
+                items(6) {
+                    Spacer(
+                        Modifier.axisSize(
+                            crossAxis = itemSizeDp,
+                            mainAxis = itemSizeDp * (it + 1)
+                        ).testTag("$it")
+                    )
+                }
+            }
+        }
+
+        rule.onNodeWithTag("3")
+            .assertIsNotDisplayed()
+
+        state.scrollBy(itemSizeDp * 3)
+
+        // [item, item x 2, item x 3]
+        rule.onNodeWithTag("3")
+            .assertIsDisplayed()
+            .assertMainAxisStartPositionInRootIsEqualTo(-itemSizeDp * 2)
+            .assertCrossAxisStartPositionInRootIsEqualTo(0.dp)
+
+        // [item x 4, item x 2, item x 3]
+        rule.onNodeWithTag("4")
+            .assertIsDisplayed()
+            .assertMainAxisStartPositionInRootIsEqualTo(-itemSizeDp)
+            .assertCrossAxisStartPositionInRootIsEqualTo(itemSizeDp)
+
+        // [item x 4, item x 7, item x 3]
+        rule.onNodeWithTag("5")
+            .assertIsDisplayed()
+            .assertMainAxisStartPositionInRootIsEqualTo(0.dp)
+            .assertCrossAxisStartPositionInRootIsEqualTo(itemSizeDp * 2)
+
+        // [item x 4, item x 7, item x 9]
+    }
+
+    @Test
+    fun itemsAreHiddenOnScroll() {
+        val state = LazyStaggeredGridState()
+        rule.setContent {
+            LazyStaggeredGrid(
+                lanes = 3,
+                state = state,
+                modifier = Modifier.axisSize(itemSizeDp * 3, itemSizeDp),
+            ) {
+                items(6) {
+                    Spacer(
+                        Modifier.axisSize(
+                            crossAxis = itemSizeDp,
+                            mainAxis = itemSizeDp * (it + 1)
+                        ).testTag("$it")
+                    )
+                }
+            }
+        }
+
+        rule.onNodeWithTag("0")
+            .assertIsDisplayed()
+
+        state.scrollBy(itemSizeDp * 3)
+
+        rule.onNodeWithTag("0")
+            .assertIsNotDisplayed()
+
+        rule.onNodeWithTag("1")
+            .assertIsNotDisplayed()
+
+        rule.onNodeWithTag("2")
+            .assertIsNotDisplayed()
+    }
+
+    @Test
+    fun itemsArePresentedWhenScrollingBack() {
+        val state = LazyStaggeredGridState()
+        rule.setContent {
+            LazyStaggeredGrid(
+                lanes = 3,
+                state = state,
+                modifier = Modifier.axisSize(itemSizeDp * 3, itemSizeDp),
+            ) {
+                items(6) {
+                    Spacer(
+                        Modifier.axisSize(
+                            crossAxis = itemSizeDp,
+                            mainAxis = itemSizeDp * (it + 1)
+                        ).testTag("$it")
+                    )
+                }
+            }
+        }
+
+        rule.onNodeWithTag("0")
+            .assertIsDisplayed()
+
+        state.scrollBy(itemSizeDp * 3)
+        state.scrollBy(-itemSizeDp * 3)
+
+        for (i in 0..2) {
+            rule.onNodeWithTag("$i")
+                .assertIsDisplayed()
+                .assertMainAxisStartPositionInRootIsEqualTo(0.dp)
+        }
+    }
+
+    @Test
+    fun scrollingALot_layoutIsNotRecomposed() {
+        val state = LazyStaggeredGridState()
+        var recomposed = 0
+        rule.setContent {
+            LazyStaggeredGrid(
+                lanes = 3,
+                state = state,
+                modifier = Modifier
+                    .mainAxisSize(itemSizeDp * 10)
+                    .composed {
+                        recomposed++
+                        Modifier
+                    }
+            ) {
+                items(1000) {
+                    Spacer(
+                        Modifier.mainAxisSize(itemSizeDp).testTag("$it")
+                    )
+                }
+            }
+        }
+
+        rule.waitForIdle()
+        assertThat(recomposed).isEqualTo(1)
+
+        state.scrollBy(1000.dp)
+
+        rule.waitForIdle()
+        assertThat(recomposed).isEqualTo(1)
+    }
+
+    @Test
+    fun onlyOneInitialMeasurePass() {
+        val state = LazyStaggeredGridState()
+        rule.setContent {
+            LazyStaggeredGrid(
+                lanes = 3,
+                state = state,
+                modifier = Modifier
+                    .mainAxisSize(itemSizeDp * 10)
+                    .composed {
+                        Modifier
+                    }
+            ) {
+                items(1000) {
+                    Spacer(
+                        Modifier.mainAxisSize(itemSizeDp).testTag("$it")
+                    )
+                }
+            }
+        }
+
+        rule.waitForIdle()
+        assertThat(state.measurePassCount).isEqualTo(1)
+    }
+}
\ No newline at end of file
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/relocation/BringIntoViewRequesterModifierTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/relocation/BringIntoViewScrollableInteractionTest.kt
similarity index 64%
rename from compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/relocation/BringIntoViewRequesterModifierTest.kt
rename to compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/relocation/BringIntoViewScrollableInteractionTest.kt
index 5a13ce4..0adee6c 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/relocation/BringIntoViewRequesterModifierTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/relocation/BringIntoViewScrollableInteractionTest.kt
@@ -16,11 +16,9 @@
 
 package androidx.compose.foundation.relocation
 
-import android.os.Build.VERSION_CODES.O
-import androidx.annotation.RequiresApi
-import androidx.compose.foundation.Canvas
 import androidx.compose.foundation.ExperimentalFoundationApi
 import androidx.compose.foundation.ScrollState
+import androidx.compose.foundation.background
 import androidx.compose.foundation.gestures.Orientation
 import androidx.compose.foundation.gestures.Orientation.Horizontal
 import androidx.compose.foundation.gestures.Orientation.Vertical
@@ -33,50 +31,50 @@
 import androidx.compose.foundation.rememberScrollState
 import androidx.compose.foundation.verticalScroll
 import androidx.compose.runtime.Composable
-import androidx.compose.testutils.assertAgainstGolden
-import androidx.compose.foundation.GOLDEN_UI
+import androidx.compose.runtime.rememberCoroutineScope
 import androidx.compose.ui.Modifier
-import androidx.compose.foundation.background
-import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.geometry.Rect
-import androidx.compose.ui.geometry.Size
 import androidx.compose.ui.graphics.Color.Companion.Blue
 import androidx.compose.ui.graphics.Color.Companion.Green
 import androidx.compose.ui.graphics.Color.Companion.LightGray
 import androidx.compose.ui.graphics.Color.Companion.Red
 import androidx.compose.ui.platform.LocalDensity
 import androidx.compose.ui.platform.testTag
-import androidx.compose.ui.test.TestMonotonicFrameClock
-import androidx.compose.ui.test.captureToImage
+import androidx.compose.ui.test.assertPositionInRootIsEqualTo
 import androidx.compose.ui.test.junit4.createComposeRule
 import androidx.compose.ui.test.onNodeWithTag
 import androidx.compose.ui.unit.Density
-import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.DpRect
+import androidx.compose.ui.unit.IntSize
+import androidx.compose.ui.unit.toSize
 import androidx.test.filters.MediumTest
-import androidx.test.filters.SdkSuppress
-import androidx.test.screenshot.AndroidXScreenshotTestRule
+import com.google.common.truth.Truth.assertThat
+import kotlin.math.roundToInt
 import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.test.advanceUntilIdle
-import kotlinx.coroutines.test.runTest
-import kotlinx.coroutines.withContext
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.runBlocking
 import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.junit.runners.Parameterized
 
-@OptIn(ExperimentalCoroutinesApi::class, ExperimentalFoundationApi::class)
+@OptIn(ExperimentalFoundationApi::class)
 @MediumTest
 @RunWith(Parameterized::class)
-@SdkSuppress(minSdkVersion = O)
-class BringIntoViewRequesterModifierTest(private val orientation: Orientation) {
+class BringIntoViewScrollableInteractionTest(private val orientation: Orientation) {
+
     @get:Rule
     val rule = createComposeRule()
 
-    @get:Rule
-    val screenshotRule = AndroidXScreenshotTestRule(GOLDEN_UI)
-
     private val parentBox = "parent box"
+    private val childBox = "child box"
+
+    /**
+     * Captures a scope from inside the composition for [runBlockingAndAwaitIdle].
+     * Make sure to call [setContentAndInitialize] instead of calling `rule.setContent` to initialize this.
+     */
+    private lateinit var testScope: CoroutineScope
 
     companion object {
         @JvmStatic
@@ -88,13 +86,13 @@
     fun noScrollableParent_noChange() {
         // Arrange.
         val bringIntoViewRequester = BringIntoViewRequester()
-        rule.setContent {
+        setContentAndInitialize {
             Box(
                 Modifier
                     .then(
                         when (orientation) {
-                            Horizontal -> Modifier.size(100.dp, 50.dp)
-                            Vertical -> Modifier.size(50.dp, 100.dp)
+                            Horizontal -> Modifier.size(100.toDp(), 50.toDp())
+                            Vertical -> Modifier.size(50.toDp(), 100.toDp())
                         }
                     )
                     .testTag(parentBox)
@@ -102,31 +100,34 @@
             ) {
                 Box(
                     Modifier
-                        .size(50.dp)
+                        .size(50.toDp())
                         .background(Blue)
                         .bringIntoViewRequester(bringIntoViewRequester)
+                        .testTag(childBox)
                 )
             }
         }
+        val startingBounds = getUnclippedBoundsInRoot(childBox)
 
         // Act.
         runBlockingAndAwaitIdle { bringIntoViewRequester.bringIntoView() }
 
         // Assert.
-        assertScreenshot(if (horizontal) "blueBoxLeft" else "blueBoxTop")
+        assertThat(getUnclippedBoundsInRoot(childBox)).isEqualTo(startingBounds)
+        assertChildMaxInView()
     }
 
     @Test
     fun noScrollableParent_itemNotVisible_noChange() {
         // Arrange.
         val bringIntoViewRequester = BringIntoViewRequester()
-        rule.setContent {
+        setContentAndInitialize {
             Box(
                 Modifier
                     .then(
                         when (orientation) {
-                            Horizontal -> Modifier.size(100.dp, 50.dp)
-                            Vertical -> Modifier.size(50.dp, 100.dp)
+                            Horizontal -> Modifier.size(100.toDp(), 50.toDp())
+                            Vertical -> Modifier.size(50.toDp(), 100.toDp())
                         }
                     )
                     .testTag(parentBox)
@@ -136,29 +137,32 @@
                     Modifier
                         .then(
                             when (orientation) {
-                                Horizontal -> Modifier.offset(x = 150.dp)
-                                Vertical -> Modifier.offset(y = 150.dp)
+                                Horizontal -> Modifier.offset(x = 150.toDp())
+                                Vertical -> Modifier.offset(y = 150.toDp())
                             }
                         )
-                        .size(50.dp)
+                        .size(50.toDp())
                         .background(Blue)
                         .bringIntoViewRequester(bringIntoViewRequester)
+                        .testTag(childBox)
                 )
             }
         }
+        val startingBounds = getUnclippedBoundsInRoot(childBox)
 
         // Act.
         runBlockingAndAwaitIdle { bringIntoViewRequester.bringIntoView() }
 
         // Assert.
-        assertScreenshot(if (horizontal) "grayRectangleHorizontal" else "grayRectangleVertical")
+        assertThat(getUnclippedBoundsInRoot(childBox)).isEqualTo(startingBounds)
+        assertChildMaxInView()
     }
 
     @Test
     fun itemAtLeadingEdge_alreadyVisible_noChange() {
         // Arrange.
         val bringIntoViewRequester = BringIntoViewRequester()
-        rule.setContent {
+        setContentAndInitialize {
             Box(
                 Modifier
                     .testTag(parentBox)
@@ -167,36 +171,39 @@
                         when (orientation) {
                             Horizontal ->
                                 Modifier
-                                    .size(100.dp, 50.dp)
+                                    .size(100.toDp(), 50.toDp())
                                     .horizontalScroll(rememberScrollState())
                             Vertical ->
                                 Modifier
-                                    .size(50.dp, 100.dp)
+                                    .size(50.toDp(), 100.toDp())
                                     .verticalScroll(rememberScrollState())
                         }
                     )
             ) {
                 Box(
                     Modifier
-                        .size(50.dp)
+                        .size(50.toDp())
                         .background(Blue)
                         .bringIntoViewRequester(bringIntoViewRequester)
+                        .testTag(childBox)
                 )
             }
         }
+        val startingBounds = getUnclippedBoundsInRoot(childBox)
 
         // Act.
         runBlockingAndAwaitIdle { bringIntoViewRequester.bringIntoView() }
 
         // Assert.
-        assertScreenshot(if (horizontal) "blueBoxLeft" else "blueBoxTop")
+        assertThat(getUnclippedBoundsInRoot(childBox)).isEqualTo(startingBounds)
+        assertChildMaxInView()
     }
 
     @Test
     fun itemAtTrailingEdge_alreadyVisible_noChange() {
         // Arrange.
         val bringIntoViewRequester = BringIntoViewRequester()
-        rule.setContent {
+        setContentAndInitialize {
             Box(
                 Modifier
                     .testTag(parentBox)
@@ -205,11 +212,11 @@
                         when (orientation) {
                             Horizontal ->
                                 Modifier
-                                    .size(100.dp, 50.dp)
+                                    .size(100.toDp(), 50.toDp())
                                     .horizontalScroll(rememberScrollState())
                             Vertical ->
                                 Modifier
-                                    .size(50.dp, 100.dp)
+                                    .size(50.toDp(), 100.toDp())
                                     .verticalScroll(rememberScrollState())
                         }
                     )
@@ -218,29 +225,32 @@
                     Modifier
                         .then(
                             when (orientation) {
-                                Horizontal -> Modifier.offset(x = 50.dp)
-                                Vertical -> Modifier.offset(y = 50.dp)
+                                Horizontal -> Modifier.offset(x = 50.toDp())
+                                Vertical -> Modifier.offset(y = 50.toDp())
                             }
                         )
-                        .size(50.dp)
+                        .size(50.toDp())
                         .background(Blue)
                         .bringIntoViewRequester(bringIntoViewRequester)
+                        .testTag(childBox)
                 )
             }
         }
+        val startingBounds = getUnclippedBoundsInRoot(childBox)
 
         // Act.
         runBlockingAndAwaitIdle { bringIntoViewRequester.bringIntoView() }
 
         // Assert.
-        assertScreenshot(if (horizontal) "blueBoxRight" else "blueBoxBottom")
+        assertThat(getUnclippedBoundsInRoot(childBox)).isEqualTo(startingBounds)
+        assertChildMaxInView()
     }
 
     @Test
     fun itemAtCenter_alreadyVisible_noChange() {
         // Arrange.
         val bringIntoViewRequester = BringIntoViewRequester()
-        rule.setContent {
+        setContentAndInitialize {
             Box(
                 Modifier
                     .testTag(parentBox)
@@ -249,11 +259,11 @@
                         when (orientation) {
                             Horizontal ->
                                 Modifier
-                                    .size(100.dp, 50.dp)
+                                    .size(100.toDp(), 50.toDp())
                                     .horizontalScroll(rememberScrollState())
                             Vertical ->
                                 Modifier
-                                    .size(50.dp, 100.dp)
+                                    .size(50.toDp(), 100.toDp())
                                     .verticalScroll(rememberScrollState())
                         }
                     )
@@ -262,32 +272,35 @@
                     Modifier
                         .then(
                             when (orientation) {
-                                Horizontal -> Modifier.offset(x = 25.dp)
-                                Vertical -> Modifier.offset(y = 25.dp)
+                                Horizontal -> Modifier.offset(x = 25.toDp())
+                                Vertical -> Modifier.offset(y = 25.toDp())
                             }
                         )
-                        .size(50.dp)
+                        .size(50.toDp())
                         .background(Blue)
                         .bringIntoViewRequester(bringIntoViewRequester)
+                        .testTag(childBox)
                 )
             }
         }
+        val startingBounds = getUnclippedBoundsInRoot(childBox)
 
         // Act.
         runBlockingAndAwaitIdle { bringIntoViewRequester.bringIntoView() }
 
         // Assert.
-        assertScreenshot(if (horizontal) "blueBoxCenterHorizontal" else "blueBoxCenterVertical")
+        assertThat(getUnclippedBoundsInRoot(childBox)).isEqualTo(startingBounds)
+        assertChildMaxInView()
     }
 
     @Test
     fun itemBiggerThanParentAtLeadingEdge_alreadyVisible_noChange() {
         // Arrange.
         val bringIntoViewRequester = BringIntoViewRequester()
-        rule.setContent {
+        setContentAndInitialize {
             Box(
                 Modifier
-                    .size(50.dp)
+                    .size(50.toDp())
                     .testTag(parentBox)
                     .background(LightGray)
                     .then(
@@ -299,19 +312,37 @@
             ) {
                 // Using a multi-colored item to make sure we can assert that the right part of
                 // the item is visible.
-                RowOrColumn(Modifier.bringIntoViewRequester(bringIntoViewRequester)) {
-                    Box(Modifier.size(50.dp).background(Blue))
-                    Box(Modifier.size(50.dp).background(Green))
-                    Box(Modifier.size(50.dp).background(Red))
+                RowOrColumn(
+                    Modifier
+                        .bringIntoViewRequester(bringIntoViewRequester)
+                        .testTag(childBox)
+                ) {
+                    Box(
+                        Modifier
+                            .size(50.toDp())
+                            .background(Blue)
+                    )
+                    Box(
+                        Modifier
+                            .size(50.toDp())
+                            .background(Green)
+                    )
+                    Box(
+                        Modifier
+                            .size(50.toDp())
+                            .background(Red)
+                    )
                 }
             }
         }
+        val startingBounds = getUnclippedBoundsInRoot(childBox)
 
         // Act.
         runBlockingAndAwaitIdle { bringIntoViewRequester.bringIntoView() }
 
         // Assert.
-        assertScreenshot("blueBox")
+        assertThat(getUnclippedBoundsInRoot(childBox)).isEqualTo(startingBounds)
+        assertChildMaxInView()
     }
 
     @Test
@@ -319,11 +350,11 @@
         // Arrange.
         val bringIntoViewRequester = BringIntoViewRequester()
         lateinit var scrollState: ScrollState
-        rule.setContent {
+        setContentAndInitialize {
             scrollState = rememberScrollState()
             Box(
                 Modifier
-                    .size(50.dp)
+                    .size(50.toDp())
                     .testTag(parentBox)
                     .background(LightGray)
                     .then(
@@ -335,20 +366,38 @@
             ) {
                 // Using a multi-colored item to make sure we can assert that the right part of
                 // the item is visible.
-                RowOrColumn(Modifier.bringIntoViewRequester(bringIntoViewRequester)) {
-                    Box(Modifier.size(50.dp).background(Red))
-                    Box(Modifier.size(50.dp).background(Green))
-                    Box(Modifier.size(50.dp).background(Blue))
+                RowOrColumn(
+                    Modifier
+                        .bringIntoViewRequester(bringIntoViewRequester)
+                        .testTag(childBox)
+                ) {
+                    Box(
+                        Modifier
+                            .size(50.toDp())
+                            .background(Red)
+                    )
+                    Box(
+                        Modifier
+                            .size(50.toDp())
+                            .background(Green)
+                    )
+                    Box(
+                        Modifier
+                            .size(50.toDp())
+                            .background(Blue)
+                    )
                 }
             }
         }
         runBlockingAndAwaitIdle { scrollState.scrollTo(scrollState.maxValue) }
+        val startingBounds = getUnclippedBoundsInRoot(childBox)
 
         // Act.
         runBlockingAndAwaitIdle { bringIntoViewRequester.bringIntoView() }
 
         // Assert.
-        assertScreenshot("blueBox")
+        assertThat(getUnclippedBoundsInRoot(childBox)).isEqualTo(startingBounds)
+        assertChildMaxInView()
     }
 
     @Test
@@ -356,11 +405,11 @@
         // Arrange.
         val bringIntoViewRequester = BringIntoViewRequester()
         lateinit var scrollState: ScrollState
-        rule.setContent {
+        setContentAndInitialize {
             scrollState = rememberScrollState()
             Box(
                 Modifier
-                    .size(50.dp)
+                    .size(50.toDp())
                     .testTag(parentBox)
                     .background(LightGray)
                     .then(
@@ -372,20 +421,38 @@
             ) {
                 // Using a multi-colored item to make sure we can assert that the right part of
                 // the item is visible.
-                RowOrColumn(Modifier.bringIntoViewRequester(bringIntoViewRequester)) {
-                    Box(Modifier.size(50.dp).background(Green))
-                    Box(Modifier.size(50.dp).background(Blue))
-                    Box(Modifier.size(50.dp).background(Red))
+                RowOrColumn(
+                    Modifier
+                        .bringIntoViewRequester(bringIntoViewRequester)
+                        .testTag(childBox)
+                ) {
+                    Box(
+                        Modifier
+                            .size(50.toDp())
+                            .background(Green)
+                    )
+                    Box(
+                        Modifier
+                            .size(50.toDp())
+                            .background(Blue)
+                    )
+                    Box(
+                        Modifier
+                            .size(50.toDp())
+                            .background(Red)
+                    )
                 }
             }
         }
         runBlockingAndAwaitIdle { scrollState.scrollTo(scrollState.maxValue / 2) }
+        val startingBounds = getUnclippedBoundsInRoot(childBox)
 
         // Act.
         runBlockingAndAwaitIdle { bringIntoViewRequester.bringIntoView() }
 
         // Assert.
-        assertScreenshot("blueBox")
+        assertThat(getUnclippedBoundsInRoot(childBox)).isEqualTo(startingBounds)
+        assertChildMaxInView()
     }
 
     @Test
@@ -393,7 +460,7 @@
         // Arrange.
         val bringIntoViewRequester = BringIntoViewRequester()
         lateinit var scrollState: ScrollState
-        rule.setContent {
+        setContentAndInitialize {
             scrollState = rememberScrollState()
             Box(
                 Modifier
@@ -403,32 +470,33 @@
                         when (orientation) {
                             Horizontal ->
                                 Modifier
-                                    .size(100.dp, 50.dp)
+                                    .size(100.toDp(), 50.toDp())
                                     .horizontalScroll(scrollState)
                             Vertical ->
                                 Modifier
-                                    .size(50.dp, 100.dp)
+                                    .size(50.toDp(), 100.toDp())
                                     .verticalScroll(scrollState)
                         }
                     )
             ) {
                 Box(
                     when (orientation) {
-                        Horizontal -> Modifier.size(200.dp, 50.dp)
-                        Vertical -> Modifier.size(50.dp, 200.dp)
+                        Horizontal -> Modifier.size(200.toDp(), 50.toDp())
+                        Vertical -> Modifier.size(50.toDp(), 200.toDp())
                     }
                 ) {
                     Box(
                         Modifier
                             .then(
                                 when (orientation) {
-                                    Horizontal -> Modifier.offset(x = 50.dp)
-                                    Vertical -> Modifier.offset(y = 50.dp)
+                                    Horizontal -> Modifier.offset(x = 50.toDp())
+                                    Vertical -> Modifier.offset(y = 50.toDp())
                                 }
                             )
-                            .size(50.dp)
+                            .size(50.toDp())
                             .background(Blue)
                             .bringIntoViewRequester(bringIntoViewRequester)
+                            .testTag(childBox)
                     )
                 }
             }
@@ -439,7 +507,8 @@
         runBlockingAndAwaitIdle { bringIntoViewRequester.bringIntoView() }
 
         // Assert.
-        assertScreenshot(if (horizontal) "blueBoxLeft" else "blueBoxTop")
+        rule.onNodeWithTag(childBox).assertPositionInRootIsEqualTo(0.toDp(), 0.toDp())
+        assertChildMaxInView()
     }
 
     @Test
@@ -447,7 +516,7 @@
         // Arrange.
         val bringIntoViewRequester = BringIntoViewRequester()
         lateinit var scrollState: ScrollState
-        rule.setContent {
+        setContentAndInitialize {
             scrollState = rememberScrollState()
             Box(
                 Modifier
@@ -457,32 +526,33 @@
                         when (orientation) {
                             Horizontal ->
                                 Modifier
-                                    .size(100.dp, 50.dp)
+                                    .size(100.toDp(), 50.toDp())
                                     .horizontalScroll(scrollState)
                             Vertical ->
                                 Modifier
-                                    .size(50.dp, 100.dp)
+                                    .size(50.toDp(), 100.toDp())
                                     .verticalScroll(scrollState)
                         }
                     )
             ) {
                 Box(
                     when (orientation) {
-                        Horizontal -> Modifier.size(200.dp, 50.dp)
-                        Vertical -> Modifier.size(50.dp, 200.dp)
+                        Horizontal -> Modifier.size(200.toDp(), 50.toDp())
+                        Vertical -> Modifier.size(50.toDp(), 200.toDp())
                     }
                 ) {
                     Box(
                         Modifier
                             .then(
                                 when (orientation) {
-                                    Horizontal -> Modifier.offset(x = 150.dp)
-                                    Vertical -> Modifier.offset(y = 150.dp)
+                                    Horizontal -> Modifier.offset(x = 150.toDp())
+                                    Vertical -> Modifier.offset(y = 150.toDp())
                                 }
                             )
-                            .size(50.dp)
+                            .size(50.toDp())
                             .background(Blue)
                             .bringIntoViewRequester(bringIntoViewRequester)
+                            .testTag(childBox)
                     )
                 }
             }
@@ -493,7 +563,11 @@
         runBlockingAndAwaitIdle { bringIntoViewRequester.bringIntoView() }
 
         // Assert.
-        assertScreenshot(if (horizontal) "blueBoxRight" else "blueBoxBottom")
+        rule.onNodeWithTag(childBox).assertPositionInRootIsEqualTo(
+            expectedLeft = if (orientation == Horizontal) 50.toDp() else 0.toDp(),
+            expectedTop = if (orientation == Horizontal) 0.toDp() else 50.toDp()
+        )
+        assertChildMaxInView()
     }
 
     @Test
@@ -501,7 +575,7 @@
         // Arrange.
         val bringIntoViewRequester = BringIntoViewRequester()
         lateinit var scrollState: ScrollState
-        rule.setContent {
+        setContentAndInitialize {
             scrollState = rememberScrollState()
             Box(
                 Modifier
@@ -511,27 +585,28 @@
                         when (orientation) {
                             Horizontal ->
                                 Modifier
-                                    .size(100.dp, 50.dp)
+                                    .size(100.toDp(), 50.toDp())
                                     .horizontalScroll(scrollState)
                             Vertical ->
                                 Modifier
-                                    .size(50.dp, 100.dp)
+                                    .size(50.toDp(), 100.toDp())
                                     .verticalScroll(scrollState)
                         }
                     )
             ) {
-                Box(Modifier.size(200.dp)) {
+                Box(Modifier.size(200.toDp())) {
                     Box(
                         Modifier
                             .then(
                                 when (orientation) {
-                                    Horizontal -> Modifier.offset(x = 25.dp)
-                                    Vertical -> Modifier.offset(y = 25.dp)
+                                    Horizontal -> Modifier.offset(x = 25.toDp())
+                                    Vertical -> Modifier.offset(y = 25.toDp())
                                 }
                             )
-                            .size(50.dp)
+                            .size(50.toDp())
                             .background(Blue)
                             .bringIntoViewRequester(bringIntoViewRequester)
+                            .testTag(childBox)
                     )
                 }
             }
@@ -542,7 +617,8 @@
         runBlockingAndAwaitIdle { bringIntoViewRequester.bringIntoView() }
 
         // Assert.
-        assertScreenshot(if (horizontal) "blueBoxLeft" else "blueBoxTop")
+        rule.onNodeWithTag(childBox).assertPositionInRootIsEqualTo(0.toDp(), 0.toDp())
+        assertChildMaxInView()
     }
 
     @Test
@@ -550,7 +626,7 @@
         // Arrange.
         val bringIntoViewRequester = BringIntoViewRequester()
         lateinit var scrollState: ScrollState
-        rule.setContent {
+        setContentAndInitialize {
             scrollState = rememberScrollState()
             Box(
                 Modifier
@@ -560,32 +636,33 @@
                         when (orientation) {
                             Horizontal ->
                                 Modifier
-                                    .size(100.dp, 50.dp)
+                                    .size(100.toDp(), 50.toDp())
                                     .horizontalScroll(scrollState)
                             Vertical ->
                                 Modifier
-                                    .size(50.dp, 100.dp)
+                                    .size(50.toDp(), 100.toDp())
                                     .verticalScroll(scrollState)
                         }
                     )
             ) {
                 Box(
                     when (orientation) {
-                        Horizontal -> Modifier.size(200.dp, 50.dp)
-                        Vertical -> Modifier.size(50.dp, 200.dp)
+                        Horizontal -> Modifier.size(200.toDp(), 50.toDp())
+                        Vertical -> Modifier.size(50.toDp(), 200.toDp())
                     }
                 ) {
                     Box(
                         Modifier
                             .then(
                                 when (orientation) {
-                                    Horizontal -> Modifier.offset(x = 150.dp)
-                                    Vertical -> Modifier.offset(y = 150.dp)
+                                    Horizontal -> Modifier.offset(x = 150.toDp())
+                                    Vertical -> Modifier.offset(y = 150.toDp())
                                 }
                             )
-                            .size(50.dp)
+                            .size(50.toDp())
                             .background(Blue)
                             .bringIntoViewRequester(bringIntoViewRequester)
+                            .testTag(childBox)
                     )
                 }
             }
@@ -596,7 +673,11 @@
         runBlockingAndAwaitIdle { bringIntoViewRequester.bringIntoView() }
 
         // Assert.
-        assertScreenshot(if (horizontal) "blueBoxRight" else "blueBoxBottom")
+        rule.onNodeWithTag(childBox).assertPositionInRootIsEqualTo(
+            expectedLeft = if (orientation == Horizontal) 50.toDp() else 0.toDp(),
+            expectedTop = if (orientation == Horizontal) 0.toDp() else 50.toDp()
+        )
+        assertChildMaxInView()
     }
 
     @Test
@@ -605,7 +686,7 @@
         val bringIntoViewRequester = BringIntoViewRequester()
         lateinit var parentScrollState: ScrollState
         lateinit var grandParentScrollState: ScrollState
-        rule.setContent {
+        setContentAndInitialize {
             parentScrollState = rememberScrollState()
             grandParentScrollState = rememberScrollState()
             Box(
@@ -616,11 +697,11 @@
                         when (orientation) {
                             Horizontal ->
                                 Modifier
-                                    .size(100.dp, 50.dp)
+                                    .size(100.toDp(), 50.toDp())
                                     .horizontalScroll(grandParentScrollState)
                             Vertical ->
                                 Modifier
-                                    .size(50.dp, 100.dp)
+                                    .size(50.toDp(), 100.toDp())
                                     .verticalScroll(grandParentScrollState)
                         }
                     )
@@ -632,32 +713,33 @@
                             when (orientation) {
                                 Horizontal ->
                                     Modifier
-                                        .size(200.dp, 50.dp)
+                                        .size(200.toDp(), 50.toDp())
                                         .horizontalScroll(parentScrollState)
                                 Vertical ->
                                     Modifier
-                                        .size(50.dp, 200.dp)
+                                        .size(50.toDp(), 200.toDp())
                                         .verticalScroll(parentScrollState)
                             }
                         )
                 ) {
                     Box(
                         when (orientation) {
-                            Horizontal -> Modifier.size(400.dp, 50.dp)
-                            Vertical -> Modifier.size(50.dp, 400.dp)
+                            Horizontal -> Modifier.size(400.toDp(), 50.toDp())
+                            Vertical -> Modifier.size(50.toDp(), 400.toDp())
                         }
                     ) {
                         Box(
                             Modifier
                                 .then(
                                     when (orientation) {
-                                        Horizontal -> Modifier.offset(x = 25.dp)
-                                        Vertical -> Modifier.offset(y = 25.dp)
+                                        Horizontal -> Modifier.offset(x = 25.toDp())
+                                        Vertical -> Modifier.offset(y = 25.toDp())
                                     }
                                 )
-                                .size(50.dp)
+                                .size(50.toDp())
                                 .background(Blue)
                                 .bringIntoViewRequester(bringIntoViewRequester)
+                                .testTag(childBox)
                         )
                     }
                 }
@@ -670,7 +752,8 @@
         runBlockingAndAwaitIdle { bringIntoViewRequester.bringIntoView() }
 
         // Assert.
-        assertScreenshot(if (horizontal) "blueBoxLeft" else "blueBoxTop")
+        rule.onNodeWithTag(childBox).assertPositionInRootIsEqualTo(0.toDp(), 0.toDp())
+        assertChildMaxInView()
     }
 
     @Test
@@ -679,7 +762,7 @@
         val bringIntoViewRequester = BringIntoViewRequester()
         lateinit var parentScrollState: ScrollState
         lateinit var grandParentScrollState: ScrollState
-        rule.setContent {
+        setContentAndInitialize {
             parentScrollState = rememberScrollState()
             grandParentScrollState = rememberScrollState()
             Box(
@@ -690,18 +773,18 @@
                         when (orientation) {
                             Horizontal ->
                                 Modifier
-                                    .size(100.dp, 50.dp)
+                                    .size(100.toDp(), 50.toDp())
                                     .verticalScroll(grandParentScrollState)
                             Vertical ->
                                 Modifier
-                                    .size(50.dp, 100.dp)
+                                    .size(50.toDp(), 100.toDp())
                                     .horizontalScroll(grandParentScrollState)
                         }
                     )
             ) {
                 Box(
                     Modifier
-                        .size(100.dp)
+                        .size(100.toDp())
                         .background(LightGray)
                         .then(
                             when (orientation) {
@@ -710,13 +793,14 @@
                             }
                         )
                 ) {
-                    Box(Modifier.size(200.dp)) {
+                    Box(Modifier.size(200.toDp())) {
                         Box(
                             Modifier
-                                .offset(x = 25.dp, y = 25.dp)
-                                .size(50.dp)
+                                .offset(x = 25.toDp(), y = 25.toDp())
+                                .size(50.toDp())
                                 .background(Blue)
                                 .bringIntoViewRequester(bringIntoViewRequester)
+                                .testTag(childBox)
                         )
                     }
                 }
@@ -729,7 +813,8 @@
         runBlockingAndAwaitIdle { bringIntoViewRequester.bringIntoView() }
 
         // Assert.
-        assertScreenshot(if (horizontal) "blueBoxLeft" else "blueBoxTop")
+        rule.onNodeWithTag(childBox).assertPositionInRootIsEqualTo(0.toDp(), 0.toDp())
+        assertChildMaxInView()
     }
 
     @Test
@@ -737,12 +822,12 @@
         // Arrange.
         val bringIntoViewRequester = BringIntoViewRequester()
         lateinit var density: Density
-        rule.setContent {
+        setContentAndInitialize {
             density = LocalDensity.current
             Box(
                 Modifier
                     .testTag(parentBox)
-                    .size(50.dp)
+                    .size(50.toDp())
                     .background(LightGray)
                     .then(
                         when (orientation) {
@@ -751,22 +836,28 @@
                         }
                     )
             ) {
-                Canvas(
-                    when (orientation) {
-                        Horizontal -> Modifier.size(150.dp, 50.dp)
-                        Vertical -> Modifier.size(50.dp, 150.dp)
-                    }.bringIntoViewRequester(bringIntoViewRequester)
-                ) {
-                    with(density) {
-                        drawRect(
-                            color = Blue,
-                            topLeft = when (orientation) {
-                                Horizontal -> Offset(50.dp.toPx(), 0.dp.toPx())
-                                Vertical -> Offset(0.dp.toPx(), 50.dp.toPx())
-                            },
-                            size = Size(50.dp.toPx(), 50.dp.toPx())
+                Box(
+                    Modifier
+                        .then(
+                            when (orientation) {
+                                Horizontal -> Modifier.size(150.toDp(), 50.toDp())
+                                Vertical -> Modifier.size(50.toDp(), 150.toDp())
+                            }
                         )
-                    }
+                        .bringIntoViewRequester(bringIntoViewRequester)
+                ) {
+                    Box(
+                        Modifier
+                            .size(50.toDp())
+                            .then(
+                                when (orientation) {
+                                    Horizontal -> Modifier.offset(50.toDp(), 0.toDp())
+                                    Vertical -> Modifier.offset(0.toDp(), 50.toDp())
+                                }
+                            )
+                            .background(Blue)
+                            .testTag(childBox)
+                    )
                 }
             }
         }
@@ -775,18 +866,40 @@
         runBlockingAndAwaitIdle {
             val rect = with(density) {
                 when (orientation) {
-                    Horizontal -> Rect(50.dp.toPx(), 0.dp.toPx(), 100.dp.toPx(), 50.dp.toPx())
-                    Vertical -> Rect(0.dp.toPx(), 50.dp.toPx(), 50.dp.toPx(), 100.dp.toPx())
+                    Horizontal -> DpRect(50.toDp(), 0.toDp(), 100.toDp(), 50.toDp()).toRect()
+                    Vertical -> DpRect(0.toDp(), 50.toDp(), 50.toDp(), 100.toDp()).toRect()
                 }
             }
             bringIntoViewRequester.bringIntoView(rect)
         }
 
         // Assert.
-        assertScreenshot("blueBox")
+        rule.onNodeWithTag(childBox).assertPositionInRootIsEqualTo(0.toDp(), 0.toDp())
+        assertChildMaxInView()
     }
 
-    private val horizontal: Boolean get() = (orientation == Horizontal)
+    private fun setContentAndInitialize(content: @Composable () -> Unit) {
+        rule.setContent {
+            testScope = rememberCoroutineScope()
+            content()
+        }
+    }
+
+    /**
+     * Sizes and offsets of the composables in these tests must be specified using this function.
+     * If they're specified using `xx.dp` syntax, a rounding error somewhere in the layout system
+     * will cause the pixel values to be off-by-one.
+     */
+    private fun Int.toDp(): Dp = with(rule.density) { this@toDp.toDp() }
+
+    /**
+     * Returns the bounds of the node with [tag], without performing any clipping by any parents.
+     */
+    @Suppress("SameParameterValue")
+    private fun getUnclippedBoundsInRoot(tag: String): Rect {
+        val node = rule.onNodeWithTag(tag).fetchSemanticsNode()
+        return Rect(node.positionInRoot, node.size.toSize())
+    }
 
     @Composable
     private fun RowOrColumn(
@@ -799,20 +912,32 @@
         }
     }
 
-    @RequiresApi(O)
-    private fun assertScreenshot(screenshot: String) {
-        rule.onNodeWithTag(parentBox)
-            .captureToImage()
-            .assertAgainstGolden(screenshotRule, "bringIntoParentBounds_$screenshot")
+    private fun runBlockingAndAwaitIdle(block: suspend CoroutineScope.() -> Unit) {
+        val job = testScope.launch(block = block)
+        rule.waitForIdle()
+        runBlocking {
+            job.join()
+        }
     }
 
-    private fun runBlockingAndAwaitIdle(block: suspend CoroutineScope.() -> Unit) {
-        runTest {
-            withContext(TestMonotonicFrameClock(this)) {
-                block()
-                advanceUntilIdle()
-            }
+    /**
+     * Asserts that as much of the child (identified by [childBox]) as can fit in the viewport
+     * (identified by [parentBox]) is visible. This is the min of the child size and the viewport
+     * size.
+     */
+    private fun assertChildMaxInView() {
+        val parentNode = rule.onNodeWithTag(parentBox).fetchSemanticsNode()
+        val childNode = rule.onNodeWithTag(childBox).fetchSemanticsNode()
+
+        // BoundsInRoot returns the clipped bounds.
+        val visibleBounds: IntSize = childNode.boundsInRoot.size.run {
+            IntSize(width.roundToInt(), height.roundToInt())
         }
-        rule.waitForIdle()
+        val expectedVisibleBounds = IntSize(
+            width = minOf(parentNode.size.width, childNode.size.width),
+            height = minOf(parentNode.size.height, childNode.size.height)
+        )
+
+        assertThat(visibleBounds).isEqualTo(expectedVisibleBounds)
     }
-}
+}
\ No newline at end of file
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGrid.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGrid.kt
new file mode 100644
index 0000000..eb8ebd4
--- /dev/null
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGrid.kt
@@ -0,0 +1,98 @@
+/*
+ * 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.clipScrollableContainer
+import androidx.compose.foundation.gestures.FlingBehavior
+import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.foundation.gestures.ScrollableDefaults
+import androidx.compose.foundation.gestures.scrollable
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.lazy.layout.LazyLayout
+import androidx.compose.foundation.overscroll
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalLayoutDirection
+import androidx.compose.ui.unit.Constraints
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.dp
+
+@ExperimentalFoundationApi
+@Composable
+internal fun LazyStaggeredGrid(
+    /** State controlling the scroll position */
+    state: LazyStaggeredGridState,
+    /** Modifier to be applied for the inner layout */
+    modifier: Modifier = Modifier,
+    /** The inner padding to be added for the whole content (not for each individual item) */
+    contentPadding: PaddingValues = PaddingValues(0.dp),
+    /** reverse the direction of scrolling and layout */
+    reverseLayout: Boolean = false,
+    /** The layout orientation of the grid */
+    orientation: Orientation,
+    /** fling behavior to be used for flinging */
+    flingBehavior: FlingBehavior = ScrollableDefaults.flingBehavior(),
+    /** Whether scrolling via the user gestures is allowed. */
+    userScrollEnabled: Boolean,
+    /** The vertical arrangement for items/lines. */
+    verticalArrangement: Arrangement.Vertical,
+    /** The horizontal arrangement for items/lines. */
+    horizontalArrangement: Arrangement.Horizontal,
+    /** Prefix sums of cross axis sizes of slots per line, e.g. the columns for vertical grid. */
+    slotSizesSums: Density.(Constraints) -> IntArray,
+    /** The content of the grid */
+    content: LazyStaggeredGridScope.() -> Unit
+) {
+    val overscrollEffect = ScrollableDefaults.overscrollEffect()
+
+    val itemProvider = rememberStaggeredGridItemProvider(state, content)
+    val measurePolicy = rememberStaggeredGridMeasurePolicy(
+        state,
+        itemProvider,
+        contentPadding,
+        reverseLayout,
+        orientation,
+        verticalArrangement,
+        horizontalArrangement,
+        slotSizesSums,
+        overscrollEffect
+    )
+
+    LazyLayout(
+        modifier = modifier
+            .then(state.remeasurementModifier)
+            .clipScrollableContainer(orientation)
+            .overscroll(overscrollEffect)
+            .scrollable(
+                orientation = orientation,
+                reverseDirection = ScrollableDefaults.reverseDirection(
+                    LocalLayoutDirection.current,
+                    orientation,
+                    reverseLayout
+                ),
+                flingBehavior = flingBehavior,
+                state = state,
+                overscrollEffect = overscrollEffect,
+                enabled = userScrollEnabled
+            ),
+        prefetchState = state.prefetchState,
+        itemProvider = itemProvider,
+        measurePolicy = measurePolicy
+    )
+}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridDsl.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridDsl.kt
new file mode 100644
index 0000000..d836948
--- /dev/null
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridDsl.kt
@@ -0,0 +1,53 @@
+/*
+ * 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.runtime.Composable
+
+@DslMarker
+internal annotation class LazyStaggeredGridScopeMarker
+
+@ExperimentalFoundationApi
+@LazyStaggeredGridScopeMarker
+internal sealed interface LazyStaggeredGridScope {
+    fun items(
+        count: Int,
+        key: ((index: Int) -> Any)? = null,
+        contentType: (index: Int) -> Any? = { null },
+        itemContent: @Composable LazyStaggeredGridItemScope.(index: Int) -> Unit
+    )
+}
+
+@ExperimentalFoundationApi
+internal sealed interface LazyStaggeredGridItemScope
+
+@ExperimentalFoundationApi
+internal fun LazyStaggeredGridScope.item(
+    key: Any? = null,
+    contentType: Any? = null,
+    itemContent: @Composable LazyStaggeredGridItemScope.(index: Int) -> Unit
+) {
+    items(
+        count = 1,
+        key = key?.let { { it } },
+        contentType = { contentType },
+        itemContent = itemContent
+    )
+}
+
+// todo(b/182882362): item DSL for lists/arrays
\ No newline at end of file
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
new file mode 100644
index 0000000..c2d02b2
--- /dev/null
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridItemProvider.kt
@@ -0,0 +1,52 @@
+/*
+ * 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.lazy.layout.DelegatingLazyLayoutItemProvider
+import androidx.compose.foundation.lazy.layout.LazyLayoutItemProvider
+import androidx.compose.foundation.lazy.layout.rememberLazyNearestItemsRangeState
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.derivedStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberUpdatedState
+
+@Composable
+@ExperimentalFoundationApi
+internal fun rememberStaggeredGridItemProvider(
+    state: LazyStaggeredGridState,
+    content: LazyStaggeredGridScope.() -> Unit,
+): LazyLayoutItemProvider {
+    val latestContent = rememberUpdatedState(content)
+    val nearestItemsRangeState = rememberLazyNearestItemsRangeState(
+        firstVisibleItemIndex = { state.firstVisibleItems.getOrNull(0) ?: 0 },
+        slidingWindowSize = { 90 },
+        extraItemCount = { 200 }
+    )
+    return remember(state) {
+        val itemProviderState = derivedStateOf {
+            val scope = LazyStaggeredGridScopeImpl().apply(latestContent.value)
+            LazyLayoutItemProvider(
+                scope.intervals,
+                nearestItemsRangeState.value,
+            ) { interval, index ->
+                interval.item.invoke(LazyStaggeredGridItemScopeImpl, index)
+            }
+        }
+        object : LazyLayoutItemProvider by DelegatingLazyLayoutItemProvider(itemProviderState) { }
+    }
+}
\ No newline at end of file
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
new file mode 100644
index 0000000..adc7424
--- /dev/null
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridMeasure.kt
@@ -0,0 +1,549 @@
+/*
+ * 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.OverscrollEffect
+import androidx.compose.foundation.checkScrollableContainerConstraints
+import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.lazy.layout.LazyLayoutItemProvider
+import androidx.compose.foundation.lazy.layout.LazyLayoutMeasureScope
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.snapshots.Snapshot
+import androidx.compose.ui.layout.Placeable
+import androidx.compose.ui.unit.Constraints
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.IntOffset
+import androidx.compose.ui.unit.IntSize
+import androidx.compose.ui.unit.constrainHeight
+import androidx.compose.ui.unit.constrainWidth
+import androidx.compose.ui.util.fastForEach
+import kotlin.math.abs
+import kotlin.math.roundToInt
+import kotlin.math.sign
+
+@Composable
+@ExperimentalFoundationApi
+internal fun rememberStaggeredGridMeasurePolicy(
+    state: LazyStaggeredGridState,
+    itemProvider: LazyLayoutItemProvider,
+    contentPadding: PaddingValues,
+    reverseLayout: Boolean,
+    orientation: Orientation,
+    verticalArrangement: Arrangement.Vertical,
+    horizontalArrangement: Arrangement.Horizontal,
+    slotSizesSums: Density.(Constraints) -> IntArray,
+    overscrollEffect: OverscrollEffect
+): LazyLayoutMeasureScope.(Constraints) -> LazyStaggeredGridMeasureResult = remember(
+    state,
+    itemProvider,
+    contentPadding,
+    reverseLayout,
+    orientation,
+    verticalArrangement,
+    horizontalArrangement,
+    slotSizesSums,
+    overscrollEffect,
+) {
+    { constraints ->
+        checkScrollableContainerConstraints(
+            constraints,
+            orientation
+        )
+        val isVertical = orientation == Orientation.Vertical
+
+        val resolvedSlotSums = slotSizesSums(this, constraints)
+        val itemCount = itemProvider.itemCount
+
+        val mainAxisAvailableSize =
+            if (isVertical) constraints.maxHeight else constraints.maxWidth
+
+        val measuredItemProvider = LazyStaggeredGridMeasureProvider(
+            isVertical,
+            itemProvider,
+            this,
+            resolvedSlotSums
+        ) { index, key, placeables ->
+            LazyStaggeredGridMeasuredItem(
+                index,
+                key,
+                placeables,
+                isVertical
+            )
+        }
+
+        val beforeContentPadding = 0
+        val afterContentPadding = 0
+
+        val initialItemIndices: IntArray
+        val initialItemOffsets: IntArray
+
+        Snapshot.withoutReadObservation {
+            initialItemIndices =
+                if (state.firstVisibleItems.size == resolvedSlotSums.size) {
+                    state.firstVisibleItems
+                } else {
+                    IntArray(resolvedSlotSums.size) { -1 }
+                }
+            initialItemOffsets =
+                if (state.firstVisibleItemScrollOffsets.size == resolvedSlotSums.size) {
+                    state.firstVisibleItemScrollOffsets
+                } else {
+                    IntArray(resolvedSlotSums.size) { 0 }
+                }
+        }
+
+        val spans = state.spans
+        val firstItemIndices = initialItemIndices.copyOf()
+        val firstItemOffsets = initialItemOffsets.copyOf()
+
+        // Measure items
+
+        if (itemCount <= 0) {
+            LazyStaggeredGridMeasureResult(
+                firstVisibleItemIndices = IntArray(0),
+                firstVisibleItemScrollOffsets = IntArray(0),
+                consumedScroll = 0f,
+                measureResult = layout(constraints.minWidth, constraints.minHeight) {},
+                canScrollForward = false,
+                canScrollBackward = false,
+                visibleItemsInfo = emptyArray()
+            )
+        } else {
+            // todo(b/182882362): content padding
+
+            // represents the real amount of scroll we applied as a result of this measure pass.
+            var scrollDelta = state.scrollToBeConsumed.roundToInt()
+
+            // applying the whole requested scroll offset. we will figure out if we can't consume
+            // all of it later
+            firstItemOffsets.offsetBy(-scrollDelta)
+
+            // if the current scroll offset is less than minimally possible
+            if (firstItemIndices[0] == 0 && firstItemOffsets[0] < 0) {
+                scrollDelta += firstItemOffsets[0]
+                firstItemOffsets.fill(0)
+            }
+
+            // this will contain all the MeasuredItems representing the visible items
+            val measuredItems = Array(resolvedSlotSums.size) {
+                mutableListOf<LazyStaggeredGridMeasuredItem>()
+            }
+
+            // include the start padding so we compose items in the padding area. before starting
+            // scrolling forward we would remove it back
+            firstItemOffsets.offsetBy(-beforeContentPadding)
+
+            // define min and max offsets (min offset currently includes beforeContentPadding)
+            val minOffset = -beforeContentPadding
+            val maxOffset = mainAxisAvailableSize
+
+            fun hasSpaceOnTop(): Boolean {
+                for (column in firstItemIndices.indices) {
+                    val itemIndex = firstItemIndices[column]
+                    val itemOffset = firstItemOffsets[column]
+
+                    if (itemOffset <= 0 && itemIndex > 0) {
+                        return true
+                    }
+                }
+
+                return false
+            }
+
+            // we had scrolled backward or we compose items in the start padding area, which means
+            // items before current firstItemScrollOffset should be visible. compose them and update
+            // firstItemScrollOffset
+            while (hasSpaceOnTop()) {
+                val columnIndex = firstItemOffsets.indexOfMinValue()
+                val previousItemIndex = spans.findPreviousItemIndex(
+                    item = firstItemIndices[columnIndex],
+                    column = columnIndex
+                )
+
+                if (previousItemIndex < 0) {
+                    break
+                }
+
+                if (spans.getSpan(previousItemIndex) == SpanLookup.SpanUnset) {
+                    spans.setSpan(previousItemIndex, columnIndex)
+                }
+
+                val measuredItem = measuredItemProvider.getAndMeasure(
+                    previousItemIndex,
+                    columnIndex
+                )
+                measuredItems[columnIndex].add(0, measuredItem)
+
+                firstItemIndices[columnIndex] = previousItemIndex
+                firstItemOffsets[columnIndex] += measuredItem.sizeWithSpacings
+            }
+
+            // if we were scrolled backward, but there were not enough items before. this means
+            // not the whole scroll was consumed
+            if (firstItemOffsets[0] < minOffset) {
+                scrollDelta += firstItemOffsets[0]
+                firstItemOffsets.offsetBy(minOffset - firstItemOffsets[0])
+            }
+
+            val currentItemIndices = initialItemIndices.copyOf()
+            val currentItemOffsets = IntArray(initialItemOffsets.size) {
+                -(initialItemOffsets[it] - scrollDelta)
+            }
+
+            // neutralize previously added start padding as we stopped filling the before content padding
+            firstItemOffsets.offsetBy(beforeContentPadding)
+
+            val maxMainAxis = (maxOffset + afterContentPadding).coerceAtLeast(0)
+
+            // compose first visible items we received from state
+            currentItemIndices.forEachIndexed { columnIndex, itemIndex ->
+                if (itemIndex == -1) return@forEachIndexed
+
+                val measuredItem = measuredItemProvider.getAndMeasure(itemIndex, columnIndex)
+                currentItemOffsets[columnIndex] += measuredItem.sizeWithSpacings
+
+                if (
+                    currentItemOffsets[columnIndex] <= minOffset &&
+                        measuredItem.index != itemCount - 1
+                ) {
+                    // this item is offscreen and will not be placed. advance item index
+                    firstItemIndices[columnIndex] = -1
+                    firstItemOffsets[columnIndex] -= measuredItem.sizeWithSpacings
+                } else {
+                    measuredItems[columnIndex].add(measuredItem)
+                }
+            }
+
+            // then composing visible items forward until we fill the whole viewport.
+            // we want to have at least one item in visibleItems even if in fact all the items are
+            // offscreen, this can happen if the content padding is larger than the available size.
+            while (
+                currentItemOffsets.any { it <= maxMainAxis } ||
+                    measuredItems.all { it.isEmpty() }
+            ) {
+                val columnIndex = currentItemOffsets.indexOfMinValue()
+                val nextItemIndex = spans.findNextItemIndex(
+                    currentItemIndices[columnIndex],
+                    columnIndex
+                )
+
+                if (nextItemIndex == itemCount) {
+                    break
+                }
+
+                if (firstItemIndices[columnIndex] == -1) {
+                    firstItemIndices[columnIndex] = nextItemIndex
+                }
+                spans.setSpan(nextItemIndex, columnIndex)
+
+                val measuredItem = measuredItemProvider.getAndMeasure(nextItemIndex, columnIndex)
+                currentItemOffsets[columnIndex] += measuredItem.sizeWithSpacings
+
+                if (
+                    currentItemOffsets[columnIndex] <= minOffset &&
+                        measuredItem.index != itemCount - 1
+                ) {
+                    // this item is offscreen and will not be placed. advance item index
+                    firstItemIndices[columnIndex] = -1
+                    firstItemOffsets[columnIndex] -= measuredItem.sizeWithSpacings
+                } else {
+                    measuredItems[columnIndex].add(measuredItem)
+                }
+
+                currentItemIndices[columnIndex] = nextItemIndex
+            }
+
+            // we didn't fill the whole viewport with items starting from firstVisibleItemIndex.
+            // lets try to scroll back if we have enough items before firstVisibleItemIndex.
+            if (currentItemOffsets.all { it < maxOffset }) {
+                val maxOffsetColumn = currentItemOffsets.indexOfMaxValue()
+                val toScrollBack = maxOffset - currentItemOffsets[maxOffsetColumn]
+                firstItemOffsets.offsetBy(-toScrollBack)
+                currentItemOffsets.offsetBy(toScrollBack)
+                while (
+                    firstItemOffsets.any { it < beforeContentPadding } &&
+                        firstItemIndices.all { it != 0 }
+                ) {
+                    val columnIndex = firstItemOffsets.indexOfMinValue()
+                    val currentIndex =
+                        if (firstItemIndices[columnIndex] == -1) {
+                            itemCount
+                        } else {
+                            firstItemIndices[columnIndex]
+                        }
+
+                    val previousIndex =
+                        spans.findPreviousItemIndex(currentIndex, columnIndex)
+
+                    if (previousIndex < 0) {
+                        break
+                    }
+
+                    val measuredItem = measuredItemProvider.getAndMeasure(
+                        previousIndex,
+                        columnIndex
+                    )
+                    measuredItems[columnIndex].add(0, measuredItem)
+                    firstItemOffsets[columnIndex] += measuredItem.sizeWithSpacings
+                    firstItemIndices[columnIndex] = previousIndex
+                }
+                scrollDelta += toScrollBack
+
+                val minOffsetColumn = firstItemOffsets.indexOfMinValue()
+                if (firstItemOffsets[minOffsetColumn] < 0) {
+                    val offsetValue = firstItemOffsets[minOffsetColumn]
+                    scrollDelta += offsetValue
+                    currentItemOffsets.offsetBy(offsetValue)
+                    firstItemOffsets.offsetBy(-offsetValue)
+                }
+            }
+
+            // report the amount of pixels we consumed. scrollDelta can be smaller than
+            // scrollToBeConsumed if there were not enough items to fill the offered space or it
+            // can be larger if items were resized, or if, for example, we were previously
+            // displaying the item 15, but now we have only 10 items in total in the data set.
+            val consumedScroll = if (
+                state.scrollToBeConsumed.roundToInt().sign == scrollDelta.sign &&
+                    abs(state.scrollToBeConsumed.roundToInt()) >= abs(scrollDelta)
+            ) {
+                scrollDelta.toFloat()
+            } else {
+                state.scrollToBeConsumed
+            }
+
+            // todo(b/182882362):
+            // even if we compose items to fill before content padding we should ignore items fully
+            // located there for the state's scroll position calculation (first item + first offset)
+
+            // end measure
+
+            val layoutWidth = if (isVertical) {
+                constraints.maxWidth
+            } else {
+                constraints.constrainWidth(currentItemOffsets.max())
+            }
+            val layoutHeight = if (isVertical) {
+                constraints.constrainHeight(currentItemOffsets.max())
+            } else {
+                constraints.maxHeight
+            }
+
+            // Placement
+
+            val itemScrollOffsets = firstItemOffsets.map { -it }
+            val positionedItems = Array(measuredItems.size) {
+                mutableListOf<LazyStaggeredGridPositionedItem>()
+            }
+
+            var currentCrossAxis = 0
+            measuredItems.forEachIndexed { i, columnItems ->
+                var currentMainAxis = itemScrollOffsets[i]
+
+                // todo(b/182882362): arrangement/spacing support
+
+                columnItems.fastForEach { item ->
+                    positionedItems[i] += item.position(
+                        currentMainAxis,
+                        currentCrossAxis,
+                    )
+                    currentMainAxis += item.sizeWithSpacings
+                }
+                if (columnItems.isNotEmpty()) {
+                    currentCrossAxis += columnItems[0].crossAxisSize
+                }
+            }
+
+            // End placement
+
+            // todo: reverse layout support
+            // 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
+            val canScrollForward = currentItemIndices.indexOf(itemCount - 1).let { columnIndex ->
+                if (columnIndex == -1) {
+                    true
+                } else {
+                    (currentItemOffsets[columnIndex] -
+                        measuredItems[columnIndex].last().sizeWithSpacings) < mainAxisAvailableSize
+                }
+            }
+
+            @Suppress("UNCHECKED_CAST")
+            LazyStaggeredGridMeasureResult(
+                firstVisibleItemIndices = firstItemIndices,
+                firstVisibleItemScrollOffsets = firstItemOffsets,
+                consumedScroll = consumedScroll,
+                measureResult = layout(layoutWidth, layoutHeight) {
+                    positionedItems.forEach {
+                        it.fastForEach { item ->
+                            item.place(this)
+                        }
+                    }
+                },
+                canScrollForward = canScrollForward,
+                canScrollBackward = canScrollBackward,
+                visibleItemsInfo = positionedItems as Array<List<LazyStaggeredGridItemInfo>>
+            ).also {
+                state.applyMeasureResult(it)
+                refreshOverscrollInfo(overscrollEffect, it)
+            }
+        }
+    }
+}
+
+@OptIn(ExperimentalFoundationApi::class)
+private fun refreshOverscrollInfo(
+    overscrollEffect: OverscrollEffect,
+    result: LazyStaggeredGridMeasureResult
+) {
+    overscrollEffect.isEnabled = result.canScrollForward || result.canScrollBackward
+}
+
+private fun IntArray.offsetBy(delta: Int) {
+    for (i in indices) {
+        this[i] = this[i] + delta
+    }
+}
+
+private fun IntArray.indexOfMinValue(): Int {
+    var result = -1
+    var min = Int.MAX_VALUE
+    for (i in indices) {
+        if (min > this[i]) {
+            min = this[i]
+            result = i
+        }
+    }
+
+    return result
+}
+
+private fun IntArray.indexOfMaxValue(): Int {
+    var result = -1
+    var max = Int.MIN_VALUE
+    for (i in indices) {
+        if (max < this[i]) {
+            max = this[i]
+            result = i
+        }
+    }
+
+    return result
+}
+
+private fun SpanLookup.findPreviousItemIndex(item: Int, column: Int): Int {
+    for (i in (item - 1) downTo 0) {
+        val span = getSpan(i)
+        if (span == column || span == SpanLookup.SpanUnset) {
+            return i
+        }
+    }
+    return -1
+}
+
+private fun SpanLookup.findNextItemIndex(item: Int, column: Int): Int {
+    for (i in (item + 1) until capacity()) {
+        val span = getSpan(i)
+        if (span == column || span == SpanLookup.SpanUnset) {
+            return i
+        }
+    }
+    return capacity()
+}
+
+@OptIn(ExperimentalFoundationApi::class)
+private class LazyStaggeredGridMeasureProvider(
+    private val isVertical: Boolean,
+    private val itemProvider: LazyLayoutItemProvider,
+    private val measureScope: LazyLayoutMeasureScope,
+    private val resolvedSlotSums: IntArray,
+    private val measuredItemFactory: MeasuredItemFactory
+) {
+    fun childConstraints(slot: Int): Constraints {
+        val previousSum = if (slot == 0) 0 else resolvedSlotSums[slot - 1]
+        val crossAxisSize = resolvedSlotSums[slot] - previousSum
+        return if (isVertical) {
+            Constraints.fixedWidth(crossAxisSize)
+        } else {
+            Constraints.fixedHeight(crossAxisSize)
+        }
+    }
+
+    fun getAndMeasure(index: Int, slot: Int): LazyStaggeredGridMeasuredItem {
+        val key = itemProvider.getKey(index)
+        val placeables = measureScope.measure(index, childConstraints(slot))
+        return measuredItemFactory.createItem(index, key, placeables)
+    }
+}
+
+private class LazyStaggeredGridMeasuredItem(
+    val index: Int,
+    val key: Any,
+    val placeables: Array<Placeable>,
+    val isVertical: Boolean
+) {
+    val sizeWithSpacings: Int = placeables.fold(0) { size, placeable ->
+        size + if (isVertical) placeable.height else placeable.width
+    }
+
+    val crossAxisSize: Int = placeables.maxOf {
+        if (isVertical) it.width else it.height
+    }
+
+    fun position(
+        mainAxis: Int,
+        crossAxis: Int,
+    ): LazyStaggeredGridPositionedItem =
+        LazyStaggeredGridPositionedItem(
+            offset = if (isVertical) {
+                IntOffset(crossAxis, mainAxis)
+            } else {
+                IntOffset(mainAxis, crossAxis)
+            },
+            index = index,
+            key = key,
+            size = IntSize(sizeWithSpacings, crossAxisSize),
+            placeables = placeables
+        )
+}
+
+private class LazyStaggeredGridPositionedItem(
+    override val offset: IntOffset,
+    override val index: Int,
+    override val key: Any,
+    override val size: IntSize,
+    val placeables: Array<Placeable>
+) : LazyStaggeredGridItemInfo {
+    fun place(scope: Placeable.PlacementScope) = with(scope) {
+        placeables.forEach { placeable ->
+            placeable.placeWithLayer(offset)
+        }
+    }
+}
+
+// This interface allows to avoid autoboxing on index param
+private fun interface MeasuredItemFactory {
+    fun createItem(
+        index: Int,
+        key: Any,
+        placeables: Array<Placeable>
+    ): LazyStaggeredGridMeasuredItem
+}
\ No newline at end of file
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridMeasureResult.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridMeasureResult.kt
new file mode 100644
index 0000000..4abccf9
--- /dev/null
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridMeasureResult.kt
@@ -0,0 +1,38 @@
+/*
+ * 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.ui.layout.MeasureResult
+import androidx.compose.ui.unit.IntOffset
+import androidx.compose.ui.unit.IntSize
+
+internal class LazyStaggeredGridMeasureResult(
+    val firstVisibleItemIndices: IntArray,
+    val firstVisibleItemScrollOffsets: IntArray,
+    val consumedScroll: Float,
+    val measureResult: MeasureResult,
+    val canScrollForward: Boolean,
+    val canScrollBackward: Boolean,
+    val visibleItemsInfo: Array<List<LazyStaggeredGridItemInfo>>
+) : MeasureResult by measureResult
+
+internal interface LazyStaggeredGridItemInfo {
+    val offset: IntOffset
+    val index: Int
+    val key: Any
+    val size: IntSize
+}
\ No newline at end of file
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridScope.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridScope.kt
new file mode 100644
index 0000000..3881306
--- /dev/null
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridScope.kt
@@ -0,0 +1,53 @@
+/*
+ * 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.lazy.layout.LazyLayoutIntervalContent
+import androidx.compose.foundation.lazy.layout.MutableIntervalList
+import androidx.compose.runtime.Composable
+
+@ExperimentalFoundationApi
+internal class LazyStaggeredGridScopeImpl : LazyStaggeredGridScope {
+    val intervals = MutableIntervalList<LazyStaggeredGridIntervalContent>()
+
+    override fun items(
+        count: Int,
+        key: ((index: Int) -> Any)?,
+        contentType: (index: Int) -> Any?,
+        itemContent: @Composable LazyStaggeredGridItemScope.(index: Int) -> Unit
+    ) {
+        intervals.addInterval(
+            count,
+            LazyStaggeredGridIntervalContent(
+                key,
+                contentType,
+                itemContent
+            )
+        )
+    }
+}
+
+@OptIn(ExperimentalFoundationApi::class)
+internal object LazyStaggeredGridItemScopeImpl : LazyStaggeredGridItemScope
+
+@OptIn(ExperimentalFoundationApi::class)
+internal class LazyStaggeredGridIntervalContent(
+    override val key: ((index: Int) -> Any)?,
+    override val type: ((index: Int) -> Any?),
+    val item: @Composable LazyStaggeredGridItemScope.(Int) -> Unit
+) : LazyLayoutIntervalContent
\ No newline at end of file
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
new file mode 100644
index 0000000..cef7880
--- /dev/null
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridState.kt
@@ -0,0 +1,150 @@
+/*
+ * 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.MutatePriority
+import androidx.compose.foundation.gestures.ScrollScope
+import androidx.compose.foundation.gestures.ScrollableState
+import androidx.compose.foundation.lazy.layout.LazyLayoutPrefetchState
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.layout.Remeasurement
+import androidx.compose.ui.layout.RemeasurementModifier
+import kotlin.math.abs
+
+@ExperimentalFoundationApi
+internal class LazyStaggeredGridState : ScrollableState {
+    var firstVisibleItems: IntArray by mutableStateOf(IntArray(0))
+        private set
+
+    var firstVisibleItemScrollOffsets: IntArray by mutableStateOf(IntArray(0))
+        private set
+
+    internal val spans: SpanLookup = SpanLookup()
+
+    private var canScrollForward = true
+    private var canScrollBackward = true
+
+    private var remeasurement: Remeasurement? = null
+
+    internal val remeasurementModifier = object : RemeasurementModifier {
+        override fun onRemeasurementAvailable(remeasurement: Remeasurement) {
+            this@LazyStaggeredGridState.remeasurement = remeasurement
+        }
+    }
+
+    internal val prefetchState: LazyLayoutPrefetchState = LazyLayoutPrefetchState()
+
+    internal var prefetchingEnabled = true
+
+    private val scrollableState = ScrollableState { -onScroll(-it) }
+
+    internal var scrollToBeConsumed = 0f
+        private set
+
+    /* @VisibleForTesting */
+    internal var measurePassCount = 0
+
+    override suspend fun scroll(
+        scrollPriority: MutatePriority,
+        block: suspend ScrollScope.() -> Unit
+    ) {
+        scrollableState.scroll(scrollPriority, block)
+    }
+
+    private fun onScroll(distance: Float): Float {
+        if (distance < 0 && !canScrollForward || distance > 0 && !canScrollBackward) {
+            return 0f
+        }
+        check(abs(scrollToBeConsumed) <= 0.5f) {
+            "entered drag with non-zero pending scroll: $scrollToBeConsumed"
+        }
+        scrollToBeConsumed += distance
+
+        // scrollToBeConsumed will be consumed synchronously during the forceRemeasure invocation
+        // inside measuring we do scrollToBeConsumed.roundToInt() so there will be no scroll if
+        // we have less than 0.5 pixels
+        if (abs(scrollToBeConsumed) > 0.5f) {
+            remeasurement?.forceRemeasure()
+            // todo(b/182882362): notify prefetch
+//            if (prefetchingEnabled) {
+//                val leftoverScroll = preScrollToBeConsumed - scrollToBeConsumed
+//                notifyPrefetch(preScrollToBeConsumed - scrollToBeConsumed)
+//            }
+        }
+
+        // here scrollToBeConsumed is already consumed during the forceRemeasure invocation
+        if (abs(scrollToBeConsumed) <= 0.5f) {
+            // We consumed all of it - we'll hold onto the fractional scroll for later, so report
+            // that we consumed the whole thing
+            return distance
+        } else {
+            val scrollConsumed = distance - scrollToBeConsumed
+            // We did not consume all of it - return the rest to be consumed elsewhere (e.g.,
+            // nested scrolling)
+            scrollToBeConsumed = 0f // We're not consuming the rest, give it back
+            return scrollConsumed
+        }
+    }
+
+    override fun dispatchRawDelta(delta: Float): Float =
+        scrollableState.dispatchRawDelta(delta)
+
+    internal fun applyMeasureResult(result: LazyStaggeredGridMeasureResult) {
+        scrollToBeConsumed -= result.consumedScroll
+        firstVisibleItems = result.firstVisibleItemIndices
+        firstVisibleItemScrollOffsets = result.firstVisibleItemScrollOffsets
+        canScrollBackward = result.canScrollBackward
+        canScrollForward = result.canScrollForward
+
+        measurePassCount++
+    }
+
+    override val isScrollInProgress: Boolean
+        get() = scrollableState.isScrollInProgress
+}
+
+internal class SpanLookup {
+    private var spans = IntArray(16)
+
+    fun setSpan(item: Int, span: Int) {
+        ensureCapacity(item + 1)
+        spans[item] = span + 1
+    }
+
+    fun getSpan(item: Int): Int =
+        spans[item] - 1
+
+    fun capacity(): Int =
+        spans.size
+
+    private fun ensureCapacity(capacity: Int) {
+        if (spans.size < capacity) {
+            spans = spans.copyInto(IntArray(spans.size * 2))
+        }
+    }
+
+    fun reset() {
+        spans.fill(0)
+    }
+
+    companion object {
+        internal const val SpanUnset = -1
+    }
+}
\ No newline at end of file
diff --git a/compose/material/material/src/androidMain/kotlin/androidx/compose/material/internal/ExposedDropdownMenuPopup.kt b/compose/material/material/src/androidMain/kotlin/androidx/compose/material/internal/ExposedDropdownMenuPopup.kt
index e6adabb..a8f5d7e4 100644
--- a/compose/material/material/src/androidMain/kotlin/androidx/compose/material/internal/ExposedDropdownMenuPopup.kt
+++ b/compose/material/material/src/androidMain/kotlin/androidx/compose/material/internal/ExposedDropdownMenuPopup.kt
@@ -234,7 +234,9 @@
     // Track parent bounds and content size; only show popup once we have both
     val canCalculatePosition by derivedStateOf { parentBounds != null && popupContentSize != null }
 
-    private val maxSupportedElevation = 30.dp
+    // On systems older than Android S, there is a bug in the surface insets matrix math used by
+    // elevation, so high values of maxSupportedElevation break accessibility services: b/232788477.
+    private val maxSupportedElevation = 8.dp
 
     // The window visible frame used for the last popup position calculation.
     private val previousWindowVisibleFrame = Rect()
diff --git a/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/internal/ExposedDropdownMenuPopup.kt b/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/internal/ExposedDropdownMenuPopup.kt
index 2c2d28c..f05f2bf 100644
--- a/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/internal/ExposedDropdownMenuPopup.kt
+++ b/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/internal/ExposedDropdownMenuPopup.kt
@@ -235,7 +235,9 @@
     // Track parent bounds and content size; only show popup once we have both
     val canCalculatePosition by derivedStateOf { parentBounds != null && popupContentSize != null }
 
-    private val maxSupportedElevation = 30.dp
+    // On systems older than Android S, there is a bug in the surface insets matrix math used by
+    // elevation, so high values of maxSupportedElevation break accessibility services: b/232788477.
+    private val maxSupportedElevation = 8.dp
 
     // The window visible frame used for the last popup position calculation.
     private val previousWindowVisibleFrame = Rect()
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/collection/IdentityArraySet.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/collection/IdentityArraySet.kt
index 6b31b4e..6bfd57c 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/collection/IdentityArraySet.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/collection/IdentityArraySet.kt
@@ -91,9 +91,7 @@
      * Remove all values from the set.
      */
     fun clear() {
-        for (i in 0 until size) {
-            values[i] = null
-        }
+        values.fill(null)
 
         size = 0
     }
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/snapshots/SnapshotStateObserver.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/snapshots/SnapshotStateObserver.kt
index 2e0396c..b7f31f9 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/snapshots/SnapshotStateObserver.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/snapshots/SnapshotStateObserver.kt
@@ -21,6 +21,7 @@
 import androidx.compose.runtime.TestOnly
 import androidx.compose.runtime.collection.IdentityArrayIntMap
 import androidx.compose.runtime.collection.IdentityArrayMap
+import androidx.compose.runtime.collection.IdentityArraySet
 import androidx.compose.runtime.collection.IdentityScopeMap
 import androidx.compose.runtime.collection.mutableVectorOf
 import androidx.compose.runtime.observeDerivedStateRecalculations
@@ -261,7 +262,7 @@
         /**
          * Scopes that were invalidated during previous apply step.
          */
-        private val invalidated = hashSetOf<Any>()
+        private val invalidated = IdentityArraySet<Any>()
 
         // derived state handling
 
@@ -417,7 +418,7 @@
                         // Invalidate only if currentValue is different than observed on read
                         if (!policy.equivalent(derivedState.currentValue, previousValue)) {
                             valueToScopes.forEachScopeOf(derivedState) { scope ->
-                                invalidated += scope
+                                invalidated.add(scope)
                                 hasValues = true
                             }
                         }
@@ -425,7 +426,7 @@
                 }
 
                 valueToScopes.forEachScopeOf(value) { scope ->
-                    invalidated += scope
+                    invalidated.add(scope)
                     hasValues = true
                 }
             }
@@ -436,7 +437,7 @@
          * Call [onChanged] for previously invalidated scopes.
          */
         fun notifyInvalidatedScopes() {
-            invalidated.forEach(onChanged)
+            invalidated.fastForEach(onChanged)
             invalidated.clear()
         }
     }
diff --git a/compose/ui/ui-text/api/current.txt b/compose/ui/ui-text/api/current.txt
index dbc0531..057beaf 100644
--- a/compose/ui/ui-text/api/current.txt
+++ b/compose/ui/ui-text/api/current.txt
@@ -640,9 +640,6 @@
   public final class AndroidFontUtils_androidKt {
   }
 
-  public final class AndroidLoadableFontsKt {
-  }
-
   public final class AndroidTypeface_androidKt {
     method public static androidx.compose.ui.text.font.FontFamily FontFamily(android.graphics.Typeface typeface);
     method @Deprecated public static androidx.compose.ui.text.font.Typeface Typeface(android.content.Context context, androidx.compose.ui.text.font.FontFamily fontFamily, optional java.util.List<kotlin.Pair<androidx.compose.ui.text.font.FontWeight,androidx.compose.ui.text.font.FontStyle>>? styles);
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 62e484e..cde0c73 100644
--- a/compose/ui/ui-text/api/public_plus_experimental_current.txt
+++ b/compose/ui/ui-text/api/public_plus_experimental_current.txt
@@ -677,6 +677,10 @@
   }
 
   public final class AndroidFontKt {
+    method @Deprecated @androidx.compose.runtime.Stable @androidx.compose.ui.text.ExperimentalTextApi public static androidx.compose.ui.text.font.Font Font(android.content.res.AssetManager assetManager, String path, optional androidx.compose.ui.text.font.FontWeight weight, optional int style);
+    method @androidx.compose.runtime.Stable @androidx.compose.ui.text.ExperimentalTextApi public static androidx.compose.ui.text.font.Font Font(String path, android.content.res.AssetManager assetManager, optional androidx.compose.ui.text.font.FontWeight weight, optional int style, optional androidx.compose.ui.text.font.FontVariation.Settings variationSettings);
+    method @androidx.compose.runtime.Stable @androidx.compose.ui.text.ExperimentalTextApi public static androidx.compose.ui.text.font.Font Font(java.io.File file, optional androidx.compose.ui.text.font.FontWeight weight, optional int style, optional androidx.compose.ui.text.font.FontVariation.Settings variationSettings);
+    method @RequiresApi(26) @androidx.compose.runtime.Stable @androidx.compose.ui.text.ExperimentalTextApi public static androidx.compose.ui.text.font.Font Font(android.os.ParcelFileDescriptor fileDescriptor, optional androidx.compose.ui.text.font.FontWeight weight, optional int style, optional androidx.compose.ui.text.font.FontVariation.Settings variationSettings);
   }
 
   public final class AndroidFontLoader_androidKt {
@@ -688,13 +692,6 @@
   public final class AndroidFontUtils_androidKt {
   }
 
-  public final class AndroidLoadableFontsKt {
-    method @Deprecated @androidx.compose.runtime.Stable @androidx.compose.ui.text.ExperimentalTextApi public static androidx.compose.ui.text.font.Font Font(android.content.res.AssetManager assetManager, String path, optional androidx.compose.ui.text.font.FontWeight weight, optional int style);
-    method @androidx.compose.runtime.Stable @androidx.compose.ui.text.ExperimentalTextApi public static androidx.compose.ui.text.font.Font Font(String path, android.content.res.AssetManager assetManager, optional androidx.compose.ui.text.font.FontWeight weight, optional int style, optional androidx.compose.ui.text.font.FontVariation.Settings variationSettings);
-    method @androidx.compose.runtime.Stable @androidx.compose.ui.text.ExperimentalTextApi public static androidx.compose.ui.text.font.Font Font(java.io.File file, optional androidx.compose.ui.text.font.FontWeight weight, optional int style, optional androidx.compose.ui.text.font.FontVariation.Settings variationSettings);
-    method @RequiresApi(26) @androidx.compose.runtime.Stable @androidx.compose.ui.text.ExperimentalTextApi public static androidx.compose.ui.text.font.Font Font(android.os.ParcelFileDescriptor fileDescriptor, optional androidx.compose.ui.text.font.FontWeight weight, optional int style, optional androidx.compose.ui.text.font.FontVariation.Settings variationSettings);
-  }
-
   public final class AndroidTypeface_androidKt {
     method public static androidx.compose.ui.text.font.FontFamily FontFamily(android.graphics.Typeface typeface);
     method @Deprecated public static androidx.compose.ui.text.font.Typeface Typeface(android.content.Context context, androidx.compose.ui.text.font.FontFamily fontFamily, optional java.util.List<kotlin.Pair<androidx.compose.ui.text.font.FontWeight,androidx.compose.ui.text.font.FontStyle>>? styles);
diff --git a/compose/ui/ui-text/api/restricted_current.txt b/compose/ui/ui-text/api/restricted_current.txt
index dbc0531..057beaf 100644
--- a/compose/ui/ui-text/api/restricted_current.txt
+++ b/compose/ui/ui-text/api/restricted_current.txt
@@ -640,9 +640,6 @@
   public final class AndroidFontUtils_androidKt {
   }
 
-  public final class AndroidLoadableFontsKt {
-  }
-
   public final class AndroidTypeface_androidKt {
     method public static androidx.compose.ui.text.font.FontFamily FontFamily(android.graphics.Typeface typeface);
     method @Deprecated public static androidx.compose.ui.text.font.Typeface Typeface(android.content.Context context, androidx.compose.ui.text.font.FontFamily fontFamily, optional java.util.List<kotlin.Pair<androidx.compose.ui.text.font.FontWeight,androidx.compose.ui.text.font.FontStyle>>? styles);
diff --git a/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/font/AndroidFont.kt b/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/font/AndroidFont.kt
index 4582e7b..524441d 100644
--- a/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/font/AndroidFont.kt
+++ b/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/font/AndroidFont.kt
@@ -17,8 +17,110 @@
 package androidx.compose.ui.text.font
 
 import android.content.Context
+import android.content.res.AssetManager
 import android.graphics.Typeface
+import android.os.ParcelFileDescriptor
+import androidx.annotation.RequiresApi
+import androidx.compose.runtime.Stable
 import androidx.compose.ui.text.ExperimentalTextApi
+import java.io.File
+
+/**
+ * Create a Font declaration from a file in the assets directory. The content of the [File] is
+ * read during construction.
+ *
+ * @param assetManager Android AssetManager
+ * @param path full path starting from the assets directory (i.e. dir/myfont.ttf for
+ * assets/dir/myfont.ttf).
+ * @param weight The weight of the font. The system uses this to match a font to a font request
+ * that is given in a [androidx.compose.ui.text.SpanStyle].
+ * @param style The style of the font, normal or italic. The system uses this to match a font to a
+ * font request that is given in a [androidx.compose.ui.text.SpanStyle].
+ */
+@ExperimentalTextApi
+@Stable
+@Deprecated("This experimental Font is replaced by Font(path, assetManager, ...)",
+    replaceWith = ReplaceWith("Font(path, assetManager, weight, style)"),
+    level = DeprecationLevel.WARNING
+)
+fun Font(
+    assetManager: AssetManager,
+    path: String,
+    weight: FontWeight = FontWeight.Normal,
+    style: FontStyle = FontStyle.Normal
+): Font = AndroidAssetFont(
+    assetManager,
+    path,
+    weight,
+    style,
+    FontVariation.Settings(weight, style)
+)
+
+/**
+ * Create a Font declaration from a file in the assets directory. The content of the [File] is
+ * read during construction.
+ *
+ * @param path full path starting from the assets directory (i.e. dir/myfont.ttf for
+ * assets/dir/myfont.ttf).
+ * @param assetManager Android AssetManager
+ * @param weight The weight of the font. The system uses this to match a font to a font request
+ * that is given in a [androidx.compose.ui.text.SpanStyle].
+ * @param style The style of the font, normal or italic. The system uses this to match a font to a
+ * font request that is given in a [androidx.compose.ui.text.SpanStyle].
+ * @param variationSettings on API 26 and above these settings are applied to a variable font when
+ * the font is loaded
+ */
+@ExperimentalTextApi
+@Stable
+fun Font(
+    path: String,
+    assetManager: AssetManager,
+    weight: FontWeight = FontWeight.Normal,
+    style: FontStyle = FontStyle.Normal,
+    variationSettings: FontVariation.Settings = FontVariation.Settings(weight, style)
+): Font = AndroidAssetFont(assetManager, path, weight, style, variationSettings)
+
+/**
+ * Create a Font declaration from a file. The content of the [File] is read during construction.
+ *
+ * @param file the font file.
+ * @param weight The weight of the font. The system uses this to match a font to a font request
+ * that is given in a [androidx.compose.ui.text.SpanStyle].
+ * @param style The style of the font, normal or italic. The system uses this to match a font to a
+ * font request that is given in a [androidx.compose.ui.text.SpanStyle].
+ * @param variationSettings on API 26 and above these settings are applied to a variable font when
+ * the font is loaded
+ */
+@ExperimentalTextApi
+@Stable
+@Suppress("StreamFiles")
+fun Font(
+    file: File,
+    weight: FontWeight = FontWeight.Normal,
+    style: FontStyle = FontStyle.Normal,
+    variationSettings: FontVariation.Settings = FontVariation.Settings(weight, style)
+): Font = AndroidFileFont(file, weight, style, variationSettings)
+
+/**
+ * Create a Font declaration from a [ParcelFileDescriptor]. The content of the
+ * [ParcelFileDescriptor] is read during construction.
+ *
+ * @param fileDescriptor the file descriptor for the font file.
+ * @param weight The weight of the font. The system uses this to match a font to a font request
+ * that is given in a [androidx.compose.ui.text.SpanStyle].
+ * @param style The style of the font, normal or italic. The system uses this to match a font to a
+ * font request that is given in a [androidx.compose.ui.text.SpanStyle].
+ * @param variationSettings these settings are applied to a variable font when the font is loaded
+ */
+@RequiresApi(26)
+@ExperimentalTextApi
+@Stable
+fun Font(
+    fileDescriptor: ParcelFileDescriptor,
+    weight: FontWeight = FontWeight.Normal,
+    style: FontStyle = FontStyle.Normal,
+    variationSettings: FontVariation.Settings = FontVariation.Settings(weight, style)
+): Font = AndroidFileDescriptorFont(fileDescriptor, weight, style, variationSettings)
 
 /**
  * Font for use on Android.
diff --git a/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/font/AndroidLoadableFonts.kt b/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/font/AndroidPreloadedFont.kt
similarity index 63%
rename from compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/font/AndroidLoadableFonts.kt
rename to compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/font/AndroidPreloadedFont.kt
index 07406a8..5f0cad7 100644
--- a/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/font/AndroidLoadableFonts.kt
+++ b/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/font/AndroidPreloadedFont.kt
@@ -24,109 +24,11 @@
 import android.os.ParcelFileDescriptor
 import androidx.annotation.DoNotInline
 import androidx.annotation.RequiresApi
-import androidx.compose.runtime.Stable
 import androidx.compose.ui.text.ExperimentalTextApi
 import androidx.compose.ui.unit.Density
 import androidx.compose.ui.util.fastMap
 import java.io.File
 
-/**
- * Create a Font declaration from a file in the assets directory. The content of the [File] is
- * read during construction.
- *
- * @param assetManager Android AssetManager
- * @param path full path starting from the assets directory (i.e. dir/myfont.ttf for
- * assets/dir/myfont.ttf).
- * @param weight The weight of the font. The system uses this to match a font to a font request
- * that is given in a [androidx.compose.ui.text.SpanStyle].
- * @param style The style of the font, normal or italic. The system uses this to match a font to a
- * font request that is given in a [androidx.compose.ui.text.SpanStyle].
- */
-@ExperimentalTextApi
-@Stable
-@Deprecated("This experimental Font is replaced by Font(path, assetManager, ...)",
-    replaceWith = ReplaceWith("Font(path, assetManager, weight, style)"),
-    level = DeprecationLevel.WARNING
-)
-fun Font(
-    assetManager: AssetManager,
-    path: String,
-    weight: FontWeight = FontWeight.Normal,
-    style: FontStyle = FontStyle.Normal
-): Font = AndroidAssetFont(
-    assetManager,
-    path,
-    weight,
-    style,
-    FontVariation.Settings(weight, style)
-)
-
-/**
- * Create a Font declaration from a file in the assets directory. The content of the [File] is
- * read during construction.
- *
- * @param path full path starting from the assets directory (i.e. dir/myfont.ttf for
- * assets/dir/myfont.ttf).
- * @param assetManager Android AssetManager
- * @param weight The weight of the font. The system uses this to match a font to a font request
- * that is given in a [androidx.compose.ui.text.SpanStyle].
- * @param style The style of the font, normal or italic. The system uses this to match a font to a
- * font request that is given in a [androidx.compose.ui.text.SpanStyle].
- * @param variationSettings on API 26 and above these settings are applied to a variable font when
- * the font is loaded
- */
-@ExperimentalTextApi
-@Stable
-fun Font(
-    path: String,
-    assetManager: AssetManager,
-    weight: FontWeight = FontWeight.Normal,
-    style: FontStyle = FontStyle.Normal,
-    variationSettings: FontVariation.Settings = FontVariation.Settings(weight, style)
-): Font = AndroidAssetFont(assetManager, path, weight, style, variationSettings)
-
-/**
- * Create a Font declaration from a file. The content of the [File] is read during construction.
- *
- * @param file the font file.
- * @param weight The weight of the font. The system uses this to match a font to a font request
- * that is given in a [androidx.compose.ui.text.SpanStyle].
- * @param style The style of the font, normal or italic. The system uses this to match a font to a
- * font request that is given in a [androidx.compose.ui.text.SpanStyle].
- * @param variationSettings on API 26 and above these settings are applied to a variable font when
- * the font is loaded
- */
-@ExperimentalTextApi
-@Stable
-@Suppress("StreamFiles")
-fun Font(
-    file: File,
-    weight: FontWeight = FontWeight.Normal,
-    style: FontStyle = FontStyle.Normal,
-    variationSettings: FontVariation.Settings = FontVariation.Settings(weight, style)
-): Font = AndroidFileFont(file, weight, style, variationSettings)
-
-/**
- * Create a Font declaration from a [ParcelFileDescriptor]. The content of the
- * [ParcelFileDescriptor] is read during construction.
- *
- * @param fileDescriptor the file descriptor for the font file.
- * @param weight The weight of the font. The system uses this to match a font to a font request
- * that is given in a [androidx.compose.ui.text.SpanStyle].
- * @param style The style of the font, normal or italic. The system uses this to match a font to a
- * font request that is given in a [androidx.compose.ui.text.SpanStyle].
- * @param variationSettings these settings are applied to a variable font when the font is loaded
- */
-@RequiresApi(26)
-@ExperimentalTextApi
-@Stable
-fun Font(
-    fileDescriptor: ParcelFileDescriptor,
-    weight: FontWeight = FontWeight.Normal,
-    style: FontStyle = FontStyle.Normal,
-    variationSettings: FontVariation.Settings = FontVariation.Settings(weight, style)
-): Font = AndroidFileDescriptorFont(fileDescriptor, weight, style, variationSettings)
-
 @OptIn(ExperimentalTextApi::class)
 internal sealed class AndroidPreloadedFont @OptIn(ExperimentalTextApi::class) constructor(
     final override val weight: FontWeight,
@@ -163,7 +65,7 @@
 }
 
 @OptIn(ExperimentalTextApi::class) /* FontVariation.Settings */
-private class AndroidAssetFont constructor(
+internal class AndroidAssetFont constructor(
     val assetManager: AssetManager,
     val path: String,
     weight: FontWeight = FontWeight.Normal,
@@ -207,7 +109,7 @@
 }
 
 @OptIn(ExperimentalTextApi::class)
-private class AndroidFileFont constructor(
+internal class AndroidFileFont constructor(
     val file: File,
     weight: FontWeight = FontWeight.Normal,
     style: FontStyle = FontStyle.Normal,
@@ -234,7 +136,7 @@
 
 @RequiresApi(26)
 @OptIn(ExperimentalTextApi::class)
-private class AndroidFileDescriptorFont constructor(
+internal class AndroidFileDescriptorFont constructor(
     val fileDescriptor: ParcelFileDescriptor,
     weight: FontWeight = FontWeight.Normal,
     style: FontStyle = FontStyle.Normal,
diff --git a/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/input/TextInputService.kt b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/input/TextInputService.kt
index b41257c..cfb4a53 100644
--- a/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/input/TextInputService.kt
+++ b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/input/TextInputService.kt
@@ -88,7 +88,7 @@
      * system service may silently ignore this request.
      */
     @Deprecated(
-        message = "Use SoftwareKeyboardController.showSoftwareKeyboard or " +
+        message = "Use SoftwareKeyboardController.show or " +
             "TextInputSession.showSoftwareKeyboard instead.",
         replaceWith = ReplaceWith("textInputSession.showSoftwareKeyboard()")
     )
@@ -103,7 +103,7 @@
      * Hide onscreen keyboard.
      */
     @Deprecated(
-        message = "Use SoftwareKeyboardController.hideSoftwareKeyboard or " +
+        message = "Use SoftwareKeyboardController.hide or " +
             "TextInputSession.hideSoftwareKeyboard instead.",
         replaceWith = ReplaceWith("textInputSession.hideSoftwareKeyboard()")
     )
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/window/DialogScreenshotTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/window/DialogScreenshotTest.kt
index bb15949..8fbb979 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/window/DialogScreenshotTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/window/DialogScreenshotTest.kt
@@ -34,6 +34,7 @@
 import androidx.test.filters.LargeTest
 import androidx.test.filters.SdkSuppress
 import androidx.test.screenshot.AndroidXScreenshotTestRule
+import androidx.test.screenshot.matchers.MSSIMMatcher
 import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -71,7 +72,7 @@
     fun dialogWithElevation() {
         rule.setContent {
             Dialog(onDismissRequest = {}) {
-                val elevation = with(LocalDensity.current) { 16.dp.toPx() }
+                val elevation = with(LocalDensity.current) { 8.dp.toPx() }
                 Box(
                     Modifier
                         .graphicsLayer(
@@ -87,6 +88,10 @@
 
         rule.onNode(isDialog())
             .captureToImage()
-            .assertAgainstGolden(screenshotRule, "dialogWithElevation")
+            .assertAgainstGolden(
+                screenshotRule,
+                "dialogWithElevation",
+                matcher = MSSIMMatcher(threshold = 0.999)
+            )
     }
 }
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/window/AndroidDialog.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/window/AndroidDialog.android.kt
index c615ddc..f7a7972 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/window/AndroidDialog.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/window/AndroidDialog.android.kt
@@ -98,7 +98,7 @@
         dismissOnBackPress: Boolean = true,
         dismissOnClickOutside: Boolean = true,
         securePolicy: SecureFlagPolicy = SecureFlagPolicy.Inherit,
-    ) : this (
+    ) : this(
         dismissOnBackPress = dismissOnBackPress,
         dismissOnClickOutside = dismissOnClickOutside,
         securePolicy = securePolicy,
@@ -297,7 +297,9 @@
 
     private val dialogLayout: DialogLayout
 
-    private val maxSupportedElevation = 30.dp
+    // On systems older than Android S, there is a bug in the surface insets matrix math used by
+    // elevation, so high values of maxSupportedElevation break accessibility services: b/232788477.
+    private val maxSupportedElevation = 8.dp
 
     override val subCompositionView: AbstractComposeView get() = dialogLayout
 
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/window/AndroidPopup.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/window/AndroidPopup.android.kt
index 1250af4..9702046 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/window/AndroidPopup.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/window/AndroidPopup.android.kt
@@ -406,7 +406,9 @@
         parentLayoutCoordinates != null && popupContentSize != null
     }
 
-    private val maxSupportedElevation = 30.dp
+    // On systems older than Android S, there is a bug in the surface insets matrix math used by
+    // elevation, so high values of maxSupportedElevation break accessibility services: b/232788477.
+    private val maxSupportedElevation = 8.dp
 
     // The window visible frame used for the last popup position calculation.
     private val previousWindowVisibleFrame = Rect()
diff --git a/docs-public/build.gradle b/docs-public/build.gradle
index 198de14..accad37 100644
--- a/docs-public/build.gradle
+++ b/docs-public/build.gradle
@@ -107,15 +107,15 @@
     docs("androidx.core.uwb:uwb:1.0.0-alpha03")
     docs("androidx.core.uwb:uwb-rxjava3:1.0.0-alpha03")
     docs("androidx.core:core-google-shortcuts:1.1.0-alpha02")
-    docs("androidx.core:core-performance:1.0.0-alpha02")
-    samples("androidx.core:core-performance-samples:1.0.0-alpha02")
+    docs("androidx.core:core-performance:1.0.0-alpha03")
+    samples("androidx.core:core-performance-samples:1.0.0-alpha03")
     docs("androidx.core:core-remoteviews:1.0.0-beta02")
-    docs("androidx.core:core-role:1.1.0-rc01")
-    docs("androidx.core:core-animation:1.0.0-beta01")
-    docs("androidx.core:core-animation-testing:1.0.0-alpha02")
-    docs("androidx.core:core:1.9.0-beta01")
-    docs("androidx.core:core-ktx:1.9.0-alpha05")
-    docs("androidx.core:core-splashscreen:1.0.0")
+    docs("androidx.core:core-role:1.2.0-alpha01")
+    docs("androidx.core:core-animation:1.0.0-beta02")
+    docs("androidx.core:core-animation-testing:1.0.0-beta01")
+    docs("androidx.core:core:1.9.0-rc01")
+    docs("androidx.core:core-ktx:1.9.0-rc01")
+    docs("androidx.core:core-splashscreen:1.1.0-alpha01")
     docs("androidx.cursoradapter:cursoradapter:1.0.0")
     docs("androidx.customview:customview:1.2.0-alpha01")
     docs("androidx.customview:customview-poolingcontainer:1.0.0-rc01")
diff --git a/glance/glance-appwidget/integration-tests/template-demos/src/main/AndroidManifest.xml b/glance/glance-appwidget/integration-tests/template-demos/src/main/AndroidManifest.xml
index 4f61b07..714f730 100644
--- a/glance/glance-appwidget/integration-tests/template-demos/src/main/AndroidManifest.xml
+++ b/glance/glance-appwidget/integration-tests/template-demos/src/main/AndroidManifest.xml
@@ -35,10 +35,38 @@
         </receiver>
 
         <receiver
-            android:name="androidx.glance.appwidget.template.demos.GalleryDemoWidgetReceiver"
+            android:name="androidx.glance.appwidget.template.demos.SmallImageGalleryReceiver"
             android:enabled="@bool/glance_appwidget_available"
             android:exported="false"
-            android:label="@string/gallery_template_name">
+            android:label="@string/small_image_gallery">
+            <intent-filter>
+                <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
+                <action android:name="android.intent.action.LOCALE_CHANGED" />
+            </intent-filter>
+            <meta-data
+                android:name="android.appwidget.provider"
+                android:resource="@xml/default_app_widget_info" />
+        </receiver>
+
+        <receiver
+            android:name="androidx.glance.appwidget.template.demos.MediumImageGalleryReceiver"
+            android:enabled="@bool/glance_appwidget_available"
+            android:exported="false"
+            android:label="@string/medium_image_gallery">
+            <intent-filter>
+                <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
+                <action android:name="android.intent.action.LOCALE_CHANGED" />
+            </intent-filter>
+            <meta-data
+                android:name="android.appwidget.provider"
+                android:resource="@xml/default_app_widget_info" />
+        </receiver>
+
+        <receiver
+            android:name="androidx.glance.appwidget.template.demos.LargeImageGalleryReceiver"
+            android:enabled="@bool/glance_appwidget_available"
+            android:exported="false"
+            android:label="@string/large_image_gallery">
             <intent-filter>
                 <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
                 <action android:name="android.intent.action.LOCALE_CHANGED" />
diff --git a/glance/glance-appwidget/integration-tests/template-demos/src/main/java/androidx/glance/appwidget/template/demos/GalleryDemoWidget.kt b/glance/glance-appwidget/integration-tests/template-demos/src/main/java/androidx/glance/appwidget/template/demos/GalleryDemoWidget.kt
index 58496d2..2c00db6 100644
--- a/glance/glance-appwidget/integration-tests/template-demos/src/main/java/androidx/glance/appwidget/template/demos/GalleryDemoWidget.kt
+++ b/glance/glance-appwidget/integration-tests/template-demos/src/main/java/androidx/glance/appwidget/template/demos/GalleryDemoWidget.kt
@@ -25,9 +25,11 @@
 import androidx.glance.appwidget.template.GalleryTemplate
 import androidx.glance.appwidget.template.GlanceTemplateAppWidget
 import androidx.glance.template.ActionBlock
+import androidx.glance.template.AspectRatio
 import androidx.glance.template.GalleryTemplateData
 import androidx.glance.template.HeaderBlock
 import androidx.glance.template.ImageBlock
+import androidx.glance.template.ImageSize
 import androidx.glance.template.TemplateImageWithDescription
 import androidx.glance.template.TemplateText
 import androidx.glance.template.TemplateTextButton
@@ -35,13 +37,58 @@
 import androidx.glance.template.TextType
 
 /**
- * A widget that uses [GalleryTemplate].
+ * Gallery demo for the default Small sized images with 1:1 aspect ratio and left-to-right main
+ * text/image block flow using data and gallery template from [BaseGalleryTemplateWidget].
  */
-class GalleryTemplateWidget : GlanceTemplateAppWidget() {
+class SmallGalleryTemplateDemoWidget : BaseGalleryTemplateWidget() {
+    @Composable
+    override fun TemplateContent() = GalleryTemplateContent()
+}
+
+/**
+ * Gallery demo for the Medium sized images with 16:9 aspect ratio and right-to-left main
+ * text/image block flow using data and gallery template from [BaseGalleryTemplateWidget].
+ */
+class MediumGalleryTemplateDemoWidget : BaseGalleryTemplateWidget() {
+    @Composable
+    override fun TemplateContent() =
+        GalleryTemplateContent(ImageSize.Medium, AspectRatio.Ratio16x9, false)
+}
+
+/**
+ * Gallery demo for the Large sized images with 2:3 aspect ratio and left-to-right main
+ * text/image block flow using data and gallery template from [BaseGalleryTemplateWidget].
+ */
+class LargeGalleryTemplateDemoWidget : BaseGalleryTemplateWidget() {
+    @Composable
+    override fun TemplateContent() = GalleryTemplateContent(ImageSize.Large, AspectRatio.Ratio2x3)
+}
+
+class SmallImageGalleryReceiver : GlanceAppWidgetReceiver() {
+    override val glanceAppWidget: GlanceAppWidget = SmallGalleryTemplateDemoWidget()
+}
+
+class MediumImageGalleryReceiver : GlanceAppWidgetReceiver() {
+    override val glanceAppWidget: GlanceAppWidget = MediumGalleryTemplateDemoWidget()
+}
+
+class LargeImageGalleryReceiver : GlanceAppWidgetReceiver() {
+    override val glanceAppWidget: GlanceAppWidget = LargeGalleryTemplateDemoWidget()
+}
+
+/**
+ * Base Gallery Demo widget binding [GalleryTemplateData] to [GalleryTemplate] layout.
+ * It is overridable by gallery image aspect ratio, image size, and main blocks ordering.
+ */
+abstract class BaseGalleryTemplateWidget : GlanceTemplateAppWidget() {
     override val sizeMode = SizeMode.Exact
 
     @Composable
-    override fun TemplateContent() {
+    internal fun GalleryTemplateContent(
+        imageSize: ImageSize = ImageSize.Small,
+        aspectRatio: AspectRatio = AspectRatio.Ratio1x1,
+        isMainTextBlockFirst: Boolean = true,
+    ) {
         val galleryContent = mutableListOf<TemplateImageWithDescription>()
         for (i in 1..30) {
             galleryContent.add(
@@ -64,7 +111,7 @@
                     text1 = TemplateText("Title1", TextType.Title),
                     text2 = TemplateText("Headline1", TextType.Headline),
                     text3 = TemplateText("Label1", TextType.Label),
-                    priority = 0,
+                    priority = if (isMainTextBlockFirst) 0 else 1,
                 ),
                 mainImageBlock = ImageBlock(
                     images = listOf(
@@ -73,7 +120,7 @@
                             "test image"
                         )
                     ),
-                    priority = 1,
+                    priority = if (isMainTextBlockFirst) 1 else 0,
                 ),
                 mainActionBlock = ActionBlock(
                     actionButtons = listOf(
@@ -89,13 +136,10 @@
                 ),
                 galleryImageBlock = ImageBlock(
                     images = galleryContent,
-                    priority = 2,
+                    aspectRatio = aspectRatio,
+                    size = imageSize,
                 ),
             )
         )
     }
 }
-
-class GalleryDemoWidgetReceiver : GlanceAppWidgetReceiver() {
-    override val glanceAppWidget: GlanceAppWidget = GalleryTemplateWidget()
-}
diff --git a/glance/glance-appwidget/integration-tests/template-demos/src/main/res/values/strings.xml b/glance/glance-appwidget/integration-tests/template-demos/src/main/res/values/strings.xml
index 8277be9..4ee5288 100644
--- a/glance/glance-appwidget/integration-tests/template-demos/src/main/res/values/strings.xml
+++ b/glance/glance-appwidget/integration-tests/template-demos/src/main/res/values/strings.xml
@@ -19,8 +19,6 @@
 
     <!-- Name of the app widgets -->
     <string name="single_entity_template_widget_name"><u>Single Entity Template demo</u></string>
-    <string name="gallery_template_name"><u>Gallery demo</u></string>
-    <string name="list_template_widget_name"><u>List Template demo</u></string>
     <string name="override_widget_name"><u>Template layout override demo</u></string>
 
     <string name="template_data_saved_message">Saved</string>
@@ -32,4 +30,9 @@
     <string name="list_style_with_header">List Template with header demo</string>
     <string name="list_style_no_header">List Template with no header demo</string>
     <string name="list_style_brief">List Template with no header demo in brief info</string>
+
+    <!-- Sample of the Gallery Template widgets -->
+    <string name="small_image_gallery">Gallery Template with small 1:1 images, text first</string>
+    <string name="medium_image_gallery">Gallery Template with medium 16:9 images, text last</string>
+    <string name="large_image_gallery">Gallery Template with large 2:3 images, text first</string>
 </resources>
diff --git a/health/connect/connect-client/api/current.txt b/health/connect/connect-client/api/current.txt
index 4dc90d3..943136b 100644
--- a/health/connect/connect-client/api/current.txt
+++ b/health/connect/connect-client/api/current.txt
@@ -5,7 +5,7 @@
     method public suspend Object? aggregate(androidx.health.connect.client.request.AggregateRequest request, kotlin.coroutines.Continuation<? super androidx.health.connect.client.aggregate.AggregationResult>);
     method public suspend Object? aggregateGroupByDuration(androidx.health.connect.client.request.AggregateGroupByDurationRequest request, kotlin.coroutines.Continuation<? super java.util.List<? extends androidx.health.connect.client.aggregate.AggregationResultGroupedByDuration>>);
     method public suspend Object? aggregateGroupByPeriod(androidx.health.connect.client.request.AggregateGroupByPeriodRequest request, kotlin.coroutines.Continuation<? super java.util.List<? extends androidx.health.connect.client.aggregate.AggregationResultGroupedByPeriod>>);
-    method public suspend Object? deleteRecords(kotlin.reflect.KClass<? extends androidx.health.connect.client.records.Record> recordType, java.util.List<java.lang.String> uidsList, java.util.List<java.lang.String> clientIdsList, kotlin.coroutines.Continuation<? super kotlin.Unit>);
+    method public suspend Object? deleteRecords(kotlin.reflect.KClass<? extends androidx.health.connect.client.records.Record> recordType, java.util.List<java.lang.String> uidsList, java.util.List<java.lang.String> clientRecordIdsList, kotlin.coroutines.Continuation<? super kotlin.Unit>);
     method public suspend Object? deleteRecords(kotlin.reflect.KClass<? extends androidx.health.connect.client.records.Record> recordType, androidx.health.connect.client.time.TimeRangeFilter timeRangeFilter, kotlin.coroutines.Continuation<? super kotlin.Unit>);
     method public suspend Object? getChanges(String changesToken, kotlin.coroutines.Continuation<? super androidx.health.connect.client.response.ChangesResponse>);
     method public suspend Object? getChangesToken(androidx.health.connect.client.request.ChangesTokenRequest request, kotlin.coroutines.Continuation<? super java.lang.String>);
@@ -1303,15 +1303,15 @@
   }
 
   public final class Metadata {
-    ctor public Metadata(optional String uid, optional androidx.health.connect.client.records.metadata.DataOrigin dataOrigin, optional java.time.Instant lastModifiedTime, optional String? clientId, optional long clientVersion, optional androidx.health.connect.client.records.metadata.Device? device);
-    method public String? getClientId();
-    method public long getClientVersion();
+    ctor public Metadata(optional String uid, optional androidx.health.connect.client.records.metadata.DataOrigin dataOrigin, optional java.time.Instant lastModifiedTime, optional String? clientRecordId, optional long clientRecordVersion, optional androidx.health.connect.client.records.metadata.Device? device);
+    method public String? getClientRecordId();
+    method public long getClientRecordVersion();
     method public androidx.health.connect.client.records.metadata.DataOrigin getDataOrigin();
     method public androidx.health.connect.client.records.metadata.Device? getDevice();
     method public java.time.Instant getLastModifiedTime();
     method public String getUid();
-    property public final String? clientId;
-    property public final long clientVersion;
+    property public final String? clientRecordId;
+    property public final long clientRecordVersion;
     property public final androidx.health.connect.client.records.metadata.DataOrigin dataOrigin;
     property public final androidx.health.connect.client.records.metadata.Device? device;
     property public final java.time.Instant lastModifiedTime;
diff --git a/health/connect/connect-client/api/public_plus_experimental_current.txt b/health/connect/connect-client/api/public_plus_experimental_current.txt
index 4dc90d3..943136b 100644
--- a/health/connect/connect-client/api/public_plus_experimental_current.txt
+++ b/health/connect/connect-client/api/public_plus_experimental_current.txt
@@ -5,7 +5,7 @@
     method public suspend Object? aggregate(androidx.health.connect.client.request.AggregateRequest request, kotlin.coroutines.Continuation<? super androidx.health.connect.client.aggregate.AggregationResult>);
     method public suspend Object? aggregateGroupByDuration(androidx.health.connect.client.request.AggregateGroupByDurationRequest request, kotlin.coroutines.Continuation<? super java.util.List<? extends androidx.health.connect.client.aggregate.AggregationResultGroupedByDuration>>);
     method public suspend Object? aggregateGroupByPeriod(androidx.health.connect.client.request.AggregateGroupByPeriodRequest request, kotlin.coroutines.Continuation<? super java.util.List<? extends androidx.health.connect.client.aggregate.AggregationResultGroupedByPeriod>>);
-    method public suspend Object? deleteRecords(kotlin.reflect.KClass<? extends androidx.health.connect.client.records.Record> recordType, java.util.List<java.lang.String> uidsList, java.util.List<java.lang.String> clientIdsList, kotlin.coroutines.Continuation<? super kotlin.Unit>);
+    method public suspend Object? deleteRecords(kotlin.reflect.KClass<? extends androidx.health.connect.client.records.Record> recordType, java.util.List<java.lang.String> uidsList, java.util.List<java.lang.String> clientRecordIdsList, kotlin.coroutines.Continuation<? super kotlin.Unit>);
     method public suspend Object? deleteRecords(kotlin.reflect.KClass<? extends androidx.health.connect.client.records.Record> recordType, androidx.health.connect.client.time.TimeRangeFilter timeRangeFilter, kotlin.coroutines.Continuation<? super kotlin.Unit>);
     method public suspend Object? getChanges(String changesToken, kotlin.coroutines.Continuation<? super androidx.health.connect.client.response.ChangesResponse>);
     method public suspend Object? getChangesToken(androidx.health.connect.client.request.ChangesTokenRequest request, kotlin.coroutines.Continuation<? super java.lang.String>);
@@ -1303,15 +1303,15 @@
   }
 
   public final class Metadata {
-    ctor public Metadata(optional String uid, optional androidx.health.connect.client.records.metadata.DataOrigin dataOrigin, optional java.time.Instant lastModifiedTime, optional String? clientId, optional long clientVersion, optional androidx.health.connect.client.records.metadata.Device? device);
-    method public String? getClientId();
-    method public long getClientVersion();
+    ctor public Metadata(optional String uid, optional androidx.health.connect.client.records.metadata.DataOrigin dataOrigin, optional java.time.Instant lastModifiedTime, optional String? clientRecordId, optional long clientRecordVersion, optional androidx.health.connect.client.records.metadata.Device? device);
+    method public String? getClientRecordId();
+    method public long getClientRecordVersion();
     method public androidx.health.connect.client.records.metadata.DataOrigin getDataOrigin();
     method public androidx.health.connect.client.records.metadata.Device? getDevice();
     method public java.time.Instant getLastModifiedTime();
     method public String getUid();
-    property public final String? clientId;
-    property public final long clientVersion;
+    property public final String? clientRecordId;
+    property public final long clientRecordVersion;
     property public final androidx.health.connect.client.records.metadata.DataOrigin dataOrigin;
     property public final androidx.health.connect.client.records.metadata.Device? device;
     property public final java.time.Instant lastModifiedTime;
diff --git a/health/connect/connect-client/api/restricted_current.txt b/health/connect/connect-client/api/restricted_current.txt
index b0afe55..9373082 100644
--- a/health/connect/connect-client/api/restricted_current.txt
+++ b/health/connect/connect-client/api/restricted_current.txt
@@ -5,7 +5,7 @@
     method public suspend Object? aggregate(androidx.health.connect.client.request.AggregateRequest request, kotlin.coroutines.Continuation<? super androidx.health.connect.client.aggregate.AggregationResult>);
     method public suspend Object? aggregateGroupByDuration(androidx.health.connect.client.request.AggregateGroupByDurationRequest request, kotlin.coroutines.Continuation<? super java.util.List<? extends androidx.health.connect.client.aggregate.AggregationResultGroupedByDuration>>);
     method public suspend Object? aggregateGroupByPeriod(androidx.health.connect.client.request.AggregateGroupByPeriodRequest request, kotlin.coroutines.Continuation<? super java.util.List<? extends androidx.health.connect.client.aggregate.AggregationResultGroupedByPeriod>>);
-    method public suspend Object? deleteRecords(kotlin.reflect.KClass<? extends androidx.health.connect.client.records.Record> recordType, java.util.List<java.lang.String> uidsList, java.util.List<java.lang.String> clientIdsList, kotlin.coroutines.Continuation<? super kotlin.Unit>);
+    method public suspend Object? deleteRecords(kotlin.reflect.KClass<? extends androidx.health.connect.client.records.Record> recordType, java.util.List<java.lang.String> uidsList, java.util.List<java.lang.String> clientRecordIdsList, kotlin.coroutines.Continuation<? super kotlin.Unit>);
     method public suspend Object? deleteRecords(kotlin.reflect.KClass<? extends androidx.health.connect.client.records.Record> recordType, androidx.health.connect.client.time.TimeRangeFilter timeRangeFilter, kotlin.coroutines.Continuation<? super kotlin.Unit>);
     method public suspend Object? getChanges(String changesToken, kotlin.coroutines.Continuation<? super androidx.health.connect.client.response.ChangesResponse>);
     method public suspend Object? getChangesToken(androidx.health.connect.client.request.ChangesTokenRequest request, kotlin.coroutines.Continuation<? super java.lang.String>);
@@ -1326,15 +1326,15 @@
   }
 
   public final class Metadata {
-    ctor public Metadata(optional String uid, optional androidx.health.connect.client.records.metadata.DataOrigin dataOrigin, optional java.time.Instant lastModifiedTime, optional String? clientId, optional long clientVersion, optional androidx.health.connect.client.records.metadata.Device? device);
-    method public String? getClientId();
-    method public long getClientVersion();
+    ctor public Metadata(optional String uid, optional androidx.health.connect.client.records.metadata.DataOrigin dataOrigin, optional java.time.Instant lastModifiedTime, optional String? clientRecordId, optional long clientRecordVersion, optional androidx.health.connect.client.records.metadata.Device? device);
+    method public String? getClientRecordId();
+    method public long getClientRecordVersion();
     method public androidx.health.connect.client.records.metadata.DataOrigin getDataOrigin();
     method public androidx.health.connect.client.records.metadata.Device? getDevice();
     method public java.time.Instant getLastModifiedTime();
     method public String getUid();
-    property public final String? clientId;
-    property public final long clientVersion;
+    property public final String? clientRecordId;
+    property public final long clientRecordVersion;
     property public final androidx.health.connect.client.records.metadata.DataOrigin dataOrigin;
     property public final androidx.health.connect.client.records.metadata.Device? device;
     property public final java.time.Instant lastModifiedTime;
diff --git a/health/connect/connect-client/samples/src/main/java/androidx/health/connect/client/samples/DeleteRecordsSamples.kt b/health/connect/connect-client/samples/src/main/java/androidx/health/connect/client/samples/DeleteRecordsSamples.kt
index da51ac0..892e6f2 100644
--- a/health/connect/connect-client/samples/src/main/java/androidx/health/connect/client/samples/DeleteRecordsSamples.kt
+++ b/health/connect/connect-client/samples/src/main/java/androidx/health/connect/client/samples/DeleteRecordsSamples.kt
@@ -35,7 +35,7 @@
     healthConnectClient.deleteRecords(
         StepsRecord::class,
         uidsList = listOf(uid1, uid2),
-        clientIdsList = emptyList()
+        clientRecordIdsList = emptyList()
     )
 }
 
diff --git a/health/connect/connect-client/src/main/java/androidx/health/connect/client/HealthConnectClient.kt b/health/connect/connect-client/src/main/java/androidx/health/connect/client/HealthConnectClient.kt
index 1a426c1..61e9f8d 100644
--- a/health/connect/connect-client/src/main/java/androidx/health/connect/client/HealthConnectClient.kt
+++ b/health/connect/connect-client/src/main/java/androidx/health/connect/client/HealthConnectClient.kt
@@ -64,10 +64,10 @@
      * To insert some heart rate data:
      * @sample androidx.health.connect.client.samples.InsertHeartRateSeries
      *
-     * [androidx.health.connect.client.records.metadata.Metadata.clientId] can be used to
+     * [androidx.health.connect.client.records.metadata.Metadata.clientRecordId] can be used to
      * deduplicate data with a client provided unique identifier. When a subsequent [insertRecords]
-     * is called with the same [androidx.health.connect.client.records.metadata.Metadata.clientId],
-     * whichever [Record] with the higher [androidx.health.connect.client.records.metadata.Metadata.clientVersion]
+     * is called with the same [androidx.health.connect.client.records.metadata.Metadata.clientRecordId],
+     * whichever [Record] with the higher [androidx.health.connect.client.records.metadata.Metadata.clientRecordVersion]
      * takes precedence.
      *
      * @param records List of records to insert
@@ -101,7 +101,7 @@
      *
      * @param recordType Which type of [Record] to delete, such as `Steps::class`
      * @param uidsList List of uids of [Record] to delete
-     * @param clientIdsList List of client IDs of [Record] to delete
+     * @param clientRecordIdsList List of client record IDs of [Record] to delete
      * @throws RemoteException For any IPC transportation failures. Deleting by invalid identifiers
      * such as a non-existing identifier or deleting the same record multiple times will result in
      * IPC failure.
@@ -112,7 +112,7 @@
     suspend fun deleteRecords(
         recordType: KClass<out Record>,
         uidsList: List<String>,
-        clientIdsList: List<String>,
+        clientRecordIdsList: List<String>,
     )
 
     /**
diff --git a/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/HealthConnectClientImpl.kt b/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/HealthConnectClientImpl.kt
index c8b51e8..4140dce 100644
--- a/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/HealthConnectClientImpl.kt
+++ b/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/HealthConnectClientImpl.kt
@@ -110,17 +110,17 @@
     override suspend fun deleteRecords(
         recordType: KClass<out Record>,
         uidsList: List<String>,
-        clientIdsList: List<String>,
+        clientRecordIdsList: List<String>,
     ) {
         delegate
             .deleteData(
                 toDataTypeIdPairProtoList(recordType, uidsList),
-                toDataTypeIdPairProtoList(recordType, clientIdsList)
+                toDataTypeIdPairProtoList(recordType, clientRecordIdsList)
             )
             .await()
         Logger.debug(
             HEALTH_CONNECT_CLIENT_TAG,
-            "${uidsList.size + clientIdsList.size} records deleted."
+            "${uidsList.size + clientRecordIdsList.size} records deleted."
         )
     }
 
diff --git a/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/converters/records/ProtoToRecordUtils.kt b/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/converters/records/ProtoToRecordUtils.kt
index 53ed952..afcfac2 100644
--- a/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/converters/records/ProtoToRecordUtils.kt
+++ b/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/converters/records/ProtoToRecordUtils.kt
@@ -82,8 +82,8 @@
             uid = if (hasUid()) uid else Metadata.EMPTY_UID,
             dataOrigin = DataOrigin(dataOrigin.applicationId),
             lastModifiedTime = Instant.ofEpochMilli(updateTimeMillis),
-            clientId = if (hasClientId()) clientId else null,
-            clientVersion = clientVersion,
+            clientRecordId = if (hasClientId()) clientId else null,
+            clientRecordVersion = clientVersion,
             device = toDevice(device)
         )
 
diff --git a/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/converters/records/RecordToProtoUtils.kt b/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/converters/records/RecordToProtoUtils.kt
index e8bd828..7ced9bd 100644
--- a/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/converters/records/RecordToProtoUtils.kt
+++ b/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/converters/records/RecordToProtoUtils.kt
@@ -67,9 +67,9 @@
         setUpdateTimeMillis(metadata.lastModifiedTime.toEpochMilli())
     }
 
-    metadata.clientId?.let { setClientId(it) }
-    if (metadata.clientVersion > 0) {
-        metadata.clientVersion.let { setClientVersion(it) }
+    metadata.clientRecordId?.let { setClientId(it) }
+    if (metadata.clientRecordVersion > 0) {
+        metadata.clientRecordVersion.let { setClientVersion(it) }
     }
     metadata.device?.let { setDevice(it.toProto()) }
 }
diff --git a/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/metadata/Metadata.kt b/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/metadata/Metadata.kt
index 8f97dff..2861b86 100644
--- a/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/metadata/Metadata.kt
+++ b/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/metadata/Metadata.kt
@@ -43,26 +43,26 @@
     public val lastModifiedTime: Instant = Instant.EPOCH,
 
     /**
-     * Optional client supplied unique data identifier associated with the data.
+     * Optional client supplied record unique data identifier associated with the data.
      *
      * There is guaranteed a single entry for any type of data with same client provided identifier
      * for a given client. Any new insertions with the same client provided identifier will either
-     * replace or be ignored depending on associated [clientVersion].
+     * replace or be ignored depending on associated [clientRecordVersion].
      *
-     * @see clientVersion
+     * @see clientRecordVersion
      */
-    public val clientId: String? = null,
+    public val clientRecordId: String? = null,
 
     /**
      * Optional client supplied version associated with the data.
      *
      * This determines conflict resolution outcome when there are multiple insertions of the same
-     * [clientId]. Data with the highest [clientVersion] takes precedence. [clientVersion] starts
-     * with 0.
+     * [clientRecordId]. Data with the highest [clientRecordVersion] takes precedence.
+     * [clientRecordVersion] starts with 0.
      *
-     * @see clientId
+     * @see clientRecordId
      */
-    public val clientVersion: Long = 0,
+    public val clientRecordVersion: Long = 0,
 
     /** Optional client supplied device information associated with the data. */
     public val device: Device? = null,
@@ -74,8 +74,8 @@
         if (uid != other.uid) return false
         if (dataOrigin != other.dataOrigin) return false
         if (lastModifiedTime != other.lastModifiedTime) return false
-        if (clientId != other.clientId) return false
-        if (clientVersion != other.clientVersion) return false
+        if (clientRecordId != other.clientRecordId) return false
+        if (clientRecordVersion != other.clientRecordVersion) return false
         if (device != other.device) return false
 
         return true
@@ -85,8 +85,8 @@
         var result = uid.hashCode()
         result = 31 * result + dataOrigin.hashCode()
         result = 31 * result + lastModifiedTime.hashCode()
-        result = 31 * result + (clientId?.hashCode() ?: 0)
-        result = 31 * result + clientVersion.hashCode()
+        result = 31 * result + (clientRecordId?.hashCode() ?: 0)
+        result = 31 * result + clientRecordVersion.hashCode()
         result = 31 * result + (device?.hashCode() ?: 0)
         return result
     }
diff --git a/health/connect/connect-client/src/test/java/androidx/health/connect/client/impl/converters/records/AllRecordsConverterTest.kt b/health/connect/connect-client/src/test/java/androidx/health/connect/client/impl/converters/records/AllRecordsConverterTest.kt
index 1510f7b..bf29d60 100644
--- a/health/connect/connect-client/src/test/java/androidx/health/connect/client/impl/converters/records/AllRecordsConverterTest.kt
+++ b/health/connect/connect-client/src/test/java/androidx/health/connect/client/impl/converters/records/AllRecordsConverterTest.kt
@@ -109,8 +109,8 @@
 private val TEST_METADATA =
     Metadata(
         uid = "uid",
-        clientId = "clientId",
-        clientVersion = 10,
+        clientRecordId = "clientId",
+        clientRecordVersion = 10,
         device = Device(manufacturer = "manufacturer"),
         lastModifiedTime = END_TIME,
         dataOrigin = DataOrigin(packageName = "appId")
diff --git a/libraryversions.toml b/libraryversions.toml
index dfba9d5..cabe69c 100644
--- a/libraryversions.toml
+++ b/libraryversions.toml
@@ -1,9 +1,9 @@
 [versions]
-ACTIVITY = "1.6.0-beta02"
+ACTIVITY = "1.6.0-rc01"
 ADS_IDENTIFIER = "1.0.0-alpha05"
 ANNOTATION = "1.5.0-beta01"
 ANNOTATION_EXPERIMENTAL = "1.3.0-rc01"
-APPCOMPAT = "1.6.0-beta02"
+APPCOMPAT = "1.6.0-rc01"
 APPSEARCH = "1.1.0-alpha02"
 ARCH_CORE = "2.2.0-alpha01"
 ASYNCLAYOUTINFLATER = "1.1.0-alpha01"
diff --git a/privacysandbox/tools/tools-apicompiler/build.gradle b/privacysandbox/tools/tools-apicompiler/build.gradle
index f4cab03..39983af 100644
--- a/privacysandbox/tools/tools-apicompiler/build.gradle
+++ b/privacysandbox/tools/tools-apicompiler/build.gradle
@@ -24,6 +24,11 @@
 dependencies {
     api(libs.kotlinStdlib)
     implementation(libs.kspApi)
+    implementation project(path: ':privacysandbox:tools:tools')
+
+    testImplementation(project(":room:room-compiler-processing-testing"))
+    testImplementation(libs.junit)
+    testImplementation(libs.truth)
 }
 
 androidx {
diff --git a/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/PrivacySandboxKspCompiler.kt b/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/PrivacySandboxKspCompiler.kt
index 6f326cd..8fc30bf 100644
--- a/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/PrivacySandboxKspCompiler.kt
+++ b/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/PrivacySandboxKspCompiler.kt
@@ -16,20 +16,21 @@
 
 package androidx.privacysandbox.tools.apicompiler
 
+import com.google.devtools.ksp.processing.KSPLogger
 import com.google.devtools.ksp.processing.Resolver
 import com.google.devtools.ksp.processing.SymbolProcessor
 import com.google.devtools.ksp.processing.SymbolProcessorEnvironment
 import com.google.devtools.ksp.processing.SymbolProcessorProvider
 import com.google.devtools.ksp.symbol.KSAnnotated
 
-class PrivacySandboxKspCompiler() : SymbolProcessor {
+class PrivacySandboxKspCompiler(val logger: KSPLogger) : SymbolProcessor {
     override fun process(resolver: Resolver): List<KSAnnotated> {
         return emptyList()
     }
 
     class Provider : SymbolProcessorProvider {
         override fun create(environment: SymbolProcessorEnvironment): SymbolProcessor {
-            return PrivacySandboxKspCompiler()
+            return PrivacySandboxKspCompiler(environment.logger)
         }
     }
 }
\ No newline at end of file
diff --git a/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/Compiler.kt b/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/model/AnnotatedInterface.kt
similarity index 72%
copy from privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/Compiler.kt
copy to privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/model/AnnotatedInterface.kt
index 4f92192..bfc8047 100644
--- a/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/Compiler.kt
+++ b/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/model/AnnotatedInterface.kt
@@ -14,4 +14,11 @@
  * limitations under the License.
  */
 
-package androidx.privacysandbox.tools.apicompiler
\ No newline at end of file
+package androidx.privacysandbox.tools.apicompiler.model
+
+/** Result of parsing a Kotlin interface. */
+data class AnnotatedInterface(
+    val name: String,
+    val packageName: String,
+    val methods: List<Method> = emptyList(),
+)
\ No newline at end of file
diff --git a/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/Compiler.kt b/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/model/Method.kt
similarity index 78%
copy from privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/Compiler.kt
copy to privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/model/Method.kt
index 4f92192..ea0f619 100644
--- a/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/Compiler.kt
+++ b/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/model/Method.kt
@@ -14,4 +14,10 @@
  * limitations under the License.
  */
 
-package androidx.privacysandbox.tools.apicompiler
\ No newline at end of file
+package androidx.privacysandbox.tools.apicompiler.model
+
+data class Method(
+    val name: String,
+    val parameters: List<Parameter>,
+    val returnType: Type,
+)
\ No newline at end of file
diff --git a/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/Compiler.kt b/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/model/Parameter.kt
similarity index 83%
copy from privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/Compiler.kt
copy to privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/model/Parameter.kt
index 4f92192..6e2c29b 100644
--- a/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/Compiler.kt
+++ b/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/model/Parameter.kt
@@ -14,4 +14,9 @@
  * limitations under the License.
  */
 
-package androidx.privacysandbox.tools.apicompiler
\ No newline at end of file
+package androidx.privacysandbox.tools.apicompiler.model
+
+data class Parameter(
+    val name: String,
+    val type: Type,
+)
\ No newline at end of file
diff --git a/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/Compiler.kt b/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/model/ParsedApi.kt
similarity index 76%
copy from privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/Compiler.kt
copy to privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/model/ParsedApi.kt
index 4f92192..210f065 100644
--- a/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/Compiler.kt
+++ b/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/model/ParsedApi.kt
@@ -14,4 +14,9 @@
  * limitations under the License.
  */
 
-package androidx.privacysandbox.tools.apicompiler
\ No newline at end of file
+package androidx.privacysandbox.tools.apicompiler.model
+
+/** Result of parsing a full developer-defined API for an SDK. */
+data class ParsedApi(
+    val services: Set<AnnotatedInterface>,
+)
\ No newline at end of file
diff --git a/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/Compiler.kt b/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/model/Type.kt
similarity index 86%
rename from privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/Compiler.kt
rename to privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/model/Type.kt
index 4f92192..ae444a8 100644
--- a/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/Compiler.kt
+++ b/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/model/Type.kt
@@ -14,4 +14,8 @@
  * limitations under the License.
  */
 
-package androidx.privacysandbox.tools.apicompiler
\ No newline at end of file
+package androidx.privacysandbox.tools.apicompiler.model
+
+data class Type(
+  val name: String,
+)
\ No newline at end of file
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
new file mode 100644
index 0000000..95d1130
--- /dev/null
+++ b/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/parser/ApiParser.kt
@@ -0,0 +1,97 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.privacysandbox.tools.apicompiler.parser
+
+import androidx.privacysandbox.tools.PrivacySandboxService
+import androidx.privacysandbox.tools.apicompiler.model.ParsedApi
+import androidx.privacysandbox.tools.apicompiler.model.AnnotatedInterface
+import androidx.privacysandbox.tools.apicompiler.model.Method
+import androidx.privacysandbox.tools.apicompiler.model.Parameter
+import androidx.privacysandbox.tools.apicompiler.model.Type
+import com.google.devtools.ksp.getDeclaredFunctions
+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.KSName
+import com.google.devtools.ksp.symbol.KSType
+import com.google.devtools.ksp.symbol.KSValueParameter
+
+/** Convenience extension to get the full qualifier + name from a [KSName]. */
+internal fun KSName.getFullName(): String {
+    if (getQualifier() == "") return getShortName()
+    return "${getQualifier()}.${getShortName()}"
+}
+
+/** 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) {
+
+    fun parseApi(): ParsedApi {
+        return ParsedApi(services = parseAllServices())
+    }
+
+    private fun parseAllServices(): Set<AnnotatedInterface> {
+        val symbolsWithServiceAnnotation = resolver
+            .getSymbolsWithAnnotation(PrivacySandboxService::class.qualifiedName!!)
+        val interfacesWithServiceAnnotation = symbolsWithServiceAnnotation
+            .filterIsInstance<KSClassDeclaration>().filter { it.classKind == ClassKind.INTERFACE }
+        if (symbolsWithServiceAnnotation.count() != interfacesWithServiceAnnotation.count()) {
+            logger.error("Only interfaces can be annotated with @PrivacySandboxService.")
+            return setOf()
+        }
+        return interfacesWithServiceAnnotation
+            .map(this::parseInterface)
+            .toSet()
+    }
+
+    private fun parseInterface(classDeclaration: KSClassDeclaration): AnnotatedInterface {
+        return AnnotatedInterface(
+            name = classDeclaration.simpleName.getShortName(),
+            packageName = classDeclaration.packageName.getFullName(),
+            methods = getAllMethods(classDeclaration),
+        )
+    }
+
+    private fun getAllMethods(classDeclaration: KSClassDeclaration) =
+        classDeclaration.getDeclaredFunctions().map(::parseMethod).toList()
+
+    private fun parseMethod(method: KSFunctionDeclaration): Method {
+        return Method(
+            name = method.simpleName.getFullName(),
+            parameters = getAllParameters(method),
+            // TODO: returnType "Can be null if an error occurred during resolution".
+            returnType = parseType(method.returnType!!.resolve()),
+        )
+    }
+
+    private fun getAllParameters(method: KSFunctionDeclaration) =
+        method.parameters.map(::parseParameter).toList()
+
+    private fun parseParameter(parameter: KSValueParameter): Parameter {
+        return Parameter(
+            name = parameter.name!!.getFullName(),
+            type = parseType(parameter.type.resolve()),
+        )
+    }
+
+    private fun parseType(type: KSType): Type {
+        return Type(
+            name = type.declaration.simpleName.getFullName(),
+        )
+    }
+}
\ No newline at end of file
diff --git a/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/parser/ApiParserTest.kt b/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/parser/ApiParserTest.kt
new file mode 100644
index 0000000..a4ad823
--- /dev/null
+++ b/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/parser/ApiParserTest.kt
@@ -0,0 +1,101 @@
+/*
+ * 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.privacysandbox.tools.apicompiler.parser
+
+import androidx.privacysandbox.tools.apicompiler.model.AnnotatedInterface
+import androidx.privacysandbox.tools.apicompiler.model.Method
+import androidx.privacysandbox.tools.apicompiler.model.Parameter
+import androidx.privacysandbox.tools.apicompiler.model.ParsedApi
+import androidx.privacysandbox.tools.apicompiler.model.Type
+import androidx.privacysandbox.tools.apicompiler.util.checkSourceFails
+import androidx.privacysandbox.tools.apicompiler.util.parseSource
+import androidx.room.compiler.processing.util.Source
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@RunWith(JUnit4::class)
+class ApiParserTest {
+
+    @Test
+    fun parseServiceInterface_ok() {
+        val source =
+            Source.kotlin(
+                "com/mysdk/MySdk.kt",
+                """
+                    package com.mysdk
+                    import androidx.privacysandbox.tools.PrivacySandboxService
+                    @PrivacySandboxService
+                    interface MySdk {
+                        fun doStuff(x: Int, y: Int): String
+                    }
+                """
+            )
+        assertThat(parseSource(source)).isEqualTo(
+            ParsedApi(
+                services = mutableSetOf(
+                    AnnotatedInterface(
+                        name = "MySdk",
+                        packageName = "com.mysdk",
+                        methods = listOf(
+                            Method(
+                                name = "doStuff",
+                                parameters = listOf(
+                                    Parameter(
+                                        name = "x",
+                                        type = Type(
+                                            name = "Int",
+                                        )
+                                    ),
+                                    Parameter(
+                                        name = "y",
+                                        type = Type(
+                                            name = "Int",
+                                        )
+                                    )
+                                ),
+                                returnType = Type(
+                                    name = "String",
+                                )
+                            )
+                        )
+                    )
+                )
+            )
+        )
+    }
+
+    @Test
+    fun serviceAnnotatedClass_fails() {
+        val source =
+            Source.kotlin(
+                "com/mysdk/MySdk.kt",
+                """
+                    package com.mysdk
+                    import androidx.privacysandbox.tools.PrivacySandboxService
+                    @PrivacySandboxService
+                    abstract class MySdk {  // Fails because it's a class, not an interface.
+                        abstract fun doStuff(x: Int, y: Int): String
+                    }
+                """
+            )
+
+        checkSourceFails(source)
+            .containsError("Only interfaces can be annotated with @PrivacySandboxService.")
+    }
+}
\ No newline at end of file
diff --git a/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/util/KspTestRunner.kt b/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/util/KspTestRunner.kt
new file mode 100644
index 0000000..e2a71a8
--- /dev/null
+++ b/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/util/KspTestRunner.kt
@@ -0,0 +1,89 @@
+/*
+ * 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.privacysandbox.tools.apicompiler.util
+
+import androidx.privacysandbox.tools.apicompiler.model.ParsedApi
+import androidx.privacysandbox.tools.apicompiler.parser.ApiParser
+import androidx.room.compiler.processing.util.DiagnosticMessage
+import androidx.room.compiler.processing.util.Source
+import androidx.room.compiler.processing.util.compiler.TestCompilationArguments
+import androidx.room.compiler.processing.util.compiler.TestCompilationResult
+import androidx.room.compiler.processing.util.compiler.compile
+import com.google.common.truth.Truth.assertThat
+import com.google.devtools.ksp.processing.KSPLogger
+import com.google.devtools.ksp.processing.Resolver
+import com.google.devtools.ksp.processing.SymbolProcessor
+import com.google.devtools.ksp.processing.SymbolProcessorEnvironment
+import com.google.devtools.ksp.processing.SymbolProcessorProvider
+import com.google.devtools.ksp.symbol.KSAnnotated
+import java.nio.file.Files
+import javax.tools.Diagnostic
+
+/**
+ * Helper to run KSP processing functionality in tests.
+ */
+fun parseSource(source: Source): ParsedApi {
+    val provider = CapturingSymbolProcessor.Provider()
+    compile(
+        Files.createTempDirectory("test").toFile(),
+        TestCompilationArguments(
+            sources = listOf(source),
+            symbolProcessorProviders = listOf(provider),
+        )
+    )
+    assert(provider.processor.capture != null) { "KSP run didn't produce any output." }
+    return provider.processor.capture!!
+}
+
+fun checkSourceFails(source: Source): CompilationResultSubject {
+    val provider = CapturingSymbolProcessor.Provider()
+    val result = compile(
+        Files.createTempDirectory("test").toFile(),
+        TestCompilationArguments(
+            sources = listOf(source),
+            symbolProcessorProviders = listOf(provider)
+        )
+    )
+    assertThat(result.success).isFalse()
+    return CompilationResultSubject(result)
+}
+
+class CompilationResultSubject(private val result: TestCompilationResult) {
+    fun containsError(error: String) {
+        assertThat(result.diagnostics[Diagnostic.Kind.ERROR]?.map(DiagnosticMessage::msg))
+            .contains(error)
+    }
+}
+
+private class CapturingSymbolProcessor(private val logger: KSPLogger) : SymbolProcessor {
+    var capture: ParsedApi? = null
+
+    override fun process(resolver: Resolver): List<KSAnnotated> {
+        capture = ApiParser(resolver, logger).parseApi()
+        return emptyList()
+    }
+
+    class Provider : SymbolProcessorProvider {
+        lateinit var processor: CapturingSymbolProcessor
+
+        override fun create(environment: SymbolProcessorEnvironment): SymbolProcessor {
+            assert(!::processor.isInitialized)
+            processor = CapturingSymbolProcessor(environment.logger)
+            return processor
+        }
+    }
+}
diff --git a/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/QueryInterceptorTest.kt b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/QueryInterceptorTest.kt
index c2d6323..145d0d1 100644
--- a/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/QueryInterceptorTest.kt
+++ b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/QueryInterceptorTest.kt
@@ -158,7 +158,7 @@
             SimpleSQLiteQuery(
                 "INSERT OR ABORT INTO `queryInterceptorTestDatabase` (`id`,`description`) " +
                     "VALUES (?,?)",
-                arrayOf<Any>("3", "Description")
+                arrayOf("3", "Description")
             )
         )
         assertQueryLogged(
diff --git a/room/room-common/api/current.txt b/room/room-common/api/current.txt
index 2d17a93..c70bacd 100644
--- a/room/room-common/api/current.txt
+++ b/room/room-common/api/current.txt
@@ -403,5 +403,10 @@
     property @androidx.room.OnConflictStrategy public abstract int onConflict;
   }
 
+  @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets=kotlin.annotation.AnnotationTarget.FUNCTION) public @interface Upsert {
+    method public abstract kotlin.reflect.KClass<?> entity() default java.lang.Object;
+    property public abstract kotlin.reflect.KClass<?> entity;
+  }
+
 }
 
diff --git a/room/room-common/api/public_plus_experimental_current.txt b/room/room-common/api/public_plus_experimental_current.txt
index 2d17a93..c70bacd 100644
--- a/room/room-common/api/public_plus_experimental_current.txt
+++ b/room/room-common/api/public_plus_experimental_current.txt
@@ -403,5 +403,10 @@
     property @androidx.room.OnConflictStrategy public abstract int onConflict;
   }
 
+  @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets=kotlin.annotation.AnnotationTarget.FUNCTION) public @interface Upsert {
+    method public abstract kotlin.reflect.KClass<?> entity() default java.lang.Object;
+    property public abstract kotlin.reflect.KClass<?> entity;
+  }
+
 }
 
diff --git a/room/room-common/api/restricted_current.txt b/room/room-common/api/restricted_current.txt
index ab2db40..ddf226b 100644
--- a/room/room-common/api/restricted_current.txt
+++ b/room/room-common/api/restricted_current.txt
@@ -412,5 +412,10 @@
     property @androidx.room.OnConflictStrategy public abstract int onConflict;
   }
 
+  @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets=kotlin.annotation.AnnotationTarget.FUNCTION) public @interface Upsert {
+    method public abstract kotlin.reflect.KClass<?> entity() default java.lang.Object;
+    property public abstract kotlin.reflect.KClass<?> entity;
+  }
+
 }
 
diff --git a/room/room-common/src/main/java/androidx/room/Upsert.kt b/room/room-common/src/main/java/androidx/room/Upsert.kt
index e7d5560..117159e 100644
--- a/room/room-common/src/main/java/androidx/room/Upsert.kt
+++ b/room/room-common/src/main/java/androidx/room/Upsert.kt
@@ -16,8 +16,6 @@
 
 package androidx.room
 
-import androidx.annotation.RestrictTo
-import androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP
 import kotlin.reflect.KClass
 
 /**
@@ -84,7 +82,6 @@
  */
 @Target(AnnotationTarget.FUNCTION)
 @Retention(AnnotationRetention.BINARY)
-@RestrictTo(LIBRARY_GROUP)
 public annotation class Upsert(
 
     /**
diff --git a/room/room-compiler/build.gradle b/room/room-compiler/build.gradle
index 13d97c6..ad40553 100644
--- a/room/room-compiler/build.gradle
+++ b/room/room-compiler/build.gradle
@@ -125,7 +125,7 @@
             dir: provider {
                 // Wrapping in a provider as we access buildDir before this project is configured
                 // Replace with AGP API once it is added b/228109260
-                "${new File(project(":sqlite:sqlite").buildDir, "libJar")}"
+                "${new File(project(":sqlite:sqlite").buildDir, "intermediates/compile_library_classes_jar/release/")}"
             },
             include : "*.jar"
     ))
diff --git a/room/room-runtime/api/restricted_current.ignore b/room/room-runtime/api/restricted_current.ignore
index d53b9c9..ac20e68 100644
--- a/room/room-runtime/api/restricted_current.ignore
+++ b/room/room-runtime/api/restricted_current.ignore
@@ -1,6 +1,30 @@
 // Baseline format: 1.0
 AddedFinal: androidx.room.Room:
     Class androidx.room.Room added 'final' qualifier
+AddedFinal: androidx.room.RoomSQLiteQuery#bindBlob(int, byte[]):
+    Method androidx.room.RoomSQLiteQuery.bindBlob has added 'final' qualifier
+AddedFinal: androidx.room.RoomSQLiteQuery#bindDouble(int, double):
+    Method androidx.room.RoomSQLiteQuery.bindDouble has added 'final' qualifier
+AddedFinal: androidx.room.RoomSQLiteQuery#bindLong(int, long):
+    Method androidx.room.RoomSQLiteQuery.bindLong has added 'final' qualifier
+AddedFinal: androidx.room.RoomSQLiteQuery#bindNull(int):
+    Method androidx.room.RoomSQLiteQuery.bindNull has added 'final' qualifier
+AddedFinal: androidx.room.RoomSQLiteQuery#bindString(int, String):
+    Method androidx.room.RoomSQLiteQuery.bindString has added 'final' qualifier
+AddedFinal: androidx.room.RoomSQLiteQuery#bindTo(androidx.sqlite.db.SupportSQLiteProgram):
+    Method androidx.room.RoomSQLiteQuery.bindTo has added 'final' qualifier
+AddedFinal: androidx.room.RoomSQLiteQuery#clearBindings():
+    Method androidx.room.RoomSQLiteQuery.clearBindings has added 'final' qualifier
+AddedFinal: androidx.room.RoomSQLiteQuery#close():
+    Method androidx.room.RoomSQLiteQuery.close has added 'final' qualifier
+AddedFinal: androidx.room.RoomSQLiteQuery#copyArgumentsFrom(androidx.room.RoomSQLiteQuery):
+    Method androidx.room.RoomSQLiteQuery.copyArgumentsFrom has added 'final' qualifier
+AddedFinal: androidx.room.RoomSQLiteQuery#getArgCount():
+    Method androidx.room.RoomSQLiteQuery.getArgCount has added 'final' qualifier
+AddedFinal: androidx.room.RoomSQLiteQuery#getSql():
+    Method androidx.room.RoomSQLiteQuery.getSql has added 'final' qualifier
+AddedFinal: androidx.room.RoomSQLiteQuery#release():
+    Method androidx.room.RoomSQLiteQuery.release has added 'final' qualifier
 
 
 ChangedType: androidx.room.DatabaseConfiguration#autoMigrationSpecs:
diff --git a/room/room-runtime/api/restricted_current.txt b/room/room-runtime/api/restricted_current.txt
index aa860b1..ee903c6 100644
--- a/room/room-runtime/api/restricted_current.txt
+++ b/room/room-runtime/api/restricted_current.txt
@@ -225,28 +225,29 @@
     field public final boolean isValid;
   }
 
-  @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public class RoomSQLiteQuery implements androidx.sqlite.db.SupportSQLiteProgram androidx.sqlite.db.SupportSQLiteQuery {
-    method public static final androidx.room.RoomSQLiteQuery acquire(String query, int argumentCount);
+  @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public final class RoomSQLiteQuery implements androidx.sqlite.db.SupportSQLiteProgram androidx.sqlite.db.SupportSQLiteQuery {
+    method public static androidx.room.RoomSQLiteQuery acquire(String query, int argumentCount);
     method public void bindBlob(int index, byte[] value);
     method public void bindDouble(int index, double value);
     method public void bindLong(int index, long value);
     method public void bindNull(int index);
     method public void bindString(int index, String value);
-    method public void bindTo(androidx.sqlite.db.SupportSQLiteProgram program);
+    method public void bindTo(androidx.sqlite.db.SupportSQLiteProgram statement);
     method public void clearBindings();
     method public void close();
     method public void copyArgumentsFrom(androidx.room.RoomSQLiteQuery other);
-    method public static final androidx.room.RoomSQLiteQuery copyFrom(androidx.sqlite.db.SupportSQLiteQuery supportSQLiteQuery);
+    method public static androidx.room.RoomSQLiteQuery copyFrom(androidx.sqlite.db.SupportSQLiteQuery supportSQLiteQuery);
     method public int getArgCount();
-    method public final int getCapacity();
+    method public int getCapacity();
     method public String getSql();
     method public void init(String query, int initArgCount);
     method public void release();
+    property public int argCount;
     property public final int capacity;
+    property public String sql;
     field public static final androidx.room.RoomSQLiteQuery.Companion Companion;
     field @VisibleForTesting public static final int DESIRED_POOL_SIZE = 10; // 0xa
     field @VisibleForTesting public static final int POOL_LIMIT = 15; // 0xf
-    field @VisibleForTesting public int argCount;
     field @VisibleForTesting public final byte[]![] blobBindings;
     field @VisibleForTesting public final double[] doubleBindings;
     field @VisibleForTesting public final long[] longBindings;
diff --git a/room/room-runtime/src/main/java/androidx/room/QueryInterceptorDatabase.kt b/room/room-runtime/src/main/java/androidx/room/QueryInterceptorDatabase.kt
index 1ae68ea3..ab9a009 100644
--- a/room/room-runtime/src/main/java/androidx/room/QueryInterceptorDatabase.kt
+++ b/room/room-runtime/src/main/java/androidx/room/QueryInterceptorDatabase.kt
@@ -103,7 +103,7 @@
         val queryInterceptorProgram = QueryInterceptorProgram()
         query.bindTo(queryInterceptorProgram)
         queryCallbackExecutor.execute {
-            queryCallback.onQuery(query.getSql(), queryInterceptorProgram.bindArgsCache)
+            queryCallback.onQuery(query.sql, queryInterceptorProgram.bindArgsCache)
         }
         return delegate.query(query)
     }
diff --git a/room/room-runtime/src/main/java/androidx/room/RoomSQLiteQuery.kt b/room/room-runtime/src/main/java/androidx/room/RoomSQLiteQuery.kt
index 4c0ee9b..c13695e 100644
--- a/room/room-runtime/src/main/java/androidx/room/RoomSQLiteQuery.kt
+++ b/room/room-runtime/src/main/java/androidx/room/RoomSQLiteQuery.kt
@@ -32,7 +32,7 @@
  * @hide
  */
 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
-open class RoomSQLiteQuery private constructor(
+class RoomSQLiteQuery private constructor(
     @field:VisibleForTesting val capacity: Int
 ) : SupportSQLiteQuery, SupportSQLiteProgram {
     @Volatile
@@ -58,11 +58,10 @@
     private val bindingTypes: IntArray
 
     // number of arguments in the query
-    @JvmField
-    @VisibleForTesting
-    var argCount = 0
+    override var argCount = 0
+        private set
 
-    open fun init(query: String, initArgCount: Int) {
+    fun init(query: String, initArgCount: Int) {
         this.query = query
         argCount = initArgCount
     }
@@ -83,29 +82,24 @@
      * After released, the statement might be returned when [.acquire] is called
      * so you should never re-use it after releasing.
      */
-    open fun release() {
+    fun release() {
         synchronized(queryPool) {
             queryPool[capacity] = this
             prunePoolLocked()
         }
     }
 
-    override fun getSql(): String {
-        return requireNotNull(query)
-    }
+    override val sql: String
+        get() = checkNotNull(this.query)
 
-    override fun getArgCount(): Int {
-        return argCount
-    }
-
-    override fun bindTo(program: SupportSQLiteProgram) {
+    override fun bindTo(statement: SupportSQLiteProgram) {
         for (index in 1..argCount) {
             when (bindingTypes[index]) {
-                NULL -> program.bindNull(index)
-                LONG -> program.bindLong(index, longBindings[index])
-                DOUBLE -> program.bindDouble(index, doubleBindings[index])
-                STRING -> program.bindString(index, requireNotNull(stringBindings[index]))
-                BLOB -> program.bindBlob(index, requireNotNull(blobBindings[index]))
+                NULL -> statement.bindNull(index)
+                LONG -> statement.bindLong(index, longBindings[index])
+                DOUBLE -> statement.bindDouble(index, doubleBindings[index])
+                STRING -> statement.bindString(index, requireNotNull(stringBindings[index]))
+                BLOB -> statement.bindBlob(index, requireNotNull(blobBindings[index]))
             }
         }
     }
@@ -143,7 +137,7 @@
      *
      * @param other The other query, which holds the arguments to be copied.
      */
-    open fun copyArgumentsFrom(other: RoomSQLiteQuery) {
+    fun copyArgumentsFrom(other: RoomSQLiteQuery) {
         val argCount = other.argCount + 1 // +1 for the binding offsets
         System.arraycopy(other.bindingTypes, 0, bindingTypes, 0, argCount)
         System.arraycopy(other.longBindings, 0, longBindings, 0, argCount)
diff --git a/room/room-runtime/src/test/java/androidx/room/RoomSQLiteQueryTest.java b/room/room-runtime/src/test/java/androidx/room/RoomSQLiteQueryTest.java
index 3ff9773..0738a49 100644
--- a/room/room-runtime/src/test/java/androidx/room/RoomSQLiteQueryTest.java
+++ b/room/room-runtime/src/test/java/androidx/room/RoomSQLiteQueryTest.java
@@ -45,7 +45,7 @@
     public void acquireBasic() {
         RoomSQLiteQuery query = RoomSQLiteQuery.acquire("abc", 3);
         assertThat(query.getSql(), is("abc"));
-        assertThat(query.argCount, is(3));
+        assertThat(query.getArgCount(), is(3));
         assertThat(query.blobBindings.length, is(4));
         assertThat(query.longBindings.length, is(4));
         assertThat(query.stringBindings.length, is(4));
diff --git a/sqlite/sqlite-framework/build.gradle b/sqlite/sqlite-framework/build.gradle
index 8f170c3..657c4f3 100644
--- a/sqlite/sqlite-framework/build.gradle
+++ b/sqlite/sqlite-framework/build.gradle
@@ -25,6 +25,7 @@
 dependencies {
     api("androidx.annotation:annotation:1.2.0")
     api(project(":sqlite:sqlite"))
+    implementation(libs.kotlinStdlib)
     androidTestImplementation(libs.kotlinStdlib)
     androidTestImplementation(libs.testExtJunit)
     androidTestImplementation(libs.testCore)
diff --git a/sqlite/sqlite/api/current.txt b/sqlite/sqlite/api/current.txt
index 1014417..53bbd20 100644
--- a/sqlite/sqlite/api/current.txt
+++ b/sqlite/sqlite/api/current.txt
@@ -2,12 +2,19 @@
 package androidx.sqlite.db {
 
   public final class SimpleSQLiteQuery implements androidx.sqlite.db.SupportSQLiteQuery {
-    ctor public SimpleSQLiteQuery(String, Object![]?);
-    ctor public SimpleSQLiteQuery(String);
-    method public static void bind(androidx.sqlite.db.SupportSQLiteProgram, Object![]?);
-    method public void bindTo(androidx.sqlite.db.SupportSQLiteProgram);
+    ctor public SimpleSQLiteQuery(String query, Object![]? bindArgs);
+    ctor public SimpleSQLiteQuery(String query);
+    method public static void bind(androidx.sqlite.db.SupportSQLiteProgram statement, Object![]? bindArgs);
+    method public void bindTo(androidx.sqlite.db.SupportSQLiteProgram statement);
     method public int getArgCount();
     method public String getSql();
+    property public int argCount;
+    property public String sql;
+    field public static final androidx.sqlite.db.SimpleSQLiteQuery.Companion Companion;
+  }
+
+  public static final class SimpleSQLiteQuery.Companion {
+    method public void bind(androidx.sqlite.db.SupportSQLiteProgram statement, Object![]? bindArgs);
   }
 
   public interface SupportSQLiteDatabase extends java.io.Closeable {
@@ -94,18 +101,20 @@
   }
 
   public interface SupportSQLiteProgram extends java.io.Closeable {
-    method public void bindBlob(int, byte[]);
-    method public void bindDouble(int, double);
-    method public void bindLong(int, long);
-    method public void bindNull(int);
-    method public void bindString(int, String);
+    method public void bindBlob(int index, byte[] value);
+    method public void bindDouble(int index, double value);
+    method public void bindLong(int index, long value);
+    method public void bindNull(int index);
+    method public void bindString(int index, String value);
     method public void clearBindings();
   }
 
   public interface SupportSQLiteQuery {
-    method public void bindTo(androidx.sqlite.db.SupportSQLiteProgram);
+    method public void bindTo(androidx.sqlite.db.SupportSQLiteProgram statement);
     method public int getArgCount();
     method public String getSql();
+    property public abstract int argCount;
+    property public abstract String sql;
   }
 
   public final class SupportSQLiteQueryBuilder {
diff --git a/sqlite/sqlite/api/public_plus_experimental_current.txt b/sqlite/sqlite/api/public_plus_experimental_current.txt
index 1014417..53bbd20 100644
--- a/sqlite/sqlite/api/public_plus_experimental_current.txt
+++ b/sqlite/sqlite/api/public_plus_experimental_current.txt
@@ -2,12 +2,19 @@
 package androidx.sqlite.db {
 
   public final class SimpleSQLiteQuery implements androidx.sqlite.db.SupportSQLiteQuery {
-    ctor public SimpleSQLiteQuery(String, Object![]?);
-    ctor public SimpleSQLiteQuery(String);
-    method public static void bind(androidx.sqlite.db.SupportSQLiteProgram, Object![]?);
-    method public void bindTo(androidx.sqlite.db.SupportSQLiteProgram);
+    ctor public SimpleSQLiteQuery(String query, Object![]? bindArgs);
+    ctor public SimpleSQLiteQuery(String query);
+    method public static void bind(androidx.sqlite.db.SupportSQLiteProgram statement, Object![]? bindArgs);
+    method public void bindTo(androidx.sqlite.db.SupportSQLiteProgram statement);
     method public int getArgCount();
     method public String getSql();
+    property public int argCount;
+    property public String sql;
+    field public static final androidx.sqlite.db.SimpleSQLiteQuery.Companion Companion;
+  }
+
+  public static final class SimpleSQLiteQuery.Companion {
+    method public void bind(androidx.sqlite.db.SupportSQLiteProgram statement, Object![]? bindArgs);
   }
 
   public interface SupportSQLiteDatabase extends java.io.Closeable {
@@ -94,18 +101,20 @@
   }
 
   public interface SupportSQLiteProgram extends java.io.Closeable {
-    method public void bindBlob(int, byte[]);
-    method public void bindDouble(int, double);
-    method public void bindLong(int, long);
-    method public void bindNull(int);
-    method public void bindString(int, String);
+    method public void bindBlob(int index, byte[] value);
+    method public void bindDouble(int index, double value);
+    method public void bindLong(int index, long value);
+    method public void bindNull(int index);
+    method public void bindString(int index, String value);
     method public void clearBindings();
   }
 
   public interface SupportSQLiteQuery {
-    method public void bindTo(androidx.sqlite.db.SupportSQLiteProgram);
+    method public void bindTo(androidx.sqlite.db.SupportSQLiteProgram statement);
     method public int getArgCount();
     method public String getSql();
+    property public abstract int argCount;
+    property public abstract String sql;
   }
 
   public final class SupportSQLiteQueryBuilder {
diff --git a/sqlite/sqlite/api/restricted_current.txt b/sqlite/sqlite/api/restricted_current.txt
index 1014417..53bbd20 100644
--- a/sqlite/sqlite/api/restricted_current.txt
+++ b/sqlite/sqlite/api/restricted_current.txt
@@ -2,12 +2,19 @@
 package androidx.sqlite.db {
 
   public final class SimpleSQLiteQuery implements androidx.sqlite.db.SupportSQLiteQuery {
-    ctor public SimpleSQLiteQuery(String, Object![]?);
-    ctor public SimpleSQLiteQuery(String);
-    method public static void bind(androidx.sqlite.db.SupportSQLiteProgram, Object![]?);
-    method public void bindTo(androidx.sqlite.db.SupportSQLiteProgram);
+    ctor public SimpleSQLiteQuery(String query, Object![]? bindArgs);
+    ctor public SimpleSQLiteQuery(String query);
+    method public static void bind(androidx.sqlite.db.SupportSQLiteProgram statement, Object![]? bindArgs);
+    method public void bindTo(androidx.sqlite.db.SupportSQLiteProgram statement);
     method public int getArgCount();
     method public String getSql();
+    property public int argCount;
+    property public String sql;
+    field public static final androidx.sqlite.db.SimpleSQLiteQuery.Companion Companion;
+  }
+
+  public static final class SimpleSQLiteQuery.Companion {
+    method public void bind(androidx.sqlite.db.SupportSQLiteProgram statement, Object![]? bindArgs);
   }
 
   public interface SupportSQLiteDatabase extends java.io.Closeable {
@@ -94,18 +101,20 @@
   }
 
   public interface SupportSQLiteProgram extends java.io.Closeable {
-    method public void bindBlob(int, byte[]);
-    method public void bindDouble(int, double);
-    method public void bindLong(int, long);
-    method public void bindNull(int);
-    method public void bindString(int, String);
+    method public void bindBlob(int index, byte[] value);
+    method public void bindDouble(int index, double value);
+    method public void bindLong(int index, long value);
+    method public void bindNull(int index);
+    method public void bindString(int index, String value);
     method public void clearBindings();
   }
 
   public interface SupportSQLiteQuery {
-    method public void bindTo(androidx.sqlite.db.SupportSQLiteProgram);
+    method public void bindTo(androidx.sqlite.db.SupportSQLiteProgram statement);
     method public int getArgCount();
     method public String getSql();
+    property public abstract int argCount;
+    property public abstract String sql;
   }
 
   public final class SupportSQLiteQueryBuilder {
diff --git a/sqlite/sqlite/build.gradle b/sqlite/sqlite/build.gradle
index 63ddf32..d3f6c60 100644
--- a/sqlite/sqlite/build.gradle
+++ b/sqlite/sqlite/build.gradle
@@ -19,10 +19,12 @@
 plugins {
     id("AndroidXPlugin")
     id("com.android.library")
+    id("org.jetbrains.kotlin.android")
 }
 
 dependencies {
     api("androidx.annotation:annotation:1.0.0")
+    implementation(libs.kotlinStdlib)
     testImplementation(libs.junit)
     testImplementation(libs.mockitoCore)
 }
diff --git a/sqlite/sqlite/src/main/java/androidx/sqlite/db/SimpleSQLiteQuery.java b/sqlite/sqlite/src/main/java/androidx/sqlite/db/SimpleSQLiteQuery.java
deleted file mode 100644
index b5e3ba7..0000000
--- a/sqlite/sqlite/src/main/java/androidx/sqlite/db/SimpleSQLiteQuery.java
+++ /dev/null
@@ -1,112 +0,0 @@
-/*
- * Copyright (C) 2017 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.sqlite.db;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-
-/**
- * A basic implementation of {@link SupportSQLiteQuery} which receives a query and its args and
- * binds args based on the passed in Object type.
- */
-public final class SimpleSQLiteQuery implements SupportSQLiteQuery {
-    private final String mQuery;
-    @Nullable
-    private final Object[] mBindArgs;
-
-    /**
-     * Creates an SQL query with the sql string and the bind arguments.
-     *
-     * @param query    The query string, can include bind arguments (.e.g ?).
-     * @param bindArgs The bind argument value that will replace the placeholders in the query.
-     */
-    public SimpleSQLiteQuery(@NonNull String query, @Nullable Object[] bindArgs) {
-        mQuery = query;
-        mBindArgs = bindArgs;
-    }
-
-    /**
-     * Creates an SQL query without any bind arguments.
-     *
-     * @param query The SQL query to execute. Cannot include bind parameters.
-     */
-    public SimpleSQLiteQuery(@NonNull String query) {
-        this(query, null);
-    }
-
-    @NonNull
-    @Override
-    public String getSql() {
-        return mQuery;
-    }
-
-    @Override
-    public void bindTo(@NonNull SupportSQLiteProgram statement) {
-        bind(statement, mBindArgs);
-    }
-
-    @Override
-    public int getArgCount() {
-        return mBindArgs == null ? 0 : mBindArgs.length;
-    }
-
-    /**
-     * Binds the given arguments into the given sqlite statement.
-     *
-     * @param statement The sqlite statement
-     * @param bindArgs  The list of bind arguments
-     */
-    public static void bind(@NonNull SupportSQLiteProgram statement, @Nullable Object[] bindArgs) {
-        if (bindArgs == null) {
-            return;
-        }
-        final int limit = bindArgs.length;
-        for (int i = 0; i < limit; i++) {
-            final Object arg = bindArgs[i];
-            bind(statement, i + 1, arg);
-        }
-    }
-
-    private static void bind(SupportSQLiteProgram statement, int index, Object arg) {
-        // extracted from android.database.sqlite.SQLiteConnection
-        if (arg == null) {
-            statement.bindNull(index);
-        } else if (arg instanceof byte[]) {
-            statement.bindBlob(index, (byte[]) arg);
-        } else if (arg instanceof Float) {
-            statement.bindDouble(index, (Float) arg);
-        } else if (arg instanceof Double) {
-            statement.bindDouble(index, (Double) arg);
-        } else if (arg instanceof Long) {
-            statement.bindLong(index, (Long) arg);
-        } else if (arg instanceof Integer) {
-            statement.bindLong(index, (Integer) arg);
-        } else if (arg instanceof Short) {
-            statement.bindLong(index, (Short) arg);
-        } else if (arg instanceof Byte) {
-            statement.bindLong(index, (Byte) arg);
-        } else if (arg instanceof String) {
-            statement.bindString(index, (String) arg);
-        } else if (arg instanceof Boolean) {
-            statement.bindLong(index, ((Boolean) arg) ? 1 : 0);
-        } else {
-            throw new IllegalArgumentException("Cannot bind " + arg + " at index " + index
-                    + " Supported types: null, byte[], float, double, long, int, short, byte,"
-                    + " string");
-        }
-    }
-}
diff --git a/sqlite/sqlite/src/main/java/androidx/sqlite/db/SimpleSQLiteQuery.kt b/sqlite/sqlite/src/main/java/androidx/sqlite/db/SimpleSQLiteQuery.kt
new file mode 100644
index 0000000..2e6a9d9
--- /dev/null
+++ b/sqlite/sqlite/src/main/java/androidx/sqlite/db/SimpleSQLiteQuery.kt
@@ -0,0 +1,112 @@
+/*
+ * Copyright (C) 2017 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.sqlite.db
+
+import android.annotation.SuppressLint
+
+/**
+ * A basic implementation of [SupportSQLiteQuery] which receives a query and its args and
+ * binds args based on the passed in Object type.
+ *
+ * @constructor Creates an SQL query with the sql string and the bind arguments.
+ *
+ * @param query    The query string, can include bind arguments (.e.g ?).
+ * @param bindArgs The bind argument value that will replace the placeholders in the query.
+ */
+class SimpleSQLiteQuery(
+    private val query: String,
+    @Suppress("ArrayReturn") // Due to legacy API
+    private val bindArgs: Array<Any?>?
+    ) : SupportSQLiteQuery {
+
+    /**
+     * Creates an SQL query without any bind arguments.
+     *
+     * @param query The SQL query to execute. Cannot include bind parameters.
+     */
+    constructor(query: String) : this(query, null)
+
+    override val sql: String
+        get() = this.query
+
+    /**
+     * Creates an SQL query without any bind arguments.
+     *
+     * @param [statement] The SQL query to execute. Cannot include bind parameters.
+     */
+    override fun bindTo(statement: SupportSQLiteProgram) {
+        bind(statement, bindArgs)
+    }
+
+    override val argCount: Int
+        get() = bindArgs?.size ?: 0
+
+    companion object {
+        /**
+         * Binds the given arguments into the given sqlite statement.
+         *
+         * @param [statement] The sqlite statement
+         * @param [bindArgs]  The list of bind arguments
+         */
+        @SuppressLint("SyntheticAccessor")
+        @JvmStatic
+        fun bind(
+            statement: SupportSQLiteProgram,
+            @Suppress("ArrayReturn") // Due to legacy API
+            bindArgs: Array<Any?>?
+        ) {
+            if (bindArgs == null) {
+                return
+            }
+
+            val limit = bindArgs.size
+            for (i in 0 until limit) {
+                val arg = bindArgs[i]
+                bind(statement, i + 1, arg)
+            }
+        }
+
+        private fun bind(statement: SupportSQLiteProgram, index: Int, arg: Any?) {
+            // extracted from android.database.sqlite.SQLiteConnection
+            if (arg == null) {
+                statement.bindNull(index)
+            } else if (arg is ByteArray) {
+                statement.bindBlob(index, arg)
+            } else if (arg is Float) {
+                statement.bindDouble(index, arg.toDouble())
+            } else if (arg is Double) {
+                statement.bindDouble(index, arg)
+            } else if (arg is Long) {
+                statement.bindLong(index, arg)
+            } else if (arg is Int) {
+                statement.bindLong(index, arg.toLong())
+            } else if (arg is Short) {
+                statement.bindLong(index, arg.toLong())
+            } else if (arg is Byte) {
+                statement.bindLong(index, arg.toLong())
+            } else if (arg is String) {
+                statement.bindString(index, arg)
+            } else if (arg is Boolean) {
+                statement.bindLong(index, if (arg) 1 else 0)
+            } else {
+                throw IllegalArgumentException(
+                    "Cannot bind $arg at index $index Supported types: Null, ByteArray, " +
+                        "Float, Double, Long, Int, Short, Byte, String"
+                )
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/sqlite/sqlite/src/main/java/androidx/sqlite/db/SupportSQLiteCompat.java b/sqlite/sqlite/src/main/java/androidx/sqlite/db/SupportSQLiteCompat.java
deleted file mode 100644
index 286d449..0000000
--- a/sqlite/sqlite/src/main/java/androidx/sqlite/db/SupportSQLiteCompat.java
+++ /dev/null
@@ -1,321 +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.sqlite.db;
-
-import android.app.ActivityManager;
-import android.content.ContentResolver;
-import android.content.Context;
-import android.database.ContentObserver;
-import android.database.Cursor;
-import android.database.sqlite.SQLiteDatabase;
-import android.database.sqlite.SQLiteOpenHelper;
-import android.net.Uri;
-import android.os.Bundle;
-import android.os.CancellationSignal;
-import android.os.OperationCanceledException;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.RequiresApi;
-import androidx.annotation.RestrictTo;
-
-import java.io.File;
-import java.util.List;
-
-/**
- * Helper for accessing features in {@link SupportSQLiteOpenHelper}.
- *
- * @hide
- */
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-public final class SupportSQLiteCompat {
-    private SupportSQLiteCompat() { }
-    /**
-     * Class for accessing functions that require SDK version 16 and higher.
-     *
-     * @hide
-     */
-    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-    @RequiresApi(16)
-    public static final class Api16Impl {
-
-        /**
-         * Cancels the operation and signals the cancellation listener. If the operation has not yet
-         * started, then it will be canceled as soon as it does.
-         *
-         * @hide
-         */
-        @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-        public static void cancel(@NonNull CancellationSignal cancellationSignal) {
-            cancellationSignal.cancel();
-        }
-
-        /**
-         * Creates a cancellation signal, initially not canceled.
-         *
-         * @return a new cancellation signal
-         *
-         * @hide
-         */
-        @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-        @NonNull
-        public static CancellationSignal createCancellationSignal() {
-            return new CancellationSignal();
-        }
-
-        /**
-         * Deletes a database including its journal file and other auxiliary files
-         * that may have been created by the database engine.
-         *
-         * @param file The database file path.
-         * @return True if the database was successfully deleted.
-         *
-         * @hide
-         */
-        @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-        @SuppressWarnings("StreamFiles")
-        public static boolean deleteDatabase(@NonNull File file) {
-            return SQLiteDatabase.deleteDatabase(file);
-        }
-
-        /**
-         * Runs the provided SQL and returns a cursor over the result set.
-         *
-         * @param sql the SQL query. The SQL string must not be ; terminated
-         * @param selectionArgs You may include ?s in where clause in the query,
-         *     which will be replaced by the values from selectionArgs. The
-         *     values will be bound as Strings.
-         * @param editTable the name of the first table, which is editable
-         * @param cancellationSignal A signal to cancel the operation in progress, or null if none.
-         * If the operation is canceled, then {@link OperationCanceledException} will be thrown
-         * when the query is executed.
-         * @param cursorFactory the cursor factory to use, or null for the default factory
-         * @return A {@link Cursor} object, which is positioned before the first entry. Note that
-         * {@link Cursor}s are not synchronized, see the documentation for more details.
-         *
-         * @hide
-         */
-        @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-        @NonNull
-        public static Cursor rawQueryWithFactory(@NonNull SQLiteDatabase sQLiteDatabase,
-                @NonNull String sql, @NonNull String[] selectionArgs,
-                @NonNull String editTable, @NonNull CancellationSignal cancellationSignal,
-                @NonNull SQLiteDatabase.CursorFactory cursorFactory) {
-            return sQLiteDatabase.rawQueryWithFactory(cursorFactory, sql, selectionArgs, editTable,
-                    cancellationSignal);
-        }
-
-        /**
-         * Sets whether foreign key constraints are enabled for the database.
-         *
-         * @param enable True to enable foreign key constraints, false to disable them.
-         *
-         * @throws IllegalStateException if the are transactions is in progress
-         * when this method is called.
-         *
-         * @hide
-         */
-        @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-        public static void setForeignKeyConstraintsEnabled(@NonNull SQLiteDatabase sQLiteDatabase,
-                boolean enable) {
-            sQLiteDatabase.setForeignKeyConstraintsEnabled(enable);
-        }
-
-        /**
-         * This method disables the features enabled by
-         * {@link SQLiteDatabase#enableWriteAheadLogging()}.
-         *
-         * @throws IllegalStateException if there are transactions in progress at the
-         * time this method is called.  WAL mode can only be changed when there are no
-         * transactions in progress.
-         *
-         * @see SQLiteDatabase#enableWriteAheadLogging
-         *
-         * @hide
-         */
-        @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-        public static void disableWriteAheadLogging(@NonNull SQLiteDatabase sQLiteDatabase) {
-            sQLiteDatabase.disableWriteAheadLogging();
-        }
-
-        /**
-         * Returns true if write-ahead logging has been enabled for this database.
-         *
-         * @return True if write-ahead logging has been enabled for this database.
-         *
-         * @see SQLiteDatabase#enableWriteAheadLogging
-         * @see SQLiteDatabase#ENABLE_WRITE_AHEAD_LOGGING
-         *
-         * @hide
-         */
-        @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-        public static boolean isWriteAheadLoggingEnabled(@NonNull SQLiteDatabase sQLiteDatabase) {
-            return sQLiteDatabase.isWriteAheadLoggingEnabled();
-        }
-
-        /**
-         * Sets {@link SQLiteDatabase#ENABLE_WRITE_AHEAD_LOGGING} flag if {@code enabled} is {@code
-         * true}, unsets otherwise.
-         *
-         * @hide
-         */
-        @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-        public static void setWriteAheadLoggingEnabled(@NonNull SQLiteOpenHelper sQLiteOpenHelper,
-                boolean enabled) {
-            sQLiteOpenHelper.setWriteAheadLoggingEnabled(enabled);
-        }
-
-        private Api16Impl() {}
-    }
-
-    /**
-     * Helper for accessing functions that require SDK version 19 and higher.
-     *
-     * @hide
-     */
-    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-    @RequiresApi(19)
-    public static final class Api19Impl {
-        /**
-         * Return the URI at which notifications of changes in this Cursor's data
-         * will be delivered.
-         *
-         * @return Returns a URI that can be used with
-         * {@link ContentResolver#registerContentObserver(android.net.Uri, boolean, ContentObserver)
-         * ContentResolver.registerContentObserver} to find out about changes to this Cursor's
-         * data. May be null if no notification URI has been set.
-         *
-         * @hide
-         */
-        @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-        @NonNull
-        public static Uri getNotificationUri(@NonNull Cursor cursor) {
-            return cursor.getNotificationUri();
-        }
-
-
-        /**
-         * Returns true if this is a low-RAM device.  Exactly whether a device is low-RAM
-         * is ultimately up to the device configuration, but currently it generally means
-         * something with 1GB or less of RAM.  This is mostly intended to be used by apps
-         * to determine whether they should turn off certain features that require more RAM.
-         *
-         * @hide
-         */
-        @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-        public static boolean isLowRamDevice(@NonNull ActivityManager activityManager) {
-            return activityManager.isLowRamDevice();
-        }
-
-        private Api19Impl() {}
-    }
-
-    /**
-     * Helper for accessing functions that require SDK version 21 and higher.
-     *
-     * @hide
-     */
-    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-    @RequiresApi(21)
-    public static final class Api21Impl {
-
-        /**
-         * Returns the absolute path to the directory on the filesystem.
-         *
-         * @return The path of the directory holding application files that will not
-         *         be automatically backed up to remote storage.
-         *
-         * @hide
-         */
-        @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-        @NonNull
-        public static File getNoBackupFilesDir(@NonNull Context context) {
-            return context.getNoBackupFilesDir();
-        }
-
-        private Api21Impl() {}
-    }
-
-    /**
-     * Helper for accessing functions that require SDK version 23 and higher.
-     *
-     * @hide
-     */
-    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-    @RequiresApi(23)
-    public static final class Api23Impl {
-
-        /**
-         * Sets a {@link Bundle} that will be returned by {@link Cursor#getExtras()}.
-         *
-         * @param extras {@link Bundle} to set, or null to set an empty bundle.
-         *
-         * @hide
-         */
-        @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-        public static void setExtras(@NonNull Cursor cursor, @NonNull Bundle extras) {
-            cursor.setExtras(extras);
-        }
-
-        private Api23Impl() {}
-    }
-
-    /**
-     * Helper for accessing functions that require SDK version 29 and higher.
-     *
-     * @hide
-     */
-    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-    @RequiresApi(29)
-    public static final class Api29Impl {
-
-        /**
-         * Similar to {@link Cursor#setNotificationUri(ContentResolver, Uri)}, except this version
-         * allows to watch multiple content URIs for changes.
-         *
-         * @param cr The content resolver from the caller's context. The listener attached to
-         * this resolver will be notified.
-         * @param uris The content URIs to watch.
-         *
-         * @hide
-         */
-        @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-        public static void setNotificationUris(@NonNull Cursor cursor, @NonNull ContentResolver cr,
-                @NonNull List<Uri> uris) {
-            cursor.setNotificationUris(cr, uris);
-        }
-
-        /**
-         * Return the URIs at which notifications of changes in this Cursor's data
-         * will be delivered, as previously set by {@link #setNotificationUris}.
-         *
-         * @return Returns URIs that can be used with
-         * {@link ContentResolver#registerContentObserver(android.net.Uri, boolean, ContentObserver)
-         * ContentResolver.registerContentObserver} to find out about changes to this Cursor's
-         * data. May be null if no notification URI has been set.
-         *
-         * @hide
-         */
-        @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-        @NonNull
-        public static List<Uri> getNotificationUris(@NonNull Cursor cursor) {
-            return cursor.getNotificationUris();
-        }
-
-        private Api29Impl() {}
-    }
-
-}
diff --git a/sqlite/sqlite/src/main/java/androidx/sqlite/db/SupportSQLiteCompat.kt b/sqlite/sqlite/src/main/java/androidx/sqlite/db/SupportSQLiteCompat.kt
new file mode 100644
index 0000000..c0d7fd8
--- /dev/null
+++ b/sqlite/sqlite/src/main/java/androidx/sqlite/db/SupportSQLiteCompat.kt
@@ -0,0 +1,315 @@
+/*
+ * 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.sqlite.db
+
+import android.app.ActivityManager
+import android.content.ContentResolver
+import android.content.Context
+import android.database.Cursor
+import android.database.sqlite.SQLiteDatabase
+import android.database.sqlite.SQLiteDatabase.CursorFactory
+import android.database.sqlite.SQLiteOpenHelper
+import android.net.Uri
+import android.os.Bundle
+import android.os.CancellationSignal
+import androidx.annotation.RequiresApi
+import androidx.annotation.RestrictTo
+import java.io.File
+
+/**
+ * Helper for accessing features in [SupportSQLiteOpenHelper].
+ *
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+class SupportSQLiteCompat private constructor() {
+    /**
+     * Class for accessing functions that require SDK version 16 and higher.
+     *
+     * @hide
+     */
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    @RequiresApi(16)
+    object Api16Impl {
+        /**
+         * Cancels the operation and signals the cancellation listener. If the operation has not yet
+         * started, then it will be canceled as soon as it does.
+         *
+         * @hide
+         */
+        @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+        @JvmStatic
+        fun cancel(cancellationSignal: CancellationSignal) {
+            cancellationSignal.cancel()
+        }
+
+        /**
+         * Creates a cancellation signal, initially not canceled.
+         *
+         * @return a new cancellation signal
+         *
+         * @hide
+         */
+        @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+        @JvmStatic
+        fun createCancellationSignal(): CancellationSignal {
+            return CancellationSignal()
+        }
+
+        /**
+         * Deletes a database including its journal file and other auxiliary files
+         * that may have been created by the database engine.
+         *
+         * @param file The database file path.
+         * @return True if the database was successfully deleted.
+         *
+         * @hide
+         */
+        @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+        @JvmStatic
+        fun deleteDatabase(file: File): Boolean {
+            return SQLiteDatabase.deleteDatabase(file)
+        }
+
+        /**
+         * Runs the provided SQL and returns a cursor over the result set.
+         *
+         * @param sql the SQL query. The SQL string must not be ; terminated
+         * @param selectionArgs You may include ?s in where clause in the query,
+         * which will be replaced by the values from selectionArgs. The
+         * values will be bound as Strings.
+         * @param editTable the name of the first table, which is editable
+         * @param cancellationSignal A signal to cancel the operation in progress, or null if none.
+         * If the operation is canceled, then [OperationCanceledException] will be thrown
+         * when the query is executed.
+         * @param cursorFactory the cursor factory to use, or null for the default factory
+         * @return A [Cursor] object, which is positioned before the first entry. Note that
+         * [Cursor]s are not synchronized, see the documentation for more details.
+         *
+         * @hide
+         */
+        @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+        @JvmStatic
+        fun rawQueryWithFactory(
+            sQLiteDatabase: SQLiteDatabase,
+            sql: String,
+            selectionArgs: Array<String?>,
+            editTable: String?,
+            cancellationSignal: CancellationSignal,
+            cursorFactory: CursorFactory
+        ): Cursor {
+            return sQLiteDatabase.rawQueryWithFactory(
+                cursorFactory, sql, selectionArgs, editTable,
+                cancellationSignal
+            )
+        }
+
+        /**
+         * Sets whether foreign key constraints are enabled for the database.
+         *
+         * @param enable True to enable foreign key constraints, false to disable them.
+         *
+         * @throws [IllegalStateException] if the are transactions is in progress
+         * when this method is called.
+         *
+         * @hide
+         */
+        @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+        @JvmStatic
+        fun setForeignKeyConstraintsEnabled(
+            sQLiteDatabase: SQLiteDatabase,
+            enable: Boolean
+        ) {
+            sQLiteDatabase.setForeignKeyConstraintsEnabled(enable)
+        }
+
+        /**
+         * This method disables the features enabled by
+         * [SQLiteDatabase.enableWriteAheadLogging].
+         *
+         * @throws - if there are transactions in progress at the
+         * time this method is called.  WAL mode can only be changed when there are no
+         * transactions in progress.
+         *
+         * @hide
+         */
+        @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+        @JvmStatic
+        fun disableWriteAheadLogging(sQLiteDatabase: SQLiteDatabase) {
+            sQLiteDatabase.disableWriteAheadLogging()
+        }
+
+        /**
+         * Returns true if [SQLiteDatabase.enableWriteAheadLogging] logging has been enabled for
+         * this database.
+         *
+         * For details, see [SQLiteDatabase.ENABLE_WRITE_AHEAD_LOGGING].
+         *
+         * @return True if write-ahead logging has been enabled for this database.
+         *
+         * @hide
+         */
+        @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+        @JvmStatic
+        fun isWriteAheadLoggingEnabled(sQLiteDatabase: SQLiteDatabase): Boolean {
+            return sQLiteDatabase.isWriteAheadLoggingEnabled
+        }
+
+        /**
+         * Sets [SQLiteDatabase.ENABLE_WRITE_AHEAD_LOGGING] flag if `enabled` is `true`, unsets
+         * otherwise.
+         *
+         * @hide
+         */
+        @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+        @JvmStatic
+        fun setWriteAheadLoggingEnabled(
+            sQLiteOpenHelper: SQLiteOpenHelper,
+            enabled: Boolean
+        ) {
+            sQLiteOpenHelper.setWriteAheadLoggingEnabled(enabled)
+        }
+    }
+
+    /**
+     * Helper for accessing functions that require SDK version 19 and higher.
+     *
+     * @hide
+     */
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    @RequiresApi(19)
+    object Api19Impl {
+        /**
+         * Return the URI at which notifications of changes in this Cursor's data
+         * will be delivered.
+         *
+         * @return Returns a URI that can be used with [ContentResolver.registerContentObserver] to
+         * find out about changes to this Cursor's data. May be null if no notification URI has been
+         * set.
+         *
+         * @hide
+         */
+        @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+        @JvmStatic
+        fun getNotificationUri(cursor: Cursor): Uri {
+            return cursor.notificationUri
+        }
+
+        /**
+         * Returns true if this is a low-RAM device.  Exactly whether a device is low-RAM
+         * is ultimately up to the device configuration, but currently it generally means
+         * something with 1GB or less of RAM.  This is mostly intended to be used by apps
+         * to determine whether they should turn off certain features that require more RAM.
+         *
+         * @hide
+         */
+        @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+        @JvmStatic
+        fun isLowRamDevice(activityManager: ActivityManager): Boolean {
+            return activityManager.isLowRamDevice
+        }
+    }
+
+    /**
+     * Helper for accessing functions that require SDK version 21 and higher.
+     *
+     * @hide
+     */
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    @RequiresApi(21)
+    object Api21Impl {
+        /**
+         * Returns the absolute path to the directory on the filesystem.
+         *
+         * @return The path of the directory holding application files that will not be
+         * automatically backed up to remote storage.
+         *
+         * @hide
+         */
+        @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+        @JvmStatic
+        fun getNoBackupFilesDir(context: Context): File {
+            return context.noBackupFilesDir
+        }
+    }
+
+    /**
+     * Helper for accessing functions that require SDK version 23 and higher.
+     *
+     * @hide
+     */
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    @RequiresApi(23)
+    object Api23Impl {
+        /**
+         * Sets a [Bundle] that will be returned by [Cursor.getExtras].
+         *
+         * @param extras [Bundle] to set, or null to set an empty bundle.
+         *
+         * @hide
+         */
+        @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+        @JvmStatic
+        fun setExtras(cursor: Cursor, extras: Bundle) {
+            cursor.extras = extras
+        }
+    }
+
+    /**
+     * Helper for accessing functions that require SDK version 29 and higher.
+     *
+     * @hide
+     */
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    @RequiresApi(29)
+    object Api29Impl {
+        /**
+         * Similar to [Cursor.setNotificationUri], except this version
+         * allows to watch multiple content URIs for changes.
+         *
+         * @param cr The content resolver from the caller's context. The listener attached to
+         * this resolver will be notified.
+         * @param uris The content URIs to watch.
+         *
+         * @hide
+         */
+        @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+        @JvmStatic
+        fun setNotificationUris(
+            cursor: Cursor,
+            cr: ContentResolver,
+            uris: List<Uri?>
+        ) {
+            cursor.setNotificationUris(cr, uris)
+        }
+
+        /**
+         * Return the URIs at which notifications of changes in this Cursor's data
+         * will be delivered, as previously set by [setNotificationUris].
+         *
+         * @return Returns URIs that can be used with [ContentResolver.registerContentObserver]
+         * to find out about changes to this Cursor's data. May be null if no notification URI has
+         * been set.
+         *
+         * @hide
+         */
+        @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+        @JvmStatic
+        fun getNotificationUris(cursor: Cursor): List<Uri> {
+            return cursor.notificationUris!!
+        }
+    }
+}
\ No newline at end of file
diff --git a/sqlite/sqlite/src/main/java/androidx/sqlite/db/SupportSQLiteProgram.java b/sqlite/sqlite/src/main/java/androidx/sqlite/db/SupportSQLiteProgram.kt
similarity index 69%
rename from sqlite/sqlite/src/main/java/androidx/sqlite/db/SupportSQLiteProgram.java
rename to sqlite/sqlite/src/main/java/androidx/sqlite/db/SupportSQLiteProgram.kt
index cd0a5ef..2397f94 100644
--- a/sqlite/sqlite/src/main/java/androidx/sqlite/db/SupportSQLiteProgram.java
+++ b/sqlite/sqlite/src/main/java/androidx/sqlite/db/SupportSQLiteProgram.kt
@@ -13,65 +13,60 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+package androidx.sqlite.db
 
-package androidx.sqlite.db;
-
-import androidx.annotation.NonNull;
-
-import java.io.Closeable;
+import java.io.Closeable
 
 /**
- * An interface to map the behavior of {@link android.database.sqlite.SQLiteProgram}.
+ * An interface to map the behavior of [android.database.sqlite.SQLiteProgram].
  */
-
-@SuppressWarnings("unused")
-public interface SupportSQLiteProgram extends Closeable {
+interface SupportSQLiteProgram : Closeable {
     /**
      * Bind a NULL value to this statement. The value remains bound until
-     * {@link #clearBindings} is called.
+     * [.clearBindings] is called.
      *
      * @param index The 1-based index to the parameter to bind null to
      */
-    void bindNull(int index);
+    fun bindNull(index: Int)
 
     /**
      * Bind a long value to this statement. The value remains bound until
-     * {@link #clearBindings} is called.
-     *addToBindArgs
+     * [clearBindings] is called.
+     * addToBindArgs
      * @param index The 1-based index to the parameter to bind
      * @param value The value to bind
      */
-    void bindLong(int index, long value);
+    fun bindLong(index: Int, value: Long)
 
     /**
      * Bind a double value to this statement. The value remains bound until
-     * {@link #clearBindings} is called.
+     * [.clearBindings] is called.
      *
      * @param index The 1-based index to the parameter to bind
      * @param value The value to bind
      */
-    void bindDouble(int index, double value);
+    fun bindDouble(index: Int, value: Double)
 
     /**
      * Bind a String value to this statement. The value remains bound until
-     * {@link #clearBindings} is called.
+     * [.clearBindings] is called.
      *
      * @param index The 1-based index to the parameter to bind
      * @param value The value to bind, must not be null
      */
-    void bindString(int index, @NonNull String value);
+    fun bindString(index: Int, value: String)
 
     /**
      * Bind a byte array value to this statement. The value remains bound until
-     * {@link #clearBindings} is called.
+     * [.clearBindings] is called.
      *
      * @param index The 1-based index to the parameter to bind
      * @param value The value to bind, must not be null
      */
-    void bindBlob(int index, @NonNull byte[] value);
+    fun bindBlob(index: Int, value: ByteArray)
 
     /**
      * Clears all existing bindings. Unset bindings are treated as NULL.
      */
-    void clearBindings();
-}
+    fun clearBindings()
+}
\ No newline at end of file
diff --git a/sqlite/sqlite/src/main/java/androidx/sqlite/db/SupportSQLiteQuery.java b/sqlite/sqlite/src/main/java/androidx/sqlite/db/SupportSQLiteQuery.kt
similarity index 67%
rename from sqlite/sqlite/src/main/java/androidx/sqlite/db/SupportSQLiteQuery.java
rename to sqlite/sqlite/src/main/java/androidx/sqlite/db/SupportSQLiteQuery.kt
index 82909fc..8108f2d 100644
--- a/sqlite/sqlite/src/main/java/androidx/sqlite/db/SupportSQLiteQuery.java
+++ b/sqlite/sqlite/src/main/java/androidx/sqlite/db/SupportSQLiteQuery.kt
@@ -13,37 +13,29 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-
-package androidx.sqlite.db;
-
-import androidx.annotation.NonNull;
+package androidx.sqlite.db
 
 /**
  * A query with typed bindings. It is better to use this API instead of
- * {@link android.database.sqlite.SQLiteDatabase#rawQuery(String, String[])} because it allows
+ * [android.database.sqlite.SQLiteDatabase.rawQuery] because it allows
  * binding type safe parameters.
  */
-public interface SupportSQLiteQuery {
+interface SupportSQLiteQuery {
     /**
      * The SQL query. This query can have placeholders(?) for bind arguments.
-     *
-     * @return The SQL query to compile
      */
-    @NonNull
-    String getSql();
+    val sql: String
 
     /**
      * Callback to bind the query parameters to the compiled statement.
      *
      * @param statement The compiled statement
      */
-    void bindTo(@NonNull SupportSQLiteProgram statement);
+    fun bindTo(statement: SupportSQLiteProgram)
 
     /**
-     * Returns the number of arguments in this query. This is equal to the number of placeholders
+     * Is the number of arguments in this query. This is equal to the number of placeholders
      * in the query string. See: https://www.sqlite.org/c3ref/bind_blob.html for details.
-     *
-     * @return The number of arguments in the query.
      */
-    int getArgCount();
-}
+    val argCount: Int
+}
\ No newline at end of file
diff --git a/sqlite/sqlite/src/main/java/androidx/sqlite/db/SupportSQLiteStatement.java b/sqlite/sqlite/src/main/java/androidx/sqlite/db/SupportSQLiteStatement.kt
similarity index 66%
rename from sqlite/sqlite/src/main/java/androidx/sqlite/db/SupportSQLiteStatement.java
rename to sqlite/sqlite/src/main/java/androidx/sqlite/db/SupportSQLiteStatement.kt
index ed00b46..097ffb5 100644
--- a/sqlite/sqlite/src/main/java/androidx/sqlite/db/SupportSQLiteStatement.java
+++ b/sqlite/sqlite/src/main/java/androidx/sqlite/db/SupportSQLiteStatement.kt
@@ -13,34 +13,30 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-
-package androidx.sqlite.db;
-
-import androidx.annotation.Nullable;
+package androidx.sqlite.db
 
 /**
- * An interface to map the behavior of {@link android.database.sqlite.SQLiteStatement}.
+ * An interface to map the behavior of [android.database.sqlite.SQLiteStatement].
  */
-@SuppressWarnings("unused")
-public interface SupportSQLiteStatement extends SupportSQLiteProgram {
+interface SupportSQLiteStatement : SupportSQLiteProgram {
     /**
      * Execute this SQL statement, if it is not a SELECT / INSERT / DELETE / UPDATE, for example
      * CREATE / DROP table, view, trigger, index etc.
      *
-     * @throws android.database.SQLException If the SQL string is invalid for
-     *         some reason
+     * @throws [android.database.SQLException] If the SQL string is invalid for
+     * some reason
      */
-    void execute();
+    fun execute()
 
     /**
      * Execute this SQL statement, if the the number of rows affected by execution of this SQL
      * statement is of any importance to the caller - for example, UPDATE / DELETE SQL statements.
      *
      * @return the number of rows affected by this SQL statement execution.
-     * @throws android.database.SQLException If the SQL string is invalid for
-     *         some reason
+     * @throws [android.database.SQLException] If the SQL string is invalid for
+     * some reason
      */
-    int executeUpdateDelete();
+    fun executeUpdateDelete(): Int
 
     /**
      * Execute this SQL statement and return the ID of the row inserted due to this call.
@@ -48,10 +44,10 @@
      *
      * @return the row ID of the last row inserted, if this insert is successful. -1 otherwise.
      *
-     * @throws android.database.SQLException If the SQL string is invalid for
-     *         some reason
+     * @throws [android.database.SQLException] If the SQL string is invalid for
+     * some reason
      */
-    long executeInsert();
+    fun executeInsert(): Long
 
     /**
      * Execute a statement that returns a 1 by 1 table with a numeric value.
@@ -59,17 +55,17 @@
      *
      * @return The result of the query.
      *
-     * @throws android.database.sqlite.SQLiteDoneException if the query returns zero rows
+     * @throws [android.database.sqlite.SQLiteDoneException] if the query returns zero rows
      */
-    long simpleQueryForLong();
+    fun simpleQueryForLong(): Long
+
     /**
      * Execute a statement that returns a 1 by 1 table with a text value.
      * For example, SELECT COUNT(*) FROM table;
      *
      * @return The result of the query.
      *
-     * @throws android.database.sqlite.SQLiteDoneException if the query returns zero rows
+     * @throws [android.database.sqlite.SQLiteDoneException] if the query returns zero rows
      */
-    @Nullable
-    String simpleQueryForString();
-}
+    fun simpleQueryForString(): String?
+}
\ No newline at end of file
diff --git a/sqlite/sqlite/src/test/java/androidx/sqlite/db/SimpleSQLiteQueryTest.kt b/sqlite/sqlite/src/test/java/androidx/sqlite/db/SimpleSQLiteQueryTest.kt
new file mode 100644
index 0000000..bf6edb4
--- /dev/null
+++ b/sqlite/sqlite/src/test/java/androidx/sqlite/db/SimpleSQLiteQueryTest.kt
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package androidx.sqlite.db
+import org.hamcrest.CoreMatchers.`is`
+import org.hamcrest.MatcherAssert.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import org.mockito.Mockito
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.verifyNoMoreInteractions
+@RunWith(JUnit4::class)
+class SimpleSQLiteQueryTest {
+    @Test
+    fun sql() {
+        val query = SimpleSQLiteQuery("foo", emptyArray())
+        assertThat(query.sql, `is`("foo"))
+    }
+    @Test
+    fun bindTo_noArgs() {
+        val query = SimpleSQLiteQuery("foo", emptyArray())
+        val program: SupportSQLiteProgram = Mockito.mock(SupportSQLiteProgram::class.java)
+        query.bindTo(program)
+        verifyNoMoreInteractions(program)
+    }
+    @Test
+    fun bindTo_withArgs() {
+        val bytes = ByteArray(3)
+        val query = SimpleSQLiteQuery("foo", arrayOf("bar", 2, true, 0.5f, null, bytes))
+        val program: SupportSQLiteProgram = Mockito.mock(SupportSQLiteProgram::class.java)
+        query.bindTo(program)
+        verify(program).bindString(1, "bar")
+        verify(program).bindLong(2, 2)
+        verify(program).bindLong(3, 1)
+        verify(program).bindDouble(
+            4,
+            (0.5f).toDouble()
+        )
+        verify(program).bindNull(5)
+        verify(program).bindBlob(6, bytes)
+        verifyNoMoreInteractions(program)
+    }
+    @Test
+    fun argCount_withArgs() {
+        val query = SimpleSQLiteQuery("foo", arrayOf("bar", 2, true))
+        assertThat(query.argCount, `is`(3))
+    }
+    @Test
+    fun argCount_noArgs() {
+        val query = SimpleSQLiteQuery("foo", emptyArray())
+        assertThat(query.argCount, `is`(0))
+    }
+}
\ No newline at end of file
diff --git a/sqlite/sqlite/src/test/java/androidx/sqlite/db/SimpleSQLiteQueryTestTest.java b/sqlite/sqlite/src/test/java/androidx/sqlite/db/SimpleSQLiteQueryTestTest.java
deleted file mode 100644
index 7b04a2b..0000000
--- a/sqlite/sqlite/src/test/java/androidx/sqlite/db/SimpleSQLiteQueryTestTest.java
+++ /dev/null
@@ -1,74 +0,0 @@
-/*
- * Copyright 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.sqlite.db;
-
-import static org.hamcrest.CoreMatchers.is;
-import static org.hamcrest.MatcherAssert.assertThat;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.verifyNoMoreInteractions;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.junit.runners.JUnit4;
-import org.mockito.Mockito;
-
-@RunWith(JUnit4.class)
-public class SimpleSQLiteQueryTestTest {
-
-    @Test
-    public void getSql() {
-        SimpleSQLiteQuery query = new SimpleSQLiteQuery("foo");
-        assertThat(query.getSql(), is("foo"));
-    }
-
-    @Test
-    public void bindTo_noArgs() {
-        SimpleSQLiteQuery query = new SimpleSQLiteQuery("foo");
-        SupportSQLiteProgram program = Mockito.mock(SupportSQLiteProgram.class);
-        query.bindTo(program);
-        verifyNoMoreInteractions(program);
-    }
-
-    @Test
-    public void bindTo_withArgs() {
-        byte[] bytes = new byte[3];
-        SimpleSQLiteQuery query = new SimpleSQLiteQuery("foo",
-                new Object[]{"bar", 2, true, .5f, null, bytes});
-        SupportSQLiteProgram program = Mockito.mock(SupportSQLiteProgram.class);
-        query.bindTo(program);
-        verify(program).bindString(1, "bar");
-        verify(program).bindLong(2, 2);
-        verify(program).bindLong(3, 1);
-        verify(program).bindDouble(4, .5f);
-        verify(program).bindNull(5);
-        verify(program).bindBlob(6, bytes);
-        verifyNoMoreInteractions(program);
-    }
-
-    @Test
-    public void getArgCount_withArgs() {
-        SimpleSQLiteQuery query = new SimpleSQLiteQuery("foo",
-                new Object[]{"bar", 2, true});
-        assertThat(query.getArgCount(), is(3));
-    }
-
-    @Test
-    public void getArgCount_noArgs() {
-        SimpleSQLiteQuery query = new SimpleSQLiteQuery("foo");
-        assertThat(query.getArgCount(), is(0));
-    }
-}
diff --git a/test/uiautomator/integration-tests/testapp/src/androidTest/java/androidx/test/uiautomator/testapp/UiObject2Tests.java b/test/uiautomator/integration-tests/testapp/src/androidTest/java/androidx/test/uiautomator/testapp/UiObject2Tests.java
index da46fd6..fa4f09d 100644
--- a/test/uiautomator/integration-tests/testapp/src/androidTest/java/androidx/test/uiautomator/testapp/UiObject2Tests.java
+++ b/test/uiautomator/integration-tests/testapp/src/androidTest/java/androidx/test/uiautomator/testapp/UiObject2Tests.java
@@ -494,52 +494,52 @@
 
     @Test
     public void testPinchClose() {
-        launchTestActivity(UiObject2TestPinchActivity.class);
+        launchTestActivity(PinchTestActivity.class);
 
         UiObject2 pinchArea = mDevice.findObject(By.res(TEST_APP, "pinch_area"));
         UiObject2 scaleText = pinchArea.findObject(By.res(TEST_APP, "scale_factor"));
         pinchArea.pinchClose(1f);
         scaleText.wait(Until.textNotEquals("1.0f"), TIMEOUT_MS);
-        float scaleValueAfterPinch = Float.valueOf(scaleText.getText());
+        float scaleValueAfterPinch = Float.parseFloat(scaleText.getText());
         assertTrue(String.format("Expected scale value to be less than 1f after pinchClose(), "
                 + "but got [%f]", scaleValueAfterPinch), scaleValueAfterPinch < 1f);
     }
 
     @Test
     public void testPinchClose_withSpeed() {
-        launchTestActivity(UiObject2TestPinchActivity.class);
+        launchTestActivity(PinchTestActivity.class);
 
         UiObject2 pinchArea = mDevice.findObject(By.res(TEST_APP, "pinch_area"));
         UiObject2 scaleText = pinchArea.findObject(By.res(TEST_APP, "scale_factor"));
         pinchArea.pinchClose(.75f, SPEED_MS);
         scaleText.wait(Until.textNotEquals("1.0f"), TIMEOUT_MS);
-        float scaleValueAfterPinch = Float.valueOf(scaleText.getText());
+        float scaleValueAfterPinch = Float.parseFloat(scaleText.getText());
         assertTrue(String.format("Expected scale value to be less than 1f after pinchClose(), "
                 + "but got [%f]", scaleValueAfterPinch), scaleValueAfterPinch < 1f);
     }
 
     @Test
     public void testPinchOpen() {
-        launchTestActivity(UiObject2TestPinchActivity.class);
+        launchTestActivity(PinchTestActivity.class);
 
         UiObject2 pinchArea = mDevice.findObject(By.res(TEST_APP, "pinch_area"));
         UiObject2 scaleText = pinchArea.findObject(By.res(TEST_APP, "scale_factor"));
         pinchArea.pinchOpen(.5f);
         scaleText.wait(Until.textNotEquals("1.0f"), TIMEOUT_MS);
-        float scaleValueAfterPinch = Float.valueOf(scaleText.getText());
+        float scaleValueAfterPinch = Float.parseFloat(scaleText.getText());
         assertTrue(String.format("Expected scale text to be greater than 1f after pinchOpen(), "
                 + "but got [%f]", scaleValueAfterPinch), scaleValueAfterPinch > 1f);
     }
 
     @Test
     public void testPinchOpen_withSpeed() {
-        launchTestActivity(UiObject2TestPinchActivity.class);
+        launchTestActivity(PinchTestActivity.class);
 
         UiObject2 pinchArea = mDevice.findObject(By.res(TEST_APP, "pinch_area"));
         UiObject2 scaleText = pinchArea.findObject(By.res(TEST_APP, "scale_factor"));
         pinchArea.pinchOpen(.25f, SPEED_MS);
         scaleText.wait(Until.textNotEquals("1.0f"), TIMEOUT_MS);
-        float scaleValueAfterPinch = Float.valueOf(scaleText.getText());
+        float scaleValueAfterPinch = Float.parseFloat(scaleText.getText());
         assertTrue(String.format("Expected scale text to be greater than 1f after pinchOpen(), "
                 + "but got [%f]", scaleValueAfterPinch), scaleValueAfterPinch > 1f);
     }
@@ -683,7 +683,7 @@
 
     @Test
     public void testSetGestureMargin() {
-        launchTestActivity(UiObject2TestPinchActivity.class);
+        launchTestActivity(PinchTestActivity.class);
 
         UiObject2 pinchArea = mDevice.findObject(By.res(TEST_APP, "pinch_area"));
         UiObject2 scaleText = pinchArea.findObject(By.res(TEST_APP, "scale_factor"));
@@ -711,7 +711,7 @@
 
     @Test
     public void testSetGestureMargins() {
-        launchTestActivity(UiObject2TestPinchActivity.class);
+        launchTestActivity(PinchTestActivity.class);
 
         UiObject2 pinchArea = mDevice.findObject(By.res(TEST_APP, "pinch_area"));
         UiObject2 scaleText = pinchArea.findObject(By.res(TEST_APP, "scale_factor"));
diff --git a/test/uiautomator/integration-tests/testapp/src/androidTest/java/androidx/test/uiautomator/testapp/UiObjectTest.java b/test/uiautomator/integration-tests/testapp/src/androidTest/java/androidx/test/uiautomator/testapp/UiObjectTest.java
index 9b898d5..7d76076 100644
--- a/test/uiautomator/integration-tests/testapp/src/androidTest/java/androidx/test/uiautomator/testapp/UiObjectTest.java
+++ b/test/uiautomator/integration-tests/testapp/src/androidTest/java/androidx/test/uiautomator/testapp/UiObjectTest.java
@@ -625,12 +625,61 @@
         assertFalse(text3.exists());
     }
 
+    @Test
+    public void testPinchOut() throws Exception {
+        launchTestActivity(PinchTestActivity.class);
+
+        UiObject pinchArea = mDevice.findObject(new UiSelector().resourceId(TEST_APP + ":id"
+                + "/pinch_area"));
+        UiObject scaleText = mDevice.findObject(new UiSelector().resourceId(TEST_APP + ":id"
+                + "/scale_factor"));
+
+        UiObject expectedScaleText = mDevice.findObject(new UiSelector().resourceId(TEST_APP + ":id"
+                + "/scale_factor").text("1.0f"));
+
+        assertTrue(pinchArea.pinchOut(100, 10));
+        assertTrue(expectedScaleText.waitUntilGone(TIMEOUT_MS));
+        float scaleValueAfterPinch = Float.parseFloat(scaleText.getText());
+        assertTrue(String.format(
+                "Expected scale text to be greater than 1f after pinchOut(), but got [%f]",
+                scaleValueAfterPinch), scaleValueAfterPinch > 1f);
+    }
+
+    @Test
+    public void testPinchIn() throws Exception {
+        launchTestActivity(PinchTestActivity.class);
+
+        UiObject pinchArea = mDevice.findObject(new UiSelector().resourceId(TEST_APP + ":id"
+                + "/pinch_area"));
+        UiObject scaleText = mDevice.findObject(new UiSelector().resourceId(TEST_APP + ":id"
+                + "/scale_factor"));
+
+        UiObject expectedScaleText = mDevice.findObject(new UiSelector().resourceId(TEST_APP + ":id"
+                + "/scale_factor").text("1.0f"));
+
+        assertTrue(pinchArea.pinchIn(100, 10));
+        assertTrue(expectedScaleText.waitUntilGone(TIMEOUT_MS));
+        float scaleValueAfterPinch = Float.parseFloat(scaleText.getText());
+        assertTrue(String.format("Expected scale value to be less than 1f after pinchIn(), "
+                + "but got [%f]", scaleValueAfterPinch), scaleValueAfterPinch < 1f);
+    }
+
+    @Test
+    public void testPinchFamily_throwsExceptions() {
+        launchTestActivity(PinchTestActivity.class);
+
+        UiObject noNode = mDevice.findObject(new UiSelector().resourceId(TEST_APP + ":id/no_node"));
+        UiObject smallArea = mDevice.findObject(new UiSelector().resourceId(TEST_APP + ":id"
+                + "/small_area"));
+
+        assertThrows(UiObjectNotFoundException.class, () -> noNode.pinchOut(100, 10));
+        assertThrows(UiObjectNotFoundException.class, () -> noNode.pinchIn(100, 10));
+        assertThrows(IllegalStateException.class, () -> smallArea.pinchOut(100, 10));
+        assertThrows(IllegalStateException.class, () -> smallArea.pinchIn(100, 10));
+    }
+
     /* TODO(b/241158642): Implement these tests, and the tests for exceptions of each tested method.
 
-    public void testPinchOut() {}
-
-    public void testPinchIn() {}
-
     public void testPerformTwoPointerGesture() {}
 
     public void testPerformMultiPointerGesture() {}
diff --git a/test/uiautomator/integration-tests/testapp/src/main/AndroidManifest.xml b/test/uiautomator/integration-tests/testapp/src/main/AndroidManifest.xml
index 521b327..2e3d6d6 100644
--- a/test/uiautomator/integration-tests/testapp/src/main/AndroidManifest.xml
+++ b/test/uiautomator/integration-tests/testapp/src/main/AndroidManifest.xml
@@ -127,6 +127,13 @@
                 <action android:name="android.intent.action.MAIN" />
             </intent-filter>
         </activity>
+        <activity android:name=".PinchTestActivity"
+            android:exported="true"
+            android:theme="@android:style/Theme.Holo.NoActionBar">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+            </intent-filter>
+        </activity>
         <activity android:name=".SplitScreenTestActivity"
             android:exported="true"
             android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation"
@@ -157,13 +164,6 @@
                 <action android:name="android.intent.action.MAIN" />
             </intent-filter>
         </activity>
-        <activity android:name=".UiObject2TestPinchActivity"
-            android:exported="true"
-            android:theme="@android:style/Theme.Holo.NoActionBar">
-            <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-            </intent-filter>
-        </activity>
         <activity android:name=".UntilTestActivity"
             android:exported="true"
             android:theme="@android:style/Theme.Holo.NoActionBar">
diff --git a/test/uiautomator/integration-tests/testapp/src/main/java/androidx/test/uiautomator/testapp/PinchTestActivity.java b/test/uiautomator/integration-tests/testapp/src/main/java/androidx/test/uiautomator/testapp/PinchTestActivity.java
new file mode 100644
index 0000000..3c30df3
--- /dev/null
+++ b/test/uiautomator/integration-tests/testapp/src/main/java/androidx/test/uiautomator/testapp/PinchTestActivity.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2014 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.test.uiautomator.testapp;
+
+import android.app.Activity;
+import android.os.Bundle;
+import android.view.MotionEvent;
+import android.view.ScaleGestureDetector;
+import android.view.ScaleGestureDetector.SimpleOnScaleGestureListener;
+import android.widget.TextView;
+
+import androidx.annotation.Nullable;
+
+public class PinchTestActivity extends Activity {
+
+    private ScaleGestureDetector mScaleDetector;
+
+    @Override
+    public void onCreate(@Nullable Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+
+        setContentView(R.layout.pinch_test_activity);
+
+        final TextView scaleFactor = findViewById(R.id.scale_factor);
+
+        mScaleDetector = new ScaleGestureDetector(this, new SimpleOnScaleGestureListener() {
+            @Override
+            public void onScaleEnd(ScaleGestureDetector detector) {
+                float scale = detector.getScaleFactor();
+                scaleFactor.setText(Float.toString(scale));
+            }
+        });
+    }
+
+    @Override
+    public boolean onTouchEvent(MotionEvent event) {
+        return mScaleDetector.onTouchEvent(event);
+    }
+}
diff --git a/test/uiautomator/integration-tests/testapp/src/main/java/androidx/test/uiautomator/testapp/UiObject2TestPinchActivity.java b/test/uiautomator/integration-tests/testapp/src/main/java/androidx/test/uiautomator/testapp/UiObject2TestPinchActivity.java
deleted file mode 100644
index d205048..0000000
--- a/test/uiautomator/integration-tests/testapp/src/main/java/androidx/test/uiautomator/testapp/UiObject2TestPinchActivity.java
+++ /dev/null
@@ -1,71 +0,0 @@
-/*
- * Copyright (C) 2014 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.test.uiautomator.testapp;
-
-import android.app.Activity;
-import android.os.Bundle;
-import android.util.Log;
-import android.view.MotionEvent;
-import android.view.ScaleGestureDetector;
-import android.view.ScaleGestureDetector.SimpleOnScaleGestureListener;
-import android.widget.TextView;
-
-import androidx.annotation.Nullable;
-
-public class UiObject2TestPinchActivity extends Activity {
-
-    private ScaleGestureDetector mScaleDetector;
-
-    @Override
-    public void onCreate(@Nullable Bundle savedInstanceState) {
-        super.onCreate(savedInstanceState);
-
-        setContentView(R.layout.uiobject2_testpinch_activity);
-
-        final TextView scaleFactor = (TextView)findViewById(R.id.scale_factor);
-
-        mScaleDetector = new ScaleGestureDetector(this, new SimpleOnScaleGestureListener() {
-            @Override
-            public boolean onScaleBegin(ScaleGestureDetector detector) {
-                float scale = detector.getScaleFactor();
-                Log.d("FOO", String.format("Beginning scale: %s", scale));
-                float span = detector.getCurrentSpan();
-                Log.d("FOO", String.format("Beginning span: %s", span));
-                Log.d("FOO", String.format("Beginning span, X: %s, Y:%s", detector.getCurrentSpanX(), detector.getCurrentSpanY()));
-                Log.d("FOO", String.format("Beginning focus: %s, %s", detector.getFocusX(), detector.getFocusY()));
-                return true;
-
-            }
-
-            @Override
-            public void onScaleEnd(ScaleGestureDetector detector) {
-                float scale = detector.getScaleFactor();
-                Log.d("FOO", String.format("Ending scale: %s", scale));
-                float span = detector.getCurrentSpan();
-                Log.d("FOO", String.format("Ending span: %s", span));
-                Log.d("FOO", String.format("Ending span, X: %s, Y:%s", detector.getCurrentSpanX(), detector.getCurrentSpanY()));
-                Log.d("FOO", String.format("Ending focus: %s, %s", detector.getFocusX(), detector.getFocusY()));
-                scaleFactor.setText(Float.toString(scale));
-            }
-        });
-    }
-
-    @Override
-    public boolean onTouchEvent(MotionEvent event) {
-        return mScaleDetector.onTouchEvent(event);
-    }
-}
diff --git a/test/uiautomator/integration-tests/testapp/src/main/res/layout/uiobject2_testpinch_activity.xml b/test/uiautomator/integration-tests/testapp/src/main/res/layout/pinch_test_activity.xml
similarity index 86%
rename from test/uiautomator/integration-tests/testapp/src/main/res/layout/uiobject2_testpinch_activity.xml
rename to test/uiautomator/integration-tests/testapp/src/main/res/layout/pinch_test_activity.xml
index 5f1ecbd..f952013 100644
--- a/test/uiautomator/integration-tests/testapp/src/main/res/layout/uiobject2_testpinch_activity.xml
+++ b/test/uiautomator/integration-tests/testapp/src/main/res/layout/pinch_test_activity.xml
@@ -18,7 +18,7 @@
     android:layout_width="match_parent"
     android:layout_height="match_parent"
     android:orientation="vertical"
-    tools:context=".UiObject2TestPinchActivity">
+    tools:context=".PinchTestActivity">
 
     <LinearLayout
         android:id="@+id/pinch_area"
@@ -35,6 +35,12 @@
             android:text="1.0f"
             android:textColor="@android:color/white" />
 
+        <TextView
+            android:id="@+id/small_area"
+            android:layout_width="1dp"
+            android:layout_height="1dp"
+            android:text="small area" />
+
     </LinearLayout>
 
 </LinearLayout>
diff --git a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/AccessibilityNodeInfoHelper.java b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/AccessibilityNodeInfoHelper.java
index 7f8f775..5c82419 100644
--- a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/AccessibilityNodeInfoHelper.java
+++ b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/AccessibilityNodeInfoHelper.java
@@ -22,53 +22,58 @@
 import android.view.accessibility.AccessibilityWindowInfo;
 
 import androidx.annotation.DoNotInline;
+import androidx.annotation.NonNull;
 import androidx.annotation.RequiresApi;
 
-/**
- * This class contains static helper methods to work with
- * {@link AccessibilityNodeInfo}
- */
+/** Static helper methods for working with {@link AccessibilityNodeInfo}s. */
 class AccessibilityNodeInfoHelper {
 
     private AccessibilityNodeInfoHelper() {}
 
     /**
-     * Returns the node's bounds clipped to the size of the display
+     * Returns the visible bounds of an {@link AccessibilityNodeInfo}.
      *
-     * @param node
-     * @param width pixel width of the display
-     * @param height pixel height of the display
-     * @return null if node is null, else a Rect containing visible bounds
+     * @param node   node to analyze
+     * @param width  display width in pixels
+     * @param height display height in pixels
+     * @return {@link Rect} containing the visible bounds
      */
-    @SuppressWarnings("RectIntersectReturnValueIgnored")
-    static Rect getVisibleBoundsInScreen(AccessibilityNodeInfo node, int width, int height) {
-        if (node == null) {
-            return null;
-        }
-        // targeted node's bounds
-        Rect nodeRect = new Rect();
-        node.getBoundsInScreen(nodeRect);
+    @NonNull
+    static Rect getVisibleBoundsInScreen(@NonNull AccessibilityNodeInfo node, int width,
+            int height) {
+        Rect nodeBounds = new Rect();
+        node.getBoundsInScreen(nodeBounds);
 
-        Rect displayRect = new Rect();
-        displayRect.top = 0;
-        displayRect.left = 0;
-        displayRect.right = width;
-        displayRect.bottom = height;
+        // Trim portions that are outside the specified display bounds.
+        Rect displayBounds = new Rect(0, 0, width, height);
+        nodeBounds = intersect(nodeBounds, displayBounds);
 
-        nodeRect.intersect(displayRect);
-
-        // On platforms that give us access to the node's window
+        // Trim portions that are outside the window bounds on API 21+.
         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
-            // Trim any portion of the bounds that are outside the window
-            Rect bounds = new Rect();
+            Rect windowBounds = new Rect();
             AccessibilityWindowInfo window = Api21Impl.getWindow(node);
             if (window != null) {
-                Api21Impl.getBoundsInScreen(window, bounds);
-                nodeRect.intersect(bounds);
+                Api21Impl.getBoundsInScreen(window, windowBounds);
+                nodeBounds = intersect(nodeBounds, windowBounds);
             }
         }
 
-        return nodeRect;
+        // Trim portions that are outside the first scrollable ancestor.
+        for (AccessibilityNodeInfo ancestor = node.getParent(); ancestor != null;
+                ancestor = ancestor.getParent()) {
+            if (ancestor.isScrollable()) {
+                Rect ancestorBounds = getVisibleBoundsInScreen(ancestor, width, height);
+                nodeBounds = intersect(nodeBounds, ancestorBounds);
+                break;
+            }
+        }
+
+        return nodeBounds;
+    }
+
+    /** Returns the intersection of two rectangles, or an empty rectangle if they do not overlap. */
+    private static Rect intersect(Rect first, Rect second) {
+        return first.intersect(second) ? first : new Rect();
     }
 
     @RequiresApi(21)
diff --git a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/UiObject.java b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/UiObject.java
index 22c1a0f..0db19ba 100644
--- a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/UiObject.java
+++ b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/UiObject.java
@@ -26,6 +26,8 @@
 import android.view.MotionEvent.PointerCoords;
 import android.view.accessibility.AccessibilityNodeInfo;
 
+import androidx.annotation.NonNull;
+
 /**
  * A UiObject is a representation of a view. It is not in any way directly bound to a
  * view as an object reference. A UiObject contains information to help it
@@ -350,56 +352,12 @@
                 rect.centerY(), rect.right - SWIPE_MARGIN_LIMIT, rect.centerY(), steps);
     }
 
-    /**
-     * Finds the visible bounds of a partially visible UI element
-     *
-     * @param node
-     * @return null if node is null, else a Rect containing visible bounds
-     */
-    @SuppressWarnings("RectIntersectReturnValueIgnored")
-    private Rect getVisibleBounds(AccessibilityNodeInfo node) {
-        if (node == null) {
-            return null;
-        }
-
-        // targeted node's bounds
+    /** Returns the visible bounds of an {@link AccessibilityNodeInfo}. */
+    @NonNull
+    private Rect getVisibleBounds(@NonNull AccessibilityNodeInfo node) {
         int w = getDevice().getDisplayWidth();
         int h = getDevice().getDisplayHeight();
-        Rect nodeRect = AccessibilityNodeInfoHelper.getVisibleBoundsInScreen(node, w, h);
-
-        // is the targeted node within a scrollable container?
-        AccessibilityNodeInfo scrollableParentNode = getScrollableParent(node);
-        if(scrollableParentNode == null) {
-            // nothing to adjust for so return the node's Rect as is
-            return nodeRect;
-        }
-
-        // Scrollable parent's visible bounds
-        Rect parentRect = AccessibilityNodeInfoHelper
-                .getVisibleBoundsInScreen(scrollableParentNode, w, h);
-        // adjust for partial clipping of targeted by parent node if required
-        nodeRect.intersect(parentRect);
-        return nodeRect;
-    }
-
-    /**
-     * Walks up the layout hierarchy to find a scrollable parent. A scrollable parent
-     * indicates that this node might be in a container where it is partially
-     * visible due to scrolling. In this case, its clickable center might not be visible and
-     * the click coordinates should be adjusted.
-     *
-     * @param node
-     * @return The accessibility node info.
-     */
-    private AccessibilityNodeInfo getScrollableParent(AccessibilityNodeInfo node) {
-        AccessibilityNodeInfo parent = node;
-        while(parent != null) {
-            parent = parent.getParent();
-            if (parent != null && parent.isScrollable()) {
-                return parent;
-            }
-        }
-        return null;
+        return AccessibilityNodeInfoHelper.getVisibleBoundsInScreen(node, w, h);
     }
 
     /**
diff --git a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/UiObject2.java b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/UiObject2.java
index 19ede49..75445bf 100644
--- a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/UiObject2.java
+++ b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/UiObject2.java
@@ -32,6 +32,7 @@
 import android.view.accessibility.AccessibilityWindowInfo;
 
 import androidx.annotation.DoNotInline;
+import androidx.annotation.NonNull;
 import androidx.annotation.RequiresApi;
 
 import java.util.ArrayList;
@@ -269,57 +270,21 @@
         return false;
     }
 
-    /** Returns the visible bounds of {@code node} in screen coordinates. */
-    @SuppressWarnings("RectIntersectReturnValueIgnored")
-    private Rect getVisibleBounds(AccessibilityNodeInfo node) {
-        // Get the object bounds in screen coordinates
-        Rect ret = new Rect();
-        node.getBoundsInScreen(ret);
-
-        // Trim any portion of the bounds that are not on the screen
-        final int displayId = getDisplayId();
-        if (displayId == Display.DEFAULT_DISPLAY) {
-            final Rect screen =
-                new Rect(0, 0, getDevice().getDisplayWidth(), getDevice().getDisplayHeight());
-            ret.intersect(screen);
-        } else {
-            final DisplayManager dm =
-                    (DisplayManager) mDevice.getInstrumentation().getContext().getSystemService(
-                            Service.DISPLAY_SERVICE);
-            final Display display = dm.getDisplay(getDisplayId());
-            if (display != null) {
-                final Point size = new Point();
-                display.getRealSize(size);
-                final Rect screen = new Rect(0, 0, size.x, size.y);
-                ret.intersect(screen);
-            }
+    /** Returns the visible bounds of an {@link AccessibilityNodeInfo}. */
+    @NonNull
+    private Rect getVisibleBounds(@NonNull AccessibilityNodeInfo node) {
+        DisplayManager displayManager =
+                (DisplayManager) mDevice.getInstrumentation().getContext().getSystemService(
+                        Service.DISPLAY_SERVICE);
+        Display display = displayManager.getDisplay(getDisplayId());
+        if (display != null) {
+            Point displaySize = new Point();
+            display.getRealSize(displaySize);
+            return AccessibilityNodeInfoHelper.getVisibleBoundsInScreen(
+                    node, displaySize.x, displaySize.y);
         }
-
-        // On platforms that give us access to the node's window
-        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
-            // Trim any portion of the bounds that are outside the window
-            Rect bounds = new Rect();
-            AccessibilityWindowInfo window = Api21Impl.getWindow(node);
-            if (window != null) {
-                Api21Impl.getBoundsInScreen(window, bounds);
-                ret.intersect(bounds);
-            }
-        }
-
-        // Find the visible bounds of our first scrollable ancestor
-        AccessibilityNodeInfo ancestor = null;
-        for (ancestor = node.getParent(); ancestor != null; ancestor = ancestor.getParent()) {
-            // If this ancestor is scrollable
-            if (ancestor.isScrollable()) {
-                // Trim any portion of the bounds that are hidden by the non-visible portion of our
-                // ancestor
-                Rect ancestorRect = getVisibleBounds(ancestor);
-                ret.intersect(ancestorRect);
-                break;
-            }
-        }
-
-        return ret;
+        return AccessibilityNodeInfoHelper.getVisibleBoundsInScreen(
+                node, Integer.MAX_VALUE, Integer.MAX_VALUE);
     }
 
     /** Returns a point in the center of the visible bounds of this object. */
@@ -796,12 +761,6 @@
         static AccessibilityWindowInfo getWindow(AccessibilityNodeInfo accessibilityNodeInfo) {
             return accessibilityNodeInfo.getWindow();
         }
-
-        @DoNotInline
-        static void getBoundsInScreen(AccessibilityWindowInfo accessibilityWindowInfo,
-                Rect outBounds) {
-            accessibilityWindowInfo.getBoundsInScreen(outBounds);
-        }
     }
 
     @RequiresApi(30)
diff --git a/text/text/src/main/java/androidx/compose/ui/text/android/StaticLayoutFactory.kt b/text/text/src/main/java/androidx/compose/ui/text/android/StaticLayoutFactory.kt
index e2c9747..53d17d4 100644
--- a/text/text/src/main/java/androidx/compose/ui/text/android/StaticLayoutFactory.kt
+++ b/text/text/src/main/java/androidx/compose/ui/text/android/StaticLayoutFactory.kt
@@ -104,7 +104,7 @@
      * Returns whether fallbackLineSpacing is enabled for the given layout.
      *
      * @param layout StaticLayout instance
-     * @param useFallbackLineSpacing fallbackLineSpacing canfiguration passed while creating the
+     * @param useFallbackLineSpacing fallbackLineSpacing configuration passed while creating the
      * StaticLayout.
      */
     fun isFallbackLineSpacingEnabled(
diff --git a/window/window/src/androidTest/java/androidx/window/embedding/ActivityFilterTest.kt b/window/window/src/androidTest/java/androidx/window/embedding/ActivityFilterTest.kt
index c389acf..0ebd08a 100644
--- a/window/window/src/androidTest/java/androidx/window/embedding/ActivityFilterTest.kt
+++ b/window/window/src/androidTest/java/androidx/window/embedding/ActivityFilterTest.kt
@@ -34,6 +34,7 @@
     private val component1 = ComponentName("a.b.c", "a.b.c.TestActivity")
     private val component2 = ComponentName("d.e.f", "d.e.f.TestActivity")
     private val wildcard = ComponentName("*", "*")
+    private val classWildcard = ComponentName("a.b.c", "*")
     private val intent = Intent()
     private val activity = mock<Activity> {
         on { intent } doReturn intent
@@ -94,4 +95,15 @@
             "of null component")
             .that(filter.matchesActivity(activity)).isTrue()
     }
+
+    @Test
+    fun testMatchActivity_MatchIntentWithPackage() {
+        val filter = ActivityFilter(classWildcard, null /* intentAction */)
+
+        intent.component = null
+        intent.`package` = classWildcard.packageName
+
+        assertWithMessage("#matchActivity must be true because intent.package matches")
+            .that(filter.matchesActivity(activity)).isTrue()
+    }
 }
\ No newline at end of file
diff --git a/window/window/src/androidTest/java/androidx/window/embedding/SplitPairFilterTest.kt b/window/window/src/androidTest/java/androidx/window/embedding/SplitPairFilterTest.kt
new file mode 100644
index 0000000..0ac37b0
--- /dev/null
+++ b/window/window/src/androidTest/java/androidx/window/embedding/SplitPairFilterTest.kt
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.window.embedding
+
+import android.app.Activity
+import android.content.ComponentName
+import android.content.Intent
+import androidx.window.core.ExperimentalWindowApi
+import com.google.common.truth.Truth.assertWithMessage
+import com.nhaarman.mockitokotlin2.doReturn
+import com.nhaarman.mockitokotlin2.mock
+import org.junit.Test
+
+@OptIn(ExperimentalWindowApi::class)
+class SplitPairFilterTest {
+    private val component1 = ComponentName("a.b.c", "a.b.c.TestActivity")
+    private val component2 = ComponentName("d.e.f", "d.e.f.TestActivity")
+    private val intentClassWildcard = ComponentName("d.e.f", "*")
+    private val intent = Intent()
+    private val activity = mock<Activity> {
+        on { intent } doReturn intent
+        on { componentName } doReturn component1
+    }
+    private val filter =
+        SplitPairFilter(component1, intentClassWildcard, null /* secondaryActivityIntentAction */)
+
+    @Test
+    fun testMatchActivityIntentPair_MatchIntentComponent() {
+        assertWithMessage("#matchesActivityIntentPair must be false because intent is empty")
+            .that(filter.matchesActivityIntentPair(activity, intent)).isFalse()
+
+        intent.component = component2
+
+        assertWithMessage("#matchesActivityIntentPair must be true because intent.component" +
+            " matches")
+            .that(filter.matchesActivityIntentPair(activity, intent)).isTrue()
+    }
+
+    @Test
+    fun testMatchActivityIntentPair_MatchIntentPackage() {
+        intent.`package` = intentClassWildcard.packageName
+
+        assertWithMessage("#matchesActivityIntentPair must be true because intent.package matches")
+            .that(filter.matchesActivityIntentPair(activity, intent)).isTrue()
+
+        intent.component = component1
+
+        assertWithMessage("#matchesActivityIntentPair must be false because intent.component" +
+            " doesn't match")
+            .that(filter.matchesActivityIntentPair(activity, intent)).isFalse()
+    }
+}
\ No newline at end of file
diff --git a/window/window/src/main/java/androidx/window/embedding/ActivityFilter.kt b/window/window/src/main/java/androidx/window/embedding/ActivityFilter.kt
index 06ff616..fd3d828 100644
--- a/window/window/src/main/java/androidx/window/embedding/ActivityFilter.kt
+++ b/window/window/src/main/java/androidx/window/embedding/ActivityFilter.kt
@@ -72,7 +72,7 @@
 
     fun matchesIntent(intent: Intent): Boolean {
         val match =
-            if (!MatcherUtils.areComponentsMatching(intent.component, componentName)) {
+            if (!MatcherUtils.isIntentMatching(intent, componentName)) {
                 false
             } else {
                 intentAction == null || intentAction == intent.action
@@ -89,7 +89,7 @@
 
     fun matchesActivity(activity: Activity): Boolean {
         val match =
-            MatcherUtils.areActivityOrIntentComponentsMatching(activity, componentName) &&
+            MatcherUtils.isActivityOrIntentMatching(activity, componentName) &&
                 (intentAction == null || intentAction == activity.intent?.action)
         if (sDebugMatchers) {
             val matchString = if (match) "MATCH" else "NO MATCH"
diff --git a/window/window/src/main/java/androidx/window/embedding/MatcherUtils.kt b/window/window/src/main/java/androidx/window/embedding/MatcherUtils.kt
index 712c94f..2574912 100644
--- a/window/window/src/main/java/androidx/window/embedding/MatcherUtils.kt
+++ b/window/window/src/main/java/androidx/window/embedding/MatcherUtils.kt
@@ -17,6 +17,7 @@
 
 import android.app.Activity
 import android.content.ComponentName
+import android.content.Intent
 import android.util.Log
 import androidx.window.core.ExperimentalWindowApi
 
@@ -54,20 +55,36 @@
     }
 
     /**
-     * Returns `true` if [Activity.getComponentName] match or
-     * [Component][android.content.Intent.getComponent] of [Activity.getIntent] match allowing
+     * Returns `true` if [Activity.getComponentName] match or [Activity.getIntent] match allowing
      * wildcard patterns.
      */
-    internal fun areActivityOrIntentComponentsMatching(
+    internal fun isActivityOrIntentMatching(
         activity: Activity,
         ruleComponent: ComponentName
     ): Boolean {
         if (areComponentsMatching(activity.componentName, ruleComponent)) {
             return true
         }
-        // Returns false if activity's intent doesn't exist or its intent's Component doesn't match.
-        return activity.intent?.component ?.let {
-                component -> areComponentsMatching(component, ruleComponent) } ?: false
+        // Returns false if activity's intent doesn't exist or its intent doesn't match.
+        return activity.intent ?.let { intent -> isIntentMatching(intent, ruleComponent) } ?: false
+    }
+
+    /**
+     * Returns `true` if [Intent.getComponent] match or [Intent.getPackage] match allowing wildcard
+     * patterns.
+     */
+    internal fun isIntentMatching(
+        intent: Intent,
+        ruleComponent: ComponentName
+    ): Boolean {
+        if (intent.component != null) {
+            // Compare the component if set.
+            return areComponentsMatching(intent.component, ruleComponent)
+        }
+        // Check if there is wildcard match for Intent that only specifies the packageName.
+        val packageName = intent.`package` ?: return false
+        return (packageName == ruleComponent.packageName ||
+            wildcardMatch(packageName, ruleComponent.packageName)) && ruleComponent.className == "*"
     }
 
     /**
diff --git a/window/window/src/main/java/androidx/window/embedding/SplitPairFilter.kt b/window/window/src/main/java/androidx/window/embedding/SplitPairFilter.kt
index c633a0e..f13ddd5 100644
--- a/window/window/src/main/java/androidx/window/embedding/SplitPairFilter.kt
+++ b/window/window/src/main/java/androidx/window/embedding/SplitPairFilter.kt
@@ -21,6 +21,7 @@
 import android.util.Log
 import androidx.window.core.ExperimentalWindowApi
 import androidx.window.embedding.MatcherUtils.areComponentsMatching
+import androidx.window.embedding.MatcherUtils.isIntentMatching
 import androidx.window.embedding.MatcherUtils.sDebugMatchers
 import androidx.window.embedding.MatcherUtils.sMatchersTag
 
@@ -85,7 +86,7 @@
         ) {
             false
         } else if (
-            !areComponentsMatching(secondaryActivityIntent.component, secondaryActivityName)
+            !isIntentMatching(secondaryActivityIntent, secondaryActivityName)
         ) {
             false
         } else {