Merge "Synchronize adding and removing table trackers." into androidx-main
diff --git a/activity/activity/src/androidTest/java/androidx/activity/OnBackPressedDispatcherTest.kt b/activity/activity/src/androidTest/java/androidx/activity/OnBackPressedDispatcherTest.kt
index a57afce..e5a2b92 100644
--- a/activity/activity/src/androidTest/java/androidx/activity/OnBackPressedDispatcherTest.kt
+++ b/activity/activity/src/androidTest/java/androidx/activity/OnBackPressedDispatcherTest.kt
@@ -352,7 +352,8 @@
     fun testLifecycleCallback_whenDestroyed() {
         val lifecycleOnBackPressedCallback = CountingOnBackPressedCallback()
 
-        val lifecycleOwner = TestLifecycleOwner(Lifecycle.State.DESTROYED)
+        val lifecycleOwner = TestLifecycleOwner(Lifecycle.State.CREATED)
+        lifecycleOwner.lifecycle.currentState = Lifecycle.State.DESTROYED
 
         dispatcher.addCallback(lifecycleOwner, lifecycleOnBackPressedCallback)
 
diff --git a/appsearch/appsearch-builtin-types/src/androidTest/java/androidx/appsearch/utils/DateTimeFormatValidatorTest.java b/appsearch/appsearch-builtin-types/src/androidTest/java/androidx/appsearch/utils/DateTimeFormatValidatorTest.java
index cf76375..df1b190 100644
--- a/appsearch/appsearch-builtin-types/src/androidTest/java/androidx/appsearch/utils/DateTimeFormatValidatorTest.java
+++ b/appsearch/appsearch-builtin-types/src/androidTest/java/androidx/appsearch/utils/DateTimeFormatValidatorTest.java
@@ -44,6 +44,12 @@
     }
 
     @Test
+    public void testValidateISO8601DateTime_validDateWithoutSeconds_returnsTrue() {
+        assertThat(DateTimeFormatValidator.validateISO8601DateTime("2022-01-01T00:00"))
+                .isTrue();
+    }
+
+    @Test
     public void testValidateISO8601DateTime_invalidDate_returnsFalse() {
         assertThat(DateTimeFormatValidator.validateISO8601DateTime("2022:01:01T00-00-00"))
                 .isFalse();
diff --git a/appsearch/appsearch-builtin-types/src/main/java/androidx/appsearch/builtintypes/Timer.java b/appsearch/appsearch-builtin-types/src/main/java/androidx/appsearch/builtintypes/Timer.java
index d2015d1..48d2840 100644
--- a/appsearch/appsearch-builtin-types/src/main/java/androidx/appsearch/builtintypes/Timer.java
+++ b/appsearch/appsearch-builtin-types/src/main/java/androidx/appsearch/builtintypes/Timer.java
@@ -247,8 +247,8 @@
     }
 
     /**
-     * Returns the amount of time remaining in milliseconds for the {@link Timer} since it was
-     * started, paused or reset.
+     * Returns the amount of time remaining in milliseconds for the {@link Timer} since its state
+     * last changed.
      *
      * <p>If it is in the {@link #STATUS_STARTED} state, then the current remaining time will be
      * different from this value. To get the current remaining time, use
@@ -472,8 +472,8 @@
         }
 
         /**
-         * Sets the amount of time remaining in milliseconds for the {@link Timer} since it was
-         * started, paused or reset.
+         * Sets the amount of time remaining in milliseconds for the {@link Timer} since its
+         * state last changed.
          */
         @NonNull
         public Builder setRemainingTimeMillisSinceUpdate(long remainingTimeMillisSinceUpdate) {
diff --git a/appsearch/appsearch-builtin-types/src/main/java/androidx/appsearch/utils/DateTimeFormatValidator.java b/appsearch/appsearch-builtin-types/src/main/java/androidx/appsearch/utils/DateTimeFormatValidator.java
index c472161..467f10f 100644
--- a/appsearch/appsearch-builtin-types/src/main/java/androidx/appsearch/utils/DateTimeFormatValidator.java
+++ b/appsearch/appsearch-builtin-types/src/main/java/androidx/appsearch/utils/DateTimeFormatValidator.java
@@ -46,7 +46,8 @@
      * Returns true if the date string matches yyyy-MM-ddTHH:mm:ss
      */
     public static boolean validateISO8601DateTime(@NonNull String dateString) {
-        return validateDateFormat("yyyy-MM-dd'T'HH:mm:ss", dateString);
+        return validateDateFormat("yyyy-MM-dd'T'HH:mm", dateString)
+                || validateDateFormat("yyyy-MM-dd'T'HH:mm:ss", dateString);
     }
 
     /**
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/DisplayInfoManager.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/DisplayInfoManager.java
index b94be04..b64900d5 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/DisplayInfoManager.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/DisplayInfoManager.java
@@ -84,11 +84,13 @@
         Display maxDisplay = null;
         int maxDisplaySize = -1;
         for (Display display : displays) {
-            Point displaySize = new Point();
-            display.getRealSize(displaySize);
-            if (displaySize.x * displaySize.y > maxDisplaySize) {
-                maxDisplaySize = displaySize.x * displaySize.y;
-                maxDisplay = display;
+            if (display.getState() != Display.STATE_OFF) {
+                Point displaySize = new Point();
+                display.getRealSize(displaySize);
+                if (displaySize.x * displaySize.y > maxDisplaySize) {
+                    maxDisplaySize = displaySize.x * displaySize.y;
+                    maxDisplay = display;
+                }
             }
         }
 
diff --git a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/DisplayInfoManagerTest.kt b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/DisplayInfoManagerTest.kt
index 2815d19..fafca66 100644
--- a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/DisplayInfoManagerTest.kt
+++ b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/DisplayInfoManagerTest.kt
@@ -21,6 +21,7 @@
 import android.hardware.display.DisplayManager
 import android.os.Build
 import android.util.Size
+import android.view.Display
 import androidx.test.core.app.ApplicationProvider
 import com.google.common.truth.Truth.assertThat
 import org.junit.After
@@ -32,16 +33,24 @@
 import org.robolectric.RobolectricTestRunner
 import org.robolectric.annotation.Config
 import org.robolectric.annotation.internal.DoNotInstrument
+import org.robolectric.shadow.api.Shadow
 import org.robolectric.shadows.ShadowDisplayManager
+import org.robolectric.shadows.ShadowDisplay
 
 @RunWith(RobolectricTestRunner::class)
 @DoNotInstrument
 @Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
 @Suppress("DEPRECATION") // getRealSize
 class DisplayInfoManagerTest {
-    private fun addDisplay(width: Int, height: Int) {
+
+    private fun addDisplay(width: Int, height: Int, state: Int = Display.STATE_ON) {
         val displayStr = String.format("w%ddp-h%ddp", width, height)
-        ShadowDisplayManager.addDisplay(displayStr)
+        val displayId = ShadowDisplayManager.addDisplay(displayStr)
+        if (state != Display.STATE_ON) {
+            val displayManager = (ApplicationProvider.getApplicationContext() as Context)
+                .getSystemService(Context.DISPLAY_SERVICE) as DisplayManager
+            (Shadow.extract(displayManager.getDisplay(displayId)) as ShadowDisplay).setState(state)
+        }
     }
 
     @After
@@ -84,6 +93,23 @@
     }
 
     @Test
+    fun canReturnMaxSizeDisplay_offState() {
+        // Arrange
+        addDisplay(2000, 3000, Display.STATE_OFF)
+        addDisplay(480, 640)
+        addDisplay(200, 300, Display.STATE_OFF)
+
+        // Act
+        val displayInfoManager = DisplayInfoManager
+            .getInstance(ApplicationProvider.getApplicationContext())
+        val size = Point()
+        displayInfoManager.maxSizeDisplay.getRealSize(size)
+
+        // Assert
+        assertThat(size).isEqualTo(Point(480, 640))
+    }
+
+    @Test
     fun canReturnPreviewSize_displaySmallerThan1080P() {
         // Arrange
         addDisplay(480, 640)
diff --git a/camera/camera-previewview/src/main/java/androidx/camera/previewview/PreviewSurfaceRequest.java b/camera/camera-previewview/src/main/java/androidx/camera/previewview/PreviewSurfaceRequest.java
index dedff4f..2636e46 100644
--- a/camera/camera-previewview/src/main/java/androidx/camera/previewview/PreviewSurfaceRequest.java
+++ b/camera/camera-previewview/src/main/java/androidx/camera/previewview/PreviewSurfaceRequest.java
@@ -75,10 +75,6 @@
 
     @NonNull private TransformationInfo mTransformationInfo;
 
-    @SuppressWarnings("WeakerAccess") /*synthetic accessor */
-    @Nullable
-    Callback mCallback;
-
     @Nullable private Executor mTransformationInfoExecutor;
     @Nullable private TransformationInfoListener mTransformationInfoListener;
 
@@ -195,9 +191,6 @@
                 // is safe to release the Surface and associated resources.
 
                 Futures.propagate(terminationFuture, sessionStatusCompleter);
-                if (mCallback != null) {
-                    mCallback.onSuccess(surface);
-                }
             }
 
             @Override
@@ -214,9 +207,6 @@
                 } else {
                     sessionStatusCompleter.set(null);
                 }
-                if (mCallback != null) {
-                    mCallback.onFailure(t);
-                }
             }
         }, CameraExecutors.directExecutor());
 
@@ -266,23 +256,6 @@
     }
 
     /**
-     * Set surface request callback.
-     *
-     * @param callback {@link Callback}.
-     */
-    @SuppressLint("ExecutorRegistration")
-    public void setCallback(@NonNull Callback callback) {
-        mCallback = callback;
-    }
-
-    /**
-     * Clear surface request callback.
-     */
-    public void clearCallback() {
-        mCallback = null;
-    }
-
-    /**
      * Closes the preview surface to mark it as safe to release.
      */
     public void markSurfaceSafeToRelease() {
diff --git a/camera/integration-tests/previewviewtestapp/build.gradle b/camera/integration-tests/previewviewtestapp/build.gradle
new file mode 100644
index 0000000..1ba1215
--- /dev/null
+++ b/camera/integration-tests/previewviewtestapp/build.gradle
@@ -0,0 +1,83 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ * implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
+
+plugins {
+    id("AndroidXPlugin")
+    id("com.android.application")
+    id("kotlin-android")
+    id("AndroidXComposePlugin")
+}
+
+android {
+    defaultConfig {
+        applicationId "androidx.camera.integration.previewview"
+        minSdkVersion 21
+        multiDexEnabled true
+    }
+
+    buildTypes {
+        release {
+            minifyEnabled true
+            shrinkResources true
+        }
+    }
+}
+
+dependencies {
+    implementation("androidx.concurrent:concurrent-futures-ktx:1.1.0")
+
+    // Internal library
+    implementation(project(":camera:camera-camera2"))
+    implementation(project(":camera:camera-lifecycle"))
+    implementation(project(":camera:camera-previewview"))
+
+    // Lifecycle and LiveData
+    implementation("androidx.lifecycle:lifecycle-livedata:2.2.0")
+    implementation("androidx.lifecycle:lifecycle-runtime:2.3.1")
+    implementation(project(":lifecycle:lifecycle-common-java8"))
+
+    // Android Support Library
+    implementation("androidx.appcompat:appcompat:1.3.0")
+    implementation 'androidx.legacy:legacy-support-v4:1.0.0'
+
+    implementation(libs.guavaAndroid)
+    implementation(libs.constraintLayout)
+
+    compileOnly(libs.kotlinCompiler)
+
+    // Testing framework
+    androidTestImplementation(libs.testExtJunit)
+    androidTestImplementation(libs.testCore)
+    androidTestImplementation(libs.testRunner)
+    androidTestImplementation(libs.testRules)
+    androidTestImplementation(libs.testUiautomator)
+    androidTestImplementation(libs.espressoCore)
+    androidTestImplementation("androidx.lifecycle:lifecycle-runtime-testing:2.3.1")
+    androidTestImplementation("androidx.lifecycle:lifecycle-runtime:2.3.1")
+    androidTestImplementation("androidx.lifecycle:lifecycle-livedata-ktx:2.2.0")
+    androidTestImplementation(libs.truth)
+    debugImplementation(libs.testCore)
+    debugImplementation("androidx.fragment:fragment-testing:1.2.3")
+    // camera-testing added as 'implementation' dependency to include camera-testing activity in APK
+    debugImplementation(project(":camera:camera-testing")) {
+        // Ensure camera-testing does not pull in camera-core project dependency which will
+        // override pinned dependency.
+        exclude(group:"androidx.camera", module:"camera-core")
+    }
+}
diff --git a/camera/integration-tests/previewviewtestapp/src/main/AndroidManifest.xml b/camera/integration-tests/previewviewtestapp/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..f151fcd
--- /dev/null
+++ b/camera/integration-tests/previewviewtestapp/src/main/AndroidManifest.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright 2021 The Android Open Source Project
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+  -->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="androidx.camera.integration.previewview">
+
+    <uses-permission android:name="android.permission.CAMERA" />
+
+    <uses-feature android:name="android.hardware.camera" />
+    <uses-feature android:name="android.hardware.camera.autofocus" />
+
+    <application
+        android:allowBackup="true"
+        android:label="@string/app_name"
+        android:theme="@style/AppTheme">
+
+        <activity
+            android:name=".CameraActivity"
+            android:exported="true"
+            android:label="@string/app_name">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="android.intent.category.LAUNCHER" />
+            </intent-filter>
+        </activity>
+    </application>
+
+</manifest>
\ No newline at end of file
diff --git a/camera/integration-tests/previewviewtestapp/src/main/java/androidx/camera/integration/previewview/CameraActivity.java b/camera/integration-tests/previewviewtestapp/src/main/java/androidx/camera/integration/previewview/CameraActivity.java
new file mode 100755
index 0000000..0479435
--- /dev/null
+++ b/camera/integration-tests/previewviewtestapp/src/main/java/androidx/camera/integration/previewview/CameraActivity.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package androidx.camera.integration.previewview;
+
+import android.os.Bundle;
+
+import androidx.annotation.Nullable;
+import androidx.appcompat.app.AppCompatActivity;
+
+/**
+ * Activity for PreviewView.
+ */
+public class CameraActivity extends AppCompatActivity {
+
+    @Override
+    protected void onCreate(@Nullable Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setContentView(R.layout.activity_camera);
+        if (null == savedInstanceState) {
+            getSupportFragmentManager().beginTransaction()
+                    .replace(R.id.container, new CameraViewFinderFragment())
+                    .commit();
+        }
+    }
+
+}
diff --git a/camera/integration-tests/previewviewtestapp/src/main/java/androidx/camera/integration/previewview/CameraViewFinderFragment.java b/camera/integration-tests/previewviewtestapp/src/main/java/androidx/camera/integration/previewview/CameraViewFinderFragment.java
new file mode 100755
index 0000000..c4737ad
--- /dev/null
+++ b/camera/integration-tests/previewviewtestapp/src/main/java/androidx/camera/integration/previewview/CameraViewFinderFragment.java
@@ -0,0 +1,1004 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.integration.previewview;
+
+import android.Manifest;
+import android.annotation.SuppressLint;
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.pm.PackageManager;
+import android.graphics.ImageFormat;
+import android.graphics.Point;
+import android.graphics.SurfaceTexture;
+import android.hardware.camera2.CameraAccessException;
+import android.hardware.camera2.CameraCaptureSession;
+import android.hardware.camera2.CameraCharacteristics;
+import android.hardware.camera2.CameraDevice;
+import android.hardware.camera2.CameraManager;
+import android.hardware.camera2.CameraMetadata;
+import android.hardware.camera2.CaptureRequest;
+import android.hardware.camera2.CaptureResult;
+import android.hardware.camera2.TotalCaptureResult;
+import android.hardware.camera2.params.StreamConfigurationMap;
+import android.media.Image;
+import android.media.ImageReader;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.util.Log;
+import android.util.Size;
+import android.util.SparseIntArray;
+import android.view.Display;
+import android.view.LayoutInflater;
+import android.view.Surface;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.Toast;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.camera.core.impl.utils.futures.FutureCallback;
+import androidx.camera.core.impl.utils.futures.Futures;
+import androidx.camera.previewview.CameraViewFinder;
+import androidx.camera.previewview.PreviewSurfaceRequest;
+import androidx.core.app.ActivityCompat;
+import androidx.core.content.ContextCompat;
+import androidx.fragment.app.DialogFragment;
+import androidx.fragment.app.Fragment;
+
+import com.google.common.base.Objects;
+import com.google.common.util.concurrent.ListenableFuture;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+import java.util.concurrent.Semaphore;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Fragment for {@link CameraViewFinder}.
+ */
+public class CameraViewFinderFragment extends Fragment
+        implements View.OnClickListener, ActivityCompat.OnRequestPermissionsResultCallback {
+
+    /**
+     * Conversion from screen rotation to JPEG orientation.
+     */
+    private static final SparseIntArray ORIENTATIONS = new SparseIntArray();
+    private static final int REQUEST_CAMERA_PERMISSION = 1;
+    private static final String FRAGMENT_DIALOG = "dialog";
+
+    static {
+        ORIENTATIONS.append(Surface.ROTATION_0, 90);
+        ORIENTATIONS.append(Surface.ROTATION_90, 0);
+        ORIENTATIONS.append(Surface.ROTATION_180, 270);
+        ORIENTATIONS.append(Surface.ROTATION_270, 180);
+    }
+
+    /**
+     * Tag for the {@link Log}.
+     */
+    private static final String TAG = "Camera2BasicFragment";
+
+    /**
+     * Camera state: Showing camera preview.
+     */
+    private static final int STATE_PREVIEW = 0;
+
+    /**
+     * Camera state: Waiting for the focus to be locked.
+     */
+    private static final int STATE_WAITING_LOCK = 1;
+
+    /**
+     * Camera state: Waiting for the exposure to be precapture state.
+     */
+    private static final int STATE_WAITING_PRECAPTURE = 2;
+
+    /**
+     * Camera state: Waiting for the exposure state to be something other than precapture.
+     */
+    private static final int STATE_WAITING_NON_PRECAPTURE = 3;
+
+    /**
+     * Camera state: Picture was taken.
+     */
+    private static final int STATE_PICTURE_TAKEN = 4;
+
+    /**
+     * Max preview width that is guaranteed by Camera2 API
+     */
+    private static final int MAX_PREVIEW_WIDTH = 1920;
+
+    /**
+     * Max preview height that is guaranteed by Camera2 API
+     */
+    private static final int MAX_PREVIEW_HEIGHT = 1080;
+
+    /**
+     * ID of the current {@link CameraDevice}.
+     */
+    private String mCameraId;
+
+    /**
+     * An instance of {@link CameraManager}.
+     */
+    private CameraManager mCameraManager;
+
+    /**
+     * An {@link CameraViewFinder} for camera preview.
+     */
+    private CameraViewFinder mCameraViewFinder;
+
+    /**
+     * A {@link CameraCaptureSession } for camera preview.
+     */
+    private CameraCaptureSession mCaptureSession;
+
+    /**
+     * A reference to the opened {@link CameraDevice}.
+     */
+    private CameraDevice mCameraDevice;
+
+    /**
+     * The {@link android.util.Size} of camera preview.
+     */
+    private Size mPreviewSize;
+
+    /**
+     * The request for preview surface.
+     */
+    private PreviewSurfaceRequest mPreviewSurfaceRequest;
+
+    /**
+     * {@link CameraDevice.StateCallback} is called when {@link CameraDevice} changes its state.
+     */
+    private final CameraDevice.StateCallback mStateCallback = new CameraDevice.StateCallback() {
+        @Override
+        public void onOpened(@NonNull CameraDevice cameraDevice) {
+            // This method is called when the camera is opened.  We start camera preview here.
+            mCameraOpenCloseLock.release();
+            mCameraDevice = cameraDevice;
+            createCameraPreviewSession();
+        }
+
+        @Override
+        public void onDisconnected(@NonNull CameraDevice cameraDevice) {
+            mCameraOpenCloseLock.release();
+            cameraDevice.close();
+            mCameraDevice = null;
+        }
+
+        @Override
+        public void onError(@NonNull CameraDevice cameraDevice, int error) {
+            mCameraOpenCloseLock.release();
+            cameraDevice.close();
+            mCameraDevice = null;
+            Activity activity = getActivity();
+            if (null != activity) {
+                activity.finish();
+            }
+        }
+    };
+
+    /**
+     * An additional thread for running tasks that shouldn't block the UI.
+     */
+    private HandlerThread mBackgroundThread;
+
+    /**
+     * A {@link Handler} for running tasks in the background.
+     */
+    private Handler mBackgroundHandler;
+
+    /**
+     * An {@link ImageReader} that handles still image capture.
+     */
+    private ImageReader mImageReader;
+
+    /**
+     * This is the output file for our picture.
+     */
+    private File mFile;
+
+    /**
+     * This a callback object for the {@link ImageReader}. "onImageAvailable" will be called when a
+     * still image is ready to be saved.
+     */
+    private final ImageReader.OnImageAvailableListener mOnImageAvailableListener =
+            new ImageReader.OnImageAvailableListener() {
+        @Override
+        public void onImageAvailable(ImageReader reader) {
+            mBackgroundHandler.post(new ImageSaver(reader.acquireNextImage(), mFile));
+        }
+    };
+
+    /**
+     * {@link CaptureRequest.Builder} for the camera preview
+     */
+    private CaptureRequest.Builder mPreviewRequestBuilder;
+
+    /**
+     * {@link CaptureRequest} generated by {@link #mPreviewRequestBuilder}
+     */
+    private CaptureRequest mPreviewRequest;
+
+    /**
+     * The current state of camera state for taking pictures.
+     *
+     * @see #mCaptureCallback
+     */
+    private int mState = STATE_PREVIEW;
+
+    /**
+     * A {@link Semaphore} to prevent the app from exiting before closing the camera.
+     */
+    private Semaphore mCameraOpenCloseLock = new Semaphore(1);
+
+    /**
+     * Whether the current camera device supports Flash or not.
+     */
+    private boolean mFlashSupported;
+
+    /**
+     * Orientation of the camera sensor
+     */
+    private int mSensorOrientation;
+
+    /**
+     * A {@link CameraCaptureSession.CaptureCallback} that handles events related to JPEG capture.
+     */
+    private CameraCaptureSession.CaptureCallback mCaptureCallback =
+            new CameraCaptureSession.CaptureCallback() {
+
+        private void process(CaptureResult result) {
+            switch (mState) {
+                case STATE_PREVIEW: {
+                    // We have nothing to do when the camera preview is working normally.
+                    break;
+                }
+                case STATE_WAITING_LOCK: {
+                    Integer afState = result.get(CaptureResult.CONTROL_AF_STATE);
+                    if (afState == null) {
+                        captureStillPicture();
+                    } else if (CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED == afState
+                            || CaptureResult.CONTROL_AF_STATE_NOT_FOCUSED_LOCKED == afState) {
+                        // CONTROL_AE_STATE can be null on some devices
+                        Integer aeState = result.get(CaptureResult.CONTROL_AE_STATE);
+                        if (aeState == null
+                                || aeState == CaptureResult.CONTROL_AE_STATE_CONVERGED) {
+                            mState = STATE_PICTURE_TAKEN;
+                            captureStillPicture();
+                        } else {
+                            runPrecaptureSequence();
+                        }
+                    }
+                    break;
+                }
+                case STATE_WAITING_PRECAPTURE: {
+                    // CONTROL_AE_STATE can be null on some devices
+                    Integer aeState = result.get(CaptureResult.CONTROL_AE_STATE);
+                    if (aeState == null
+                            || aeState == CaptureResult.CONTROL_AE_STATE_PRECAPTURE
+                            || aeState == CaptureRequest.CONTROL_AE_STATE_FLASH_REQUIRED) {
+                        mState = STATE_WAITING_NON_PRECAPTURE;
+                    }
+                    break;
+                }
+                case STATE_WAITING_NON_PRECAPTURE: {
+                    // CONTROL_AE_STATE can be null on some devices
+                    Integer aeState = result.get(CaptureResult.CONTROL_AE_STATE);
+                    if (aeState == null || aeState != CaptureResult.CONTROL_AE_STATE_PRECAPTURE) {
+                        mState = STATE_PICTURE_TAKEN;
+                        captureStillPicture();
+                    }
+                    break;
+                }
+            }
+        }
+
+        @Override
+        public void onCaptureProgressed(@NonNull CameraCaptureSession session,
+                @NonNull CaptureRequest request,
+                @NonNull CaptureResult partialResult) {
+            process(partialResult);
+        }
+
+        @Override
+        public void onCaptureCompleted(@NonNull CameraCaptureSession session,
+                @NonNull CaptureRequest request,
+                @NonNull TotalCaptureResult result) {
+            process(result);
+        }
+
+    };
+
+    /**
+     * Shows a {@link Toast} on the UI thread.
+     *
+     * @param text The message to show
+     */
+    private void showToast(final String text) {
+        final Activity activity = getActivity();
+        if (activity != null) {
+            activity.runOnUiThread(() -> Toast.makeText(activity, text, Toast.LENGTH_SHORT).show());
+        }
+    }
+
+    /**
+     * Given {@code choices} of {@code Size}s supported by a camera, choose the smallest one that
+     * is at least as large as the respective texture view size, and that is at most as large as the
+     * respective max size, and whose aspect ratio matches with the specified value. If such size
+     * doesn't exist, choose the largest one that is at most as large as the respective max size,
+     * and whose aspect ratio matches with the specified value.
+     *
+     * @param choices           The list of sizes that the camera supports for the intended output
+     *                          class
+     * @param textureViewWidth  The width of the texture view relative to sensor coordinate
+     * @param textureViewHeight The height of the texture view relative to sensor coordinate
+     * @param maxWidth          The maximum width that can be chosen
+     * @param maxHeight         The maximum height that can be chosen
+     * @param aspectRatio       The aspect ratio
+     * @return The optimal {@code Size}, or an arbitrary one if none were big enough
+     */
+    private static Size chooseOptimalSize(Size[] choices, int textureViewWidth,
+            int textureViewHeight, int maxWidth, int maxHeight, Size aspectRatio) {
+
+        // Collect the supported resolutions that are at least as big as the preview Surface
+        List<Size> bigEnough = new ArrayList<>();
+        // Collect the supported resolutions that are smaller than the preview Surface
+        List<Size> notBigEnough = new ArrayList<>();
+        int w = aspectRatio.getWidth();
+        int h = aspectRatio.getHeight();
+        for (Size option : choices) {
+            if (option.getWidth() <= maxWidth && option.getHeight() <= maxHeight
+                    && option.getHeight() == option.getWidth() * h / w) {
+                if (option.getWidth() >= textureViewWidth
+                        && option.getHeight() >= textureViewHeight) {
+                    bigEnough.add(option);
+                } else {
+                    notBigEnough.add(option);
+                }
+            }
+        }
+
+        // Pick the smallest of those big enough. If there is no one big enough, pick the
+        // largest of those not big enough.
+        if (bigEnough.size() > 0) {
+            return Collections.min(bigEnough, new CompareSizesByArea());
+        } else if (notBigEnough.size() > 0) {
+            return Collections.max(notBigEnough, new CompareSizesByArea());
+        } else {
+            Log.e(TAG, "Couldn't find any suitable preview size");
+            return choices[0];
+        }
+    }
+
+    /**
+     * Returns {@link CameraViewFinderFragment} instance.
+     * @return {@link CameraViewFinderFragment}.
+     */
+    @NonNull
+    public static CameraViewFinderFragment newInstance() {
+        return new CameraViewFinderFragment();
+    }
+
+    @Nullable
+    @Override
+    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
+            @Nullable Bundle savedInstanceState) {
+        return inflater.inflate(R.layout.fragment_camera_view_finder, container, false);
+    }
+
+    @SuppressLint("ClassVerificationFailure")
+    @Override
+    public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
+        view.findViewById(R.id.picture).setOnClickListener(this);
+        view.findViewById(R.id.toggle).setOnClickListener(this);
+        mCameraViewFinder = view.findViewById(R.id.view_finder);
+
+        if (Build.VERSION.SDK_INT >= 23) {
+            mCameraManager = getActivity().getSystemService(CameraManager.class);
+        } else {
+            mCameraManager = (CameraManager) getActivity().getSystemService(Context.CAMERA_SERVICE);
+        }
+    }
+
+    @Override
+    public void onActivityCreated(@Nullable Bundle savedInstanceState) {
+        super.onActivityCreated(savedInstanceState);
+        mFile = new File(getActivity().getExternalFilesDir(null), "pic.jpg");
+    }
+
+    @Override
+    public void onResume() {
+        super.onResume();
+        startBackgroundThread();
+
+        // When the screen is turned off and turned back on, the SurfaceTexture is already
+        // available, and "onSurfaceTextureAvailable" will not be called. In that case, we can open
+        // a camera and start preview from here (otherwise, we wait until the surface is ready in
+        // the SurfaceTextureListener).
+        mCameraViewFinder.post(() -> {
+            openCamera(mCameraViewFinder.getWidth(), mCameraViewFinder.getHeight(), false);
+        });
+    }
+
+    @Override
+    public void onPause() {
+        closeCamera();
+        stopBackgroundThread();
+        super.onPause();
+    }
+
+    private void requestCameraPermission() {
+        if (shouldShowRequestPermissionRationale(Manifest.permission.CAMERA)) {
+            new ConfirmationDialog().show(getChildFragmentManager(), FRAGMENT_DIALOG);
+        } else {
+            requestPermissions(new String[]{Manifest.permission.CAMERA}, REQUEST_CAMERA_PERMISSION);
+        }
+    }
+
+    @Override
+    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions,
+            @NonNull int[] grantResults) {
+        if (requestCode == REQUEST_CAMERA_PERMISSION) {
+            if (grantResults.length != 1 || grantResults[0] != PackageManager.PERMISSION_GRANTED) {
+                ErrorDialog.newInstance(getString(R.string.request_permission))
+                        .show(getChildFragmentManager(), FRAGMENT_DIALOG);
+            }
+        } else {
+            super.onRequestPermissionsResult(requestCode, permissions, grantResults);
+        }
+    }
+
+    /**
+     * Sets up member variables related to camera.
+     *
+     * @param width  The width of available size for camera preview
+     * @param height The height of available size for camera preview
+     * @param toggleCamera Whether to toggle front and back camera or not.
+     */
+    @SuppressWarnings("SuspiciousNameCombination")
+    private void setUpCameraOutputs(int width, int height, boolean toggleCamera) {
+        try {
+            for (String cameraId : mCameraManager.getCameraIdList()) {
+                CameraCharacteristics characteristics =
+                        mCameraManager.getCameraCharacteristics(cameraId);
+                Integer facing = characteristics.get(CameraCharacteristics.LENS_FACING);
+
+                // Toggle the front and back camera
+                if (toggleCamera && mCameraId != null) {
+                    Integer currentFacing =
+                            mCameraManager.getCameraCharacteristics(mCameraId)
+                                    .get(CameraCharacteristics.LENS_FACING);
+
+                    if (Objects.equal(currentFacing, facing)) {
+                        continue;
+                    }
+                }
+
+                StreamConfigurationMap map = characteristics.get(
+                        CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);
+                if (map == null) {
+                    continue;
+                }
+
+                // For still image captures, we use the largest available size.
+                Size largest = Collections.max(
+                        Arrays.asList(map.getOutputSizes(ImageFormat.JPEG)),
+                        new CompareSizesByArea());
+                mImageReader = ImageReader.newInstance(largest.getWidth(), largest.getHeight(),
+                        ImageFormat.JPEG, /*maxImages*/2);
+                mImageReader.setOnImageAvailableListener(
+                        mOnImageAvailableListener, mBackgroundHandler);
+
+                // Find out if we need to swap dimension to get the preview size relative to sensor
+                // coordinate.
+
+                // TODO: CameraViewFinder gets its own display instead of being provided by the
+                //  app. It has 2 benefits:
+                //  1) one less line of boilerplate code for developers
+                //  2) it will always use the correct display
+                Display display = mCameraViewFinder.getDisplay();
+                int displayRotation = display.getRotation();
+                mSensorOrientation = characteristics.get(CameraCharacteristics.SENSOR_ORIENTATION);
+                boolean swappedDimensions = false;
+                switch (displayRotation) {
+                    case Surface.ROTATION_0:
+                    case Surface.ROTATION_180:
+                        if (mSensorOrientation == 90 || mSensorOrientation == 270) {
+                            swappedDimensions = true;
+                        }
+                        break;
+                    case Surface.ROTATION_90:
+                    case Surface.ROTATION_270:
+                        if (mSensorOrientation == 0 || mSensorOrientation == 180) {
+                            swappedDimensions = true;
+                        }
+                        break;
+                    default:
+                        Log.e(TAG, "Display rotation is invalid: " + displayRotation);
+                }
+
+                Point displaySize = new Point();
+                display.getSize(displaySize);
+                int rotatedPreviewWidth = width;
+                int rotatedPreviewHeight = height;
+                int maxPreviewWidth = displaySize.x;
+                int maxPreviewHeight = displaySize.y;
+
+                if (swappedDimensions) {
+                    rotatedPreviewWidth = height;
+                    rotatedPreviewHeight = width;
+                    maxPreviewWidth = displaySize.y;
+                    maxPreviewHeight = displaySize.x;
+                }
+
+                if (maxPreviewWidth > MAX_PREVIEW_WIDTH) {
+                    maxPreviewWidth = MAX_PREVIEW_WIDTH;
+                }
+
+                if (maxPreviewHeight > MAX_PREVIEW_HEIGHT) {
+                    maxPreviewHeight = MAX_PREVIEW_HEIGHT;
+                }
+
+                mPreviewSize = chooseOptimalSize(map.getOutputSizes(SurfaceTexture.class),
+                        rotatedPreviewWidth, rotatedPreviewHeight, maxPreviewWidth,
+                        maxPreviewHeight, largest);
+
+                // Check if the flash is supported.
+                Boolean available = characteristics.get(CameraCharacteristics.FLASH_INFO_AVAILABLE);
+                mFlashSupported = available == null ? false : available;
+                mCameraId = cameraId;
+                mPreviewSurfaceRequest = new PreviewSurfaceRequest(
+                        mPreviewSize,
+                        display,
+                        characteristics.get(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL)
+                                == CameraMetadata.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY,
+                        facing == CameraCharacteristics.LENS_FACING_FRONT,
+                        mSensorOrientation);
+                return;
+            }
+        } catch (CameraAccessException e) {
+            Log.e(TAG, "setUpCameraOutputs CameraAccessException message = " + e.getMessage());
+        }
+    }
+
+    /**
+     * Opens the camera specified by Camera CameraId.
+     */
+    private void openCamera(int width, int height, boolean toggleCamera) {
+        if (ContextCompat.checkSelfPermission(getActivity(), Manifest.permission.CAMERA)
+                != PackageManager.PERMISSION_GRANTED) {
+            requestCameraPermission();
+            return;
+        }
+        setUpCameraOutputs(width, height, toggleCamera);
+        try {
+            if (!mCameraOpenCloseLock.tryAcquire(2500, TimeUnit.MILLISECONDS)) {
+                throw new RuntimeException("Time out waiting to lock camera opening.");
+            }
+            mCameraManager.openCamera(mCameraId, mStateCallback, null);
+        } catch (CameraAccessException e) {
+            Log.e(TAG, "openCamera CameraAccessException message = " + e.getMessage());
+        } catch (InterruptedException e) {
+            throw new RuntimeException("Interrupted while trying to lock camera opening.", e);
+        }
+    }
+
+    /**
+     * Closes the current {@link CameraDevice}.
+     */
+    private void closeCamera() {
+        try {
+            mCameraOpenCloseLock.acquire();
+            if (null != mCaptureSession) {
+                mCaptureSession.close();
+                mCaptureSession = null;
+            }
+            if (null != mCameraDevice) {
+                mCameraDevice.close();
+                mCameraDevice = null;
+            }
+            if (null != mImageReader) {
+                mImageReader.close();
+                mImageReader = null;
+            }
+        } catch (InterruptedException e) {
+            throw new RuntimeException("Interrupted while trying to lock camera closing.", e);
+        } finally {
+            mCameraOpenCloseLock.release();
+        }
+    }
+
+    /**
+     * Starts a background thread and its {@link Handler}.
+     */
+    private void startBackgroundThread() {
+        mBackgroundThread = new HandlerThread("CameraBackground");
+        mBackgroundThread.start();
+        mBackgroundHandler = new Handler(mBackgroundThread.getLooper());
+    }
+
+    /**
+     * Stops the background thread and its {@link Handler}.
+     */
+    private void stopBackgroundThread() {
+        mBackgroundThread.quitSafely();
+        try {
+            mBackgroundThread.join();
+            mBackgroundThread = null;
+            mBackgroundHandler = null;
+        } catch (InterruptedException e) {
+            Log.e(TAG, "stopBackgroundThread InterruptedException message = "
+                    + e.getMessage());
+        }
+    }
+
+    /**
+     * Creates a new {@link CameraCaptureSession} for camera preview.
+     */
+    private void createCameraPreviewSession() {
+        ListenableFuture<Surface> surfaceListenableFuture =
+                mCameraViewFinder.requestSurfaceAsync(mPreviewSurfaceRequest);
+
+        Futures.addCallback(surfaceListenableFuture, new FutureCallback<Surface>() {
+            @Override
+            public void onSuccess(@Nullable Surface surface) {
+                Log.d(TAG,
+                        "request onSurfaceAvailable surface = " + surface);
+                if (surface != null) {
+                    createCaptureSession(surface);
+                }
+            }
+
+            @Override
+            public void onFailure(Throwable t) {
+                Log.e(TAG, "request onSurfaceClosed");
+            }
+        }, ContextCompat.getMainExecutor(getContext()));
+    }
+
+    private void createCaptureSession(@NonNull Surface surface) {
+        try {
+            mPreviewRequestBuilder =
+                    mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
+            mPreviewRequestBuilder.addTarget(surface);
+
+            mCameraDevice.createCaptureSession(Arrays.asList(surface, mImageReader.getSurface()),
+                    new CameraCaptureSession.StateCallback() {
+
+                        @Override
+                        public void onConfigured(
+                                @NonNull CameraCaptureSession cameraCaptureSession) {
+                            // The camera is already closed
+                            if (null == mCameraDevice) {
+                                return;
+                            }
+
+                            // When the session is ready, we start displaying the preview.
+                            mCaptureSession = cameraCaptureSession;
+                            try {
+                                // Auto focus should be continuous for camera preview.
+                                mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AF_MODE,
+                                        CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE);
+                                // Flash is automatically enabled when necessary.
+                                setAutoFlash(mPreviewRequestBuilder);
+
+                                // Finally, we start displaying the camera preview.
+                                mPreviewRequest = mPreviewRequestBuilder.build();
+                                mCaptureSession.setRepeatingRequest(mPreviewRequest,
+                                        mCaptureCallback, null);
+                            } catch (CameraAccessException e) {
+                                Log.e(TAG, "onConfigured CameraAccessException message = "
+                                        + e.getMessage());
+                            }
+                        }
+
+                        @Override
+                        public void onConfigureFailed(
+                                @NonNull CameraCaptureSession cameraCaptureSession) {
+                            showToast("Failed");
+                        }
+                    }, null
+            );
+        } catch (CameraAccessException e) {
+            Log.e(TAG, "createCaptureSession CameraAccessException message = " + e.getMessage());
+        }
+    }
+
+    /**
+     * Initiate a still image capture.
+     */
+    private void takePicture() {
+        lockFocus();
+    }
+
+    /**
+     * Toggle the front and back camera
+     */
+    private void toggleCamera() {
+        closeCamera();
+        openCamera(mCameraViewFinder.getWidth(), mCameraViewFinder.getHeight(), true);
+    }
+
+    /**
+     * Lock the focus as the first step for a still image capture.
+     */
+    private void lockFocus() {
+        try {
+            // This is how to tell the camera to lock focus.
+            mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AF_TRIGGER,
+                    CameraMetadata.CONTROL_AF_TRIGGER_START);
+            // Tell #mCaptureCallback to wait for the lock.
+            mState = STATE_WAITING_LOCK;
+            mCaptureSession.capture(mPreviewRequestBuilder.build(), mCaptureCallback,
+                    null);
+        } catch (CameraAccessException e) {
+            Log.e(TAG, "lockFocus CameraAccessException message = " + e.getMessage());
+        }
+    }
+
+    /**
+     * Run the precapture sequence for capturing a still image. This method should be called when
+     * we get a response in {@link #mCaptureCallback} from {@link #lockFocus()}.
+     */
+    private void runPrecaptureSequence() {
+        try {
+            // This is how to tell the camera to trigger.
+            mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER,
+                    CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_START);
+            // Tell #mCaptureCallback to wait for the precapture sequence to be set.
+            mState = STATE_WAITING_PRECAPTURE;
+            mCaptureSession.capture(mPreviewRequestBuilder.build(), mCaptureCallback,
+                    null);
+        } catch (CameraAccessException e) {
+            Log.e(TAG, "runPrecaptureSequence CameraAccessException message = " + e.getMessage());
+        }
+    }
+
+    /**
+     * Capture a still picture. This method should be called when we get a response in
+     * {@link #mCaptureCallback} from both {@link #lockFocus()}.
+     */
+    private void captureStillPicture() {
+        try {
+            final Activity activity = getActivity();
+            if (null == activity || null == mCameraDevice) {
+                return;
+            }
+            // This is the CaptureRequest.Builder that we use to take a picture.
+            final CaptureRequest.Builder captureBuilder =
+                    mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE);
+            captureBuilder.addTarget(mImageReader.getSurface());
+
+            // Use the same AE and AF modes as the preview.
+            captureBuilder.set(CaptureRequest.CONTROL_AF_MODE,
+                    CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE);
+            setAutoFlash(captureBuilder);
+
+            // Orientation
+            int rotation = activity.getWindowManager().getDefaultDisplay().getRotation();
+            captureBuilder.set(CaptureRequest.JPEG_ORIENTATION, getOrientation(rotation));
+
+            CameraCaptureSession.CaptureCallback captureCallback =
+                    new CameraCaptureSession.CaptureCallback() {
+
+                @Override
+                public void onCaptureCompleted(@NonNull CameraCaptureSession session,
+                        @NonNull CaptureRequest request,
+                        @NonNull TotalCaptureResult result) {
+                    showToast("Saved: " + mFile);
+                    Log.d(TAG, mFile.toString());
+                    unlockFocus();
+                }
+            };
+
+            mCaptureSession.stopRepeating();
+            mCaptureSession.abortCaptures();
+            mCaptureSession.capture(captureBuilder.build(), captureCallback, null);
+        } catch (CameraAccessException e) {
+            Log.e(TAG, "captureStillPicture CameraAccessException message = " + e.getMessage());
+        }
+    }
+
+    /**
+     * Retrieves the JPEG orientation from the specified screen rotation.
+     *
+     * @param rotation The screen rotation.
+     * @return The JPEG orientation (one of 0, 90, 270, and 360)
+     */
+    private int getOrientation(int rotation) {
+        // Sensor orientation is 90 for most devices, or 270 for some devices (eg. Nexus 5X)
+        // We have to take that into account and rotate JPEG properly.
+        // For devices with orientation of 90, we simply return our mapping from ORIENTATIONS.
+        // For devices with orientation of 270, we need to rotate the JPEG 180 degrees.
+        return (ORIENTATIONS.get(rotation) + mSensorOrientation + 270) % 360;
+    }
+
+    /**
+     * Unlock the focus. This method should be called when still image capture sequence is
+     * finished.
+     */
+    private void unlockFocus() {
+        try {
+            // Reset the auto-focus trigger
+            mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AF_TRIGGER,
+                    CameraMetadata.CONTROL_AF_TRIGGER_CANCEL);
+            setAutoFlash(mPreviewRequestBuilder);
+            mCaptureSession.capture(mPreviewRequestBuilder.build(), mCaptureCallback,
+                    null);
+            // After this, the camera will go back to the normal state of preview.
+            mState = STATE_PREVIEW;
+            mCaptureSession.setRepeatingRequest(mPreviewRequest, mCaptureCallback,
+                    null);
+        } catch (CameraAccessException e) {
+            Log.e(TAG, "unlockFocus CameraAccessException message = " + e.getMessage());
+        }
+    }
+
+    @Override
+    public void onClick(View view) {
+        switch (view.getId()) {
+            case R.id.picture: {
+                takePicture();
+                break;
+            }
+            case R.id.toggle: {
+                toggleCamera();
+                break;
+            }
+            default:
+                break;
+        }
+    }
+
+    private void setAutoFlash(CaptureRequest.Builder requestBuilder) {
+        if (mFlashSupported) {
+            requestBuilder.set(CaptureRequest.CONTROL_AE_MODE,
+                    CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH);
+        }
+    }
+
+    /**
+     * Saves a JPEG {@link Image} into the specified {@link File}.
+     */
+    private static class ImageSaver implements Runnable {
+
+        /**
+         * The JPEG image
+         */
+        private final Image mImage;
+        /**
+         * The file we save the image into.
+         */
+        private final File mFile;
+
+        ImageSaver(Image image, File file) {
+            mImage = image;
+            mFile = file;
+        }
+
+        @Override
+        public void run() {
+            ByteBuffer buffer = mImage.getPlanes()[0].getBuffer();
+            byte[] bytes = new byte[buffer.remaining()];
+            buffer.get(bytes);
+
+            try (Image imageToClose = mImage;
+                 FileOutputStream output = new FileOutputStream(mFile)) {
+                output.write(bytes);
+            } catch (IOException e) {
+                Log.e(TAG, "ImageSaver CameraAccessException message = " + e.getMessage());
+            }
+        }
+    }
+
+    /**
+     * Compares two {@code Size}s based on their areas.
+     */
+    static class CompareSizesByArea implements Comparator<Size> {
+
+        @Override
+        public int compare(Size lhs, Size rhs) {
+            // We cast here to ensure the multiplications won't overflow
+            return Long.signum((long) lhs.getWidth() * lhs.getHeight()
+                    - (long) rhs.getWidth() * rhs.getHeight());
+        }
+
+    }
+
+    /**
+     * Shows an error message dialog.
+     */
+    public static class ErrorDialog extends DialogFragment {
+
+        private static final String ARG_MESSAGE = "message";
+
+        /**
+         * Returns {@link ErrorDialog} instance.
+         * @param message text message to show.
+         * @return {@link ErrorDialog}.
+         */
+        @NonNull
+        public static ErrorDialog newInstance(@NonNull String message) {
+            ErrorDialog dialog = new ErrorDialog();
+            Bundle args = new Bundle();
+            args.putString(ARG_MESSAGE, message);
+            dialog.setArguments(args);
+            return dialog;
+        }
+
+        @NonNull
+        @Override
+        public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
+            final Activity activity = getActivity();
+            return new AlertDialog.Builder(activity)
+                    .setMessage(getArguments().getString(ARG_MESSAGE))
+                    .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
+                        @Override
+                        public void onClick(DialogInterface dialogInterface, int i) {
+                            activity.finish();
+                        }
+                    })
+                    .create();
+        }
+    }
+
+    /**
+     * Shows OK/Cancel confirmation dialog about camera permission.
+     */
+    public static class ConfirmationDialog extends DialogFragment {
+
+        @NonNull
+        @Override
+        public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
+            final Fragment parent = getParentFragment();
+            return new AlertDialog.Builder(getActivity())
+                    .setMessage(R.string.request_permission)
+                    .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
+                        @Override
+                        public void onClick(DialogInterface dialog, int which) {
+                            parent.requestPermissions(new String[]{Manifest.permission.CAMERA},
+                                    REQUEST_CAMERA_PERMISSION);
+                        }
+                    })
+                    .setNegativeButton(android.R.string.cancel,
+                            new DialogInterface.OnClickListener() {
+                                @Override
+                                public void onClick(DialogInterface dialog, int which) {
+                                    Activity activity = parent.getActivity();
+                                    if (activity != null) {
+                                        activity.finish();
+                                    }
+                                }
+                            })
+                    .create();
+        }
+    }
+}
diff --git a/camera/integration-tests/previewviewtestapp/src/main/res/layout/activity_camera.xml b/camera/integration-tests/previewviewtestapp/src/main/res/layout/activity_camera.xml
new file mode 100644
index 0000000..8005442
--- /dev/null
+++ b/camera/integration-tests/previewviewtestapp/src/main/res/layout/activity_camera.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+ Copyright 2022 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+     http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<FrameLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/container"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:background="#000"/>
diff --git a/camera/integration-tests/previewviewtestapp/src/main/res/layout/fragment_camera_view_finder.xml b/camera/integration-tests/previewviewtestapp/src/main/res/layout/fragment_camera_view_finder.xml
new file mode 100644
index 0000000..33a2e58
--- /dev/null
+++ b/camera/integration-tests/previewviewtestapp/src/main/res/layout/fragment_camera_view_finder.xml
@@ -0,0 +1,57 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  Copyright 2022 The Android Open Source Project
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+  -->
+<RelativeLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    xmlns:app="http://schemas.android.com/apk/res-auto">
+
+    <androidx.camera.previewview.CameraViewFinder
+        android:id="@+id/view_finder"
+        app:scaleType="fitCenter"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"/>
+
+    <RelativeLayout
+        android:id="@+id/control"
+        android:layout_width="match_parent"
+        android:layout_height="112dp"
+        android:gravity="center|bottom"
+        android:layout_alignParentBottom="true"
+        android:layout_alignParentStart="true">
+
+        <Button
+            android:id="@+id/picture"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_margin="20dp"
+            android:padding="20dp"
+            android:layout_gravity="center"
+            android:text="@string/picture" />
+
+        <Button
+            android:id="@+id/toggle"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_gravity="center"
+            android:layout_toRightOf="@+id/picture"
+            android:layout_margin="20dp"
+            android:padding="20dp"
+            android:text="@string/toggle" />
+
+    </RelativeLayout>
+
+</RelativeLayout>
\ No newline at end of file
diff --git a/camera/integration-tests/previewviewtestapp/src/main/res/values/donottranslate-strings.xml b/camera/integration-tests/previewviewtestapp/src/main/res/values/donottranslate-strings.xml
new file mode 100644
index 0000000..a579d51
--- /dev/null
+++ b/camera/integration-tests/previewviewtestapp/src/main/res/values/donottranslate-strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 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.
+-->
+<resources xmlns:tools="http://schemas.android.com/tools">
+
+    <string name="app_name">CameraViewFinderTest</string>
+    <string name="intro_message">This sample demonstrates the basic use of CameraViewFinder API.</string>
+    <string name="picture">Picture</string>
+    <string name="toggle">Toggle</string>
+    <string name="description_info">Info</string>
+    <string name="request_permission">This sample needs camera permission.</string>
+
+</resources>
diff --git a/camera/integration-tests/previewviewtestapp/src/main/res/values/styles.xml b/camera/integration-tests/previewviewtestapp/src/main/res/values/styles.xml
new file mode 100644
index 0000000..b6303c4
--- /dev/null
+++ b/camera/integration-tests/previewviewtestapp/src/main/res/values/styles.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+ Copyright 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.
+-->
+<resources>
+    <style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar" />
+</resources>
diff --git a/car/app/app-samples/showcase/automotive/src/main/AndroidManifest.xml b/car/app/app-samples/showcase/automotive/src/main/AndroidManifest.xml
index e15984a..a9298ff 100644
--- a/car/app/app-samples/showcase/automotive/src/main/AndroidManifest.xml
+++ b/car/app/app-samples/showcase/automotive/src/main/AndroidManifest.xml
@@ -108,11 +108,15 @@
         android:exported="true"
         android:launchMode="singleTask"
         android:label="Showcase">
-
       <intent-filter>
         <action android:name="android.intent.action.MAIN" />
         <category android:name="android.intent.category.LAUNCHER" />
       </intent-filter>
+      <intent-filter>
+        <action android:name="androidx.car.app.action.NAVIGATE" />
+        <category android:name="android.intent.category.DEFAULT"/>
+        <data android:scheme="geo" />
+      </intent-filter>
       <meta-data android:name="distractionOptimized" android:value="true"/>
     </activity>
 
@@ -122,17 +126,10 @@
         android:exported="true"
         android:launchMode="singleTask"
         android:label="Showcase - Debug">
-
       <intent-filter>
         <action android:name="android.intent.action.MAIN" />
         <category android:name="android.intent.category.LAUNCHER" />
       </intent-filter>
-
-      <intent-filter>
-        <action android:name="androidx.car.app.action.NAVIGATE" />
-        <category android:name="android.intent.category.DEFAULT"/>
-        <data android:scheme="geo" />
-      </intent-filter>
     </activity>
 
   </application>
diff --git a/car/app/app-samples/showcase/automotive/src/main/AndroidManifestWithSdkVersion.xml b/car/app/app-samples/showcase/automotive/src/main/AndroidManifestWithSdkVersion.xml
index 54b62dca..13e311ad 100644
--- a/car/app/app-samples/showcase/automotive/src/main/AndroidManifestWithSdkVersion.xml
+++ b/car/app/app-samples/showcase/automotive/src/main/AndroidManifestWithSdkVersion.xml
@@ -115,11 +115,15 @@
         android:exported="true"
         android:launchMode="singleTask"
         android:label="Showcase">
-
       <intent-filter>
         <action android:name="android.intent.action.MAIN" />
         <category android:name="android.intent.category.LAUNCHER" />
       </intent-filter>
+      <intent-filter>
+        <action android:name="androidx.car.app.action.NAVIGATE" />
+        <category android:name="android.intent.category.DEFAULT"/>
+        <data android:scheme="geo" />
+      </intent-filter>
       <meta-data android:name="distractionOptimized" android:value="true"/>
     </activity>
 
@@ -129,16 +133,10 @@
         android:exported="true"
         android:launchMode="singleTask"
         android:label="Showcase - Debug">
-
       <intent-filter>
         <action android:name="android.intent.action.MAIN" />
         <category android:name="android.intent.category.LAUNCHER" />
       </intent-filter>
-      <intent-filter>
-        <action android:name="androidx.car.app.action.NAVIGATE" />
-        <category android:name="android.intent.category.DEFAULT"/>
-        <data android:scheme="geo" />
-      </intent-filter>
     </activity>
 
   </application>
diff --git a/compose/foundation/foundation/api/public_plus_experimental_current.txt b/compose/foundation/foundation/api/public_plus_experimental_current.txt
index 90aafa9..04cf3b3 100644
--- a/compose/foundation/foundation/api/public_plus_experimental_current.txt
+++ b/compose/foundation/foundation/api/public_plus_experimental_current.txt
@@ -482,7 +482,7 @@
   }
 
   public final class LazyGridKt {
-    method @androidx.compose.foundation.ExperimentalFoundationApi @androidx.compose.runtime.Composable public static void LazyVerticalGrid(androidx.compose.foundation.lazy.GridCells cells, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.foundation.lazy.LazyGridState state, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.foundation.layout.Arrangement.Vertical verticalArrangement, optional androidx.compose.foundation.layout.Arrangement.Horizontal horizontalArrangement, optional androidx.compose.foundation.gestures.FlingBehavior flingBehavior, optional boolean userScrollEnabled, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.lazy.LazyGridScope,kotlin.Unit> content);
+    method @androidx.compose.foundation.ExperimentalFoundationApi @androidx.compose.runtime.Composable public static void LazyVerticalGrid(androidx.compose.foundation.lazy.GridCells cells, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.foundation.lazy.LazyGridState state, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional boolean reverseLayout, optional androidx.compose.foundation.layout.Arrangement.Vertical verticalArrangement, optional androidx.compose.foundation.layout.Arrangement.Horizontal horizontalArrangement, optional androidx.compose.foundation.gestures.FlingBehavior flingBehavior, optional boolean userScrollEnabled, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.lazy.LazyGridScope,kotlin.Unit> content);
     method @androidx.compose.foundation.ExperimentalFoundationApi public static inline <T> void items(androidx.compose.foundation.lazy.LazyGridScope, java.util.List<? extends T> items, optional kotlin.jvm.functions.Function1<? super T,?>? key, optional kotlin.jvm.functions.Function2<? super androidx.compose.foundation.lazy.LazyGridItemSpanScope,? super T,androidx.compose.foundation.lazy.GridItemSpan>? span, optional kotlin.jvm.functions.Function1<? super T,?> contentType, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.lazy.LazyGridItemScope,? super T,kotlin.Unit> itemContent);
     method @androidx.compose.foundation.ExperimentalFoundationApi public static inline <T> void items(androidx.compose.foundation.lazy.LazyGridScope, T![] items, optional kotlin.jvm.functions.Function1<? super T,?>? key, optional kotlin.jvm.functions.Function2<? super androidx.compose.foundation.lazy.LazyGridItemSpanScope,? super T,androidx.compose.foundation.lazy.GridItemSpan>? span, optional kotlin.jvm.functions.Function1<? super T,?> contentType, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.lazy.LazyGridItemScope,? super T,kotlin.Unit> itemContent);
     method @androidx.compose.foundation.ExperimentalFoundationApi public static inline <T> void itemsIndexed(androidx.compose.foundation.lazy.LazyGridScope, java.util.List<? extends T> items, optional kotlin.jvm.functions.Function2<? super java.lang.Integer,? super T,?>? key, optional kotlin.jvm.functions.Function3<? super androidx.compose.foundation.lazy.LazyGridItemSpanScope,? super java.lang.Integer,? super T,androidx.compose.foundation.lazy.GridItemSpan>? span, optional kotlin.jvm.functions.Function2<? super java.lang.Integer,? super T,?> contentType, kotlin.jvm.functions.Function3<? super androidx.compose.foundation.lazy.LazyGridItemScope,? super java.lang.Integer,? super T,kotlin.Unit> itemContent);
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/grid/LazyArrangementsTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/grid/LazyArrangementsTest.kt
index 446fcf9..0873c9f 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/grid/LazyArrangementsTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/grid/LazyArrangementsTest.kt
@@ -442,21 +442,22 @@
 
     // with reverseLayout == true
 
-    // @Test
-    // fun vertical_defaultArrangementIsBottomWithReverseLayout() {
-    //     rule.setContent {
-    //         LazyColumn(
-    //             reverseLayout = true,
-    //             modifier = Modifier.requiredSize(containerSize)
-    //         ) {
-    //             items(2) {
-    //                 Item(it)
-    //             }
-    //         }
-    //     }
+    @Test
+    fun vertical_defaultArrangementIsBottomWithReverseLayout() {
+        rule.setContent {
+            LazyVerticalGrid(
+                GridCells.Fixed(1),
+                reverseLayout = true,
+                modifier = Modifier.size(containerSize)
+            ) {
+                items(2) {
+                    Item(it)
+                }
+            }
+        }
 
-    //     assertArrangementForTwoItems(Arrangement.Bottom, reverseLayout = true)
-    // }
+        assertArrangementForTwoItems(Arrangement.Bottom, reverseLayout = true)
+    }
 
     // @Test
     // fun row_defaultArrangementIsEndWithReverseLayout() {
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/grid/LazyGridLayoutInfoTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/grid/LazyGridLayoutInfoTest.kt
index a938ed8..27d78ef 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/grid/LazyGridLayoutInfoTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/grid/LazyGridLayoutInfoTest.kt
@@ -47,18 +47,20 @@
 import org.junit.Before
 import org.junit.Rule
 import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
 
 @MediumTest
 @OptIn(ExperimentalFoundationApi::class)
-// @RunWith(Parameterized::class)
+@RunWith(Parameterized::class)
 class LazyGridLayoutInfoTest(
-    // private val reverseLayout: Boolean = false
+    private val reverseLayout: Boolean
 ) {
-    // companion object {
-    //     @JvmStatic
-    //     @Parameterized.Parameters(name = "reverseLayout={0}")
-    //     fun initParameters(): Array<Any> = arrayOf(false, true)
-    // }
+    companion object {
+        @JvmStatic
+        @Parameterized.Parameters(name = "reverseLayout={0}")
+        fun initParameters(): Array<Any> = arrayOf(false, true)
+    }
 
     @get:Rule
     val rule = createComposeRule()
@@ -82,7 +84,7 @@
         rule.setContent {
             LazyVerticalGrid(
                 state = rememberLazyGridState().also { state = it },
-                // reverseLayout = reverseLayout,
+                reverseLayout = reverseLayout,
                 modifier = Modifier.width(gridWidthDp).height(itemSizeDp * 3.5f),
                 cells = GridCells.Fixed(2)
             ) {
@@ -103,7 +105,7 @@
         rule.setContent {
             LazyVerticalGrid(
                 state = rememberLazyGridState().also { state = it },
-                // reverseLayout = reverseLayout,
+                reverseLayout = reverseLayout,
                 modifier = Modifier.width(gridWidthDp).height(itemSizeDp * 3.5f),
                 cells = GridCells.Fixed(2)
             ) {
@@ -128,7 +130,7 @@
         rule.setContent {
             LazyVerticalGrid(
                 state = rememberLazyGridState().also { state = it },
-                // reverseLayout = reverseLayout,
+                reverseLayout = reverseLayout,
                 verticalArrangement = Arrangement.spacedBy(itemSizeDp),
                 modifier = Modifier.width(itemSizeDp).height(itemSizeDp * 3.5f),
                 cells = GridCells.Fixed(1)
@@ -155,7 +157,7 @@
         rule.setContent {
             LazyVerticalGrid(
                 state = rememberLazyGridState().also { state = it },
-                // reverseLayout = reverseLayout,
+                reverseLayout = reverseLayout,
                 modifier = Modifier.size(itemSizeDp * 2f, itemSizeDp * 3.5f),
                 cells = GridCells.Fixed(2)
             ) {
@@ -193,7 +195,7 @@
         rule.setContent {
             LazyVerticalGrid(
                 modifier = Modifier.width(itemSizeDp),
-                // reverseLayout = reverseLayout,
+                reverseLayout = reverseLayout,
                 state = rememberLazyGridState().also { state = it },
                 cells = GridCells.Fixed(1)
             ) {
@@ -231,7 +233,7 @@
         lateinit var state: LazyGridState
         rule.setContent {
             LazyVerticalGrid(
-                // reverseLayout = reverseLayout,
+                reverseLayout = reverseLayout,
                 state = rememberLazyGridState().also { state = it },
                 cells = GridCells.Fixed(2)
             ) {
@@ -259,7 +261,7 @@
         rule.setContent {
             LazyVerticalGrid(
                 modifier = Modifier.height(sizeDp).width(sizeDp * 2),
-                // reverseLayout = reverseLayout,
+                reverseLayout = reverseLayout,
                 state = rememberLazyGridState().also { state = it },
                 cells = GridCells.Fixed(2)
             ) {
@@ -283,10 +285,10 @@
         val endPaddingPx = 15
         val sizeDp = with(rule.density) { sizePx.toDp() }
         val topPaddingDp = with(rule.density) {
-            if (!false/*reverseLayout*/) startPaddingPx.toDp() else endPaddingPx.toDp()
+            if (!reverseLayout) startPaddingPx.toDp() else endPaddingPx.toDp()
         }
         val bottomPaddingDp = with(rule.density) {
-            if (!false/*reverseLayout*/) endPaddingPx.toDp() else startPaddingPx.toDp()
+            if (!reverseLayout) endPaddingPx.toDp() else startPaddingPx.toDp()
         }
         lateinit var state: LazyGridState
         rule.setContent {
@@ -298,7 +300,7 @@
                     start = 2.dp,
                     end = 2.dp
                 ),
-                // reverseLayout = reverseLayout,
+                reverseLayout = reverseLayout,
                 state = rememberLazyGridState().also { state = it },
                 cells = GridCells.Fixed(2)
             ) {
@@ -311,8 +313,7 @@
         rule.runOnIdle {
             assertThat(state.layoutInfo.viewportStartOffset).isEqualTo(-startPaddingPx)
             assertThat(state.layoutInfo.viewportEndOffset).isEqualTo(sizePx - startPaddingPx)
-            // TODO(b/211753558) currently failing because we need to port aosp/1903956 to grids
-            // assertThat(state.layoutInfo.viewportSize).isEqualTo(IntSize(sizePx * 2, sizePx))
+            assertThat(state.layoutInfo.viewportSize).isEqualTo(IntSize(sizePx * 2, sizePx))
         }
     }
 
@@ -342,7 +343,7 @@
         rule.setContent {
             LazyVerticalGrid(
                 state = rememberLazyGridState().also { state = it },
-                // reverseLayout = reverseLayout,
+                reverseLayout = reverseLayout,
                 modifier = Modifier.width(gridWidthDp).height(itemSizeDp * 3.5f),
                 cells = GridCells.Fixed(2)
             ) {
@@ -353,7 +354,7 @@
         }
 
         rule.runOnIdle {
-            assertThat(state.layoutInfo.reverseLayout).isEqualTo(false/*reverseLayout*/)
+            assertThat(state.layoutInfo.reverseLayout).isEqualTo(reverseLayout)
         }
     }
 
@@ -363,7 +364,7 @@
         rule.setContent {
             LazyVerticalGrid(
                 state = rememberLazyGridState().also { state = it },
-                // reverseLayout = reverseLayout,
+                reverseLayout = reverseLayout,
                 modifier = Modifier.width(gridWidthDp).height(itemSizeDp * 3.5f),
                 cells = GridCells.Fixed(2)
             ) {
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/grid/LazyGridPrefetcherTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/grid/LazyGridPrefetcherTest.kt
index aa97dda..e7d6655 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/grid/LazyGridPrefetcherTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/grid/LazyGridPrefetcherTest.kt
@@ -209,37 +209,45 @@
             .assertDoesNotExist()
     }
 
-    // @Test
-    // fun prefetchingForwardAndBackwardReverseLayout() {
-    //     composeList(firstItem = 1, reverseLayout = true)
+    @Test
+    fun prefetchingForwardAndBackwardReverseLayout() {
+        composeList(firstItem = 2, reverseLayout = true)
 
-    //     rule.runOnIdle {
-    //         runBlocking {
-    //             state.scrollBy(5f)
-    //         }
-    //     }
+        rule.runOnIdle {
+            runBlocking {
+                state.scrollBy(5f)
+            }
+        }
 
-    //     waitForPrefetch(3)
+        waitForPrefetch(6)
 
-    //     rule.onNodeWithTag("3")
-    //         .assertExists()
-    //     rule.onNodeWithTag("0")
-    //         .assertDoesNotExist()
+        rule.onNodeWithTag("6")
+            .assertExists()
+        rule.onNodeWithTag("7")
+            .assertExists()
+        rule.onNodeWithTag("0")
+            .assertDoesNotExist()
+        rule.onNodeWithTag("1")
+            .assertDoesNotExist()
 
-    //     rule.runOnIdle {
-    //         runBlocking {
-    //             state.scrollBy(-2f)
-    //             state.scrollBy(-1f)
-    //         }
-    //     }
+        rule.runOnIdle {
+            runBlocking {
+                state.scrollBy(-2f)
+                state.scrollBy(-1f)
+            }
+        }
 
-    //     waitForPrefetch(0)
+        waitForPrefetch(0)
 
-    //     rule.onNodeWithTag("0")
-    //         .assertExists()
-    //     rule.onNodeWithTag("3")
-    //         .assertDoesNotExist()
-    // }
+        rule.onNodeWithTag("0")
+            .assertExists()
+        rule.onNodeWithTag("1")
+            .assertExists()
+        rule.onNodeWithTag("6")
+            .assertDoesNotExist()
+        rule.onNodeWithTag("7")
+            .assertDoesNotExist()
+    }
 
     @Test
     fun prefetchingForwardAndBackwardWithContentPadding() {
@@ -347,7 +355,7 @@
     private fun composeList(
         firstItem: Int = 0,
         itemOffset: Int = 0,
-        // reverseLayout: Boolean = false,
+        reverseLayout: Boolean = false,
         contentPadding: PaddingValues = PaddingValues(0.dp)
     ) {
         rule.setContent {
@@ -359,7 +367,7 @@
                 GridCells.Fixed(2),
                 Modifier.height(itemsSizeDp * 1.5f),
                 state,
-                // reverseLayout = reverseLayout,
+                reverseLayout = reverseLayout,
                 contentPadding = contentPadding
             ) {
                 items(100) {
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/grid/LazyGridsContentPaddingTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/grid/LazyGridsContentPaddingTest.kt
index 56ca84da..6e61140 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/grid/LazyGridsContentPaddingTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/grid/LazyGridsContentPaddingTest.kt
@@ -31,10 +31,12 @@
 import androidx.compose.foundation.lazy.LazyVerticalGrid
 import androidx.compose.foundation.lazy.LazyGridState
 import androidx.compose.foundation.lazy.items
+import androidx.compose.foundation.lazy.list.setContentWithTestViewConfiguration
 import androidx.compose.foundation.lazy.rememberLazyGridState
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.platform.testTag
 import androidx.compose.ui.test.assertHeightIsEqualTo
+import androidx.compose.ui.test.assertIsNotDisplayed
 import androidx.compose.ui.test.assertLeftPositionInRootIsEqualTo
 import androidx.compose.ui.test.assertTopPositionInRootIsEqualTo
 import androidx.compose.ui.test.assertWidthIsEqualTo
@@ -376,41 +378,384 @@
             .assertHeightIsEqualTo(12.dp)
     }
 
-    // @Test
-    // fun verticalGrid_contentPaddingAndReverseLayout() {
-    //     val topPadding = itemSize * 2
-    //     val bottomPadding = itemSize / 2
-    //     val listSize = itemSize * 3
-    //     lateinit var state: LazyGridState
-    //     rule.setContentWithTestViewConfiguration {
-    //         LazyVerticalGrid(
-    //             reverseLayout = true,
-    //             state = rememberLazyGridState().also { state = it },
-    //             modifier = Modifier.requiredSize(listSize),
-    //             contentPadding = PaddingValues(top = topPadding, bottom = bottomPadding),
-    //         ) {
-    //             items(3) { index ->
-    //                 Box(Modifier.requiredSize(itemSize).testTag("$index"))
-    //             }
-    //         }
-    //     }
+    @Test
+    fun verticalGrid_contentPaddingAndReverseLayout() {
+        val topPadding = itemSize * 2
+        val bottomPadding = itemSize / 2
+        val listSize = itemSize * 3
+        lateinit var state: LazyGridState
+        rule.setContentWithTestViewConfiguration {
+            LazyVerticalGrid(
+                GridCells.Fixed(1),
+                reverseLayout = true,
+                state = rememberLazyGridState().also { state = it },
+                modifier = Modifier.size(listSize),
+                contentPadding = PaddingValues(top = topPadding, bottom = bottomPadding),
+            ) {
+                items(3) { index ->
+                    Box(Modifier.size(itemSize).testTag("$index"))
+                }
+            }
+        }
 
-    //     rule.onNodeWithTag("0")
-    //         .assertTopPositionInRootIsEqualTo(listSize - bottomPadding - itemSize)
-    //     rule.onNodeWithTag("1")
-    //         .assertTopPositionInRootIsEqualTo(listSize - bottomPadding - itemSize * 2)
-    //     // Partially visible.
-    //     rule.onNodeWithTag("2")
-    //         .assertTopPositionInRootIsEqualTo(-itemSize / 2)
+        rule.onNodeWithTag("0")
+            .assertTopPositionInRootIsEqualTo(listSize - bottomPadding - itemSize)
+        rule.onNodeWithTag("1")
+            .assertTopPositionInRootIsEqualTo(listSize - bottomPadding - itemSize * 2)
+        // Partially visible.
+        rule.onNodeWithTag("2")
+            .assertTopPositionInRootIsEqualTo(-itemSize / 2)
 
-    //     // Scroll to the top.
-    //     state.scrollBy(itemSize * 2.5f)
+        // Scroll to the top.
+        state.scrollBy(itemSize * 2.5f)
 
-    //     rule.onNodeWithTag("2").assertTopPositionInRootIsEqualTo(topPadding)
-    //     // Shouldn't be visible
-    //     rule.onNodeWithTag("1").assertIsNotDisplayed()
-    //     rule.onNodeWithTag("0").assertIsNotDisplayed()
-    // }
+        rule.onNodeWithTag("2").assertTopPositionInRootIsEqualTo(topPadding)
+        // Shouldn't be visible
+        rule.onNodeWithTag("1").assertIsNotDisplayed()
+        rule.onNodeWithTag("0").assertIsNotDisplayed()
+    }
+
+    @Test
+    fun totalPaddingLargerParentSize_initialState() {
+        lateinit var state: LazyGridState
+        rule.setContent {
+            state = rememberLazyGridState()
+            Box(modifier = Modifier.testTag(ContainerTag).size(itemSize * 1.5f)) {
+                LazyVerticalGrid(
+                    GridCells.Fixed(1),
+                    state = state,
+                    contentPadding = PaddingValues(vertical = itemSize)
+                ) {
+                    items(4) {
+                        Box(Modifier.testTag("$it").size(itemSize))
+                    }
+                }
+            }
+        }
+
+        rule.onNodeWithTag("0")
+            .assertTopPositionInRootIsEqualTo(itemSize)
+
+        rule.onNodeWithTag("1")
+            .assertDoesNotExist()
+
+        rule.runOnIdle {
+            state.assertScrollPosition(0, 0.dp)
+            state.assertVisibleItems(0 to 0.dp)
+            state.assertLayoutInfoOffsetRange(-itemSize, itemSize * 0.5f)
+        }
+    }
+
+    @Test
+    fun totalPaddingLargerParentSize_scrollByPadding() {
+        lateinit var state: LazyGridState
+        rule.setContent {
+            state = rememberLazyGridState()
+            Box(modifier = Modifier.testTag(ContainerTag).size(itemSize * 1.5f)) {
+                LazyVerticalGrid(
+                    GridCells.Fixed(1),
+                    state = state,
+                    contentPadding = PaddingValues(vertical = itemSize)
+                ) {
+                    items(4) {
+                        Box(Modifier.testTag("$it").size(itemSize))
+                    }
+                }
+            }
+        }
+
+        state.scrollBy(itemSize)
+
+        rule.onNodeWithTag("0")
+            .assertTopPositionInRootIsEqualTo(0.dp)
+
+        rule.onNodeWithTag("1")
+            .assertTopPositionInRootIsEqualTo(itemSize)
+
+        rule.onNodeWithTag("2")
+            .assertIsNotDisplayed()
+
+        rule.runOnIdle {
+            state.assertScrollPosition(1, 0.dp)
+            state.assertVisibleItems(0 to -itemSize, 1 to 0.dp)
+        }
+    }
+
+    @Test
+    fun totalPaddingLargerParentSize_scrollToLastItem() {
+        lateinit var state: LazyGridState
+        rule.setContent {
+            state = rememberLazyGridState()
+            Box(modifier = Modifier.testTag(ContainerTag).size(itemSize * 1.5f)) {
+                LazyVerticalGrid(
+                    GridCells.Fixed(1),
+                    state = state,
+                    contentPadding = PaddingValues(vertical = itemSize)
+                ) {
+                    items(4) {
+                        Box(Modifier.testTag("$it").size(itemSize))
+                    }
+                }
+            }
+        }
+
+        state.scrollTo(3)
+
+        rule.onNodeWithTag("1")
+            .assertDoesNotExist()
+
+        rule.onNodeWithTag("2")
+            .assertTopPositionInRootIsEqualTo(0.dp)
+
+        rule.onNodeWithTag("3")
+            .assertTopPositionInRootIsEqualTo(itemSize)
+
+        rule.runOnIdle {
+            state.assertScrollPosition(3, 0.dp)
+            state.assertVisibleItems(2 to -itemSize, 3 to 0.dp)
+        }
+    }
+
+    @Test
+    fun totalPaddingLargerParentSize_scrollToLastItemByDelta() {
+        lateinit var state: LazyGridState
+        rule.setContent {
+            state = rememberLazyGridState()
+            Box(modifier = Modifier.testTag(ContainerTag).size(itemSize * 1.5f)) {
+                LazyVerticalGrid(
+                    GridCells.Fixed(1),
+                    state = state,
+                    contentPadding = PaddingValues(vertical = itemSize)
+                ) {
+                    items(4) {
+                        Box(Modifier.testTag("$it").size(itemSize))
+                    }
+                }
+            }
+        }
+
+        state.scrollBy(itemSize * 3)
+
+        rule.onNodeWithTag("1")
+            .assertIsNotDisplayed()
+
+        rule.onNodeWithTag("2")
+            .assertTopPositionInRootIsEqualTo(0.dp)
+
+        rule.onNodeWithTag("3")
+            .assertTopPositionInRootIsEqualTo(itemSize)
+
+        rule.runOnIdle {
+            state.assertScrollPosition(3, 0.dp)
+            state.assertVisibleItems(2 to -itemSize, 3 to 0.dp)
+        }
+    }
+
+    @Test
+    fun totalPaddingLargerParentSize_scrollTillTheEnd() {
+        // the whole end content padding is displayed
+        lateinit var state: LazyGridState
+        rule.setContent {
+            state = rememberLazyGridState()
+            Box(modifier = Modifier.testTag(ContainerTag).size(itemSize * 1.5f)) {
+                LazyVerticalGrid(
+                    GridCells.Fixed(1),
+                    state = state,
+                    contentPadding = PaddingValues(vertical = itemSize)
+                ) {
+                    items(4) {
+                        Box(Modifier.testTag("$it").size(itemSize))
+                    }
+                }
+            }
+        }
+
+        state.scrollBy(itemSize * 4.5f)
+
+        rule.onNodeWithTag("2")
+            .assertIsNotDisplayed()
+
+        rule.onNodeWithTag("3")
+            .assertTopPositionInRootIsEqualTo(-itemSize * 0.5f)
+
+        rule.runOnIdle {
+            state.assertScrollPosition(3, itemSize * 1.5f)
+            state.assertVisibleItems(3 to -itemSize * 1.5f)
+        }
+    }
+
+    @Test
+    fun eachPaddingLargerParentSize_initialState() {
+        lateinit var state: LazyGridState
+        rule.setContent {
+            state = rememberLazyGridState()
+            Box(modifier = Modifier.testTag(ContainerTag).size(itemSize * 1.5f)) {
+                LazyVerticalGrid(
+                    GridCells.Fixed(1),
+                    state = state,
+                    contentPadding = PaddingValues(vertical = itemSize * 2)
+                ) {
+                    items(4) {
+                        Box(Modifier.testTag("$it").size(itemSize))
+                    }
+                }
+            }
+        }
+
+        rule.onNodeWithTag("0")
+            .assertIsNotDisplayed()
+
+        rule.runOnIdle {
+            state.assertScrollPosition(0, 0.dp)
+            state.assertVisibleItems(0 to 0.dp)
+            state.assertLayoutInfoOffsetRange(-itemSize * 2, -itemSize * 0.5f)
+        }
+    }
+
+    @Test
+    fun eachPaddingLargerParentSize_scrollByPadding() {
+        lateinit var state: LazyGridState
+        rule.setContent {
+            state = rememberLazyGridState()
+            Box(modifier = Modifier.testTag(ContainerTag).size(itemSize * 1.5f)) {
+                LazyVerticalGrid(
+                    GridCells.Fixed(1),
+                    state = state,
+                    contentPadding = PaddingValues(vertical = itemSize * 2)
+                ) {
+                    items(4) {
+                        Box(Modifier.testTag("$it").size(itemSize))
+                    }
+                }
+            }
+        }
+
+        state.scrollBy(itemSize * 2)
+
+        rule.onNodeWithTag("0")
+            .assertTopPositionInRootIsEqualTo(0.dp)
+
+        rule.onNodeWithTag("1")
+            .assertTopPositionInRootIsEqualTo(itemSize)
+
+        rule.onNodeWithTag("2")
+            .assertIsNotDisplayed()
+
+        rule.runOnIdle {
+            state.assertScrollPosition(2, 0.dp)
+            state.assertVisibleItems(0 to -itemSize * 2, 1 to -itemSize, 2 to 0.dp)
+        }
+    }
+
+    @Test
+    fun eachPaddingLargerParentSize_scrollToLastItem() {
+        lateinit var state: LazyGridState
+        rule.setContent {
+            state = rememberLazyGridState()
+            Box(modifier = Modifier.testTag(ContainerTag).size(itemSize * 1.5f)) {
+                LazyVerticalGrid(
+                    GridCells.Fixed(1),
+                    state = state,
+                    contentPadding = PaddingValues(vertical = itemSize * 2)
+                ) {
+                    items(4) {
+                        Box(Modifier.testTag("$it").size(itemSize))
+                    }
+                }
+            }
+        }
+
+        state.scrollTo(3)
+
+        rule.onNodeWithTag("0")
+            .assertIsNotDisplayed()
+
+        rule.onNodeWithTag("1")
+            .assertTopPositionInRootIsEqualTo(0.dp)
+
+        rule.onNodeWithTag("2")
+            .assertTopPositionInRootIsEqualTo(itemSize)
+
+        rule.onNodeWithTag("3")
+            .assertIsNotDisplayed()
+
+        rule.runOnIdle {
+            state.assertScrollPosition(3, 0.dp)
+            state.assertVisibleItems(1 to -itemSize * 2, 2 to -itemSize, 3 to 0.dp)
+        }
+    }
+
+    @Test
+    fun eachPaddingLargerParentSize_scrollToLastItemByDelta() {
+        lateinit var state: LazyGridState
+        rule.setContent {
+            state = rememberLazyGridState()
+            Box(modifier = Modifier.testTag(ContainerTag).size(itemSize * 1.5f)) {
+                LazyVerticalGrid(
+                    GridCells.Fixed(1),
+                    state = state,
+                    contentPadding = PaddingValues(vertical = itemSize * 2)
+                ) {
+                    items(4) {
+                        Box(Modifier.testTag("$it").size(itemSize))
+                    }
+                }
+            }
+        }
+
+        state.scrollBy(itemSize * 3)
+
+        rule.onNodeWithTag("0")
+            .assertIsNotDisplayed()
+
+        rule.onNodeWithTag("1")
+            .assertTopPositionInRootIsEqualTo(0.dp)
+
+        rule.onNodeWithTag("2")
+            .assertTopPositionInRootIsEqualTo(itemSize)
+
+        rule.onNodeWithTag("3")
+            .assertIsNotDisplayed()
+
+        rule.runOnIdle {
+            state.assertScrollPosition(3, 0.dp)
+            state.assertVisibleItems(1 to -itemSize * 2, 2 to -itemSize, 3 to 0.dp)
+        }
+    }
+
+    @Test
+    fun eachPaddingLargerParentSize_scrollTillTheEnd() {
+        // only the end content padding is displayed
+        lateinit var state: LazyGridState
+        rule.setContent {
+            state = rememberLazyGridState()
+            Box(modifier = Modifier.testTag(ContainerTag).size(itemSize * 1.5f)) {
+                LazyVerticalGrid(
+                    GridCells.Fixed(1),
+                    state = state,
+                    contentPadding = PaddingValues(vertical = itemSize * 2)
+                ) {
+                    items(4) {
+                        Box(Modifier.testTag("$it").size(itemSize))
+                    }
+                }
+            }
+        }
+
+        state.scrollBy(
+            itemSize * 1.5f + // container size
+                itemSize * 2 + // start padding
+                itemSize * 3 // all items
+        )
+
+        rule.onNodeWithTag("3")
+            .assertIsNotDisplayed()
+
+        rule.runOnIdle {
+            state.assertScrollPosition(3, itemSize * 3.5f)
+            state.assertVisibleItems(3 to -itemSize * 3.5f)
+        }
+    }
 
     // @Test
     // fun row_contentPaddingIsApplied() {
@@ -754,4 +1099,21 @@
         assertThat(this@assertScrollPosition.firstVisibleItemIndex).isEqualTo(index)
         assertThat(firstVisibleItemScrollOffset.toDp().value).isWithin(0.5f).of(offset.value)
     }
+
+    private fun LazyGridState.assertLayoutInfoOffsetRange(from: Dp, to: Dp) = with(rule.density) {
+        assertThat(layoutInfo.viewportStartOffset to layoutInfo.viewportEndOffset)
+            .isEqualTo(from.roundToPx() to to.roundToPx())
+    }
+
+    private fun LazyGridState.assertVisibleItems(vararg expected: Pair<Int, Dp>) =
+        with(rule.density) {
+            assertThat(layoutInfo.visibleItemsInfo.map { it.index to it.offset.y })
+                .isEqualTo(expected.map { it.first to it.second.roundToPx() })
+        }
+
+    fun LazyGridState.scrollTo(index: Int) {
+        runBlocking(Dispatchers.Main + AutoTestFrameClock()) {
+            scrollToItem(index)
+        }
+    }
 }
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/grid/LazyGridsReverseLayoutTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/grid/LazyGridsReverseLayoutTest.kt
new file mode 100644
index 0000000..b84adbd
--- /dev/null
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/grid/LazyGridsReverseLayoutTest.kt
@@ -0,0 +1,562 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.foundation.lazy.grid
+
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.requiredSize
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.lazy.GridCells
+import androidx.compose.foundation.lazy.LazyGridState
+import androidx.compose.foundation.lazy.LazyVerticalGrid
+import androidx.compose.foundation.lazy.items
+import androidx.compose.foundation.lazy.list.scrollBy
+import androidx.compose.foundation.lazy.list.setContentWithTestViewConfiguration
+import androidx.compose.foundation.lazy.rememberLazyGridState
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.assertLeftPositionInRootIsEqualTo
+import androidx.compose.ui.test.assertTopPositionInRootIsEqualTo
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+
+@OptIn(ExperimentalFoundationApi::class)
+class LazyGridsReverseLayoutTest {
+
+    private val ContainerTag = "ContainerTag"
+
+    @get:Rule
+    val rule = createComposeRule()
+
+    private var itemSize: Dp = Dp.Infinity
+
+    @Before
+    fun before() {
+        with(rule.density) {
+            itemSize = 50.toDp()
+        }
+    }
+
+    @Test
+    fun verticalGrid_reverseLayout() {
+        rule.setContentWithTestViewConfiguration {
+            LazyVerticalGrid(
+                GridCells.Fixed(2),
+                Modifier.width(itemSize * 2),
+                reverseLayout = true
+            ) {
+                items(4) {
+                    Box(Modifier.height(itemSize).testTag(it.toString()))
+                }
+            }
+        }
+
+        rule.onNodeWithTag("2")
+            .assertTopPositionInRootIsEqualTo(0.dp)
+            .assertLeftPositionInRootIsEqualTo(0.dp)
+        rule.onNodeWithTag("3")
+            .assertTopPositionInRootIsEqualTo(0.dp)
+            .assertLeftPositionInRootIsEqualTo(itemSize)
+        rule.onNodeWithTag("0")
+            .assertTopPositionInRootIsEqualTo(itemSize)
+            .assertLeftPositionInRootIsEqualTo(0.dp)
+        rule.onNodeWithTag("1")
+            .assertTopPositionInRootIsEqualTo(itemSize)
+            .assertLeftPositionInRootIsEqualTo(itemSize)
+    }
+
+    @Test
+    fun column_emitTwoElementsAsOneItem() {
+        rule.setContentWithTestViewConfiguration {
+            LazyVerticalGrid(
+                GridCells.Fixed(2),
+                Modifier.width(itemSize * 2),
+                reverseLayout = true
+            ) {
+                items(4) {
+                    Box(Modifier.height(itemSize).testTag((it * 2).toString()))
+                    Box(Modifier.height(itemSize).testTag((it * 2 + 1).toString()))
+                }
+            }
+        }
+
+        rule.onNodeWithTag("4")
+            .assertTopPositionInRootIsEqualTo(0.dp)
+            .assertLeftPositionInRootIsEqualTo(0.dp)
+        rule.onNodeWithTag("5")
+            .assertTopPositionInRootIsEqualTo(0.dp)
+            .assertLeftPositionInRootIsEqualTo(0.dp)
+        rule.onNodeWithTag("6")
+            .assertTopPositionInRootIsEqualTo(0.dp)
+            .assertLeftPositionInRootIsEqualTo(itemSize)
+        rule.onNodeWithTag("7")
+            .assertTopPositionInRootIsEqualTo(0.dp)
+            .assertLeftPositionInRootIsEqualTo(itemSize)
+        rule.onNodeWithTag("0")
+            .assertTopPositionInRootIsEqualTo(itemSize)
+            .assertLeftPositionInRootIsEqualTo(0.dp)
+        rule.onNodeWithTag("1")
+            .assertTopPositionInRootIsEqualTo(itemSize)
+            .assertLeftPositionInRootIsEqualTo(0.dp)
+        rule.onNodeWithTag("2")
+            .assertTopPositionInRootIsEqualTo(itemSize)
+            .assertLeftPositionInRootIsEqualTo(itemSize)
+        rule.onNodeWithTag("3")
+            .assertTopPositionInRootIsEqualTo(itemSize)
+            .assertLeftPositionInRootIsEqualTo(itemSize)
+    }
+
+    @Test
+    fun verticalGrid_initialScrollPositionIs0() {
+        lateinit var state: LazyGridState
+        rule.setContentWithTestViewConfiguration {
+            LazyVerticalGrid(
+                GridCells.Fixed(2),
+                reverseLayout = true,
+                state = rememberLazyGridState().also { state = it },
+                modifier = Modifier.size(itemSize * 2).testTag(ContainerTag)
+            ) {
+                items((0..5).toList()) {
+                    Box(Modifier.size(itemSize).testTag("$it"))
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            assertThat(state.firstVisibleItemScrollOffset).isEqualTo(0)
+            assertThat(state.firstVisibleItemIndex).isEqualTo(0)
+        }
+    }
+
+    @Test
+    fun verticalGrid_scrollInWrongDirectionDoesNothing() {
+        lateinit var state: LazyGridState
+        rule.setContentWithTestViewConfiguration {
+            LazyVerticalGrid(
+                GridCells.Fixed(1),
+                reverseLayout = true,
+                state = rememberLazyGridState().also { state = it },
+                modifier = Modifier.size(itemSize * 2).testTag(ContainerTag)
+            ) {
+                items((0..2).toList()) {
+                    Box(Modifier.size(itemSize).testTag("$it"))
+                }
+            }
+        }
+
+        // we scroll down and as the scrolling is reversed it shouldn't affect anything
+        rule.onNodeWithTag(ContainerTag)
+            .scrollBy(y = itemSize, density = rule.density)
+
+        rule.runOnIdle {
+            assertThat(state.firstVisibleItemScrollOffset).isEqualTo(0)
+            assertThat(state.firstVisibleItemIndex).isEqualTo(0)
+        }
+
+        rule.onNodeWithTag("1")
+            .assertTopPositionInRootIsEqualTo(0.dp)
+        rule.onNodeWithTag("0")
+            .assertTopPositionInRootIsEqualTo(itemSize)
+    }
+
+    @Test
+    fun verticalGrid_scrollForwardHalfWay() {
+        lateinit var state: LazyGridState
+        rule.setContentWithTestViewConfiguration {
+            LazyVerticalGrid(
+                GridCells.Fixed(1),
+                reverseLayout = true,
+                state = rememberLazyGridState().also { state = it },
+                modifier = Modifier.requiredSize(itemSize * 2).testTag(ContainerTag)
+            ) {
+                items((0..2).toList()) {
+                    Box(Modifier.requiredSize(itemSize).testTag("$it"))
+                }
+            }
+        }
+
+        rule.onNodeWithTag(ContainerTag)
+            .scrollBy(y = -itemSize * 0.5f, density = rule.density)
+
+        val scrolled = rule.runOnIdle {
+            assertThat(state.firstVisibleItemScrollOffset).isGreaterThan(0)
+            assertThat(state.firstVisibleItemIndex).isEqualTo(0)
+            with(rule.density) { state.firstVisibleItemScrollOffset.toDp() }
+        }
+
+        rule.onNodeWithTag("2")
+            .assertTopPositionInRootIsEqualTo(-itemSize + scrolled)
+        rule.onNodeWithTag("1")
+            .assertTopPositionInRootIsEqualTo(scrolled)
+        rule.onNodeWithTag("0")
+            .assertTopPositionInRootIsEqualTo(itemSize + scrolled)
+    }
+
+    @Test
+    fun verticalGrid_scrollForwardTillTheEnd() {
+        lateinit var state: LazyGridState
+        rule.setContentWithTestViewConfiguration {
+            LazyVerticalGrid(
+                GridCells.Fixed(1),
+                reverseLayout = true,
+                state = rememberLazyGridState().also { state = it },
+                modifier = Modifier.size(itemSize * 2).testTag(ContainerTag)
+            ) {
+                items((0..3).toList()) {
+                    Box(Modifier.size(itemSize).testTag("$it"))
+                }
+            }
+        }
+
+        // we scroll a bit more than it is possible just to make sure we would stop correctly
+        rule.onNodeWithTag(ContainerTag)
+            .scrollBy(y = -itemSize * 2.2f, density = rule.density)
+
+        rule.runOnIdle {
+            with(rule.density) {
+                val realOffset = state.firstVisibleItemScrollOffset.toDp() +
+                    itemSize * state.firstVisibleItemIndex
+                assertThat(realOffset).isEqualTo(itemSize * 2)
+            }
+        }
+
+        rule.onNodeWithTag("3")
+            .assertTopPositionInRootIsEqualTo(0.dp)
+        rule.onNodeWithTag("2")
+            .assertTopPositionInRootIsEqualTo(itemSize)
+    }
+
+    // @Test
+    // fun row_emitTwoElementsAsOneItem_positionedReversed() {
+    //     rule.setContentWithTestViewConfiguration {
+    //         LazyRow(
+    //             reverseLayout = true
+    //         ) {
+    //             item {
+    //                 Box(Modifier.requiredSize(itemSize).testTag("0"))
+    //                 Box(Modifier.requiredSize(itemSize).testTag("1"))
+    //             }
+    //         }
+    //     }
+
+    //     rule.onNodeWithTag("1")
+    //         .assertLeftPositionInRootIsEqualTo(0.dp)
+    //     rule.onNodeWithTag("0")
+    //         .assertLeftPositionInRootIsEqualTo(itemSize)
+    // }
+
+    // @Test
+    // fun row_emitTwoItems_positionedReversed() {
+    //     rule.setContentWithTestViewConfiguration {
+    //         LazyRow(
+    //             reverseLayout = true
+    //         ) {
+    //             item {
+    //                 Box(Modifier.requiredSize(itemSize).testTag("0"))
+    //             }
+    //             item {
+    //                 Box(Modifier.requiredSize(itemSize).testTag("1"))
+    //             }
+    //         }
+    //     }
+
+    //     rule.onNodeWithTag("1")
+    //         .assertLeftPositionInRootIsEqualTo(0.dp)
+    //     rule.onNodeWithTag("0")
+    //         .assertLeftPositionInRootIsEqualTo(itemSize)
+    // }
+
+    // @Test
+    // fun row_initialScrollPositionIs0() {
+    //     lateinit var state: LazyListState
+    //     rule.setContentWithTestViewConfiguration {
+    //         LazyRow(
+    //             reverseLayout = true,
+    //             state = rememberLazyListState().also { state = it },
+    //             modifier = Modifier.requiredSize(itemSize * 2).testTag(ContainerTag)
+    //         ) {
+    //             items((0..2).toList()) {
+    //                 Box(Modifier.requiredSize(itemSize).testTag("$it"))
+    //             }
+    //         }
+    //     }
+
+    //     rule.runOnIdle {
+    //         assertThat(state.firstVisibleItemScrollOffset).isEqualTo(0)
+    //         assertThat(state.firstVisibleItemIndex).isEqualTo(0)
+    //     }
+    // }
+
+    // @Test
+    // fun row_scrollInWrongDirectionDoesNothing() {
+    //     lateinit var state: LazyListState
+    //     rule.setContentWithTestViewConfiguration {
+    //         LazyRow(
+    //             reverseLayout = true,
+    //             state = rememberLazyListState().also { state = it },
+    //             modifier = Modifier.requiredSize(itemSize * 2).testTag(ContainerTag)
+    //         ) {
+    //             items((0..2).toList()) {
+    //                 Box(Modifier.requiredSize(itemSize).testTag("$it"))
+    //             }
+    //         }
+    //     }
+
+    //     // we scroll down and as the scrolling is reversed it shouldn't affect anything
+    //     rule.onNodeWithTag(ContainerTag)
+    //         .scrollBy(x = itemSize, density = rule.density)
+
+    //     rule.runOnIdle {
+    //         assertThat(state.firstVisibleItemScrollOffset).isEqualTo(0)
+    //         assertThat(state.firstVisibleItemIndex).isEqualTo(0)
+    //     }
+
+    //     rule.onNodeWithTag("1")
+    //         .assertLeftPositionInRootIsEqualTo(0.dp)
+    //     rule.onNodeWithTag("0")
+    //         .assertLeftPositionInRootIsEqualTo(itemSize)
+    // }
+
+    // @Test
+    // fun row_scrollForwardHalfWay() {
+    //     lateinit var state: LazyListState
+    //     rule.setContentWithTestViewConfiguration {
+    //         LazyRow(
+    //             reverseLayout = true,
+    //             state = rememberLazyListState().also { state = it },
+    //             modifier = Modifier.requiredSize(itemSize * 2).testTag(ContainerTag)
+    //         ) {
+    //             items((0..2).toList()) {
+    //                 Box(Modifier.requiredSize(itemSize).testTag("$it"))
+    //             }
+    //         }
+    //     }
+
+    //     rule.onNodeWithTag(ContainerTag)
+    //         .scrollBy(x = -itemSize * 0.5f, density = rule.density)
+
+    //     val scrolled = rule.runOnIdle {
+    //         assertThat(state.firstVisibleItemScrollOffset).isGreaterThan(0)
+    //         assertThat(state.firstVisibleItemIndex).isEqualTo(0)
+    //         with(rule.density) { state.firstVisibleItemScrollOffset.toDp() }
+    //     }
+
+    //     rule.onNodeWithTag("2")
+    //         .assertLeftPositionInRootIsEqualTo(-itemSize + scrolled)
+    //     rule.onNodeWithTag("1")
+    //         .assertLeftPositionInRootIsEqualTo(scrolled)
+    //     rule.onNodeWithTag("0")
+    //         .assertLeftPositionInRootIsEqualTo(itemSize + scrolled)
+    // }
+
+    // @Test
+    // fun row_scrollForwardTillTheEnd() {
+    //     lateinit var state: LazyListState
+    //     rule.setContentWithTestViewConfiguration {
+    //         LazyRow(
+    //             reverseLayout = true,
+    //             state = rememberLazyListState().also { state = it },
+    //             modifier = Modifier.requiredSize(itemSize * 2).testTag(ContainerTag)
+    //         ) {
+    //             items((0..3).toList()) {
+    //                 Box(Modifier.requiredSize(itemSize).testTag("$it"))
+    //             }
+    //         }
+    //     }
+
+    //     // we scroll a bit more than it is possible just to make sure we would stop correctly
+    //     rule.onNodeWithTag(ContainerTag)
+    //         .scrollBy(x = -itemSize * 2.2f, density = rule.density)
+
+    //     rule.runOnIdle {
+    //         with(rule.density) {
+    //             val realOffset = state.firstVisibleItemScrollOffset.toDp() +
+    //                 itemSize * state.firstVisibleItemIndex
+    //             assertThat(realOffset).isEqualTo(itemSize * 2)
+    //         }
+    //     }
+
+    //     rule.onNodeWithTag("3")
+    //         .assertLeftPositionInRootIsEqualTo(0.dp)
+    //     rule.onNodeWithTag("2")
+    //         .assertLeftPositionInRootIsEqualTo(itemSize)
+    // }
+
+    // @Test
+    // fun row_rtl_emitTwoElementsAsOneItem_positionedReversed() {
+    //     rule.setContentWithTestViewConfiguration {
+    //         CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Rtl) {
+    //             LazyRow(
+    //                 reverseLayout = true
+    //             ) {
+    //                 item {
+    //                     Box(Modifier.requiredSize(itemSize).testTag("0"))
+    //                     Box(Modifier.requiredSize(itemSize).testTag("1"))
+    //                 }
+    //             }
+    //         }
+    //     }
+
+    //     rule.onNodeWithTag("1")
+    //         .assertLeftPositionInRootIsEqualTo(itemSize)
+    //     rule.onNodeWithTag("0")
+    //         .assertLeftPositionInRootIsEqualTo(0.dp)
+    // }
+
+    // @Test
+    // fun row_rtl_emitTwoItems_positionedReversed() {
+    //     rule.setContentWithTestViewConfiguration {
+    //         CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Rtl) {
+    //             LazyRow(
+    //                 reverseLayout = true
+    //             ) {
+    //                 item {
+    //                     Box(Modifier.requiredSize(itemSize).testTag("0"))
+    //                 }
+    //                 item {
+    //                     Box(Modifier.requiredSize(itemSize).testTag("1"))
+    //                 }
+    //             }
+    //         }
+    //     }
+
+    //     rule.onNodeWithTag("1")
+    //         .assertLeftPositionInRootIsEqualTo(itemSize)
+    //     rule.onNodeWithTag("0")
+    //         .assertLeftPositionInRootIsEqualTo(0.dp)
+    // }
+
+    // @Test
+    // fun row_rtl_scrollForwardHalfWay() {
+    //     lateinit var state: LazyListState
+    //     rule.setContentWithTestViewConfiguration {
+    //         CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Rtl) {
+    //             LazyRow(
+    //                 reverseLayout = true,
+    //                 state = rememberLazyListState().also { state = it },
+    //                 modifier = Modifier.requiredSize(itemSize * 2).testTag(ContainerTag)
+    //             ) {
+    //                 items((0..2).toList()) {
+    //                     Box(Modifier.requiredSize(itemSize).testTag("$it"))
+    //                 }
+    //             }
+    //         }
+    //     }
+
+    //     rule.onNodeWithTag(ContainerTag)
+    //         .scrollBy(x = itemSize * 0.5f, density = rule.density)
+
+    //     val scrolled = rule.runOnIdle {
+    //         assertThat(state.firstVisibleItemScrollOffset).isGreaterThan(0)
+    //         assertThat(state.firstVisibleItemIndex).isEqualTo(0)
+    //         with(rule.density) { state.firstVisibleItemScrollOffset.toDp() }
+    //     }
+
+    //     rule.onNodeWithTag("0")
+    //         .assertLeftPositionInRootIsEqualTo(-scrolled)
+    //     rule.onNodeWithTag("1")
+    //         .assertLeftPositionInRootIsEqualTo(itemSize - scrolled)
+    //     rule.onNodeWithTag("2")
+    //         .assertLeftPositionInRootIsEqualTo(itemSize * 2 - scrolled)
+    // }
+
+    @Test
+    fun verticalGrid_whenParameterChanges() {
+        var reverse by mutableStateOf(true)
+        rule.setContentWithTestViewConfiguration {
+            LazyVerticalGrid(
+                GridCells.Fixed(2),
+                Modifier.width(itemSize * 2),
+                reverseLayout = reverse
+            ) {
+                items(4) {
+                    Box(Modifier.size(itemSize).testTag(it.toString()))
+                }
+            }
+        }
+
+        rule.onNodeWithTag("2")
+            .assertTopPositionInRootIsEqualTo(0.dp)
+            .assertLeftPositionInRootIsEqualTo(0.dp)
+        rule.onNodeWithTag("3")
+            .assertTopPositionInRootIsEqualTo(0.dp)
+            .assertLeftPositionInRootIsEqualTo(itemSize)
+        rule.onNodeWithTag("0")
+            .assertTopPositionInRootIsEqualTo(itemSize)
+            .assertLeftPositionInRootIsEqualTo(0.dp)
+        rule.onNodeWithTag("1")
+            .assertTopPositionInRootIsEqualTo(itemSize)
+            .assertLeftPositionInRootIsEqualTo(itemSize)
+
+        rule.runOnIdle {
+            reverse = false
+        }
+
+        rule.onNodeWithTag("0")
+            .assertTopPositionInRootIsEqualTo(0.dp)
+            .assertLeftPositionInRootIsEqualTo(0.dp)
+        rule.onNodeWithTag("1")
+            .assertTopPositionInRootIsEqualTo(0.dp)
+            .assertLeftPositionInRootIsEqualTo(itemSize)
+        rule.onNodeWithTag("2")
+            .assertTopPositionInRootIsEqualTo(itemSize)
+            .assertLeftPositionInRootIsEqualTo(0.dp)
+        rule.onNodeWithTag("3")
+            .assertTopPositionInRootIsEqualTo(itemSize)
+            .assertLeftPositionInRootIsEqualTo(itemSize)
+    }
+
+    // @Test
+    // fun row_whenParameterChanges() {
+    //     var reverse by mutableStateOf(true)
+    //     rule.setContentWithTestViewConfiguration {
+    //         LazyRow(
+    //             reverseLayout = reverse
+    //         ) {
+    //             item {
+    //                 Box(Modifier.requiredSize(itemSize).testTag("0"))
+    //                 Box(Modifier.requiredSize(itemSize).testTag("1"))
+    //             }
+    //         }
+    //     }
+
+    //     rule.onNodeWithTag("1")
+    //         .assertLeftPositionInRootIsEqualTo(0.dp)
+    //     rule.onNodeWithTag("0")
+    //         .assertLeftPositionInRootIsEqualTo(itemSize)
+
+    //     rule.runOnIdle {
+    //         reverse = false
+    //     }
+
+    //     rule.onNodeWithTag("0")
+    //         .assertLeftPositionInRootIsEqualTo(0.dp)
+    //     rule.onNodeWithTag("1")
+    //         .assertLeftPositionInRootIsEqualTo(itemSize)
+    // }
+}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyGrid.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyGrid.kt
index 1813721..4c005cb 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyGrid.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyGrid.kt
@@ -41,6 +41,9 @@
  * @param modifier the modifier to apply to this layout
  * @param state the state object to be used to control or observe the list's state
  * @param contentPadding specify a padding around the whole content
+ * @param reverseLayout reverse the direction of scrolling and layout, when `true` items will be
+ * composed from the end to the start and [LazyGridState.firstVisibleItemIndex] == 0 will mean
+ * the first item is located at the end.
  * @param verticalArrangement The vertical arrangement of the layout's children
  * @param horizontalArrangement The horizontal arrangement of the layout's children
  * @param flingBehavior logic describing fling behavior
@@ -55,7 +58,9 @@
     modifier: Modifier = Modifier,
     state: LazyGridState = rememberLazyGridState(),
     contentPadding: PaddingValues = PaddingValues(0.dp),
-    verticalArrangement: Arrangement.Vertical = Arrangement.Top,
+    reverseLayout: Boolean = false,
+    verticalArrangement: Arrangement.Vertical =
+        if (!reverseLayout) Arrangement.Top else Arrangement.Bottom,
     horizontalArrangement: Arrangement.Horizontal = Arrangement.Start,
     flingBehavior: FlingBehavior = ScrollableDefaults.flingBehavior(),
     userScrollEnabled: Boolean = true,
@@ -76,6 +81,7 @@
         modifier = modifier,
         state = state,
         contentPadding = contentPadding,
+        reverseLayout = reverseLayout,
         horizontalArrangement = horizontalArrangement,
         verticalArrangement = verticalArrangement,
         flingBehavior = flingBehavior,
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGrid.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGrid.kt
index 9d88a96..7cd6054 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGrid.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGrid.kt
@@ -29,7 +29,6 @@
 import androidx.compose.foundation.layout.PaddingValues
 import androidx.compose.foundation.layout.calculateEndPadding
 import androidx.compose.foundation.layout.calculateStartPadding
-import androidx.compose.foundation.layout.padding
 import androidx.compose.foundation.lazy.LazyGridScope
 import androidx.compose.foundation.lazy.LazyGridState
 import androidx.compose.foundation.lazy.layout.LazyLayout
@@ -42,12 +41,15 @@
 import androidx.compose.runtime.rememberCoroutineScope
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.geometry.Size
-import androidx.compose.ui.layout.IntrinsicMeasureScope
 import androidx.compose.ui.platform.LocalLayoutDirection
 import androidx.compose.ui.unit.Constraints
 import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.IntOffset
 import androidx.compose.ui.unit.LayoutDirection
+import androidx.compose.ui.unit.constrainHeight
+import androidx.compose.ui.unit.constrainWidth
 import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.offset
 import androidx.compose.ui.util.fastForEach
 
 @OptIn(ExperimentalFoundationApi::class)
@@ -140,8 +142,7 @@
                 state = state,
                 overScrollController = overScrollController,
                 enabled = userScrollEnabled
-            )
-            .padding(contentPadding),
+            ),
         prefetchPolicy = state.prefetchPolicy,
         measurePolicy = measurePolicy,
         itemsProvider = { stateOfItemsProvider.value }
@@ -200,6 +201,23 @@
     LazyMeasurePolicy { placeablesProvider, constraints ->
         constraints.assertNotNestingScrollableContainers(isVertical)
 
+        // resolve content paddings
+        val startPadding = contentPadding.calculateStartPadding(layoutDirection).roundToPx()
+        val endPadding = contentPadding.calculateEndPadding(layoutDirection).roundToPx()
+        val topPadding = contentPadding.calculateTopPadding().roundToPx()
+        val bottomPadding = contentPadding.calculateBottomPadding().roundToPx()
+        val totalVerticalPadding = topPadding + bottomPadding
+        val totalHorizontalPadding = startPadding + endPadding
+        val totalMainAxisPadding = if (isVertical) totalVerticalPadding else totalHorizontalPadding
+        val beforeContentPadding = when {
+            isVertical && !reverseLayout -> topPadding
+            isVertical && reverseLayout -> bottomPadding
+            !isVertical && !reverseLayout -> startPadding
+            else -> endPadding // !isVertical && reverseLayout
+        }
+        val afterContentPadding = totalMainAxisPadding - beforeContentPadding
+        val contentConstraints = constraints.offset(-totalHorizontalPadding, -totalVerticalPadding)
+
         val itemsProvider = stateOfItemsProvider.value
         state.updateScrollPositionIfTheFirstItemWasMoved(itemsProvider)
 
@@ -211,21 +229,6 @@
         val resolvedSlotsPerLine = slotsPerLine(constraints)
         spanLayoutProvider.slotsPerLine = resolvedSlotsPerLine
 
-        val rawBeforeContentPadding = if (isVertical) {
-            contentPadding.calculateTopPadding()
-        } else {
-            contentPadding.calculateStartPadding(layoutDirection)
-        }.roundToPx()
-        val rawAfterContentPadding = if (isVertical) {
-            contentPadding.calculateBottomPadding()
-        } else {
-            contentPadding.calculateEndPadding(layoutDirection)
-        }.roundToPx()
-        val beforeContentPadding =
-            if (reverseLayout) rawAfterContentPadding else rawBeforeContentPadding
-        val afterContentPadding =
-            if (reverseLayout) rawBeforeContentPadding else rawAfterContentPadding
-        val mainAxisMaxSize = (if (isVertical) constraints.maxHeight else constraints.maxWidth)
         val spaceBetweenLinesDp = if (isVertical) {
             requireNotNull(verticalArrangement).spacing
         } else {
@@ -256,12 +259,13 @@
                 layoutDirection = layoutDirection,
                 beforeContentPadding = beforeContentPadding,
                 afterContentPadding = afterContentPadding,
+                visualOffset = IntOffset(startPadding, topPadding),
                 placeables = placeables,
                 placementAnimator = placementAnimator
             )
         }
         val measuredLineProvider = LazyMeasuredLineProvider(
-            constraints,
+            contentConstraints,
             isVertical,
             resolvedSlotsPerLine,
             spaceBetweenSlots,
@@ -275,6 +279,7 @@
                 items = items,
                 spans = spans,
                 isVertical = isVertical,
+                reverseLayout = reverseLayout,
                 slotsPerLine = resolvedSlotsPerLine,
                 layoutDirection = layoutDirection,
                 mainAxisSpacing = mainAxisSpacing,
@@ -295,6 +300,13 @@
             result
         }
 
+        // can be negative if the content padding is larger than the max size from constraints
+        val mainAxisAvailableSize = if (isVertical) {
+            constraints.maxHeight - totalVerticalPadding
+        } else {
+            constraints.maxWidth - totalHorizontalPadding
+        }
+
         val firstVisibleLineIndex: LineIndex
         val firstVisibleLineScrollOffset: Int
         if (state.firstVisibleItemIndexNonObservable.value < itemsCount || itemsCount <= 0) {
@@ -312,14 +324,14 @@
             itemsCount = itemsCount,
             measuredLineProvider = measuredLineProvider,
             measuredItemProvider = measuredItemProvider,
-            mainAxisMaxSize = mainAxisMaxSize,
+            mainAxisAvailableSize = mainAxisAvailableSize,
             slotsPerLine = resolvedSlotsPerLine,
             beforeContentPadding = beforeContentPadding,
             afterContentPadding = afterContentPadding,
             firstVisibleLineIndex = firstVisibleLineIndex,
             firstVisibleLineScrollOffset = firstVisibleLineScrollOffset,
             scrollToBeConsumed = state.scrollToBeConsumed,
-            constraints = constraints,
+            constraints = contentConstraints,
             isVertical = isVertical,
             verticalArrangement = verticalArrangement,
             horizontalArrangement = horizontalArrangement,
@@ -327,36 +339,43 @@
             density = this,
             layoutDirection = layoutDirection,
             placementAnimator = placementAnimator,
-            layout = { width, height, placement -> layout(width, height, emptyMap(), placement) }
+            layout = { width, height, placement ->
+                layout(
+                    constraints.constrainWidth(width + totalHorizontalPadding),
+                    constraints.constrainHeight(height + totalVerticalPadding),
+                    emptyMap(),
+                    placement
+                )
+            }
         ).also {
             state.applyMeasureResult(it)
-            refreshOverScrollInfo(overScrollController, it, contentPadding)
+            refreshOverScrollInfo(
+                overScrollController,
+                it,
+                constraints,
+                totalHorizontalPadding,
+                totalVerticalPadding
+            )
         }
     }
 }
 
-private fun IntrinsicMeasureScope.refreshOverScrollInfo(
+private fun refreshOverScrollInfo(
     overScrollController: OverScrollController,
     result: LazyGridMeasureResult,
-    contentPadding: PaddingValues
+    constraints: Constraints,
+    totalHorizontalPadding: Int,
+    totalVerticalPadding: Int
 ) {
-    val verticalPadding =
-        contentPadding.calculateTopPadding() +
-            contentPadding.calculateBottomPadding()
-
-    val horizontalPadding =
-        contentPadding.calculateLeftPadding(layoutDirection) +
-            contentPadding.calculateRightPadding(layoutDirection)
-
     val canScrollForward = result.canScrollForward
     val canScrollBackward = (result.firstVisibleLine?.items?.firstOrNull() ?: 0) != 0 ||
         result.firstVisibleLineScrollOffset != 0
 
     overScrollController.refreshContainerInfo(
         Size(
-            result.width.toFloat() + horizontalPadding.roundToPx(),
-            result.height.toFloat() + verticalPadding.roundToPx()
+            constraints.constrainWidth(result.width + totalHorizontalPadding).toFloat(),
+            constraints.constrainHeight(result.height + totalVerticalPadding).toFloat()
         ),
         canScrollForward || canScrollBackward
     )
-}
\ No newline at end of file
+}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridMeasure.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridMeasure.kt
index 0fc0811..3e7e34b 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridMeasure.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridMeasure.kt
@@ -28,6 +28,7 @@
 import androidx.compose.ui.unit.constrainWidth
 import androidx.compose.ui.util.fastForEach
 import kotlin.math.abs
+import kotlin.math.min
 import kotlin.math.roundToInt
 import kotlin.math.sign
 
@@ -40,7 +41,7 @@
     itemsCount: Int,
     measuredLineProvider: LazyMeasuredLineProvider,
     measuredItemProvider: LazyMeasuredItemProvider,
-    mainAxisMaxSize: Int,
+    mainAxisAvailableSize: Int,
     slotsPerLine: Int,
     beforeContentPadding: Int,
     afterContentPadding: Int,
@@ -100,7 +101,7 @@
 
         // define min and max offsets (min offset currently includes beforeContentPadding)
         val minOffset = -beforeContentPadding
-        val maxOffset = mainAxisMaxSize
+        val maxOffset = mainAxisAvailableSize
 
         // we had scrolled backward or we compose items in the start padding area, which means
         // items before current firstLineScrollOffset should be visible. compose them and update
@@ -123,25 +124,28 @@
         currentFirstLineScrollOffset += beforeContentPadding
 
         var index = currentFirstLineIndex
-        val maxMainAxis = maxOffset + afterContentPadding
-        var mainAxisUsed = -currentFirstLineScrollOffset
+        val maxMainAxis = (maxOffset + afterContentPadding).coerceAtLeast(0)
+        var currentMainAxisOffset = -currentFirstLineScrollOffset
 
         // first we need to skip lines we already composed while composing backward
         visibleLines.fastForEach {
             index++
-            mainAxisUsed += it.mainAxisSizeWithSpacings
+            currentMainAxisOffset += it.mainAxisSizeWithSpacings
         }
 
-        // then composing visible lines forward until we fill the whole viewport
-        while (mainAxisUsed <= maxMainAxis) {
+        // then composing visible lines forward until we fill the whole viewport.
+        // we want to have at least one line 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 (currentMainAxisOffset <= maxMainAxis || visibleLines.isEmpty()) {
             val measuredLine = measuredLineProvider.getAndMeasure(index)
             if (measuredLine.isEmpty()) {
                 --index
                 break
             }
 
-            mainAxisUsed += measuredLine.mainAxisSizeWithSpacings
-            if (mainAxisUsed <= minOffset) {
+            currentMainAxisOffset += measuredLine.mainAxisSizeWithSpacings
+            if (currentMainAxisOffset <= minOffset &&
+                measuredLine.items.last().index.value != itemsCount - 1) {
                 // this line is offscreen and will not be placed. advance firstVisibleLineIndex
                 currentFirstLineIndex = index + 1
                 currentFirstLineScrollOffset -= measuredLine.mainAxisSizeWithSpacings
@@ -153,10 +157,10 @@
 
         // we didn't fill the whole viewport with lines starting from firstVisibleLineIndex.
         // lets try to scroll back if we have enough lines before firstVisibleLineIndex.
-        if (mainAxisUsed < maxOffset) {
-            val toScrollBack = maxOffset - mainAxisUsed
+        if (currentMainAxisOffset < maxOffset) {
+            val toScrollBack = maxOffset - currentMainAxisOffset
             currentFirstLineScrollOffset -= toScrollBack
-            mainAxisUsed += toScrollBack
+            currentMainAxisOffset += toScrollBack
             while (currentFirstLineScrollOffset < 0 && currentFirstLineIndex > LineIndex(0)) {
                 val previousIndex = LineIndex(currentFirstLineIndex.value - 1)
                 val measuredLine = measuredLineProvider.getAndMeasure(previousIndex)
@@ -167,7 +171,7 @@
             scrollDelta += toScrollBack
             if (currentFirstLineScrollOffset < 0) {
                 scrollDelta += currentFirstLineScrollOffset
-                mainAxisUsed += currentFirstLineScrollOffset
+                currentMainAxisOffset += currentFirstLineScrollOffset
                 currentFirstLineScrollOffset = 0
             }
         }
@@ -186,7 +190,7 @@
 
         // the initial offset for lines from visibleLines list
         val visibleLinesScrollOffset = -currentFirstLineScrollOffset
-        var firstLine = visibleLines.firstOrNull()
+        var firstLine = visibleLines.first()
 
         // even if we compose lines to fill before content padding we should ignore lines fully
         // located there for the state's scroll position calculation (first line + first offset)
@@ -202,16 +206,23 @@
             }
         }
 
-        val layoutWidth =
-            if (isVertical) constraints.maxWidth else constraints.constrainWidth(mainAxisUsed)
-        val layoutHeight =
-            if (isVertical) constraints.constrainHeight(mainAxisUsed) else constraints.maxHeight
+        val layoutWidth = if (isVertical) {
+            constraints.maxWidth
+        } else {
+            constraints.constrainWidth(currentMainAxisOffset)
+        }
+        val layoutHeight = if (isVertical) {
+            constraints.constrainHeight(currentMainAxisOffset)
+        } else {
+            constraints.maxHeight
+        }
 
         val positionedItems = calculateItemsOffsets(
             lines = visibleLines,
             layoutWidth = layoutWidth,
             layoutHeight = layoutHeight,
-            usedMainAxisSize = mainAxisUsed,
+            finalMainAxisOffset = currentMainAxisOffset,
+            maxOffset = maxOffset,
             firstLineScrollOffset = visibleLinesScrollOffset,
             isVertical = isVertical,
             verticalArrangement = verticalArrangement,
@@ -231,12 +242,12 @@
             measuredItemProvider = measuredItemProvider
         )
 
-        val maximumVisibleOffset = minOf(mainAxisUsed, mainAxisMaxSize) + afterContentPadding
+        val maximumVisibleOffset = minOf(currentMainAxisOffset, maxOffset) + afterContentPadding
 
         return LazyGridMeasureResult(
             firstVisibleLine = firstLine,
             firstVisibleLineScrollOffset = currentFirstLineScrollOffset,
-            canScrollForward = mainAxisUsed > maxOffset,
+            canScrollForward = currentMainAxisOffset > maxOffset,
             consumedScroll = consumedScroll,
             measureResult = layout(layoutWidth, layoutHeight) {
                 positionedItems.fastForEach { it.place(this) }
@@ -259,7 +270,8 @@
     lines: List<LazyMeasuredLine>,
     layoutWidth: Int,
     layoutHeight: Int,
-    usedMainAxisSize: Int,
+    finalMainAxisOffset: Int,
+    maxOffset: Int,
     firstLineScrollOffset: Int,
     isVertical: Boolean,
     verticalArrangement: Arrangement.Vertical?,
@@ -269,7 +281,7 @@
     layoutDirection: LayoutDirection
 ): MutableList<LazyGridPositionedItem> {
     val mainAxisLayoutSize = if (isVertical) layoutHeight else layoutWidth
-    val hasSpareSpace = usedMainAxisSize < mainAxisLayoutSize
+    val hasSpareSpace = finalMainAxisOffset < min(mainAxisLayoutSize, maxOffset)
     if (hasSpareSpace) {
         check(firstLineScrollOffset == 0)
     }
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyMeasuredItem.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyMeasuredItem.kt
index facf78b..f54b0a9 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyMeasuredItem.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyMeasuredItem.kt
@@ -44,7 +44,12 @@
     private val beforeContentPadding: Int,
     private val afterContentPadding: Int,
     val placeables: Array<LazyLayoutPlaceable>,
-    private val placementAnimator: LazyGridItemPlacementAnimator
+    private val placementAnimator: LazyGridItemPlacementAnimator,
+    /**
+     * The offset which shouldn't affect any calculations but needs to be applied for the final
+     * value passed into the place() call.
+     */
+    private val visualOffset: IntOffset
 ) {
     /**
      * Main axis size of the item - the max main axis size of the placeables.
@@ -92,7 +97,7 @@
             rawMainAxisOffset
         }
         val crossAxisLayoutSize = if (isVertical) layoutWidth else layoutHeight
-        val crossAxisOffset = if (layoutDirection == LayoutDirection.Rtl && isVertical) {
+        val crossAxisOffset = if (isVertical && layoutDirection == LayoutDirection.Rtl) {
             crossAxisLayoutSize - rawCrossAxisOffset - crossAxisSize
         } else {
             rawCrossAxisOffset
@@ -115,7 +120,12 @@
         }
 
         return LazyGridPositionedItem(
-            offset = placeableOffset,
+            offset = if (isVertical) {
+                IntOffset(rawCrossAxisOffset, rawMainAxisOffset)
+            } else {
+                IntOffset(rawMainAxisOffset, rawCrossAxisOffset)
+            },
+            placeableOffset = placeableOffset,
             index = index.value,
             key = key,
             row = row,
@@ -136,7 +146,8 @@
                 if (!reverseLayout) afterContentPadding else beforeContentPadding,
             isVertical = isVertical,
             wrappers = wrappers,
-            placementAnimator = placementAnimator
+            placementAnimator = placementAnimator,
+            visualOffset = visualOffset
         )
     }
 }
@@ -144,6 +155,7 @@
 @OptIn(ExperimentalFoundationApi::class)
 internal class LazyGridPositionedItem(
     override val offset: IntOffset,
+    val placeableOffset: IntOffset,
     override val index: Int,
     override val key: Any,
     override val row: Int,
@@ -155,7 +167,8 @@
     private val maxMainAxisOffset: Int,
     private val isVertical: Boolean,
     private val wrappers: List<LazyGridPlaceableWrapper>,
-    private val placementAnimator: LazyGridItemPlacementAnimator
+    private val placementAnimator: LazyGridItemPlacementAnimator,
+    private val visualOffset: IntOffset
 ) : LazyGridItemInfo {
     val placeablesCount: Int get() = wrappers.size
 
@@ -187,16 +200,16 @@
             val maxOffset = maxMainAxisOffset
             val offset = if (getAnimationSpec(index) != null) {
                 placementAnimator.getAnimatedOffset(
-                    key, index, minOffset, maxOffset, offset
+                    key, index, minOffset, maxOffset, placeableOffset
                 )
             } else {
-                offset
+                placeableOffset
             }
             if (offset.mainAxis > minOffset && offset.mainAxis < maxOffset) {
                 if (isVertical) {
-                    placeable.placeWithLayer(offset)
+                    placeable.placeWithLayer(offset + visualOffset)
                 } else {
-                    placeable.placeRelativeWithLayer(offset)
+                    placeable.placeRelativeWithLayer(offset + visualOffset)
                 }
             }
         }
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyMeasuredLine.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyMeasuredLine.kt
index d0e5099..b3753f6 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyMeasuredLine.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyMeasuredLine.kt
@@ -30,6 +30,7 @@
     val items: Array<LazyMeasuredItem>,
     private val spans: List<GridItemSpan>,
     private val isVertical: Boolean,
+    private val reverseLayout: Boolean,
     private val slotsPerLine: Int,
     private val layoutDirection: LayoutDirection,
     /**
@@ -75,8 +76,7 @@
         var usedSpan = 0
         return items.mapIndexed { itemIndex, item ->
             val span = spans[itemIndex].currentLineSpan
-            // TODO(b/215572963): check if we need to consider reverseLayout here
-            val startSlot = if (layoutDirection == LayoutDirection.Rtl && isVertical) {
+            val startSlot = if (layoutDirection == LayoutDirection.Rtl) {
                 slotsPerLine - usedSpan - span
             } else {
                 usedSpan
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyMeasuredLineProvider.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyMeasuredLineProvider.kt
index 7a7bf2a..b5a8bbf 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyMeasuredLineProvider.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyMeasuredLineProvider.kt
@@ -72,6 +72,7 @@
         var startSlot = 0
         val items = Array(lineItemsCount) {
             val span = lineConfiguration.spans[it].currentLineSpan
+            // TODO(vadimsemenov): consider reverseLayout when calculating childConstraints
             val constraints = childConstraints(startSlot, span)
             measuredItemProvider.getAndMeasure(
                 ItemIndex(lineConfiguration.firstItemIndex + it),
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/AlertDialogTest.kt b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/AlertDialogTest.kt
index 0e68e64..dab01cc 100644
--- a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/AlertDialogTest.kt
+++ b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/AlertDialogTest.kt
@@ -19,6 +19,7 @@
 import android.os.Build
 import androidx.compose.foundation.border
 import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.lazy.LazyColumn
 import androidx.compose.material.icons.Icons
 import androidx.compose.material.icons.filled.Favorite
 import androidx.compose.material3.tokens.DialogTokens
@@ -316,6 +317,62 @@
             "padding between the text and the button"
         )
     }
+
+    @Test
+    fun alertDialog_positioningWithLazyColumnText() {
+        rule.setMaterialContent(lightColorScheme()) {
+            AlertDialog(
+                onDismissRequest = {},
+                title = { Text(text = "Title", modifier = Modifier.testTag(TitleTestTag)) },
+                text = {
+                    LazyColumn(modifier = Modifier.testTag(TextTestTag)) {
+                        items(100) {
+                            Text(
+                                text = "Message!"
+                            )
+                        }
+                    }
+                },
+                confirmButton = {},
+                dismissButton = {
+                    TextButton(
+                        onClick = { /* doSomething() */ },
+                        Modifier.testTag(DismissButtonTestTag).semantics(mergeDescendants = true) {}
+                    ) {
+                        Text("Dismiss")
+                    }
+                }
+            )
+        }
+
+        val dialogBounds = rule.onNode(isDialog()).getUnclippedBoundsInRoot()
+        val titleBounds = rule.onNodeWithTag(TitleTestTag).getUnclippedBoundsInRoot()
+        val textBounds = rule.onNodeWithTag(TextTestTag).getUnclippedBoundsInRoot()
+        val dismissBtBounds = rule.onNodeWithTag(DismissButtonTestTag).getUnclippedBoundsInRoot()
+
+        rule.onNodeWithTag(TitleTestTag)
+            // Title should 24dp from the left.
+            .assertLeftPositionInRootIsEqualTo(24.dp)
+            // Title should be 24dp from the top.
+            .assertTopPositionInRootIsEqualTo(24.dp)
+
+        rule.onNodeWithTag(TextTestTag)
+            // Text should be 24dp from the start.
+            .assertLeftPositionInRootIsEqualTo(24.dp)
+            // Text should be 16dp below the title.
+            .assertTopPositionInRootIsEqualTo(titleBounds.bottom + 16.dp)
+
+        rule.onNodeWithTag(DismissButtonTestTag)
+            // Dismiss button should be 24dp from the right.
+            .assertLeftPositionInRootIsEqualTo(dialogBounds.right - 24.dp - dismissBtBounds.width)
+            // Buttons should be 18dp from the bottom (test button default height is 48dp).
+            .assertTopPositionInRootIsEqualTo(dialogBounds.bottom - 18.dp - 48.dp)
+
+        (dismissBtBounds.top - textBounds.bottom).assertIsEqualTo(
+            18.dp,
+            "padding between the text and the button"
+        )
+    }
 }
 
 private val AlertDialogMinWidth = 280.dp
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/NavigationDrawerTest.kt b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/NavigationDrawerTest.kt
index 4261656..ec056ca 100644
--- a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/NavigationDrawerTest.kt
+++ b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/NavigationDrawerTest.kt
@@ -69,6 +69,8 @@
         rule.mainClock.advanceTimeBy(100_000L)
     }
 
+    val NavigationDrawerWidth = NavigationDrawerTokens.ContainerWidth
+
     @Test
     fun navigationDrawer_testOffset_whenOpen() {
         rule.setMaterialContent(lightColorScheme()) {
@@ -99,9 +101,8 @@
             )
         }
 
-        val width = rule.rootWidth()
         rule.onNodeWithTag("content")
-            .assertLeftPositionInRootIsEqualTo(-width)
+            .assertLeftPositionInRootIsEqualTo(-NavigationDrawerWidth)
     }
 
     @Test
@@ -156,10 +157,8 @@
             )
         }
 
-        val width = rule.rootWidth()
-
         // Drawer should start in closed state
-        rule.onNodeWithTag(DrawerTestTag).assertLeftPositionInRootIsEqualTo(-width)
+        rule.onNodeWithTag(DrawerTestTag).assertLeftPositionInRootIsEqualTo(-NavigationDrawerWidth)
 
         // When the drawer state is set to Opened
         drawerState.open()
@@ -169,7 +168,7 @@
         // When the drawer state is set to Closed
         drawerState.close()
         // Then the drawer should be closed
-        rule.onNodeWithTag(DrawerTestTag).assertLeftPositionInRootIsEqualTo(-width)
+        rule.onNodeWithTag(DrawerTestTag).assertLeftPositionInRootIsEqualTo(-NavigationDrawerWidth)
     }
 
     @Test
@@ -187,10 +186,8 @@
             )
         }
 
-        val width = rule.rootWidth()
-
         // Drawer should start in closed state
-        rule.onNodeWithTag(DrawerTestTag).assertLeftPositionInRootIsEqualTo(-width)
+        rule.onNodeWithTag(DrawerTestTag).assertLeftPositionInRootIsEqualTo(-NavigationDrawerWidth)
 
         // When the drawer state is set to Opened
         drawerState.animateTo(DrawerValue.Open, TweenSpec())
@@ -200,7 +197,7 @@
         // When the drawer state is set to Closed
         drawerState.animateTo(DrawerValue.Closed, TweenSpec())
         // Then the drawer should be closed
-        rule.onNodeWithTag(DrawerTestTag).assertLeftPositionInRootIsEqualTo(-width)
+        rule.onNodeWithTag(DrawerTestTag).assertLeftPositionInRootIsEqualTo(-NavigationDrawerWidth)
     }
 
     @Test
@@ -218,10 +215,8 @@
             )
         }
 
-        val width = rule.rootWidth()
-
         // Drawer should start in closed state
-        rule.onNodeWithTag(DrawerTestTag).assertLeftPositionInRootIsEqualTo(-width)
+        rule.onNodeWithTag(DrawerTestTag).assertLeftPositionInRootIsEqualTo(-NavigationDrawerWidth)
 
         // When the drawer state is set to Opened
         drawerState.snapTo(DrawerValue.Open)
@@ -231,7 +226,7 @@
         // When the drawer state is set to Closed
         drawerState.snapTo(DrawerValue.Closed)
         // Then the drawer should be closed
-        rule.onNodeWithTag(DrawerTestTag).assertLeftPositionInRootIsEqualTo(-width)
+        rule.onNodeWithTag(DrawerTestTag).assertLeftPositionInRootIsEqualTo(-NavigationDrawerWidth)
     }
 
     @Test
@@ -524,7 +519,7 @@
             .performSemanticsAction(SemanticsActions.OnClick)
 
         // Then the drawer should be closed
-        rule.onNodeWithTag(DrawerTestTag).assertLeftPositionInRootIsEqualTo(-rule.rootWidth())
+        rule.onNodeWithTag(DrawerTestTag).assertLeftPositionInRootIsEqualTo(-NavigationDrawerWidth)
 
         topNode = rule.onNodeWithTag(topTag).fetchSemanticsNode()
         assertEquals(2, topNode.children.size)
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/AlertDialog.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/AlertDialog.kt
index 1d28bed..0a7a61f 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/AlertDialog.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/AlertDialog.kt
@@ -100,6 +100,7 @@
                     ProvideTextStyle(textStyle) {
                         Box(
                             Modifier
+                                .weight(weight = 1f, fill = false)
                                 .padding(TextPadding)
                                 .align(Alignment.Start)
                         ) {
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/NavigationDrawer.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/NavigationDrawer.kt
index 8a3290b..44707f5 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/NavigationDrawer.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/NavigationDrawer.kt
@@ -22,7 +22,6 @@
 import androidx.compose.foundation.gestures.Orientation
 import androidx.compose.foundation.gestures.detectTapGestures
 import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.BoxWithConstraints
 import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.ColumnScope
 import androidx.compose.foundation.layout.fillMaxSize
@@ -53,9 +52,9 @@
 import androidx.compose.ui.unit.IntOffset
 import androidx.compose.ui.unit.LayoutDirection
 import androidx.compose.ui.unit.dp
+import kotlin.math.roundToInt
 import kotlinx.coroutines.CancellationException
 import kotlinx.coroutines.launch
-import kotlin.math.roundToInt
 
 /**
  * Possible values of [DrawerState].
@@ -256,80 +255,65 @@
     content: @Composable () -> Unit
 ) {
     val scope = rememberCoroutineScope()
-    BoxWithConstraints(modifier.fillMaxSize()) {
-        val navigationDrawerConstraints = constraints
-        // TODO : think about Infinite max bounds case
-        if (!navigationDrawerConstraints.hasBoundedWidth) {
-            throw IllegalStateException("Drawer shouldn't have infinite width")
+    val minValue = -with(LocalDensity.current) { NavigationDrawerTokens.ContainerWidth.toPx() }
+    val maxValue = 0f
+
+    val anchors = mapOf(minValue to DrawerValue.Closed, maxValue to DrawerValue.Open)
+    val isRtl = LocalLayoutDirection.current == LayoutDirection.Rtl
+    Box(
+        modifier.fillMaxSize().swipeable(
+            state = drawerState.swipeableState,
+            anchors = anchors,
+            thresholds = { _, _ -> FractionalThreshold(0.5f) },
+            orientation = Orientation.Horizontal,
+            enabled = gesturesEnabled,
+            reverseDirection = isRtl,
+            velocityThreshold = DrawerVelocityThreshold,
+            resistance = null
+        )
+    ) {
+        Box {
+            content()
         }
-
-        val minValue = -navigationDrawerConstraints.maxWidth.toFloat()
-        val maxValue = 0f
-
-        val anchors = mapOf(minValue to DrawerValue.Closed, maxValue to DrawerValue.Open)
-        val isRtl = LocalLayoutDirection.current == LayoutDirection.Rtl
-        Box(
-            Modifier.swipeable(
-                state = drawerState.swipeableState,
-                anchors = anchors,
-                thresholds = { _, _ -> FractionalThreshold(0.5f) },
-                orientation = Orientation.Horizontal,
-                enabled = gesturesEnabled,
-                reverseDirection = isRtl,
-                velocityThreshold = DrawerVelocityThreshold,
-                resistance = null
-            )
-        ) {
-            Box {
-                content()
-            }
-            Scrim(
-                open = drawerState.isOpen,
-                onClose = {
-                    if (
-                        gesturesEnabled &&
-                        drawerState.swipeableState.confirmStateChange(DrawerValue.Closed)
-                    ) {
-                        scope.launch { drawerState.close() }
+        Scrim(
+            open = drawerState.isOpen,
+            onClose = {
+                if (
+                    gesturesEnabled &&
+                    drawerState.swipeableState.confirmStateChange(DrawerValue.Closed)
+                ) {
+                    scope.launch { drawerState.close() }
+                }
+            },
+            fraction = {
+                calculateFraction(minValue, maxValue, drawerState.offset.value)
+            },
+            color = scrimColor
+        )
+        val navigationMenu = getString(Strings.NavigationMenu)
+        Surface(
+            modifier = Modifier
+                .sizeIn(maxWidth = NavigationDrawerTokens.ContainerWidth)
+                .offset { IntOffset(drawerState.offset.value.roundToInt(), 0) }
+                .semantics {
+                    paneTitle = navigationMenu
+                    if (drawerState.isOpen) {
+                        dismiss {
+                            if (
+                                drawerState.swipeableState
+                                    .confirmStateChange(DrawerValue.Closed)
+                            ) {
+                                scope.launch { drawerState.close() }
+                            }; true
+                        }
                     }
                 },
-                fraction = {
-                    calculateFraction(minValue, maxValue, drawerState.offset.value)
-                },
-                color = scrimColor
-            )
-            val navigationMenu = getString(Strings.NavigationMenu)
-            Surface(
-                modifier = with(LocalDensity.current) {
-                    Modifier
-                        .sizeIn(
-                            minWidth = navigationDrawerConstraints.minWidth.toDp(),
-                            minHeight = navigationDrawerConstraints.minHeight.toDp(),
-                            maxWidth = NavigationDrawerTokens.ContainerWidth,
-                            maxHeight = navigationDrawerConstraints.maxHeight.toDp()
-                        )
-                }
-                    .offset { IntOffset(drawerState.offset.value.roundToInt(), 0) }
-                    .semantics {
-                        paneTitle = navigationMenu
-                        if (drawerState.isOpen) {
-                            dismiss {
-                                if (
-                                    drawerState.swipeableState
-                                        .confirmStateChange(DrawerValue.Closed)
-                                ) {
-                                    scope.launch { drawerState.close() }
-                                }; true
-                            }
-                        }
-                    },
-                shape = drawerShape,
-                color = drawerContainerColor,
-                contentColor = drawerContentColor,
-                tonalElevation = drawerTonalElevation
-            ) {
-                Column(Modifier.fillMaxSize(), content = drawerContent)
-            }
+            shape = drawerShape,
+            color = drawerContainerColor,
+            contentColor = drawerContentColor,
+            tonalElevation = drawerTonalElevation
+        ) {
+            Column(Modifier.fillMaxSize(), content = drawerContent)
         }
     }
 }
diff --git a/core/core-ktx/api/current.txt b/core/core-ktx/api/current.txt
index 823f91d..c09a63a 100644
--- a/core/core-ktx/api/current.txt
+++ b/core/core-ktx/api/current.txt
@@ -274,6 +274,7 @@
 
   public final class DrawableKt {
     method public static android.graphics.Bitmap toBitmap(android.graphics.drawable.Drawable, optional @Px int width, optional @Px int height, optional android.graphics.Bitmap.Config? config);
+    method public static android.graphics.Bitmap? toBitmapOrNull(android.graphics.drawable.Drawable, optional @Px int width, optional @Px int height, optional android.graphics.Bitmap.Config? config);
     method public static void updateBounds(android.graphics.drawable.Drawable, optional @Px int left, optional @Px int top, optional @Px int right, optional @Px int bottom);
   }
 
diff --git a/core/core-ktx/api/public_plus_experimental_current.txt b/core/core-ktx/api/public_plus_experimental_current.txt
index 823f91d..c09a63a 100644
--- a/core/core-ktx/api/public_plus_experimental_current.txt
+++ b/core/core-ktx/api/public_plus_experimental_current.txt
@@ -274,6 +274,7 @@
 
   public final class DrawableKt {
     method public static android.graphics.Bitmap toBitmap(android.graphics.drawable.Drawable, optional @Px int width, optional @Px int height, optional android.graphics.Bitmap.Config? config);
+    method public static android.graphics.Bitmap? toBitmapOrNull(android.graphics.drawable.Drawable, optional @Px int width, optional @Px int height, optional android.graphics.Bitmap.Config? config);
     method public static void updateBounds(android.graphics.drawable.Drawable, optional @Px int left, optional @Px int top, optional @Px int right, optional @Px int bottom);
   }
 
diff --git a/core/core-ktx/api/restricted_current.txt b/core/core-ktx/api/restricted_current.txt
index 823f91d..c09a63a 100644
--- a/core/core-ktx/api/restricted_current.txt
+++ b/core/core-ktx/api/restricted_current.txt
@@ -274,6 +274,7 @@
 
   public final class DrawableKt {
     method public static android.graphics.Bitmap toBitmap(android.graphics.drawable.Drawable, optional @Px int width, optional @Px int height, optional android.graphics.Bitmap.Config? config);
+    method public static android.graphics.Bitmap? toBitmapOrNull(android.graphics.drawable.Drawable, optional @Px int width, optional @Px int height, optional android.graphics.Bitmap.Config? config);
     method public static void updateBounds(android.graphics.drawable.Drawable, optional @Px int left, optional @Px int top, optional @Px int right, optional @Px int bottom);
   }
 
diff --git a/core/core-ktx/src/androidTest/java/androidx/core/graphics/drawable/DrawableTest.kt b/core/core-ktx/src/androidTest/java/androidx/core/graphics/drawable/DrawableTest.kt
index 9c011eb..93703e8 100644
--- a/core/core-ktx/src/androidTest/java/androidx/core/graphics/drawable/DrawableTest.kt
+++ b/core/core-ktx/src/androidTest/java/androidx/core/graphics/drawable/DrawableTest.kt
@@ -24,6 +24,7 @@
 import androidx.test.core.app.ApplicationProvider
 import androidx.test.filters.SmallTest
 import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNull
 import org.junit.Test
 
 @SmallTest
@@ -109,6 +110,22 @@
         assertEquals(Color.RED, bitmap.getPixel(5, 5))
     }
 
+    @Test
+    fun drawableOrNullEmptyBitmap() {
+        @Suppress("DEPRECATION") val drawable = BitmapDrawable()
+
+        val bitmap = drawable.toBitmapOrNull()
+        assertNull(bitmap)
+    }
+
+    @Test(expected = IllegalArgumentException::class)
+    fun drawableEmptyBitmapThrowsIAE() {
+        @Suppress("DEPRECATION") val drawable = BitmapDrawable()
+
+        val bitmap = drawable.toBitmap()
+        assertNull(bitmap)
+    }
+
     @Test fun drawableConfig() {
         val drawable = object : ColorDrawable(Color.RED) {
             override fun getIntrinsicWidth() = 10
diff --git a/core/core-ktx/src/main/java/androidx/core/graphics/drawable/Drawable.kt b/core/core-ktx/src/main/java/androidx/core/graphics/drawable/Drawable.kt
index be4a115..b82a760 100644
--- a/core/core-ktx/src/main/java/androidx/core/graphics/drawable/Drawable.kt
+++ b/core/core-ktx/src/main/java/androidx/core/graphics/drawable/Drawable.kt
@@ -38,6 +38,10 @@
  * @param height Height of the desired bitmap. Defaults to [Drawable.getIntrinsicHeight].
  * @param config Bitmap config of the desired bitmap. Null attempts to use the native config, if
  * any. Defaults to [Config.ARGB_8888] otherwise.
+ * @throws IllegalArgumentException if the underlying drawable is a [BitmapDrawable] where
+ * [BitmapDrawable.getBitmap] returns `null` or the drawable cannot otherwise be represented as a
+ * bitmap
+ * @see toBitmapOrNull
  */
 public fun Drawable.toBitmap(
     @Px width: Int = intrinsicWidth,
@@ -45,6 +49,10 @@
     config: Config? = null
 ): Bitmap {
     if (this is BitmapDrawable) {
+        if (bitmap == null) {
+            // This is slightly better than returning an empty, zero-size bitmap.
+            throw IllegalArgumentException("bitmap is null")
+        }
         if (config == null || bitmap.config == config) {
             // Fast-path to return original. Bitmap.createScaledBitmap will do this check, but it
             // involves allocation and two jumps into native code so we perform the check ourselves.
@@ -66,6 +74,34 @@
 }
 
 /**
+ * Returns a [Bitmap] representation of this [Drawable] or `null` if the drawable cannot be
+ * represented as a bitmap.
+ *
+ * If this instance is a [BitmapDrawable] and the [width], [height], and [config] match, the
+ * underlying [Bitmap] instance will be returned directly. If any of those three properties differ
+ * then a new [Bitmap] is created. For all other [Drawable] types, a new [Bitmap] is created.
+ *
+ * If the result of [BitmapDrawable.getBitmap] is `null` or the drawable cannot otherwise be
+ * represented as a bitmap, returns `null`.
+ *
+ * @param width Width of the desired bitmap. Defaults to [Drawable.getIntrinsicWidth].
+ * @param height Height of the desired bitmap. Defaults to [Drawable.getIntrinsicHeight].
+ * @param config Bitmap config of the desired bitmap. Null attempts to use the native config, if
+ * any. Defaults to [Config.ARGB_8888] otherwise.
+ * @see toBitmap
+ */
+public fun Drawable.toBitmapOrNull(
+    @Px width: Int = intrinsicWidth,
+    @Px height: Int = intrinsicHeight,
+    config: Config? = null
+): Bitmap? {
+    if (this is BitmapDrawable && bitmap == null) {
+        return null
+    }
+    return toBitmap(width, height, config)
+}
+
+/**
  * Updates this drawable's bounds. This version of the method allows using named parameters
  * to just set one or more axes.
  *
diff --git a/development/publishScan.sh b/development/publishScan.sh
new file mode 100755
index 0000000..cd5fdd0
--- /dev/null
+++ b/development/publishScan.sh
@@ -0,0 +1,66 @@
+#!/bin/bash
+set -e
+
+buildId="$1"
+target="$2"
+
+function usage() {
+  echo "usage: $0 <buildId> <target>"
+  echo
+  echo "Downloads build scan information for the corresponding build and uploads it to the enterprise server configured in settings.gradle"
+  exit 1
+}
+
+if [ "$buildId" == "" ]; then
+  usage
+fi
+
+if [ "$target" == "" ]; then
+  usage
+fi
+
+function downloadScan() {
+  echo downloading build scan from $buildId $target
+  if [ "$target" == "androidx_incremental" ]; then
+    downloadPath="incremental/scan.zip"
+  else
+    downloadPath="scan.zip"
+  fi
+  cd /tmp
+  /google/data/ro/projects/android/fetch_artifact --bid $buildId --target $target "$downloadPath"
+  cd -
+}
+downloadScan
+
+# find scan dir
+if [ "$OUT_DIR" != "" ]; then
+  effectiveGradleUserHome="$OUT_DIR/.gradle"
+else
+  if [ "$GRADLE_USER_HOME" != "" ]; then
+    effectiveGradleUserHome="$GRADLE_USER_HOME"
+  else
+    effectiveGradleUserHome="$HOME/.gradle"
+  fi
+fi
+scanDir="$effectiveGradleUserHome/build-scan-data"
+
+function unzipScan() {
+  echo
+  echo unzipping build scan
+  rm -rf "$scanDir"
+  unzip /tmp/scan.zip -d "$scanDir"
+}
+unzipScan
+
+function uploadScan() {
+  log="$scanDir/upload-failure.log"
+  rm -f "$log"
+  echo
+  echo uploading build scan
+  ./gradlew buildScanPublishPrevious
+  sleep 2
+  if cat "$log" 2>/dev/null; then
+    echo upload failed
+  fi
+}
+uploadScan
diff --git a/glance/glance-appwidget-proto/src/main/proto/layout.proto b/glance/glance-appwidget-proto/src/main/proto/layout.proto
index 17c6a1c..3f07cbf 100644
--- a/glance/glance-appwidget-proto/src/main/proto/layout.proto
+++ b/glance/glance-appwidget-proto/src/main/proto/layout.proto
@@ -77,4 +77,6 @@
   IMAGE = 13;
   LINEAR_PROGRESS_INDICATOR = 14;
   CIRCULAR_PROGRESS_INDICATOR = 15;
+  LAZY_VERTICAL_GRID = 16;
+  VERTICAL_GRID_ITEM = 17;
 }
diff --git a/glance/glance-appwidget/api/current.txt b/glance/glance-appwidget/api/current.txt
index 2e8f665..c4268fc 100644
--- a/glance/glance-appwidget/api/current.txt
+++ b/glance/glance-appwidget/api/current.txt
@@ -190,6 +190,16 @@
 
 package androidx.glance.appwidget.lazy {
 
+  public final inline class GridCells {
+    ctor public GridCells();
+  }
+
+  public static final class GridCells.Companion {
+    method public int Fixed(int count);
+    method public int getAdaptive();
+    property public final int Adaptive;
+  }
+
   @androidx.glance.appwidget.lazy.LazyScopeMarker public interface LazyItemScope {
   }
 
@@ -215,6 +225,25 @@
   @kotlin.DslMarker public @interface LazyScopeMarker {
   }
 
+  public final class LazyVerticalGridKt {
+    method @androidx.compose.runtime.Composable public static void LazyVerticalGrid(int gridCells, optional androidx.glance.GlanceModifier modifier, optional int horizontalAlignment, kotlin.jvm.functions.Function1<? super androidx.glance.appwidget.lazy.LazyVerticalGridScope,kotlin.Unit> content);
+    method public static inline <T> void items(androidx.glance.appwidget.lazy.LazyVerticalGridScope, java.util.List<? extends T> items, optional kotlin.jvm.functions.Function1<? super T,java.lang.Long> itemId, kotlin.jvm.functions.Function2<? super androidx.glance.appwidget.lazy.LazyItemScope,? super T,kotlin.Unit> itemContent);
+    method public static inline <T> void items(androidx.glance.appwidget.lazy.LazyVerticalGridScope, T![] items, optional kotlin.jvm.functions.Function1<? super T,java.lang.Long> itemId, kotlin.jvm.functions.Function2<? super androidx.glance.appwidget.lazy.LazyItemScope,? super T,kotlin.Unit> itemContent);
+    method public static inline <T> void itemsIndexed(androidx.glance.appwidget.lazy.LazyVerticalGridScope, java.util.List<? extends T> items, optional kotlin.jvm.functions.Function2<? super java.lang.Integer,? super T,java.lang.Long> itemId, kotlin.jvm.functions.Function3<? super androidx.glance.appwidget.lazy.LazyItemScope,? super java.lang.Integer,? super T,kotlin.Unit> itemContent);
+    method public static inline <T> void itemsIndexed(androidx.glance.appwidget.lazy.LazyVerticalGridScope, T![] items, optional kotlin.jvm.functions.Function2<? super java.lang.Integer,? super T,java.lang.Long> itemId, kotlin.jvm.functions.Function3<? super androidx.glance.appwidget.lazy.LazyItemScope,? super java.lang.Integer,? super T,kotlin.Unit> itemContent);
+  }
+
+  @androidx.glance.appwidget.lazy.LazyScopeMarker public interface LazyVerticalGridScope {
+    method public void item(optional long itemId, kotlin.jvm.functions.Function1<? super androidx.glance.appwidget.lazy.LazyItemScope,kotlin.Unit> content);
+    method public void items(int count, optional kotlin.jvm.functions.Function1<? super java.lang.Integer,java.lang.Long> itemId, kotlin.jvm.functions.Function2<? super androidx.glance.appwidget.lazy.LazyItemScope,? super java.lang.Integer,kotlin.Unit> itemContent);
+    field public static final androidx.glance.appwidget.lazy.LazyVerticalGridScope.Companion Companion;
+    field public static final long UnspecifiedItemId = -9223372036854775808L; // 0x8000000000000000L
+  }
+
+  public static final class LazyVerticalGridScope.Companion {
+    field public static final long UnspecifiedItemId = -9223372036854775808L; // 0x8000000000000000L
+  }
+
 }
 
 package androidx.glance.appwidget.state {
@@ -245,6 +274,9 @@
   public final class LazyListTranslatorKt {
   }
 
+  public final class LazyVerticalGridTranslatorKt {
+  }
+
   public final class LinearProgressIndicatorTranslatorKt {
   }
 
diff --git a/glance/glance-appwidget/api/public_plus_experimental_current.txt b/glance/glance-appwidget/api/public_plus_experimental_current.txt
index 2e8f665..c4268fc 100644
--- a/glance/glance-appwidget/api/public_plus_experimental_current.txt
+++ b/glance/glance-appwidget/api/public_plus_experimental_current.txt
@@ -190,6 +190,16 @@
 
 package androidx.glance.appwidget.lazy {
 
+  public final inline class GridCells {
+    ctor public GridCells();
+  }
+
+  public static final class GridCells.Companion {
+    method public int Fixed(int count);
+    method public int getAdaptive();
+    property public final int Adaptive;
+  }
+
   @androidx.glance.appwidget.lazy.LazyScopeMarker public interface LazyItemScope {
   }
 
@@ -215,6 +225,25 @@
   @kotlin.DslMarker public @interface LazyScopeMarker {
   }
 
+  public final class LazyVerticalGridKt {
+    method @androidx.compose.runtime.Composable public static void LazyVerticalGrid(int gridCells, optional androidx.glance.GlanceModifier modifier, optional int horizontalAlignment, kotlin.jvm.functions.Function1<? super androidx.glance.appwidget.lazy.LazyVerticalGridScope,kotlin.Unit> content);
+    method public static inline <T> void items(androidx.glance.appwidget.lazy.LazyVerticalGridScope, java.util.List<? extends T> items, optional kotlin.jvm.functions.Function1<? super T,java.lang.Long> itemId, kotlin.jvm.functions.Function2<? super androidx.glance.appwidget.lazy.LazyItemScope,? super T,kotlin.Unit> itemContent);
+    method public static inline <T> void items(androidx.glance.appwidget.lazy.LazyVerticalGridScope, T![] items, optional kotlin.jvm.functions.Function1<? super T,java.lang.Long> itemId, kotlin.jvm.functions.Function2<? super androidx.glance.appwidget.lazy.LazyItemScope,? super T,kotlin.Unit> itemContent);
+    method public static inline <T> void itemsIndexed(androidx.glance.appwidget.lazy.LazyVerticalGridScope, java.util.List<? extends T> items, optional kotlin.jvm.functions.Function2<? super java.lang.Integer,? super T,java.lang.Long> itemId, kotlin.jvm.functions.Function3<? super androidx.glance.appwidget.lazy.LazyItemScope,? super java.lang.Integer,? super T,kotlin.Unit> itemContent);
+    method public static inline <T> void itemsIndexed(androidx.glance.appwidget.lazy.LazyVerticalGridScope, T![] items, optional kotlin.jvm.functions.Function2<? super java.lang.Integer,? super T,java.lang.Long> itemId, kotlin.jvm.functions.Function3<? super androidx.glance.appwidget.lazy.LazyItemScope,? super java.lang.Integer,? super T,kotlin.Unit> itemContent);
+  }
+
+  @androidx.glance.appwidget.lazy.LazyScopeMarker public interface LazyVerticalGridScope {
+    method public void item(optional long itemId, kotlin.jvm.functions.Function1<? super androidx.glance.appwidget.lazy.LazyItemScope,kotlin.Unit> content);
+    method public void items(int count, optional kotlin.jvm.functions.Function1<? super java.lang.Integer,java.lang.Long> itemId, kotlin.jvm.functions.Function2<? super androidx.glance.appwidget.lazy.LazyItemScope,? super java.lang.Integer,kotlin.Unit> itemContent);
+    field public static final androidx.glance.appwidget.lazy.LazyVerticalGridScope.Companion Companion;
+    field public static final long UnspecifiedItemId = -9223372036854775808L; // 0x8000000000000000L
+  }
+
+  public static final class LazyVerticalGridScope.Companion {
+    field public static final long UnspecifiedItemId = -9223372036854775808L; // 0x8000000000000000L
+  }
+
 }
 
 package androidx.glance.appwidget.state {
@@ -245,6 +274,9 @@
   public final class LazyListTranslatorKt {
   }
 
+  public final class LazyVerticalGridTranslatorKt {
+  }
+
   public final class LinearProgressIndicatorTranslatorKt {
   }
 
diff --git a/glance/glance-appwidget/api/restricted_current.txt b/glance/glance-appwidget/api/restricted_current.txt
index 2e8f665..c4268fc 100644
--- a/glance/glance-appwidget/api/restricted_current.txt
+++ b/glance/glance-appwidget/api/restricted_current.txt
@@ -190,6 +190,16 @@
 
 package androidx.glance.appwidget.lazy {
 
+  public final inline class GridCells {
+    ctor public GridCells();
+  }
+
+  public static final class GridCells.Companion {
+    method public int Fixed(int count);
+    method public int getAdaptive();
+    property public final int Adaptive;
+  }
+
   @androidx.glance.appwidget.lazy.LazyScopeMarker public interface LazyItemScope {
   }
 
@@ -215,6 +225,25 @@
   @kotlin.DslMarker public @interface LazyScopeMarker {
   }
 
+  public final class LazyVerticalGridKt {
+    method @androidx.compose.runtime.Composable public static void LazyVerticalGrid(int gridCells, optional androidx.glance.GlanceModifier modifier, optional int horizontalAlignment, kotlin.jvm.functions.Function1<? super androidx.glance.appwidget.lazy.LazyVerticalGridScope,kotlin.Unit> content);
+    method public static inline <T> void items(androidx.glance.appwidget.lazy.LazyVerticalGridScope, java.util.List<? extends T> items, optional kotlin.jvm.functions.Function1<? super T,java.lang.Long> itemId, kotlin.jvm.functions.Function2<? super androidx.glance.appwidget.lazy.LazyItemScope,? super T,kotlin.Unit> itemContent);
+    method public static inline <T> void items(androidx.glance.appwidget.lazy.LazyVerticalGridScope, T![] items, optional kotlin.jvm.functions.Function1<? super T,java.lang.Long> itemId, kotlin.jvm.functions.Function2<? super androidx.glance.appwidget.lazy.LazyItemScope,? super T,kotlin.Unit> itemContent);
+    method public static inline <T> void itemsIndexed(androidx.glance.appwidget.lazy.LazyVerticalGridScope, java.util.List<? extends T> items, optional kotlin.jvm.functions.Function2<? super java.lang.Integer,? super T,java.lang.Long> itemId, kotlin.jvm.functions.Function3<? super androidx.glance.appwidget.lazy.LazyItemScope,? super java.lang.Integer,? super T,kotlin.Unit> itemContent);
+    method public static inline <T> void itemsIndexed(androidx.glance.appwidget.lazy.LazyVerticalGridScope, T![] items, optional kotlin.jvm.functions.Function2<? super java.lang.Integer,? super T,java.lang.Long> itemId, kotlin.jvm.functions.Function3<? super androidx.glance.appwidget.lazy.LazyItemScope,? super java.lang.Integer,? super T,kotlin.Unit> itemContent);
+  }
+
+  @androidx.glance.appwidget.lazy.LazyScopeMarker public interface LazyVerticalGridScope {
+    method public void item(optional long itemId, kotlin.jvm.functions.Function1<? super androidx.glance.appwidget.lazy.LazyItemScope,kotlin.Unit> content);
+    method public void items(int count, optional kotlin.jvm.functions.Function1<? super java.lang.Integer,java.lang.Long> itemId, kotlin.jvm.functions.Function2<? super androidx.glance.appwidget.lazy.LazyItemScope,? super java.lang.Integer,kotlin.Unit> itemContent);
+    field public static final androidx.glance.appwidget.lazy.LazyVerticalGridScope.Companion Companion;
+    field public static final long UnspecifiedItemId = -9223372036854775808L; // 0x8000000000000000L
+  }
+
+  public static final class LazyVerticalGridScope.Companion {
+    field public static final long UnspecifiedItemId = -9223372036854775808L; // 0x8000000000000000L
+  }
+
 }
 
 package androidx.glance.appwidget.state {
@@ -245,6 +274,9 @@
   public final class LazyListTranslatorKt {
   }
 
+  public final class LazyVerticalGridTranslatorKt {
+  }
+
   public final class LinearProgressIndicatorTranslatorKt {
   }
 
diff --git a/glance/glance-appwidget/integration-tests/demos/src/main/AndroidManifest.xml b/glance/glance-appwidget/integration-tests/demos/src/main/AndroidManifest.xml
index 0c1ed98..b06545a 100644
--- a/glance/glance-appwidget/integration-tests/demos/src/main/AndroidManifest.xml
+++ b/glance/glance-appwidget/integration-tests/demos/src/main/AndroidManifest.xml
@@ -160,5 +160,18 @@
                 android:name="android.appwidget.provider"
                 android:resource="@xml/default_app_widget_info" />
         </receiver>
+
+        <receiver
+            android:name="androidx.glance.appwidget.demos.VerticalGridAppWidgetReceiver"
+            android:label="@string/grid_widget_name"
+            android:enabled="@bool/glance_appwidget_available"
+            android:exported="false">
+            <intent-filter>
+                <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
+            </intent-filter>
+            <meta-data
+                android:name="android.appwidget.provider"
+                android:resource="@xml/default_app_widget_info" />
+        </receiver>
     </application>
 </manifest>
diff --git a/glance/glance-appwidget/integration-tests/demos/src/main/java/androidx/glance/appwidget/demos/ScrollableAppWidget.kt b/glance/glance-appwidget/integration-tests/demos/src/main/java/androidx/glance/appwidget/demos/ScrollableAppWidget.kt
index d0d3248..c1e47c3 100644
--- a/glance/glance-appwidget/integration-tests/demos/src/main/java/androidx/glance/appwidget/demos/ScrollableAppWidget.kt
+++ b/glance/glance-appwidget/integration-tests/demos/src/main/java/androidx/glance/appwidget/demos/ScrollableAppWidget.kt
@@ -111,7 +111,6 @@
                 Row {
                     val modifier = GlanceModifier.fillMaxHeight().defaultWeight()
                     ScrollColumn(modifier)
-                    ScrollColumn(modifier)
                     if (width >= tripleColumn.width) {
                         ScrollColumn(modifier)
                     }
diff --git a/glance/glance-appwidget/integration-tests/demos/src/main/java/androidx/glance/appwidget/demos/VerticalGridAppWidget.kt b/glance/glance-appwidget/integration-tests/demos/src/main/java/androidx/glance/appwidget/demos/VerticalGridAppWidget.kt
new file mode 100644
index 0000000..8ae60bc
--- /dev/null
+++ b/glance/glance-appwidget/integration-tests/demos/src/main/java/androidx/glance/appwidget/demos/VerticalGridAppWidget.kt
@@ -0,0 +1,88 @@
+/*
+ * 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.glance.appwidget.demos
+
+import android.content.Intent
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.runtime.Composable
+import androidx.glance.Button
+import androidx.glance.GlanceModifier
+import androidx.glance.LocalContext
+import androidx.glance.action.actionStartActivity
+import androidx.glance.appwidget.GlanceAppWidget
+import androidx.glance.appwidget.GlanceAppWidgetReceiver
+import androidx.glance.appwidget.action.actionStartActivity
+import androidx.glance.appwidget.appWidgetBackground
+import androidx.glance.appwidget.cornerRadius
+import androidx.glance.appwidget.lazy.LazyVerticalGrid
+import androidx.glance.appwidget.lazy.GridCells
+import androidx.glance.appwidget.lazy.itemsIndexed
+import androidx.glance.layout.Alignment
+import androidx.glance.layout.Column
+import androidx.glance.layout.Row
+import androidx.glance.layout.fillMaxHeight
+import androidx.glance.layout.fillMaxSize
+import androidx.glance.layout.fillMaxWidth
+import androidx.glance.layout.padding
+import androidx.glance.state.PreferencesGlanceStateDefinition
+import androidx.glance.text.Text
+
+class VerticalGridAppWidget : GlanceAppWidget() {
+
+  override val stateDefinition = PreferencesGlanceStateDefinition
+
+  @Composable
+  override fun Content() {
+        Column(
+            modifier = GlanceModifier.padding(R.dimen.external_padding).fillMaxSize()
+                .appWidgetBackground().cornerRadius(R.dimen.corner_radius),
+            verticalAlignment = Alignment.Vertical.CenterVertically,
+            horizontalAlignment = Alignment.Horizontal.CenterHorizontally
+        ) {
+          val modifier = GlanceModifier.fillMaxHeight().defaultWeight()
+          LazyVerticalGrid(gridCells = GridCells.Fixed(2), modifier) {
+            item { Text("LazyVerticalGrid") }
+            items(2, { it * 2L }) { index -> Text("Item $index") }
+            itemsIndexed(
+            listOf(
+                GlanceAppWidgetDemoActivity::class.java,
+                ListClickDestinationActivity::class.java
+            )
+        ) { index, activityClass ->
+            Row(
+                GlanceModifier.fillMaxWidth(),
+                horizontalAlignment = Alignment.Horizontal.CenterHorizontally
+            ) {
+                Button(
+                    text = "Activity ${index + 1}",
+                    onClick = actionStartActivity(
+                        Intent(
+                            LocalContext.current,
+                            activityClass
+                        )
+                    )
+                )
+            }
+        }
+        }
+    }
+  }
+}
+
+class VerticalGridAppWidgetReceiver : GlanceAppWidgetReceiver() {
+  override val glanceAppWidget: GlanceAppWidget = VerticalGridAppWidget()
+}
\ No newline at end of file
diff --git a/glance/glance-appwidget/integration-tests/demos/src/main/res/values/strings.xml b/glance/glance-appwidget/integration-tests/demos/src/main/res/values/strings.xml
index 2f1891c..f7f27d8 100644
--- a/glance/glance-appwidget/integration-tests/demos/src/main/res/values/strings.xml
+++ b/glance/glance-appwidget/integration-tests/demos/src/main/res/values/strings.xml
@@ -29,4 +29,5 @@
     <string name="error_widget_name">Error UI Widget</string>
     <string name="scrollable_widget_name">Scrollable Widget</string>
     <string name="image_widget_name">Image Widget</string>
+    <string name="grid_widget_name">Vertical Grid Widget</string>
 </resources>
diff --git a/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/LayoutSelection.kt b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/LayoutSelection.kt
index 0308350..19653ee 100644
--- a/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/LayoutSelection.kt
+++ b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/LayoutSelection.kt
@@ -89,6 +89,12 @@
     Frame,
     LinearProgressIndicator,
     CircularProgressIndicator,
+    VerticalGridOneColumn,
+    VerticalGridTwoColumns,
+    VerticalGridThreeColumns,
+    VerticalGridFourColumns,
+    VerticalGridFiveColumns,
+    VerticalGridAutoFit,
 
     // Note: Java keywords, such as 'switch', can't be used for layout ids.
     Swtch,
@@ -113,6 +119,12 @@
     LayoutType.ImageFillBounds to R.layout.image_fill_bounds,
     LayoutType.LinearProgressIndicator to R.layout.linear_progress_indicator,
     LayoutType.CircularProgressIndicator to R.layout.circular_progress_indicator,
+    LayoutType.VerticalGridOneColumn to R.layout.vertical_grid_one_column,
+    LayoutType.VerticalGridTwoColumns to R.layout.vertical_grid_two_columns,
+    LayoutType.VerticalGridThreeColumns to R.layout.vertical_grid_three_columns,
+    LayoutType.VerticalGridFourColumns to R.layout.vertical_grid_four_columns,
+    LayoutType.VerticalGridFiveColumns to R.layout.vertical_grid_five_columns,
+    LayoutType.VerticalGridAutoFit to R.layout.vertical_grid_auto_fit,
 )
 
 internal data class SizeSelector(
diff --git a/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/RemoteViewsTranslator.kt b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/RemoteViewsTranslator.kt
index 7d77f6a..90d3348 100644
--- a/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/RemoteViewsTranslator.kt
+++ b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/RemoteViewsTranslator.kt
@@ -33,6 +33,8 @@
 import androidx.glance.EmittableImage
 import androidx.glance.appwidget.lazy.EmittableLazyColumn
 import androidx.glance.appwidget.lazy.EmittableLazyListItem
+import androidx.glance.appwidget.lazy.EmittableLazyVerticalGrid
+import androidx.glance.appwidget.lazy.EmittableLazyVerticalGridListItem
 import androidx.glance.appwidget.translators.setText
 import androidx.glance.appwidget.translators.translateEmittableCheckBox
 import androidx.glance.appwidget.translators.translateEmittableImage
@@ -42,6 +44,8 @@
 import androidx.glance.appwidget.translators.translateEmittableText
 import androidx.glance.appwidget.translators.translateEmittableLinearProgressIndicator
 import androidx.glance.appwidget.translators.translateEmittableCircularProgressIndicator
+import androidx.glance.appwidget.translators.translateEmittableLazyVerticalGrid
+import androidx.glance.appwidget.translators.translateEmittableLazyVerticalGridListItem
 import androidx.glance.layout.Alignment
 import androidx.glance.layout.EmittableBox
 import androidx.glance.layout.EmittableColumn
@@ -151,6 +155,12 @@
         is EmittableCircularProgressIndicator -> {
             translateEmittableCircularProgressIndicator(translationContext, element)
         }
+        is EmittableLazyVerticalGrid -> {
+            translateEmittableLazyVerticalGrid(translationContext, element)
+        }
+        is EmittableLazyVerticalGridListItem -> {
+          translateEmittableLazyVerticalGridListItem(translationContext, element)
+      }
         else -> {
             throw IllegalArgumentException(
                 "Unknown element type ${element.javaClass.canonicalName}"
diff --git a/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/WidgetLayout.kt b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/WidgetLayout.kt
index 98fe5bc..eb28846 100644
--- a/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/WidgetLayout.kt
+++ b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/WidgetLayout.kt
@@ -34,6 +34,8 @@
 import androidx.glance.appwidget.lazy.EmittableLazyColumn
 import androidx.glance.appwidget.lazy.EmittableLazyList
 import androidx.glance.appwidget.lazy.EmittableLazyListItem
+import androidx.glance.appwidget.lazy.EmittableLazyVerticalGrid
+import androidx.glance.appwidget.lazy.EmittableLazyVerticalGridListItem
 import androidx.glance.appwidget.proto.LayoutProto
 import androidx.glance.appwidget.proto.LayoutProto.LayoutConfig
 import androidx.glance.appwidget.proto.LayoutProto.LayoutDefinition
@@ -319,6 +321,8 @@
         is EmittableImage -> LayoutProto.LayoutType.IMAGE
         is EmittableLinearProgressIndicator -> LayoutProto.LayoutType.LINEAR_PROGRESS_INDICATOR
         is EmittableCircularProgressIndicator -> LayoutProto.LayoutType.CIRCULAR_PROGRESS_INDICATOR
+        is EmittableLazyVerticalGrid -> LayoutProto.LayoutType.LAZY_VERTICAL_GRID
+        is EmittableLazyVerticalGridListItem -> LayoutProto.LayoutType.LIST_ITEM
         is RemoteViewsRoot -> LayoutProto.LayoutType.REMOTE_VIEWS_ROOT
         else ->
             throw IllegalArgumentException("Unknown element type ${this.javaClass.canonicalName}")
diff --git a/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/lazy/LazyList.kt b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/lazy/LazyList.kt
index d1a31f4..b41b2c4 100644
--- a/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/lazy/LazyList.kt
+++ b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/lazy/LazyList.kt
@@ -16,7 +16,6 @@
 
 package androidx.glance.appwidget.lazy
 
-import androidx.annotation.VisibleForTesting
 import androidx.compose.runtime.Composable
 import androidx.glance.EmittableWithChildren
 import androidx.glance.GlanceModifier
@@ -113,7 +112,6 @@
  * Values between -2^63 and -2^62 are reserved for list items whose id has not been explicitly
  * defined.
  */
-@VisibleForTesting
 internal const val ReservedItemIdRangeEnd = -0x4_000_000_000_000_000L
 
 @DslMarker
diff --git a/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/lazy/LazyVerticalGrid.kt b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/lazy/LazyVerticalGrid.kt
new file mode 100644
index 0000000..2a03358
--- /dev/null
+++ b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/lazy/LazyVerticalGrid.kt
@@ -0,0 +1,285 @@
+/*
+ * 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.glance.appwidget.lazy
+import androidx.compose.runtime.Composable
+import androidx.glance.GlanceModifier
+import androidx.glance.GlanceNode
+import androidx.glance.layout.Alignment
+import androidx.glance.EmittableWithChildren
+import androidx.glance.layout.fillMaxWidth
+import androidx.glance.layout.wrapContentHeight
+
+/**
+ * The DSL implementation of a lazy grid layout. It composes only visible rows of the grid.
+ *
+ * @param gridCells the number of columns in the grid.
+ * @param modifier the modifier to apply to this layout
+ * @param horizontalAlignment the horizontal alignment applied to the items.
+ * @param content a block which describes the content. Inside this block you can use methods like
+ * [LazyVerticalGridScope.item] to add a single item or
+ * [LazyVerticalGridScope.items] to add a list of items.
+ */
+@Composable
+public fun LazyVerticalGrid(
+    gridCells: GridCells,
+    modifier: GlanceModifier = GlanceModifier,
+    horizontalAlignment: Alignment.Horizontal = Alignment.Start,
+    content: LazyVerticalGridScope.() -> Unit
+) {
+    GlanceNode(
+        factory = ::EmittableLazyVerticalGrid,
+        update = {
+            this.set(gridCells) { this.gridCells = it }
+            this.set(modifier) { this.modifier = it }
+            this.set(horizontalAlignment) { this.horizontalAlignment = it }
+        },
+        content = applyVerticalGridScope(
+            Alignment(horizontalAlignment, Alignment.Vertical.CenterVertically),
+            content
+        )
+    )
+}
+
+internal fun applyVerticalGridScope(
+    alignment: Alignment,
+    content: LazyVerticalGridScope.() -> Unit
+): @Composable () -> Unit {
+    var nextImplicitItemId = ReservedItemIdRangeEnd
+    val itemList = mutableListOf<Pair<Long?, @Composable LazyItemScope.() -> Unit>>()
+    val listScopeImpl = object : LazyVerticalGridScope {
+        override fun item(itemId: Long, content: @Composable LazyItemScope.() -> Unit) {
+            require(itemId == LazyVerticalGridScope.UnspecifiedItemId ||
+                    itemId > ReservedItemIdRangeEnd) {
+                """
+                    You may not specify item ids less than $ReservedItemIdRangeEnd in a Glance
+                    widget. These are reserved.
+                """.trimIndent()
+            }
+            itemList.add(itemId to content)
+        }
+
+        override fun items(
+            count: Int,
+            itemId: ((index: Int) -> Long),
+            itemContent: @Composable LazyItemScope.(index: Int) -> Unit
+        ) {
+            repeat(count) { index ->
+                item(itemId(index)) { itemContent(index) }
+            }
+        }
+    }
+    listScopeImpl.apply(content)
+    return {
+        itemList.forEach { (itemId, composable) ->
+            val id = itemId.takeIf {
+              it != LazyVerticalGridScope.UnspecifiedItemId } ?: nextImplicitItemId--
+            check(id != LazyVerticalGridScope.UnspecifiedItemId) {
+                "Implicit list item ids exhausted."
+            }
+            LazyVerticalGridItem(id, alignment) {
+                object : LazyItemScope { }.apply { composable() }
+            }
+        }
+    }
+}
+
+@Composable
+private fun LazyVerticalGridItem(
+    itemId: Long,
+    alignment: Alignment,
+    content: @Composable () -> Unit
+) {
+    GlanceNode(
+        factory = ::EmittableLazyVerticalGridListItem,
+        update = {
+            this.set(itemId) { this.itemId = it }
+            this.set(alignment) { this.alignment = it }
+        },
+        content = content
+    )
+}
+
+/**
+ * Receiver scope which is used by [LazyColumn].
+ */
+@LazyScopeMarker
+interface LazyVerticalGridScope {
+    /**
+     * Adds a single item.
+     *
+     * @param itemId a stable and unique id representing the item. The value may not be less than
+     * or equal to -2^62, as these values are reserved by the Glance API. Specifying the list
+     * item ids will maintain the scroll position through app widget updates in Android S and
+     * higher devices.
+     * @param content the content of the item
+     */
+    fun item(itemId: Long = UnspecifiedItemId, content: @Composable LazyItemScope.() -> Unit)
+
+    /**
+     * Adds a [count] of items.
+     *
+     * @param count the count of items
+     * @param itemId a factory of stable and unique ids representing the item. The value may not be
+     * less than or equal to -2^62, as these values are reserved by the Glance API. Specifying
+     * the list item ids will maintain the scroll position through app widget updates in Android
+     * S and higher devices.
+     * @param itemContent the content displayed by a single item
+     */
+    fun items(
+        count: Int,
+        itemId: ((index: Int) -> Long) = { UnspecifiedItemId },
+        itemContent: @Composable LazyItemScope.(index: Int) -> Unit
+    )
+
+    companion object {
+        const val UnspecifiedItemId = Long.MIN_VALUE
+    }
+}
+
+/**
+ * Adds a list of items.
+ *
+ * @param items the data list
+ * @param itemId a factory of stable and unique ids representing the item. The value may not be
+ * less than or equal to -2^62, as these values are reserved by the Glance API. Specifying
+ * the list item ids will maintain the scroll position through app widget updates in Android
+ * S and higher devices.
+ * @param itemContent the content displayed by a single item
+ */
+inline fun <T> LazyVerticalGridScope.items(
+    items: List<T>,
+    crossinline itemId: ((item: T) -> Long) = { LazyVerticalGridScope.UnspecifiedItemId },
+    crossinline itemContent: @Composable LazyItemScope.(item: T) -> Unit
+) = items(items.size, { index: Int -> itemId(items[index]) }) {
+    itemContent(items[it])
+}
+
+/**
+ * Adds a list of items where the content of an item is aware of its index.
+ *
+ * @param items the data list
+ * @param itemId a factory of stable and unique ids representing the item. The value may not be
+ * less than or equal to -2^62, as these values are reserved by the Glance API. Specifying
+ * the list item ids will maintain the scroll position through app widget updates in Android
+ * S and higher devices.
+ * @param itemContent the content displayed by a single item
+ */
+inline fun <T> LazyVerticalGridScope.itemsIndexed(
+    items: List<T>,
+    crossinline itemId: ((index: Int, item: T) -> Long) =
+        { _, _ -> LazyVerticalGridScope.UnspecifiedItemId },
+    crossinline itemContent: @Composable LazyItemScope.(index: Int, item: T) -> Unit
+) = items(items.size, { index: Int -> itemId(index, items[index]) }) {
+    itemContent(it, items[it])
+}
+
+/**
+ * Adds an array of items.
+ *
+ * @param items the data array
+ * @param itemId a factory of stable and unique list item ids. Using the same itemId for multiple
+ * items in the array is not allowed. When you specify the itemId, the scroll position will be
+ * maintained based on the itemId, which means if you add/remove items before the current visible
+ * item the item with the given itemId will be kept as the first visible one.
+ * @param itemContent the content displayed by a single item
+ */
+inline fun <T> LazyVerticalGridScope.items(
+    items: Array<T>,
+    noinline itemId: ((item: T) -> Long) = { LazyVerticalGridScope.UnspecifiedItemId },
+    crossinline itemContent: @Composable LazyItemScope.(item: T) -> Unit
+) = items(items.size, { index: Int -> itemId(items[index]) }) {
+    itemContent(items[it])
+}
+
+/**
+ * Adds a array of items where the content of an item is aware of its index.
+ *
+ * @param items the data array
+ * @param itemId a factory of stable and unique list item ids. Using the same itemId for multiple
+ * items in the array is not allowed. When you specify the itemId the scroll position will be
+ * maintained based on the itemId, which means if you add/remove items before the current visible
+ * item the item with the given itemId will be kept as the first visible one.
+ * @param itemContent the content displayed by a single item
+ */
+inline fun <T> LazyVerticalGridScope.itemsIndexed(
+    items: Array<T>,
+    noinline itemId: ((index: Int, item: T) -> Long) = {
+      _, _ -> LazyVerticalGridScope.UnspecifiedItemId
+    },
+    crossinline itemContent: @Composable LazyItemScope.(index: Int, item: T) -> Unit
+) = items(items.size, { index: Int -> itemId(index, items[index]) }) {
+    itemContent(it, items[it])
+}
+
+internal abstract class EmittableLazyVerticalGridList : EmittableWithChildren(
+  resetsDepthForChildren = true
+  ) {
+  override var modifier: GlanceModifier = GlanceModifier
+  public var horizontalAlignment: Alignment.Horizontal = Alignment.Start
+  public var gridCells: GridCells = GridCells.Adaptive
+
+  override fun toString() =
+      "EmittableLazyVerticalGridList(modifier=$modifier, " +
+      "horizontalAlignment=$horizontalAlignment, " +
+      "numColumn=$gridCells, " +
+      "children=[\n${childrenToString()}\n])"
+}
+
+internal class EmittableLazyVerticalGridListItem : EmittableWithChildren() {
+  override var modifier: GlanceModifier
+      get() = children.singleOrNull()?.modifier
+          ?: GlanceModifier.wrapContentHeight().fillMaxWidth()
+      set(_) {
+          throw IllegalAccessError(
+            "You cannot set the modifier of an EmittableLazyVerticalGridListItem"
+          )
+      }
+  var itemId: Long = 0
+  var alignment: Alignment = Alignment.CenterStart
+
+  override fun toString() =
+      "EmittableLazyVerticalGridListItem(" +
+      "modifier=$modifier, " +
+      "alignment=$alignment, " +
+      "children=[\n${childrenToString()}\n])"
+}
+
+internal class EmittableLazyVerticalGrid : EmittableLazyVerticalGridList()
+
+/**
+ * Defines the number of columns of the GridView.
+ */
+@Suppress("INLINE_CLASS_DEPRECATED")
+public inline class GridCells internal constructor(private val value: Int) {
+  override fun toString(): String {
+      return when (value) {
+          0 -> "GridCells.Adaptive"
+          else -> "GridCells.Fixed($value)"
+      }
+  }
+
+  companion object {
+      fun Fixed(count: Int): GridCells {
+          require(count in 1..5) {
+              "Only counts from 1 to 5 are supported."
+          }
+          return GridCells(count)
+      }
+
+      val Adaptive = GridCells(0)
+  }
+}
\ No newline at end of file
diff --git a/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/translators/LazyVerticalGridTranslator.kt b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/translators/LazyVerticalGridTranslator.kt
new file mode 100644
index 0000000..17a9bb2
--- /dev/null
+++ b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/translators/LazyVerticalGridTranslator.kt
@@ -0,0 +1,134 @@
+/*
+ * 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.glance.appwidget.translators
+
+import android.util.Log
+import android.app.PendingIntent
+import android.app.PendingIntent.FLAG_MUTABLE
+import android.app.PendingIntent.FLAG_UPDATE_CURRENT
+import android.content.Intent
+import android.content.Intent.FILL_IN_COMPONENT
+import android.widget.RemoteViews
+import androidx.core.widget.RemoteViewsCompat
+import androidx.glance.appwidget.InsertedViewInfo
+import androidx.glance.appwidget.LayoutType
+import androidx.glance.appwidget.TopLevelLayoutsCount
+import androidx.glance.appwidget.TranslationContext
+import androidx.glance.appwidget.applyModifiers
+import androidx.glance.appwidget.insertView
+import androidx.glance.appwidget.lazy.EmittableLazyVerticalGrid
+import androidx.glance.appwidget.lazy.EmittableLazyVerticalGridListItem
+import androidx.glance.appwidget.lazy.GridCells
+import androidx.glance.appwidget.lazy.ReservedItemIdRangeEnd
+import androidx.glance.appwidget.translateChild
+import androidx.glance.appwidget.translateComposition
+import androidx.glance.layout.Alignment
+
+/**
+ * Translates an EmittableLazyVerticalGrid and its children to a EmittableLazyList.
+ */
+internal fun RemoteViews.translateEmittableLazyVerticalGrid(
+    translationContext: TranslationContext,
+    element: EmittableLazyVerticalGrid,
+) {
+
+    val viewDef = insertView(translationContext, element.gridCells.toLayout(), element.modifier)
+    translateEmittableLazyVerticalGrid(
+        translationContext,
+        element,
+        viewDef,
+    )
+}
+
+private fun RemoteViews.translateEmittableLazyVerticalGrid(
+    translationContext: TranslationContext,
+    element: EmittableLazyVerticalGrid,
+    viewDef: InsertedViewInfo,
+) {
+    check(!translationContext.isLazyCollectionDescendant) {
+        "Glance does not support nested list views."
+    }
+    setPendingIntentTemplate(
+        viewDef.mainViewId,
+        PendingIntent.getActivity(
+            translationContext.context,
+            0,
+            Intent(),
+            FILL_IN_COMPONENT or FLAG_MUTABLE or FLAG_UPDATE_CURRENT,
+        )
+    )
+    val items = RemoteViewsCompat.RemoteCollectionItems.Builder().apply {
+        val childContext = translationContext.forLazyCollection(viewDef.mainViewId)
+        element.children.foldIndexed(false) { position, previous, itemEmittable ->
+            itemEmittable as EmittableLazyVerticalGridListItem
+            val itemId = itemEmittable.itemId
+            addItem(
+                itemId,
+                translateComposition(
+                    childContext.forLazyViewItem(position, LazyVerticalGridItemStartingViewId),
+                    listOf(itemEmittable),
+                    translationContext.layoutConfiguration.addLayout(itemEmittable),
+                )
+            )
+            // If the user specifies any explicit ids, we assume the list to be stable
+            previous || (itemId > ReservedItemIdRangeEnd)
+        }.let { setHasStableIds(it) }
+        setViewTypeCount(TopLevelLayoutsCount)
+    }.build()
+    RemoteViewsCompat.setRemoteAdapter(
+        translationContext.context,
+        this,
+        translationContext.appWidgetId,
+        viewDef.mainViewId,
+        items
+    )
+    applyModifiers(translationContext, this, element.modifier, viewDef)
+}
+
+/**
+ * Translates a list item either to its immediate only child, or a column layout wrapping all its
+ * children.
+ */
+internal fun RemoteViews.translateEmittableLazyVerticalGridListItem(
+    translationContext: TranslationContext,
+    element: EmittableLazyVerticalGridListItem
+) {
+    require(element.children.size == 1 && element.alignment == Alignment.CenterStart) {
+        "Lazy vertical grid items can only have a single child align at the center start of the " +
+        "view. The normalization of the composition tree failed."
+    }
+    translateChild(translationContext, element.children.first())
+}
+
+// All the lazy list items should use the same ids, to ensure the layouts can be re-used.
+// Using a very high number to avoid collision with the main app widget ids.
+private const val LazyVerticalGridItemStartingViewId: Int = 0x00100000
+
+private fun GridCells.toLayout(): LayoutType =
+  when (this) {
+    GridCells.Adaptive -> LayoutType.VerticalGridAutoFit
+    GridCells.Fixed(1) -> LayoutType.VerticalGridOneColumn
+    GridCells.Fixed(2) -> LayoutType.VerticalGridTwoColumns
+    GridCells.Fixed(3) -> LayoutType.VerticalGridThreeColumns
+    GridCells.Fixed(4) -> LayoutType.VerticalGridFourColumns
+    GridCells.Fixed(5) -> LayoutType.VerticalGridFiveColumns
+    else -> {
+      Log.w("GlanceLazyVerticalGrid",
+"Unknown GridCells $this number of columns to AutoFit instead")
+      LayoutType.VerticalGridAutoFit
+    }
+  }
\ No newline at end of file
diff --git a/glance/glance-appwidget/src/androidMain/res/layout/vertical_grid_auto_fit.xml b/glance/glance-appwidget/src/androidMain/res/layout/vertical_grid_auto_fit.xml
new file mode 100644
index 0000000..bc9f7e45
--- /dev/null
+++ b/glance/glance-appwidget/src/androidMain/res/layout/vertical_grid_auto_fit.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  Copyright 2022 The Android Open Source Project
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+  -->
+
+<GridView xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="wrap_content"
+    android:layout_height="wrap_content"
+    android:divider="@null"
+    android:numColumns="auto_fit"
+    style="@style/Glance.AppWidget.VerticalGrid"/>
diff --git a/glance/glance-appwidget/src/androidMain/res/layout/vertical_grid_five_columns.xml b/glance/glance-appwidget/src/androidMain/res/layout/vertical_grid_five_columns.xml
new file mode 100644
index 0000000..d4eb3d0
--- /dev/null
+++ b/glance/glance-appwidget/src/androidMain/res/layout/vertical_grid_five_columns.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  Copyright 2022 The Android Open Source Project
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+  -->
+
+<GridView xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="wrap_content"
+    android:layout_height="wrap_content"
+    android:divider="@null"
+    android:numColumns="5"
+    style="@style/Glance.AppWidget.VerticalGrid"/>
diff --git a/glance/glance-appwidget/src/androidMain/res/layout/vertical_grid_four_columns.xml b/glance/glance-appwidget/src/androidMain/res/layout/vertical_grid_four_columns.xml
new file mode 100644
index 0000000..c5779a6
--- /dev/null
+++ b/glance/glance-appwidget/src/androidMain/res/layout/vertical_grid_four_columns.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  Copyright 2022 The Android Open Source Project
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+  -->
+
+<GridView xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="wrap_content"
+    android:layout_height="wrap_content"
+    android:divider="@null"
+    android:numColumns="4"
+    style="@style/Glance.AppWidget.VerticalGrid"/>
diff --git a/glance/glance-appwidget/src/androidMain/res/layout/vertical_grid_one_column.xml b/glance/glance-appwidget/src/androidMain/res/layout/vertical_grid_one_column.xml
new file mode 100644
index 0000000..130b429
--- /dev/null
+++ b/glance/glance-appwidget/src/androidMain/res/layout/vertical_grid_one_column.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  Copyright 2022 The Android Open Source Project
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+  -->
+
+<GridView xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="wrap_content"
+    android:layout_height="wrap_content"
+    android:divider="@null"
+    android:numColumns="1"
+    style="@style/Glance.AppWidget.VerticalGrid"/>
diff --git a/glance/glance-appwidget/src/androidMain/res/layout/vertical_grid_three_columns.xml b/glance/glance-appwidget/src/androidMain/res/layout/vertical_grid_three_columns.xml
new file mode 100644
index 0000000..226862ec
--- /dev/null
+++ b/glance/glance-appwidget/src/androidMain/res/layout/vertical_grid_three_columns.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  Copyright 2022 The Android Open Source Project
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+  -->
+
+<GridView xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="wrap_content"
+    android:layout_height="wrap_content"
+    android:divider="@null"
+    android:numColumns="3"
+    style="@style/Glance.AppWidget.VerticalGrid"/>
diff --git a/glance/glance-appwidget/src/androidMain/res/layout/vertical_grid_two_columns.xml b/glance/glance-appwidget/src/androidMain/res/layout/vertical_grid_two_columns.xml
new file mode 100644
index 0000000..5243b80
--- /dev/null
+++ b/glance/glance-appwidget/src/androidMain/res/layout/vertical_grid_two_columns.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  Copyright 2022 The Android Open Source Project
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+  -->
+
+<GridView xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="wrap_content"
+    android:layout_height="wrap_content"
+    android:divider="@null"
+    android:numColumns="2"
+    style="@style/Glance.AppWidget.VerticalGrid"/>
diff --git a/glance/glance-appwidget/src/androidMain/res/values/styles.xml b/glance/glance-appwidget/src/androidMain/res/values/styles.xml
index 5961bec..41718cb 100644
--- a/glance/glance-appwidget/src/androidMain/res/values/styles.xml
+++ b/glance/glance-appwidget/src/androidMain/res/values/styles.xml
@@ -33,6 +33,7 @@
     <style name="Glance.AppWidget.LinearProgressIndicator"
            parent="@android:style/Widget.ProgressBar.Horizontal"/>
     <style name="Glance.AppWidget.CircularProgressIndicator" parent=""/>
+    <style name="Glance.AppWidget.VerticalGrid" parent=""/>
 
     <style name="Base.Glance.AppWidget.Background" parent="">
         <item name="android:id">@android:id/background</item>
diff --git a/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/RemoteViewsTranslatorKtTest.kt b/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/RemoteViewsTranslatorKtTest.kt
index e0fd8ff..01f8de1 100644
--- a/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/RemoteViewsTranslatorKtTest.kt
+++ b/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/RemoteViewsTranslatorKtTest.kt
@@ -27,6 +27,7 @@
 import android.util.Log
 import android.view.View
 import android.widget.FrameLayout
+import android.widget.GridView
 import android.widget.ImageView
 import android.widget.LinearLayout
 import android.widget.ListView
@@ -46,6 +47,8 @@
 import androidx.glance.appwidget.TextViewSubject.Companion.assertThat
 import androidx.glance.appwidget.ViewSubject.Companion.assertThat
 import androidx.glance.appwidget.lazy.LazyColumn
+import androidx.glance.appwidget.lazy.LazyVerticalGrid
+import androidx.glance.appwidget.lazy.GridCells
 import androidx.glance.appwidget.lazy.ReservedItemIdRangeEnd
 import androidx.glance.appwidget.test.R
 import androidx.glance.background
@@ -583,6 +586,38 @@
     }
 
     @Test
+    fun canTranslateLazyVerticalGrid_emptyList() = fakeCoroutineScope.runBlockingTest {
+        val rv = context.runAndTranslate {
+            LazyVerticalGrid(gridCells = GridCells.Adaptive) { }
+        }
+
+        assertIs<GridView>(context.applyRemoteViews(rv))
+    }
+
+    @Test
+    fun canTranslateLazyVerticalGrid_withItem() = fakeCoroutineScope.runBlockingTest {
+        val rv = context.runAndTranslate {
+            LazyVerticalGrid(gridCells = GridCells.Adaptive) {
+                item { Text("First") }
+                item { Row { Text("Second") } }
+            }
+        }
+
+        assertIs<GridView>(context.applyRemoteViews(rv))
+    }
+
+    @Test
+    fun canTranslateLazyVerticalGrid_withItems() = fakeCoroutineScope.runBlockingTest {
+        val rv = context.runAndTranslate {
+            LazyVerticalGrid(gridCells = GridCells.Adaptive) {
+                items(2, { it * 2L }) { index -> Text("Item $index") }
+            }
+        }
+
+        assertIs<GridView>(context.applyRemoteViews(rv))
+    }
+
+    @Test
     fun canTranslateAndroidRemoteViews() = fakeCoroutineScope.runBlockingTest {
         val result = context.runAndTranslate {
             val providedViews = RemoteViews(context.packageName, R.layout.text_sample).also {
diff --git a/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/layout/LazyVerticalGridTest.kt b/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/layout/LazyVerticalGridTest.kt
new file mode 100644
index 0000000..6068240
--- /dev/null
+++ b/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/layout/LazyVerticalGridTest.kt
@@ -0,0 +1,262 @@
+/*
+ * 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.glance.appwidget.layout
+
+import androidx.glance.appwidget.lazy.EmittableLazyVerticalGrid
+import androidx.glance.appwidget.lazy.EmittableLazyVerticalGridListItem
+import androidx.glance.appwidget.lazy.LazyVerticalGrid
+import androidx.glance.appwidget.lazy.GridCells
+import androidx.glance.appwidget.lazy.ReservedItemIdRangeEnd
+import androidx.glance.appwidget.lazy.items
+import androidx.glance.appwidget.lazy.itemsIndexed
+import androidx.glance.appwidget.runTestingComposition
+import androidx.glance.layout.EmittableRow
+import androidx.glance.layout.Row
+import androidx.glance.text.EmittableText
+import androidx.glance.text.Text
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.TestCoroutineScope
+import kotlinx.coroutines.test.runBlockingTest
+import org.junit.Before
+import org.junit.Test
+import kotlin.test.assertIs
+
+@OptIn(ExperimentalCoroutinesApi::class)
+class LazyVerticalGridTest {
+    private lateinit var fakeCoroutineScope: TestCoroutineScope
+
+    @Before
+    fun setUp() {
+        fakeCoroutineScope = TestCoroutineScope()
+    }
+
+    @Test
+    fun emptyLazyVerticalGrid_addsLazyVerticalGridToTree() = fakeCoroutineScope.runBlockingTest {
+        val root = runTestingComposition {
+            LazyVerticalGrid(gridCells = GridCells.Adaptive) { }
+        }
+
+        val verticalGrid = assertIs<EmittableLazyVerticalGrid>(root.children.single())
+        assertThat(verticalGrid.children).isEmpty()
+    }
+
+    @Test
+    fun items_createsListItemsEachWithChild() = fakeCoroutineScope.runBlockingTest {
+        val root = runTestingComposition {
+            LazyVerticalGrid(gridCells = GridCells.Adaptive) {
+                items(2, { it * 2L }) { index -> Text("Item $index") }
+                item(4L) { Text("Item 2") }
+                item(6L) { Text("Item 3") }
+            }
+        }
+
+        val verticalGrid = assertIs<EmittableLazyVerticalGrid>(root.children.single())
+        val listItems = assertAre<EmittableLazyVerticalGridListItem>(verticalGrid.children)
+        assertThat(listItems).hasSize(4)
+        assertThat(verticalGrid.getTextAtChild(0)).isEqualTo("Item 0")
+        assertThat(verticalGrid.getTextAtChild(1)).isEqualTo("Item 1")
+        assertThat(verticalGrid.getTextAtChild(2)).isEqualTo("Item 2")
+        assertThat(verticalGrid.getTextAtChild(3)).isEqualTo("Item 3")
+    }
+
+    @Test
+    fun item_multipleChildren_createsListItemWithChildren() = fakeCoroutineScope.runBlockingTest {
+        val root = runTestingComposition {
+            LazyVerticalGrid(gridCells = GridCells.Adaptive) {
+                item {
+                    Text("First")
+                    Row { Text("Second") }
+                }
+            }
+        }
+
+        val verticalGrid = assertIs<EmittableLazyVerticalGrid>(root.children.single())
+        val listItem = assertIs<EmittableLazyVerticalGridListItem>(verticalGrid.children.single())
+        assertThat(listItem.children).hasSize(2)
+        assertIs<EmittableText>(listItem.children[0])
+        assertIs<EmittableRow>(listItem.children[1])
+    }
+
+    @Test
+    fun items_withItemId_addsChildrenWithIds() = fakeCoroutineScope.runBlockingTest {
+        val root = runTestingComposition {
+            LazyVerticalGrid(gridCells = GridCells.Adaptive) {
+                items(2, { it * 2L }) { index -> Text("Item $index") }
+                item(4L) { Text("Item 2") }
+                item(6L) { Text("Item 3") }
+            }
+        }
+
+        val verticalGrid = assertIs<EmittableLazyVerticalGrid>(root.children.single())
+        val listItems = assertAre<EmittableLazyVerticalGridListItem>(verticalGrid.children)
+        assertThat(listItems[0].itemId).isEqualTo(0L)
+        assertThat(listItems[1].itemId).isEqualTo(2L)
+        assertThat(listItems[2].itemId).isEqualTo(4L)
+        assertThat(listItems[3].itemId).isEqualTo(6L)
+    }
+
+    @Test
+    fun items_withoutItemId_addsItemsWithConsecutiveReservedIds() =
+        fakeCoroutineScope.runBlockingTest {
+            val root = runTestingComposition {
+                LazyVerticalGrid(gridCells = GridCells.Adaptive) {
+                    items(2) { index -> Text("Item $index") }
+                    item { Text("Item 2") }
+                }
+            }
+
+            val verticalGrid = assertIs<EmittableLazyVerticalGrid>(root.children.single())
+            val listItems = assertAre<EmittableLazyVerticalGridListItem>(verticalGrid.children)
+            assertThat(listItems[0].itemId).isEqualTo(ReservedItemIdRangeEnd)
+            assertThat(listItems[1].itemId).isEqualTo(ReservedItemIdRangeEnd - 1)
+            assertThat(listItems[2].itemId).isEqualTo(ReservedItemIdRangeEnd - 2)
+        }
+
+    @Test
+    fun items_someWithItemIds_addsChildrenWithIds() = fakeCoroutineScope.runBlockingTest {
+        val root = runTestingComposition {
+            LazyVerticalGrid(gridCells = GridCells.Adaptive) {
+                items(1, { 5L }) { Text("Item 0") }
+                item { Text("Item 1") }
+                items(1) { Text("Item 2") }
+                item(6L) { Text("Item 3") }
+            }
+        }
+
+        val verticalGrid = assertIs<EmittableLazyVerticalGrid>(root.children.single())
+        val listItems = assertAre<EmittableLazyVerticalGridListItem>(verticalGrid.children)
+        assertThat(listItems[0].itemId).isEqualTo(5L)
+        assertThat(listItems[1].itemId).isEqualTo(ReservedItemIdRangeEnd)
+        assertThat(listItems[2].itemId).isEqualTo(ReservedItemIdRangeEnd - 1)
+        assertThat(listItems[3].itemId).isEqualTo(6L)
+    }
+
+    @Test
+    fun items_listItemsWithoutItemIds_addsChildren() = fakeCoroutineScope.runBlockingTest {
+        val root = runTestingComposition {
+            LazyVerticalGrid(gridCells = GridCells.Adaptive) {
+                items(people) { person ->
+                    Text(person.name)
+                }
+            }
+        }
+
+        val verticalGrid = assertIs<EmittableLazyVerticalGrid>(root.children.single())
+        val listItems = assertAre<EmittableLazyVerticalGridListItem>(verticalGrid.children)
+        assertThat(listItems[0].itemId).isEqualTo(ReservedItemIdRangeEnd)
+        assertThat(listItems[1].itemId).isEqualTo(ReservedItemIdRangeEnd - 1)
+        assertThat(verticalGrid.getTextAtChild(0)).isEqualTo("Alice")
+        assertThat(verticalGrid.getTextAtChild(1)).isEqualTo("Bob")
+    }
+
+    @Test
+    fun items_listItemsWithItemIds_addsChildren() = fakeCoroutineScope.runBlockingTest {
+        val root = runTestingComposition {
+            LazyVerticalGrid(gridCells = GridCells.Adaptive) {
+                items(people, itemId = { person -> person.userId }) { person ->
+                    Text(person.name)
+                }
+            }
+        }
+
+        val verticalGrid = assertIs<EmittableLazyVerticalGrid>(root.children.single())
+        val listItems = assertAre<EmittableLazyVerticalGridListItem>(verticalGrid.children)
+        assertThat(listItems[0].itemId).isEqualTo(101)
+        assertThat(listItems[1].itemId).isEqualTo(202)
+        assertThat(verticalGrid.getTextAtChild(0)).isEqualTo("Alice")
+        assertThat(verticalGrid.getTextAtChild(1)).isEqualTo("Bob")
+    }
+
+    @Test
+    fun itemsIndexed_listItems_addsChildren() = fakeCoroutineScope.runBlockingTest {
+        val root = runTestingComposition {
+            LazyVerticalGrid(gridCells = GridCells.Adaptive) {
+                itemsIndexed(people) { index, person ->
+                    Text("${index + 1} - ${person.name}")
+                }
+            }
+        }
+
+        val verticalGrid = assertIs<EmittableLazyVerticalGrid>(root.children.single())
+        assertThat(verticalGrid.getTextAtChild(0)).isEqualTo("1 - Alice")
+        assertThat(verticalGrid.getTextAtChild(1)).isEqualTo("2 - Bob")
+    }
+
+    @Test
+    fun items_arrayItemsWithoutItemIds_addsChildren() = fakeCoroutineScope.runBlockingTest {
+        val root = runTestingComposition {
+            LazyVerticalGrid(gridCells = GridCells.Adaptive) {
+                items(people.toTypedArray()) { person ->
+                    Text(person.name)
+                }
+            }
+        }
+
+        val verticalGrid = assertIs<EmittableLazyVerticalGrid>(root.children.single())
+        val listItems = assertAre<EmittableLazyVerticalGridListItem>(verticalGrid.children)
+        assertThat(listItems[0].itemId).isEqualTo(ReservedItemIdRangeEnd)
+        assertThat(listItems[1].itemId).isEqualTo(ReservedItemIdRangeEnd - 1)
+        assertThat(verticalGrid.getTextAtChild(0)).isEqualTo("Alice")
+        assertThat(verticalGrid.getTextAtChild(1)).isEqualTo("Bob")
+    }
+
+    @Test
+    fun items_arrayItemsWithItemIds_addsChildren() = fakeCoroutineScope.runBlockingTest {
+        val root = runTestingComposition {
+            LazyVerticalGrid(gridCells = GridCells.Adaptive) {
+                items(people.toTypedArray(), itemId = { person -> person.userId }) { person ->
+                    Text(person.name)
+                }
+            }
+        }
+
+        val verticalGrid = assertIs<EmittableLazyVerticalGrid>(root.children.single())
+        val listItems = assertAre<EmittableLazyVerticalGridListItem>(verticalGrid.children)
+        assertThat(listItems[0].itemId).isEqualTo(101)
+        assertThat(listItems[1].itemId).isEqualTo(202)
+        assertThat(verticalGrid.getTextAtChild(0)).isEqualTo("Alice")
+        assertThat(verticalGrid.getTextAtChild(1)).isEqualTo("Bob")
+    }
+
+    @Test
+    fun itemsIndexed_arrayItems_addsChildren() = fakeCoroutineScope.runBlockingTest {
+        val root = runTestingComposition {
+            LazyVerticalGrid(gridCells = GridCells.Adaptive) {
+                itemsIndexed(people.toTypedArray()) { index, person ->
+                    Text("${index + 1} - ${person.name}")
+                }
+            }
+        }
+
+        val verticalGrid = assertIs<EmittableLazyVerticalGrid>(root.children.single())
+        assertThat(verticalGrid.getTextAtChild(0)).isEqualTo("1 - Alice")
+        assertThat(verticalGrid.getTextAtChild(1)).isEqualTo("2 - Bob")
+    }
+
+    private fun EmittableLazyVerticalGrid.getTextAtChild(index: Int): String =
+        assertIs<EmittableText>((children[index] as
+            EmittableLazyVerticalGridListItem).children.first()).text
+
+    private companion object {
+        data class Person(val name: String, val userId: Long)
+        val people = listOf(Person("Alice", userId = 101), Person("Bob", userId = 202))
+    }
+}
+
+private inline fun <reified T> assertAre(items: Iterable<Any>) =
+    items.map { assertIs<T>(it) }.toList()
diff --git a/lifecycle/integration-tests/kotlintestapp/src/test-common/java/androidx.lifecycle/LifecycleCoroutineScopeTestBase.kt b/lifecycle/integration-tests/kotlintestapp/src/test-common/java/androidx.lifecycle/LifecycleCoroutineScopeTestBase.kt
index d7b8a69..a0ce679 100644
--- a/lifecycle/integration-tests/kotlintestapp/src/test-common/java/androidx.lifecycle/LifecycleCoroutineScopeTestBase.kt
+++ b/lifecycle/integration-tests/kotlintestapp/src/test-common/java/androidx.lifecycle/LifecycleCoroutineScopeTestBase.kt
@@ -59,7 +59,8 @@
 
     @Test
     fun launchAfterDestroy() {
-        val owner = TestLifecycleOwner(Lifecycle.State.DESTROYED, TestCoroutineDispatcher())
+        val owner = TestLifecycleOwner(Lifecycle.State.CREATED, TestCoroutineDispatcher())
+        owner.lifecycle.currentState = Lifecycle.State.DESTROYED
         runBlocking {
             owner.lifecycleScope.launch {
                 // do nothing
diff --git a/lifecycle/lifecycle-livedata-core/src/test/java/androidx/lifecycle/LiveDataTest.java b/lifecycle/lifecycle-livedata-core/src/test/java/androidx/lifecycle/LiveDataTest.java
index 77e06a7..fcdd42b 100644
--- a/lifecycle/lifecycle-livedata-core/src/test/java/androidx/lifecycle/LiveDataTest.java
+++ b/lifecycle/lifecycle-livedata-core/src/test/java/androidx/lifecycle/LiveDataTest.java
@@ -233,6 +233,7 @@
     @Test
     public void testInactiveRegistry() {
         Observer<String> observer = (Observer<String>) mock(Observer.class);
+        mOwner.handleLifecycleEvent(ON_CREATE);
         mOwner.handleLifecycleEvent(ON_DESTROY);
         mLiveData.observe(mOwner, observer);
         assertThat(mLiveData.hasObservers(), is(false));
diff --git a/lifecycle/lifecycle-runtime-ktx/src/androidTest/java/androidx/lifecycle/FlowWithLifecycleTest.kt b/lifecycle/lifecycle-runtime-ktx/src/androidTest/java/androidx/lifecycle/FlowWithLifecycleTest.kt
index d6e6ed3..7e6f298 100644
--- a/lifecycle/lifecycle-runtime-ktx/src/androidTest/java/androidx/lifecycle/FlowWithLifecycleTest.kt
+++ b/lifecycle/lifecycle-runtime-ktx/src/androidTest/java/androidx/lifecycle/FlowWithLifecycleTest.kt
@@ -65,6 +65,7 @@
 
     @Test
     fun testFlowDoesNotCollectIfLifecycleIsDestroyed() = runBlocking(Dispatchers.Main) {
+        owner.setState(Lifecycle.State.CREATED)
         owner.setState(Lifecycle.State.DESTROYED)
         val result = flowOf(1, 2, 3)
             .flowWithLifecycle(owner.lifecycle, Lifecycle.State.RESUMED)
diff --git a/lifecycle/lifecycle-runtime-ktx/src/androidTest/java/androidx/lifecycle/RepeatOnLifecycleTest.kt b/lifecycle/lifecycle-runtime-ktx/src/androidTest/java/androidx/lifecycle/RepeatOnLifecycleTest.kt
index c4ff4be..6d64b64 100644
--- a/lifecycle/lifecycle-runtime-ktx/src/androidTest/java/androidx/lifecycle/RepeatOnLifecycleTest.kt
+++ b/lifecycle/lifecycle-runtime-ktx/src/androidTest/java/androidx/lifecycle/RepeatOnLifecycleTest.kt
@@ -202,6 +202,7 @@
 
     @Test
     fun testBlockDoesNotStartIfLifecycleIsDestroyed() = runBlocking(Dispatchers.Main) {
+        owner.setState(Lifecycle.State.CREATED)
         owner.setState(Lifecycle.State.DESTROYED)
         expectations.expect(1)
 
diff --git a/lifecycle/lifecycle-runtime-ktx/src/androidTest/java/androidx/lifecycle/WithLifecycleStateTest.kt b/lifecycle/lifecycle-runtime-ktx/src/androidTest/java/androidx/lifecycle/WithLifecycleStateTest.kt
index 75c2e55..66f4780 100644
--- a/lifecycle/lifecycle-runtime-ktx/src/androidTest/java/androidx/lifecycle/WithLifecycleStateTest.kt
+++ b/lifecycle/lifecycle-runtime-ktx/src/androidTest/java/androidx/lifecycle/WithLifecycleStateTest.kt
@@ -58,7 +58,8 @@
 
     @Test
     fun testBlockCancelledWhenInitiallyDestroyed() = runBlocking(Dispatchers.Main) {
-        val owner = FakeLifecycleOwner(Lifecycle.State.DESTROYED)
+        val owner = FakeLifecycleOwner(Lifecycle.State.CREATED)
+        owner.setState(Lifecycle.State.DESTROYED)
 
         val result = runCatching {
             owner.withStarted {}
diff --git a/lifecycle/lifecycle-runtime/src/main/java/androidx/lifecycle/LifecycleRegistry.java b/lifecycle/lifecycle-runtime/src/main/java/androidx/lifecycle/LifecycleRegistry.java
index 03d96b7..a4a6ed4 100644
--- a/lifecycle/lifecycle-runtime/src/main/java/androidx/lifecycle/LifecycleRegistry.java
+++ b/lifecycle/lifecycle-runtime/src/main/java/androidx/lifecycle/LifecycleRegistry.java
@@ -138,6 +138,9 @@
         if (mState == next) {
             return;
         }
+        if (mState == INITIALIZED && next == DESTROYED) {
+            throw new IllegalStateException("no event down from " + mState);
+        }
         mState = next;
         if (mHandlingEvent || mAddingObserverCounter != 0) {
             mNewEventOccurred = true;
@@ -147,6 +150,9 @@
         mHandlingEvent = true;
         sync();
         mHandlingEvent = false;
+        if (mState == DESTROYED) {
+            mObserverMap = new FastSafeIterableMap<>();
+        }
     }
 
     private boolean isSynced() {
diff --git a/lifecycle/lifecycle-runtime/src/test/java/androidx/lifecycle/LifecycleRegistryTest.java b/lifecycle/lifecycle-runtime/src/test/java/androidx/lifecycle/LifecycleRegistryTest.java
index 3130d48..68de883 100644
--- a/lifecycle/lifecycle-runtime/src/test/java/androidx/lifecycle/LifecycleRegistryTest.java
+++ b/lifecycle/lifecycle-runtime/src/test/java/androidx/lifecycle/LifecycleRegistryTest.java
@@ -64,6 +64,15 @@
     }
 
     @Test
+    public void moveInitializedToDestroyed() {
+        try {
+            mRegistry.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY);
+        } catch (IllegalStateException e) {
+            assertThat(e.getMessage(), is("no event down from INITIALIZED"));
+        }
+    }
+
+    @Test
     public void setCurrentState() {
         mRegistry.setCurrentState(Lifecycle.State.RESUMED);
         assertThat(mRegistry.getCurrentState(), is(Lifecycle.State.RESUMED));
diff --git a/room/room-common/api/current.txt b/room/room-common/api/current.txt
index 7df459b..85ffce4 100644
--- a/room/room-common/api/current.txt
+++ b/room/room-common/api/current.txt
@@ -24,16 +24,16 @@
   }
 
   @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.FUNCTION}) public @interface ColumnInfo {
-    method public abstract int collate() default androidx.room.ColumnInfo.UNSPECIFIED;
+    method @androidx.room.ColumnInfo.Collate public abstract int collate() default androidx.room.ColumnInfo.UNSPECIFIED;
     method public abstract String defaultValue() default androidx.room.ColumnInfo.VALUE_UNSPECIFIED;
     method public abstract boolean index() default false;
     method public abstract String name() default androidx.room.ColumnInfo.INHERIT_FIELD_NAME;
-    method public abstract int typeAffinity() default androidx.room.ColumnInfo.UNDEFINED;
-    property public abstract int collate;
+    method @androidx.room.ColumnInfo.SQLiteTypeAffinity public abstract int typeAffinity() default androidx.room.ColumnInfo.UNDEFINED;
+    property @androidx.room.ColumnInfo.Collate public abstract int collate;
     property public abstract String defaultValue;
     property public abstract boolean index;
     property public abstract String name;
-    property public abstract int typeAffinity;
+    property @androidx.room.ColumnInfo.SQLiteTypeAffinity public abstract int typeAffinity;
     field public static final int BINARY = 2; // 0x2
     field public static final int BLOB = 5; // 0x5
     field public static final androidx.room.ColumnInfo.Companion Companion;
@@ -146,14 +146,14 @@
     method public abstract String[] childColumns();
     method public abstract boolean deferred() default false;
     method public abstract kotlin.reflect.KClass<?> entity();
-    method public abstract int onDelete() default androidx.room.ForeignKey.NO_ACTION;
-    method public abstract int onUpdate() default androidx.room.ForeignKey.NO_ACTION;
+    method @androidx.room.ForeignKey.Action public abstract int onDelete() default androidx.room.ForeignKey.NO_ACTION;
+    method @androidx.room.ForeignKey.Action public abstract int onUpdate() default androidx.room.ForeignKey.NO_ACTION;
     method public abstract String[] parentColumns();
     property public abstract String![] childColumns;
     property public abstract boolean deferred;
     property public abstract kotlin.reflect.KClass<?> entity;
-    property public abstract int onDelete;
-    property public abstract int onUpdate;
+    property @androidx.room.ForeignKey.Action public abstract int onDelete;
+    property @androidx.room.ForeignKey.Action public abstract int onUpdate;
     property public abstract String![] parentColumns;
     field public static final int CASCADE = 5; // 0x5
     field public static final androidx.room.ForeignKey.Companion Companion;
@@ -239,9 +239,9 @@
 
   @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets=kotlin.annotation.AnnotationTarget.FUNCTION) public @interface Insert {
     method public abstract kotlin.reflect.KClass<?> entity() default java.lang.Object;
-    method public abstract int onConflict() default androidx.room.OnConflictStrategy.ABORT;
+    method @androidx.room.OnConflictStrategy public abstract int onConflict() default androidx.room.OnConflictStrategy.ABORT;
     property public abstract kotlin.reflect.KClass<?> entity;
-    property public abstract int onConflict;
+    property @androidx.room.OnConflictStrategy public abstract int onConflict;
   }
 
   @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={}) public @interface Junction {
@@ -260,7 +260,7 @@
     property public abstract String valueColumn;
   }
 
-  @IntDef({androidx.room.OnConflictStrategy.REPLACE, androidx.room.OnConflictStrategy.ROLLBACK, androidx.room.OnConflictStrategy.ABORT, androidx.room.OnConflictStrategy.FAIL, androidx.room.OnConflictStrategy.IGNORE}) @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) public @interface OnConflictStrategy {
+  @IntDef({androidx.room.OnConflictStrategy.Companion.REPLACE, androidx.room.OnConflictStrategy.Companion.ROLLBACK, androidx.room.OnConflictStrategy.Companion.ABORT, androidx.room.OnConflictStrategy.Companion.FAIL, androidx.room.OnConflictStrategy.Companion.IGNORE}) @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) public @interface OnConflictStrategy {
     field public static final int ABORT = 3; // 0x3
     field public static final androidx.room.OnConflictStrategy.Companion Companion;
     field @Deprecated public static final int FAIL = 4; // 0x4
@@ -383,9 +383,9 @@
 
   @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets=kotlin.annotation.AnnotationTarget.FUNCTION) public @interface Update {
     method public abstract kotlin.reflect.KClass<?> entity() default java.lang.Object;
-    method public abstract int onConflict() default androidx.room.OnConflictStrategy.ABORT;
+    method @androidx.room.OnConflictStrategy public abstract int onConflict() default androidx.room.OnConflictStrategy.ABORT;
     property public abstract kotlin.reflect.KClass<?> entity;
-    property public abstract int onConflict;
+    property @androidx.room.OnConflictStrategy public abstract int onConflict;
   }
 
 }
diff --git a/room/room-common/api/public_plus_experimental_current.txt b/room/room-common/api/public_plus_experimental_current.txt
index 7df459b..85ffce4 100644
--- a/room/room-common/api/public_plus_experimental_current.txt
+++ b/room/room-common/api/public_plus_experimental_current.txt
@@ -24,16 +24,16 @@
   }
 
   @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.FUNCTION}) public @interface ColumnInfo {
-    method public abstract int collate() default androidx.room.ColumnInfo.UNSPECIFIED;
+    method @androidx.room.ColumnInfo.Collate public abstract int collate() default androidx.room.ColumnInfo.UNSPECIFIED;
     method public abstract String defaultValue() default androidx.room.ColumnInfo.VALUE_UNSPECIFIED;
     method public abstract boolean index() default false;
     method public abstract String name() default androidx.room.ColumnInfo.INHERIT_FIELD_NAME;
-    method public abstract int typeAffinity() default androidx.room.ColumnInfo.UNDEFINED;
-    property public abstract int collate;
+    method @androidx.room.ColumnInfo.SQLiteTypeAffinity public abstract int typeAffinity() default androidx.room.ColumnInfo.UNDEFINED;
+    property @androidx.room.ColumnInfo.Collate public abstract int collate;
     property public abstract String defaultValue;
     property public abstract boolean index;
     property public abstract String name;
-    property public abstract int typeAffinity;
+    property @androidx.room.ColumnInfo.SQLiteTypeAffinity public abstract int typeAffinity;
     field public static final int BINARY = 2; // 0x2
     field public static final int BLOB = 5; // 0x5
     field public static final androidx.room.ColumnInfo.Companion Companion;
@@ -146,14 +146,14 @@
     method public abstract String[] childColumns();
     method public abstract boolean deferred() default false;
     method public abstract kotlin.reflect.KClass<?> entity();
-    method public abstract int onDelete() default androidx.room.ForeignKey.NO_ACTION;
-    method public abstract int onUpdate() default androidx.room.ForeignKey.NO_ACTION;
+    method @androidx.room.ForeignKey.Action public abstract int onDelete() default androidx.room.ForeignKey.NO_ACTION;
+    method @androidx.room.ForeignKey.Action public abstract int onUpdate() default androidx.room.ForeignKey.NO_ACTION;
     method public abstract String[] parentColumns();
     property public abstract String![] childColumns;
     property public abstract boolean deferred;
     property public abstract kotlin.reflect.KClass<?> entity;
-    property public abstract int onDelete;
-    property public abstract int onUpdate;
+    property @androidx.room.ForeignKey.Action public abstract int onDelete;
+    property @androidx.room.ForeignKey.Action public abstract int onUpdate;
     property public abstract String![] parentColumns;
     field public static final int CASCADE = 5; // 0x5
     field public static final androidx.room.ForeignKey.Companion Companion;
@@ -239,9 +239,9 @@
 
   @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets=kotlin.annotation.AnnotationTarget.FUNCTION) public @interface Insert {
     method public abstract kotlin.reflect.KClass<?> entity() default java.lang.Object;
-    method public abstract int onConflict() default androidx.room.OnConflictStrategy.ABORT;
+    method @androidx.room.OnConflictStrategy public abstract int onConflict() default androidx.room.OnConflictStrategy.ABORT;
     property public abstract kotlin.reflect.KClass<?> entity;
-    property public abstract int onConflict;
+    property @androidx.room.OnConflictStrategy public abstract int onConflict;
   }
 
   @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={}) public @interface Junction {
@@ -260,7 +260,7 @@
     property public abstract String valueColumn;
   }
 
-  @IntDef({androidx.room.OnConflictStrategy.REPLACE, androidx.room.OnConflictStrategy.ROLLBACK, androidx.room.OnConflictStrategy.ABORT, androidx.room.OnConflictStrategy.FAIL, androidx.room.OnConflictStrategy.IGNORE}) @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) public @interface OnConflictStrategy {
+  @IntDef({androidx.room.OnConflictStrategy.Companion.REPLACE, androidx.room.OnConflictStrategy.Companion.ROLLBACK, androidx.room.OnConflictStrategy.Companion.ABORT, androidx.room.OnConflictStrategy.Companion.FAIL, androidx.room.OnConflictStrategy.Companion.IGNORE}) @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) public @interface OnConflictStrategy {
     field public static final int ABORT = 3; // 0x3
     field public static final androidx.room.OnConflictStrategy.Companion Companion;
     field @Deprecated public static final int FAIL = 4; // 0x4
@@ -383,9 +383,9 @@
 
   @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets=kotlin.annotation.AnnotationTarget.FUNCTION) public @interface Update {
     method public abstract kotlin.reflect.KClass<?> entity() default java.lang.Object;
-    method public abstract int onConflict() default androidx.room.OnConflictStrategy.ABORT;
+    method @androidx.room.OnConflictStrategy public abstract int onConflict() default androidx.room.OnConflictStrategy.ABORT;
     property public abstract kotlin.reflect.KClass<?> entity;
-    property public abstract int onConflict;
+    property @androidx.room.OnConflictStrategy public abstract int onConflict;
   }
 
 }
diff --git a/room/room-common/api/restricted_current.txt b/room/room-common/api/restricted_current.txt
index 8149ffa..f9cc4615 100644
--- a/room/room-common/api/restricted_current.txt
+++ b/room/room-common/api/restricted_current.txt
@@ -24,16 +24,16 @@
   }
 
   @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.FUNCTION}) public @interface ColumnInfo {
-    method public abstract int collate() default androidx.room.ColumnInfo.UNSPECIFIED;
+    method @androidx.room.ColumnInfo.Collate public abstract int collate() default androidx.room.ColumnInfo.UNSPECIFIED;
     method public abstract String defaultValue() default androidx.room.ColumnInfo.VALUE_UNSPECIFIED;
     method public abstract boolean index() default false;
     method public abstract String name() default androidx.room.ColumnInfo.INHERIT_FIELD_NAME;
-    method public abstract int typeAffinity() default androidx.room.ColumnInfo.UNDEFINED;
-    property public abstract int collate;
+    method @androidx.room.ColumnInfo.SQLiteTypeAffinity public abstract int typeAffinity() default androidx.room.ColumnInfo.UNDEFINED;
+    property @androidx.room.ColumnInfo.Collate public abstract int collate;
     property public abstract String defaultValue;
     property public abstract boolean index;
     property public abstract String name;
-    property public abstract int typeAffinity;
+    property @androidx.room.ColumnInfo.SQLiteTypeAffinity public abstract int typeAffinity;
     field public static final int BINARY = 2; // 0x2
     field public static final int BLOB = 5; // 0x5
     field public static final androidx.room.ColumnInfo.Companion Companion;
@@ -146,14 +146,14 @@
     method public abstract String[] childColumns();
     method public abstract boolean deferred() default false;
     method public abstract kotlin.reflect.KClass<?> entity();
-    method public abstract int onDelete() default androidx.room.ForeignKey.NO_ACTION;
-    method public abstract int onUpdate() default androidx.room.ForeignKey.NO_ACTION;
+    method @androidx.room.ForeignKey.Action public abstract int onDelete() default androidx.room.ForeignKey.NO_ACTION;
+    method @androidx.room.ForeignKey.Action public abstract int onUpdate() default androidx.room.ForeignKey.NO_ACTION;
     method public abstract String[] parentColumns();
     property public abstract String![] childColumns;
     property public abstract boolean deferred;
     property public abstract kotlin.reflect.KClass<?> entity;
-    property public abstract int onDelete;
-    property public abstract int onUpdate;
+    property @androidx.room.ForeignKey.Action public abstract int onDelete;
+    property @androidx.room.ForeignKey.Action public abstract int onUpdate;
     property public abstract String![] parentColumns;
     field public static final int CASCADE = 5; // 0x5
     field public static final androidx.room.ForeignKey.Companion Companion;
@@ -239,9 +239,9 @@
 
   @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets=kotlin.annotation.AnnotationTarget.FUNCTION) public @interface Insert {
     method public abstract kotlin.reflect.KClass<?> entity() default java.lang.Object;
-    method public abstract int onConflict() default androidx.room.OnConflictStrategy.ABORT;
+    method @androidx.room.OnConflictStrategy public abstract int onConflict() default androidx.room.OnConflictStrategy.ABORT;
     property public abstract kotlin.reflect.KClass<?> entity;
-    property public abstract int onConflict;
+    property @androidx.room.OnConflictStrategy public abstract int onConflict;
   }
 
   @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={}) public @interface Junction {
@@ -260,7 +260,7 @@
     property public abstract String valueColumn;
   }
 
-  @IntDef({androidx.room.OnConflictStrategy.REPLACE, androidx.room.OnConflictStrategy.ROLLBACK, androidx.room.OnConflictStrategy.ABORT, androidx.room.OnConflictStrategy.FAIL, androidx.room.OnConflictStrategy.IGNORE}) @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) public @interface OnConflictStrategy {
+  @IntDef({androidx.room.OnConflictStrategy.Companion.REPLACE, androidx.room.OnConflictStrategy.Companion.ROLLBACK, androidx.room.OnConflictStrategy.Companion.ABORT, androidx.room.OnConflictStrategy.Companion.FAIL, androidx.room.OnConflictStrategy.Companion.IGNORE}) @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) public @interface OnConflictStrategy {
     field public static final int ABORT = 3; // 0x3
     field public static final androidx.room.OnConflictStrategy.Companion Companion;
     field @Deprecated public static final int FAIL = 4; // 0x4
@@ -392,9 +392,9 @@
 
   @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets=kotlin.annotation.AnnotationTarget.FUNCTION) public @interface Update {
     method public abstract kotlin.reflect.KClass<?> entity() default java.lang.Object;
-    method public abstract int onConflict() default androidx.room.OnConflictStrategy.ABORT;
+    method @androidx.room.OnConflictStrategy public abstract int onConflict() default androidx.room.OnConflictStrategy.ABORT;
     property public abstract kotlin.reflect.KClass<?> entity;
-    property public abstract int onConflict;
+    property @androidx.room.OnConflictStrategy public abstract int onConflict;
   }
 
 }
diff --git a/room/room-common/src/main/java/androidx/room/ColumnInfo.kt b/room/room-common/src/main/java/androidx/room/ColumnInfo.kt
index c3817a0d..9134c14 100644
--- a/room/room-common/src/main/java/androidx/room/ColumnInfo.kt
+++ b/room/room-common/src/main/java/androidx/room/ColumnInfo.kt
@@ -46,7 +46,7 @@
      * [INTEGER], [REAL], or [BLOB].
      */
     @SuppressWarnings("unused")
-    @SQLiteTypeAffinity
+    @get:SQLiteTypeAffinity
     val typeAffinity: Int = UNDEFINED,
 
     /**
@@ -67,7 +67,7 @@
      * @return The collation sequence of the column. This is either [UNSPECIFIED],
      * [BINARY], [NOCASE], [RTRIM], [LOCALIZED] or [UNICODE].
      */
-    @Collate
+    @get:Collate
     val collate: Int = UNSPECIFIED,
 
     /**
diff --git a/room/room-common/src/main/java/androidx/room/ForeignKey.kt b/room/room-common/src/main/java/androidx/room/ForeignKey.kt
index 38f9b76..35f4f99 100644
--- a/room/room-common/src/main/java/androidx/room/ForeignKey.kt
+++ b/room/room-common/src/main/java/androidx/room/ForeignKey.kt
@@ -80,7 +80,7 @@
      *
      * @return The action to take when the referenced entity is deleted from the database.
      */
-    @Action
+    @get:Action
     val onDelete: Int = NO_ACTION,
 
     /**
@@ -90,7 +90,7 @@
      *
      * @return The action to take when the referenced entity is updated in the database.
      */
-    @Action
+    @get:Action
     val onUpdate: Int = NO_ACTION,
 
     /**
diff --git a/room/room-common/src/main/java/androidx/room/Insert.kt b/room/room-common/src/main/java/androidx/room/Insert.kt
index 8fe2cf7..cc9acdc 100644
--- a/room/room-common/src/main/java/androidx/room/Insert.kt
+++ b/room/room-common/src/main/java/androidx/room/Insert.kt
@@ -109,6 +109,6 @@
      *
      * @return How to handle conflicts. Defaults to [OnConflictStrategy.ABORT].
      */
-    @OnConflictStrategy
+    @get:OnConflictStrategy
     val onConflict: Int = OnConflictStrategy.ABORT
 )
diff --git a/room/room-common/src/main/java/androidx/room/OnConflictStrategy.kt b/room/room-common/src/main/java/androidx/room/OnConflictStrategy.kt
index dcce625..cb4b260d 100644
--- a/room/room-common/src/main/java/androidx/room/OnConflictStrategy.kt
+++ b/room/room-common/src/main/java/androidx/room/OnConflictStrategy.kt
@@ -16,18 +16,19 @@
 package androidx.room
 
 import androidx.annotation.IntDef
-import androidx.room.OnConflictStrategy.Companion.ABORT
-import androidx.room.OnConflictStrategy.Companion.FAIL
-import androidx.room.OnConflictStrategy.Companion.IGNORE
-import androidx.room.OnConflictStrategy.Companion.REPLACE
-import androidx.room.OnConflictStrategy.Companion.ROLLBACK
 
 /**
  * Set of conflict handling strategies for various {@link Dao} methods.
  */
 @Retention(AnnotationRetention.BINARY)
 @Suppress("DEPRECATION")
-@IntDef(REPLACE, ROLLBACK, ABORT, FAIL, IGNORE)
+@IntDef(
+    OnConflictStrategy.REPLACE,
+    OnConflictStrategy.ROLLBACK,
+    OnConflictStrategy.ABORT,
+    OnConflictStrategy.FAIL,
+    OnConflictStrategy.IGNORE
+)
 public annotation class OnConflictStrategy {
     public companion object {
         /**
diff --git a/room/room-common/src/main/java/androidx/room/Update.kt b/room/room-common/src/main/java/androidx/room/Update.kt
index b60a2ab..2e8793f 100644
--- a/room/room-common/src/main/java/androidx/room/Update.kt
+++ b/room/room-common/src/main/java/androidx/room/Update.kt
@@ -105,6 +105,6 @@
      *
      * @return How to handle conflicts. Defaults to [OnConflictStrategy.ABORT].
      */
-    @OnConflictStrategy
+    @get:OnConflictStrategy
     val onConflict: Int = OnConflictStrategy.ABORT
 )
diff --git a/settings.gradle b/settings.gradle
index e2907f9..6f2f3c3 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -298,6 +298,7 @@
 includeProject(":camera:integration-tests:camera-testapp-camera2-pipe", "camera/integration-tests/camerapipetestapp", [BuildType.MAIN])
 includeProject(":camera:integration-tests:camera-testapp-core", "camera/integration-tests/coretestapp", [BuildType.MAIN])
 includeProject(":camera:integration-tests:camera-testapp-extensions", "camera/integration-tests/extensionstestapp", [BuildType.MAIN])
+includeProject(":camera:integration-tests:camera-testapp-previewview", "camera/integration-tests/previewviewtestapp", [BuildType.MAIN])
 includeProject(":camera:integration-tests:camera-testapp-timing", "camera/integration-tests/timingtestapp", [BuildType.MAIN])
 includeProject(":camera:integration-tests:camera-testapp-uiwidgets", "camera/integration-tests/uiwidgetstestapp", [BuildType.MAIN])
 includeProject(":camera:integration-tests:camera-testapp-view", "camera/integration-tests/viewtestapp", [BuildType.MAIN])
diff --git a/wear/compose/compose-foundation/src/androidAndroidTest/kotlin/androidx/wear/compose/foundation/CurvedRowTest.kt b/wear/compose/compose-foundation/src/androidAndroidTest/kotlin/androidx/wear/compose/foundation/CurvedRowTest.kt
index 52f3c30..4d02d12 100644
--- a/wear/compose/compose-foundation/src/androidAndroidTest/kotlin/androidx/wear/compose/foundation/CurvedRowTest.kt
+++ b/wear/compose/compose-foundation/src/androidAndroidTest/kotlin/androidx/wear/compose/foundation/CurvedRowTest.kt
@@ -1,3 +1,21 @@
+/*
+ * 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.wear.compose.foundation
+
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.size
 import androidx.compose.ui.Modifier
@@ -6,16 +24,13 @@
 import androidx.compose.ui.layout.onGloballyPositioned
 import androidx.compose.ui.test.junit4.createComposeRule
 import androidx.compose.ui.unit.dp
-import androidx.wear.compose.foundation.AnchorType
-import androidx.wear.compose.foundation.CurvedRow
-import androidx.wear.compose.foundation.RadialAlignment
+import kotlin.math.PI
+import kotlin.math.atan2
+import kotlin.math.min
 import org.junit.Assert.assertEquals
 import org.junit.Assert.fail
 import org.junit.Rule
 import org.junit.Test
-import kotlin.math.PI
-import kotlin.math.atan2
-import kotlin.math.min
 
 // When components are laid out, position is specified by integers, so we can't expect
 // much precision.
diff --git a/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/SwipeToDismissBoxTest.kt b/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/SwipeToDismissBoxTest.kt
index b8de966..05982c7 100644
--- a/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/SwipeToDismissBoxTest.kt
+++ b/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/SwipeToDismissBoxTest.kt
@@ -14,6 +14,8 @@
  * limitations under the License.
  */
 
+package androidx.wear.compose.material
+
 import androidx.compose.foundation.layout.Arrangement
 import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.fillMaxSize
@@ -40,16 +42,6 @@
 import androidx.compose.ui.test.performTouchInput
 import androidx.compose.ui.test.swipeLeft
 import androidx.compose.ui.test.swipeRight
-import androidx.wear.compose.material.Button
-import androidx.wear.compose.material.ExperimentalWearMaterialApi
-import androidx.wear.compose.material.MaterialTheme
-import androidx.wear.compose.material.SwipeDismissTarget
-import androidx.wear.compose.material.SwipeToDismissBox
-import androidx.wear.compose.material.TEST_TAG
-import androidx.wear.compose.material.Text
-import androidx.wear.compose.material.ToggleButton
-import androidx.wear.compose.material.rememberSwipeToDismissBoxState
-import androidx.wear.compose.material.setContentWithTheme
 import org.junit.Assert.assertEquals
 import org.junit.Rule
 import org.junit.Test
@@ -317,10 +309,10 @@
     }
 }
 
-internal const val BACKGROUND_MESSAGE = "The Background"
-internal const val CONTENT_MESSAGE = "The Content"
-internal const val LONG_SWIPE = 1000L
-internal const val TOGGLE_SCREEN = "Toggle"
-internal const val COUNTER_SCREEN = "Counter"
-internal const val TOGGLE_ON = "On"
-internal const val TOGGLE_OFF = "Off"
\ No newline at end of file
+private const val BACKGROUND_MESSAGE = "The Background"
+private const val CONTENT_MESSAGE = "The Content"
+private const val LONG_SWIPE = 1000L
+private const val TOGGLE_SCREEN = "Toggle"
+private const val COUNTER_SCREEN = "Counter"
+private const val TOGGLE_ON = "On"
+private const val TOGGLE_OFF = "Off"
\ No newline at end of file
diff --git a/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/SwipeableTest.kt b/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/SwipeableTest.kt
index 05b8c1a..21427f7 100644
--- a/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/SwipeableTest.kt
+++ b/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/SwipeableTest.kt
@@ -14,6 +14,8 @@
  * limitations under the License.
  */
 
+package androidx.wear.compose.material
+
 import androidx.compose.foundation.gestures.Orientation
 import androidx.compose.foundation.gestures.Orientation.Horizontal
 import androidx.compose.foundation.gestures.Orientation.Vertical
@@ -38,12 +40,9 @@
 import androidx.compose.ui.test.onNodeWithTag
 import androidx.compose.ui.test.performTouchInput
 import androidx.compose.ui.unit.dp
-import androidx.wear.compose.material.ExperimentalWearMaterialApi
-import androidx.wear.compose.material.SwipeableState
-import androidx.wear.compose.material.swipeable
+import kotlin.math.absoluteValue
 import org.junit.Rule
 import org.junit.Test
-import kotlin.math.absoluteValue
 
 // TODO(b/201009199) Some of these tests may need specific values adjusted when swipeable
 //  supports property nested scrolling, but the tests should all still be valid.
@@ -228,5 +227,3 @@
         } ?: false
     }
 }
-
-private const val TEST_TAG = "swipeable"