Merge "Fix missing invalidations while movable content is moving" into androidx-main
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/Release.kt b/buildSrc/private/src/main/kotlin/androidx/build/Release.kt
index 46c80b0..a6afb13 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/Release.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/Release.kt
@@ -88,7 +88,9 @@
             onRegister = {
             }
         )
-        finalizedBy(verifyTask)
+        if (!isPresubmitBuild()) {
+            finalizedBy(verifyTask)
+        }
     }
 
     /**
@@ -122,7 +124,7 @@
                 include(inclusion)
             }
         }
-        verifyTask.get().addFile(fromDir)
+        verifyTask.get().addFile(File(fromDir, "${artifact.version}"))
     }
     /**
      * Config action that configures the task when necessary.
@@ -396,21 +398,27 @@
     @TaskAction
     fun execute() {
         val missingFiles = mutableListOf<String>()
+        val emptyDirs = mutableListOf<String>()
         filesToVerify.forEach { file ->
             if (!file.exists()) {
                 missingFiles.add(file.path)
+            } else {
+                if (file.isDirectory) {
+                    if (file.listFiles().isEmpty()) {
+                        emptyDirs.add(file.path)
+                    }
+                }
             }
         }
 
-        if (missingFiles.isNotEmpty()) {
-            val checkedFilesString = filesToVerify.map {
-                it -> it.toString()
-            }.reduce {
-                acc, s -> "$acc, $s"
-            }
-            val missingFileString = missingFiles.reduce { acc, s -> "$acc, $s" }
+        if (missingFiles.isNotEmpty() || emptyDirs.isNotEmpty()) {
+            val checkedFilesString = filesToVerify.toString()
+            val missingFileString = missingFiles.toString()
+            val emptyDirsString = emptyDirs.toString()
             throw FileNotFoundException(
-                "GMavenZip file missing: $missingFileString. Checked files: $checkedFilesString"
+                "GMavenZip ${missingFiles.size} missing files: $missingFileString, " +
+                    "${emptyDirs.size} empty dirs: $emptyDirsString. " +
+                    "Checked files: $checkedFilesString"
             )
         }
     }
diff --git a/camera/camera-camera2-pipe-integration/src/androidTest/java/androidx/camera/camera2/pipe/integration/impl/DisplayInfoManagerTest.kt b/camera/camera-camera2-pipe-integration/src/androidTest/java/androidx/camera/camera2/pipe/integration/impl/DisplayInfoManagerTest.kt
index 7bbc024..02991ed 100644
--- a/camera/camera-camera2-pipe-integration/src/androidTest/java/androidx/camera/camera2/pipe/integration/impl/DisplayInfoManagerTest.kt
+++ b/camera/camera-camera2-pipe-integration/src/androidTest/java/androidx/camera/camera2/pipe/integration/impl/DisplayInfoManagerTest.kt
@@ -58,7 +58,7 @@
     @Test
     fun previewSizeAreaIsWithinMaxPreviewArea() {
         // Act & Assert
-        val previewSize = displayInfoManager.previewSize
+        val previewSize = displayInfoManager.getPreviewSize()
         assertTrue("$previewSize has larger area than 1920 * 1080",
             previewSize.width * previewSize.height <= 1920 * 1080)
     }
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraUseCaseAdapter.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraUseCaseAdapter.kt
index 91b1c4f..5ef866d 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraUseCaseAdapter.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraUseCaseAdapter.kt
@@ -129,7 +129,7 @@
         if (captureType == CaptureType.PREVIEW) {
             mutableConfig.insertOption(
                 ImageOutputConfig.OPTION_MAX_RESOLUTION,
-                displayInfoManager.previewSize
+                displayInfoManager.getPreviewSize()
             )
         }
 
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/SupportedSurfaceCombination.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/SupportedSurfaceCombination.kt
index c8a98dc0..e8e8998 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/SupportedSurfaceCombination.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/SupportedSurfaceCombination.kt
@@ -30,7 +30,10 @@
 import androidx.annotation.RequiresApi
 import androidx.camera.camera2.pipe.CameraMetadata
 import androidx.camera.camera2.pipe.integration.compat.StreamConfigurationMapCompat
+import androidx.camera.camera2.pipe.integration.compat.workaround.ExtraSupportedSurfaceCombinationsContainer
 import androidx.camera.camera2.pipe.integration.compat.workaround.OutputSizesCorrector
+import androidx.camera.camera2.pipe.integration.compat.workaround.ResolutionCorrector
+import androidx.camera.camera2.pipe.integration.impl.DisplayInfoManager
 import androidx.camera.core.impl.AttachedSurfaceInfo
 import androidx.camera.core.impl.EncoderProfilesProxy
 import androidx.camera.core.impl.ImageFormatConstants
@@ -76,6 +79,10 @@
     private val displayManager: DisplayManager =
         (context.getSystemService(Context.DISPLAY_SERVICE) as DisplayManager)
     private val streamConfigurationMapCompat = getStreamConfigurationMapCompat()
+    private val extraSupportedSurfaceCombinationsContainer =
+        ExtraSupportedSurfaceCombinationsContainer()
+    private val displayInfoManager = DisplayInfoManager(context)
+    private val resolutionCorrector = ResolutionCorrector()
 
     init {
         checkCapabilities()
@@ -124,8 +131,10 @@
     ): SurfaceConfig {
         val maxOutputSizeForConcurrentMode = if (isConcurrentCameraModeOn)
             getMaxOutputSizeByFormat(imageFormat) else null
-        return SurfaceConfig.transformSurfaceConfig(isConcurrentCameraModeOn,
-            imageFormat, size, surfaceSizeDefinition, maxOutputSizeForConcurrentMode)
+        return SurfaceConfig.transformSurfaceConfig(
+            isConcurrentCameraModeOn,
+            imageFormat, size, surfaceSizeDefinition, maxOutputSizeForConcurrentMode
+        )
     }
 
     /**
@@ -182,8 +191,12 @@
 
         // Collect supported output sizes for all use cases
         for (index in useCasesPriorityOrder) {
-            val supportedOutputSizes: List<Size> =
+            var supportedOutputSizes: List<Size> =
                 newUseCaseConfigsSupportedSizeMap[newUseCaseConfigs[index]]!!
+            supportedOutputSizes = resolutionCorrector.insertOrPrioritize(
+                SurfaceConfig.getConfigType(newUseCaseConfigs[index].inputFormat),
+                supportedOutputSizes
+            )
             supportedOutputSizesList.add(supportedOutputSizes)
         }
         // Get all possible size arrangements
@@ -223,9 +236,11 @@
                 for (useCaseConfig in newUseCaseConfigs) {
                     suggestedStreamSpecMap.put(
                         useCaseConfig,
-                        StreamSpec.builder(possibleSizeList[useCasesPriorityOrder.indexOf(
-                            newUseCaseConfigs.indexOf(useCaseConfig)
-                        )]).build()
+                        StreamSpec.builder(
+                            possibleSizeList[useCasesPriorityOrder.indexOf(
+                                newUseCaseConfigs.indexOf(useCaseConfig)
+                            )]
+                        ).build()
                     )
                 }
                 break
@@ -250,14 +265,19 @@
      * Refresh Preview Size based on current display configurations.
      */
     private fun refreshPreviewSize() {
-        val previewSize: Size = calculatePreviewSize()
-        surfaceSizeDefinition = SurfaceSizeDefinition.create(
-            surfaceSizeDefinition.analysisSize,
-            surfaceSizeDefinition.s720pSize,
-            previewSize,
-            surfaceSizeDefinition.s1440pSize,
-            surfaceSizeDefinition.recordSize
-        )
+        displayInfoManager.refresh()
+        if (!::surfaceSizeDefinition.isInitialized) {
+            generateSurfaceSizeDefinition()
+        } else {
+            val previewSize: Size = displayInfoManager.getPreviewSize()
+            surfaceSizeDefinition = SurfaceSizeDefinition.create(
+                surfaceSizeDefinition.analysisSize,
+                surfaceSizeDefinition.s720pSize,
+                previewSize,
+                surfaceSizeDefinition.s1440pSize,
+                surfaceSizeDefinition.recordSize
+            )
+        }
     }
 
     /**
@@ -284,13 +304,15 @@
                 isRawSupported, isBurstCaptureSupported
             )
         )
-        // TODO(b/246609101): ExtraSupportedSurfaceCombinationsQuirk is supposed to be here to add additional
-        //  surface combinations to the list
+        surfaceCombinations.addAll(
+            extraSupportedSurfaceCombinationsContainer[cameraId, hardwareLevel]
+        )
     }
 
     private fun generateConcurrentSupportedCombinationList() {
         concurrentSurfaceCombinations.addAll(
-            GuaranteedConfigurationsUtil.generateConcurrentSupportedCombinationList())
+            GuaranteedConfigurationsUtil.generateConcurrentSupportedCombinationList()
+        )
     }
 
     /**
@@ -304,10 +326,12 @@
         // Same for s1440p.
         val s720pSize = Size(1280, 720)
         val s1440pSize = Size(1920, 1440)
-        val previewSize: Size = calculatePreviewSize()
+        val previewSize: Size = displayInfoManager.getPreviewSize()
         val recordSize: Size = getRecordSize()
-        surfaceSizeDefinition = SurfaceSizeDefinition.create(vgaSize, s720pSize, previewSize,
-            s1440pSize, recordSize)
+        surfaceSizeDefinition = SurfaceSizeDefinition.create(
+            vgaSize, s720pSize, previewSize,
+            s1440pSize, recordSize
+        )
     }
 
     /**
@@ -337,7 +361,7 @@
     private fun getStreamConfigurationMapCompat(): StreamConfigurationMapCompat {
         val map = cameraMetadata[CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP]
             ?: throw IllegalArgumentException("Cannot retrieve SCALER_STREAM_CONFIGURATION_MAP")
-        return StreamConfigurationMapCompat(map, OutputSizesCorrector(cameraMetadata))
+        return StreamConfigurationMapCompat(map, OutputSizesCorrector(cameraMetadata, map))
     }
 
     /**
@@ -347,7 +371,9 @@
      * @return Maximum supported video size.
      */
     private fun getRecordSizeFromStreamConfigurationMapCompat(): Size {
-        val videoSizeArr = streamConfigurationMapCompat.getOutputSizes(
+        val map: StreamConfigurationMap =
+            streamConfigurationMapCompat.toStreamConfigurationMap()
+        val videoSizeArr = map.getOutputSizes(
             MediaRecorder::class.java
         ) ?: return RESOLUTION_480P
         Arrays.sort(videoSizeArr, CompareSizesByArea(true))
@@ -394,31 +420,6 @@
     }
 
     /**
-     * Calculates the size for preview. If the max size is larger than 1080p, use 1080p.
-     */
-    @SuppressWarnings("deprecation")
-    /* getRealSize */
-    private fun calculatePreviewSize(): Size {
-        val displaySize = Point()
-        val display: Display = getMaxSizeDisplay()
-        display.getRealSize(displaySize)
-        var displayViewSize: Size
-        displayViewSize = if (displaySize.x > displaySize.y) {
-            Size(displaySize.x, displaySize.y)
-        } else {
-            Size(displaySize.y, displaySize.x)
-        }
-        if (displayViewSize.width * displayViewSize.height
-            > RESOLUTION_1080P.width * RESOLUTION_1080P.height
-        ) {
-            displayViewSize = RESOLUTION_1080P
-        }
-        // TODO(b/245619094): Use ExtraCroppingQuirk to potentially override this with select
-        //  resolution
-        return displayViewSize
-    }
-
-    /**
      * Retrieves the display which has the max size among all displays.
      */
     private fun getMaxSizeDisplay(): Display {
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/quirk/AspectRatioLegacyApi21Quirk.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/quirk/AspectRatioLegacyApi21Quirk.kt
new file mode 100644
index 0000000..c46b711
--- /dev/null
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/quirk/AspectRatioLegacyApi21Quirk.kt
@@ -0,0 +1,56 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.camera2.pipe.integration.compat.quirk
+
+import android.hardware.camera2.CameraCharacteristics
+import android.os.Build
+import androidx.annotation.RequiresApi
+import androidx.camera.camera2.pipe.CameraMetadata
+import androidx.camera.camera2.pipe.integration.compat.workaround.TargetAspectRatio
+import androidx.camera.core.impl.Quirk
+
+/**
+ *
+ * QuirkSummary
+ * Bug Id: b/128924712
+ * Description: Quirk that produces stretched use cases on all the legacy API 21 devices. If
+ * the device is LEGACY + Android 5.0, then return the same aspect ratio as maximum JPEG
+ * resolution. The Camera2 LEGACY mode API always sends the HAL a configure call with the
+ * same aspect ratio as the maximum JPEG resolution, and do the cropping/scaling before
+ * returning the output. There is a bug because of a flipped scaling factor in the
+ * intermediate texture transform matrix, and it was fixed in L MR1.
+ * Device(s): All the legacy API 21 devices
+ * @see androidx.camera.camera2.internal.compat.workaround.TargetAspectRatio
+ */
+@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
+class AspectRatioLegacyApi21Quirk : Quirk {
+    /**
+     * Get the corrected aspect ratio.
+     */
+    @TargetAspectRatio.Ratio
+    fun getCorrectedAspectRatio(): Int {
+        return TargetAspectRatio.RATIO_MAX_JPEG
+    }
+
+    companion object {
+        fun load(cameraMetadata: CameraMetadata): Boolean {
+            val level: Int? = cameraMetadata[CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL]
+            return level == CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY &&
+                Build.VERSION.SDK_INT == 21
+        }
+    }
+}
\ No newline at end of file
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/quirk/CameraQuirks.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/quirk/CameraQuirks.kt
index c0c7158..08310aa 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/quirk/CameraQuirks.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/quirk/CameraQuirks.kt
@@ -45,6 +45,9 @@
         if (AfRegionFlipHorizontallyQuirk.isEnabled(cameraMetadata)) {
             quirks.add(AfRegionFlipHorizontallyQuirk())
         }
+        if (AspectRatioLegacyApi21Quirk.load(cameraMetadata)) {
+            quirks.add(AspectRatioLegacyApi21Quirk())
+        }
         if (CamcorderProfileResolutionQuirk.isEnabled(cameraMetadata)) {
             quirks.add(CamcorderProfileResolutionQuirk(streamConfigurationMapCompat))
         }
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/quirk/DeviceQuirksLoader.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/quirk/DeviceQuirksLoader.kt
index 4508e2f..24cdf62 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/quirk/DeviceQuirksLoader.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/quirk/DeviceQuirksLoader.kt
@@ -55,9 +55,18 @@
         if (ExcludedSupportedSizesQuirk.load()) {
             quirks.add(ExcludedSupportedSizesQuirk())
         }
+        if (ExtraCroppingQuirk.load()) {
+            quirks.add(ExtraCroppingQuirk())
+        }
         if (ExtraSupportedOutputSizeQuirk.load()) {
             quirks.add(ExtraSupportedOutputSizeQuirk())
         }
+        if (ExtraSupportedSurfaceCombinationsQuirk.load()) {
+            quirks.add(ExtraSupportedSurfaceCombinationsQuirk())
+        }
+        if (Nexus4AndroidLTargetAspectRatioQuirk.load()) {
+            quirks.add(Nexus4AndroidLTargetAspectRatioQuirk())
+        }
         if (PreviewPixelHDRnetQuirk.isEnabled()) {
             quirks.add(PreviewPixelHDRnetQuirk())
         }
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/quirk/ExtraCroppingQuirk.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/quirk/ExtraCroppingQuirk.kt
new file mode 100644
index 0000000..f391079
--- /dev/null
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/quirk/ExtraCroppingQuirk.kt
@@ -0,0 +1,99 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.camera2.pipe.integration.compat.quirk
+
+import android.os.Build
+import android.util.Range
+import android.util.Size
+import androidx.annotation.RequiresApi
+import androidx.camera.core.impl.Quirk
+import androidx.camera.core.impl.SurfaceConfig.ConfigType
+
+/**
+ * Quirk that requires specific resolutions as the workaround.
+ *
+ * QuirkSummary
+ * Bug Id: 190203334
+ * Description: The symptom of these devices is that the output of one or many streams,
+ * including PRIV, JPEG and/or YUV, can have an unintended 25% crop, and the
+ * cropped image is stretched to fill the Surface, which results in a distorted
+ * output. The streams can also have an unintended 25% double crop, in which
+ * case the stretched image will not be distorted, but the FOV is smaller than
+ * it should be. The behavior is inconsistent in a way that the extra cropping
+ * depends on the resolution of the streams. The existence of the issue also
+ * depends on API level and/or build number. See discussion in
+ * go/samsung-camera-distortion.
+ * Device(s): Samsung Galaxy Tab A (2016) SM-T580, Samsung Galaxy J7 (2016) SM-J710MN,
+ * Samsung Galaxy A3 (2017) SM-A320FL, Samsung Galaxy J5 Prime SM-G570M,
+ * Samsung Galaxy J7 Prime SM-G610F, Samsung Galaxy J7 Prime SM-G610M
+ */
+@RequiresApi(21)
+class ExtraCroppingQuirk : Quirk {
+    /**
+     * Get a verified resolution that is guaranteed to work.
+     *
+     *  The selected resolution have been manually tested by CameraX team. It is known to
+     * work for the given device/stream.
+     *
+     * @return null if no resolution provided, in which case the calling code should fallback to
+     * user provided target resolution.
+     */
+    fun getVerifiedResolution(configType: ConfigType): Size? {
+        return if (isSamsungDistortion) {
+            // The following resolutions are needed for both the front and the back camera.
+            when (configType) {
+                ConfigType.PRIV -> Size(1920, 1080)
+                ConfigType.YUV -> Size(1280, 720)
+                ConfigType.JPEG -> Size(3264, 1836)
+                else -> null
+            }
+        } else null
+    }
+
+    companion object {
+        private val SAMSUNG_DISTORTION_MODELS_TO_API_LEVEL_MAP: MutableMap<String, Range<Int>?> =
+            mutableMapOf(
+                "SM-T580" to null,
+                "SM-J710MN" to Range(21, 26),
+                "SM-A320FL" to null,
+                "SM-G570M" to null,
+                "SM-G610F" to null,
+                "SM-G610M" to Range(21, 26)
+            )
+
+        fun load(): Boolean {
+            return isSamsungDistortion
+        }
+
+        /**
+         * Checks for device model with Samsung output distortion bug (b/190203334).
+         */
+        internal val isSamsungDistortion: Boolean
+            get() {
+                val isDeviceModelContained = ("samsung".equals(Build.BRAND, ignoreCase = true) &&
+                    SAMSUNG_DISTORTION_MODELS_TO_API_LEVEL_MAP.containsKey(
+                        Build.MODEL.uppercase()
+                    ))
+                if (!isDeviceModelContained) {
+                    return false
+                }
+                val apiLevelRange =
+                    SAMSUNG_DISTORTION_MODELS_TO_API_LEVEL_MAP[Build.MODEL.uppercase()]
+                return apiLevelRange?.contains(Build.VERSION.SDK_INT) ?: true
+            }
+    }
+}
\ No newline at end of file
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/quirk/ExtraSupportedSurfaceCombinationsQuirk.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/quirk/ExtraSupportedSurfaceCombinationsQuirk.kt
new file mode 100644
index 0000000..5a63102
--- /dev/null
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/quirk/ExtraSupportedSurfaceCombinationsQuirk.kt
@@ -0,0 +1,352 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.camera2.pipe.integration.compat.quirk
+
+import android.hardware.camera2.CameraCharacteristics
+import android.os.Build
+import androidx.annotation.RequiresApi
+import androidx.camera.core.impl.Quirk
+import androidx.camera.core.impl.SurfaceCombination
+import androidx.camera.core.impl.SurfaceConfig
+
+/**
+ *
+ * QuirkSummary
+ * Bug Id: b/194149215
+ * Description: Quirk required to include extra supported surface combinations which are
+ * additional to the guaranteed supported configurations. An example is the
+ * Samsung S7's LIMITED-level camera device can support additional YUV/640x480 +
+ * PRIV/PREVIEW + YUV/MAXIMUM combination. Some other Samsung devices can
+ * support additional YUV/640x480 + PRIV/PREVIEW + YUV/MAXIMUM and YUV/640x480 +
+ * YUV/PREVIEW + YUV/MAXIMUM configurations.
+ * Device(s): Some Samsung devices
+ */
+@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
+
+class ExtraSupportedSurfaceCombinationsQuirk : Quirk {
+    /**
+     * Returns the extra supported surface combinations for specific camera on the device.
+     */
+    fun getExtraSupportedSurfaceCombinations(
+        cameraId: String,
+        hardwareLevel: Int
+    ): List<SurfaceCombination> {
+        if (isSamsungS7) {
+            return getSamsungS7ExtraCombinations(cameraId)
+        }
+        if (supportExtraFullConfigurationsSamsungDevice()) {
+            return getLimitedDeviceExtraSupportedFullConfigurations(hardwareLevel)
+        }
+        return if (supportExtraLevel3ConfigurationsGoogleDevice()) {
+            listOf(LEVEL_3_LEVEL_PRIV_PRIV_YUV_RAW_CONFIGURATION)
+        } else emptyList()
+    }
+
+    private fun getSamsungS7ExtraCombinations(cameraId: String): List<SurfaceCombination> {
+        val extraCombinations: MutableList<SurfaceCombination> = ArrayList()
+        if (cameraId == "1") {
+            // (YUV, ANALYSIS) + (PRIV, PREVIEW) + (YUV, MAXIMUM)
+            extraCombinations.add(FULL_LEVEL_YUV_PRIV_YUV_CONFIGURATION)
+        }
+        return extraCombinations
+    }
+
+    private fun getLimitedDeviceExtraSupportedFullConfigurations(
+        hardwareLevel: Int
+    ): List<SurfaceCombination> {
+        val extraCombinations: MutableList<SurfaceCombination> = ArrayList()
+        if (hardwareLevel == CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED) {
+            // (YUV, ANALYSIS) + (PRIV, PREVIEW) + (YUV, MAXIMUM)
+            extraCombinations.add(FULL_LEVEL_YUV_PRIV_YUV_CONFIGURATION)
+            // (YUV, ANALYSIS) + (YUV, PREVIEW) + (YUV, MAXIMUM)
+            extraCombinations.add(FULL_LEVEL_YUV_YUV_YUV_CONFIGURATION)
+        }
+        return extraCombinations
+    }
+
+    companion object {
+        private const val TAG = "ExtraSupportedSurfaceCombinationsQuirk"
+        private val FULL_LEVEL_YUV_PRIV_YUV_CONFIGURATION = createFullYuvPrivYuvConfiguration()
+        private val FULL_LEVEL_YUV_YUV_YUV_CONFIGURATION = createFullYuvYuvYuvConfiguration()
+        private val LEVEL_3_LEVEL_PRIV_PRIV_YUV_RAW_CONFIGURATION =
+            createLevel3PrivPrivYuvRawConfiguration()
+        private val SUPPORT_EXTRA_FULL_CONFIGURATIONS_SAMSUNG_MODELS: Set<String> =
+            setOf(
+                "SM-A515F", // Galaxy A51
+                "SM-A515U", // Galaxy A51
+                "SM-A515U1", // Galaxy A51
+                "SM-A515W", // Galaxy A51
+                "SM-S515DL", // Galaxy A51
+                "SC-54A", // Galaxy A51 5G
+                "SCG07", // Galaxy A51 5G
+                "SM-A5160", // Galaxy A51 5G
+                "SM-A516B", // Galaxy A51 5G
+                "SM-A516N", // Galaxy A51 5G
+                "SM-A516U", // Galaxy A51 5G
+                "SM-A516U1", // Galaxy A51 5G
+                "SM-A516V", // Galaxy A51 5G
+                "SM-A715F", // Galaxy A71
+                "SM-A715W", // Galaxy A71
+                "SM-A7160", // Galaxy A71 5G
+                "SM-A716B", // Galaxy A71 5G
+                "SM-A716U", // Galaxy A71 5G
+                "SM-A716U1", // Galaxy A71 5G
+                "SM-A716V", // Galaxy A71 5G
+                "SM-A8050", // Galaxy A80
+                "SM-A805F", // Galaxy A80
+                "SM-A805N", // Galaxy A80
+                "SCV44", // Galaxy Fold
+                "SM-F9000", // Galaxy Fold
+                "SM-F900F", // Galaxy Fold
+                "SM-F900U", // Galaxy Fold
+                "SM-F900U1", // Galaxy Fold
+                "SM-F900W", // Galaxy Fold
+                "SM-F907B", // Galaxy Fold 5G
+                "SM-F907N", // Galaxy Fold 5G
+                "SM-N970F", // Galaxy Note10
+                "SM-N9700", // Galaxy Note10
+                "SM-N970U", // Galaxy Note10
+                "SM-N970U1", // Galaxy Note10
+                "SM-N970W", // Galaxy Note10
+                "SM-N971N", // Galaxy Note10 5G
+                "SM-N770F", // Galaxy Note10 Lite
+                "SC-01M", // Galaxy Note10+
+                "SCV45", // Galaxy Note10+
+                "SM-N9750", // Galaxy Note10+
+                "SM-N975C", // Galaxy Note10+
+                "SM-N975U", // Galaxy Note10+
+                "SM-N975U1", // Galaxy Note10+
+                "SM-N975W", // Galaxy Note10+
+                "SM-N975F", // Galaxy Note10+
+                "SM-N976B", // Galaxy Note10+ 5G
+                "SM-N976N", // Galaxy Note10+ 5G
+                "SM-N9760", // Galaxy Note10+ 5G
+                "SM-N976Q", // Galaxy Note10+ 5G
+                "SM-N976V", // Galaxy Note10+ 5G
+                "SM-N976U", // Galaxy Note10+ 5G
+                "SM-N9810", // Galaxy Note20 5G
+                "SM-N981N", // Galaxy Note20 5G
+                "SM-N981U", // Galaxy Note20 5G
+                "SM-N981U1", // Galaxy Note20 5G
+                "SM-N981W", // Galaxy Note20 5G
+                "SM-N981B", // Galaxy Note20 5G
+                "SC-53A", // Galaxy Note20 Ultra 5G
+                "SCG06", // Galaxy Note20 Ultra 5G
+                "SM-N9860", // Galaxy Note20 Ultra 5G
+                "SM-N986N", // Galaxy Note20 Ultra 5G
+                "SM-N986U", // Galaxy Note20 Ultra 5G
+                "SM-N986U1", // Galaxy Note20 Ultra 5G
+                "SM-N986W", // Galaxy Note20 Ultra 5G
+                "SM-N986B", // Galaxy Note20 Ultra 5G
+                "SC-03L", // Galaxy S10
+                "SCV41", // Galaxy S10
+                "SM-G973F", // Galaxy S10
+                "SM-G973N", // Galaxy S10
+                "SM-G9730", // Galaxy S10
+                "SM-G9738", // Galaxy S10
+                "SM-G973C", // Galaxy S10
+                "SM-G973U", // Galaxy S10
+                "SM-G973U1", // Galaxy S10
+                "SM-G973W", // Galaxy S10
+                "SM-G977B", // Galaxy S10 5G
+                "SM-G977N", // Galaxy S10 5G
+                "SM-G977P", // Galaxy S10 5G
+                "SM-G977T", // Galaxy S10 5G
+                "SM-G977U", // Galaxy S10 5G
+                "SM-G770F", // Galaxy S10 Lite
+                "SM-G770U1", // Galaxy S10 Lite
+                "SC-04L", // Galaxy S10+
+                "SCV42", // Galaxy S10+
+                "SM-G975F", // Galaxy S10+
+                "SM-G975N", // Galaxy S10+
+                "SM-G9750", // Galaxy S10+
+                "SM-G9758", // Galaxy S10+
+                "SM-G975U", // Galaxy S10+
+                "SM-G975U1", // Galaxy S10+
+                "SM-G975W", // Galaxy S10+
+                "SC-05L", // Galaxy S10+ Olympic Games Edition
+                "SM-G970F", // Galaxy S10e
+                "SM-G970N", // Galaxy S10e
+                "SM-G9700", // Galaxy S10e
+                "SM-G9708", // Galaxy S10e
+                "SM-G970U", // Galaxy S10e
+                "SM-G970U1", // Galaxy S10e
+                "SM-G970W", // Galaxy S10e
+                "SC-51A", // Galaxy S20 5G
+                "SC51Aa", // Galaxy S20 5G
+                "SCG01", // Galaxy S20 5G
+                "SM-G9810", // Galaxy S20 5G
+                "SM-G981N", // Galaxy S20 5G
+                "SM-G981U", // Galaxy S20 5G
+                "SM-G981U1", // Galaxy S20 5G
+                "SM-G981V", // Galaxy S20 5G
+                "SM-G981W", // Galaxy S20 5G
+                "SM-G981B", // Galaxy S20 5G
+                "SCG03", // Galaxy S20 Ultra 5G
+                "SM-G9880", // Galaxy S20 Ultra 5G
+                "SM-G988N", // Galaxy S20 Ultra 5G
+                "SM-G988Q", // Galaxy S20 Ultra 5G
+                "SM-G988U", // Galaxy S20 Ultra 5G
+                "SM-G988U1", // Galaxy S20 Ultra 5G
+                "SM-G988W", // Galaxy S20 Ultra 5G
+                "SM-G988B", // Galaxy S20 Ultra 5G
+                "SC-52A", // Galaxy S20+ 5G
+                "SCG02", // Galaxy S20+ 5G
+                "SM-G9860", // Galaxy S20+ 5G
+                "SM-G986N", // Galaxy S20+ 5G
+                "SM-G986U", // Galaxy S20+ 5G
+                "SM-G986U1", // Galaxy S20+ 5G
+                "SM-G986W", // Galaxy S20+ 5G
+                "SM-G986B", // Galaxy S20+ 5G
+                "SCV47", // Galaxy Z Flip
+                "SM-F7000", // Galaxy Z Flip
+                "SM-F700F", // Galaxy Z Flip
+                "SM-F700N", // Galaxy Z Flip
+                "SM-F700U", // Galaxy Z Flip
+                "SM-F700U1", // Galaxy Z Flip
+                "SM-F700W", // Galaxy Z Flip
+                "SCG04", // Galaxy Z Flip 5G
+                "SM-F7070", // Galaxy Z Flip 5G
+                "SM-F707B", // Galaxy Z Flip 5G
+                "SM-F707N", // Galaxy Z Flip 5G
+                "SM-F707U", // Galaxy Z Flip 5G
+                "SM-F707U1", // Galaxy Z Flip 5G
+                "SM-F707W", // Galaxy Z Flip 5G
+                "SM-F9160", // Galaxy Z Fold2 5G
+                "SM-F916B", // Galaxy Z Fold2 5G
+                "SM-F916N", // Galaxy Z Fold2 5G
+                "SM-F916Q", // Galaxy Z Fold2 5G
+                "SM-F916U", // Galaxy Z Fold2 5G
+                "SM-F916U1", // Galaxy Z Fold2 5G
+                "SM-F916W" // Galaxy Z Fold2 5G
+            )
+        private val SUPPORT_EXTRA_LEVEL_3_CONFIGURATIONS_GOOGLE_MODELS: Set<String> =
+            setOf(
+                "PIXEL 6",
+                "PIXEL 6 PRO",
+                "PIXEL 7",
+                "PIXEL 7 PRO"
+            )
+
+        fun load(): Boolean {
+            return (isSamsungS7 || supportExtraFullConfigurationsSamsungDevice() ||
+                supportExtraLevel3ConfigurationsGoogleDevice())
+        }
+
+        internal val isSamsungS7: Boolean
+            get() = "heroqltevzw".equals(
+                Build.DEVICE,
+                ignoreCase = true
+            ) || "heroqltetmo".equals(
+                Build.DEVICE, ignoreCase = true
+            )
+
+        internal fun supportExtraFullConfigurationsSamsungDevice(): Boolean {
+            if (!"samsung".equals(Build.BRAND, ignoreCase = true)) {
+                return false
+            }
+            val capitalModelName = Build.MODEL.uppercase()
+            return SUPPORT_EXTRA_FULL_CONFIGURATIONS_SAMSUNG_MODELS.contains(capitalModelName)
+        }
+
+        internal fun supportExtraLevel3ConfigurationsGoogleDevice(): Boolean {
+            if (!"google".equals(Build.BRAND, ignoreCase = true)) {
+                return false
+            }
+            val capitalModelName = Build.MODEL.uppercase()
+            return SUPPORT_EXTRA_LEVEL_3_CONFIGURATIONS_GOOGLE_MODELS.contains(capitalModelName)
+        }
+
+        internal fun createFullYuvPrivYuvConfiguration(): SurfaceCombination {
+            // (YUV, ANALYSIS) + (PRIV, PREVIEW) + (YUV, MAXIMUM)
+            val surfaceCombination = SurfaceCombination()
+            surfaceCombination.addSurfaceConfig(
+                SurfaceConfig.create(
+                    SurfaceConfig.ConfigType.YUV,
+                    SurfaceConfig.ConfigSize.VGA
+                )
+            )
+            surfaceCombination.addSurfaceConfig(
+                SurfaceConfig.create(
+                    SurfaceConfig.ConfigType.PRIV,
+                    SurfaceConfig.ConfigSize.PREVIEW
+                )
+            )
+            surfaceCombination.addSurfaceConfig(
+                SurfaceConfig.create(
+                    SurfaceConfig.ConfigType.YUV,
+                    SurfaceConfig.ConfigSize.MAXIMUM
+                )
+            )
+            return surfaceCombination
+        }
+
+        internal fun createFullYuvYuvYuvConfiguration(): SurfaceCombination {
+            // (YUV, ANALYSIS) + (YUV, PREVIEW) + (YUV, MAXIMUM)
+            val surfaceCombination = SurfaceCombination()
+            surfaceCombination.addSurfaceConfig(
+                SurfaceConfig.create(
+                    SurfaceConfig.ConfigType.YUV,
+                    SurfaceConfig.ConfigSize.VGA
+                )
+            )
+            surfaceCombination.addSurfaceConfig(
+                SurfaceConfig.create(
+                    SurfaceConfig.ConfigType.YUV,
+                    SurfaceConfig.ConfigSize.PREVIEW
+                )
+            )
+            surfaceCombination.addSurfaceConfig(
+                SurfaceConfig.create(
+                    SurfaceConfig.ConfigType.YUV,
+                    SurfaceConfig.ConfigSize.MAXIMUM
+                )
+            )
+            return surfaceCombination
+        }
+
+        internal fun createLevel3PrivPrivYuvRawConfiguration(): SurfaceCombination {
+            // (PRIV, PREVIEW) + (PRIV, ANALYSIS) + (YUV, MAXIMUM) + (RAW, MAXIMUM)
+            val surfaceCombination = SurfaceCombination()
+            surfaceCombination.addSurfaceConfig(
+                SurfaceConfig.create(
+                    SurfaceConfig.ConfigType.PRIV,
+                    SurfaceConfig.ConfigSize.PREVIEW
+                )
+            )
+            surfaceCombination.addSurfaceConfig(
+                SurfaceConfig.create(
+                    SurfaceConfig.ConfigType.PRIV,
+                    SurfaceConfig.ConfigSize.VGA
+                )
+            )
+            surfaceCombination.addSurfaceConfig(
+                SurfaceConfig.create(
+                    SurfaceConfig.ConfigType.YUV,
+                    SurfaceConfig.ConfigSize.MAXIMUM
+                )
+            )
+            surfaceCombination.addSurfaceConfig(
+                SurfaceConfig.create(
+                    SurfaceConfig.ConfigType.RAW,
+                    SurfaceConfig.ConfigSize.MAXIMUM
+                )
+            )
+            return surfaceCombination
+        }
+    }
+}
\ No newline at end of file
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/quirk/Nexus4AndroidLTargetAspectRatioQuirk.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/quirk/Nexus4AndroidLTargetAspectRatioQuirk.kt
new file mode 100644
index 0000000..7147ca8
--- /dev/null
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/quirk/Nexus4AndroidLTargetAspectRatioQuirk.kt
@@ -0,0 +1,61 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.camera2.pipe.integration.compat.quirk
+
+import android.os.Build
+import androidx.annotation.RequiresApi
+import androidx.camera.camera2.pipe.integration.compat.workaround.TargetAspectRatio
+import androidx.camera.core.impl.Quirk
+
+/**
+ *
+ * QuirkSummary
+ * Bug Id: b/19606058
+ * Description: Quirk that produces stretched preview on Nexus 4 devices running Android L
+ * (API levels 21 and 22). There is a Camera1/HAL1 issue on the Nexus 4. The preview will be
+ * stretched when configuring a JPEG that doesn't actually have the same aspect ratio as the
+ * maximum JPEG resolution.
+ * Device(s): Google Nexus 4
+ * @see androidx.camera.camera2.internal.compat.workaround.TargetAspectRatio
+ */
+@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
+
+class Nexus4AndroidLTargetAspectRatioQuirk : Quirk {
+    /**
+     * Get the corrected aspect ratio.
+     */
+    @TargetAspectRatio.Ratio
+    fun getCorrectedAspectRatio(): Int {
+        return TargetAspectRatio.RATIO_MAX_JPEG
+    }
+
+    companion object {
+        // List of devices with the issue.
+        private val DEVICE_MODELS = listOf(
+            "NEXUS 4" // b/158749159
+        )
+
+        fun load(): Boolean {
+            return "GOOGLE".equals(
+                Build.BRAND,
+                ignoreCase = true
+            ) && Build.VERSION.SDK_INT < 23 && DEVICE_MODELS.contains(
+                Build.MODEL.uppercase()
+            )
+        }
+    }
+}
\ No newline at end of file
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/workaround/ExtraSupportedSurfaceCombinationsContainer.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/workaround/ExtraSupportedSurfaceCombinationsContainer.kt
new file mode 100644
index 0000000..b56f416
--- /dev/null
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/workaround/ExtraSupportedSurfaceCombinationsContainer.kt
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.camera2.pipe.integration.compat.workaround
+
+import androidx.annotation.RequiresApi
+import androidx.camera.camera2.pipe.integration.compat.quirk.DeviceQuirks
+import androidx.camera.camera2.pipe.integration.compat.quirk.ExtraSupportedSurfaceCombinationsQuirk
+import androidx.camera.core.impl.SurfaceCombination
+
+@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
+class ExtraSupportedSurfaceCombinationsContainer {
+    private val quirk: ExtraSupportedSurfaceCombinationsQuirk? =
+        DeviceQuirks[ExtraSupportedSurfaceCombinationsQuirk::class.java]
+
+    /**
+     * Retrieves the extra surface combinations which can be supported on the device.
+     */
+    operator fun get(cameraId: String, hardwareLevel: Int): List<SurfaceCombination> {
+        return quirk?.getExtraSupportedSurfaceCombinations(cameraId, hardwareLevel) ?: listOf()
+    }
+}
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/workaround/MaxPreviewSize.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/workaround/MaxPreviewSize.kt
new file mode 100644
index 0000000..93a724e
--- /dev/null
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/workaround/MaxPreviewSize.kt
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.camera2.pipe.integration.compat.workaround
+
+import android.util.Size
+import androidx.annotation.RequiresApi
+import androidx.camera.camera2.pipe.integration.compat.quirk.DeviceQuirks
+import androidx.camera.camera2.pipe.integration.compat.quirk.ExtraCroppingQuirk
+import androidx.camera.core.impl.SurfaceConfig
+
+/**
+ * Helper class that overrides the maximum preview size used in surface combination check.
+ */
+@RequiresApi(21)
+class MaxPreviewSize constructor(
+    private val extraCroppingQuirk: ExtraCroppingQuirk? =
+        DeviceQuirks[ExtraCroppingQuirk::class.java]
+) {
+
+    /**
+     * Gets the max preview resolution based on the default preview max resolution.
+     *
+     *
+     *  If select resolution is larger than the default resolution, return the select
+     * resolution. The select resolution has been manually tested on the device. Otherwise,
+     * return the default max resolution.
+     */
+    fun getMaxPreviewResolution(defaultMaxPreviewResolution: Size): Size {
+        if (extraCroppingQuirk == null) {
+            return defaultMaxPreviewResolution
+        }
+        val selectResolution: Size = extraCroppingQuirk.getVerifiedResolution(
+            SurfaceConfig.ConfigType.PRIV
+        ) ?: return defaultMaxPreviewResolution
+        val isSelectResolutionLarger = (selectResolution.width * selectResolution.height >
+            defaultMaxPreviewResolution.width * defaultMaxPreviewResolution.height)
+        return if (isSelectResolutionLarger) selectResolution else defaultMaxPreviewResolution
+    }
+}
\ No newline at end of file
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/workaround/OutputSizesCorrector.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/workaround/OutputSizesCorrector.kt
index a873e6b..723c840 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/workaround/OutputSizesCorrector.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/workaround/OutputSizesCorrector.kt
@@ -16,31 +16,40 @@
 
 package androidx.camera.camera2.pipe.integration.compat.workaround
 
+import android.hardware.camera2.params.StreamConfigurationMap
+import android.util.Rational
 import android.util.Size
 import androidx.annotation.RequiresApi
 import androidx.camera.camera2.pipe.CameraMetadata
-import androidx.camera.camera2.pipe.integration.config.CameraScope
-import javax.inject.Inject
+import androidx.camera.camera2.pipe.integration.compat.StreamConfigurationMapCompat
 import androidx.camera.camera2.pipe.integration.compat.quirk.DeviceQuirks
 import androidx.camera.camera2.pipe.integration.compat.quirk.ExcludedSupportedSizesQuirk
 import androidx.camera.camera2.pipe.integration.compat.quirk.ExtraSupportedOutputSizeQuirk
+import androidx.camera.camera2.pipe.integration.config.CameraScope
+import androidx.camera.core.impl.utils.AspectRatioUtil
+import androidx.camera.core.impl.utils.CompareSizesByArea
+import java.util.Collections
+import javax.inject.Inject
 
 /**
  * Helper class to provide the StreamConfigurationMap output sizes related correction functions.
  *
  * 1. ExtraSupportedOutputSizeQuirk
  * 2. ExcludedSupportedSizesContainer
- * 3. TargetAspectRatio
+ * 3. Nexus4AndroidLTargetAspectRatioQuirk
+ * 4. AspectRatioLegacyApi21Quirk
  */
 @CameraScope
 @RequiresApi(21)
 class OutputSizesCorrector @Inject constructor(
-    private val cameraMetadata: CameraMetadata
+    private val cameraMetadata: CameraMetadata,
+    private val streamConfigurationMap: StreamConfigurationMap
 ) {
     private val excludedSupportedSizesQuirk: ExcludedSupportedSizesQuirk? =
         DeviceQuirks[ExcludedSupportedSizesQuirk::class.java]
     private val extraSupportedOutputSizeQuirk: ExtraSupportedOutputSizeQuirk? =
         DeviceQuirks[ExtraSupportedOutputSizeQuirk::class.java]
+    private val targetAspectRatio: TargetAspectRatio = TargetAspectRatio()
 
     /**
      * Applies the output sizes related quirks onto the input sizes array.
@@ -132,8 +141,51 @@
      * Excludes output sizes by TargetAspectRatio.
      */
     private fun excludeOutputSizesByTargetAspectRatioWorkaround(sizes: Array<Size>?): Array<Size>? {
-        // TODO(b/245622117): Nexus4AndroidLTargetAspectRatioQuirk and AspectRatioLegacyApi21Quirk
-        return sizes
+        if (sizes == null) {
+            return null
+        }
+
+        val targetAspectRatio: Int =
+            targetAspectRatio[
+                cameraMetadata,
+                StreamConfigurationMapCompat(streamConfigurationMap, this)
+            ]
+
+        var ratio: Rational? = null
+
+        when (targetAspectRatio) {
+            TargetAspectRatio.RATIO_4_3 -> ratio =
+                AspectRatioUtil.ASPECT_RATIO_4_3
+
+            TargetAspectRatio.RATIO_16_9 -> ratio =
+                AspectRatioUtil.ASPECT_RATIO_16_9
+
+            TargetAspectRatio.RATIO_MAX_JPEG -> {
+                val maxJpegSize = Collections.max(sizes.asList(), CompareSizesByArea())
+                ratio = Rational(maxJpegSize.width, maxJpegSize.height)
+            }
+
+            TargetAspectRatio.RATIO_ORIGINAL -> ratio =
+                null
+        }
+
+        if (ratio == null) {
+            return sizes
+        }
+
+        val resultList: MutableList<Size> = java.util.ArrayList()
+
+        for (size in sizes) {
+            if (AspectRatioUtil.hasMatchingAspectRatio(size, ratio)) {
+                resultList.add(size)
+            }
+        }
+
+        return if (resultList.isEmpty()) {
+            null
+        } else {
+            resultList.toTypedArray()
+        }
     }
 
     private fun concatNullableSizeLists(
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/workaround/ResolutionCorrector.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/workaround/ResolutionCorrector.kt
new file mode 100644
index 0000000..c3cee29
--- /dev/null
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/workaround/ResolutionCorrector.kt
@@ -0,0 +1,63 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.camera2.pipe.integration.compat.workaround
+
+import android.util.Size
+import androidx.annotation.RequiresApi
+import androidx.camera.camera2.pipe.integration.compat.quirk.DeviceQuirks
+import androidx.camera.camera2.pipe.integration.compat.quirk.ExtraCroppingQuirk
+import androidx.camera.core.impl.SurfaceConfig.ConfigType
+
+/**
+ * Helper class that overrides user configured resolution with resolution selected based on device
+ * quirks.
+ */
+@RequiresApi(21)
+class ResolutionCorrector {
+    private val extraCroppingQuirk = DeviceQuirks[ExtraCroppingQuirk::class.java]
+    /**
+     * Returns a new list of resolution with the selected resolution inserted or prioritized.
+     *
+     *
+     *  If the list contains the selected resolution, move it to be the first element; if it
+     * does not contain the selected resolution, insert it as the first element; if there is no
+     * device quirk, return the original list.
+     *
+     * @param configType           the config type based on which the supported resolution is
+     * calculated.
+     * @param supportedResolutions a ordered list of resolutions calculated by CameraX.
+     */
+    fun insertOrPrioritize(
+        configType: ConfigType,
+        supportedResolutions: List<Size>,
+
+    ): List<Size> {
+        if (extraCroppingQuirk == null) {
+            return supportedResolutions
+        }
+        val selectResolution: Size = extraCroppingQuirk.getVerifiedResolution(configType)
+            ?: return supportedResolutions
+        val newResolutions: MutableList<Size> = mutableListOf()
+        newResolutions.add(selectResolution)
+        for (size in supportedResolutions) {
+            if (size != selectResolution) {
+                newResolutions.add(size)
+            }
+        }
+        return newResolutions
+    }
+}
\ No newline at end of file
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/workaround/TargetAspectRatio.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/workaround/TargetAspectRatio.kt
new file mode 100644
index 0000000..99c6d19
--- /dev/null
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/workaround/TargetAspectRatio.kt
@@ -0,0 +1,83 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.camera2.pipe.integration.compat.workaround
+
+import androidx.annotation.IntDef
+import androidx.annotation.RequiresApi
+import androidx.annotation.RestrictTo
+import androidx.camera.camera2.pipe.CameraMetadata
+import androidx.camera.camera2.pipe.integration.compat.StreamConfigurationMapCompat
+import androidx.camera.camera2.pipe.integration.compat.quirk.AspectRatioLegacyApi21Quirk
+import androidx.camera.camera2.pipe.integration.compat.quirk.CameraQuirks
+import androidx.camera.camera2.pipe.integration.compat.quirk.DeviceQuirks
+import androidx.camera.camera2.pipe.integration.compat.quirk.Nexus4AndroidLTargetAspectRatioQuirk
+
+/**
+ * Workaround to get corrected target aspect ratio.
+ *
+ * @see Nexus4AndroidLTargetAspectRatioQuirk
+ *
+ * @see AspectRatioLegacyApi21Quirk
+ */
+@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
+
+class TargetAspectRatio {
+    /**
+     * Gets corrected target aspect ratio based on device and camera quirks.
+     */
+    @Ratio
+    operator fun get(
+        cameraMetadata: CameraMetadata,
+        streamConfigurationMapCompat: StreamConfigurationMapCompat
+    ): Int {
+        val cameraQuirks = CameraQuirks(
+            cameraMetadata,
+            streamConfigurationMapCompat
+        )
+        val nexus4AndroidLTargetAspectRatioQuirk =
+            DeviceQuirks[Nexus4AndroidLTargetAspectRatioQuirk::class.java]
+        if (nexus4AndroidLTargetAspectRatioQuirk != null) {
+            return nexus4AndroidLTargetAspectRatioQuirk.getCorrectedAspectRatio()
+        }
+
+        val aspectRatioLegacyApi21Quirk =
+            cameraQuirks.quirks[AspectRatioLegacyApi21Quirk::class.java]
+        return aspectRatioLegacyApi21Quirk?.getCorrectedAspectRatio() ?: RATIO_ORIGINAL
+    }
+
+    /**
+     */
+    @IntDef(RATIO_4_3, RATIO_16_9, RATIO_MAX_JPEG, RATIO_ORIGINAL)
+    @Retention(AnnotationRetention.SOURCE)
+    @RestrictTo(
+        RestrictTo.Scope.LIBRARY_GROUP
+    )
+    annotation class Ratio
+    companion object {
+        /** 4:3 standard aspect ratio.  */
+        const val RATIO_4_3 = 0
+
+        /** 16:9 standard aspect ratio.  */
+        const val RATIO_16_9 = 1
+
+        /** The same aspect ratio as the maximum JPEG resolution.  */
+        const val RATIO_MAX_JPEG = 2
+
+        /** No correction is needed.  */
+        const val RATIO_ORIGINAL = 3
+    }
+}
\ No newline at end of file
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/DisplayInfoManager.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/DisplayInfoManager.kt
index c7745f6..c1ea143 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/DisplayInfoManager.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/DisplayInfoManager.kt
@@ -25,6 +25,7 @@
 import android.util.Size
 import android.view.Display
 import androidx.annotation.RequiresApi
+import androidx.camera.camera2.pipe.integration.compat.workaround.MaxPreviewSize
 import javax.inject.Inject
 import javax.inject.Singleton
 
@@ -33,6 +34,7 @@
 @RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
 class DisplayInfoManager @Inject constructor(context: Context) {
     private val MAX_PREVIEW_SIZE = Size(1920, 1080)
+    private val maxPreviewSize: MaxPreviewSize = MaxPreviewSize()
 
     companion object {
         private var lazyMaxDisplay: Display? = null
@@ -69,8 +71,27 @@
     val defaultDisplay: Display
         get() = getMaxSizeDisplay()
 
-    val previewSize: Size
-        get() = calculatePreviewSize()
+    private var previewSize: Size? = null
+
+    /**
+     * Update the preview size according to current display size.
+     */
+    fun refresh() {
+        previewSize = calculatePreviewSize()
+    }
+
+    /**
+     * PREVIEW refers to the best size match to the device's screen resolution, or to 1080p
+     * (1920x1080), whichever is smaller.
+     */
+    fun getPreviewSize(): Size {
+        // Use cached value to speed up since this would be called multiple times.
+        if (previewSize != null) {
+            return previewSize as Size
+        }
+        previewSize = calculatePreviewSize()
+        return previewSize as Size
+    }
 
     private fun getMaxSizeDisplay(): Display {
         lazyMaxDisplay?.let { return it }
@@ -130,7 +151,7 @@
         ) {
             displayViewSize = MAX_PREVIEW_SIZE
         }
-        // TODO(b/230402463): Migrate extra cropping quirk from CameraX.
+        displayViewSize = maxPreviewSize.getMaxPreviewResolution(displayViewSize)
 
         return displayViewSize.also { lazyPreviewSize = displayViewSize }
     }
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/MeteringRepeating.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/MeteringRepeating.kt
index 9f2844a..00d5f2c4 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/MeteringRepeating.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/MeteringRepeating.kt
@@ -166,7 +166,7 @@
 
         // Find maximum supported resolution that is <= min(VGA, display resolution)
         // Using minimum supported size could cause some issue on certain devices.
-        val previewSize = displayInfoManager.previewSize
+        val previewSize = displayInfoManager.getPreviewSize()
         val maxSizeProduct =
             min(640L * 480L, previewSize.width.toLong() * previewSize.height.toLong())
 
diff --git a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/adapter/FocusMeteringControlTest.kt b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/adapter/FocusMeteringControlTest.kt
index 77f4fe0..9898814 100644
--- a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/adapter/FocusMeteringControlTest.kt
+++ b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/adapter/FocusMeteringControlTest.kt
@@ -1272,11 +1272,13 @@
         // camera 5 supports 1 AF and 0 AE/AWB regions
         focusMeteringControl = initFocusMeteringControl(cameraId = CAMERA_ID_5)
 
-        startFocusMeteringAndAwait(FocusMeteringAction.Builder(
-            point1,
-            FocusMeteringAction.FLAG_AF or FocusMeteringAction.FLAG_AE or
-                FocusMeteringAction.FLAG_AWB
-        ).build())
+        startFocusMeteringAndAwait(
+            FocusMeteringAction.Builder(
+                point1,
+                FocusMeteringAction.FLAG_AF or FocusMeteringAction.FLAG_AE or
+                    FocusMeteringAction.FLAG_AWB
+            ).build()
+        )
 
         with(fakeRequestControl.focusMeteringCalls.last()) {
             assertWithMessage("Wrong number of AE regions").that(aeRegions).isNull()
@@ -1398,7 +1400,10 @@
                 cameraPropertiesMap[cameraId]!!.metadata,
                 StreamConfigurationMapCompat(
                     StreamConfigurationMapBuilder.newBuilder().build(),
-                    OutputSizesCorrector(cameraPropertiesMap[cameraId]!!.metadata)
+                    OutputSizesCorrector(
+                        cameraPropertiesMap[cameraId]!!.metadata,
+                        StreamConfigurationMapBuilder.newBuilder().build()
+                    ),
                 )
             )
         ),
diff --git a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/adapter/SupportedSurfaceCombinationTest.kt b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/adapter/SupportedSurfaceCombinationTest.kt
index 2f82f0a..5074974 100644
--- a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/adapter/SupportedSurfaceCombinationTest.kt
+++ b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/adapter/SupportedSurfaceCombinationTest.kt
@@ -180,7 +180,8 @@
         for (combination in combinationList) {
             val isSupported =
                 supportedSurfaceCombination.checkSupported(
-                    false, combination.surfaceConfigList)
+                    false, combination.surfaceConfigList
+                )
             assertThat(isSupported).isTrue()
         }
     }
@@ -194,7 +195,8 @@
         )
         val combinationList = getLegacySupportedCombinationList()
         val isSupported = isAllSubConfigListSupported(
-            false, supportedSurfaceCombination, combinationList)
+            false, supportedSurfaceCombination, combinationList
+        )
         assertThat(isSupported).isTrue()
     }
 
@@ -209,7 +211,8 @@
         for (combination in combinationList) {
             val isSupported =
                 supportedSurfaceCombination.checkSupported(
-                    false, combination.surfaceConfigList)
+                    false, combination.surfaceConfigList
+                )
             assertThat(isSupported).isFalse()
         }
     }
@@ -225,7 +228,8 @@
         for (combination in combinationList) {
             val isSupported =
                 supportedSurfaceCombination.checkSupported(
-                    false, combination.surfaceConfigList)
+                    false, combination.surfaceConfigList
+                )
             assertThat(isSupported).isFalse()
         }
     }
@@ -241,7 +245,8 @@
         for (combination in combinationList) {
             val isSupported =
                 supportedSurfaceCombination.checkSupported(
-                    false, combination.surfaceConfigList)
+                    false, combination.surfaceConfigList
+                )
             assertThat(isSupported).isFalse()
         }
     }
@@ -257,7 +262,8 @@
         for (combination in combinationList) {
             val isSupported =
                 supportedSurfaceCombination.checkSupported(
-                    false, combination.surfaceConfigList)
+                    false, combination.surfaceConfigList
+                )
             assertThat(isSupported).isTrue()
         }
     }
@@ -271,7 +277,8 @@
         )
         val combinationList = getLimitedSupportedCombinationList()
         val isSupported = isAllSubConfigListSupported(
-            false, supportedSurfaceCombination, combinationList)
+            false, supportedSurfaceCombination, combinationList
+        )
         assertThat(isSupported).isTrue()
     }
 
@@ -286,7 +293,8 @@
         for (combination in combinationList) {
             val isSupported =
                 supportedSurfaceCombination.checkSupported(
-                    false, combination.surfaceConfigList)
+                    false, combination.surfaceConfigList
+                )
             assertThat(isSupported).isFalse()
         }
     }
@@ -302,7 +310,8 @@
         for (combination in combinationList) {
             val isSupported =
                 supportedSurfaceCombination.checkSupported(
-                    false, combination.surfaceConfigList)
+                    false, combination.surfaceConfigList
+                )
             assertThat(isSupported).isFalse()
         }
     }
@@ -318,7 +327,8 @@
         for (combination in combinationList) {
             val isSupported =
                 supportedSurfaceCombination.checkSupported(
-                    false, combination.surfaceConfigList)
+                    false, combination.surfaceConfigList
+                )
             assertThat(isSupported).isTrue()
         }
     }
@@ -332,7 +342,8 @@
         )
         val combinationList = getFullSupportedCombinationList()
         val isSupported = isAllSubConfigListSupported(
-            false, supportedSurfaceCombination, combinationList)
+            false, supportedSurfaceCombination, combinationList
+        )
         assertThat(isSupported).isTrue()
     }
 
@@ -347,7 +358,8 @@
         for (combination in combinationList) {
             val isSupported =
                 supportedSurfaceCombination.checkSupported(
-                    false, combination.surfaceConfigList)
+                    false, combination.surfaceConfigList
+                )
             assertThat(isSupported).isFalse()
         }
     }
@@ -367,7 +379,8 @@
         for (combination in combinationList) {
             val isSupported =
                 supportedSurfaceCombination.checkSupported(
-                    false, combination.surfaceConfigList)
+                    false, combination.surfaceConfigList
+                )
             assertThat(isSupported).isTrue()
         }
     }
@@ -387,7 +400,8 @@
         for (combination in combinationList) {
             val isSupported =
                 supportedSurfaceCombination.checkSupported(
-                    false, combination.surfaceConfigList)
+                    false, combination.surfaceConfigList
+                )
             assertThat(isSupported).isTrue()
         }
     }
@@ -407,7 +421,8 @@
         for (combination in combinationList) {
             val isSupported =
                 supportedSurfaceCombination.checkSupported(
-                    false, combination.surfaceConfigList)
+                    false, combination.surfaceConfigList
+                )
             assertThat(isSupported).isTrue()
         }
     }
@@ -427,7 +442,8 @@
         for (combination in combinationList) {
             val isSupported =
                 supportedSurfaceCombination.checkSupported(
-                    false, combination.surfaceConfigList)
+                    false, combination.surfaceConfigList
+                )
             assertThat(isSupported).isTrue()
         }
     }
@@ -443,7 +459,8 @@
         for (combination in combinationList) {
             val isSupported =
                 supportedSurfaceCombination.checkSupported(
-                    false, combination.surfaceConfigList)
+                    false, combination.surfaceConfigList
+                )
             assertThat(isSupported).isTrue()
         }
     }
@@ -457,14 +474,16 @@
         )
         val combinationList = getLevel3SupportedCombinationList()
         val isSupported = isAllSubConfigListSupported(
-            false, supportedSurfaceCombination, combinationList)
+            false, supportedSurfaceCombination, combinationList
+        )
         assertThat(isSupported).isTrue()
     }
 
     @Test
     fun checkConcurrentSurfaceCombinationSupportedInConcurrentCameraMode() {
         Shadows.shadowOf(context.packageManager).setSystemFeature(
-            PackageManager.FEATURE_CAMERA_CONCURRENT, true)
+            PackageManager.FEATURE_CAMERA_CONCURRENT, true
+        )
         setupCamera(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_3)
         val supportedSurfaceCombination = SupportedSurfaceCombination(
             context, fakeCameraMetadata,
@@ -474,7 +493,8 @@
         for (combination in combinationList) {
             val isSupported =
                 supportedSurfaceCombination.checkSupported(
-                    true, combination.surfaceConfigList)
+                    true, combination.surfaceConfigList
+                )
             assertThat(isSupported).isTrue()
         }
     }
@@ -482,7 +502,8 @@
     @Test
     fun checkConcurrentSurfaceCombinationSubListSupportedInConcurrentCameraMode() {
         Shadows.shadowOf(context.packageManager).setSystemFeature(
-            PackageManager.FEATURE_CAMERA_CONCURRENT, true)
+            PackageManager.FEATURE_CAMERA_CONCURRENT, true
+        )
         setupCamera(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_3)
         val supportedSurfaceCombination = SupportedSurfaceCombination(
             context, fakeCameraMetadata,
@@ -490,7 +511,8 @@
         )
         val combinationList = getConcurrentSupportedCombinationList()
         val isSupported = isAllSubConfigListSupported(
-            true, supportedSurfaceCombination, combinationList)
+            true, supportedSurfaceCombination, combinationList
+        )
         assertThat(isSupported).isTrue()
     }
 
@@ -1666,7 +1688,8 @@
                 val subConfigurationList: MutableList<SurfaceConfig> = ArrayList(configList)
                 subConfigurationList.removeAt(index)
                 val isSupported = supportedSurfaceCombination.checkSupported(
-                    isConcurrentCameraModeOn, subConfigurationList)
+                    isConcurrentCameraModeOn, subConfigurationList
+                )
                 if (!isSupported) {
                     return false
                 }
@@ -1679,11 +1702,13 @@
         captureType: UseCaseConfigFactory.CaptureType,
         targetFrameRate: Range<Int>? = null
     ): UseCase {
-        val builder = FakeUseCaseConfig.Builder(captureType, when (captureType) {
-            UseCaseConfigFactory.CaptureType.IMAGE_CAPTURE -> ImageFormat.JPEG
-            UseCaseConfigFactory.CaptureType.IMAGE_ANALYSIS -> ImageFormat.YUV_420_888
-            else -> ImageFormatConstants.INTERNAL_DEFINED_IMAGE_FORMAT_PRIVATE
-        })
+        val builder = FakeUseCaseConfig.Builder(
+            captureType, when (captureType) {
+                UseCaseConfigFactory.CaptureType.IMAGE_CAPTURE -> ImageFormat.JPEG
+                UseCaseConfigFactory.CaptureType.IMAGE_ANALYSIS -> ImageFormat.YUV_420_888
+                else -> ImageFormatConstants.INTERNAL_DEFINED_IMAGE_FORMAT_PRIVATE
+            }
+        )
         targetFrameRate?.let {
             builder.mutableConfig.insertOption(UseCaseConfig.OPTION_TARGET_FRAME_RATE, it)
         }
diff --git a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/compat/OutputSizesCorrectorTest.kt b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/compat/OutputSizesCorrectorTest.kt
index b81db73..63199bb 100644
--- a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/compat/OutputSizesCorrectorTest.kt
+++ b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/compat/OutputSizesCorrectorTest.kt
@@ -40,6 +40,8 @@
 private const val MOTOROLA_E5_PLAY_MODEL_NAME = "moto e5 play"
 private const val SAMSUNG_BRAND_NAME = "SAMSUNG"
 private const val SAMSUNG_J7_DEVICE_NAME = "J7XELTE"
+private const val FAKE_BRAND_NAME = "Fake-Brand"
+private const val FAKE_DEVICE_NAME = "Fake-Device"
 
 private val outputSizes = arrayOf(
     // Samsung J7 API 27 above excluded sizes
@@ -206,6 +208,98 @@
         ).inOrder()
     }
 
+    @Test
+    fun canExcludeApi21LegacyLevelProblematicSizesByFormat() {
+        val outputSizesCorrector = createOutputSizesCorrector(
+            FAKE_BRAND_NAME,
+            FAKE_DEVICE_NAME,
+            null,
+            CAMERA_ID_0,
+            CameraCharacteristics.LENS_FACING_BACK,
+            CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY
+        )
+
+        val resultList = outputSizesCorrector.applyQuirks(
+            outputSizes,
+            ImageFormat.YUV_420_888
+        )!!.toList()
+
+        val expectedList = if (Build.VERSION.SDK_INT == 21) {
+            // non-4:3 sizes are removed
+            listOf(
+                Size(4128, 3096),
+                Size(3264, 2448),
+                Size(2048, 1536),
+                Size(1280, 960),
+                Size(640, 480),
+                Size(320, 240),
+            )
+        } else {
+            listOf(
+                Size(4128, 3096),
+                Size(4128, 2322),
+                Size(3088, 3088),
+                Size(3264, 2448),
+                Size(3264, 1836),
+                Size(2048, 1536),
+                Size(2048, 1152),
+                Size(1920, 1080),
+                Size(1280, 960),
+                Size(1280, 720),
+                Size(640, 480),
+                Size(320, 240),
+            )
+        }
+
+        Truth.assertThat(resultList).containsExactlyElementsIn(expectedList).inOrder()
+    }
+
+    @Test
+    fun canExcludeApi21LegacyLevelProblematicSizesByClass() {
+        val outputSizesCorrector = createOutputSizesCorrector(
+            FAKE_BRAND_NAME,
+            FAKE_DEVICE_NAME,
+            null,
+            CAMERA_ID_0,
+            CameraCharacteristics.LENS_FACING_BACK,
+            CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY
+        )
+
+        val resultList = outputSizesCorrector.applyQuirks(
+            outputSizes,
+            SurfaceTexture::class.java
+        )!!.toList()
+
+        val expectedList = if (Build.VERSION.SDK_INT == 21) {
+            // non-4:3 sizes are removed
+            listOf(
+                Size(4128, 3096),
+                Size(3264, 2448),
+                Size(2048, 1536),
+                Size(1280, 960),
+                Size(640, 480),
+                Size(320, 240),
+            )
+        } else {
+            listOf(
+                Size(4128, 3096),
+                Size(4128, 2322),
+                Size(3088, 3088),
+                Size(3264, 2448),
+                Size(3264, 1836),
+                Size(2048, 1536),
+                Size(2048, 1152),
+                Size(1920, 1080),
+                Size(1280, 960),
+                Size(1280, 720),
+                Size(640, 480),
+                Size(320, 240),
+            )
+        }
+
+        Truth.assertThat(resultList).containsExactlyElementsIn(expectedList).inOrder()
+    }
+
     private fun createOutputSizesCorrector(
         brand: String,
         device: String?,
@@ -222,20 +316,22 @@
             ReflectionHelpers.setStaticField(Build::class.java, "MODEL", it)
         }
 
+        val map = StreamConfigurationMapBuilder.newBuilder()
+            .apply {
+                outputSizes.forEach { outputSize ->
+                    addOutputSize(outputSize)
+                }
+            }.build()
+
         return OutputSizesCorrector(
             FakeCameraMetadata(
                 cameraId = CameraId(cameraId), characteristics = mapOf(
                     CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL to hardwareLevel,
                     CameraCharacteristics.LENS_FACING to lensFacing,
-                    CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP to
-                        StreamConfigurationMapBuilder.newBuilder()
-                            .apply {
-                                outputSizes.forEach { outputSize ->
-                                    addOutputSize(outputSize)
-                                }
-                            }.build()
+                    CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP to map
                 )
-            )
+            ),
+            map
         )
     }
 }
\ No newline at end of file
diff --git a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/compat/StreamConfigurationMapCompatTest.kt b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/compat/StreamConfigurationMapCompatTest.kt
index f972ee6..ef595571 100644
--- a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/compat/StreamConfigurationMapCompatTest.kt
+++ b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/compat/StreamConfigurationMapCompatTest.kt
@@ -60,7 +60,7 @@
         streamConfigurationMapCompat =
             StreamConfigurationMapCompat(
                 builder.build(),
-            OutputSizesCorrector(FakeCameraMetadata())
+                OutputSizesCorrector(FakeCameraMetadata(), builder.build())
             )
     }
 
diff --git a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/compat/quirk/AfRegionFlipHorizontallyQuirkTest.kt b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/compat/quirk/AfRegionFlipHorizontallyQuirkTest.kt
index 5f4c468..9e3a6be 100644
--- a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/compat/quirk/AfRegionFlipHorizontallyQuirkTest.kt
+++ b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/compat/quirk/AfRegionFlipHorizontallyQuirkTest.kt
@@ -73,7 +73,10 @@
             cameraMetadata,
             StreamConfigurationMapCompat(
                 StreamConfigurationMapBuilder.newBuilder().build(),
-                OutputSizesCorrector(cameraMetadata)
+                OutputSizesCorrector(
+                    cameraMetadata,
+                    StreamConfigurationMapBuilder.newBuilder().build()
+                )
             )
         ).quirks
     }
diff --git a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/compat/quirk/CamcorderProfileResolutionQuirkTest.kt b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/compat/quirk/CamcorderProfileResolutionQuirkTest.kt
index 7b2f9e2..5f0a226 100644
--- a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/compat/quirk/CamcorderProfileResolutionQuirkTest.kt
+++ b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/compat/quirk/CamcorderProfileResolutionQuirkTest.kt
@@ -74,7 +74,10 @@
         val quirk = CamcorderProfileResolutionQuirk(
             StreamConfigurationMapCompat(
                 cameraMetadata[CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP]!!,
-                OutputSizesCorrector(cameraMetadata)
+                OutputSizesCorrector(
+                    cameraMetadata,
+                    cameraMetadata[CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP]!!
+                )
             )
         )
 
diff --git a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/compat/quirk/ExtraCroppingQuirkTest.kt b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/compat/quirk/ExtraCroppingQuirkTest.kt
new file mode 100644
index 0000000..42b703a
--- /dev/null
+++ b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/compat/quirk/ExtraCroppingQuirkTest.kt
@@ -0,0 +1,97 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.camera2.pipe.integration.compat.quirk
+
+import android.os.Build
+import android.util.Size
+import androidx.camera.core.impl.SurfaceConfig
+import com.google.common.truth.Truth
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.RobolectricTestRunner
+import org.robolectric.annotation.Config
+import org.robolectric.annotation.internal.DoNotInstrument
+import org.robolectric.util.ReflectionHelpers
+
+@RunWith(RobolectricTestRunner::class)
+@DoNotInstrument
+@Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
+class ExtraCroppingQuirkTest {
+    private val quirk = ExtraCroppingQuirk()
+
+    @Test
+    fun deviceModelNotContained_returnsNull() {
+        ReflectionHelpers.setStaticField(Build::class.java, "MODEL", "non-existent")
+        assert(!ExtraCroppingQuirk.load())
+        Truth.assertThat(quirk.getVerifiedResolution(SurfaceConfig.ConfigType.PRIV)).isNull()
+    }
+
+    @Test
+    fun deviceBrandNotSamsung_returnsNull() {
+        ReflectionHelpers.setStaticField(Build::class.java, "BRAND", "non-existent")
+        assert(!ExtraCroppingQuirk.load())
+        Truth.assertThat(quirk.getVerifiedResolution(SurfaceConfig.ConfigType.PRIV)).isNull()
+    }
+
+    @Test
+    fun osVersionNotInRange_returnsNull() {
+        ReflectionHelpers.setStaticField(Build::class.java, "BRAND", "samsung")
+        ReflectionHelpers.setStaticField(Build::class.java, "MODEL", "SM-J710MN")
+        ReflectionHelpers.setStaticField(Build.VERSION::class.java, "SDK_INT", 20)
+        assert(!ExtraCroppingQuirk.load())
+        Truth.assertThat(quirk.getVerifiedResolution(SurfaceConfig.ConfigType.PRIV)).isNull()
+    }
+
+    @Test
+    fun rawConfigType_returnsNull() {
+        ReflectionHelpers.setStaticField(Build::class.java, "BRAND", "samsung")
+        ReflectionHelpers.setStaticField(Build::class.java, "MODEL", "SM-J710MN")
+        ReflectionHelpers.setStaticField(Build.VERSION::class.java, "SDK_INT", 22)
+        assert(ExtraCroppingQuirk.load())
+        Truth.assertThat(quirk.getVerifiedResolution(SurfaceConfig.ConfigType.RAW)).isNull()
+    }
+
+    @Test
+    fun privConfigType_returnsSize() {
+        ReflectionHelpers.setStaticField(Build::class.java, "BRAND", "samsung")
+        ReflectionHelpers.setStaticField(Build::class.java, "MODEL", "SM-J710MN")
+        ReflectionHelpers.setStaticField(Build.VERSION::class.java, "SDK_INT", 22)
+        assert(ExtraCroppingQuirk.load())
+        Truth.assertThat(quirk.getVerifiedResolution(SurfaceConfig.ConfigType.PRIV))
+            .isEqualTo(Size(1920, 1080))
+    }
+
+    @Test
+    fun yuvConfigType_returnsSize() {
+        ReflectionHelpers.setStaticField(Build::class.java, "BRAND", "samsung")
+        ReflectionHelpers.setStaticField(Build::class.java, "MODEL", "SM-J710MN")
+        ReflectionHelpers.setStaticField(Build.VERSION::class.java, "SDK_INT", 22)
+        assert(ExtraCroppingQuirk.load())
+        Truth.assertThat(quirk.getVerifiedResolution(SurfaceConfig.ConfigType.YUV))
+            .isEqualTo(Size(1280, 720))
+    }
+
+    @Test
+    fun jpegConfigType_returnsSize() {
+        ReflectionHelpers.setStaticField(Build::class.java, "BRAND", "samsung")
+        ReflectionHelpers.setStaticField(Build::class.java, "MODEL", "SM-J710MN")
+        ReflectionHelpers.setStaticField(Build.VERSION::class.java, "SDK_INT", 22)
+        assert(ExtraCroppingQuirk.load())
+        Truth.assertThat(quirk.getVerifiedResolution(SurfaceConfig.ConfigType.JPEG))
+            .isEqualTo(Size(3264, 1836))
+    }
+}
\ No newline at end of file
diff --git a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/compat/quirk/ExtraSupportedSurfaceCombinationsQuirkTest.kt b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/compat/quirk/ExtraSupportedSurfaceCombinationsQuirkTest.kt
new file mode 100644
index 0000000..88b54d5
--- /dev/null
+++ b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/compat/quirk/ExtraSupportedSurfaceCombinationsQuirkTest.kt
@@ -0,0 +1,314 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.camera2.pipe.integration.compat.quirk
+
+import android.hardware.camera2.CameraCharacteristics
+import android.os.Build
+import androidx.camera.core.impl.SurfaceCombination
+import androidx.camera.core.impl.SurfaceConfig
+import com.google.common.truth.Truth
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.ParameterizedRobolectricTestRunner
+import org.robolectric.annotation.Config
+import org.robolectric.annotation.internal.DoNotInstrument
+import org.robolectric.util.ReflectionHelpers
+
+@RunWith(ParameterizedRobolectricTestRunner::class)
+@DoNotInstrument
+@Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
+class ExtraSupportedSurfaceCombinationsQuirkTest(private val config: Config) {
+
+    @Test
+    fun checkExtraSupportedSurfaceCombinations() {
+        // Set up brand properties
+        if (config.brand != null) {
+            ReflectionHelpers.setStaticField(Build::class.java, "BRAND", config.brand)
+        }
+
+        // Set up device properties
+        if (config.device != null) {
+            ReflectionHelpers.setStaticField(Build::class.java, "DEVICE", config.device)
+        }
+
+        // Set up model properties
+        if (config.model != null) {
+            ReflectionHelpers.setStaticField(Build::class.java, "MODEL", config.model)
+        }
+
+        // Initializes ExtraSupportedSurfaceCombinationsContainer instance with camera id
+        val quirk =
+            ExtraSupportedSurfaceCombinationsQuirk()
+
+        // Gets the extra supported surface combinations on the device
+        val extraSurfaceCombinations: List<SurfaceCombination> =
+            quirk.getExtraSupportedSurfaceCombinations(
+                config.cameraId,
+                config.hardwareLevel
+            )
+        for (expectedSupportedSurfaceCombination in config.expectedSupportedSurfaceCombinations) {
+            var isSupported = false
+
+            // Checks the combination is supported by the list retrieved from the
+            // ExtraSupportedSurfaceCombinationsContainer.
+            for (extraSurfaceCombination in extraSurfaceCombinations) {
+                if (extraSurfaceCombination.isSupported(
+                        expectedSupportedSurfaceCombination.surfaceConfigList
+                    )
+                ) {
+                    isSupported = true
+                    break
+                }
+            }
+            Truth.assertThat(isSupported).isTrue()
+        }
+    }
+
+    companion object {
+        @JvmStatic
+        @ParameterizedRobolectricTestRunner.Parameters(name = "{0}")
+        fun data() = listOf(
+            Config(
+                null,
+                "heroqltevzw",
+                null,
+                "0",
+                CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_FULL
+            ),
+            Config(
+                null,
+                "heroqltevzw",
+                null,
+                "1",
+                CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED,
+                createFullLevelYPYSupportedCombinations()
+            ),
+            // Tests for Samsung S7 case
+            Config(
+                null,
+                "heroqltetmo",
+                null,
+                "0",
+                CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_FULL
+            ),
+            Config(
+                null,
+                "heroqltetmo",
+                null, "1",
+                CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED,
+                createFullLevelYPYSupportedCombinations()
+            ),
+            // Tests for Samsung limited device case
+            Config(
+                "samsung",
+                null,
+                "sm-g9860",
+                "0",
+                CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_FULL
+            ),
+            Config(
+                "samsung",
+                null,
+                "sm-g9860",
+                "1",
+                CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED,
+                createFullLevelYPYAndYYYSupportedCombinations()
+            ),
+            // Tests for FULL Pixel devices
+            Config(
+                "Google",
+                null,
+                "Pixel 6",
+                "0",
+                CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_FULL,
+                createLevel3PrivPrivYuvRawConfiguration()
+            ),
+            Config(
+                "Google",
+                null,
+                "Pixel 6",
+                "1",
+                CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_FULL,
+                createLevel3PrivPrivYuvRawConfiguration()
+            ),
+            Config(
+                "Google",
+                null,
+                "Pixel 6 Pro",
+                "0",
+                CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_FULL,
+                createLevel3PrivPrivYuvRawConfiguration()
+            ),
+            Config(
+                "Google",
+                null,
+                "Pixel 6 Pro",
+                "1",
+                CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_FULL,
+                createLevel3PrivPrivYuvRawConfiguration()
+            ),
+            Config(
+                "Google",
+                null,
+                "Pixel 7",
+                "0",
+                CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_FULL,
+                createLevel3PrivPrivYuvRawConfiguration()
+            ),
+            Config(
+                "Google",
+                null,
+                "Pixel 6 Pro",
+                "1",
+                CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_FULL,
+                createLevel3PrivPrivYuvRawConfiguration()
+            ),
+            Config(
+                "Google",
+                null,
+                "Pixel 7 Pro",
+                "0",
+                CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_FULL,
+                createLevel3PrivPrivYuvRawConfiguration()
+            ),
+            Config(
+                "Google",
+                null,
+                "Pixel 7 Pro",
+                "1",
+                CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_FULL,
+                createLevel3PrivPrivYuvRawConfiguration()
+            ),
+            // Other cases
+            Config(
+                null,
+                null,
+                null,
+                "0",
+                CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED
+            )
+        )
+
+        private fun createFullLevelYPYSupportedCombinations(): Array<SurfaceCombination> {
+            // (YUV, ANALYSIS) + (PRIV, PREVIEW) + (YUV, MAXIMUM)
+            val surfaceCombination = SurfaceCombination()
+            surfaceCombination.addSurfaceConfig(
+                SurfaceConfig.create(
+                    SurfaceConfig.ConfigType.YUV,
+                    SurfaceConfig.ConfigSize.VGA
+                )
+            )
+            surfaceCombination.addSurfaceConfig(
+                SurfaceConfig.create(
+                    SurfaceConfig.ConfigType.PRIV,
+                    SurfaceConfig.ConfigSize.PREVIEW
+                )
+            )
+            surfaceCombination.addSurfaceConfig(
+                SurfaceConfig.create(
+                    SurfaceConfig.ConfigType.YUV,
+                    SurfaceConfig.ConfigSize.MAXIMUM
+                )
+            )
+            return arrayOf(surfaceCombination)
+        }
+
+        private fun createFullLevelYPYAndYYYSupportedCombinations(): Array<SurfaceCombination> {
+            // (YUV, ANALYSIS) + (PRIV, PREVIEW) + (YUV, MAXIMUM)
+            val surfaceCombination1 = SurfaceCombination()
+            surfaceCombination1.addSurfaceConfig(
+                SurfaceConfig.create(
+                    SurfaceConfig.ConfigType.YUV,
+                    SurfaceConfig.ConfigSize.VGA
+                )
+            )
+            surfaceCombination1.addSurfaceConfig(
+                SurfaceConfig.create(
+                    SurfaceConfig.ConfigType.PRIV,
+                    SurfaceConfig.ConfigSize.PREVIEW
+                )
+            )
+            surfaceCombination1.addSurfaceConfig(
+                SurfaceConfig.create(
+                    SurfaceConfig.ConfigType.YUV,
+                    SurfaceConfig.ConfigSize.MAXIMUM
+                )
+            )
+
+            // (YUV, ANALYSIS) + (YUV, PREVIEW) + (YUV, MAXIMUM)
+            val surfaceCombination2 = SurfaceCombination()
+            surfaceCombination2.addSurfaceConfig(
+                SurfaceConfig.create(
+                    SurfaceConfig.ConfigType.YUV,
+                    SurfaceConfig.ConfigSize.VGA
+                )
+            )
+            surfaceCombination2.addSurfaceConfig(
+                SurfaceConfig.create(
+                    SurfaceConfig.ConfigType.YUV,
+                    SurfaceConfig.ConfigSize.PREVIEW
+                )
+            )
+            surfaceCombination2.addSurfaceConfig(
+                SurfaceConfig.create(
+                    SurfaceConfig.ConfigType.YUV,
+                    SurfaceConfig.ConfigSize.MAXIMUM
+                )
+            )
+            return arrayOf(surfaceCombination1, surfaceCombination2)
+        }
+
+        private fun createLevel3PrivPrivYuvRawConfiguration(): Array<SurfaceCombination> {
+            // (PRIV, PREVIEW) + (PRIV, ANALYSIS) + (YUV, MAXIMUM) + (RAW, MAXIMUM)
+            val surfaceCombination = SurfaceCombination()
+            surfaceCombination.addSurfaceConfig(
+                SurfaceConfig.create(
+                    SurfaceConfig.ConfigType.PRIV,
+                    SurfaceConfig.ConfigSize.PREVIEW
+                )
+            )
+            surfaceCombination.addSurfaceConfig(
+                SurfaceConfig.create(
+                    SurfaceConfig.ConfigType.PRIV,
+                    SurfaceConfig.ConfigSize.VGA
+                )
+            )
+            surfaceCombination.addSurfaceConfig(
+                SurfaceConfig.create(
+                    SurfaceConfig.ConfigType.YUV,
+                    SurfaceConfig.ConfigSize.MAXIMUM
+                )
+            )
+            surfaceCombination.addSurfaceConfig(
+                SurfaceConfig.create(
+                    SurfaceConfig.ConfigType.RAW,
+                    SurfaceConfig.ConfigSize.MAXIMUM
+                )
+            )
+            return arrayOf(surfaceCombination)
+        }
+    }
+
+    class Config(
+        val brand: String?,
+        val device: String?,
+        val model: String?,
+        val cameraId: String,
+        val hardwareLevel: Int,
+        val expectedSupportedSurfaceCombinations: Array<SurfaceCombination> = arrayOf()
+    )
+}
\ No newline at end of file
diff --git a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/compat/quirk/JpegHalCorruptImageQuirkTest.kt b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/compat/quirk/JpegHalCorruptImageQuirkTest.kt
index bc371e4..edd60eb 100644
--- a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/compat/quirk/JpegHalCorruptImageQuirkTest.kt
+++ b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/compat/quirk/JpegHalCorruptImageQuirkTest.kt
@@ -55,7 +55,10 @@
             FakeCameraMetadata(),
             StreamConfigurationMapCompat(
                 StreamConfigurationMapBuilder.newBuilder().build(),
-                OutputSizesCorrector(FakeCameraMetadata())
+                OutputSizesCorrector(
+                    FakeCameraMetadata(),
+                    StreamConfigurationMapBuilder.newBuilder().build()
+                )
             )
         ).quirks
 
diff --git a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/compat/quirk/YuvImageOnePixelShiftQuirkTest.kt b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/compat/quirk/YuvImageOnePixelShiftQuirkTest.kt
index 43d8e7f..c7b452a 100644
--- a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/compat/quirk/YuvImageOnePixelShiftQuirkTest.kt
+++ b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/compat/quirk/YuvImageOnePixelShiftQuirkTest.kt
@@ -46,7 +46,10 @@
             FakeCameraMetadata(),
             StreamConfigurationMapCompat(
                 StreamConfigurationMapBuilder.newBuilder().build(),
-                OutputSizesCorrector(FakeCameraMetadata())
+                OutputSizesCorrector(
+                    FakeCameraMetadata(),
+                    StreamConfigurationMapBuilder.newBuilder().build()
+                )
             )
         ).quirks
 
diff --git a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/compat/workaround/AutoFlashAEModeDisablerTest.kt b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/compat/workaround/AutoFlashAEModeDisablerTest.kt
index 04025fc..f7ad78c 100644
--- a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/compat/workaround/AutoFlashAEModeDisablerTest.kt
+++ b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/compat/workaround/AutoFlashAEModeDisablerTest.kt
@@ -113,7 +113,10 @@
             CameraQuirks(
                 metadata, StreamConfigurationMapCompat(
                     StreamConfigurationMapBuilder.newBuilder().build(),
-                    OutputSizesCorrector(FakeCameraMetadata())
+                    OutputSizesCorrector(
+                        FakeCameraMetadata(),
+                        StreamConfigurationMapBuilder.newBuilder().build()
+                    )
                 )
             )
         )
diff --git a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/compat/workaround/ResolutionCorrectorTest.kt b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/compat/workaround/ResolutionCorrectorTest.kt
new file mode 100644
index 0000000..ca3adf66
--- /dev/null
+++ b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/compat/workaround/ResolutionCorrectorTest.kt
@@ -0,0 +1,121 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.camera2.pipe.integration.compat.workaround
+
+import android.os.Build
+import android.util.Size
+import androidx.camera.core.impl.SurfaceConfig
+import com.google.common.truth.Truth
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.RobolectricTestRunner
+import org.robolectric.annotation.Config
+import org.robolectric.annotation.internal.DoNotInstrument
+import org.robolectric.util.ReflectionHelpers
+
+private val RESOLUTION_1 = Size(101, 100)
+private val RESOLUTION_2 = Size(102, 100)
+
+private val SELECT_RESOLUTION_PRIV = Size(1920, 1080)
+private val SELECT_RESOLUTION_YUV = Size(1280, 720)
+private val SELECT_RESOLUTION_JPEG = Size(3264, 1836)
+
+private val SUPPORTED_RESOLUTIONS = listOf(RESOLUTION_1, RESOLUTION_2)
+
+/**
+ * Unit test for [ResolutionCorrector].
+ */
+@RunWith(RobolectricTestRunner::class)
+@DoNotInstrument
+@Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
+class ResolutionCorrectorTest {
+
+    private var mResolutionCorrector: ResolutionCorrector? = null
+
+    @Before
+    fun setup() {
+        ReflectionHelpers.setStaticField(Build::class.java, "BRAND", "samsung")
+        ReflectionHelpers.setStaticField(Build::class.java, "MODEL", "SM-J710MN")
+        ReflectionHelpers.setStaticField(Build.VERSION::class.java, "SDK_INT", 22)
+        mResolutionCorrector = ResolutionCorrector()
+    }
+
+    @Test
+    fun hasPrivResolution_prioritized() {
+        hasResolution_prioritized(SurfaceConfig.ConfigType.PRIV, SELECT_RESOLUTION_PRIV)
+    }
+
+    @Test
+    fun hasYuvResolution_prioritized() {
+        hasResolution_prioritized(SurfaceConfig.ConfigType.YUV, SELECT_RESOLUTION_YUV)
+    }
+
+    @Test
+    fun hasJpegResolution_prioritized() {
+        hasResolution_prioritized(SurfaceConfig.ConfigType.JPEG, SELECT_RESOLUTION_JPEG)
+    }
+
+    private fun hasResolution_prioritized(
+        configType: SurfaceConfig.ConfigType,
+        resolution: Size
+    ) {
+        val resolutions: MutableList<Size> = ArrayList<Size>(SUPPORTED_RESOLUTIONS)
+        resolutions.add(resolution)
+        Truth.assertThat(mResolutionCorrector!!.insertOrPrioritize(configType, resolutions))
+            .containsExactly(resolution, RESOLUTION_1, RESOLUTION_2).inOrder()
+    }
+
+    @Test
+    fun noPrivResolution_inserted() {
+        noResolution_inserted(SurfaceConfig.ConfigType.PRIV, SELECT_RESOLUTION_PRIV)
+    }
+
+    @Test
+    fun noYuvResolution_inserted() {
+        noResolution_inserted(SurfaceConfig.ConfigType.YUV, SELECT_RESOLUTION_YUV)
+    }
+
+    @Test
+    fun noJpegResolution_inserted() {
+        noResolution_inserted(SurfaceConfig.ConfigType.JPEG, SELECT_RESOLUTION_JPEG)
+    }
+
+    private fun noResolution_inserted(
+        configType: SurfaceConfig.ConfigType,
+        resolution: Size
+    ) {
+        Truth.assertThat(
+            mResolutionCorrector!!.insertOrPrioritize(
+                configType,
+                SUPPORTED_RESOLUTIONS
+            )
+        )
+            .containsExactly(resolution, RESOLUTION_1, RESOLUTION_2).inOrder()
+    }
+
+    @Test
+    fun noQuirk_returnsOriginalSupportedResolutions() {
+        ReflectionHelpers.setStaticField(Build::class.java, "BRAND", "notsamsung")
+        val resolutionCorrector = ResolutionCorrector()
+        val result = resolutionCorrector.insertOrPrioritize(
+            SurfaceConfig.ConfigType.PRIV,
+            SUPPORTED_RESOLUTIONS
+        )
+        Truth.assertThat(result).containsExactlyElementsIn(SUPPORTED_RESOLUTIONS)
+    }
+}
\ No newline at end of file
diff --git a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/DisplayInfoManagerTest.kt b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/DisplayInfoManagerTest.kt
index 1d12d9b..02a742b 100644
--- a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/DisplayInfoManagerTest.kt
+++ b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/DisplayInfoManagerTest.kt
@@ -198,7 +198,7 @@
         addDisplay(480, 640)
 
         // Act & Assert
-        assertEquals(Size(640, 480), displayInfoManager.previewSize)
+        assertEquals(Size(640, 480), displayInfoManager.getPreviewSize())
     }
 
     @Test
@@ -207,7 +207,7 @@
         addDisplay(2000, 3000)
 
         // Act & Assert
-        assertEquals(Size(1920, 1080), displayInfoManager.previewSize)
+        assertEquals(Size(1920, 1080), displayInfoManager.getPreviewSize())
     }
 
     @Test
@@ -216,10 +216,11 @@
         addDisplay(480, 640)
 
         // Act
-        displayInfoManager.previewSize
+        displayInfoManager.getPreviewSize()
         addDisplay(2000, 3000)
+        displayInfoManager.refresh()
 
         // Assert
-        assertEquals(Size(1920, 1080), displayInfoManager.previewSize)
+        assertEquals(Size(1920, 1080), displayInfoManager.getPreviewSize())
     }
 }
diff --git a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/testing/FakeCameraInfoAdapterCreator.kt b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/testing/FakeCameraInfoAdapterCreator.kt
index b687bf0..f0b520c 100644
--- a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/testing/FakeCameraInfoAdapterCreator.kt
+++ b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/testing/FakeCameraInfoAdapterCreator.kt
@@ -100,7 +100,7 @@
         }
         val fakeStreamConfigurationMap = StreamConfigurationMapCompat(
             streamConfigurationMap,
-            OutputSizesCorrector(cameraProperties.metadata)
+            OutputSizesCorrector(cameraProperties.metadata, streamConfigurationMap)
         )
         val fakeCameraQuirks = CameraQuirks(
             cameraProperties.metadata,
diff --git a/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/Camera2CameraImplTest.java b/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/Camera2CameraImplTest.java
index 2d941f0..973ad8b 100644
--- a/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/Camera2CameraImplTest.java
+++ b/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/Camera2CameraImplTest.java
@@ -56,7 +56,6 @@
 import androidx.camera.core.CameraSelector;
 import androidx.camera.core.ImageCapture;
 import androidx.camera.core.InitializationException;
-import androidx.camera.core.ResolutionSelector;
 import androidx.camera.core.UseCase;
 import androidx.camera.core.impl.CameraCaptureCallback;
 import androidx.camera.core.impl.CameraCaptureResult;
@@ -69,6 +68,8 @@
 import androidx.camera.core.impl.SessionConfig;
 import androidx.camera.core.impl.StreamSpec;
 import androidx.camera.core.impl.utils.executor.CameraXExecutors;
+import androidx.camera.core.resolutionselector.HighResolution;
+import androidx.camera.core.resolutionselector.ResolutionSelector;
 import androidx.camera.testing.CameraUtil;
 import androidx.camera.testing.HandlerUtil;
 import androidx.camera.testing.fakes.FakeCamera;
@@ -965,7 +966,8 @@
 
         // Creates a test use case with high resolution enabled.
         ResolutionSelector highResolutionSelector =
-                new ResolutionSelector.Builder().setHighResolutionEnabled(true).build();
+                new ResolutionSelector.Builder().setHighResolutionEnabledFlags(
+                        HighResolution.FLAG_DEFAULT_MODE_ON).build();
         FakeUseCaseConfig.Builder configBuilder =
                 new FakeUseCaseConfig.Builder().setSessionOptionUnpacker(
                         new Camera2SessionOptionUnpacker()).setTargetName(
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2UseCaseConfigFactory.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2UseCaseConfigFactory.java
index fe5cae3..66ce64d 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2UseCaseConfigFactory.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2UseCaseConfigFactory.java
@@ -17,6 +17,7 @@
 package androidx.camera.camera2.internal;
 
 import static androidx.camera.core.impl.ImageOutputConfig.OPTION_MAX_RESOLUTION;
+import static androidx.camera.core.impl.ImageOutputConfig.OPTION_RESOLUTION_SELECTOR;
 import static androidx.camera.core.impl.ImageOutputConfig.OPTION_TARGET_ROTATION;
 import static androidx.camera.core.impl.UseCaseConfig.OPTION_CAPTURE_CONFIG_UNPACKER;
 import static androidx.camera.core.impl.UseCaseConfig.OPTION_DEFAULT_CAPTURE_CONFIG;
@@ -26,6 +27,7 @@
 
 import android.content.Context;
 import android.hardware.camera2.CameraDevice;
+import android.util.Size;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.RequiresApi;
@@ -37,6 +39,8 @@
 import androidx.camera.core.impl.OptionsBundle;
 import androidx.camera.core.impl.SessionConfig;
 import androidx.camera.core.impl.UseCaseConfigFactory;
+import androidx.camera.core.resolutionselector.ResolutionSelector;
+import androidx.camera.core.resolutionselector.ResolutionStrategy;
 
 /**
  * Implementation of UseCaseConfigFactory to provide the default camera2 configurations for use
@@ -90,7 +94,7 @@
                 captureBuilder.setTemplateType(
                         captureMode == ImageCapture.CAPTURE_MODE_ZERO_SHUTTER_LAG
                                 ? CameraDevice.TEMPLATE_ZERO_SHUTTER_LAG :
-                        CameraDevice.TEMPLATE_STILL_CAPTURE);
+                                CameraDevice.TEMPLATE_STILL_CAPTURE);
                 break;
             case PREVIEW:
             case IMAGE_ANALYSIS:
@@ -109,8 +113,13 @@
                         : Camera2CaptureOptionUnpacker.INSTANCE);
 
         if (captureType == CaptureType.PREVIEW) {
-            mutableConfig.insertOption(OPTION_MAX_RESOLUTION,
-                    mDisplayInfoManager.getPreviewSize());
+            Size previewSize = mDisplayInfoManager.getPreviewSize();
+            mutableConfig.insertOption(OPTION_MAX_RESOLUTION, previewSize);
+            ResolutionStrategy resolutionStrategy = ResolutionStrategy.create(previewSize,
+                    ResolutionStrategy.FALLBACK_RULE_CLOSEST_LOWER);
+            mutableConfig.insertOption(OPTION_RESOLUTION_SELECTOR,
+                    new ResolutionSelector.Builder().setResolutionStrategy(
+                            resolutionStrategy).build());
         }
 
         int targetRotation = mDisplayInfoManager.getMaxSizeDisplay().getRotation();
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/ImageAnalysis.java b/camera/camera-core/src/main/java/androidx/camera/core/ImageAnalysis.java
index 59a07ee4..573a0d7 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/ImageAnalysis.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/ImageAnalysis.java
@@ -23,6 +23,7 @@
 import static androidx.camera.core.impl.ImageAnalysisConfig.OPTION_OUTPUT_IMAGE_FORMAT;
 import static androidx.camera.core.impl.ImageAnalysisConfig.OPTION_OUTPUT_IMAGE_ROTATION_ENABLED;
 import static androidx.camera.core.impl.ImageOutputConfig.OPTION_CUSTOM_ORDERED_RESOLUTIONS;
+import static androidx.camera.core.impl.ImageOutputConfig.OPTION_DEFAULT_RESOLUTION;
 import static androidx.camera.core.impl.ImageOutputConfig.OPTION_MAX_RESOLUTION;
 import static androidx.camera.core.impl.ImageOutputConfig.OPTION_RESOLUTION_SELECTOR;
 import static androidx.camera.core.impl.ImageOutputConfig.OPTION_SUPPORTED_RESOLUTIONS;
@@ -82,6 +83,10 @@
 import androidx.camera.core.internal.TargetConfig;
 import androidx.camera.core.internal.ThreadConfig;
 import androidx.camera.core.internal.compat.quirk.OnePixelShiftQuirk;
+import androidx.camera.core.internal.utils.SizeUtil;
+import androidx.camera.core.resolutionselector.AspectRatioStrategy;
+import androidx.camera.core.resolutionselector.ResolutionSelector;
+import androidx.camera.core.resolutionselector.ResolutionStrategy;
 import androidx.core.util.Preconditions;
 import androidx.lifecycle.LifecycleOwner;
 
@@ -265,35 +270,38 @@
                     ? mSubscribedAnalyzer.getDefaultTargetResolution() : null;
         }
 
-        if (analyzerResolution != null) {
-            if (!builder.getMutableConfig().containsOption(OPTION_RESOLUTION_SELECTOR)) {
-                int targetRotation = builder.getMutableConfig().retrieveOption(
-                        OPTION_TARGET_ROTATION, Surface.ROTATION_0);
-                // analyzerResolution is a size in the sensor coordinate system, but the legacy
-                // target resolution setting is in the view coordinate system. Flips the
-                // analyzerResolution according to the sensor rotation degrees.
-                if (cameraInfo.getSensorRotationDegrees(targetRotation) % 180 == 90) {
-                    analyzerResolution = new Size(/* width= */ analyzerResolution.getHeight(),
-                            /* height= */ analyzerResolution.getWidth());
-                }
+        if (analyzerResolution == null) {
+            return builder.getUseCaseConfig();
+        }
 
-                if (!builder.getUseCaseConfig().containsOption(OPTION_TARGET_RESOLUTION)) {
-                    builder.getMutableConfig().insertOption(OPTION_TARGET_RESOLUTION,
-                            analyzerResolution);
-                }
-            } else {
-                // Merges analyzerResolution or default resolution to ResolutionSelector.
-                ResolutionSelector resolutionSelector =
-                        builder.getMutableConfig().retrieveOption(OPTION_RESOLUTION_SELECTOR);
+        int targetRotation = builder.getMutableConfig().retrieveOption(
+                OPTION_TARGET_ROTATION, Surface.ROTATION_0);
+        // analyzerResolution is a size in the sensor coordinate system, but the legacy
+        // target resolution setting is in the view coordinate system. Flips the
+        // analyzerResolution according to the sensor rotation degrees.
+        if (cameraInfo.getSensorRotationDegrees(targetRotation) % 180 == 90) {
+            analyzerResolution = new Size(/* width= */ analyzerResolution.getHeight(),
+                    /* height= */ analyzerResolution.getWidth());
+        }
 
-                if (resolutionSelector.getPreferredResolution() == null) {
-                    ResolutionSelector.Builder resolutionSelectorBuilder =
-                            ResolutionSelector.Builder.fromSelector(resolutionSelector);
-                    resolutionSelectorBuilder.setPreferredResolution(analyzerResolution);
-                    builder.getMutableConfig().insertOption(OPTION_RESOLUTION_SELECTOR,
-                            resolutionSelectorBuilder.build());
-                }
-            }
+        // Merges the analyzerResolution as legacy target resolution setting so that it can take
+        // effect when running the legacy resolution selection logic flow.
+        if (!builder.getUseCaseConfig().containsOption(OPTION_TARGET_RESOLUTION)) {
+            builder.getMutableConfig().insertOption(OPTION_TARGET_RESOLUTION,
+                    analyzerResolution);
+        }
+
+        // Merges the analyzerResolution to ResolutionSelector.
+        ResolutionSelector resolutionSelector =
+                builder.getMutableConfig().retrieveOption(OPTION_RESOLUTION_SELECTOR, null);
+        if (resolutionSelector != null && resolutionSelector.getResolutionStrategy() == null) {
+            ResolutionSelector.Builder resolutionSelectorBuilder =
+                    ResolutionSelector.Builder.fromResolutionSelector(resolutionSelector);
+            resolutionSelectorBuilder.setResolutionStrategy(
+                    ResolutionStrategy.create(analyzerResolution,
+                            ResolutionStrategy.FALLBACK_RULE_CLOSEST_HIGHER_THEN_LOWER));
+            builder.getMutableConfig().insertOption(OPTION_RESOLUTION_SELECTOR,
+                    resolutionSelectorBuilder.build());
         }
 
         return builder.getUseCaseConfig();
@@ -721,6 +729,18 @@
         return super.getResolutionInfo();
     }
 
+    /**
+     * Returns the resolution selector setting.
+     *
+     * <p>This setting is set when constructing an ImageCapture using
+     * {@link Builder#setResolutionSelector(ResolutionSelector)}.
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @Nullable
+    public ResolutionSelector getResolutionSelector() {
+        return ((ImageOutputConfig) getCurrentConfig()).getResolutionSelector(null);
+    }
+
     @Override
     @NonNull
     public String toString() {
@@ -985,13 +1005,21 @@
         private static final int DEFAULT_SURFACE_OCCUPANCY_PRIORITY = 1;
         private static final int DEFAULT_ASPECT_RATIO = AspectRatio.RATIO_4_3;
 
+        private static final ResolutionSelector DEFAULT_RESOLUTION_SELECTOR =
+                new ResolutionSelector.Builder().setAspectRatioStrategy(
+                        AspectRatioStrategy.RATIO_4_3_FALLBACK_AUTO_STRATEGY).setResolutionStrategy(
+                        ResolutionStrategy.create(SizeUtil.RESOLUTION_VGA,
+                                ResolutionStrategy.FALLBACK_RULE_CLOSEST_HIGHER_THEN_LOWER))
+                        .build();
+
         private static final ImageAnalysisConfig DEFAULT_CONFIG;
 
         static {
             Builder builder = new Builder()
                     .setDefaultResolution(DEFAULT_TARGET_RESOLUTION)
                     .setSurfaceOccupancyPriority(DEFAULT_SURFACE_OCCUPANCY_PRIORITY)
-                    .setTargetAspectRatio(DEFAULT_ASPECT_RATIO);
+                    .setTargetAspectRatio(DEFAULT_ASPECT_RATIO)
+                    .setResolutionSelector(DEFAULT_RESOLUTION_SELECTOR);
 
             DEFAULT_CONFIG = builder.getUseCaseConfig();
         }
@@ -1383,7 +1411,7 @@
         @RestrictTo(Scope.LIBRARY_GROUP)
         @Override
         public Builder setDefaultResolution(@NonNull Size resolution) {
-            getMutableConfig().insertOption(ImageOutputConfig.OPTION_DEFAULT_RESOLUTION,
+            getMutableConfig().insertOption(OPTION_DEFAULT_RESOLUTION,
                     resolution);
             return this;
         }
@@ -1415,14 +1443,14 @@
         /**
          * Sets the resolution selector to select the preferred supported resolution.
          *
-         * <p>ImageAnalysis has a default minimal bounding size as 640x480. The input
-         * {@link ResolutionSelector}'s' preferred resolution can override the minimal bounding
-         * size to find the best resolution.
+         * <p>ImageAnalysis has a default {@link ResolutionStrategy} with bound size as 640x480
+         * and fallback rule of {@link ResolutionStrategy#FALLBACK_RULE_CLOSEST_HIGHER_THEN_LOWER}.
+         * Applications can override this default strategy with a different resolution strategy.
          *
-         * <p>When using the {@code camera-camera2} CameraX implementation, which resolution will
-         * be finally selected will depend on the camera device's hardware level, capabilities
-         * and the bound use cases combination. The device hardware level and capabilities
-         * information can be retrieved via the interop class
+         * <p>When using the {@code camera-camera2} CameraX implementation, which resolution is
+         * finally selected depends on the camera device's hardware level, capabilities and the
+         * bound use cases combination. The device hardware level and capabilities information
+         * can be retrieved via the interop class
          * {@link androidx.camera.camera2.interop.Camera2CameraInfo#getCameraCharacteristic(android.hardware.camera2.CameraCharacteristics.Key)}
          * with
          * {@link android.hardware.camera2.CameraCharacteristics#INFO_SUPPORTED_HARDWARE_LEVEL} and
@@ -1431,13 +1459,14 @@
          * <p>A {@code LIMITED-level} above device can support a {@code RECORD} size resolution
          * for {@link ImageAnalysis} when it is bound together with {@link Preview} and
          * {@link ImageCapture}. The trade-off is the selected resolution for the
-         * {@link ImageCapture} will also be restricted by the {@code RECORD} size. To
-         * successfully select a {@code RECORD} size resolution for {@link ImageAnalysis}, a
-         * {@code RECORD} size preferred resolution should be set on both {@link ImageCapture} and
-         * {@link ImageAnalysis}. This indicates that the application clearly understand the
-         * trade-off and prefer the {@link ImageAnalysis} to have a larger resolution rather than
-         * the {@link ImageCapture} to have a {@code MAXIMUM} size resolution. For the
-         * definitions of {@code RECORD}, {@code MAXIMUM} sizes and more details see the
+         * {@link ImageCapture} is also restricted by the {@code RECORD} size. To successfully
+         * select a {@code RECORD} size resolution for {@link ImageAnalysis}, a
+         * {@link ResolutionStrategy} of selecting {@code RECORD} size resolution should be set
+         * on both {@link ImageCapture} and {@link ImageAnalysis}. This indicates that the
+         * application clearly understand the trade-off and prefer the {@link ImageAnalysis} to
+         * have a larger resolution rather than the {@link ImageCapture} to have a {@code MAXIMUM
+         * } size resolution. For the definitions of {@code RECORD}, {@code MAXIMUM} sizes and
+         * more details see the
          * <a href="https://developer.android.com/reference/android/hardware/camera2/CameraDevice#regular-capture">Regular capture</a>
          * section in {@link android.hardware.camera2.CameraDevice}'s. The {@code RECORD} size
          * refers to the camera device's maximum supported recording resolution, as determined by
@@ -1447,11 +1476,13 @@
          *
          * <p>The existing {@link #setTargetResolution(Size)} and
          * {@link #setTargetAspectRatio(int)} APIs are deprecated and are not compatible with
-         * {@link ResolutionSelector}. Calling any of these APIs together with
-         * {@link ResolutionSelector} will throw an {@link IllegalArgumentException} while
-         * {@link #build()} is called to create the {@link ImageAnalysis} instance.
+         * {@link #setResolutionSelector(ResolutionSelector)}. Calling either of these APIs
+         * together with {@link #setResolutionSelector(ResolutionSelector)} will result in an
+         * {@link IllegalArgumentException} being thrown when you attempt to build the
+         * {@link ImageAnalysis} instance.
          *
-         **/
+         * @return The current Builder.
+         */
         @RestrictTo(Scope.LIBRARY_GROUP)
         @Override
         @NonNull
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/ImageCapture.java b/camera/camera-core/src/main/java/androidx/camera/core/ImageCapture.java
index ebae469..159f7ff 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/ImageCapture.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/ImageCapture.java
@@ -119,6 +119,9 @@
 import androidx.camera.core.internal.compat.quirk.SoftwareJpegEncodingPreferredQuirk;
 import androidx.camera.core.internal.compat.workaround.ExifRotationAvailability;
 import androidx.camera.core.internal.utils.ImageUtil;
+import androidx.camera.core.resolutionselector.AspectRatioStrategy;
+import androidx.camera.core.resolutionselector.ResolutionSelector;
+import androidx.camera.core.resolutionselector.ResolutionStrategy;
 import androidx.concurrent.futures.CallbackToFutureAdapter;
 import androidx.core.util.Preconditions;
 import androidx.lifecycle.LifecycleOwner;
@@ -933,6 +936,18 @@
     }
 
     /**
+     * Returns the resolution selector setting.
+     *
+     * <p>This setting is set when constructing an ImageCapture using
+     * {@link Builder#setResolutionSelector(ResolutionSelector)}.
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @Nullable
+    public ResolutionSelector getResolutionSelector() {
+        return ((ImageOutputConfig) getCurrentConfig()).getResolutionSelector(null);
+    }
+
+    /**
      * Captures a new still image for in memory access.
      *
      * <p>The callback will be called only once for every invocation of this method. The listener
@@ -2017,11 +2032,18 @@
         private static final int DEFAULT_SURFACE_OCCUPANCY_PRIORITY = 4;
         private static final int DEFAULT_ASPECT_RATIO = AspectRatio.RATIO_4_3;
 
+        private static final ResolutionSelector DEFAULT_RESOLUTION_SELECTOR =
+                new ResolutionSelector.Builder().setAspectRatioStrategy(
+                        AspectRatioStrategy.RATIO_4_3_FALLBACK_AUTO_STRATEGY).setResolutionStrategy(
+                        ResolutionStrategy.HIGHEST_AVAILABLE_STRATEGY).build();
+
         private static final ImageCaptureConfig DEFAULT_CONFIG;
 
         static {
-            Builder builder = new Builder().setSurfaceOccupancyPriority(
-                    DEFAULT_SURFACE_OCCUPANCY_PRIORITY).setTargetAspectRatio(DEFAULT_ASPECT_RATIO);
+            Builder builder = new Builder()
+                    .setSurfaceOccupancyPriority(DEFAULT_SURFACE_OCCUPANCY_PRIORITY)
+                    .setTargetAspectRatio(DEFAULT_ASPECT_RATIO)
+                    .setResolutionSelector(DEFAULT_RESOLUTION_SELECTOR);
 
             DEFAULT_CONFIG = builder.getUseCaseConfig();
         }
@@ -2885,21 +2907,20 @@
         /**
          * Sets the resolution selector to select the preferred supported resolution.
          *
-         * <p>If no resolution selector is set, the largest available resolution will be selected
-         * to use. Usually, users will intend to get the largest still image that the camera
-         * device can support. Unlike {@link Builder#setTargetResolution(Size)},
-         * {@link #setCropAspectRatio(Rational)} won't be automatically called to set the
-         * corresponding value and crop the output image when a target resolution is set. Use
-         * {@link ViewPort} instead if the output images need to be cropped in a specific
-         * aspect ratio.
+         * <p>The default resolution strategy for ImageCapture is
+         * {@link ResolutionStrategy#HIGHEST_AVAILABLE_STRATEGY}, which will select the largest
+         * available resolution to use. Applications can override this default strategy with a
+         * different resolution strategy.
          *
          * <p>The existing {@link #setTargetResolution(Size)} and
          * {@link #setTargetAspectRatio(int)} APIs are deprecated and are not compatible with
-         * {@link ResolutionSelector}. Calling any of these APIs together with
-         * {@link ResolutionSelector} will throw an {@link IllegalArgumentException} while
-         * {@link #build()} is called to create the {@link ImageCapture} instance.
+         * {@link #setResolutionSelector(ResolutionSelector)}. Calling either of these APIs
+         * together with {@link #setResolutionSelector(ResolutionSelector)} will result in an
+         * {@link IllegalArgumentException} being thrown when you attempt to build the
+         * {@link ImageCapture} instance.
          *
-         **/
+         * @return The current Builder.
+         */
         @RestrictTo(Scope.LIBRARY_GROUP)
         @Override
         @NonNull
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/Preview.java b/camera/camera-core/src/main/java/androidx/camera/core/Preview.java
index 862dce9..f994a2d 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/Preview.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/Preview.java
@@ -89,6 +89,9 @@
 import androidx.camera.core.processing.Node;
 import androidx.camera.core.processing.SurfaceEdge;
 import androidx.camera.core.processing.SurfaceProcessorNode;
+import androidx.camera.core.resolutionselector.AspectRatioStrategy;
+import androidx.camera.core.resolutionselector.ResolutionSelector;
+import androidx.camera.core.resolutionselector.ResolutionStrategy;
 import androidx.core.util.Consumer;
 import androidx.lifecycle.LifecycleOwner;
 
@@ -534,6 +537,18 @@
         return super.getResolutionInfo();
     }
 
+    /**
+     * Returns the resolution selector setting.
+     *
+     * <p>This setting is set when constructing an ImageCapture using
+     * {@link Builder#setResolutionSelector(ResolutionSelector)}.
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @Nullable
+    public ResolutionSelector getResolutionSelector() {
+        return ((ImageOutputConfig) getCurrentConfig()).getResolutionSelector(null);
+    }
+
     @NonNull
     @Override
     public String toString() {
@@ -573,20 +588,6 @@
         builder.getMutableConfig().insertOption(OPTION_INPUT_FORMAT,
                 INTERNAL_DEFINED_IMAGE_FORMAT_PRIVATE);
 
-        // Merges Preview's default max resolution setting when resolution selector is used
-        ResolutionSelector resolutionSelector =
-                builder.getMutableConfig().retrieveOption(OPTION_RESOLUTION_SELECTOR, null);
-        if (resolutionSelector != null && resolutionSelector.getMaxResolution() == null) {
-            Size maxResolution = builder.getMutableConfig().retrieveOption(OPTION_MAX_RESOLUTION);
-            if (maxResolution != null) {
-                ResolutionSelector.Builder resolutionSelectorBuilder =
-                        ResolutionSelector.Builder.fromSelector(resolutionSelector);
-                resolutionSelectorBuilder.setMaxResolution(maxResolution);
-                builder.getMutableConfig().insertOption(OPTION_RESOLUTION_SELECTOR,
-                        resolutionSelectorBuilder.build());
-            }
-        }
-
         return builder.getUseCaseConfig();
     }
 
@@ -738,11 +739,18 @@
         private static final int DEFAULT_ASPECT_RATIO = AspectRatio.RATIO_4_3;
         private static final int DEFAULT_MIRROR_MODE = MIRROR_MODE_FRONT_ON;
 
+        private static final ResolutionSelector DEFAULT_RESOLUTION_SELECTOR =
+                new ResolutionSelector.Builder().setAspectRatioStrategy(
+                        AspectRatioStrategy.RATIO_4_3_FALLBACK_AUTO_STRATEGY).setResolutionStrategy(
+                        ResolutionStrategy.HIGHEST_AVAILABLE_STRATEGY).build();
+
         private static final PreviewConfig DEFAULT_CONFIG;
 
         static {
-            Builder builder = new Builder().setSurfaceOccupancyPriority(
-                    DEFAULT_SURFACE_OCCUPANCY_PRIORITY).setTargetAspectRatio(DEFAULT_ASPECT_RATIO);
+            Builder builder = new Builder()
+                    .setSurfaceOccupancyPriority(DEFAULT_SURFACE_OCCUPANCY_PRIORITY)
+                    .setTargetAspectRatio(DEFAULT_ASPECT_RATIO)
+                    .setResolutionSelector(DEFAULT_RESOLUTION_SELECTOR);
             DEFAULT_CONFIG = builder.getUseCaseConfig();
         }
 
@@ -1061,21 +1069,23 @@
          * size match to the device's screen resolution, or to 1080p (1920x1080), whichever is
          * smaller. See the
          * <a href="https://developer.android.com/reference/android/hardware/camera2/CameraDevice#regular-capture">Regular capture</a>
-         * section in {@link android.hardware.camera2.CameraDevice}'. If the
-         * {@link ResolutionSelector} contains the max resolution setting larger than the {@code
-         * PREVIEW} size, a size larger than the device's screen resolution or 1080p can be
-         * selected to use for {@link Preview}.
+         * section in {@link android.hardware.camera2.CameraDevice}'. {@link Preview} has a
+         * default {@link ResolutionStrategy} with the {@code PREVIEW} bound size and
+         * {@link ResolutionStrategy#FALLBACK_RULE_CLOSEST_LOWER} to achieve this. Applications
+         * can override this default strategy with a different resolution strategy.
          *
          * <p>Note that due to compatibility reasons, CameraX may select a resolution that is
          * larger than the default screen resolution on certain devices.
          *
          * <p>The existing {@link #setTargetResolution(Size)} and
          * {@link #setTargetAspectRatio(int)} APIs are deprecated and are not compatible with
-         * {@link ResolutionSelector}. Calling any of these APIs together with
-         * {@link ResolutionSelector} will throw an {@link IllegalArgumentException} while
-         * {@link #build()} is called to create the {@link Preview} instance.
+         * {@link #setResolutionSelector(ResolutionSelector)}. Calling either of these APIs
+         * together with {@link #setResolutionSelector(ResolutionSelector)} will result in an
+         * {@link IllegalArgumentException} being thrown when you attempt to build the
+         * {@link Preview} instance.
          *
-         **/
+         * @return The current Builder.
+         */
         @RestrictTo(Scope.LIBRARY_GROUP)
         @Override
         @NonNull
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/ResolutionSelector.java b/camera/camera-core/src/main/java/androidx/camera/core/ResolutionSelector.java
deleted file mode 100644
index 9d18df4..0000000
--- a/camera/camera-core/src/main/java/androidx/camera/core/ResolutionSelector.java
+++ /dev/null
@@ -1,381 +0,0 @@
-/*
- * Copyright 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.camera.core;
-
-import android.util.Size;
-import android.view.View;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.annotation.RequiresApi;
-import androidx.annotation.RestrictTo;
-import androidx.camera.core.impl.SizeCoordinate;
-
-/**
- * A set of requirements and priorities used to select a resolution for the use case.
- *
- * <p>The resolution selection mechanism is determined by the following three steps:
- * <ol>
- *     <li> Collect supported output sizes to the candidate resolution list
- *     <li> Determine the selecting priority of the candidate resolution list by the preference
- *     settings
- *     <li> Consider all the resolution selector settings of bound use cases to find the best
- *     resolution for each use case
- * </ol>
- *
- * <p>For the first step, all supported resolution output sizes are put into the candidate
- * resolution list as the base in the beginning.
- *
- * <p>ResolutionSelector provides the following two functions for applications to adjust the
- * conditions of the candidate resolutions.
- * <ul>
- *     <li> {@link Builder#setMaxResolution(Size)}
- *     <li> {@link Builder#setHighResolutionEnabled(boolean)}
- * </ul>
- *
- * <p>For the second step, ResolutionSelector provides the following three functions for
- * applications to determine which resolution has higher priority to be selected.
- * <ul>
- *     <li> {@link Builder#setPreferredResolution(Size)}
- *     <li> {@link Builder#setPreferredResolutionByViewSize(Size)}
- *     <li> {@link Builder#setPreferredAspectRatio(int)}
- * </ul>
- *
- * <p>The resolution that exactly matches the preferred resolution is selected in first priority.
- * If the resolution can't be found, CameraX falls back to use the sizes of the preferred aspect
- * ratio. In this case, the preferred resolution is treated as the minimal bounding size to find
- * the best resolution.
- *
- * <p>Different types of use cases might have their own additional conditions. Please see the use
- * case config builders’ {@code setResolutionSelector()} function to know the condition details
- * for each type of use case.
- *
- * <p>For the third step, CameraX selects the final resolution for the use case based on the
- * camera device's hardware level, capabilities and the bound use case combination. Applications
- * can check which resolution is finally selected by using the use case's {@code
- * getResolutionInfo()} function.
- *
- */
-@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-public class ResolutionSelector {
-    @Nullable
-    private final Size mPreferredResolution;
-
-    private final SizeCoordinate mSizeCoordinate;
-
-    private final int mPreferredAspectRatio;
-
-    @Nullable
-    private final Size mMaxResolution;
-
-    private final boolean mIsHighResolutionEnabled;
-
-    ResolutionSelector(int preferredAspectRatio,
-            @Nullable Size preferredResolution,
-            @NonNull SizeCoordinate sizeCoordinate,
-            @Nullable Size maxResolution,
-            boolean isHighResolutionEnabled) {
-        mPreferredAspectRatio = preferredAspectRatio;
-        mPreferredResolution = preferredResolution;
-        mSizeCoordinate = sizeCoordinate;
-        mMaxResolution = maxResolution;
-        mIsHighResolutionEnabled = isHighResolutionEnabled;
-    }
-
-    /**
-     * Retrieves the preferred aspect ratio in the ResolutionSelector.
-     *
-     */
-    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-    @AspectRatio.Ratio
-    public int getPreferredAspectRatio() {
-        return mPreferredAspectRatio;
-    }
-
-    /**
-     * Retrieves the preferred resolution in the ResolutionSelector.
-     *
-     */
-    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-    @Nullable
-    public Size getPreferredResolution() {
-        return mPreferredResolution;
-    }
-
-    /**
-     * Retrieves the size coordinate of the preferred resolution in the ResolutionSelector.
-     *
-     */
-    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-    @NonNull
-    public SizeCoordinate getSizeCoordinate() {
-        return mSizeCoordinate;
-    }
-
-    /**
-     * Returns the max resolution in the ResolutionSelector.
-     *
-     */
-    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-    @Nullable
-    public Size getMaxResolution() {
-        return mMaxResolution;
-    }
-
-    /**
-     * Returns {@code true} if high resolutions are allowed to be selected, otherwise {@code false}.
-     *
-     */
-    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-    public boolean isHighResolutionEnabled() {
-        return mIsHighResolutionEnabled;
-    }
-
-    /**
-     * Builder for a {@link ResolutionSelector}.
-     */
-    public static final class Builder {
-        @AspectRatio.Ratio
-        private int mPreferredAspectRatio = AspectRatio.RATIO_4_3;
-        @Nullable
-        private Size mPreferredResolution = null;
-        @NonNull
-        private SizeCoordinate mSizeCoordinate = SizeCoordinate.CAMERA_SENSOR;
-        @Nullable
-        private Size mMaxResolution = null;
-        private boolean mIsHighResolutionEnabled = false;
-
-        /**
-         * Creates a new Builder object.
-         */
-        public Builder() {
-        }
-
-        private Builder(@NonNull ResolutionSelector selector) {
-            mPreferredAspectRatio = selector.getPreferredAspectRatio();
-            mPreferredResolution = selector.getPreferredResolution();
-            mSizeCoordinate = selector.getSizeCoordinate();
-            mMaxResolution = selector.getMaxResolution();
-            mIsHighResolutionEnabled = selector.isHighResolutionEnabled();
-        }
-
-        /**
-         * Generates a Builder from another {@link ResolutionSelector} object.
-         *
-         * @param selector an existing {@link ResolutionSelector}.
-         * @return the new Builder.
-         */
-        @NonNull
-        public static Builder fromSelector(@NonNull ResolutionSelector selector) {
-            return new Builder(selector);
-        }
-
-        /**
-         * Sets the preferred aspect ratio that the output images are expected to have.
-         *
-         * <p>The aspect ratio is the ratio of width to height in the camera sensor's natural
-         * orientation. If set, CameraX finds the sizes that match the aspect ratio with priority
-         * . Among the sizes that match the aspect ratio, the larger the size, the higher the
-         * priority.
-         *
-         * <p>If CameraX can't find any available sizes that match the preferred aspect ratio,
-         * CameraX falls back to select the sizes with the nearest aspect ratio that can contain
-         * the full field of view of the sizes with preferred aspect ratio.
-         *
-         * <p>If preferred aspect ratio is not set, the default aspect ratio is
-         * {@link AspectRatio#RATIO_4_3}, which usually has largest field of view because most
-         * camera sensor are {@code 4:3}.
-         *
-         * <p>This API is useful for apps that want to capture images matching the {@code 16:9}
-         * display aspect ratio. Apps can set preferred aspect ratio as
-         * {@link AspectRatio#RATIO_16_9} to achieve this.
-         *
-         * <p>The actual aspect ratio of the output may differ from the specified preferred
-         * aspect ratio value. Application code should check the resulting output's resolution.
-         *
-         * @param preferredAspectRatio the aspect ratio you prefer to use.
-         * @return the current Builder.
-         */
-        @NonNull
-        public Builder setPreferredAspectRatio(@AspectRatio.Ratio int preferredAspectRatio) {
-            mPreferredAspectRatio = preferredAspectRatio;
-            return this;
-        }
-
-        /**
-         * Sets the preferred resolution you expect to select. The resolution is expressed in the
-         * camera sensor's natural orientation (landscape), which means you can set the size
-         * retrieved from
-         * {@link android.hardware.camera2.params.StreamConfigurationMap#getOutputSizes} directly.
-         *
-         * <p>Once the preferred resolution is set, CameraX finds exactly matched size first
-         * regardless of the preferred aspect ratio. This API is useful for apps that want to
-         * select an exact size retrieved from
-         * {@link android.hardware.camera2.params.StreamConfigurationMap#getOutputSizes}.
-         *
-         * <p>If CameraX can't find the size that matches the preferred resolution, it attempts
-         * to establish a minimal bound for the given resolution. The actual resolution is the
-         * closest available resolution that is not smaller than the preferred resolution.
-         * However, if no resolution exists that is equal to or larger than the preferred
-         * resolution, the nearest available resolution smaller than the preferred resolution is
-         * chosen.
-         *
-         * <p>When the preferred resolution is used as a minimal bound, CameraX also considers
-         * the preferred aspect ratio to find the sizes that either match it or are close to it.
-         * Using preferred resolution as the minimal bound is useful for apps that want to shrink
-         * the size for the surface. For example, for apps that just show the camera preview in a
-         * small view, apps can specify a size smaller than display size. CameraX can effectively
-         * select a smaller size for better efficiency.
-         *
-         * <p>If both {@link Builder#setPreferredResolution(Size)} and
-         * {@link Builder#setPreferredResolutionByViewSize(Size)} are invoked, which one set
-         * later overrides the one set before.
-         *
-         * @param preferredResolution the preferred resolution expressed in the orientation of
-         *                            the device camera sensor coordinate to choose the preferred
-         *                            resolution from supported output sizes list.
-         * @return the current Builder.
-         */
-        @NonNull
-        public Builder setPreferredResolution(@NonNull Size preferredResolution) {
-            mPreferredResolution = preferredResolution;
-            mSizeCoordinate = SizeCoordinate.CAMERA_SENSOR;
-            return this;
-        }
-
-        /**
-         * Sets the preferred resolution you expect to select. The resolution is expressed in the
-         * Android {@link View} coordinate system.
-         *
-         * <p>For phone devices, the sensor coordinate orientation usually has 90 degrees
-         * difference from the phone device display’s natural orientation. Depending on the
-         * display rotation value when the use case is bound, CameraX transforms the input
-         * resolution into the camera sensor's natural orientation to find the best suitable
-         * resolution.
-         *
-         * <p>Once the preferred resolution is set, CameraX finds the size that exactly matches
-         * the preferred resolution first regardless of the preferred aspect ratio.
-         *
-         * <p>If CameraX can't find the size that matches the preferred resolution, it attempts
-         * to establish a minimal bound for the given resolution. The actual resolution is the
-         * closest available resolution that is not smaller than the preferred resolution.
-         * However, if no resolution exists that is equal to or larger than the preferred
-         * resolution, the nearest available resolution smaller than the preferred resolution is
-         * chosen.
-         *
-         * <p>When the preferred resolution is used as a minimal bound, CameraX also considers
-         * the preferred aspect ratio to find the sizes that either match it or are close to it.
-         * Using Android {@link View} size as preferred resolution is useful for apps that want
-         * to shrink the size for the surface. For example, for apps that just show the camera
-         * preview in a small view, apps can specify the small size of Android {@link View}.
-         * CameraX can effectively select a smaller size for better efficiency.
-         *
-         * <p>If both {@link Builder#setPreferredResolution(Size)} and
-         * {@link Builder#setPreferredResolutionByViewSize(Size)} are invoked, the later setting
-         * overrides the former one.
-         *
-         * @param preferredResolutionByViewSize the preferred resolution expressed in the
-         *                                      orientation of the app layout's Android
-         *                                      {@link View} to choose the preferred resolution
-         *                                      from supported output sizes list.
-         * @return the current Builder.
-         */
-        @NonNull
-        public Builder setPreferredResolutionByViewSize(
-                @NonNull Size preferredResolutionByViewSize) {
-            mPreferredResolution = preferredResolutionByViewSize;
-            mSizeCoordinate = SizeCoordinate.ANDROID_VIEW;
-            return this;
-        }
-
-        /**
-         * Sets the max resolution condition for the use case.
-         *
-         * <p>The max resolution prevents the use case to select the sizes which either width or
-         * height exceeds the specified resolution.
-         *
-         * <p>The resolution should be expressed in the camera sensor's natural orientation
-         * (landscape).
-         *
-         * <p>For example, if applications want to select a resolution smaller than a specific
-         * resolution to have better performance, a {@link ResolutionSelector} which sets this
-         * specific resolution as the max resolution can be used. Or, if applications want to
-         * select a larger resolution for a {@link Preview} which has the default max resolution
-         * of the small one of device's screen size and 1080p (1920x1080), use a
-         * {@link ResolutionSelector} with max resolution.
-         *
-         * @param resolution the max resolution limitation to choose from supported output sizes
-         *                   list.
-         * @return the current Builder.
-         */
-        @NonNull
-        public Builder setMaxResolution(@NonNull Size resolution) {
-            mMaxResolution = resolution;
-            return this;
-        }
-
-        /**
-         * Sets whether high resolutions are allowed to be selected for the use cases.
-         *
-         * <p>Calling this function allows the use case to select the high resolution output
-         * sizes if it is supported for the camera device.
-         *
-         * <p>When high resolution is enabled, if an {@link ImageCapture} with
-         * {@link ImageCapture#CAPTURE_MODE_ZERO_SHUTTER_LAG} mode is bound, the
-         * {@link ImageCapture#CAPTURE_MODE_ZERO_SHUTTER_LAG} mode is forced disabled.
-         *
-         * <p>When using the {@code camera-extensions} to enable an extension mode, even if high
-         * resolution is enabled, the supported high resolution output sizes are still excluded
-         * from the candidate resolution list.
-         *
-         * <p>When using the {@code camera-camera2} CameraX implementation, the supported
-         * high resolutions are retrieved from
-         * {@link android.hardware.camera2.params.StreamConfigurationMap#getHighResolutionOutputSizes(int)}.
-         * Be noticed that the high resolution sizes might cause the entire capture session to
-         * not meet the 20 fps frame rate. Even if only an ImageCapture use case selects a high
-         * resolution, it might still impact the FPS of the Preview, ImageAnalysis or
-         * VideoCapture use cases which are bound together. This function only takes effect on
-         * devices with
-         * {@link android.hardware.camera2.CameraCharacteristics#REQUEST_AVAILABLE_CAPABILITIES_BURST_CAPTURE}
-         * capability. For devices without
-         * {@link android.hardware.camera2.CameraCharacteristics#REQUEST_AVAILABLE_CAPABILITIES_BURST_CAPTURE}
-         * capability, all resolutions can be retrieved from
-         * {@link android.hardware.camera2.params.StreamConfigurationMap#getOutputSizes(int)},
-         * but it is not guaranteed to meet >= 20 fps for any resolution in the list.
-         *
-         * @param enabled {@code true} to allow to select high resolution for the use case.
-         * @return the current Builder.
-         */
-        @NonNull
-        public Builder setHighResolutionEnabled(boolean enabled) {
-            mIsHighResolutionEnabled = enabled;
-            return this;
-        }
-
-        /**
-         * Builds the {@link ResolutionSelector}.
-         *
-         * @return the {@link ResolutionSelector} built with the specified resolution settings.
-         */
-        @NonNull
-        public ResolutionSelector build() {
-            return new ResolutionSelector(mPreferredAspectRatio, mPreferredResolution,
-                    mSizeCoordinate, mMaxResolution, mIsHighResolutionEnabled);
-        }
-    }
-}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/UseCase.java b/camera/camera-core/src/main/java/androidx/camera/core/UseCase.java
index 362e463..3257ece 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/UseCase.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/UseCase.java
@@ -19,6 +19,9 @@
 import static androidx.camera.core.MirrorMode.MIRROR_MODE_FRONT_ON;
 import static androidx.camera.core.MirrorMode.MIRROR_MODE_OFF;
 import static androidx.camera.core.MirrorMode.MIRROR_MODE_ON;
+import static androidx.camera.core.impl.ImageOutputConfig.OPTION_RESOLUTION_SELECTOR;
+import static androidx.camera.core.impl.ImageOutputConfig.OPTION_TARGET_ASPECT_RATIO;
+import static androidx.camera.core.impl.ImageOutputConfig.OPTION_TARGET_RESOLUTION;
 import static androidx.camera.core.impl.utils.TransformUtils.within360;
 import static androidx.camera.core.processing.TargetUtils.isSuperset;
 import static androidx.core.util.Preconditions.checkArgument;
@@ -209,6 +212,16 @@
             mergedConfig = MutableOptionsBundle.create();
         }
 
+        // Removes the default resolution selector setting to go for the legacy resolution
+        // selection logic flow if applications call the legacy setTargetAspectRatio and
+        // setTargetResolution APIs to do the setting.
+        if (mUseCaseConfig.containsOption(OPTION_TARGET_ASPECT_RATIO)
+                || mUseCaseConfig.containsOption(OPTION_TARGET_RESOLUTION)) {
+            if (mergedConfig.containsOption(OPTION_RESOLUTION_SELECTOR)) {
+                mergedConfig.removeOption(OPTION_RESOLUTION_SELECTOR);
+            }
+        }
+
         // If any options need special handling, this is the place to do it. For now we'll just copy
         // over all options.
         for (Option<?> opt : mUseCaseConfig.listOptions()) {
@@ -247,7 +260,8 @@
         // Forces disable ZSL when high resolution is enabled.
         if (mergedConfig.containsOption(ImageOutputConfig.OPTION_RESOLUTION_SELECTOR)
                 && mergedConfig.retrieveOption(
-                ImageOutputConfig.OPTION_RESOLUTION_SELECTOR).isHighResolutionEnabled()) {
+                ImageOutputConfig.OPTION_RESOLUTION_SELECTOR).getHighResolutionEnabledFlags()
+                != 0) {
             mergedConfig.insertOption(UseCaseConfig.OPTION_ZSL_DISABLED, true);
         }
 
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/impl/Config.java b/camera/camera-core/src/main/java/androidx/camera/core/impl/Config.java
index 2d3ae77..67493a2 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/impl/Config.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/impl/Config.java
@@ -19,9 +19,12 @@
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.annotation.RequiresApi;
+import androidx.camera.core.impl.utils.ResolutionSelectorUtil;
+import androidx.camera.core.resolutionselector.ResolutionSelector;
 
 import com.google.auto.value.AutoValue;
 
+import java.util.Objects;
 import java.util.Set;
 
 /**
@@ -321,11 +324,23 @@
             // just copy over all options.
             for (Config.Option<?> opt : extendedConfig.listOptions()) {
                 @SuppressWarnings("unchecked") // Options/values are being copied directly
-                        Config.Option<Object> objectOpt = (Config.Option<Object>) opt;
+                Config.Option<Object> objectOpt = (Config.Option<Object>) opt;
 
-                mergedConfig.insertOption(objectOpt,
-                        extendedConfig.getOptionPriority(opt),
-                        extendedConfig.retrieveOption(objectOpt));
+                // ResolutionSelector needs special handling to merge the underlying settings.
+                if (Objects.equals(objectOpt, ImageOutputConfig.OPTION_RESOLUTION_SELECTOR)) {
+                    ResolutionSelector resolutionSelectorToOverride =
+                            (ResolutionSelector) extendedConfig.retrieveOption(objectOpt);
+                    ResolutionSelector baseResolutionSelector =
+                            (ResolutionSelector) baseConfig.retrieveOption(objectOpt);
+                    mergedConfig.insertOption(objectOpt,
+                            extendedConfig.getOptionPriority(opt),
+                            ResolutionSelectorUtil.overrideResolutionSelectors(
+                                    baseResolutionSelector, resolutionSelectorToOverride));
+                } else {
+                    mergedConfig.insertOption(objectOpt,
+                            extendedConfig.getOptionPriority(opt),
+                            extendedConfig.retrieveOption(objectOpt));
+                }
             }
         }
 
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/impl/ImageOutputConfig.java b/camera/camera-core/src/main/java/androidx/camera/core/impl/ImageOutputConfig.java
index a2168c3..390e103 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/impl/ImageOutputConfig.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/impl/ImageOutputConfig.java
@@ -29,7 +29,7 @@
 import androidx.annotation.RequiresApi;
 import androidx.camera.core.AspectRatio;
 import androidx.camera.core.MirrorMode;
-import androidx.camera.core.ResolutionSelector;
+import androidx.camera.core.resolutionselector.ResolutionSelector;
 
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/impl/utils/ResolutionSelectorUtil.java b/camera/camera-core/src/main/java/androidx/camera/core/impl/utils/ResolutionSelectorUtil.java
new file mode 100644
index 0000000..da7aaa3
--- /dev/null
+++ b/camera/camera-core/src/main/java/androidx/camera/core/impl/utils/ResolutionSelectorUtil.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.core.impl.utils;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+import androidx.camera.core.resolutionselector.ResolutionSelector;
+
+/**
+ * Utility class for resolution selector related operations.
+ */
+@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
+public class ResolutionSelectorUtil {
+    private ResolutionSelectorUtil() {
+    }
+
+    /**
+     * Merges two resolution selectors.
+     *
+     * @param baseResolutionSelector       the base resolution selector.
+     * @param resolutionSelectorToOverride the resolution selector that the inside settings can
+     *                                     override the corresponding setting in the base
+     *                                     resolution selector.
+     * @return {@code null} if both the input resolution selectors are null. Otherwise, returns
+     * the merged resolution selector.
+     */
+    @Nullable
+    public static ResolutionSelector overrideResolutionSelectors(
+            @Nullable ResolutionSelector baseResolutionSelector,
+            @Nullable ResolutionSelector resolutionSelectorToOverride) {
+        if (resolutionSelectorToOverride == null) {
+            return baseResolutionSelector;
+        } else if (baseResolutionSelector == null) {
+            return resolutionSelectorToOverride;
+        }
+
+        ResolutionSelector.Builder builder =
+                ResolutionSelector.Builder.fromResolutionSelector(baseResolutionSelector);
+
+        if (resolutionSelectorToOverride.getAspectRatioStrategy() != null) {
+            builder.setAspectRatioStrategy(resolutionSelectorToOverride.getAspectRatioStrategy());
+        }
+
+        if (resolutionSelectorToOverride.getResolutionStrategy() != null) {
+            builder.setResolutionStrategy(resolutionSelectorToOverride.getResolutionStrategy());
+        }
+
+        if (resolutionSelectorToOverride.getResolutionFilter() != null) {
+            builder.setResolutionFilter(resolutionSelectorToOverride.getResolutionFilter());
+        }
+
+        if (resolutionSelectorToOverride.getHighResolutionEnabledFlags() != 0) {
+            builder.setHighResolutionEnabledFlags(
+                    resolutionSelectorToOverride.getHighResolutionEnabledFlags());
+        }
+
+        return builder.build();
+    }
+}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/internal/SupportedOutputSizesSorter.java b/camera/camera-core/src/main/java/androidx/camera/core/internal/SupportedOutputSizesSorter.java
index ad15a23..4c44a62 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/internal/SupportedOutputSizesSorter.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/internal/SupportedOutputSizesSorter.java
@@ -26,41 +26,58 @@
 import android.util.Pair;
 import android.util.Rational;
 import android.util.Size;
+import android.view.Surface;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.annotation.RequiresApi;
 import androidx.camera.core.AspectRatio;
+import androidx.camera.core.CameraSelector;
 import androidx.camera.core.Logger;
-import androidx.camera.core.ResolutionSelector;
 import androidx.camera.core.impl.CameraInfoInternal;
 import androidx.camera.core.impl.ImageOutputConfig;
 import androidx.camera.core.impl.UseCaseConfig;
 import androidx.camera.core.impl.utils.AspectRatioUtil;
+import androidx.camera.core.impl.utils.CameraOrientationUtil;
 import androidx.camera.core.impl.utils.CompareSizesByArea;
-import androidx.camera.core.internal.utils.SizeUtil;
-import androidx.core.util.Preconditions;
+import androidx.camera.core.resolutionselector.AspectRatioStrategy;
+import androidx.camera.core.resolutionselector.ResolutionFilter;
+import androidx.camera.core.resolutionselector.ResolutionSelector;
+import androidx.camera.core.resolutionselector.ResolutionStrategy;
 
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.HashMap;
+import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
 
 /**
- * A class used to sort the supported output sizes according to the use case configs
+ * The supported output sizes collector to help collect the available resolution candidate list
+ * according to the use case config and the following settings in {@link ResolutionSelector}:
+ *
+ * <ul>
+ *   <li>Aspect ratio strategy
+ *   <li>Resolution strategy
+ *   <li>Custom resolution filter
+ *   <li>High resolution enabled flags
+ * </ul>
  */
 @RequiresApi(21)
 public class SupportedOutputSizesSorter {
     private static final String TAG = "SupportedOutputSizesCollector";
     private final CameraInfoInternal mCameraInfoInternal;
+    private final int mSensorOrientation;
+    private final int mLensFacing;
     private final Rational mFullFovRatio;
     private final boolean mIsSensorLandscapeResolution;
     private final SupportedOutputSizesSorterLegacy mSupportedOutputSizesSorterLegacy;
 
     public SupportedOutputSizesSorter(@NonNull CameraInfoInternal cameraInfoInternal) {
         mCameraInfoInternal = cameraInfoInternal;
+        mSensorOrientation = mCameraInfoInternal.getSensorRotationDegrees();
+        mLensFacing = mCameraInfoInternal.getLensFacing();
         mFullFovRatio = calculateFullFovRatio(mCameraInfoInternal);
         // Determines the sensor resolution orientation info by the full FOV ratio.
         mIsSensorLandscapeResolution = mFullFovRatio != null ? mFullFovRatio.getNumerator()
@@ -86,6 +103,13 @@
         return new Rational(maxSize.getWidth(), maxSize.getHeight());
     }
 
+    /**
+     * Returns the sorted output sizes according to the use case config.
+     *
+     * <p>If ResolutionSelector is specified in the use case config, the output sizes will be
+     * sorted according to the ResolutionSelector setting and logic. Otherwise, the output sizes
+     * will be sorted according to the legacy resolution API settings and logic.
+     */
     @NonNull
     public List<Size> getSortedSupportedOutputSizes(@NonNull UseCaseConfig<?> useCaseConfig) {
         ImageOutputConfig imageOutputConfig = (ImageOutputConfig) useCaseConfig;
@@ -96,50 +120,16 @@
             return customOrderedResolutions;
         }
 
-        // Retrieves the resolution candidate list according to the use case config if
-        List<Size> resolutionCandidateList = getResolutionCandidateList(useCaseConfig);
-
         ResolutionSelector resolutionSelector = imageOutputConfig.getResolutionSelector(null);
 
         if (resolutionSelector == null) {
             return mSupportedOutputSizesSorterLegacy.sortSupportedOutputSizes(
-                    resolutionCandidateList, useCaseConfig);
+                    getResolutionCandidateList(useCaseConfig), useCaseConfig);
         } else {
-            Size miniBoundingSize = resolutionSelector.getPreferredResolution();
-            if (miniBoundingSize == null) {
-                miniBoundingSize = imageOutputConfig.getDefaultResolution(null);
-            }
-            return sortSupportedOutputSizesByResolutionSelector(resolutionCandidateList,
-                    resolutionSelector, miniBoundingSize);
+            return sortSupportedOutputSizesByResolutionSelector(useCaseConfig);
         }
     }
 
-    @NonNull
-    private List<Size> getResolutionCandidateList(@NonNull UseCaseConfig<?> useCaseConfig) {
-        int imageFormat = useCaseConfig.getInputFormat();
-        ImageOutputConfig imageOutputConfig = (ImageOutputConfig) useCaseConfig;
-        // Tries to get the custom supported resolutions list if it is set
-        List<Size> resolutionCandidateList = getCustomizedSupportedResolutionsFromConfig(
-                imageFormat, imageOutputConfig);
-
-        // Tries to get the supported output sizes from the CameraInfoInternal if both custom
-        // ordered and supported resolutions lists are not set.
-        if (resolutionCandidateList == null) {
-            resolutionCandidateList = mCameraInfoInternal.getSupportedResolutions(imageFormat);
-        }
-
-        // Appends high resolution output sizes if high resolution is enabled by ResolutionSelector
-        if (imageOutputConfig.getResolutionSelector(null) != null
-                && imageOutputConfig.getResolutionSelector().isHighResolutionEnabled()) {
-            List<Size> allSizesList = new ArrayList<>();
-            allSizesList.addAll(resolutionCandidateList);
-            allSizesList.addAll(mCameraInfoInternal.getSupportedHighResolutions(imageFormat));
-            return allSizesList;
-        }
-
-        return resolutionCandidateList;
-    }
-
     /**
      * Retrieves the customized supported resolutions from the use case config.
      *
@@ -171,148 +161,56 @@
     }
 
     /**
-     * Sorts the resolution candidate list by the following steps:
+     * Sorts the resolution candidate list according to the ResolutionSelector API logic.
      *
-     * 1. Filters out the candidate list according to the max resolution.
-     * 2. Sorts the candidate list according to ResolutionSelector strategies.
+     * <ol>
+     *   <li>Collects the output sizes
+     *     <ul>
+     *       <li>Applies the high resolution settings
+     *     </ul>
+     *   <li>Applies the aspect ratio strategy
+     *     <ul>
+     *       <li>Applies the aspect ratio strategy fallback rule
+     *     </ul>
+     *   <li>Applies the resolution strategy
+     *     <ul>
+     *       <li>Applies the resolution strategy fallback rule
+     *     </ul>
+     *   <li>Applies the resolution filter
+     * </ol>
+     *
+     * @return a size list which has been filtered and sorted by the specified resolution
+     * selector settings.
+     * @throws IllegalArgumentException if the specified resolution filter returns any size which
+     *                                  is not included in the provided supported size list.
      */
     @NonNull
     private List<Size> sortSupportedOutputSizesByResolutionSelector(
-            @NonNull List<Size> resolutionCandidateList,
-            @NonNull ResolutionSelector resolutionSelector,
-            @Nullable Size miniBoundingSize) {
-        if (resolutionCandidateList.isEmpty()) {
-            return resolutionCandidateList;
+            @NonNull UseCaseConfig<?> useCaseConfig) {
+        ResolutionSelector resolutionSelector =
+                ((ImageOutputConfig) useCaseConfig).getResolutionSelector();
+
+        // Retrieves the normal supported output sizes.
+        List<Size> resolutionCandidateList = getResolutionCandidateList(useCaseConfig);
+
+        // Applies the high resolution settings onto the resolution candidate list.
+        if (!useCaseConfig.isHigResolutionDisabled(false)) {
+            resolutionCandidateList = applyHighResolutionSettings(resolutionCandidateList,
+                    resolutionSelector, useCaseConfig.getInputFormat());
         }
 
-        List<Size> descendingSizeList = new ArrayList<>(resolutionCandidateList);
+        // Applies the aspect ratio strategy onto the resolution candidate list.
+        LinkedHashMap<Rational, List<Size>> aspectRatioSizeListMap =
+                applyAspectRatioStrategy(resolutionCandidateList,
+                        resolutionSelector.getAspectRatioStrategy());
 
-        // Sort the result sizes. The Comparator result must be reversed to have a descending
-        // order result.
-        Collections.sort(descendingSizeList, new CompareSizesByArea(true));
+        // Applies the resolution strategy onto the resolution candidate list.
+        applyResolutionStrategy(aspectRatioSizeListMap, resolutionSelector.getResolutionStrategy());
 
-        // 1. Filters out the candidate list according to the min size bound and max resolution.
-        List<Size> filteredSizeList = filterOutResolutionCandidateListByMaxResolutionSetting(
-                descendingSizeList, resolutionSelector);
-
-        // 2. Sorts the candidate list according to the rules of new Resolution API.
-        return sortResolutionCandidateListByTargetAspectRatioAndResolutionSettings(
-                filteredSizeList, resolutionSelector, miniBoundingSize);
-
-    }
-
-    /**
-     * Filters out the resolution candidate list by the max resolution setting.
-     *
-     * The input size list should have been sorted in descending order.
-     */
-    private static List<Size> filterOutResolutionCandidateListByMaxResolutionSetting(
-            @NonNull List<Size> resolutionCandidateList,
-            @NonNull ResolutionSelector resolutionSelector) {
-        // Retrieves the max resolution setting. When ResolutionSelector is used, all resolution
-        // selection logic should depend on ResolutionSelector's settings.
-        Size maxResolution = resolutionSelector.getMaxResolution();
-
-        if (maxResolution == null) {
-            return resolutionCandidateList;
-        }
-
-        // Filter out the resolution candidate list by the max resolution. Sizes that any edge
-        // exceeds the max resolution will be filtered out.
+        // Collects all sizes from the sorted aspect ratio size groups into the final sorted list.
         List<Size> resultList = new ArrayList<>();
-        for (Size outputSize : resolutionCandidateList) {
-            if (!SizeUtil.isLongerInAnyEdge(outputSize, maxResolution)) {
-                resultList.add(outputSize);
-            }
-        }
-
-        if (resultList.isEmpty()) {
-            throw new IllegalArgumentException(
-                    "Resolution candidate list is empty after filtering out by the settings!");
-        }
-
-        return resultList;
-    }
-
-    /**
-     * Sorts the resolution candidate list according to the new ResolutionSelector API logic.
-     *
-     * The list will be sorted by the following order:
-     * 1. size of preferred resolution
-     * 2. a resolution with preferred aspect ratio, is not smaller than, and is closest to the
-     * preferred resolution.
-     * 3. resolutions with preferred aspect ratio and is smaller than the preferred resolution
-     * size in descending order of resolution area size.
-     * 4. Other sizes sorted by CompareAspectRatiosByMappingAreaInFullFovAspectRatioSpace and
-     * area size.
-     */
-    @NonNull
-    private List<Size> sortResolutionCandidateListByTargetAspectRatioAndResolutionSettings(
-            @NonNull List<Size> resolutionCandidateList,
-            @NonNull ResolutionSelector resolutionSelector,
-            @Nullable Size miniBoundingSize) {
-        Rational aspectRatio = getTargetAspectRatioRationalValue(
-                resolutionSelector.getPreferredAspectRatio(), mIsSensorLandscapeResolution);
-        Preconditions.checkNotNull(aspectRatio, "ResolutionSelector should also have aspect ratio"
-                + " value.");
-
-        Size targetSize = resolutionSelector.getPreferredResolution();
-        List<Size> resultList = sortResolutionCandidateListByTargetAspectRatioAndSize(
-                resolutionCandidateList, aspectRatio, miniBoundingSize);
-
-        // Moves the target size to the first position if it exists in the resolution candidate
-        // list.
-        if (resultList.contains(targetSize)) {
-            resultList.remove(targetSize);
-            resultList.add(0, targetSize);
-        }
-
-        return resultList;
-    }
-
-    /**
-     * Sorts the resolution candidate list according to the target aspect ratio and size settings.
-     *
-     * 1. The resolution candidate list will be grouped by aspect ratio.
-     * 2. Moves the smallest size larger than the mini bounding size to the first position for each
-     * aspect ratio sizes group.
-     * 3. The aspect ratios of groups will be sorted against to the target aspect ratio setting by
-     * CompareAspectRatiosByMappingAreaInFullFovAspectRatioSpace.
-     * 4. Concatenate all sizes as the result list
-     */
-    @NonNull
-    private List<Size> sortResolutionCandidateListByTargetAspectRatioAndSize(
-            @NonNull List<Size> resolutionCandidateList, @NonNull Rational aspectRatio,
-            @Nullable Size miniBoundingSize) {
-        // Rearrange the supported size to put the ones with the same aspect ratio in the front
-        // of the list and put others in the end from large to small. Some low end devices may
-        // not able to get an supported resolution that match the preferred aspect ratio.
-
-        // Group output sizes by aspect ratio.
-        Map<Rational, List<Size>> aspectRatioSizeListMap =
-                groupSizesByAspectRatio(resolutionCandidateList);
-
-        // If the target resolution is set, use it to remove unnecessary larger sizes.
-        if (miniBoundingSize != null) {
-            // Sorts sizes from each aspect ratio size list
-            for (Rational key : aspectRatioSizeListMap.keySet()) {
-                List<Size> sortedResult = sortSupportedSizesByMiniBoundingSize(
-                        aspectRatioSizeListMap.get(key), miniBoundingSize);
-                aspectRatioSizeListMap.put(key, sortedResult);
-            }
-        }
-
-        // Sort the aspect ratio key set by the target aspect ratio.
-        List<Rational> aspectRatios = new ArrayList<>(aspectRatioSizeListMap.keySet());
-        Collections.sort(aspectRatios,
-                new AspectRatioUtil.CompareAspectRatiosByMappingAreaInFullFovAspectRatioSpace(
-                        aspectRatio, mFullFovRatio));
-
-        List<Size> resultList = new ArrayList<>();
-
-        // Put available sizes into final result list by aspect ratio distance to target ratio.
-        for (Rational rational : aspectRatios) {
-            for (Size size : aspectRatioSizeListMap.get(rational)) {
+        for (List<Size> sortedSizes : aspectRatioSizeListMap.values()) {
+            for (Size size : sortedSizes) {
                 // A size may exist in multiple groups in mod16 condition. Keep only one in
                 // the final list.
                 if (!resultList.contains(size)) {
@@ -321,7 +219,322 @@
             }
         }
 
-        return resultList;
+        // Applies the resolution filter onto the resolution candidate list.
+        return applyResolutionFilter(resultList, resolutionSelector.getResolutionFilter(),
+                ((ImageOutputConfig) useCaseConfig).getTargetRotation(Surface.ROTATION_0));
+    }
+
+    /**
+     * Returns the normal supported output sizes.
+     *
+     * <p>When using camera-camera2 implementation, the output sizes are retrieved via
+     * StreamConfigurationMap#getOutputSizes().
+     *
+     * @return the resolution candidate list sorted in descending order.
+     */
+    @NonNull
+    private List<Size> getResolutionCandidateList(@NonNull UseCaseConfig<?> useCaseConfig) {
+        int imageFormat = useCaseConfig.getInputFormat();
+        ImageOutputConfig imageOutputConfig = (ImageOutputConfig) useCaseConfig;
+        // Tries to get the custom supported resolutions list if it is set
+        List<Size> resolutionCandidateList = getCustomizedSupportedResolutionsFromConfig(
+                imageFormat, imageOutputConfig);
+
+        // Tries to get the supported output sizes from the CameraInfoInternal if both custom
+        // ordered and supported resolutions lists are not set.
+        if (resolutionCandidateList == null) {
+            resolutionCandidateList = mCameraInfoInternal.getSupportedResolutions(imageFormat);
+        }
+
+        Collections.sort(resolutionCandidateList, new CompareSizesByArea(true));
+
+        return resolutionCandidateList;
+    }
+
+    /**
+     * Appends the high resolution supported output sizes according to the high resolution settings.
+     *
+     * <p>When using camera-camera2 implementation, the output sizes are retrieved via
+     * StreamConfigurationMap#getHighResolutionOutputSizes().
+     *
+     * @param resolutionCandidateList the supported size list which contains only normal output
+     *                                sizes.
+     * @param resolutionSelector      the specified resolution selector.
+     * @param imageFormat             the desired image format for the target use case.
+     * @return the resolution candidate list including the high resolution output sizes sorted in
+     * descending order.
+     */
+    @NonNull
+    private List<Size> applyHighResolutionSettings(@NonNull List<Size> resolutionCandidateList,
+            @NonNull ResolutionSelector resolutionSelector, int imageFormat) {
+        // Appends high resolution output sizes if high resolution is enabled by ResolutionSelector
+        if (resolutionSelector.getHighResolutionEnabledFlags() != 0) {
+            List<Size> allSizesList = new ArrayList<>();
+            allSizesList.addAll(resolutionCandidateList);
+            allSizesList.addAll(mCameraInfoInternal.getSupportedHighResolutions(imageFormat));
+            Collections.sort(allSizesList, new CompareSizesByArea(true));
+            return allSizesList;
+        }
+
+        return resolutionCandidateList;
+    }
+
+    /**
+     * Applies the aspect ratio strategy onto the input resolution candidate list.
+     *
+     * @param resolutionCandidateList the supported sizes list which has been sorted in
+     *                                descending order.
+     * @param aspectRatioStrategy     the specified aspect ratio strategy.
+     * @return an aspect ratio to size list linked hash map which the aspect ratio fallback rule
+     * is applied and is sorted against the preferred aspect ratio.
+     */
+    @NonNull
+    private LinkedHashMap<Rational, List<Size>> applyAspectRatioStrategy(
+            @NonNull List<Size> resolutionCandidateList,
+            @NonNull AspectRatioStrategy aspectRatioStrategy) {
+        // Group output sizes by aspect ratio.
+        Map<Rational, List<Size>> aspectRatioSizeListMap =
+                groupSizesByAspectRatio(resolutionCandidateList);
+
+        // Applies the aspect ratio fallback rule
+        return applyAspectRatioStrategyFallbackRule(aspectRatioSizeListMap, aspectRatioStrategy);
+    }
+
+    /**
+     * Applies the aspect ratio strategy fallback rule to the aspect ratio to size list map.
+     *
+     * @param sizeGroupsMap       the aspect ratio to size list map. The size list should have been
+     *                            sorted in descending order.
+     * @param aspectRatioStrategy the specified aspect ratio strategy.
+     * @return an aspect ratio to size list linked hash map which the aspect ratio fallback rule
+     * is applied and is sorted against the preferred aspect ratio.
+     */
+    private LinkedHashMap<Rational, List<Size>> applyAspectRatioStrategyFallbackRule(
+            @NonNull Map<Rational, List<Size>> sizeGroupsMap,
+            @NonNull AspectRatioStrategy aspectRatioStrategy) {
+        Rational aspectRatio = getTargetAspectRatioRationalValue(
+                aspectRatioStrategy.getPreferredAspectRatio(), mIsSensorLandscapeResolution);
+
+        // Remove items of all other aspect ratios if the fallback rule is AspectRatioStrategy
+        // .FALLBACK_RULE_NONE
+        if (aspectRatioStrategy.getFallbackRule() == AspectRatioStrategy.FALLBACK_RULE_NONE) {
+            Rational preferredAspectRatio = getTargetAspectRatioRationalValue(
+                    aspectRatioStrategy.getPreferredAspectRatio(), mIsSensorLandscapeResolution);
+            for (Rational ratio : new ArrayList<>(sizeGroupsMap.keySet())) {
+                if (!ratio.equals(preferredAspectRatio)) {
+                    sizeGroupsMap.remove(ratio);
+                }
+            }
+        }
+
+        // Sorts the aspect ratio key set by the preferred aspect ratio.
+        List<Rational> aspectRatios = new ArrayList<>(sizeGroupsMap.keySet());
+        Collections.sort(aspectRatios,
+                new AspectRatioUtil.CompareAspectRatiosByMappingAreaInFullFovAspectRatioSpace(
+                        aspectRatio, mFullFovRatio));
+
+        // Stores the size groups into LinkedHashMap to keep the order
+        LinkedHashMap<Rational, List<Size>> sortedAspectRatioSizeListMap = new LinkedHashMap<>();
+        for (Rational ratio : aspectRatios) {
+            sortedAspectRatioSizeListMap.put(ratio, sizeGroupsMap.get(ratio));
+        }
+
+        return sortedAspectRatioSizeListMap;
+    }
+
+    /**
+     * Applies the resolution strategy onto the aspect ratio to size list linked hash map.
+     *
+     * <p>The resolution fallback rule is applied to filter out and sort the sizes in the
+     * underlying size list.
+     *
+     * @param sortedAspectRatioSizeListMap the aspect ratio to size list linked hash map. The
+     *                                     entries order should not be changed.
+     * @param resolutionStrategy           the resolution strategy to sort the candidate
+     *                                     resolutions.
+     */
+    private static void applyResolutionStrategy(
+            @NonNull LinkedHashMap<Rational, List<Size>> sortedAspectRatioSizeListMap,
+            @Nullable ResolutionStrategy resolutionStrategy) {
+        if (resolutionStrategy == null) {
+            return;
+        }
+
+        // Applies the resolution strategy with the specified fallback rule
+        for (Rational key : sortedAspectRatioSizeListMap.keySet()) {
+            applyResolutionStrategyFallbackRule(sortedAspectRatioSizeListMap.get(key),
+                    resolutionStrategy);
+        }
+    }
+
+    /**
+     * Applies the resolution strategy fallback rule to the size list.
+     *
+     * @param supportedSizesList the supported sizes list which has been sorted in descending order.
+     * @param resolutionStrategy the resolution strategy to sort the candidate resolutions.
+     */
+    private static void applyResolutionStrategyFallbackRule(
+            @NonNull List<Size> supportedSizesList,
+            @NonNull ResolutionStrategy resolutionStrategy) {
+        if (supportedSizesList.isEmpty()) {
+            return;
+        }
+        Integer fallbackRule = resolutionStrategy.getFallbackRule();
+
+        if (fallbackRule == null) {
+            // Do nothing for HIGHEST_AVAILABLE_STRATEGY case.
+            return;
+        }
+
+        Size boundSize = resolutionStrategy.getBoundSize();
+
+        switch (fallbackRule) {
+            case ResolutionStrategy.FALLBACK_RULE_NONE:
+                sortSupportedSizesByFallbackRuleNone(supportedSizesList, boundSize);
+                break;
+            case ResolutionStrategy.FALLBACK_RULE_CLOSEST_HIGHER_THEN_LOWER:
+                sortSupportedSizesByFallbackRuleClosestHigherThenLower(supportedSizesList,
+                        boundSize, true);
+                break;
+            case ResolutionStrategy.FALLBACK_RULE_CLOSEST_HIGHER:
+                sortSupportedSizesByFallbackRuleClosestHigherThenLower(supportedSizesList,
+                        boundSize, false);
+                break;
+            case ResolutionStrategy.FALLBACK_RULE_CLOSEST_LOWER_THEN_HIGHER:
+                sortSupportedSizesByFallbackRuleClosestLowerThenHigher(supportedSizesList,
+                        boundSize, true);
+                break;
+            case ResolutionStrategy.FALLBACK_RULE_CLOSEST_LOWER:
+                sortSupportedSizesByFallbackRuleClosestLowerThenHigher(supportedSizesList,
+                        boundSize, false);
+                break;
+            default:
+                break;
+        }
+    }
+
+    /**
+     * Applies the resolution filtered to the sorted output size list.
+     *
+     * @param sizeList         the supported size list which has been filtered and sorted by the
+     *                         specified aspect ratio, resolution strategies.
+     * @param resolutionFilter the specified resolution filter.
+     * @param targetRotation   the use case target rotation info
+     * @return the result size list applied the specified resolution filter.
+     * @throws IllegalArgumentException if the specified resolution filter returns any size which
+     *                                  is not included in the provided supported size list.
+     */
+    @NonNull
+    private List<Size> applyResolutionFilter(@NonNull List<Size> sizeList,
+            @Nullable ResolutionFilter resolutionFilter,
+            @ImageOutputConfig.RotationValue int targetRotation) {
+        if (resolutionFilter == null) {
+            return sizeList;
+        }
+
+        // Invokes ResolutionFilter#filter() to filter/sort and return the result if it is
+        // specified.
+        int destRotationDegrees = CameraOrientationUtil.surfaceRotationToDegrees(
+                targetRotation);
+        int rotationDegrees =
+                CameraOrientationUtil.getRelativeImageRotation(destRotationDegrees,
+                        mSensorOrientation,
+                        mLensFacing == CameraSelector.LENS_FACING_BACK);
+        List<Size> filteredResultList = resolutionFilter.filter(new ArrayList<>(sizeList),
+                rotationDegrees);
+        if (sizeList.containsAll(filteredResultList)) {
+            return filteredResultList;
+        } else {
+            throw new IllegalArgumentException("The returned sizes list of the resolution "
+                    + "filter must be a subset of the provided sizes list.");
+        }
+    }
+
+    /**
+     * Sorts the size list for {@link ResolutionStrategy#FALLBACK_RULE_NONE}.
+     *
+     * @param supportedSizesList the supported sizes list which has been sorted in descending order.
+     * @param boundSize          the resolution strategy bound size.
+     */
+    private static void sortSupportedSizesByFallbackRuleNone(
+            @NonNull List<Size> supportedSizesList, @NonNull Size boundSize) {
+        boolean containsBoundSize = supportedSizesList.contains(boundSize);
+        supportedSizesList.clear();
+        if (containsBoundSize) {
+            supportedSizesList.add(boundSize);
+        }
+    }
+
+    /**
+     * Sorts the size list for {@link ResolutionStrategy#FALLBACK_RULE_CLOSEST_HIGHER_THEN_LOWER}
+     * or {@link ResolutionStrategy#FALLBACK_RULE_CLOSEST_HIGHER}.
+     *
+     * @param supportedSizesList the supported sizes list which has been sorted in descending order.
+     * @param boundSize          the resolution strategy bound size.
+     * @param keepLowerSizes     keeps the sizes lower than the bound size in the result list if
+     *                           this is {@code true}.
+     */
+    static void sortSupportedSizesByFallbackRuleClosestHigherThenLower(
+            @NonNull List<Size> supportedSizesList, @NonNull Size boundSize,
+            boolean keepLowerSizes) {
+        List<Size> lowerSizes = new ArrayList<>();
+
+        for (int i = supportedSizesList.size() - 1; i >= 0; i--) {
+            Size outputSize = supportedSizesList.get(i);
+            if (outputSize.getWidth() < boundSize.getWidth()
+                    || outputSize.getHeight() < boundSize.getHeight()) {
+                // The supportedSizesList is in descending order. Checking and put the
+                // bounding-below size at position 0 so that the largest smaller resolution
+                // will be put in the first position finally.
+                lowerSizes.add(0, outputSize);
+            } else {
+                break;
+            }
+        }
+        // Removes the lower sizes from the list
+        supportedSizesList.removeAll(lowerSizes);
+        // Reverses the list so that the smallest larger resolution will be put in the first
+        // position.
+        Collections.reverse(supportedSizesList);
+        if (keepLowerSizes) {
+            // Appends the lower sizes to the tail
+            supportedSizesList.addAll(lowerSizes);
+        }
+    }
+
+    /**
+     * Sorts the size list for {@link ResolutionStrategy#FALLBACK_RULE_CLOSEST_LOWER_THEN_HIGHER}
+     * or {@link ResolutionStrategy#FALLBACK_RULE_CLOSEST_LOWER}.
+     *
+     * @param supportedSizesList the supported sizes list which has been sorted in descending order.
+     * @param boundSize          the resolution strategy bound size.
+     * @param keepHigherSizes    keeps the sizes higher than the bound size in the result list if
+     *                           this is {@code true}.
+     */
+    private static void sortSupportedSizesByFallbackRuleClosestLowerThenHigher(
+            @NonNull List<Size> supportedSizesList, @NonNull Size boundSize,
+            boolean keepHigherSizes) {
+        List<Size> higherSizes = new ArrayList<>();
+
+        for (int i = 0; i < supportedSizesList.size(); i++) {
+            Size outputSize = supportedSizesList.get(i);
+            if (outputSize.getWidth() > boundSize.getWidth()
+                    || outputSize.getHeight() > boundSize.getHeight()) {
+                // The supportedSizesList is in descending order. Checking and put the
+                // bounding-above size at position 0 so that the smallest larger resolution
+                // will be put in the first position finally.
+                higherSizes.add(0, outputSize);
+            } else {
+                // Breaks the for-loop to keep the equal-to or lower sizes in the list.
+                break;
+            }
+        }
+        // Removes the higher sizes from the list
+        supportedSizesList.removeAll(higherSizes);
+        if (keepHigherSizes) {
+            // Appends the higher sizes to the tail
+            supportedSizesList.addAll(higherSizes);
+        }
     }
 
     /**
@@ -391,42 +604,8 @@
     }
 
     /**
-     * Removes unnecessary sizes by target size.
-     *
-     * <p>If the target resolution is set, a size that is equal to or closest to the target
-     * resolution will be selected. If the list includes more than one size equal to or larger
-     * than the target resolution, only one closest size needs to be kept. The other larger sizes
-     * can be removed so that they won't be selected to use.
-     *
-     * @param supportedSizesList The list should have been sorted in descending order.
-     * @param miniBoundingSize   The target size used to remove unnecessary sizes.
+     * Groups the input sizes into an aspect ratio to size list map.
      */
-    static List<Size> sortSupportedSizesByMiniBoundingSize(@NonNull List<Size> supportedSizesList,
-            @NonNull Size miniBoundingSize) {
-        if (supportedSizesList.isEmpty()) {
-            return supportedSizesList;
-        }
-
-        List<Size> resultList = new ArrayList<>();
-
-        // Get the index of the item that is equal to or closest to the target size.
-        for (int i = 0; i < supportedSizesList.size(); i++) {
-            Size outputSize = supportedSizesList.get(i);
-            if (outputSize.getWidth() >= miniBoundingSize.getWidth()
-                    && outputSize.getHeight() >= miniBoundingSize.getHeight()) {
-                // The supportedSizesList is in descending order. Checking and put the
-                // mini-bounding-above size at position 0 so that the smallest larger resolution
-                // will be put in the first position finally.
-                resultList.add(0, outputSize);
-            } else {
-                // Appends the remaining smaller sizes in descending order.
-                resultList.add(outputSize);
-            }
-        }
-
-        return resultList;
-    }
-
     static Map<Rational, List<Size>> groupSizesByAspectRatio(@NonNull List<Size> sizes) {
         Map<Rational, List<Size>> aspectRatioSizeListMap = new HashMap<>();
 
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/internal/SupportedOutputSizesSorterLegacy.java b/camera/camera-core/src/main/java/androidx/camera/core/internal/SupportedOutputSizesSorterLegacy.java
index 2329794..7483407 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/internal/SupportedOutputSizesSorterLegacy.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/internal/SupportedOutputSizesSorterLegacy.java
@@ -19,7 +19,7 @@
 import static androidx.camera.core.impl.utils.AspectRatioUtil.hasMatchingAspectRatio;
 import static androidx.camera.core.internal.SupportedOutputSizesSorter.getResolutionListGroupingAspectRatioKeys;
 import static androidx.camera.core.internal.SupportedOutputSizesSorter.groupSizesByAspectRatio;
-import static androidx.camera.core.internal.SupportedOutputSizesSorter.sortSupportedSizesByMiniBoundingSize;
+import static androidx.camera.core.internal.SupportedOutputSizesSorter.sortSupportedSizesByFallbackRuleClosestHigherThenLower;
 import static androidx.camera.core.internal.utils.SizeUtil.RESOLUTION_VGA;
 import static androidx.camera.core.internal.utils.SizeUtil.RESOLUTION_ZERO;
 import static androidx.camera.core.internal.utils.SizeUtil.getArea;
@@ -141,7 +141,8 @@
 
             // If the target resolution is set, use it to sort the sizes list.
             if (targetSize != null) {
-                resultSizeList = sortSupportedSizesByMiniBoundingSize(resultSizeList, targetSize);
+                sortSupportedSizesByFallbackRuleClosestHigherThenLower(resultSizeList, targetSize,
+                        true);
             }
         } else {
             // Rearrange the supported size to put the ones with the same aspect ratio in the front
@@ -151,13 +152,11 @@
             // Group output sizes by aspect ratio.
             aspectRatioSizeListMap = groupSizesByAspectRatio(filteredSizeList);
 
-            // If the target resolution is set, use it to remove unnecessary larger sizes.
+            // If the target resolution is set, sort the sizes against it.
             if (targetSize != null) {
-                // Remove unnecessary larger sizes from each aspect ratio size list
                 for (Rational key : aspectRatioSizeListMap.keySet()) {
-                    List<Size> sortedResult = sortSupportedSizesByMiniBoundingSize(
-                            aspectRatioSizeListMap.get(key), targetSize);
-                    aspectRatioSizeListMap.put(key, sortedResult);
+                    sortSupportedSizesByFallbackRuleClosestHigherThenLower(
+                            aspectRatioSizeListMap.get(key), targetSize, true);
                 }
             }
 
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/processing/SurfaceEdge.java b/camera/camera-core/src/main/java/androidx/camera/core/processing/SurfaceEdge.java
index e71e058..77a1cc3 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/processing/SurfaceEdge.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/processing/SurfaceEdge.java
@@ -315,7 +315,7 @@
     @NonNull
     public ListenableFuture<SurfaceOutput> createSurfaceOutputFuture(@NonNull Size inputSize,
             @CameraEffect.Formats int format, @NonNull Rect cropRect, int rotationDegrees,
-            boolean mirroring, @NonNull CameraInternal cameraInternal) {
+            boolean mirroring, @Nullable CameraInternal cameraInternal) {
         checkMainThread();
         checkNotClosed();
         checkAndSetHasConsumer();
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/processing/SurfaceOutputImpl.java b/camera/camera-core/src/main/java/androidx/camera/core/processing/SurfaceOutputImpl.java
index 177090e..17715d6 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/processing/SurfaceOutputImpl.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/processing/SurfaceOutputImpl.java
@@ -20,6 +20,7 @@
 import static androidx.camera.core.impl.utils.TransformUtils.getRectToRect;
 import static androidx.camera.core.impl.utils.TransformUtils.rotateSize;
 import static androidx.camera.core.impl.utils.TransformUtils.sizeToRectF;
+import static androidx.core.util.Preconditions.checkState;
 
 import android.graphics.Rect;
 import android.graphics.RectF;
@@ -76,7 +77,7 @@
     private final float[] mAdditionalTransform = new float[16];
     // The inverted value of SurfaceTexture#getTransformMatrix()
     @NonNull
-    private final float[] mInvertedCameraTransform = new float[16];
+    private final float[] mInvertedTextureTransform = new float[16];
     @GuardedBy("mLock")
     @Nullable
     private Consumer<Event> mEventListener;
@@ -91,6 +92,8 @@
     @NonNull
     private final ListenableFuture<Void> mCloseFuture;
     private CallbackToFutureAdapter.Completer<Void> mCloseFutureCompleter;
+    @Nullable
+    private CameraInternal mCameraInternal;
 
     SurfaceOutputImpl(
             @NonNull Surface surface,
@@ -101,7 +104,7 @@
             @NonNull Rect inputCropRect,
             int rotationDegree,
             boolean mirroring,
-            @NonNull CameraInternal cameraInternal) {
+            @Nullable CameraInternal cameraInternal) {
         mSurface = surface;
         mTargets = targets;
         mFormat = format;
@@ -110,7 +113,8 @@
         mInputCropRect = new Rect(inputCropRect);
         mMirroring = mirroring;
         mRotationDegrees = rotationDegree;
-        calculateAdditionalTransform(cameraInternal);
+        mCameraInternal = cameraInternal;
+        calculateAdditionalTransform();
         mCloseFuture = CallbackToFutureAdapter.getFuture(
                 completer -> {
                     mCloseFutureCompleter = completer;
@@ -211,6 +215,11 @@
         return mMirroring;
     }
 
+    @VisibleForTesting
+    public CameraInternal getCamera() {
+        return mCameraInternal;
+    }
+
     /**
      * This method can be invoked by the processor implementation on any thread.
      *
@@ -229,7 +238,6 @@
 
     /**
      * Returns the close state.
-     *
      */
     @RestrictTo(RestrictTo.Scope.TESTS)
     public boolean isClosed() {
@@ -258,22 +266,22 @@
     /**
      * Calculates the additional GL transform and saves it to {@link #mAdditionalTransform}.
      *
-     * <p>The effect implementation needs to apply this value on top of camera transform obtained
+     * <p>The effect implementation needs to apply this value on top of texture transform obtained
      * from {@link SurfaceTexture#getTransformMatrix}.
      *
-     * <p>The overall transformation (A * B) is a concatenation of 2 values: A) the camera/GL
+     * <p>The overall transformation (A * B) is a concatenation of 2 values: A) the texture
      * transform (value of SurfaceTexture#getTransformMatrix), and B) CameraX's additional
      * transform based on user config such as the ViewPort API and UseCase#targetRotation. To
      * calculate B, we do it in 3 steps:
      * <ol>
      * <li>1. Calculate A * B by using CameraX transformation value such as crop rect, relative
-     * rotation, and mirroring. These info already contain the camera transform(A) in it.
-     * <li>2. Calculate A^-1 by predicating the camera transform(A) based on camera
+     * rotation, and mirroring. It already contains the texture transform(A).
+     * <li>2. Calculate A^-1 by predicating the texture transform(A) based on camera
      * characteristics then inverting it.
      * <li>3. Calculate B by multiplying A^-1 * A * B.
      * </ol>
      */
-    private void calculateAdditionalTransform(@NonNull CameraInternal cameraInternal) {
+    private void calculateAdditionalTransform() {
         Matrix.setIdentityM(mAdditionalTransform, 0);
 
         // Step 1, calculate the overall transformation(A * B) with the following steps:
@@ -313,47 +321,48 @@
         Matrix.translateM(mAdditionalTransform, 0, offsetX, offsetY, 0f);
         Matrix.scaleM(mAdditionalTransform, 0, scaleX, scaleY, 1f);
 
-        // Step 2: calculate the inverted camera/GL transform: A^-1
-        calculateInvertedCameraTransform(cameraInternal);
+        // Step 2: calculate the inverted texture transform: A^-1
+        calculateInvertedTextureTransform();
 
         // Step 3: calculate the additional transform: B = A^-1 * A * B
-        Matrix.multiplyMM(mAdditionalTransform, 0, mInvertedCameraTransform, 0,
+        Matrix.multiplyMM(mAdditionalTransform, 0, mInvertedTextureTransform, 0,
                 mAdditionalTransform, 0);
     }
 
     /**
-     * Calculates the inverted camera/GL transform and saves it to
-     * {@link #mInvertedCameraTransform}.
+     * Calculates the inverted texture transform and saves it to
+     * {@link #mInvertedTextureTransform}.
      *
      * <p>This method predicts the value of {@link SurfaceTexture#getTransformMatrix} based on
-     * camera characteristics then invert it. The result is used to remove the camera transform
+     * camera characteristics then invert it. The result is used to remove the texture transform
      * from overall transformation.
      */
-    private void calculateInvertedCameraTransform(@NonNull CameraInternal cameraInternal) {
-        Matrix.setIdentityM(mInvertedCameraTransform, 0);
+    private void calculateInvertedTextureTransform() {
+        Matrix.setIdentityM(mInvertedTextureTransform, 0);
 
         // Flip for GL. SurfaceTexture#getTransformMatrix always contains this flipping regardless
         // of whether it has the camera transform.
-        Matrix.translateM(mInvertedCameraTransform, 0, 0f, 1f, 0f);
-        Matrix.scaleM(mInvertedCameraTransform, 0, 1f, -1f, 1f);
+        Matrix.translateM(mInvertedTextureTransform, 0, 0f, 1f, 0f);
+        Matrix.scaleM(mInvertedTextureTransform, 0, 1f, -1f, 1f);
 
-        // Applies the transform from CameraInfo if the camera has transformation.
-        if (cameraInternal.getHasTransform()) {
+        // Applies the camera sensor orientation if the input surface contains camera transform.
+        if (mCameraInternal != null) {
+            checkState(mCameraInternal.getHasTransform(), "Camera has no transform.");
 
             // Rotation
-            preRotate(mInvertedCameraTransform,
-                    cameraInternal.getCameraInfo().getSensorRotationDegrees(),
+            preRotate(mInvertedTextureTransform,
+                    mCameraInternal.getCameraInfo().getSensorRotationDegrees(),
                     0.5f,
                     0.5f);
 
             // Mirroring
-            if (cameraInternal.isFrontFacing()) {
-                Matrix.translateM(mInvertedCameraTransform, 0, 1, 0f, 0f);
-                Matrix.scaleM(mInvertedCameraTransform, 0, -1, 1f, 1f);
+            if (mCameraInternal.isFrontFacing()) {
+                Matrix.translateM(mInvertedTextureTransform, 0, 1, 0f, 0f);
+                Matrix.scaleM(mInvertedTextureTransform, 0, -1, 1f, 1f);
             }
         }
 
         // Invert the matrix so it can be used to "undo" the SurfaceTexture#getTransformMatrix.
-        Matrix.invertM(mInvertedCameraTransform, 0, mInvertedCameraTransform, 0);
+        Matrix.invertM(mInvertedTextureTransform, 0, mInvertedTextureTransform, 0);
     }
 }
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/processing/SurfaceProcessorNode.java b/camera/camera-core/src/main/java/androidx/camera/core/processing/SurfaceProcessorNode.java
index 66ca7ab4..040006d 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/processing/SurfaceProcessorNode.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/processing/SurfaceProcessorNode.java
@@ -200,7 +200,7 @@
                 output.getKey().getCropRect(),
                 output.getKey().getRotationDegrees(),
                 output.getKey().getMirroring(),
-                mCameraInternal);
+                input.hasCameraTransform() ? mCameraInternal : null);
         Futures.addCallback(future, new FutureCallback<SurfaceOutput>() {
             @Override
             public void onSuccess(@Nullable SurfaceOutput output) {
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/resolutionselector/AspectRatioStrategy.java b/camera/camera-core/src/main/java/androidx/camera/core/resolutionselector/AspectRatioStrategy.java
new file mode 100644
index 0000000..bf36dfde
--- /dev/null
+++ b/camera/camera-core/src/main/java/androidx/camera/core/resolutionselector/AspectRatioStrategy.java
@@ -0,0 +1,132 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.core.resolutionselector;
+
+import static androidx.camera.core.AspectRatio.RATIO_4_3;
+
+import androidx.annotation.IntDef;
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+import androidx.annotation.RestrictTo;
+import androidx.camera.core.AspectRatio;
+import androidx.camera.core.CameraSelector;
+import androidx.camera.core.UseCase;
+import androidx.lifecycle.LifecycleOwner;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * The aspect ratio strategy defines the sequence of aspect ratios that are used to select the
+ * best size for a particular image.
+ */
+@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public final class AspectRatioStrategy {
+    /**
+     * CameraX doesn't fall back to select sizes of any other aspect ratio when this fallback
+     * rule is used.
+     */
+    public static final int FALLBACK_RULE_NONE = 0;
+    /**
+     * CameraX automatically chooses the next best aspect ratio which contains the closest field
+     * of view (FOV) of the camera sensor, from the remaining options.
+     */
+    public static final int FALLBACK_RULE_AUTO = 1;
+
+    /**
+     * Defines the available fallback rules for AspectRatioStrategy.
+     */
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef({FALLBACK_RULE_NONE,
+            FALLBACK_RULE_AUTO
+    })
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    public @interface AspectRatioFallbackRule {
+    }
+
+    /**
+     * The pre-defined default aspect ratio strategy that selects sizes with
+     * {@link AspectRatio#RATIO_4_3} in priority. Then, selects sizes with other aspect ratios
+     * according to which aspect ratio can contain the closest FOV of the camera sensor.
+     *
+     * <p>Please see the
+     * <a href="https://source.android.com/docs/core/camera/camera3_crop_reprocess">Output streams,
+     * Cropping, and Zoom</a> introduction to know more about the camera FOV.
+     */
+    @NonNull
+    public static final AspectRatioStrategy RATIO_4_3_FALLBACK_AUTO_STRATEGY =
+            AspectRatioStrategy.create(RATIO_4_3, FALLBACK_RULE_AUTO);
+
+    @AspectRatio.Ratio
+    private final int mPreferredAspectRatio;
+    @AspectRatioFallbackRule
+    private final int mFallbackRule;
+
+    private AspectRatioStrategy(@AspectRatio.Ratio int preferredAspectRatio,
+            @AspectRatioFallbackRule int fallbackRule) {
+        mPreferredAspectRatio = preferredAspectRatio;
+        mFallbackRule = fallbackRule;
+    }
+
+    /**
+     * Returns the specified preferred aspect ratio.
+     */
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    @AspectRatio.Ratio
+    public int getPreferredAspectRatio() {
+        return mPreferredAspectRatio;
+    }
+
+    /**
+     * Returns the specified fallback rule for choosing the aspect ratio when the preferred aspect
+     * ratio is not available.
+     */
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    @AspectRatioFallbackRule
+    public int getFallbackRule() {
+        return mFallbackRule;
+    }
+
+    /**
+     * Creates a new AspectRatioStrategy instance, configured with the specified preferred aspect
+     * ratio and fallback rule.
+     *
+     * <p>OEMs might make the width or height of the supported output sizes be mod 16 aligned for
+     * performance reasons. This means that the device might support 1920x1088 instead of
+     * 1920x1080, even though a 16:9 aspect ratio size is 1920x1080. CameraX can select these mod
+     * 16 aligned sizes when applications specify the preferred aspect ratio as
+     * {@link AspectRatio#RATIO_16_9}.
+     *
+     * <p>Some devices may have issues using sizes of the preferred aspect ratios. CameraX
+     * recommends that applications use the {@link #FALLBACK_RULE_AUTO} setting to avoid no
+     * resolution being available, as an {@link IllegalArgumentException} may be thrown when
+     * calling
+     * {@link androidx.camera.lifecycle.ProcessCameraProvider#bindToLifecycle(LifecycleOwner, CameraSelector, UseCase...)}
+     * to bind {@link UseCase}s with the AspectRatioStrategy specified in the
+     * {@link ResolutionSelector}.
+     *
+     * @param preferredAspectRatio the preferred aspect ratio to select first.
+     * @param fallbackRule the rule to follow when the preferred aspect ratio is not available.
+     */
+    @NonNull
+    public static AspectRatioStrategy create(
+            @AspectRatio.Ratio int preferredAspectRatio,
+            @AspectRatioFallbackRule int fallbackRule) {
+        return new AspectRatioStrategy(preferredAspectRatio, fallbackRule);
+    }
+}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/resolutionselector/HighResolution.java b/camera/camera-core/src/main/java/androidx/camera/core/resolutionselector/HighResolution.java
new file mode 100644
index 0000000..f9c0941
--- /dev/null
+++ b/camera/camera-core/src/main/java/androidx/camera/core/resolutionselector/HighResolution.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.core.resolutionselector;
+
+import androidx.annotation.RequiresApi;
+import androidx.annotation.RestrictTo;
+
+/**
+ * The flags that are available in high resolution.
+ */
+@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public final class HighResolution {
+    /**
+     * This flag enables high resolution in the default sensor pixel mode.
+     *
+     * <p>When using the <code>camera-camera2</code> CameraX implementation, please see
+     * {@link android.hardware.camera2.CaptureRequest#SENSOR_PIXEL_MODE} to know more information
+     * about the default and maximum resolution sensor pixel mode.
+     *
+     * <p>When this high resolution flag is set, the high resolution is retrieved via the
+     * {@link android.hardware.camera2.params.StreamConfigurationMap#getHighResolutionOutputSizes(int)}
+     * from the stream configuration map obtained with the
+     * {@link android.hardware.camera2.CameraCharacteristics#SCALER_STREAM_CONFIGURATION_MAP}
+     * camera characteristics.
+     *
+     * <p>Since Android S, some devices might support a maximum resolution sensor pixel mode,
+     * which allows them to capture additional ultra high resolutions retrieved from
+     * {@link android.hardware.camera2.CameraCharacteristics#SCALER_STREAM_CONFIGURATION_MAP_MAXIMUM_RESOLUTION}
+     * . Enabling high resolution with this flag does not allow applications to select those
+     * ultra high resolutions.
+     */
+    public static final int FLAG_DEFAULT_MODE_ON = 0x1;
+
+    private HighResolution() {
+    }
+}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/resolutionselector/ResolutionFilter.java b/camera/camera-core/src/main/java/androidx/camera/core/resolutionselector/ResolutionFilter.java
new file mode 100644
index 0000000..0756560
--- /dev/null
+++ b/camera/camera-core/src/main/java/androidx/camera/core/resolutionselector/ResolutionFilter.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.core.resolutionselector;
+
+import android.util.Size;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+import androidx.annotation.RestrictTo;
+import androidx.camera.core.AspectRatio;
+import androidx.camera.core.ImageCapture;
+import androidx.camera.core.UseCase;
+
+import java.util.List;
+
+/**
+ * Applications can filter out unsuitable sizes and sort the resolution list in the preferred
+ * order by implementing the resolution filter interface. The preferred order is the order in
+ * which the resolutions should be tried first.
+ */
+@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public interface ResolutionFilter {
+    /**
+     * Removes unsuitable sizes and sorts the resolution list in the preferred order.
+     *
+     * <p>OEMs might make the width or height of the supported output sizes be mod 16 aligned for
+     * performance reasons. This means that the device might support 1920x1088 instead of
+     * 1920x1080, even though a 16:9 aspect ratio size is 1920x1080. Therefore, the input
+     * supported sizes list also contains these aspect ratio sizes when applications specify
+     * an {@link AspectRatioStrategy} with {@link AspectRatio#RATIO_16_9} and then also specify a
+     * ResolutionFilter to apply their own selection logic.
+     *
+     * @param supportedSizes  the supported output sizes which have been filtered and sorted
+     *                        according to the other resolution selector settings.
+     * @param rotationDegrees the rotation degrees to rotate the image to the desired
+     *                        orientation, matching the {@link UseCase}’s target rotation setting
+     *                        . For example, the target rotation set via
+     *                        {@link ImageCapture.Builder#setTargetRotation(int)} or
+     *                        {@link ImageCapture#setTargetRotation(int)}.
+     * @return the desired ordered sizes list for resolution selection. The returned list should
+     * only include sizes in the provided input supported sizes list.
+     */
+    @NonNull
+    List<Size> filter(@NonNull List<Size> supportedSizes, int rotationDegrees);
+}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/resolutionselector/ResolutionSelector.java b/camera/camera-core/src/main/java/androidx/camera/core/resolutionselector/ResolutionSelector.java
new file mode 100644
index 0000000..006cb53
--- /dev/null
+++ b/camera/camera-core/src/main/java/androidx/camera/core/resolutionselector/ResolutionSelector.java
@@ -0,0 +1,227 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.core.resolutionselector;
+
+import static androidx.camera.core.resolutionselector.AspectRatioStrategy.RATIO_4_3_FALLBACK_AUTO_STRATEGY;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+import androidx.annotation.RestrictTo;
+import androidx.camera.core.UseCase;
+
+/**
+ * A set of requirements and priorities used to select a resolution for the {@link UseCase}.
+ *
+ * <p>The resolution selection mechanism is determined by the following three steps:
+ * <ol>
+ *     <li> Collect the supported output sizes and add them to the candidate resolution list.
+ *     <li> Filter and sort the candidate resolution list according to the preference settings.
+ *     <li> Consider all the resolution selector settings of bound {@link UseCase}s to find the
+ *     resolution that best suits each {@link UseCase}.
+ * </ol>
+ *
+ * <p>For the first step, all supported resolution output sizes are added to the candidate
+ * resolution list as the starting point.
+ *
+ * <p>ResolutionSelector provides the following function for applications to adjust the candidate
+ * resolution settings.
+ * <ul>
+ *     <li> {@link Builder#setHighResolutionEnabledFlags(int)}
+ * </ul>
+ *
+ * <p>For the second step, ResolutionSelector provides the following three functions for
+ * applications to determine which resolution should be selected with higher priority.
+ * <ul>
+ *     <li> {@link Builder#setAspectRatioStrategy(AspectRatioStrategy)}
+ *     <li> {@link Builder#setResolutionStrategy(ResolutionStrategy)}
+ *     <li> {@link Builder#setResolutionFilter(ResolutionFilter)}
+ * </ul>
+ *
+ * <p>CameraX sorts the collected sizes according to the specified aspect ratio and resolution
+ * strategies. The aspect ratio strategy has precedence over the resolution strategy for sorting
+ * the resolution candidate list. If applications specify a custom resolution filter, CameraX
+ * passes the resulting sizes list, sorted by the specified aspect ratio and resolution
+ * strategies, to the resolution filter to get the final desired list.
+ *
+ * <p>Different types of {@link UseCase}s might have their own default settings. You can see the
+ * {@link UseCase} builders’ {@code setResolutionSelector()} function to know the details for each
+ * type of {@link UseCase}.
+ *
+ * <p>In the third step, CameraX selects the final resolution for the {@link UseCase} based on the
+ * camera device's hardware level, capabilities, and the bound {@link UseCase} combination.
+ * Applications can check which resolution is finally selected by using the {@link UseCase}'s
+ * {@code getResolutionInfo()} function.
+ *
+ * <p>Note that a ResolutionSelector with more restricted settings may result in that no
+ * resolution can be selected to use. Applications will receive {@link IllegalArgumentException}
+ * when binding the {@link UseCase}s with such kind of ResolutionSelector. Applications can
+ * specify the {@link AspectRatioStrategy} and {@link ResolutionStrategy} with proper fallback
+ * rules to avoid the {@link IllegalArgumentException} or try-catch it and show a proper message
+ * to the end users.
+ *
+ * <p>When creating a ResolutionSelector instance, the
+ * {@link AspectRatioStrategy#RATIO_4_3_FALLBACK_AUTO_STRATEGY} will be the default
+ * {@link AspectRatioStrategy} if it is not set. However, if neither the
+ * {@link ResolutionStrategy} nor the high resolution enabled flags are set, there will be no
+ * default value specified.
+ */
+@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public final class ResolutionSelector {
+    @Nullable
+    private final AspectRatioStrategy mAspectRatioStrategy;
+    @Nullable
+    private final ResolutionStrategy mResolutionStrategy;
+    @Nullable
+    private final ResolutionFilter mResolutionFilter;
+    private final int mHighResolutionEnabledFlags;
+
+    ResolutionSelector(
+            @Nullable AspectRatioStrategy aspectRatioStrategy,
+            @Nullable ResolutionStrategy resolutionStrategy,
+            @Nullable ResolutionFilter resolutionFilter,
+            int highResolutionEnabledFlags) {
+        mAspectRatioStrategy = aspectRatioStrategy;
+        mResolutionStrategy = resolutionStrategy;
+        mResolutionFilter = resolutionFilter;
+        mHighResolutionEnabledFlags = highResolutionEnabledFlags;
+    }
+
+    /**
+     * Returns the specified {@link AspectRatioStrategy}, or null if not specified.
+     */
+    @Nullable
+    public AspectRatioStrategy getAspectRatioStrategy() {
+        return mAspectRatioStrategy;
+    }
+
+    /**
+     * Returns the specified {@link ResolutionStrategy}, or null if not specified.
+     */
+    @Nullable
+    public ResolutionStrategy getResolutionStrategy() {
+        return mResolutionStrategy;
+    }
+
+    /**
+     * Returns the specified {@link ResolutionFilter} implementation, or null if not specified.
+     */
+    @Nullable
+    public ResolutionFilter getResolutionFilter() {
+        return mResolutionFilter;
+    }
+
+    /**
+     * Returns the specified high resolution enabled flags.
+     */
+    public int getHighResolutionEnabledFlags() {
+        return mHighResolutionEnabledFlags;
+    }
+
+    /**
+     * Builder for a {@link ResolutionSelector}.
+     */
+    public static final class Builder {
+        @Nullable
+        private AspectRatioStrategy mAspectRatioStrategy = RATIO_4_3_FALLBACK_AUTO_STRATEGY;
+        @Nullable
+        private ResolutionStrategy mResolutionStrategy = null;
+        @Nullable
+        private ResolutionFilter mResolutionFilter = null;
+        private int mHighResolutionEnabledFlags = 0;
+
+        /**
+         * Creates a Builder instance.
+         */
+        public Builder() {
+        }
+
+        private Builder(@NonNull ResolutionSelector resolutionSelector) {
+            mAspectRatioStrategy = resolutionSelector.getAspectRatioStrategy();
+            mResolutionStrategy = resolutionSelector.getResolutionStrategy();
+            mResolutionFilter = resolutionSelector.getResolutionFilter();
+            mHighResolutionEnabledFlags = resolutionSelector.getHighResolutionEnabledFlags();
+        }
+
+        /**
+         * Creates a Builder from an existing resolution selector.
+         */
+        @NonNull
+        @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+        public static Builder fromResolutionSelector(
+                @NonNull ResolutionSelector resolutionSelector) {
+            return new Builder(resolutionSelector);
+        }
+
+        /**
+         * Sets the aspect ratio selection strategy for the {@link UseCase}. The aspect ratio
+         * selection strategy determines how the {@link UseCase} will choose the aspect ratio of
+         * the captured image.
+         */
+        @NonNull
+        public Builder setAspectRatioStrategy(@NonNull AspectRatioStrategy aspectRatioStrategy) {
+            mAspectRatioStrategy = aspectRatioStrategy;
+            return this;
+        }
+
+        /**
+         * Sets the resolution selection strategy for the {@link UseCase}. The resolution selection
+         * strategy determines how the {@link UseCase} will choose the resolution of the captured
+         * image.
+         */
+        @NonNull
+        public Builder setResolutionStrategy(@NonNull ResolutionStrategy resolutionStrategy) {
+            mResolutionStrategy = resolutionStrategy;
+            return this;
+        }
+
+        /**
+         * Sets the resolution filter to output the final desired sizes list. The resolution
+         * filter will filter out unsuitable sizes and sort the resolution list in the preferred
+         * order. The preferred order is the order in which the resolutions should be tried first.
+         */
+        @NonNull
+        public Builder setResolutionFilter(@NonNull ResolutionFilter resolutionFilter) {
+            mResolutionFilter = resolutionFilter;
+            return this;
+        }
+
+        /**
+         * Sets high resolutions enabled flags to allow the application to select high
+         * resolutions for the {@link UseCase}s. This will enable the application to choose high
+         * resolutions for the captured image, which may result in better quality images.
+         *
+         * <p>Now, only {@link HighResolution#FLAG_DEFAULT_MODE_ON} is allowed for this function.
+         */
+        @NonNull
+        public Builder setHighResolutionEnabledFlags(int flags) {
+            mHighResolutionEnabledFlags = flags;
+            return this;
+        }
+
+        /**
+         * Builds the resolution selector. This will create a resolution selector that can be
+         * used to select the desired resolution for the captured image.
+         */
+        @NonNull
+        public ResolutionSelector build() {
+            return new ResolutionSelector(mAspectRatioStrategy, mResolutionStrategy,
+                    mResolutionFilter, mHighResolutionEnabledFlags);
+        }
+    }
+}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/resolutionselector/ResolutionStrategy.java b/camera/camera-core/src/main/java/androidx/camera/core/resolutionselector/ResolutionStrategy.java
new file mode 100644
index 0000000..b0282e1
--- /dev/null
+++ b/camera/camera-core/src/main/java/androidx/camera/core/resolutionselector/ResolutionStrategy.java
@@ -0,0 +1,172 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.core.resolutionselector;
+
+import android.util.Size;
+
+import androidx.annotation.IntDef;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+import androidx.annotation.RestrictTo;
+import androidx.camera.core.CameraSelector;
+import androidx.camera.core.UseCase;
+import androidx.lifecycle.LifecycleOwner;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * The resolution strategy defines the resolution selection sequence to select the best size.
+ */
+@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public final class ResolutionStrategy {
+    /**
+     * A resolution strategy chooses the highest available resolution. This strategy does not
+     * have a bound size or fallback rule. When using this strategy, CameraX selects the
+     * available resolutions to use in descending order, starting with the highest quality
+     * resolution available.
+     */
+    @NonNull
+    public static final ResolutionStrategy HIGHEST_AVAILABLE_STRATEGY = new ResolutionStrategy();
+
+    /**
+     * CameraX doesn't select an alternate size when the specified bound size is unavailable.
+     */
+    public static final int FALLBACK_RULE_NONE = 0;
+    /**
+     * When the specified bound size is unavailable, CameraX falls back to select the closest
+     * higher resolution size. If CameraX still cannot find any available resolution, it will
+     * fallback to select other lower resolutions.
+     */
+    public static final int FALLBACK_RULE_CLOSEST_HIGHER_THEN_LOWER = 1;
+    /**
+     * When the specified bound size is unavailable, CameraX falls back to the closest higher
+     * resolution size.
+     */
+    public static final int FALLBACK_RULE_CLOSEST_HIGHER = 2;
+    /**
+     * When the specified bound size is unavailable, CameraX falls back to select the closest
+     * lower resolution size. If CameraX still cannot find any available resolution, it will
+     * fallback to select other higher resolutions.
+     */
+    public static final int FALLBACK_RULE_CLOSEST_LOWER_THEN_HIGHER = 3;
+    /**
+     * When the specified bound size is unavailable, CameraX falls back to the closest lower
+     * resolution size.
+     */
+    public static final int FALLBACK_RULE_CLOSEST_LOWER = 4;
+
+    /**
+     * Defines the available fallback rules for ResolutionStrategy.
+     */
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef({FALLBACK_RULE_NONE,
+            FALLBACK_RULE_CLOSEST_HIGHER_THEN_LOWER,
+            FALLBACK_RULE_CLOSEST_HIGHER,
+            FALLBACK_RULE_CLOSEST_LOWER_THEN_HIGHER,
+            FALLBACK_RULE_CLOSEST_LOWER
+    })
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    public @interface ResolutionFallbackRule {
+    }
+
+    private boolean mIsHighestAvailableStrategy = false;
+    @Nullable
+    private Size mBoundSize = null;
+    @Nullable
+    private Integer mFallbackRule = null;
+
+    /**
+     * Creates a default ResolutionStrategy instance to select the highest available resolution.
+     */
+    private ResolutionStrategy() {
+        mIsHighestAvailableStrategy = true;
+    }
+
+    /**
+     * Creates a ResolutionStrategy instance to select resolution according to the specified bound
+     * size and fallback rule.
+     */
+    private ResolutionStrategy(@NonNull Size boundSize,
+            @NonNull Integer fallbackRule) {
+        mBoundSize = boundSize;
+        mFallbackRule = fallbackRule;
+    }
+
+    /**
+     * Returns {@code true} if the instance is a highest available resolution strategy.
+     * Otherwise, returns {@code false}.
+     */
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    public boolean isHighestAvailableStrategy() {
+        return mIsHighestAvailableStrategy;
+    }
+
+    /**
+     * Returns the specified bound size.
+     *
+     * @return the specified bound size or {@code null} if this is instance of
+     * {@link #HIGHEST_AVAILABLE_STRATEGY}.
+     */
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    @Nullable
+    public Size getBoundSize() {
+        return mBoundSize;
+    }
+
+    /**
+     * Returns the fallback rule for choosing an alternate size when the specified bound size is
+     * unavailable.
+     */
+    @Nullable
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    public Integer getFallbackRule() {
+        return mFallbackRule;
+    }
+
+    /**
+     * Creates a new ResolutionStrategy instance, configured with the specified bound size and
+     * fallback rule.
+     *
+     * <p>If the resolution candidate list contains the bound size and the bound size can fulfill
+     * all resolution selector settings, CameraX can also select the specified bound size as the
+     * result for the {@link UseCase}.
+     *
+     * <p>Some devices may have issues using sizes of the preferred aspect ratios. CameraX
+     * recommends that applications use the following fallback rule setting to avoid no
+     * resolution being available, as an {@link IllegalArgumentException} may be thrown when
+     * calling
+     * {@link androidx.camera.lifecycle.ProcessCameraProvider#bindToLifecycle(LifecycleOwner, CameraSelector, UseCase...)}
+     * to bind {@link UseCase}s with the ResolutionStrategy specified in the
+     * {@link ResolutionSelector}.
+     * <ul>
+     *     <li> {@link #FALLBACK_RULE_CLOSEST_HIGHER_THEN_LOWER}
+     *     <li> {@link #FALLBACK_RULE_CLOSEST_LOWER_THEN_HIGHER}
+     * </ul>
+     *
+     * @param boundSize the bound size to select the best resolution with the fallback rule.
+     * @param fallbackRule the rule to follow when the specified bound size is not available.
+     */
+    @NonNull
+    public static ResolutionStrategy create(
+            @NonNull Size boundSize,
+            @ResolutionFallbackRule int fallbackRule) {
+        return new ResolutionStrategy(boundSize, fallbackRule);
+    }
+}
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/ImageAnalysisTest.java b/camera/camera-core/src/test/java/androidx/camera/core/ImageAnalysisTest.java
index 30de277..36df55c 100644
--- a/camera/camera-core/src/test/java/androidx/camera/core/ImageAnalysisTest.java
+++ b/camera/camera-core/src/test/java/androidx/camera/core/ImageAnalysisTest.java
@@ -42,6 +42,9 @@
 import androidx.camera.core.impl.utils.executor.CameraXExecutors;
 import androidx.camera.core.internal.CameraUseCaseAdapter;
 import androidx.camera.core.internal.utils.SizeUtil;
+import androidx.camera.core.resolutionselector.AspectRatioStrategy;
+import androidx.camera.core.resolutionselector.ResolutionSelector;
+import androidx.camera.core.resolutionselector.ResolutionStrategy;
 import androidx.camera.testing.CameraUtil;
 import androidx.camera.testing.CameraXUtil;
 import androidx.camera.testing.fakes.FakeAppConfig;
@@ -197,7 +200,7 @@
     public void setAnalyzerWithResolution_doesNotOverridesUseCaseResolution_resolutionSelector() {
         ImageAnalysisConfig config = getMergedImageAnalysisConfig(APP_RESOLUTION,
                 ANALYZER_RESOLUTION, -1, true);
-        assertThat(config.getResolutionSelector().getPreferredResolution()).isEqualTo(
+        assertThat(config.getResolutionSelector().getResolutionStrategy().getBoundSize()).isEqualTo(
                 APP_RESOLUTION);
     }
 
@@ -212,8 +215,8 @@
     public void setAnalyzerWithResolution_usedAsDefaultUseCaseResolution_resolutionSelector() {
         ImageAnalysisConfig config = getMergedImageAnalysisConfig(null,
                 ANALYZER_RESOLUTION, -1, true);
-        assertThat(config.getResolutionSelector().getPreferredResolution()).isEqualTo(
-                ANALYZER_RESOLUTION);
+        assertThat(config.getResolutionSelector().getResolutionStrategy().getBoundSize()).isEqualTo(
+                FLIPPED_ANALYZER_RESOLUTION);
     }
 
     @Test(expected = IllegalArgumentException.class)
@@ -232,11 +235,15 @@
 
         // Sets preferred resolution by ResolutionSelector or legacy API
         if (useResolutionSelector) {
-            ResolutionSelector.Builder resolutionSelectorBuilder = new ResolutionSelector.Builder();
+            ResolutionSelector.Builder selectorBuilder = new ResolutionSelector.Builder();
             if (appResolution != null) {
-                resolutionSelectorBuilder.setPreferredResolution(appResolution);
+                selectorBuilder.setResolutionStrategy(ResolutionStrategy.create(appResolution,
+                        ResolutionStrategy.FALLBACK_RULE_CLOSEST_HIGHER_THEN_LOWER));
+            } else {
+                selectorBuilder.setAspectRatioStrategy(
+                        AspectRatioStrategy.RATIO_4_3_FALLBACK_AUTO_STRATEGY);
             }
-            builder.setResolutionSelector(resolutionSelectorBuilder.build());
+            builder.setResolutionSelector(selectorBuilder.build());
         } else {
             if (appResolution != null) {
                 builder.setTargetResolution(appResolution);
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/ImageCaptureTest.kt b/camera/camera-core/src/test/java/androidx/camera/core/ImageCaptureTest.kt
index de6014c..bff9468 100644
--- a/camera/camera-core/src/test/java/androidx/camera/core/ImageCaptureTest.kt
+++ b/camera/camera-core/src/test/java/androidx/camera/core/ImageCaptureTest.kt
@@ -48,6 +48,7 @@
 import androidx.camera.core.impl.utils.futures.Futures
 import androidx.camera.core.internal.CameraUseCaseAdapter
 import androidx.camera.core.internal.utils.SizeUtil
+import androidx.camera.core.resolutionselector.ResolutionSelector
 import androidx.camera.testing.CameraUtil
 import androidx.camera.testing.CameraXUtil
 import androidx.camera.testing.fakes.FakeAppConfig
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/PreviewTest.kt b/camera/camera-core/src/test/java/androidx/camera/core/PreviewTest.kt
index 6ef3e65..2968588 100644
--- a/camera/camera-core/src/test/java/androidx/camera/core/PreviewTest.kt
+++ b/camera/camera-core/src/test/java/androidx/camera/core/PreviewTest.kt
@@ -44,6 +44,7 @@
 import androidx.camera.core.impl.utils.executor.CameraXExecutors.mainThreadExecutor
 import androidx.camera.core.internal.CameraUseCaseAdapter
 import androidx.camera.core.internal.utils.SizeUtil
+import androidx.camera.core.resolutionselector.ResolutionSelector
 import androidx.camera.testing.CameraUtil
 import androidx.camera.testing.CameraXUtil
 import androidx.camera.testing.fakes.FakeAppConfig
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/internal/SupportedOutputSizesSorterTest.kt b/camera/camera-core/src/test/java/androidx/camera/core/internal/SupportedOutputSizesSorterTest.kt
index c18ffdc..e7265d0 100644
--- a/camera/camera-core/src/test/java/androidx/camera/core/internal/SupportedOutputSizesSorterTest.kt
+++ b/camera/camera-core/src/test/java/androidx/camera/core/internal/SupportedOutputSizesSorterTest.kt
@@ -20,15 +20,31 @@
 import android.os.Build
 import android.util.Pair
 import android.util.Size
+import androidx.camera.core.AspectRatio
+import androidx.camera.core.impl.UseCaseConfig
 import androidx.camera.core.impl.UseCaseConfigFactory.CaptureType
+import androidx.camera.core.impl.utils.AspectRatioUtil
+import androidx.camera.core.resolutionselector.AspectRatioStrategy
+import androidx.camera.core.resolutionselector.HighResolution
+import androidx.camera.core.resolutionselector.ResolutionFilter
+import androidx.camera.core.resolutionselector.ResolutionSelector
+import androidx.camera.core.resolutionselector.ResolutionStrategy
+import androidx.camera.core.resolutionselector.ResolutionStrategy.FALLBACK_RULE_CLOSEST_HIGHER
+import androidx.camera.core.resolutionselector.ResolutionStrategy.FALLBACK_RULE_CLOSEST_HIGHER_THEN_LOWER
+import androidx.camera.core.resolutionselector.ResolutionStrategy.FALLBACK_RULE_CLOSEST_LOWER
+import androidx.camera.core.resolutionselector.ResolutionStrategy.FALLBACK_RULE_CLOSEST_LOWER_THEN_HIGHER
 import androidx.camera.testing.fakes.FakeCameraInfoInternal
 import androidx.camera.testing.fakes.FakeUseCaseConfig
 import com.google.common.truth.Truth.assertThat
+import java.util.Collections
+import org.junit.Assert.assertThrows
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.robolectric.RobolectricTestRunner
+import org.robolectric.annotation.Config
 import org.robolectric.annotation.internal.DoNotInstrument
 
+private const val TARGET_ASPECT_RATIO_NONE = -99
 private val DEFAULT_SUPPORTED_SIZES = listOf(
     Size(4032, 3024), // 4:3
     Size(3840, 2160), // 16:9
@@ -36,6 +52,7 @@
     Size(1920, 1080), // 16:9
     Size(1280, 960), // 4:3
     Size(1280, 720), // 16:9
+    Size(960, 960), // 1:1
     Size(960, 544), // a mod16 version of resolution with 16:9 aspect ratio.
     Size(800, 450), // 16:9
     Size(640, 480), // 4:3
@@ -43,14 +60,39 @@
     Size(320, 180), // 16:9
     Size(256, 144) // 16:9
 )
+private val HIGH_RESOLUTION_SUPPORTED_SIZES = listOf(
+    Size(8000, 6000),
+    Size(8000, 4500),
+)
+private val CUSTOM_SUPPORTED_SIZES = listOf(
+    Size(1920, 1080),
+    Size(720, 480),
+    Size(640, 480)
+)
+private val PORTRAIT_SUPPORTED_SIZES = listOf(
+    Size(1440, 1920),
+    Size(1080, 1920),
+    Size(1080, 1440),
+    Size(960, 1280),
+    Size(720, 1280),
+    Size(960, 540),
+    Size(480, 640),
+    Size(640, 480),
+    Size(360, 480)
+)
 
 /**
  * Unit tests for [SupportedOutputSizesSorter].
  */
 @RunWith(RobolectricTestRunner::class)
 @DoNotInstrument
-@org.robolectric.annotation.Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
+@Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
 class SupportedOutputSizesSorterTest {
+    private val cameraInfoInternal = FakeCameraInfoInternal().apply {
+        setSupportedResolutions(ImageFormat.JPEG, DEFAULT_SUPPORTED_SIZES)
+        setSupportedHighResolutions(ImageFormat.JPEG, HIGH_RESOLUTION_SUPPORTED_SIZES)
+    }
+    private val supportedOutputSizesSorter = SupportedOutputSizesSorter(cameraInfoInternal)
 
     @Test
     fun canSelectCustomOrderedResolutions() {
@@ -63,26 +105,14 @@
         // Sets up the custom ordered resolutions
         val useCaseConfig =
             FakeUseCaseConfig.Builder(CaptureType.IMAGE_CAPTURE, imageFormat).apply {
-                setCustomOrderedResolutions(
-                    listOf(
-                        Size(1920, 1080),
-                        Size(720, 480),
-                        Size(640, 480)
-                    )
-                )
+                setCustomOrderedResolutions(CUSTOM_SUPPORTED_SIZES)
             }.useCaseConfig
 
         // Act
         val sortedResult = supportedOutputSizesSorter.getSortedSupportedOutputSizes(useCaseConfig)
 
         // Assert
-        assertThat(sortedResult).containsExactlyElementsIn(
-            listOf(
-                Size(1920, 1080),
-                Size(720, 480),
-                Size(640, 480)
-            )
-        ).inOrder()
+        assertThat(sortedResult).containsExactlyElementsIn(CUSTOM_SUPPORTED_SIZES).inOrder()
     }
 
     @Test
@@ -97,15 +127,7 @@
         val useCaseConfig =
             FakeUseCaseConfig.Builder(CaptureType.IMAGE_CAPTURE, imageFormat).apply {
                 setSupportedResolutions(
-                    listOf(
-                        Pair.create(
-                            imageFormat, arrayOf(
-                                Size(1920, 1080),
-                                Size(720, 480),
-                                Size(640, 480)
-                            )
-                        )
-                    )
+                    listOf(Pair.create(imageFormat, CUSTOM_SUPPORTED_SIZES.toTypedArray()))
                 )
             }.useCaseConfig
 
@@ -113,12 +135,460 @@
         val sortedResult = supportedOutputSizesSorter.getSortedSupportedOutputSizes(useCaseConfig)
 
         // Assert
-        assertThat(sortedResult).containsExactlyElementsIn(
-            listOf(
-                Size(1920, 1080),
-                Size(720, 480),
-                Size(640, 480)
-            )
-        ).inOrder()
+        assertThat(sortedResult).containsExactlyElementsIn(CUSTOM_SUPPORTED_SIZES).inOrder()
     }
-}
\ No newline at end of file
+
+    @Test
+    fun getSupportedOutputSizes_aspectRatio4x3_fallbackRuleNone() {
+        verifySupportedOutputSizesWithResolutionSelectorSettings(
+            preferredAspectRatio = AspectRatio.RATIO_4_3,
+            aspectRatioFallbackRule = AspectRatioStrategy.FALLBACK_RULE_NONE,
+            expectedList = listOf(
+                // Only returns preferred AspectRatio matched items, sorted by area size.
+                Size(4032, 3024),
+                Size(1920, 1440),
+                Size(1280, 960),
+                Size(640, 480),
+                Size(320, 240),
+            )
+        )
+    }
+
+    @Test
+    fun getSupportedOutputSizes_aspectRatio4x3_fallbackRuleAuto() {
+        verifySupportedOutputSizesWithResolutionSelectorSettings(
+            preferredAspectRatio = AspectRatio.RATIO_4_3,
+            aspectRatioFallbackRule = AspectRatioStrategy.FALLBACK_RULE_AUTO,
+            expectedList = listOf(
+                // Matched preferred AspectRatio items, sorted by area size.
+                Size(4032, 3024), // 4:3
+                Size(1920, 1440),
+                Size(1280, 960),
+                Size(640, 480),
+                Size(320, 240),
+                // Mismatched preferred AspectRatio items, sorted by FOV and area size.
+                Size(960, 960), // 1:1
+                Size(3840, 2160), // 16:9
+                Size(1920, 1080),
+                Size(1280, 720),
+                Size(960, 544),
+                Size(800, 450),
+                Size(320, 180),
+                Size(256, 144)
+            )
+        )
+    }
+
+    @Test
+    fun getSupportedOutputSizes_aspectRatio16x9_fallbackRuleNone() {
+        verifySupportedOutputSizesWithResolutionSelectorSettings(
+            preferredAspectRatio = AspectRatio.RATIO_16_9,
+            aspectRatioFallbackRule = AspectRatioStrategy.FALLBACK_RULE_NONE,
+            expectedList = listOf(
+                // Only returns preferred AspectRatio matched items, sorted by area size.
+                Size(3840, 2160),
+                Size(1920, 1080),
+                Size(1280, 720),
+                Size(960, 544),
+                Size(800, 450),
+                Size(320, 180),
+                Size(256, 144),
+            )
+        )
+    }
+
+    @Test
+    fun getSupportedOutputSizes_aspectRatio16x9_fallbackRuleAuto() {
+        verifySupportedOutputSizesWithResolutionSelectorSettings(
+            preferredAspectRatio = AspectRatio.RATIO_16_9,
+            aspectRatioFallbackRule = AspectRatioStrategy.FALLBACK_RULE_AUTO,
+            expectedList = listOf(
+                // Matched preferred AspectRatio items, sorted by area size.
+                Size(3840, 2160), // 16:9
+                Size(1920, 1080),
+                Size(1280, 720),
+                Size(960, 544),
+                Size(800, 450),
+                Size(320, 180),
+                Size(256, 144),
+                // Mismatched preferred AspectRatio items, sorted by FOV and area size.
+                Size(4032, 3024), // 4:3
+                Size(1920, 1440),
+                Size(1280, 960),
+                Size(640, 480),
+                Size(320, 240),
+                Size(960, 960), // 1:1
+            )
+        )
+    }
+
+    @Test
+    fun getSupportedOutputSizes_boundSize1920x1080_withResolutionFallbackRuleNone() {
+        verifySupportedOutputSizesWithResolutionSelectorSettings(
+            boundSize = Size(1280, 960),
+            resolutionFallbackRule = ResolutionStrategy.FALLBACK_RULE_NONE,
+            // Only returns preferred AspectRatio matched item.
+            expectedList = listOf(Size(1280, 960))
+        )
+    }
+
+    @Test
+    fun getSupportedOutputSizes_boundSize1920x1080_withBothAspectRatioResolutionFallbackRuleNone() {
+        verifySupportedOutputSizesWithResolutionSelectorSettings(
+            preferredAspectRatio = AspectRatio.RATIO_4_3,
+            aspectRatioFallbackRule = AspectRatioStrategy.FALLBACK_RULE_NONE,
+            boundSize = Size(1920, 1080),
+            resolutionFallbackRule = ResolutionStrategy.FALLBACK_RULE_NONE,
+            // No size is returned since only 1920x1080 is allowed but it is not a 4:3 size
+            expectedList = Collections.emptyList()
+        )
+    }
+
+    @Test
+    fun getSupportedOutputSizes_withHighestAvailableResolutionStrategy() {
+        // Bound size will be ignored when fallback rule is HIGHEST_AVAILABLE
+        verifySupportedOutputSizesWithResolutionSelectorSettings(
+            resolutionStrategy = ResolutionStrategy.HIGHEST_AVAILABLE_STRATEGY,
+            // The 4:3 default aspect ratio will make sizes of 4/3 have the highest priority
+            expectedList = listOf(
+                // Matched default preferred AspectRatio items, sorted by area size.
+                Size(4032, 3024),
+                Size(1920, 1440),
+                Size(1280, 960),
+                Size(640, 480),
+                Size(320, 240),
+                // Mismatched default preferred AspectRatio items, sorted by FOV and area size.
+                Size(960, 960), // 1:1
+                Size(3840, 2160), // 16:9
+                Size(1920, 1080),
+                Size(1280, 720),
+                Size(960, 544),
+                Size(800, 450),
+                Size(320, 180),
+                Size(256, 144),
+            )
+        )
+    }
+
+    @Test
+    fun getSupportedOutputSizes_boundSize1920x1080_withClosestHigherThenLowerRule() {
+        verifySupportedOutputSizesWithResolutionSelectorSettings(
+            boundSize = Size(1920, 1080),
+            resolutionFallbackRule = FALLBACK_RULE_CLOSEST_HIGHER_THEN_LOWER,
+            // The 4:3 default aspect ratio will make sizes of 4/3 have the highest priority
+            expectedList = listOf(
+                // Matched default preferred AspectRatio items, sorted by area size.
+                Size(1920, 1440), // 4:3 smallest larger size has highest priority
+                Size(4032, 3024), // the remaining 4:3 larger sizes
+                Size(1280, 960), // the remaining 4:3 smaller sizes
+                Size(640, 480),
+                Size(320, 240),
+                // Mismatched default preferred AspectRatio items, sorted by FOV and area size.
+                Size(960, 960), // 1:1
+                Size(1920, 1080), // 16:9 smallest larger size
+                Size(3840, 2160), // the remaining 16:9 larger sizes
+                Size(1280, 720), // the remaining 16:9 smaller sizes
+                Size(960, 544),
+                Size(800, 450),
+                Size(320, 180),
+                Size(256, 144),
+            )
+        )
+    }
+
+    @Test
+    fun getSupportedOutputSizes_boundSize1920x1080_withClosestHigherRule() {
+        verifySupportedOutputSizesWithResolutionSelectorSettings(
+            boundSize = Size(1920, 1080),
+            resolutionFallbackRule = FALLBACK_RULE_CLOSEST_HIGHER,
+            // The 4:3 default aspect ratio will make sizes of 4/3 have the highest priority
+            expectedList = listOf(
+                // Matched default preferred AspectRatio items, sorted by area size.
+                Size(1920, 1440), // 4:3 smallest larger size has highest priority
+                Size(4032, 3024), // the remaining 4:3 larger sizes
+                // Mismatched default preferred AspectRatio items, sorted by FOV and area size.
+                Size(1920, 1080), // 16:9 smallest larger size
+                Size(3840, 2160), // the remaining 16:9 larger sizes
+            )
+        )
+    }
+
+    @Test
+    fun getSupportedOutputSizes_boundSize1920x1080_withClosestLowerThenHigherRule() {
+        verifySupportedOutputSizesWithResolutionSelectorSettings(
+            boundSize = Size(1920, 1080),
+            resolutionFallbackRule = FALLBACK_RULE_CLOSEST_LOWER_THEN_HIGHER,
+            // The 4:3 default aspect ratio will make sizes of 4/3 have the highest priority
+            expectedList = listOf(
+                // Matched default preferred AspectRatio items, sorted by area size.
+                Size(1280, 960), // 4:3 largest smaller size has highest priority
+                Size(640, 480), // the remaining 4:3 smaller sizes
+                Size(320, 240),
+                Size(1920, 1440), // the remaining 4:3 larger sizes
+                Size(4032, 3024),
+                // Mismatched default preferred AspectRatio items, sorted by FOV and area size.
+                Size(960, 960), // 1:1
+                Size(1920, 1080), // 16:9 largest smaller size
+                Size(1280, 720), // the remaining 16:9 smaller sizes
+                Size(960, 544),
+                Size(800, 450),
+                Size(320, 180),
+                Size(256, 144),
+                Size(3840, 2160), // the remaining 16:9 larger sizes
+            )
+        )
+    }
+
+    @Test
+    fun getSupportedOutputSizes_boundSize1920x1080_withClosestLowerRule() {
+        verifySupportedOutputSizesWithResolutionSelectorSettings(
+            boundSize = Size(1920, 1080),
+            resolutionFallbackRule = FALLBACK_RULE_CLOSEST_LOWER,
+            // The 4:3 default aspect ratio will make sizes of 4/3 have the highest priority
+            expectedList = listOf(
+                // Matched default preferred AspectRatio items, sorted by area size.
+                Size(1280, 960), // 4:3 largest smaller size has highest priority
+                Size(640, 480), // the remaining 4:3 smaller sizes
+                Size(320, 240),
+                // Mismatched default preferred AspectRatio items, sorted by FOV and area size.
+                Size(960, 960), // 1:1
+                Size(1920, 1080), // 16:9 largest smaller size
+                Size(1280, 720), // the remaining 16:9 smaller sizes
+                Size(960, 544),
+                Size(800, 450),
+                Size(320, 180),
+                Size(256, 144),
+            )
+        )
+    }
+
+    @Test
+    fun getSupportedOutputSizesWithPortraitPixelArraySize_aspectRatio16x9() {
+        val cameraInfoInternal = FakeCameraInfoInternal().apply {
+            setSupportedResolutions(ImageFormat.JPEG, PORTRAIT_SUPPORTED_SIZES)
+        }
+        val supportedOutputSizesSorter = SupportedOutputSizesSorter(cameraInfoInternal)
+        verifySupportedOutputSizesWithResolutionSelectorSettings(
+            outputSizesSorter = supportedOutputSizesSorter,
+            preferredAspectRatio = AspectRatio.RATIO_16_9,
+            expectedList = listOf(
+                // Matched preferred AspectRatio items, sorted by area size.
+                Size(1080, 1920),
+                Size(720, 1280),
+                // Mismatched preferred AspectRatio items, sorted by area size.
+                Size(1440, 1920),
+                Size(1080, 1440),
+                Size(960, 1280),
+                Size(480, 640),
+                Size(360, 480),
+                Size(640, 480),
+                Size(960, 540)
+            )
+        )
+    }
+
+    @Config(minSdk = Build.VERSION_CODES.M)
+    @Test
+    fun getSupportedOutputSizes_whenHighResolutionIsEnabled_aspectRatio16x9() {
+        verifySupportedOutputSizesWithResolutionSelectorSettings(
+            preferredAspectRatio = AspectRatio.RATIO_16_9,
+            highResolutionEnabledFlags = HighResolution.FLAG_DEFAULT_MODE_ON,
+            expectedList = listOf(
+                // Matched preferred AspectRatio items, sorted by area size.
+                Size(8000, 4500), // 16:9 high resolution size
+                Size(3840, 2160),
+                Size(1920, 1080),
+                Size(1280, 720),
+                Size(960, 544),
+                Size(800, 450),
+                Size(320, 180),
+                Size(256, 144),
+                // Mismatched preferred AspectRatio items, sorted by area size.
+                Size(8000, 6000), // 4:3 high resolution size
+                Size(4032, 3024),
+                Size(1920, 1440),
+                Size(1280, 960),
+                Size(640, 480),
+                Size(320, 240),
+                Size(960, 960), // 1:1
+            )
+        )
+    }
+
+    @Config(minSdk = Build.VERSION_CODES.M)
+    @Test
+    fun highResolutionCanNotBeSelected_whenHighResolutionForceDisabled() {
+        verifySupportedOutputSizesWithResolutionSelectorSettings(
+            preferredAspectRatio = AspectRatio.RATIO_16_9,
+            highResolutionEnabledFlags = HighResolution.FLAG_DEFAULT_MODE_ON,
+            highResolutionForceDisabled = true,
+            expectedList = listOf(
+                // Matched preferred AspectRatio items, sorted by area size.
+                Size(3840, 2160),
+                Size(1920, 1080),
+                Size(1280, 720),
+                Size(960, 544),
+                Size(800, 450),
+                Size(320, 180),
+                Size(256, 144),
+                // Mismatched preferred AspectRatio items, sorted by area size.
+                Size(4032, 3024), // 4:3
+                Size(1920, 1440),
+                Size(1280, 960),
+                Size(640, 480),
+                Size(320, 240),
+                Size(960, 960), // 1:1
+            )
+        )
+    }
+
+    @Test
+    fun checkAllSupportedSizesCanBeSelected() {
+        // For 4:3 and 16:9 sizes, sets each of supported size as bound size and also calculates
+        // its ratio to set as preferred aspect ratio, so that the size can be put in the first
+        // priority position. For sizes of other aspect ratio, creates a ResolutionFilter to select
+        // it.
+        DEFAULT_SUPPORTED_SIZES.forEach { supportedSize ->
+            var resolutionFilter: ResolutionFilter? = null
+            val preferredAspectRatio =
+                if (AspectRatioUtil.hasMatchingAspectRatio(
+                        supportedSize,
+                        AspectRatioUtil.ASPECT_RATIO_4_3
+                    )
+                ) {
+                    AspectRatio.RATIO_4_3
+                } else if (AspectRatioUtil.hasMatchingAspectRatio(
+                        supportedSize,
+                        AspectRatioUtil.ASPECT_RATIO_16_9
+                    )
+                ) {
+                    AspectRatio.RATIO_16_9
+                } else {
+                    // Sizes of special aspect ratios as 1:1 or 18:9 need to implement
+                    // ResolutionFilter to select them.
+                    resolutionFilter = ResolutionFilter { supportedSizes, _ ->
+                        supportedSizes.remove(supportedSize)
+                        supportedSizes.add(0, supportedSize)
+                        supportedSizes
+                    }
+                    TARGET_ASPECT_RATIO_NONE
+                }
+
+            val useCaseConfig = createUseCaseConfig(
+                preferredAspectRatio = preferredAspectRatio,
+                boundSize = supportedSize,
+                resolutionFilter = resolutionFilter
+            )
+
+            val resultList = supportedOutputSizesSorter.getSortedSupportedOutputSizes(useCaseConfig)
+            assertThat(resultList[0]).isEqualTo(supportedSize)
+        }
+    }
+
+    @Test
+    fun getSupportedOutputSizes_withResolutionFilter() {
+        val filteredSizesList = arrayListOf(
+            Size(1280, 720),
+            Size(4032, 3024),
+            Size(960, 960)
+        )
+        val resolutionFilter = ResolutionFilter { _, _ -> filteredSizesList }
+        verifySupportedOutputSizesWithResolutionSelectorSettings(
+            resolutionFilter = resolutionFilter,
+            expectedList = filteredSizesList
+        )
+    }
+
+    @Test
+    fun getSupportedOutputSizes_resolutionFilterReturnsAdditionalItem() {
+        val resolutionFilter = ResolutionFilter { supportedSizes, _ ->
+            supportedSizes.add(Size(1599, 899)) // Adds an additional size item
+            supportedSizes
+        }
+        // Throws exception when additional items is added in the returned list
+        assertThrows(IllegalArgumentException::class.java) {
+            verifySupportedOutputSizesWithResolutionSelectorSettings(
+                resolutionFilter = resolutionFilter
+            )
+        }
+    }
+
+    private fun verifySupportedOutputSizesWithResolutionSelectorSettings(
+        outputSizesSorter: SupportedOutputSizesSorter = supportedOutputSizesSorter,
+        captureType: CaptureType = CaptureType.IMAGE_CAPTURE,
+        preferredAspectRatio: Int = TARGET_ASPECT_RATIO_NONE,
+        aspectRatioFallbackRule: Int = AspectRatioStrategy.FALLBACK_RULE_AUTO,
+        resolutionStrategy: ResolutionStrategy? = null,
+        boundSize: Size? = null,
+        resolutionFallbackRule: Int = FALLBACK_RULE_CLOSEST_HIGHER_THEN_LOWER,
+        resolutionFilter: ResolutionFilter? = null,
+        highResolutionEnabledFlags: Int = 0,
+        highResolutionForceDisabled: Boolean = false,
+        expectedList: List<Size> = Collections.emptyList(),
+    ) {
+        val useCaseConfig = createUseCaseConfig(
+            captureType,
+            preferredAspectRatio,
+            aspectRatioFallbackRule,
+            resolutionStrategy,
+            boundSize,
+            resolutionFallbackRule,
+            resolutionFilter,
+            highResolutionEnabledFlags,
+            highResolutionForceDisabled
+        )
+        val resultList = outputSizesSorter.getSortedSupportedOutputSizes(useCaseConfig)
+        assertThat(resultList).containsExactlyElementsIn(expectedList).inOrder()
+    }
+
+    private fun createUseCaseConfig(
+        captureType: CaptureType = CaptureType.IMAGE_CAPTURE,
+        preferredAspectRatio: Int = TARGET_ASPECT_RATIO_NONE,
+        aspectRatioFallbackRule: Int = AspectRatioStrategy.FALLBACK_RULE_AUTO,
+        resolutionStrategy: ResolutionStrategy? = null,
+        boundSize: Size? = null,
+        resolutionFallbackRule: Int = FALLBACK_RULE_CLOSEST_HIGHER_THEN_LOWER,
+        resolutionFilter: ResolutionFilter? = null,
+        highResolutionEnabledFlags: Int = 0,
+        highResolutionForceDisabled: Boolean = false,
+    ): UseCaseConfig<*> {
+        val useCaseConfigBuilder = FakeUseCaseConfig.Builder(captureType, ImageFormat.JPEG)
+        val resolutionSelectorBuilder = ResolutionSelector.Builder()
+
+        // Creates aspect ratio strategy and sets to resolution selector
+        val aspectRatioStrategy = if (preferredAspectRatio != TARGET_ASPECT_RATIO_NONE) {
+            AspectRatioStrategy.create(preferredAspectRatio, aspectRatioFallbackRule)
+        } else {
+            null
+        }
+
+        aspectRatioStrategy?.let { resolutionSelectorBuilder.setAspectRatioStrategy(it) }
+
+        // Creates resolution strategy and sets to resolution selector
+        if (resolutionStrategy != null) {
+            resolutionSelectorBuilder.setResolutionStrategy(resolutionStrategy)
+        } else {
+            boundSize?.let {
+                resolutionSelectorBuilder.setResolutionStrategy(
+                    ResolutionStrategy.create(
+                        boundSize,
+                        resolutionFallbackRule
+                    )
+                )
+            }
+        }
+
+        // Sets the resolution filter to resolution selector
+        resolutionFilter?.let { resolutionSelectorBuilder.setResolutionFilter(it) }
+        // Sets the high resolution enabled flags to resolution selector
+        resolutionSelectorBuilder.setHighResolutionEnabledFlags(highResolutionEnabledFlags)
+
+        // Sets the custom resolution selector to use case config
+        useCaseConfigBuilder.setResolutionSelector(resolutionSelectorBuilder.build())
+
+        // Sets the high resolution force disabled setting
+        useCaseConfigBuilder.setHighResolutionDisabled(highResolutionForceDisabled)
+
+        return useCaseConfigBuilder.useCaseConfig
+    }
+}
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/processing/SurfaceOutputImplTest.kt b/camera/camera-core/src/test/java/androidx/camera/core/processing/SurfaceOutputImplTest.kt
index eb809d7..bc76e238 100644
--- a/camera/camera-core/src/test/java/androidx/camera/core/processing/SurfaceOutputImplTest.kt
+++ b/camera/camera-core/src/test/java/androidx/camera/core/processing/SurfaceOutputImplTest.kt
@@ -125,9 +125,7 @@
     @Test
     fun updateMatrixWithoutCameraTransform_noCameraTransform() {
         // Arrange.
-        val cameraInfo = FakeCameraInfoInternal(180, LENS_FACING_FRONT)
-        val camera = FakeCamera(null, cameraInfo).apply { hasTransform = false }
-        val surfaceOut = createFakeSurfaceOutputImpl(camera)
+        val surfaceOut = createFakeSurfaceOutputImpl(null)
         val input = FloatArray(16).also {
             Matrix.setIdentityM(it, 0)
         }
@@ -179,7 +177,7 @@
         assertThat(hasRequestedClose).isFalse()
     }
 
-    private fun createFakeSurfaceOutputImpl(camera: FakeCamera = FakeCamera()) = SurfaceOutputImpl(
+    private fun createFakeSurfaceOutputImpl(camera: FakeCamera? = FakeCamera()) = SurfaceOutputImpl(
         fakeSurface,
         TARGET,
         INTERNAL_DEFINED_IMAGE_FORMAT_PRIVATE,
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/processing/SurfaceProcessorNodeTest.kt b/camera/camera-core/src/test/java/androidx/camera/core/processing/SurfaceProcessorNodeTest.kt
index 1107797..f6287b4 100644
--- a/camera/camera-core/src/test/java/androidx/camera/core/processing/SurfaceProcessorNodeTest.kt
+++ b/camera/camera-core/src/test/java/androidx/camera/core/processing/SurfaceProcessorNodeTest.kt
@@ -116,6 +116,36 @@
     }
 
     @Test
+    fun inputHasNoCameraTransform_surfaceOutputReceivesNullCamera() {
+        // Arrange: configure node to produce JPEG output.
+        createSurfaceProcessorNode()
+        createInputEdge(
+            inputEdge = SurfaceEdge(
+                PREVIEW,
+                INTERNAL_DEFINED_IMAGE_FORMAT_PRIVATE,
+                StreamSpec.builder(INPUT_SIZE).build(),
+                Matrix.IDENTITY_MATRIX,
+                false,
+                PREVIEW_CROP_RECT,
+                0,
+                false
+            )
+        )
+
+        // Act.
+        val nodeOutput = node.transform(nodeInput)
+        provideSurfaces(nodeOutput)
+        shadowOf(getMainLooper()).idle()
+
+        val previewSurfaceOutput =
+            surfaceProcessorInternal.surfaceOutputs[PREVIEW]!! as SurfaceOutputImpl
+        assertThat(previewSurfaceOutput.camera).isNull()
+        val videoSurfaceOutput =
+            surfaceProcessorInternal.surfaceOutputs[VIDEO_CAPTURE]!! as SurfaceOutputImpl
+        assertThat(videoSurfaceOutput.camera).isNull()
+    }
+
+    @Test
     fun configureJpegOutput_returnsJpegFormat() {
         // Arrange: configure node to produce JPEG output.
         createSurfaceProcessorNode()
@@ -288,6 +318,7 @@
         assertThat(previewTransformInfo.rotationDegrees).isEqualTo(0)
         assertThat(previewSurfaceOutput.inputSize).isEqualTo(INPUT_SIZE)
         assertThat(previewSurfaceOutput.mirroring).isFalse()
+        assertThat(previewSurfaceOutput.camera).isNotNull()
 
         val videoSurfaceOutput =
             surfaceProcessorInternal.surfaceOutputs[VIDEO_CAPTURE]!! as SurfaceOutputImpl
@@ -298,6 +329,7 @@
         assertThat(videoTransformInfo.rotationDegrees).isEqualTo(270)
         assertThat(videoSurfaceOutput.inputSize).isEqualTo(INPUT_SIZE)
         assertThat(videoSurfaceOutput.mirroring).isTrue()
+        assertThat(videoSurfaceOutput.camera).isNotNull()
     }
 
     @Test
@@ -421,9 +453,8 @@
         inputRotationDegrees: Int = INPUT_ROTATION_DEGREES,
         mirroring: Boolean = MIRRORING,
         videoOutputSize: Size = VIDEO_SIZE,
-        frameRateRange: Range<Int> = StreamSpec.FRAME_RATE_RANGE_UNSPECIFIED
-    ) {
-        val inputEdge = SurfaceEdge(
+        frameRateRange: Range<Int> = StreamSpec.FRAME_RATE_RANGE_UNSPECIFIED,
+        inputEdge: SurfaceEdge = SurfaceEdge(
             previewTarget,
             INTERNAL_DEFINED_IMAGE_FORMAT_PRIVATE,
             StreamSpec.builder(previewSize).setExpectedFrameRateRange(frameRateRange).build(),
@@ -432,7 +463,8 @@
             previewCropRect,
             inputRotationDegrees,
             mirroring,
-        )
+        ),
+    ) {
         videoOutConfig = OutConfig.of(
             VIDEO_CAPTURE,
             INTERNAL_DEFINED_IMAGE_FORMAT_PRIVATE,
diff --git a/camera/camera-testing/src/main/java/androidx/camera/testing/CameraUtil.java b/camera/camera-testing/src/main/java/androidx/camera/testing/CameraUtil.java
index a87cc14..198e8c24 100644
--- a/camera/camera-testing/src/main/java/androidx/camera/testing/CameraUtil.java
+++ b/camera/camera-testing/src/main/java/androidx/camera/testing/CameraUtil.java
@@ -1040,6 +1040,7 @@
     public static TestRule grantCameraPermissionAndPreTest(@Nullable PreTestCamera cameraTestRule,
             @Nullable PreTestCameraIdList cameraIdListTestRule) {
         RuleChain rule = RuleChain.outerRule(GrantPermissionRule.grant(Manifest.permission.CAMERA));
+        rule = rule.around(new IgnoreProblematicDeviceRule());
         if (cameraIdListTestRule != null) {
             rule = rule.around(cameraIdListTestRule);
         }
diff --git a/camera/camera-testing/src/main/java/androidx/camera/testing/IgnoreProblematicDeviceRule.kt b/camera/camera-testing/src/main/java/androidx/camera/testing/IgnoreProblematicDeviceRule.kt
new file mode 100644
index 0000000..32d1274
--- /dev/null
+++ b/camera/camera-testing/src/main/java/androidx/camera/testing/IgnoreProblematicDeviceRule.kt
@@ -0,0 +1,81 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package androidx.camera.testing
+
+import android.os.Build
+import android.util.Log
+import androidx.annotation.RequiresApi
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.uiautomator.UiDevice
+import org.junit.AssumptionViolatedException
+import org.junit.rules.TestRule
+import org.junit.runner.Description
+import org.junit.runners.model.Statement
+
+/**
+ * Test class to set the TestRule should not be run on the problematic devices.
+ */
+@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
+class IgnoreProblematicDeviceRule : TestRule {
+    private var avdName: String = try {
+        val device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
+        device.executeShellCommand("getprop ro.kernel.qemu.avd_name").filterNot {
+            it == '_' || it == '-' || it == ' '
+        }
+    } catch (e: Exception) {
+        Log.d("ProblematicDeviceRule", "Cannot get avd name", e)
+        ""
+    }
+
+    private val pixel2Api26Emulator = isEmulator && avdName.contains(
+        "Pixel2", ignoreCase = true
+    ) && Build.VERSION.SDK_INT == Build.VERSION_CODES.O
+
+    private val api21Emulator = isEmulator && Build.VERSION.SDK_INT == Build.VERSION_CODES.LOLLIPOP
+
+    private val isProblematicDevices = pixel2Api26Emulator || api21Emulator
+
+    override fun apply(base: Statement, description: Description): Statement {
+        return object : Statement() {
+            override fun evaluate() {
+                if (isProblematicDevices) {
+                    throw AssumptionViolatedException(
+                        "CameraDevice of the emulator may not be well prepared for camera" +
+                            " related tests. Ignore the test: " + description.displayName +
+                            ". To test on emulator devices, please remove the " +
+                            "IgnoreProblematicDeviceRule from the test class."
+                    )
+                } else {
+                    base.evaluate()
+                }
+            }
+        }
+    }
+
+    companion object {
+        // Sync from TestRequestBuilder.RequiresDeviceFilter
+        private const val EMULATOR_HARDWARE_GOLDFISH = "goldfish"
+        private const val EMULATOR_HARDWARE_RANCHU = "ranchu"
+        private const val EMULATOR_HARDWARE_GCE = "gce_x86"
+        private val emulatorHardwareNames: Set<String> = setOf(
+            EMULATOR_HARDWARE_GOLDFISH,
+            EMULATOR_HARDWARE_RANCHU,
+            EMULATOR_HARDWARE_GCE
+        )
+
+        private val isEmulator = emulatorHardwareNames.contains(Build.HARDWARE.lowercase())
+    }
+}
\ No newline at end of file
diff --git a/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeUseCaseConfig.java b/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeUseCaseConfig.java
index 2267928..88ae896 100644
--- a/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeUseCaseConfig.java
+++ b/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeUseCaseConfig.java
@@ -25,7 +25,6 @@
 import androidx.annotation.RequiresApi;
 import androidx.camera.core.CameraSelector;
 import androidx.camera.core.MirrorMode;
-import androidx.camera.core.ResolutionSelector;
 import androidx.camera.core.UseCase;
 import androidx.camera.core.impl.CaptureConfig;
 import androidx.camera.core.impl.Config;
@@ -36,6 +35,7 @@
 import androidx.camera.core.impl.SessionConfig;
 import androidx.camera.core.impl.UseCaseConfig;
 import androidx.camera.core.impl.UseCaseConfigFactory.CaptureType;
+import androidx.camera.core.resolutionselector.ResolutionSelector;
 
 import java.util.List;
 import java.util.UUID;
diff --git a/camera/camera-video/src/main/java/androidx/camera/video/VideoCapture.java b/camera/camera-video/src/main/java/androidx/camera/video/VideoCapture.java
index 7dc5a90..3eb0bf2 100644
--- a/camera/camera-video/src/main/java/androidx/camera/video/VideoCapture.java
+++ b/camera/camera-video/src/main/java/androidx/camera/video/VideoCapture.java
@@ -72,7 +72,6 @@
 import androidx.camera.core.ImageCapture;
 import androidx.camera.core.Logger;
 import androidx.camera.core.MirrorMode;
-import androidx.camera.core.ResolutionSelector;
 import androidx.camera.core.SurfaceRequest;
 import androidx.camera.core.UseCase;
 import androidx.camera.core.ViewPort;
@@ -104,6 +103,7 @@
 import androidx.camera.core.processing.DefaultSurfaceProcessor;
 import androidx.camera.core.processing.SurfaceEdge;
 import androidx.camera.core.processing.SurfaceProcessorNode;
+import androidx.camera.core.resolutionselector.ResolutionSelector;
 import androidx.camera.video.StreamInfo.StreamState;
 import androidx.camera.video.impl.VideoCaptureConfig;
 import androidx.camera.video.internal.VideoValidatedEncoderProfilesProxy;
diff --git a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/ImageAnalysisTest.kt b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/ImageAnalysisTest.kt
index 17d87da..e8e96f9 100644
--- a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/ImageAnalysisTest.kt
+++ b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/ImageAnalysisTest.kt
@@ -32,9 +32,10 @@
 import androidx.camera.core.ImageAnalysis.BackpressureStrategy
 import androidx.camera.core.ImageCapture
 import androidx.camera.core.ImageProxy
-import androidx.camera.core.ResolutionSelector
 import androidx.camera.core.impl.ImageOutputConfig
 import androidx.camera.core.impl.utils.executor.CameraXExecutors
+import androidx.camera.core.resolutionselector.HighResolution
+import androidx.camera.core.resolutionselector.ResolutionSelector
 import androidx.camera.lifecycle.ProcessCameraProvider
 import androidx.camera.testing.CameraPipeConfigTestRule
 import androidx.camera.testing.CameraUtil
@@ -428,9 +429,11 @@
         assumeTrue(maxHighResolutionOutputSize != null)
 
         val resolutionSelector = ResolutionSelector.Builder()
-                .setPreferredResolution(maxHighResolutionOutputSize!!)
-                .setHighResolutionEnabled(true)
-                .build()
+            .setHighResolutionEnabledFlags(HighResolution.FLAG_DEFAULT_MODE_ON)
+            .setResolutionFilter { _, _ ->
+                listOf(maxHighResolutionOutputSize)
+            }
+            .build()
 
         val imageAnalysis = ImageAnalysis.Builder()
             .setResolutionSelector(resolutionSelector)
diff --git a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/ImageCaptureTest.kt b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/ImageCaptureTest.kt
index 119b3a0..129edde 100644
--- a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/ImageCaptureTest.kt
+++ b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/ImageCaptureTest.kt
@@ -48,7 +48,6 @@
 import androidx.camera.core.ImageCaptureException
 import androidx.camera.core.ImageProxy
 import androidx.camera.core.Preview
-import androidx.camera.core.ResolutionSelector
 import androidx.camera.core.UseCase
 import androidx.camera.core.UseCaseGroup
 import androidx.camera.core.ViewPort
@@ -63,6 +62,8 @@
 import androidx.camera.core.impl.utils.CameraOrientationUtil
 import androidx.camera.core.impl.utils.Exif
 import androidx.camera.core.internal.compat.workaround.ExifRotationAvailability
+import androidx.camera.core.resolutionselector.HighResolution
+import androidx.camera.core.resolutionselector.ResolutionSelector
 import androidx.camera.integration.core.util.CameraPipeUtil
 import androidx.camera.lifecycle.ProcessCameraProvider
 import androidx.camera.testing.CameraPipeConfigTestRule
@@ -1582,11 +1583,12 @@
         // Only runs the test when the device has high resolution output sizes
         assumeTrue(maxHighResolutionOutputSize != null)
 
-        val resolutionSelector =
-            ResolutionSelector.Builder()
-                .setPreferredResolution(maxHighResolutionOutputSize!!)
-                .setHighResolutionEnabled(true)
-                .build()
+        val resolutionSelector = ResolutionSelector.Builder()
+            .setHighResolutionEnabledFlags(HighResolution.FLAG_DEFAULT_MODE_ON)
+            .setResolutionFilter { _, _ ->
+                listOf(maxHighResolutionOutputSize)
+            }
+            .build()
         val sensorOrientation = CameraUtil.getSensorOrientation(BACK_SELECTOR.lensFacing!!)
         // Sets the target rotation to the camera sensor orientation to avoid the captured image
         // buffer data rotated by the HAL and impact the final image resolution check
diff --git a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/camera2/PreviewTest.kt b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/camera2/PreviewTest.kt
index 8348922..b744eea 100644
--- a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/camera2/PreviewTest.kt
+++ b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/camera2/PreviewTest.kt
@@ -29,11 +29,12 @@
 import androidx.camera.core.CameraXConfig
 import androidx.camera.core.ImageCapture
 import androidx.camera.core.Preview
-import androidx.camera.core.ResolutionSelector
 import androidx.camera.core.UseCase
 import androidx.camera.core.impl.ImageOutputConfig
 import androidx.camera.core.impl.utils.executor.CameraXExecutors
 import androidx.camera.core.internal.CameraUseCaseAdapter
+import androidx.camera.core.resolutionselector.HighResolution
+import androidx.camera.core.resolutionselector.ResolutionSelector
 import androidx.camera.testing.CameraPipeConfigTestRule
 import androidx.camera.testing.CameraUtil
 import androidx.camera.testing.CameraUtil.PreTestCameraIdList
@@ -111,7 +112,9 @@
         CameraXUtil.initialize(context!!, cameraConfig).get()
 
         // init CameraX before creating Preview to get preview size with CameraX's context
-        defaultBuilder = Preview.Builder.fromConfig(Preview.DEFAULT_CONFIG.config)
+        defaultBuilder = Preview.Builder.fromConfig(Preview.DEFAULT_CONFIG.config).also {
+            it.mutableConfig.removeOption(ImageOutputConfig.OPTION_TARGET_ASPECT_RATIO)
+        }
         surfaceFutureSemaphore = Semaphore( /*permits=*/0)
         safeToReleaseSemaphore = Semaphore( /*permits=*/0)
     }
@@ -548,9 +551,10 @@
         // Arrange.
         val resolutionSelector =
             ResolutionSelector.Builder()
-                .setMaxResolution(maxHighResolutionOutputSize!!)
-                .setPreferredResolution(maxHighResolutionOutputSize)
-                .setHighResolutionEnabled(true)
+                .setHighResolutionEnabledFlags(HighResolution.FLAG_DEFAULT_MODE_ON)
+                .setResolutionFilter { _, _ ->
+                    listOf(maxHighResolutionOutputSize)
+                }
                 .build()
         val preview = Preview.Builder().setResolutionSelector(resolutionSelector).build()
 
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/pager/PagerStateTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/pager/PagerStateTest.kt
index 8aa3c7a..f979d1a 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/pager/PagerStateTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/pager/PagerStateTest.kt
@@ -34,6 +34,7 @@
 import androidx.test.filters.SdkSuppress
 import com.google.common.truth.Truth.assertThat
 import kotlin.test.assertFalse
+import kotlin.test.assertTrue
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.runBlocking
@@ -464,7 +465,7 @@
         // Act
         // Moving more than threshold
         val backwardDelta = scrollForwardSign.toFloat() * with(rule.density) {
-                -DefaultPositionThreshold.toPx() / 2
+            -DefaultPositionThreshold.toPx() / 2
         }
 
         previousTargetPage = state.targetPage
@@ -622,6 +623,48 @@
     }
 
     @Test
+    fun settledPage_shouldChangeOnScrollFinished() {
+        // Arrange
+        val state = PagerState()
+        var settledPageChanges = 0
+        createPager(
+            state = state,
+            modifier = Modifier.fillMaxSize(),
+            effects = {
+                LaunchedEffect(key1 = state.settledPage) {
+                    settledPageChanges++
+                }
+            }
+        )
+        rule.runOnIdle {
+            assertThat(state.settledPage).isEqualTo(state.currentPage)
+            assertTrue { settledPageChanges == 1 }
+        }
+
+        settledPageChanges = 0
+        val previousSettled = state.settledPage
+        rule.mainClock.autoAdvance = false
+        // Act
+        // Moving forward
+        rule.runOnIdle {
+            scope.launch {
+                state.animateScrollToPage(DefaultPageCount - 1)
+            }
+        }
+
+        assertTrue { state.isScrollInProgress }
+        assertTrue { settledPageChanges == 0 }
+        assertThat(state.settledPage).isEqualTo(previousSettled)
+
+        rule.mainClock.advanceTimeUntil { settledPageChanges != 0 }
+
+        rule.runOnIdle {
+            assertTrue { !state.isScrollInProgress }
+            assertThat(state.settledPage).isEqualTo(state.currentPage)
+        }
+    }
+
+    @Test
     fun currentPageOffset_shouldReflectScrollingOfCurrentPage() {
         // Arrange
         val state = PagerState(DefaultPageCount / 2)
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/SelectionContainerFocusTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/SelectionContainerFocusTest.kt
index 1552480..0fe45bd 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/SelectionContainerFocusTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/SelectionContainerFocusTest.kt
@@ -26,20 +26,26 @@
 import androidx.compose.foundation.text.BasicText
 import androidx.compose.foundation.text.TEST_FONT_FAMILY
 import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.geometry.Rect
 import androidx.compose.ui.hapticfeedback.HapticFeedback
 import androidx.compose.ui.hapticfeedback.HapticFeedbackType
 import androidx.compose.ui.layout.onGloballyPositioned
 import androidx.compose.ui.platform.LocalHapticFeedback
 import androidx.compose.ui.platform.LocalLayoutDirection
 import androidx.compose.ui.platform.LocalTextToolbar
+import androidx.compose.ui.platform.TextToolbar
+import androidx.compose.ui.platform.TextToolbarStatus
 import androidx.compose.ui.platform.testTag
 import androidx.compose.ui.test.click
 import androidx.compose.ui.test.hasTestTag
 import androidx.compose.ui.test.junit4.createAndroidComposeRule
 import androidx.compose.ui.test.longClick
+import androidx.compose.ui.test.onNodeWithTag
 import androidx.compose.ui.test.performTouchInput
 import androidx.compose.ui.text.AnnotatedString
 import androidx.compose.ui.text.TextStyle
@@ -54,10 +60,10 @@
 import com.nhaarman.mockitokotlin2.mock
 import com.nhaarman.mockitokotlin2.times
 import com.nhaarman.mockitokotlin2.verify
+import java.util.concurrent.CountDownLatch
 import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
-import java.util.concurrent.CountDownLatch
 
 @LargeTest
 @RunWith(AndroidJUnit4::class)
@@ -139,6 +145,52 @@
         }
     }
 
+    @Test
+    fun leavingComposition_hidesTextToolbar() {
+        // null -> nothing got called, true -> show, false -> hide
+        var lastShowCalled: Boolean? = null
+        val fakeTextToolbar = FakeTextToolbar(
+            onShowMenu = { _, _, _, _, _ -> lastShowCalled = true },
+            onHideMenu = { lastShowCalled = false }
+        )
+
+        val tag = "SelectionContainer"
+
+        var inComposition by mutableStateOf(true)
+
+        rule.setContent {
+            CompositionLocalProvider(
+                LocalTextToolbar provides fakeTextToolbar
+            ) {
+                if (inComposition) {
+                    SelectionContainer(modifier = Modifier.testTag("SelectionContainer")) {
+                        BasicText(
+                            AnnotatedString(textContent),
+                            Modifier.fillMaxWidth(),
+                            style = TextStyle(fontFamily = fontFamily, fontSize = fontSize),
+                            softWrap = true,
+                            overflow = TextOverflow.Clip,
+                            maxLines = Int.MAX_VALUE,
+                            inlineContent = mapOf(),
+                            onTextLayout = {}
+                        )
+                    }
+                }
+            }
+        }
+
+        rule.onNodeWithTag(tag).performTouchInput { longClick() }
+        rule.runOnIdle {
+            assertThat(lastShowCalled).isTrue()
+        }
+
+        inComposition = false
+
+        rule.runOnIdle {
+            assertThat(lastShowCalled).isFalse()
+        }
+    }
+
     private fun createSelectionContainer(isRtl: Boolean = false) {
         val measureLatch = CountDownLatch(1)
 
@@ -151,9 +203,11 @@
             ) {
                 Column {
                     SelectionContainer(
-                        modifier = Modifier.onGloballyPositioned {
-                            measureLatch.countDown()
-                        }.testTag("selectionContainer1"),
+                        modifier = Modifier
+                            .onGloballyPositioned {
+                                measureLatch.countDown()
+                            }
+                            .testTag("selectionContainer1"),
                         selection = selection1.value,
                         onSelectionChange = {
                             selection1.value = it
@@ -170,14 +224,19 @@
                                 inlineContent = mapOf(),
                                 onTextLayout = {}
                             )
-                            Box(Modifier.size(boxSize, boxSize).testTag("box"))
+                            Box(
+                                Modifier
+                                    .size(boxSize, boxSize)
+                                    .testTag("box"))
                         }
                     }
 
                     SelectionContainer(
-                        modifier = Modifier.onGloballyPositioned {
-                            measureLatch.countDown()
-                        }.testTag("selectionContainer2"),
+                        modifier = Modifier
+                            .onGloballyPositioned {
+                                measureLatch.countDown()
+                            }
+                            .testTag("selectionContainer2"),
                         selection = selection2.value,
                         onSelectionChange = {
                             selection2.value = it
@@ -201,4 +260,44 @@
             view = it.findViewById<ViewGroup>(android.R.id.content)
         }
     }
+}
+
+internal fun FakeTextToolbar(
+    onShowMenu: (
+        rect: Rect,
+        onCopyRequested: (() -> Unit)?,
+        onPasteRequested: (() -> Unit)?,
+        onCutRequested: (() -> Unit)?,
+        onSelectAllRequested: (() -> Unit)?
+    ) -> Unit,
+    onHideMenu: () -> Unit
+): TextToolbar {
+    return object : TextToolbar {
+        private var _status: TextToolbarStatus = TextToolbarStatus.Hidden
+
+        override fun showMenu(
+            rect: Rect,
+            onCopyRequested: (() -> Unit)?,
+            onPasteRequested: (() -> Unit)?,
+            onCutRequested: (() -> Unit)?,
+            onSelectAllRequested: (() -> Unit)?
+        ) {
+            onShowMenu(
+                rect,
+                onCopyRequested,
+                onPasteRequested,
+                onCutRequested,
+                onSelectAllRequested
+            )
+            _status = TextToolbarStatus.Shown
+        }
+
+        override fun hide() {
+            onHideMenu()
+            _status = TextToolbarStatus.Hidden
+        }
+
+        override val status: TextToolbarStatus
+            get() = _status
+    }
 }
\ No newline at end of file
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/textfield/TextFieldTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/textfield/TextFieldTest.kt
index 3d8a7782..ef5913b 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/textfield/TextFieldTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/textfield/TextFieldTest.kt
@@ -14,9 +14,6 @@
 * limitations under the License.
 */
 
-// TODO(b/160821157): Replace FocusState with FocusState2.isFocused
-@file:Suppress("DEPRECATION")
-
 package androidx.compose.foundation.textfield
 
 import android.os.Build
@@ -1373,8 +1370,8 @@
             )
         }
         val textNode = rule.onNodeWithTag(Tag)
-        textNode.performTouchInput { longClick() }
-        textNode.performTextInputSelection(TextRange(0, 4))
+        textNode.performSemanticsAction(SemanticsActions.RequestFocus)
+        textNode.performTextInputSelection(TextRange(0, 5))
         textNode.performKeyPress(
             KeyEvent(
                 NativeKeyEvent(
@@ -1392,14 +1389,18 @@
             )
         )
 
+        rule.waitForIdle()
+        textNode.assertTextEquals("")
+        val selection = textNode.fetchSemanticsNode().config
+            .getOrNull(SemanticsProperties.TextSelectionRange)
+        assertThat(selection).isEqualTo(TextRange(0))
+
         textFieldValue.value = "Hello"
 
         rule.waitForIdle()
-
-        val expected = TextRange(0, 0)
         val actual = textNode.fetchSemanticsNode().config
             .getOrNull(SemanticsProperties.TextSelectionRange)
-        assertThat(actual).isEqualTo(expected)
+        assertThat(actual).isEqualTo(TextRange(0))
     }
 
     @Test
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutSemanticState.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyLayoutSemanticState.kt
similarity index 92%
rename from compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutSemanticState.kt
rename to compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyLayoutSemanticState.kt
index c4e0403..5709700 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutSemanticState.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyLayoutSemanticState.kt
@@ -14,10 +14,10 @@
  * limitations under the License.
  */
 
-package androidx.compose.foundation.lazy.layout
+package androidx.compose.foundation.lazy
 
 import androidx.compose.foundation.gestures.animateScrollBy
-import androidx.compose.foundation.lazy.LazyListState
+import androidx.compose.foundation.lazy.layout.LazyLayoutSemanticState
 import androidx.compose.ui.semantics.CollectionInfo
 
 internal fun LazyLayoutSemanticState(
@@ -44,4 +44,4 @@
         } else {
             CollectionInfo(rowCount = 1, columnCount = -1)
         }
-}
\ No newline at end of file
+}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListItemProvider.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListItemProvider.kt
index 664ac94..31cbd85 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListItemProvider.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListItemProvider.kt
@@ -92,9 +92,9 @@
  * We use the idea of sliding window as an optimization, so user can scroll up to this number of
  * items until we have to regenerate the key to index map.
  */
-private const val NearestItemsSlidingWindowSize = 30
+internal const val NearestItemsSlidingWindowSize = 30
 
 /**
  * The minimum amount of items near the current first visible item we want to have mapping for.
  */
-private const val NearestItemsExtraItemCount = 100
\ No newline at end of file
+internal const val NearestItemsExtraItemCount = 100
\ No newline at end of file
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/lazy/LazyLayoutPager.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/lazy/LazyLayoutPager.kt
new file mode 100644
index 0000000..1fcdf60
--- /dev/null
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/lazy/LazyLayoutPager.kt
@@ -0,0 +1,221 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.foundation.pager.lazy
+
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.clipScrollableContainer
+import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.foundation.gestures.ScrollableDefaults
+import androidx.compose.foundation.gestures.scrollable
+import androidx.compose.foundation.gestures.snapping.SnapFlingBehavior
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.lazy.LazyListBeyondBoundsInfo
+import androidx.compose.foundation.lazy.NearestItemsExtraItemCount
+import androidx.compose.foundation.lazy.NearestItemsSlidingWindowSize
+import androidx.compose.foundation.lazy.layout.IntervalList
+import androidx.compose.foundation.lazy.layout.LazyLayout
+import androidx.compose.foundation.lazy.layout.LazyLayoutIntervalContent
+import androidx.compose.foundation.lazy.layout.LazyLayoutItemProvider
+import androidx.compose.foundation.lazy.layout.LazyLayoutKeyIndexMap
+import androidx.compose.foundation.lazy.layout.MutableIntervalList
+import androidx.compose.foundation.lazy.layout.NearestRangeKeyIndexMapState
+import androidx.compose.foundation.lazy.layout.PinnableItem
+import androidx.compose.foundation.lazy.layout.lazyLayoutSemantics
+import androidx.compose.foundation.overscroll
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.snapshotFlow
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
+import androidx.compose.ui.input.nestedscroll.nestedScroll
+import androidx.compose.ui.platform.LocalLayoutDirection
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+import kotlinx.coroutines.flow.drop
+import kotlinx.coroutines.flow.filter
+
+@ExperimentalFoundationApi
+@Composable
+internal fun Pager(
+    /** Modifier to be applied for the inner layout */
+    modifier: Modifier,
+    /** The amount of Pages that will be present in this Pager **/
+    pageCount: Int,
+    /** State controlling the scroll position */
+    state: PagerState,
+    /** The inner padding to be added for the whole content(not for each individual page) */
+    contentPadding: PaddingValues,
+    /** reverse the direction of scrolling and layout */
+    reverseLayout: Boolean,
+    /** The layout orientation of the Pager */
+    orientation: Orientation,
+    /** fling behavior to be used for flinging */
+    flingBehavior: SnapFlingBehavior,
+    /** Whether scrolling via the user gestures is allowed. */
+    userScrollEnabled: Boolean,
+    /** Number of pages to layout before and after the visible pages */
+    beyondBoundsPageCount: Int = 0,
+    /** Space between pages **/
+    pageSpacing: Dp = 0.dp,
+    /** Allows to change how to calculate the Page size **/
+    pageSize: PageSize,
+    /** A [NestedScrollConnection] that dictates how this [Pager] behaves with nested lists.  **/
+    pageNestedScrollConnection: NestedScrollConnection,
+    /** a stable and unique key representing the Page **/
+    key: ((index: Int) -> Any)?,
+    /** The alignment to align pages horizontally. Required when isVertical is true */
+    horizontalAlignment: Alignment.Horizontal,
+    /** The alignment to align pages vertically. Required when isVertical is false */
+    verticalAlignment: Alignment.Vertical,
+    /** The content of the list */
+    pageContent: @Composable (page: Int) -> Unit
+) {
+    require(beyondBoundsPageCount >= 0) {
+        "beyondBoundsPageCount should be greater than or equal to 0, " +
+            "you selected $beyondBoundsPageCount"
+    }
+
+    val overscrollEffect = ScrollableDefaults.overscrollEffect()
+
+    // TODO(levima) Add test for pageContent change
+    val pagerItemProvider = remember(state, pageCount, key, pageContent as Any?) {
+        val intervalList = MutableIntervalList<PagerIntervalContent>().apply {
+            addInterval(pageCount, PagerIntervalContent(key = key, item = pageContent))
+        }
+        PagerLazyLayoutItemProvider(
+            state = state,
+            intervals = intervalList
+        )
+    }
+
+    val beyondBoundsInfo = remember { LazyListBeyondBoundsInfo() }
+
+    val measurePolicy = rememberPagerMeasurePolicy(
+        state = state,
+        contentPadding = contentPadding,
+        reverseLayout = reverseLayout,
+        orientation = orientation,
+        beyondBoundsPageCount = beyondBoundsPageCount,
+        pageSpacing = pageSpacing,
+        pageSize = pageSize,
+        horizontalAlignment = horizontalAlignment,
+        verticalAlignment = verticalAlignment,
+        itemProvider = pagerItemProvider,
+        pageCount = pageCount,
+        beyondBoundsInfo = beyondBoundsInfo
+    )
+
+    val pagerFlingBehavior = remember(flingBehavior, state) {
+        PagerWrapperFlingBehavior(flingBehavior, state)
+    }
+
+    // TODO(levima) Move this logic to measure pass
+    LaunchedEffect(state) {
+        snapshotFlow { state.isScrollInProgress }
+            .filter { !it }
+            .drop(1) // Initial scroll is false
+            .collect { state.updateOnScrollStopped() }
+    }
+
+    val pagerSemantics = if (userScrollEnabled) {
+        Modifier.pagerSemantics(state, orientation == Orientation.Vertical)
+    } else {
+        Modifier
+    }
+
+    val semanticState = rememberPagerSemanticState(
+        state,
+        pagerItemProvider,
+        reverseLayout,
+        orientation == Orientation.Vertical
+    )
+
+    LazyLayout(
+        modifier = modifier
+            .then(state.remeasurementModifier)
+            .then(state.awaitLayoutModifier)
+            .then(pagerSemantics)
+            .lazyLayoutSemantics(
+                itemProvider = pagerItemProvider,
+                state = semanticState,
+                orientation = orientation,
+                userScrollEnabled = userScrollEnabled,
+                reverseScrolling = reverseLayout
+            )
+            .clipScrollableContainer(orientation)
+            .pagerBeyondBoundsModifier(state, beyondBoundsInfo, reverseLayout, orientation)
+            .overscroll(overscrollEffect)
+            .scrollable(
+                orientation = orientation,
+                reverseDirection = ScrollableDefaults.reverseDirection(
+                    LocalLayoutDirection.current,
+                    orientation,
+                    reverseLayout
+                ),
+                interactionSource = state.internalInteractionSource,
+                flingBehavior = pagerFlingBehavior,
+                state = state,
+                overscrollEffect = overscrollEffect,
+                enabled = userScrollEnabled
+            )
+            .nestedScroll(pageNestedScrollConnection),
+        measurePolicy = measurePolicy,
+        prefetchState = state.prefetchState,
+        itemProvider = pagerItemProvider
+    )
+}
+
+@ExperimentalFoundationApi
+internal class PagerLazyLayoutItemProvider(
+    intervals: IntervalList<PagerIntervalContent>,
+    val state: PagerState
+) : LazyLayoutItemProvider {
+    private val pagerContent = PagerLayoutIntervalContent(intervals)
+    private val keyToIndexMap: LazyLayoutKeyIndexMap by NearestRangeKeyIndexMapState(
+        firstVisibleItemIndex = { state.firstVisiblePage },
+        slidingWindowSize = { NearestItemsSlidingWindowSize },
+        extraItemCount = { NearestItemsExtraItemCount },
+        content = { pagerContent }
+    )
+    override val itemCount: Int
+        get() = pagerContent.itemCount
+
+    @Composable
+    override fun Item(index: Int) {
+        pagerContent.PinnableItem(index, state.pinnedPages) { localIndex ->
+            item(localIndex)
+        }
+    }
+
+    override fun getKey(index: Int): Any = pagerContent.getKey(index)
+
+    override fun getIndex(key: Any): Int = keyToIndexMap[key]
+}
+
+@OptIn(ExperimentalFoundationApi::class)
+private class PagerLayoutIntervalContent(
+    override val intervals: IntervalList<PagerIntervalContent>
+) : LazyLayoutIntervalContent<PagerIntervalContent>()
+
+@OptIn(ExperimentalFoundationApi::class)
+internal class PagerIntervalContent(
+    override val key: ((page: Int) -> Any)?,
+    val item: @Composable (page: Int) -> Unit
+) : LazyLayoutIntervalContent.Interval
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutSemanticState.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/lazy/LazyLayoutSemanticState.kt
similarity index 79%
copy from compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutSemanticState.kt
copy to compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/lazy/LazyLayoutSemanticState.kt
index c4e0403..1e8af8c 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutSemanticState.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/lazy/LazyLayoutSemanticState.kt
@@ -14,19 +14,21 @@
  * limitations under the License.
  */
 
-package androidx.compose.foundation.lazy.layout
+package androidx.compose.foundation.pager.lazy
 
+import androidx.compose.foundation.ExperimentalFoundationApi
 import androidx.compose.foundation.gestures.animateScrollBy
-import androidx.compose.foundation.lazy.LazyListState
+import androidx.compose.foundation.lazy.layout.LazyLayoutSemanticState
 import androidx.compose.ui.semantics.CollectionInfo
 
+@OptIn(ExperimentalFoundationApi::class)
 internal fun LazyLayoutSemanticState(
-    state: LazyListState,
+    state: PagerState,
     isVertical: Boolean
 ): LazyLayoutSemanticState = object : LazyLayoutSemanticState {
 
     override val currentPosition: Float
-        get() = state.firstVisibleItemIndex + state.firstVisibleItemScrollOffset / 100_000f
+        get() = state.firstVisiblePage + state.firstVisiblePageOffset / 100_000f
     override val canScrollForward: Boolean
         get() = state.canScrollForward
 
@@ -35,7 +37,7 @@
     }
 
     override suspend fun scrollToItem(index: Int) {
-        state.scrollToItem(index)
+        state.scrollToPage(index)
     }
 
     override fun collectionInfo(): CollectionInfo =
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/lazy/MeasuredPage.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/lazy/MeasuredPage.kt
new file mode 100644
index 0000000..aedd26b
--- /dev/null
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/lazy/MeasuredPage.kt
@@ -0,0 +1,101 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.foundation.pager.lazy
+
+import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.layout.Placeable
+import androidx.compose.ui.unit.IntOffset
+import androidx.compose.ui.unit.LayoutDirection
+import androidx.compose.ui.util.fastForEach
+
+internal class MeasuredPage(
+    val index: Int,
+    val size: Int,
+    val placeables: List<Placeable>,
+    val visualOffset: IntOffset,
+    val key: Any,
+    val orientation: Orientation,
+    val horizontalAlignment: Alignment.Horizontal?,
+    val verticalAlignment: Alignment.Vertical?,
+    val layoutDirection: LayoutDirection,
+    val reverseLayout: Boolean,
+    val beforeContentPadding: Int,
+    val afterContentPadding: Int,
+) {
+
+    val crossAxisSize: Int
+
+    init {
+        var maxCrossAxis = 0
+        placeables.fastForEach {
+            maxCrossAxis = maxOf(
+                maxCrossAxis,
+                if (orientation != Orientation.Vertical) it.height else it.width
+            )
+        }
+        crossAxisSize = maxCrossAxis
+    }
+
+    fun position(
+        offset: Int,
+        layoutWidth: Int,
+        layoutHeight: Int
+    ): PositionedPage {
+        val wrappers = mutableListOf<PagerPlaceableWrapper>()
+        val mainAxisLayoutSize =
+            if (orientation == Orientation.Vertical) layoutHeight else layoutWidth
+        var mainAxisOffset = if (reverseLayout) {
+            mainAxisLayoutSize - offset - size
+        } else {
+            offset
+        }
+        var index = if (reverseLayout) placeables.lastIndex else 0
+        while (if (reverseLayout) index >= 0 else index < placeables.size) {
+            val it = placeables[index]
+            val addIndex = if (reverseLayout) 0 else wrappers.size
+            val placeableOffset = if (orientation == Orientation.Vertical) {
+                val x = requireNotNull(horizontalAlignment)
+                    .align(it.width, layoutWidth, layoutDirection)
+                IntOffset(x, mainAxisOffset)
+            } else {
+                val y = requireNotNull(verticalAlignment).align(it.height, layoutHeight)
+                IntOffset(mainAxisOffset, y)
+            }
+            mainAxisOffset += if (orientation == Orientation.Vertical) it.height else it.width
+            wrappers.add(
+                addIndex,
+                PagerPlaceableWrapper(placeableOffset, it, placeables[index].parentData)
+            )
+            if (reverseLayout) index-- else index++
+        }
+        return PositionedPage(
+            offset = offset,
+            index = this.index,
+            key = key,
+            orientation = orientation,
+            wrappers = wrappers,
+            visualOffset = visualOffset,
+        )
+    }
+}
+
+internal class PagerPlaceableWrapper(
+    val offset: IntOffset,
+    val placeable: Placeable,
+    val parentData: Any?
+)
\ No newline at end of file
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/impl/SizeCoordinate.java b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/lazy/PageInfo.kt
similarity index 65%
rename from camera/camera-core/src/main/java/androidx/camera/core/impl/SizeCoordinate.java
rename to compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/lazy/PageInfo.kt
index b1cfbf1..c1b85f1 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/impl/SizeCoordinate.java
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/lazy/PageInfo.kt
@@ -14,19 +14,12 @@
  * limitations under the License.
  */
 
-package androidx.camera.core.impl;
+package androidx.compose.foundation.pager.lazy
 
-/**
- * The size coordinate system enum.
- */
-public enum SizeCoordinate {
-    /**
-     * Size is expressed in the camera sensor's natural orientation (landscape).
-     */
-    CAMERA_SENSOR,
+import androidx.compose.foundation.ExperimentalFoundationApi
 
-    /**
-     * Size is expressed in the Android View's orientation.
-     */
-    ANDROID_VIEW
-}
+@ExperimentalFoundationApi
+internal interface PageInfo {
+    val index: Int
+    val offset: Int
+}
\ No newline at end of file
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/lazy/Pager.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/lazy/Pager.kt
new file mode 100644
index 0000000..e12d3f8
--- /dev/null
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/lazy/Pager.kt
@@ -0,0 +1,631 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.foundation.pager.lazy
+
+import androidx.compose.animation.core.AnimationSpec
+import androidx.compose.animation.core.DecayAnimationSpec
+import androidx.compose.animation.core.LinearEasing
+import androidx.compose.animation.core.Spring
+import androidx.compose.animation.core.calculateTargetValue
+import androidx.compose.animation.core.spring
+import androidx.compose.animation.core.tween
+import androidx.compose.animation.rememberSplineBasedDecay
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.gestures.FlingBehavior
+import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.foundation.gestures.ScrollScope
+import androidx.compose.foundation.gestures.snapping.SnapFlingBehavior
+import androidx.compose.foundation.gestures.snapping.SnapLayoutInfoProvider
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.Stable
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
+import androidx.compose.ui.input.nestedscroll.NestedScrollSource
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.semantics.pageDown
+import androidx.compose.ui.semantics.pageLeft
+import androidx.compose.ui.semantics.pageRight
+import androidx.compose.ui.semantics.pageUp
+import androidx.compose.ui.semantics.semantics
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.Velocity
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.util.fastFirstOrNull
+import androidx.compose.ui.util.fastForEach
+import kotlin.math.absoluteValue
+import kotlin.math.ceil
+import kotlin.math.floor
+import kotlin.math.sign
+import kotlinx.coroutines.launch
+
+/**
+ * A Pager that scrolls horizontally. Pages are lazily placed in accordance to the available
+ * viewport size. By definition, pages in a [Pager] have the same size, defined by [pageSize] and
+ * use a snap animation (provided by [flingBehavior] to scroll pages into a specific position). You
+ * can use [beyondBoundsPageCount] to place more pages before and after the visible pages.
+ *
+ * If you need snapping with pages of different size, you can use a [SnapFlingBehavior] with a
+ * [SnapLayoutInfoProvider] adapted to a LazyList.
+ * @see androidx.compose.foundation.gestures.snapping.SnapLayoutInfoProvider for the implementation
+ * of a [SnapLayoutInfoProvider] that uses [androidx.compose.foundation.lazy.LazyListState].
+ *
+ * Please refer to the sample to learn how to use this API.
+ * @sample androidx.compose.foundation.samples.SimpleHorizontalPagerSample
+ *
+ * @param pageCount The number of pages this Pager will contain
+ * @param modifier A modifier instance to be applied to this Pager outer layout
+ * @param state The state to control this pager
+ * @param contentPadding a padding around the whole content. This will add padding for the
+ * content after it has been clipped, which is not possible via [modifier] param. You can use it
+ * to add a padding before the first page or after the last one. Use [pageSpacing] to add spacing
+ * between the pages.
+ * @param pageSize Use this to change how the pages will look like inside this pager.
+ * @param beyondBoundsPageCount Pages to load before and after the list of visible
+ * pages. Note: Be aware that using a large value for [beyondBoundsPageCount] will cause a lot of
+ * pages to be composed, measured and placed which will defeat the purpose of using lazy loading.
+ * This should be used as an optimization to pre-load a couple of pages before and after the visible
+ * ones.
+ * @param pageSpacing The amount of space to be used to separate the pages in this Pager
+ * @param verticalAlignment How pages are aligned vertically in this Pager.
+ * @param flingBehavior The [FlingBehavior] to be used for post scroll gestures.
+ * @param userScrollEnabled whether the scrolling via the user gestures or accessibility actions
+ * is allowed. You can still scroll programmatically using [PagerState.scroll] even when it is
+ * disabled.
+ * @param reverseLayout reverse the direction of scrolling and layout.
+ * @param key a stable and unique key representing the page. When you specify the key the scroll
+ * position will be maintained based on the key, which means if you add/remove pages before the
+ * current visible page the page with the given key will be kept as the first visible one.
+ * @param pageNestedScrollConnection A [NestedScrollConnection] that dictates how this [Pager]
+ * behaves with nested lists. The default behavior will see [Pager] to consume all nested deltas.
+ * @param pageContent This Pager's page Composable.
+ */
+@Composable
+@ExperimentalFoundationApi
+internal fun HorizontalPager(
+    pageCount: Int,
+    modifier: Modifier = Modifier,
+    state: PagerState = rememberPagerState(),
+    contentPadding: PaddingValues = PaddingValues(0.dp),
+    pageSize: PageSize = PageSize.Fill,
+    beyondBoundsPageCount: Int = 0,
+    pageSpacing: Dp = 0.dp,
+    verticalAlignment: Alignment.Vertical = Alignment.CenterVertically,
+    flingBehavior: SnapFlingBehavior = PagerDefaults.flingBehavior(state = state),
+    userScrollEnabled: Boolean = true,
+    reverseLayout: Boolean = false,
+    key: ((index: Int) -> Any)? = null,
+    pageNestedScrollConnection: NestedScrollConnection = PagerDefaults.pageNestedScrollConnection(
+        Orientation.Horizontal
+    ),
+    pageContent: @Composable (page: Int) -> Unit
+) {
+    Pager(
+        modifier = modifier,
+        pageCount = pageCount,
+        state = state,
+        contentPadding = contentPadding,
+        reverseLayout = reverseLayout,
+        orientation = Orientation.Horizontal,
+        flingBehavior = flingBehavior,
+        userScrollEnabled = userScrollEnabled,
+        pageSize = pageSize,
+        beyondBoundsPageCount = beyondBoundsPageCount,
+        pageSpacing = pageSpacing,
+        pageContent = pageContent,
+        pageNestedScrollConnection = pageNestedScrollConnection,
+        verticalAlignment = verticalAlignment,
+        horizontalAlignment = Alignment.CenterHorizontally,
+        key = key
+    )
+}
+
+/**
+ * A Pager that scrolls vertically. Pages are lazily placed in accordance to the available
+ * viewport size. By definition, pages in a [Pager] have the same size, defined by [pageSize] and
+ * use a snap animation (provided by [flingBehavior] to scroll pages into a specific position). You
+ * can use [beyondBoundsPageCount] to place more pages before and after the visible pages.
+ *
+ * If you need snapping with pages of different size, you can use a [SnapFlingBehavior] with a
+ * [SnapLayoutInfoProvider] adapted to a LazyList.
+ * @see androidx.compose.foundation.gestures.snapping.SnapLayoutInfoProvider for the implementation
+ * of a [SnapLayoutInfoProvider] that uses [androidx.compose.foundation.lazy.LazyListState].
+ *
+ * Please refer to the sample to learn how to use this API.
+ * @sample androidx.compose.foundation.samples.SimpleVerticalPagerSample
+ *
+ * @param pageCount The number of pages this Pager will contain
+ * @param modifier A modifier instance to be apply to this Pager outer layout
+ * @param state The state to control this pager
+ * @param contentPadding a padding around the whole content. This will add padding for the
+ * content after it has been clipped, which is not possible via [modifier] param. You can use it
+ * to add a padding before the first page or after the last one. Use [pageSpacing] to add spacing
+ * between the pages.
+ * @param pageSize Use this to change how the pages will look like inside this pager.
+ * @param beyondBoundsPageCount Pages to load before and after the list of visible
+ * pages. Note: Be aware that using a large value for [beyondBoundsPageCount] will cause a lot of
+ * pages to be composed, measured and placed which will defeat the purpose of using lazy loading.
+ * This should be used as an optimization to pre-load a couple of pages before and after the visible
+ * ones.
+ * @param pageSpacing The amount of space to be used to separate the pages in this Pager
+ * @param horizontalAlignment How pages are aligned horizontally in this Pager.
+ * @param flingBehavior The [FlingBehavior] to be used for post scroll gestures.
+ * @param userScrollEnabled whether the scrolling via the user gestures or accessibility actions
+ * is allowed. You can still scroll programmatically using [PagerState.scroll] even when it is
+ * disabled.
+ * @param reverseLayout reverse the direction of scrolling and layout.
+ * @param key a stable and unique key representing the page. When you specify the key the scroll
+ * position will be maintained based on the key, which means if you add/remove pages before the
+ * current visible page the page with the given key will be kept as the first visible one.
+ * @param pageNestedScrollConnection A [NestedScrollConnection] that dictates how this [Pager] behaves
+ * with nested lists. The default behavior will see [Pager] to consume all nested deltas.
+ * @param pageContent This Pager's page Composable.
+ */
+@Composable
+@ExperimentalFoundationApi
+internal fun VerticalPager(
+    pageCount: Int,
+    modifier: Modifier = Modifier,
+    state: PagerState = rememberPagerState(),
+    contentPadding: PaddingValues = PaddingValues(0.dp),
+    pageSize: PageSize = PageSize.Fill,
+    beyondBoundsPageCount: Int = 0,
+    pageSpacing: Dp = 0.dp,
+    horizontalAlignment: Alignment.Horizontal = Alignment.CenterHorizontally,
+    flingBehavior: SnapFlingBehavior = PagerDefaults.flingBehavior(state = state),
+    userScrollEnabled: Boolean = true,
+    reverseLayout: Boolean = false,
+    key: ((index: Int) -> Any)? = null,
+    pageNestedScrollConnection: NestedScrollConnection = PagerDefaults.pageNestedScrollConnection(
+        Orientation.Vertical
+    ),
+    pageContent: @Composable (page: Int) -> Unit
+) {
+    Pager(
+        modifier = modifier,
+        pageCount = pageCount,
+        state = state,
+        contentPadding = contentPadding,
+        reverseLayout = reverseLayout,
+        orientation = Orientation.Vertical,
+        flingBehavior = flingBehavior,
+        userScrollEnabled = userScrollEnabled,
+        pageSize = pageSize,
+        beyondBoundsPageCount = beyondBoundsPageCount,
+        pageSpacing = pageSpacing,
+        pageContent = pageContent,
+        pageNestedScrollConnection = pageNestedScrollConnection,
+        verticalAlignment = Alignment.CenterVertically,
+        horizontalAlignment = horizontalAlignment,
+        key = key
+    )
+}
+
+/**
+ * This is used to determine how Pages are laid out in [Pager]. By changing the size of the pages
+ * one can change how many pages are shown.
+ *
+ * Please refer to the sample to learn how to use this API.
+ * @sample androidx.compose.foundation.samples.CustomPageSizeSample
+ *
+ */
+@ExperimentalFoundationApi
+@Stable
+internal interface PageSize {
+
+    /**
+     * Based on [availableSpace] pick a size for the pages
+     * @param availableSpace The amount of space the pages in this Pager can use.
+     * @param pageSpacing The amount of space used to separate pages.
+     */
+    fun Density.calculateMainAxisPageSize(availableSpace: Int, pageSpacing: Int): Int
+
+    /**
+     * Pages take up the whole Pager size.
+     */
+    @ExperimentalFoundationApi
+    object Fill : PageSize {
+        override fun Density.calculateMainAxisPageSize(availableSpace: Int, pageSpacing: Int): Int {
+            return availableSpace
+        }
+    }
+
+    /**
+     * Multiple pages in a viewport
+     * @param pageSize A fixed size for pages
+     */
+    @ExperimentalFoundationApi
+    class Fixed(val pageSize: Dp) : PageSize {
+        override fun Density.calculateMainAxisPageSize(availableSpace: Int, pageSpacing: Int): Int {
+            return pageSize.roundToPx()
+        }
+    }
+}
+
+/**
+ * Contains the default values used by [Pager].
+ */
+@ExperimentalFoundationApi
+internal object PagerDefaults {
+
+    /**
+     * A [SnapFlingBehavior] that will snap pages to the start of the layout. One can use the
+     * given parameters to control how the snapping animation will happen.
+     * @see androidx.compose.foundation.gestures.snapping.SnapFlingBehavior for more information
+     * on what which parameter controls in the overall snapping animation.
+     *
+     * @param state The [PagerState] that controls the which to which this FlingBehavior will
+     * be applied to.
+     * @param pagerSnapDistance A way to control the snapping destination for this [Pager].
+     * The default behavior will result in any fling going to the next page in the direction of the
+     * fling (if the fling has enough velocity, otherwise we'll bounce back). Use
+     * [PagerSnapDistance.atMost] to define a maximum number of pages this [Pager] is allowed to
+     * fling after scrolling is finished and fling has started.
+     * @param lowVelocityAnimationSpec The animation spec used to approach the target offset. When
+     * the fling velocity is not large enough. Large enough means large enough to naturally decay.
+     * @param highVelocityAnimationSpec The animation spec used to approach the target offset. When
+     * the fling velocity is large enough. Large enough means large enough to naturally decay.
+     * @param snapAnimationSpec The animation spec used to finally snap to the position.
+     *
+     * @return An instance of [FlingBehavior] that will perform Snapping to the next page by
+     * default. The animation will be governed by the post scroll velocity and we'll use either
+     * [lowVelocityAnimationSpec] or [highVelocityAnimationSpec] to approach the snapped position
+     * and the last step of the animation will be performed by [snapAnimationSpec].
+     */
+    @Composable
+    fun flingBehavior(
+        state: PagerState,
+        pagerSnapDistance: PagerSnapDistance = PagerSnapDistance.atMost(1),
+        lowVelocityAnimationSpec: AnimationSpec<Float> = tween(
+            easing = LinearEasing,
+            durationMillis = LowVelocityAnimationDefaultDuration
+        ),
+        highVelocityAnimationSpec: DecayAnimationSpec<Float> = rememberSplineBasedDecay(),
+        snapAnimationSpec: AnimationSpec<Float> = spring(stiffness = Spring.StiffnessMediumLow),
+    ): SnapFlingBehavior {
+        val density = LocalDensity.current
+
+        return remember(
+            lowVelocityAnimationSpec,
+            highVelocityAnimationSpec,
+            snapAnimationSpec,
+            pagerSnapDistance,
+            density
+        ) {
+            val snapLayoutInfoProvider =
+                SnapLayoutInfoProvider(
+                    state,
+                    pagerSnapDistance,
+                    highVelocityAnimationSpec
+                )
+            SnapFlingBehavior(
+                snapLayoutInfoProvider = snapLayoutInfoProvider,
+                lowVelocityAnimationSpec = lowVelocityAnimationSpec,
+                highVelocityAnimationSpec = highVelocityAnimationSpec,
+                snapAnimationSpec = snapAnimationSpec,
+                density = density
+            )
+        }
+    }
+
+    /**
+     * The default implementation of Pager's pageNestedScrollConnection. All fling scroll deltas
+     * will be consumed by the Pager.
+     *
+     * @param orientation The orientation of the pager. This will be used to determine which
+     * direction it will consume everything. The other direction will not be consumed.
+     */
+    fun pageNestedScrollConnection(orientation: Orientation): NestedScrollConnection {
+        return if (orientation == Orientation.Horizontal) {
+            ConsumeHorizontalFlingNestedScrollConnection
+        } else {
+            ConsumeVerticalFlingNestedScrollConnection
+        }
+    }
+}
+
+/**
+ * [PagerSnapDistance] defines the way the [Pager] will treat the distance between the current
+ * page and the page where it will settle.
+ */
+@ExperimentalFoundationApi
+@Stable
+internal interface PagerSnapDistance {
+
+    /** Provides a chance to change where the [Pager] fling will settle.
+     *
+     * @param startPage The current page right before the fling starts.
+     * @param suggestedTargetPage The proposed target page where this fling will stop. This target
+     * will be the page that will be correctly positioned (snapped) after naturally decaying with
+     * [velocity] using a [DecayAnimationSpec].
+     * @param velocity The initial fling velocity.
+     * @param pageSize The page size for this [Pager].
+     * @param pageSpacing The spacing used between pages.
+     *
+     * @return An updated target page where to settle. Note that this value needs to be between 0
+     * and the total count of pages in this pager. If an invalid value is passed, the pager will
+     * coerce within the valid values.
+     */
+    fun calculateTargetPage(
+        startPage: Int,
+        suggestedTargetPage: Int,
+        velocity: Float,
+        pageSize: Int,
+        pageSpacing: Int
+    ): Int
+
+    companion object {
+        /**
+         * Limits the maximum number of pages that can be flung per fling gesture.
+         * @param pages The maximum number of extra pages that can be flung at once.
+         */
+        fun atMost(pages: Int): PagerSnapDistance {
+            require(pages >= 0) {
+                "pages should be greater than or equal to 0. You have used $pages."
+            }
+            return PagerSnapDistanceMaxPages(pages)
+        }
+    }
+}
+
+/**
+ * Limits the maximum number of pages that can be flung per fling gesture.
+ * @param pagesLimit The maximum number of extra pages that can be flung at once.
+ */
+@OptIn(ExperimentalFoundationApi::class)
+internal class PagerSnapDistanceMaxPages(private val pagesLimit: Int) : PagerSnapDistance {
+    override fun calculateTargetPage(
+        startPage: Int,
+        suggestedTargetPage: Int,
+        velocity: Float,
+        pageSize: Int,
+        pageSpacing: Int,
+    ): Int {
+        return suggestedTargetPage.coerceIn(startPage - pagesLimit, startPage + pagesLimit)
+    }
+
+    override fun equals(other: Any?): Boolean {
+        return if (other is PagerSnapDistanceMaxPages) {
+            this.pagesLimit == other.pagesLimit
+        } else {
+            false
+        }
+    }
+
+    override fun hashCode(): Int {
+        return pagesLimit.hashCode()
+    }
+}
+
+@OptIn(ExperimentalFoundationApi::class)
+private fun SnapLayoutInfoProvider(
+    pagerState: PagerState,
+    pagerSnapDistance: PagerSnapDistance,
+    decayAnimationSpec: DecayAnimationSpec<Float>
+): SnapLayoutInfoProvider {
+    return object : SnapLayoutInfoProvider {
+        val layoutInfo: PagerLayoutInfo
+            get() = pagerState.layoutInfo
+
+        override fun Density.calculateSnappingOffsetBounds(): ClosedFloatingPointRange<Float> {
+            var lowerBoundOffset = Float.NEGATIVE_INFINITY
+            var upperBoundOffset = Float.POSITIVE_INFINITY
+
+            layoutInfo.visiblePagesInfo.fastForEach { page ->
+                val offset = calculateDistanceToDesiredSnapPosition(
+                    layoutInfo,
+                    page,
+                    SnapAlignmentStartToStart
+                )
+
+                // Find page that is closest to the snap position, but before it
+                if (offset <= 0 && offset > lowerBoundOffset) {
+                    lowerBoundOffset = offset
+                }
+
+                // Find page that is closest to the snap position, but after it
+                if (offset >= 0 && offset < upperBoundOffset) {
+                    upperBoundOffset = offset
+                }
+            }
+
+            return lowerBoundOffset.rangeTo(upperBoundOffset)
+        }
+
+        override fun Density.calculateSnapStepSize(): Float = layoutInfo.pageSize.toFloat()
+
+        override fun Density.calculateApproachOffset(initialVelocity: Float): Float {
+            val effectivePageSizePx = pagerState.pageSize + pagerState.pageSpacing
+            val animationOffsetPx =
+                decayAnimationSpec.calculateTargetValue(0f, initialVelocity)
+            val startPage = pagerState.firstVisiblePageInfo?.let {
+                if (initialVelocity < 0) it.index + 1 else it.index
+            } ?: pagerState.currentPage
+
+            val scrollOffset =
+                layoutInfo.visiblePagesInfo.fastFirstOrNull { it.index == startPage }?.offset ?: 0
+
+            debugLog {
+                "Initial Offset=$scrollOffset " +
+                    "\nAnimation Offset=$animationOffsetPx " +
+                    "\nFling Start Page=$startPage " +
+                    "\nEffective Page Size=$effectivePageSizePx"
+            }
+
+            val targetOffsetPx = startPage * effectivePageSizePx + animationOffsetPx
+
+            val targetPageValue = targetOffsetPx / effectivePageSizePx
+            val targetPage = if (initialVelocity > 0) {
+                ceil(targetPageValue)
+            } else {
+                floor(targetPageValue)
+            }.toInt().coerceIn(0, pagerState.pageCount)
+
+            debugLog { "Fling Target Page=$targetPage" }
+
+            val correctedTargetPage = pagerSnapDistance.calculateTargetPage(
+                startPage,
+                targetPage,
+                initialVelocity,
+                pagerState.pageSize,
+                pagerState.pageSpacing
+            ).coerceIn(0, pagerState.pageCount)
+
+            debugLog { "Fling Corrected Target Page=$correctedTargetPage" }
+
+            val proposedFlingOffset = (correctedTargetPage - startPage) * effectivePageSizePx
+
+            debugLog { "Proposed Fling Approach Offset=$proposedFlingOffset" }
+
+            val flingApproachOffsetPx =
+                (proposedFlingOffset.absoluteValue - scrollOffset.absoluteValue).coerceAtLeast(0)
+
+            return if (flingApproachOffsetPx == 0) {
+                flingApproachOffsetPx.toFloat()
+            } else {
+                flingApproachOffsetPx * initialVelocity.sign
+            }.also {
+                debugLog { "Fling Approach Offset=$it" }
+            }
+        }
+    }
+}
+
+@OptIn(ExperimentalFoundationApi::class)
+internal class PagerWrapperFlingBehavior(
+    val originalFlingBehavior: SnapFlingBehavior,
+    val pagerState: PagerState
+) : FlingBehavior {
+    override suspend fun ScrollScope.performFling(initialVelocity: Float): Float {
+        return with(originalFlingBehavior) {
+            performFling(initialVelocity) { remainingScrollOffset ->
+                pagerState.snapRemainingScrollOffset = remainingScrollOffset
+            }
+        }
+    }
+}
+
+private val ConsumeHorizontalFlingNestedScrollConnection =
+    ConsumeAllFlingOnDirection(Orientation.Horizontal)
+private val ConsumeVerticalFlingNestedScrollConnection =
+    ConsumeAllFlingOnDirection(Orientation.Vertical)
+
+private class ConsumeAllFlingOnDirection(val orientation: Orientation) : NestedScrollConnection {
+
+    fun Velocity.consumeOnOrientation(orientation: Orientation): Velocity {
+        return if (orientation == Orientation.Vertical) {
+            copy(x = 0f)
+        } else {
+            copy(y = 0f)
+        }
+    }
+
+    fun Offset.consumeOnOrientation(orientation: Orientation): Offset {
+        return if (orientation == Orientation.Vertical) {
+            copy(x = 0f)
+        } else {
+            copy(y = 0f)
+        }
+    }
+
+    override fun onPostScroll(
+        consumed: Offset,
+        available: Offset,
+        source: NestedScrollSource
+    ): Offset {
+        return when (source) {
+            NestedScrollSource.Fling -> available.consumeOnOrientation(orientation)
+            else -> Offset.Zero
+        }
+    }
+
+    override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity {
+        return available.consumeOnOrientation(orientation)
+    }
+}
+
+@OptIn(ExperimentalFoundationApi::class)
+@Suppress("ComposableModifierFactory")
+@Composable
+internal fun Modifier.pagerSemantics(state: PagerState, isVertical: Boolean): Modifier {
+    val scope = rememberCoroutineScope()
+    fun performForwardPaging(): Boolean {
+        return if (state.canScrollForward) {
+            scope.launch {
+                state.animateToNextPage()
+            }
+            true
+        } else {
+            false
+        }
+    }
+
+    fun performBackwardPaging(): Boolean {
+        return if (state.canScrollBackward) {
+            scope.launch {
+                state.animateToPreviousPage()
+            }
+            true
+        } else {
+            false
+        }
+    }
+
+    return this.then(Modifier.semantics {
+        if (isVertical) {
+            pageUp { performBackwardPaging() }
+            pageDown { performForwardPaging() }
+        } else {
+            pageLeft { performBackwardPaging() }
+            pageRight { performForwardPaging() }
+        }
+    })
+}
+
+@OptIn(ExperimentalFoundationApi::class)
+internal fun Density.calculateDistanceToDesiredSnapPosition(
+    layoutInfo: PagerLayoutInfo,
+    page: PageInfo,
+    positionInLayout: Density.(layoutSize: Float, itemSize: Float) -> Float
+): Float {
+    val containerSize =
+        with(layoutInfo) { singleAxisViewportSize - beforeContentPadding - afterContentPadding }
+
+    val desiredDistance =
+        positionInLayout(containerSize.toFloat(), layoutInfo.pageSize.toFloat())
+
+    val itemCurrentPosition = page.offset
+    return itemCurrentPosition - desiredDistance
+}
+
+@OptIn(ExperimentalFoundationApi::class)
+private val PagerLayoutInfo.singleAxisViewportSize: Int
+    get() = if (orientation == Orientation.Vertical) viewportSize.height else viewportSize.width
+
+private const val LowVelocityAnimationDefaultDuration = 500
+
+private const val DEBUG = false
+private inline fun debugLog(generateMsg: () -> String) {
+    if (DEBUG) {
+        println("Pager: ${generateMsg()}")
+    }
+}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/lazy/PagerBeyondBoundsModifier.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/lazy/PagerBeyondBoundsModifier.kt
new file mode 100644
index 0000000..b55d013
--- /dev/null
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/lazy/PagerBeyondBoundsModifier.kt
@@ -0,0 +1,73 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package androidx.compose.foundation.pager.lazy
+
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.foundation.lazy.LazyListBeyondBoundsInfo
+import androidx.compose.foundation.lazy.layout.BeyondBoundsState
+import androidx.compose.foundation.lazy.layout.LazyLayoutBeyondBoundsModifierLocal
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalLayoutDirection
+
+/**
+ * This modifier is used to measure and place additional pages when the Pager receives a
+ * request to layout pages beyond the visible bounds.
+ */
+@OptIn(ExperimentalFoundationApi::class)
+@Suppress("ComposableModifierFactory")
+@Composable
+internal fun Modifier.pagerBeyondBoundsModifier(
+    state: PagerState,
+    beyondBoundsInfo: LazyListBeyondBoundsInfo,
+    reverseLayout: Boolean,
+    orientation: Orientation
+): Modifier {
+    val layoutDirection = LocalLayoutDirection.current
+    return this then remember(
+        state,
+        beyondBoundsInfo,
+        reverseLayout,
+        layoutDirection,
+        orientation
+    ) {
+        LazyLayoutBeyondBoundsModifierLocal(
+            PagerBeyondBoundsState(state),
+            beyondBoundsInfo,
+            reverseLayout,
+            layoutDirection,
+            orientation
+        )
+    }
+}
+
+@OptIn(ExperimentalFoundationApi::class)
+internal class PagerBeyondBoundsState(val state: PagerState) : BeyondBoundsState {
+    override fun remeasure() {
+        state.remeasurement?.forceRemeasure()
+    }
+
+    override val itemCount: Int
+        get() = state.layoutInfo.pagesCount
+    override val hasVisibleItems: Boolean
+        get() = state.layoutInfo.visiblePagesInfo.isNotEmpty()
+    override val firstVisibleIndex: Int
+        get() = state.firstVisiblePage
+    override val lastVisibleIndex: Int
+        get() = state.layoutInfo.visiblePagesInfo.last().index
+}
\ No newline at end of file
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/lazy/PagerLayoutInfo.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/lazy/PagerLayoutInfo.kt
new file mode 100644
index 0000000..d482850
--- /dev/null
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/lazy/PagerLayoutInfo.kt
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.foundation.pager.lazy
+
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.ui.unit.IntSize
+
+@ExperimentalFoundationApi
+internal interface PagerLayoutInfo {
+    val visiblePagesInfo: List<PageInfo>
+    val pagesCount: Int
+    val pageSize: Int
+    val pageSpacing: Int
+    val viewportStartOffset: Int
+    val viewportEndOffset: Int
+    val beforeContentPadding: Int
+    val afterContentPadding: Int
+    val viewportSize: IntSize
+    val orientation: Orientation
+    val reverseLayout: Boolean
+}
\ No newline at end of file
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/lazy/PagerMeasure.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/lazy/PagerMeasure.kt
new file mode 100644
index 0000000..1d01a66
--- /dev/null
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/lazy/PagerMeasure.kt
@@ -0,0 +1,645 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.foundation.pager.lazy
+
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.fastFilter
+import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.foundation.layout.Arrangement.Absolute.spacedBy
+import androidx.compose.foundation.lazy.LazyListBeyondBoundsInfo
+import androidx.compose.foundation.lazy.layout.LazyLayoutMeasureScope
+import androidx.compose.foundation.lazy.layout.LazyLayoutPinnedItemList
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.layout.MeasureResult
+import androidx.compose.ui.layout.Placeable
+import androidx.compose.ui.unit.Constraints
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.IntOffset
+import androidx.compose.ui.unit.LayoutDirection
+import androidx.compose.ui.unit.constrainHeight
+import androidx.compose.ui.unit.constrainWidth
+import androidx.compose.ui.util.fastForEach
+import androidx.compose.ui.util.fastMaxBy
+import kotlin.math.abs
+import kotlin.math.min
+import kotlin.math.roundToInt
+import kotlin.math.sign
+
+@OptIn(ExperimentalFoundationApi::class)
+internal fun LazyLayoutMeasureScope.measurePager(
+    pageCount: Int,
+    pagerItemProvider: PagerLazyLayoutItemProvider,
+    mainAxisAvailableSize: Int,
+    beforeContentPadding: Int,
+    afterContentPadding: Int,
+    spaceBetweenPages: Int,
+    firstVisiblePage: Int,
+    firstVisiblePageOffset: Int,
+    scrollToBeConsumed: Float,
+    constraints: Constraints,
+    orientation: Orientation,
+    verticalAlignment: Alignment.Vertical?,
+    horizontalAlignment: Alignment.Horizontal?,
+    reverseLayout: Boolean,
+    visualPageOffset: IntOffset,
+    pageAvailableSize: Int,
+    beyondBoundsPageCount: Int,
+    beyondBoundsInfo: LazyListBeyondBoundsInfo,
+    pinnedPages: LazyLayoutPinnedItemList,
+    layout: (Int, Int, Placeable.PlacementScope.() -> Unit) -> MeasureResult
+): PagerMeasureResult {
+    require(beforeContentPadding >= 0)
+    require(afterContentPadding >= 0)
+
+    val pageSizeWithSpacing = (pageAvailableSize + spaceBetweenPages).coerceAtLeast(0)
+
+    debugLog { "Remeasuring..." }
+
+    return if (pageCount <= 0) {
+        PagerMeasureResult(
+            visiblePagesInfo = emptyList(),
+            pagesCount = 0,
+            pageSize = pageAvailableSize,
+            pageSpacing = spaceBetweenPages,
+            afterContentPadding = afterContentPadding,
+            orientation = orientation,
+            viewportStartOffset = -beforeContentPadding,
+            viewportEndOffset = mainAxisAvailableSize + afterContentPadding,
+            measureResult = layout(constraints.minWidth, constraints.minHeight) {},
+            consumedScroll = 0f,
+            closestPageToSnapPosition = null,
+            firstVisiblePage = null,
+            firstVisiblePageOffset = 0,
+            reverseLayout = false,
+            canScrollForward = false
+        )
+    } else {
+
+        val childConstraints = Constraints(
+            maxWidth = if (orientation == Orientation.Vertical) {
+                constraints.maxWidth
+            } else {
+                pageAvailableSize
+            },
+            maxHeight = if (orientation != Orientation.Vertical) {
+                constraints.maxHeight
+            } else {
+                pageAvailableSize
+            }
+        )
+
+        var currentFirstPage = firstVisiblePage
+        var currentFirstPageScrollOffset = firstVisiblePageOffset
+        if (currentFirstPage >= pageCount) {
+            // the data set has been updated and now we have less pages that we were
+            // scrolled to before
+            currentFirstPage = pageCount - 1
+            currentFirstPageScrollOffset = 0
+        }
+
+        // represents the real amount of scroll we applied as a result of this measure pass.
+        var scrollDelta = scrollToBeConsumed.roundToInt()
+
+        // applying the whole requested scroll offset. we will figure out if we can't consume
+        // all of it later
+        currentFirstPageScrollOffset -= scrollDelta
+
+        // if the current scroll offset is less than minimally possible
+        if (currentFirstPage == 0 && currentFirstPageScrollOffset < 0) {
+            scrollDelta += currentFirstPageScrollOffset
+            currentFirstPageScrollOffset = 0
+        }
+
+        // this will contain all the measured pages representing the visible pages
+        val visiblePages = mutableListOf<MeasuredPage>()
+
+        // define min and max offsets
+        val minOffset = -beforeContentPadding + if (spaceBetweenPages < 0) spaceBetweenPages else 0
+        val maxOffset = mainAxisAvailableSize
+
+        // include the start padding so we compose pages in the padding area and neutralise page
+        // spacing (if the spacing is negative this will make sure the previous page is composed)
+        // before starting scrolling forward we will remove it back
+        currentFirstPageScrollOffset += minOffset
+
+        // max of cross axis sizes of all visible pages
+        var maxCrossAxis = 0
+
+        // we had scrolled backward or we compose pages in the start padding area, which means
+        // pages before current firstPageScrollOffset should be visible. compose them and update
+        // firstPageScrollOffset
+        while (currentFirstPageScrollOffset < 0 && currentFirstPage > 0) {
+            val previous = currentFirstPage - 1
+            val measuredPage = getAndMeasure(
+                index = previous,
+                childConstraints = childConstraints,
+                pagerItemProvider = pagerItemProvider,
+                visualPageOffset = visualPageOffset,
+                orientation = orientation,
+                horizontalAlignment = horizontalAlignment,
+                verticalAlignment = verticalAlignment,
+                afterContentPadding = afterContentPadding,
+                beforeContentPadding = beforeContentPadding,
+                layoutDirection = layoutDirection,
+                reverseLayout = reverseLayout,
+                pageAvailableSize = pageAvailableSize
+            )
+            visiblePages.add(0, measuredPage)
+            maxCrossAxis = maxOf(maxCrossAxis, measuredPage.crossAxisSize)
+            currentFirstPageScrollOffset += pageSizeWithSpacing
+            currentFirstPage = previous
+        }
+
+        // if we were scrolled backward, but there were not enough pages before. this means
+        // not the whole scroll was consumed
+        if (currentFirstPageScrollOffset < minOffset) {
+            scrollDelta += currentFirstPageScrollOffset
+            currentFirstPageScrollOffset = minOffset
+        }
+
+        // neutralize previously added padding as we stopped filling the before content padding
+        currentFirstPageScrollOffset -= minOffset
+
+        var index = currentFirstPage
+        val maxMainAxis = (maxOffset + afterContentPadding).coerceAtLeast(0)
+        var currentMainAxisOffset = -currentFirstPageScrollOffset
+
+        // first we need to skip pages we already composed while composing backward
+        visiblePages.fastForEach {
+            index++
+            currentMainAxisOffset += pageSizeWithSpacing
+        }
+
+        // then composing visible pages forward until we fill the whole viewport.
+        // we want to have at least one page in visiblePages even if in fact all the pages are
+        // offscreen, this can happen if the content padding is larger than the available size.
+        while (index < pageCount &&
+            (currentMainAxisOffset < maxMainAxis ||
+                currentMainAxisOffset <= 0 || // filling beforeContentPadding area
+                visiblePages.isEmpty())
+        ) {
+            val measuredPage = getAndMeasure(
+                index = index,
+                childConstraints = childConstraints,
+                pagerItemProvider = pagerItemProvider,
+                visualPageOffset = visualPageOffset,
+                orientation = orientation,
+                horizontalAlignment = horizontalAlignment,
+                verticalAlignment = verticalAlignment,
+                afterContentPadding = afterContentPadding,
+                beforeContentPadding = beforeContentPadding,
+                layoutDirection = layoutDirection,
+                reverseLayout = reverseLayout,
+                pageAvailableSize = pageAvailableSize
+            )
+            currentMainAxisOffset += pageSizeWithSpacing
+
+            if (currentMainAxisOffset <= minOffset && index != pageCount - 1) {
+                // this page is offscreen and will not be placed. advance firstVisiblePage
+                currentFirstPage = index + 1
+                currentFirstPageScrollOffset -= pageSizeWithSpacing
+            } else {
+                maxCrossAxis = maxOf(maxCrossAxis, measuredPage.crossAxisSize)
+                visiblePages.add(measuredPage)
+            }
+
+            index++
+        }
+
+        // we didn't fill the whole viewport with pages starting from firstVisiblePage.
+        // lets try to scroll back if we have enough pages before firstVisiblePage.
+        if (currentMainAxisOffset < maxOffset) {
+            val toScrollBack = maxOffset - currentMainAxisOffset
+            currentFirstPageScrollOffset -= toScrollBack
+            currentMainAxisOffset += toScrollBack
+            while (currentFirstPageScrollOffset < beforeContentPadding &&
+                currentFirstPage > 0
+            ) {
+                val previousIndex = currentFirstPage - 1
+                val measuredPage = getAndMeasure(
+                    index = previousIndex,
+                    childConstraints = childConstraints,
+                    pagerItemProvider = pagerItemProvider,
+                    visualPageOffset = visualPageOffset,
+                    orientation = orientation,
+                    horizontalAlignment = horizontalAlignment,
+                    verticalAlignment = verticalAlignment,
+                    afterContentPadding = afterContentPadding,
+                    beforeContentPadding = beforeContentPadding,
+                    layoutDirection = layoutDirection,
+                    reverseLayout = reverseLayout,
+                    pageAvailableSize = pageAvailableSize
+                )
+                visiblePages.add(0, measuredPage)
+                maxCrossAxis = maxOf(maxCrossAxis, measuredPage.crossAxisSize)
+                currentFirstPageScrollOffset += pageSizeWithSpacing
+                currentFirstPage = previousIndex
+            }
+            scrollDelta += toScrollBack
+            if (currentFirstPageScrollOffset < 0) {
+                scrollDelta += currentFirstPageScrollOffset
+                currentMainAxisOffset += currentFirstPageScrollOffset
+                currentFirstPageScrollOffset = 0
+            }
+        }
+
+        // report the amount of pixels we consumed. scrollDelta can be smaller than
+        // scrollToBeConsumed if there were not enough pages to fill the offered space or it
+        // can be larger if pages were resized, or if, for example, we were previously
+        // displaying the page 15, but now we have only 10 pages in total in the data set.
+        val consumedScroll = if (scrollToBeConsumed.roundToInt().sign == scrollDelta.sign &&
+            abs(scrollToBeConsumed.roundToInt()) >= abs(scrollDelta)
+        ) {
+            scrollDelta.toFloat()
+        } else {
+            scrollToBeConsumed
+        }
+
+        // the initial offset for pages from visiblePages list
+        require(currentFirstPageScrollOffset >= 0)
+        val visiblePagesScrollOffset = -currentFirstPageScrollOffset
+        var firstPage = visiblePages.first()
+
+        // even if we compose pages to fill before content padding we should ignore pages fully
+        // located there for the state's scroll position calculation (first page + first offset)
+        if (beforeContentPadding > 0 || spaceBetweenPages < 0) {
+            for (i in visiblePages.indices) {
+                val size = pageSizeWithSpacing
+                if (currentFirstPageScrollOffset != 0 && size <= currentFirstPageScrollOffset &&
+                    i != visiblePages.lastIndex
+                ) {
+                    currentFirstPageScrollOffset -= size
+                    firstPage = visiblePages[i + 1]
+                } else {
+                    break
+                }
+            }
+        }
+
+        // Compose extra pages before
+        val extraPagesBefore = createPagesBeforeList(
+            beyondBoundsInfo = beyondBoundsInfo,
+            currentFirstPage = currentFirstPage,
+            pagesCount = pageCount,
+            beyondBoundsPageCount = beyondBoundsPageCount,
+            pinnedPages = pinnedPages
+        ) {
+            getAndMeasure(
+                index = it,
+                childConstraints = childConstraints,
+                pagerItemProvider = pagerItemProvider,
+                visualPageOffset = visualPageOffset,
+                orientation = orientation,
+                horizontalAlignment = horizontalAlignment,
+                verticalAlignment = verticalAlignment,
+                afterContentPadding = afterContentPadding,
+                beforeContentPadding = beforeContentPadding,
+                layoutDirection = layoutDirection,
+                reverseLayout = reverseLayout,
+                pageAvailableSize = pageAvailableSize
+            )
+        }
+
+        // Update maxCrossAxis with extra pages
+        extraPagesBefore.fastForEach {
+            maxCrossAxis = maxOf(maxCrossAxis, it.crossAxisSize)
+        }
+
+        // Compose pages after last page
+        val extraPagesAfter = createPagesAfterList(
+            beyondBoundsInfo = beyondBoundsInfo,
+            visiblePages = visiblePages,
+            pagesCount = pageCount,
+            beyondBoundsPageCount = beyondBoundsPageCount,
+            pinnedPages = pinnedPages
+        ) {
+            getAndMeasure(
+                index = it,
+                childConstraints = childConstraints,
+                pagerItemProvider = pagerItemProvider,
+                visualPageOffset = visualPageOffset,
+                orientation = orientation,
+                horizontalAlignment = horizontalAlignment,
+                verticalAlignment = verticalAlignment,
+                afterContentPadding = afterContentPadding,
+                beforeContentPadding = beforeContentPadding,
+                layoutDirection = layoutDirection,
+                reverseLayout = reverseLayout,
+                pageAvailableSize = pageAvailableSize
+            )
+        }
+
+        // Update maxCrossAxis with extra pages
+        extraPagesAfter.fastForEach {
+            maxCrossAxis = maxOf(maxCrossAxis, it.crossAxisSize)
+        }
+
+        val noExtraPages = firstPage == visiblePages.first() &&
+            extraPagesBefore.isEmpty() &&
+            extraPagesAfter.isEmpty()
+
+        val layoutWidth = constraints
+            .constrainWidth(
+                if (orientation == Orientation.Vertical)
+                    maxCrossAxis
+                else
+                    currentMainAxisOffset
+            )
+        val layoutHeight = constraints
+            .constrainHeight(
+                if (orientation == Orientation.Vertical)
+                    currentMainAxisOffset
+                else
+                    maxCrossAxis
+            )
+
+        val positionedPages = calculatePagesOffsets(
+            pages = visiblePages,
+            extraPagesBefore = extraPagesBefore,
+            extraPagesAfter = extraPagesAfter,
+            layoutWidth = layoutWidth,
+            layoutHeight = layoutHeight,
+            finalMainAxisOffset = currentMainAxisOffset,
+            maxOffset = maxOffset,
+            pagesScrollOffset = visiblePagesScrollOffset,
+            orientation = orientation,
+            reverseLayout = reverseLayout,
+            density = this,
+            pageAvailableSize = pageAvailableSize,
+            spaceBetweenPages = spaceBetweenPages
+        )
+
+        val visiblePagesInfo = if (noExtraPages) positionedPages else positionedPages.fastFilter {
+            (it.index >= visiblePages.first().index && it.index <= visiblePages.last().index)
+        }
+        val viewPortSize = if (orientation == Orientation.Vertical) layoutHeight else layoutWidth
+
+        val closestPageToSnapPosition = visiblePagesInfo.fastMaxBy {
+            -abs(
+                calculateDistanceToDesiredSnapPosition(
+                    viewPortSize,
+                    beforeContentPadding,
+                    afterContentPadding,
+                    pageAvailableSize,
+                    it,
+                    SnapAlignmentStartToStart
+                )
+            )
+        }
+
+        return PagerMeasureResult(
+            firstVisiblePage = firstPage,
+            firstVisiblePageOffset = currentFirstPageScrollOffset,
+            closestPageToSnapPosition = closestPageToSnapPosition,
+            consumedScroll = consumedScroll,
+            measureResult = layout(layoutWidth, layoutHeight) {
+                positionedPages.fastForEach {
+                    it.place(this)
+                }
+            },
+            viewportStartOffset = -beforeContentPadding,
+            viewportEndOffset = maxOffset + afterContentPadding,
+            visiblePagesInfo = visiblePagesInfo,
+            pagesCount = pageCount,
+            reverseLayout = reverseLayout,
+            orientation = orientation,
+            pageSize = pageAvailableSize,
+            pageSpacing = spaceBetweenPages,
+            afterContentPadding = afterContentPadding,
+            canScrollForward = index < pageCount || currentMainAxisOffset > maxOffset
+        )
+    }
+}
+
+@OptIn(ExperimentalFoundationApi::class)
+private fun Density.calculateDistanceToDesiredSnapPosition(
+    axisViewPortSize: Int,
+    beforeContentPadding: Int,
+    afterContentPadding: Int,
+    pageSize: Int,
+    page: PageInfo,
+    positionInLayout: Density.(layoutSize: Float, itemSize: Float) -> Float
+): Float {
+    val containerSize = axisViewPortSize - beforeContentPadding - afterContentPadding
+
+    val desiredDistance =
+        positionInLayout(containerSize.toFloat(), pageSize.toFloat())
+
+    val itemCurrentPosition = page.offset
+    return itemCurrentPosition - desiredDistance
+}
+
+@OptIn(ExperimentalFoundationApi::class)
+private fun createPagesAfterList(
+    beyondBoundsInfo: LazyListBeyondBoundsInfo,
+    visiblePages: MutableList<MeasuredPage>,
+    pagesCount: Int,
+    beyondBoundsPageCount: Int,
+    pinnedPages: LazyLayoutPinnedItemList,
+    getAndMeasure: (Int) -> MeasuredPage
+): List<MeasuredPage> {
+    fun LazyListBeyondBoundsInfo.endIndex() = min(end, pagesCount - 1)
+
+    var list: MutableList<MeasuredPage>? = null
+
+    var end = visiblePages.last().index
+
+    fun addPage(index: Int) {
+        if (list == null) list = mutableListOf()
+        requireNotNull(list).add(getAndMeasure(index))
+    }
+
+    if (beyondBoundsInfo.hasIntervals()) {
+        end = maxOf(beyondBoundsInfo.endIndex(), end)
+    }
+
+    end = minOf(end + beyondBoundsPageCount, pagesCount - 1)
+
+    for (i in visiblePages.last().index + 1..end) {
+        addPage(i)
+    }
+
+    pinnedPages.fastForEach { page ->
+        if (page.index in (end + 1) until pagesCount) {
+            addPage(page.index)
+        }
+    }
+
+    return list ?: emptyList()
+}
+
+@OptIn(ExperimentalFoundationApi::class)
+private fun createPagesBeforeList(
+    beyondBoundsInfo: LazyListBeyondBoundsInfo,
+    currentFirstPage: Int,
+    pagesCount: Int,
+    beyondBoundsPageCount: Int,
+    pinnedPages: LazyLayoutPinnedItemList,
+    getAndMeasure: (Int) -> MeasuredPage
+): List<MeasuredPage> {
+    fun LazyListBeyondBoundsInfo.startIndex() = min(start, pagesCount - 1)
+
+    var list: MutableList<MeasuredPage>? = null
+
+    var start = currentFirstPage
+
+    fun addPage(index: Int) {
+        if (list == null) list = mutableListOf()
+        requireNotNull(list).add(
+            getAndMeasure(index)
+        )
+    }
+
+    if (beyondBoundsInfo.hasIntervals()) {
+        start = minOf(beyondBoundsInfo.startIndex(), start)
+    }
+
+    start = maxOf(0, start - beyondBoundsPageCount)
+
+    for (i in currentFirstPage - 1 downTo start) {
+        addPage(i)
+    }
+
+    pinnedPages.fastForEach { page ->
+        if (page.index < start) {
+            addPage(page.index)
+        }
+    }
+
+    return list ?: emptyList()
+}
+
+@OptIn(ExperimentalFoundationApi::class)
+private fun LazyLayoutMeasureScope.getAndMeasure(
+    index: Int,
+    childConstraints: Constraints,
+    pagerItemProvider: PagerLazyLayoutItemProvider,
+    visualPageOffset: IntOffset,
+    orientation: Orientation,
+    horizontalAlignment: Alignment.Horizontal?,
+    verticalAlignment: Alignment.Vertical?,
+    afterContentPadding: Int,
+    beforeContentPadding: Int,
+    layoutDirection: LayoutDirection,
+    reverseLayout: Boolean,
+    pageAvailableSize: Int
+): MeasuredPage {
+    val key = pagerItemProvider.getKey(index)
+    val placeable = measure(index, childConstraints)
+
+    return MeasuredPage(
+        index = index,
+        placeables = placeable,
+        visualOffset = visualPageOffset,
+        horizontalAlignment = horizontalAlignment,
+        verticalAlignment = verticalAlignment,
+        afterContentPadding = afterContentPadding,
+        beforeContentPadding = beforeContentPadding,
+        layoutDirection = layoutDirection,
+        reverseLayout = reverseLayout,
+        size = pageAvailableSize,
+        orientation = orientation,
+        key = key
+    )
+}
+
+@OptIn(ExperimentalFoundationApi::class)
+private fun LazyLayoutMeasureScope.calculatePagesOffsets(
+    pages: List<MeasuredPage>,
+    extraPagesBefore: List<MeasuredPage>,
+    extraPagesAfter: List<MeasuredPage>,
+    layoutWidth: Int,
+    layoutHeight: Int,
+    finalMainAxisOffset: Int,
+    maxOffset: Int,
+    pagesScrollOffset: Int,
+    orientation: Orientation,
+    reverseLayout: Boolean,
+    density: Density,
+    spaceBetweenPages: Int,
+    pageAvailableSize: Int
+): MutableList<PositionedPage> {
+    val pageSizeWithSpacing = (pageAvailableSize + spaceBetweenPages)
+    val mainAxisLayoutSize = if (orientation == Orientation.Vertical) layoutHeight else layoutWidth
+    val hasSpareSpace = finalMainAxisOffset < minOf(mainAxisLayoutSize, maxOffset)
+    if (hasSpareSpace) {
+        check(pagesScrollOffset == 0)
+    }
+    val positionedPages =
+        ArrayList<PositionedPage>(pages.size + extraPagesBefore.size + extraPagesAfter.size)
+
+    if (hasSpareSpace) {
+        require(extraPagesBefore.isEmpty() && extraPagesAfter.isEmpty())
+
+        val pagesCount = pages.size
+        fun Int.reverseAware() =
+            if (!reverseLayout) this else pagesCount - this - 1
+
+        val sizes = IntArray(pagesCount) { pageAvailableSize }
+        val offsets = IntArray(pagesCount) { 0 }
+
+        val arrangement = spacedBy(pageAvailableSize.toDp())
+        if (orientation == Orientation.Vertical) {
+            with(arrangement) { density.arrange(mainAxisLayoutSize, sizes, offsets) }
+        } else {
+            with(arrangement) {
+                // Enforces Ltr layout direction as it is mirrored with placeRelative later.
+                density.arrange(mainAxisLayoutSize, sizes, LayoutDirection.Ltr, offsets)
+            }
+        }
+
+        val reverseAwareOffsetIndices =
+            if (!reverseLayout) offsets.indices else offsets.indices.reversed()
+        for (index in reverseAwareOffsetIndices) {
+            val absoluteOffset = offsets[index]
+            // when reverseLayout == true, offsets are stored in the reversed order to pages
+            val page = pages[index.reverseAware()]
+            val relativeOffset = if (reverseLayout) {
+                // inverse offset to align with scroll direction for positioning
+                mainAxisLayoutSize - absoluteOffset - page.size
+            } else {
+                absoluteOffset
+            }
+            positionedPages.add(page.position(relativeOffset, layoutWidth, layoutHeight))
+        }
+    } else {
+        var currentMainAxis = pagesScrollOffset
+        extraPagesBefore.fastForEach {
+            currentMainAxis -= pageSizeWithSpacing
+            positionedPages.add(it.position(currentMainAxis, layoutWidth, layoutHeight))
+        }
+
+        currentMainAxis = pagesScrollOffset
+        pages.fastForEach {
+            positionedPages.add(it.position(currentMainAxis, layoutWidth, layoutHeight))
+            currentMainAxis += pageSizeWithSpacing
+        }
+
+        extraPagesAfter.fastForEach {
+            positionedPages.add(it.position(currentMainAxis, layoutWidth, layoutHeight))
+            currentMainAxis += pageSizeWithSpacing
+        }
+    }
+    return positionedPages
+}
+
+private const val DEBUG = true
+private inline fun debugLog(generateMsg: () -> String) {
+    if (DEBUG) {
+        println("PagerMeasure: ${generateMsg()}")
+    }
+}
\ No newline at end of file
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/lazy/PagerMeasurePolicy.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/lazy/PagerMeasurePolicy.kt
new file mode 100644
index 0000000..1747273
--- /dev/null
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/lazy/PagerMeasurePolicy.kt
@@ -0,0 +1,187 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.foundation.pager.lazy
+
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.checkScrollableContainerConstraints
+import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.calculateEndPadding
+import androidx.compose.foundation.layout.calculateStartPadding
+import androidx.compose.foundation.lazy.LazyListBeyondBoundsInfo
+import androidx.compose.foundation.lazy.layout.LazyLayoutMeasureScope
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.snapshots.Snapshot
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.layout.MeasureResult
+import androidx.compose.ui.unit.Constraints
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.IntOffset
+import androidx.compose.ui.unit.constrainHeight
+import androidx.compose.ui.unit.constrainWidth
+import androidx.compose.ui.unit.offset
+import kotlin.math.roundToInt
+
+@OptIn(ExperimentalFoundationApi::class)
+@Composable
+internal fun rememberPagerMeasurePolicy(
+    itemProvider: PagerLazyLayoutItemProvider,
+    state: PagerState,
+    contentPadding: PaddingValues,
+    reverseLayout: Boolean,
+    orientation: Orientation,
+    beyondBoundsPageCount: Int,
+    pageSpacing: Dp,
+    pageSize: PageSize,
+    horizontalAlignment: Alignment.Horizontal?,
+    verticalAlignment: Alignment.Vertical?,
+    pageCount: Int,
+    beyondBoundsInfo: LazyListBeyondBoundsInfo
+) = remember<LazyLayoutMeasureScope.(Constraints) -> MeasureResult>(
+    contentPadding,
+    pageSpacing,
+    pageSize,
+    state,
+    contentPadding,
+    reverseLayout,
+    orientation,
+    horizontalAlignment,
+    verticalAlignment,
+    pageCount,
+    beyondBoundsInfo
+) {
+    { containerConstraints ->
+        val isVertical = orientation == Orientation.Vertical
+        checkScrollableContainerConstraints(
+            containerConstraints,
+            if (isVertical) Orientation.Vertical else Orientation.Horizontal
+        )
+
+        // resolve content paddings
+        val startPadding =
+            if (isVertical) {
+                contentPadding.calculateLeftPadding(layoutDirection).roundToPx()
+            } else {
+                // in horizontal configuration, padding is reversed by placeRelative
+                contentPadding.calculateStartPadding(layoutDirection).roundToPx()
+            }
+
+        val endPadding =
+            if (isVertical) {
+                contentPadding.calculateRightPadding(layoutDirection).roundToPx()
+            } else {
+                // in horizontal configuration, padding is reversed by placeRelative
+                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 =
+            containerConstraints.offset(-totalHorizontalPadding, -totalVerticalPadding)
+
+        state.density = this
+
+        val spaceBetweenPages = pageSpacing.roundToPx()
+
+        // can be negative if the content padding is larger than the max size from constraints
+        val mainAxisAvailableSize = if (isVertical) {
+            containerConstraints.maxHeight - totalVerticalPadding
+        } else {
+            containerConstraints.maxWidth - totalHorizontalPadding
+        }
+        val visualItemOffset = if (!reverseLayout || mainAxisAvailableSize > 0) {
+            IntOffset(startPadding, topPadding)
+        } else {
+            // When layout is reversed and paddings together take >100% of the available space,
+            // layout size is coerced to 0 when positioning. To take that space into account,
+            // we offset start padding by negative space between paddings.
+            IntOffset(
+                if (isVertical) startPadding else startPadding + mainAxisAvailableSize,
+                if (isVertical) topPadding + mainAxisAvailableSize else topPadding
+            )
+        }
+
+        val pageAvailableSize =
+            with(pageSize) { calculateMainAxisPageSize(mainAxisAvailableSize, spaceBetweenPages) }
+
+        state.premeasureConstraints = Constraints(
+            maxWidth = if (orientation == Orientation.Vertical) {
+                contentConstraints.maxWidth
+            } else {
+                pageAvailableSize
+            },
+            maxHeight = if (orientation != Orientation.Vertical) {
+                contentConstraints.maxHeight
+            } else {
+                pageAvailableSize
+            }
+        )
+
+        val firstVisiblePage: Int
+        val firstVisiblePageOffset: Int
+        Snapshot.withoutReadObservation {
+            firstVisiblePage = state.firstVisiblePage
+            firstVisiblePageOffset = if (state.layoutInfo == EmptyLayoutInfo) {
+                (state.initialPageOffsetFraction * pageAvailableSize).roundToInt()
+            } else {
+                state.firstVisiblePageOffset
+            }
+        }
+
+        measurePager(
+            beforeContentPadding = beforeContentPadding,
+            afterContentPadding = afterContentPadding,
+            constraints = contentConstraints,
+            pageCount = pageCount,
+            spaceBetweenPages = spaceBetweenPages,
+            mainAxisAvailableSize = mainAxisAvailableSize,
+            visualPageOffset = visualItemOffset,
+            pageAvailableSize = pageAvailableSize,
+            beyondBoundsPageCount = beyondBoundsPageCount,
+            orientation = orientation,
+            firstVisiblePage = firstVisiblePage,
+            firstVisiblePageOffset = firstVisiblePageOffset,
+            horizontalAlignment = horizontalAlignment,
+            verticalAlignment = verticalAlignment,
+            pagerItemProvider = itemProvider,
+            reverseLayout = reverseLayout,
+            scrollToBeConsumed = state.scrollToBeConsumed,
+            beyondBoundsInfo = beyondBoundsInfo,
+            pinnedPages = state.pinnedPages,
+            layout = { width, height, placement ->
+                layout(
+                    containerConstraints.constrainWidth(width + totalHorizontalPadding),
+                    containerConstraints.constrainHeight(height + totalVerticalPadding),
+                    emptyMap(),
+                    placement
+                )
+            }
+        ).also {
+            state.applyMeasureResult(it)
+        }
+    }
+}
\ No newline at end of file
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/lazy/PagerMeasureResult.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/lazy/PagerMeasureResult.kt
new file mode 100644
index 0000000..20409e1
--- /dev/null
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/lazy/PagerMeasureResult.kt
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.foundation.pager.lazy
+
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.ui.layout.MeasureResult
+import androidx.compose.ui.unit.IntSize
+
+@OptIn(ExperimentalFoundationApi::class)
+internal class PagerMeasureResult(
+    override val visiblePagesInfo: List<PageInfo>,
+    override val pagesCount: Int,
+    override val pageSize: Int,
+    override val pageSpacing: Int,
+    override val afterContentPadding: Int,
+    override val orientation: Orientation,
+    override val viewportStartOffset: Int,
+    override val viewportEndOffset: Int,
+    override val reverseLayout: Boolean,
+    val consumedScroll: Float,
+    val firstVisiblePage: MeasuredPage?,
+    val closestPageToSnapPosition: PageInfo?,
+    val firstVisiblePageOffset: Int,
+    val canScrollForward: Boolean,
+    measureResult: MeasureResult,
+) : PagerLayoutInfo, MeasureResult by measureResult {
+    override val viewportSize: IntSize
+        get() = IntSize(width, height)
+    override val beforeContentPadding: Int get() = -viewportStartOffset
+}
\ No newline at end of file
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/lazy/PagerScrollPosition.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/lazy/PagerScrollPosition.kt
new file mode 100644
index 0000000..8d96e99
--- /dev/null
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/lazy/PagerScrollPosition.kt
@@ -0,0 +1,101 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.foundation.pager.lazy
+
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.lazy.DataIndex
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import androidx.compose.runtime.snapshots.Snapshot
+
+/**
+ * Contains the current scroll position represented by the first visible page  and the first
+ * visible page scroll offset.
+ */
+@OptIn(ExperimentalFoundationApi::class)
+internal class PagerScrollPosition(
+    initialPage: Int = 0,
+    initialScrollOffset: Int = 0
+) {
+    var firstVisiblePage by mutableStateOf(DataIndex(initialPage))
+    var currentPage by mutableStateOf(DataIndex(initialPage))
+
+    var scrollOffset by mutableStateOf(initialScrollOffset)
+        private set
+
+    private var hadFirstNotEmptyLayout = false
+
+    /** The last know key of the page at [firstVisiblePage] position. */
+    private var lastKnownFirstPageKey: Any? = null
+
+    /**
+     * Updates the current scroll position based on the results of the last measurement.
+     */
+    fun updateFromMeasureResult(measureResult: PagerMeasureResult) {
+        lastKnownFirstPageKey = measureResult.firstVisiblePage?.key
+        // we ignore the index and offset from measureResult until we get at least one
+        // measurement with real pages. otherwise the initial index and scroll passed to the
+        // state would be lost and overridden with zeros.
+        if (hadFirstNotEmptyLayout || measureResult.pagesCount > 0) {
+            hadFirstNotEmptyLayout = true
+            val scrollOffset = measureResult.firstVisiblePageOffset
+            check(scrollOffset >= 0f) { "scrollOffset should be non-negative ($scrollOffset)" }
+
+            Snapshot.withoutReadObservation {
+                update(
+                    DataIndex(measureResult.firstVisiblePage?.index ?: 0),
+                    scrollOffset
+                )
+                measureResult.closestPageToSnapPosition?.index?.let {
+                    val currentPage = DataIndex(it)
+                    if (currentPage != this.currentPage) {
+                        this.currentPage = currentPage
+                    }
+                }
+            }
+        }
+    }
+
+    /**
+     * Updates the scroll position - the passed values will be used as a start position for
+     * composing the pages during the next measure pass and will be updated by the real
+     * position calculated during the measurement. This means that there is no guarantee that
+     * exactly this index and offset will be applied as it is possible that:
+     * a) there will be no page at this index in reality
+     * b) page at this index will be smaller than the asked scrollOffset, which means we would
+     * switch to the next page
+     * c) there will be not enough pages to fill the viewport after the requested index, so we
+     * would have to compose few elements before the asked index, changing the first visible page.
+     */
+    fun requestPosition(index: DataIndex, scrollOffset: Int) {
+        update(index, scrollOffset)
+        // clear the stored key as we have a direct request to scroll to [index] position and the
+        // next [checkIfFirstVisibleItemWasMoved] shouldn't override this.
+        lastKnownFirstPageKey = null
+    }
+
+    private fun update(index: DataIndex, scrollOffset: Int) {
+        require(index.value >= 0f) { "Index should be non-negative (${index.value})" }
+        if (index != this.firstVisiblePage) {
+            this.firstVisiblePage = index
+        }
+        if (scrollOffset != this.scrollOffset) {
+            this.scrollOffset = scrollOffset
+        }
+    }
+}
\ No newline at end of file
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/lazy/PagerSemantics.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/lazy/PagerSemantics.kt
new file mode 100644
index 0000000..e47f801
--- /dev/null
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/lazy/PagerSemantics.kt
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.foundation.pager.lazy
+
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.lazy.layout.LazyLayoutItemProvider
+import androidx.compose.foundation.lazy.layout.LazyLayoutSemanticState
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+
+@OptIn(ExperimentalFoundationApi::class)
+@Composable
+internal fun rememberPagerSemanticState(
+    state: PagerState,
+    itemProvider: LazyLayoutItemProvider,
+    reverseScrolling: Boolean,
+    isVertical: Boolean
+): LazyLayoutSemanticState {
+    return remember(state, itemProvider, reverseScrolling, isVertical) {
+        LazyLayoutSemanticState(state, isVertical)
+    }
+}
\ No newline at end of file
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/lazy/PagerState.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/lazy/PagerState.kt
new file mode 100644
index 0000000..f550e81
--- /dev/null
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/lazy/PagerState.kt
@@ -0,0 +1,594 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.foundation.pager.lazy
+
+import androidx.compose.animation.core.AnimationSpec
+import androidx.compose.animation.core.Spring
+import androidx.compose.animation.core.spring
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.MutatePriority
+import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.foundation.gestures.ScrollScope
+import androidx.compose.foundation.gestures.ScrollableState
+import androidx.compose.foundation.gestures.animateScrollBy
+import androidx.compose.foundation.interaction.InteractionSource
+import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.foundation.lazy.AwaitFirstLayoutModifier
+import androidx.compose.foundation.lazy.DataIndex
+import androidx.compose.foundation.lazy.layout.LazyLayoutPinnedItemList
+import androidx.compose.foundation.lazy.layout.LazyLayoutPrefetchState
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.Stable
+import androidx.compose.runtime.derivedStateOf
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.saveable.Saver
+import androidx.compose.runtime.saveable.listSaver
+import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.runtime.setValue
+import androidx.compose.runtime.structuralEqualityPolicy
+import androidx.compose.ui.layout.Remeasurement
+import androidx.compose.ui.layout.RemeasurementModifier
+import androidx.compose.ui.unit.Constraints
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.IntSize
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.util.fastFirstOrNull
+import kotlin.math.abs
+import kotlin.math.roundToInt
+import kotlin.math.sign
+
+/**
+ * Creates and remember a [PagerState] to be used with a [Pager]
+ *
+ * Please refer to the sample to learn how to use this API.
+ * @sample androidx.compose.foundation.samples.PagerWithStateSample
+ *
+ * @param initialPage The pager that should be shown first.
+ * @param initialPageOffsetFraction The offset of the initial page as a fraction of the page size.
+ * This should vary between -0.5 and 0.5 and indicates how to offset the initial page from the
+ * snapped position.
+ */
+@ExperimentalFoundationApi
+@Composable
+internal fun rememberPagerState(
+    initialPage: Int = 0,
+    initialPageOffsetFraction: Float = 0f
+): PagerState {
+    return rememberSaveable(saver = PagerState.Saver) {
+        PagerState(initialPage = initialPage, initialPageOffsetFraction = initialPageOffsetFraction)
+    }
+}
+
+/**
+ * The state that can be used to control [VerticalPager] and [HorizontalPager]
+ * @param initialPage The initial page to be displayed
+ * @param initialPageOffsetFraction The offset of the initial page with respect to the start of
+ * the layout.
+ */
+@ExperimentalFoundationApi
+@Stable
+internal class PagerState(
+    val initialPage: Int = 0,
+    val initialPageOffsetFraction: Float = 0f
+) : ScrollableState {
+
+    init {
+        require(initialPageOffsetFraction in -0.5..0.5) {
+            "initialPageOffsetFraction $initialPageOffsetFraction is " +
+                "not within the range -0.5 to 0.5"
+        }
+    }
+
+    internal var snapRemainingScrollOffset by mutableStateOf(0f)
+
+    // TODO(levima) Use a Pager specific scroll position that will be based on
+    //  currentPage/currentPageOffsetFraction
+    private val scrollPosition = PagerScrollPosition(initialPage, 0)
+
+    internal val firstVisiblePage: Int get() = scrollPosition.firstVisiblePage.value
+
+    internal val firstVisiblePageOffset: Int get() = scrollPosition.scrollOffset
+
+    internal var scrollToBeConsumed = 0f
+        private set
+
+    /**
+     * The ScrollableController instance. We keep it as we need to call stopAnimation on it once
+     * we reached the end of the list.
+     */
+    private val scrollableState = ScrollableState { -performScroll(-it) }
+
+    /**
+     * Only used for testing to confirm that we're not making too many measure passes
+     */
+    internal var numMeasurePasses: Int = 0
+        private set
+
+    /**
+     * Only used for testing to disable prefetching when needed to test the main logic.
+     */
+    internal var prefetchingEnabled: Boolean = true
+
+    /**
+     * The index scheduled to be prefetched (or the last prefetched index if the prefetch is done).
+     */
+    private var indexToPrefetch = -1
+
+    /**
+     * The handle associated with the current index from [indexToPrefetch].
+     */
+    private var currentPrefetchHandle: LazyLayoutPrefetchState.PrefetchHandle? = null
+
+    /**
+     * Keeps the scrolling direction during the previous calculation in order to be able to
+     * detect the scrolling direction change.
+     */
+    private var wasScrollingForward = false
+
+    /** Backing state for PagerLayoutInfo **/
+    private var pagerLayoutInfoState = mutableStateOf(EmptyLayoutInfo)
+
+    internal val layoutInfo: PagerLayoutInfo get() = pagerLayoutInfoState.value
+
+    internal val pageSpacing: Int
+        get() = pagerLayoutInfoState.value.pageSpacing
+
+    internal val pageSize: Int
+        get() = pagerLayoutInfoState.value.pageSize
+
+    internal var density: Density by mutableStateOf(UnitDensity)
+
+    private val visiblePages: List<PageInfo>
+        get() = pagerLayoutInfoState.value.visiblePagesInfo
+
+    private val pageAvailableSpace: Int
+        get() = pageSize + pageSpacing
+
+    /**
+     * How far the current page needs to scroll so the target page is considered to be the next
+     * page.
+     */
+    private val positionThresholdFraction: Float
+        get() = with(density) {
+            val minThreshold = minOf(DefaultPositionThreshold.toPx(), pageSize / 2f)
+            minThreshold / pageSize.toFloat()
+        }
+
+    internal val pageCount: Int
+        get() = pagerLayoutInfoState.value.pagesCount
+
+    internal val firstVisiblePageInfo: PageInfo?
+        get() = visiblePages.lastOrNull {
+            density.calculateDistanceToDesiredSnapPosition(
+                pagerLayoutInfoState.value,
+                it,
+                SnapAlignmentStartToStart
+            ) <= 0
+        }
+
+    private val distanceToSnapPosition: Float
+        get() = layoutInfo.visiblePagesInfo.fastFirstOrNull { it.index == currentPage }?.let {
+            density.calculateDistanceToDesiredSnapPosition(
+                layoutInfo,
+                it,
+                SnapAlignmentStartToStart
+            )
+        } ?: 0f
+
+    internal val internalInteractionSource: MutableInteractionSource = MutableInteractionSource()
+
+    /**
+     * [InteractionSource] that will be used to dispatch drag events when this
+     * list is being dragged. If you want to know whether the fling (or animated scroll) is in
+     * progress, use [isScrollInProgress].
+     */
+    val interactionSource: InteractionSource
+        get() = internalInteractionSource
+
+    /**
+     * The page that sits closest to the snapped position. This is an observable value and will
+     * change as the pager scrolls either by gesture or animation.
+     *
+     * Please refer to the sample to learn how to use this API.
+     * @sample androidx.compose.foundation.samples.ObservingStateChangesInPagerStateSample
+     *
+     */
+    val currentPage: Int get() = scrollPosition.currentPage.value
+
+    private var animationTargetPage by mutableStateOf(-1)
+
+    private var settledPageState = mutableStateOf(initialPage)
+
+    /**
+     * The page that is currently "settled". This is an animation/gesture unaware page in the sense
+     * that it will not be updated while the pages are being scrolled, but rather when the
+     * animation/scroll settles.
+     *
+     * Please refer to the sample to learn how to use this API.
+     * @sample androidx.compose.foundation.samples.ObservingStateChangesInPagerStateSample
+     */
+    val settledPage: Int get() = settledPageState.value.coerceInPageRange()
+
+    /**
+     * The page this [Pager] intends to settle to.
+     * During fling or animated scroll (from [animateScrollToPage] this will represent the page
+     * this pager intends to settle to. When no scroll is ongoing, this will be equal to
+     * [currentPage].
+     *
+     * Please refer to the sample to learn how to use this API.
+     * @sample androidx.compose.foundation.samples.ObservingStateChangesInPagerStateSample
+     */
+    val targetPage: Int by derivedStateOf(structuralEqualityPolicy()) {
+        val finalPage = if (!isScrollInProgress) {
+            currentPage
+        } else if (animationTargetPage != -1) {
+            animationTargetPage
+        } else if (snapRemainingScrollOffset == 0.0f) {
+            // act on scroll only
+            if (abs(currentPageOffsetFraction) >= abs(positionThresholdFraction)) {
+                currentPage + currentPageOffsetFraction.sign.toInt()
+            } else {
+                currentPage
+            }
+        } else {
+            // act on flinging
+            val pageDisplacement = snapRemainingScrollOffset / pageAvailableSpace
+            (currentPage + pageDisplacement.roundToInt())
+        }
+        finalPage.coerceInPageRange()
+    }
+
+    /**
+     * Indicates how far the current page is to the snapped position, this will vary from
+     * -0.5 (page is offset towards the start of the layout) to 0.5 (page is offset towards the end
+     * of the layout). This is 0.0 if the [currentPage] is in the snapped position. The value will
+     * flip once the current page changes.
+     *
+     * This property is observable and shouldn't be used as is in a composable function due to
+     * potential performance issues. To use it in the composition, please consider using a
+     * derived state (e.g [derivedStateOf]) to only have recompositions when the derived
+     * value changes.
+     *
+     * Please refer to the sample to learn how to use this API.
+     * @sample androidx.compose.foundation.samples.ObservingStateChangesInPagerStateSample
+     */
+    val currentPageOffsetFraction: Float by derivedStateOf(structuralEqualityPolicy()) {
+        val currentPagePositionOffset =
+            layoutInfo.visiblePagesInfo.fastFirstOrNull { it.index == currentPage }?.offset ?: 0
+        val pageUsedSpace = pageAvailableSpace.toFloat()
+        if (pageUsedSpace == 0f) {
+            // Default to 0 when there's no info about the page size yet.
+            initialPageOffsetFraction
+        } else {
+            ((-currentPagePositionOffset) / (pageUsedSpace)).coerceIn(
+                MinPageOffset, MaxPageOffset
+            )
+        }
+    }
+
+    internal val prefetchState = LazyLayoutPrefetchState()
+
+    /**
+     * Provides a modifier which allows to delay some interactions (e.g. scroll)
+     * until layout is ready.
+     */
+    internal val awaitLayoutModifier = AwaitFirstLayoutModifier()
+
+    /**
+     * The [Remeasurement] object associated with our layout. It allows us to remeasure
+     * synchronously during scroll.
+     */
+    internal var remeasurement: Remeasurement? by mutableStateOf(null)
+        private set
+
+    /**
+     * The modifier which provides [remeasurement].
+     */
+    internal val remeasurementModifier = object : RemeasurementModifier {
+        override fun onRemeasurementAvailable(remeasurement: Remeasurement) {
+            this@PagerState.remeasurement = remeasurement
+        }
+    }
+
+    /**
+     * Constraints passed to the prefetcher for premeasuring the prefetched items.
+     */
+    internal var premeasureConstraints by mutableStateOf(Constraints())
+
+    /**
+     * Stores currently pinned pages which are always composed, used by for beyond bound pages.
+     */
+    internal val pinnedPages = LazyLayoutPinnedItemList()
+
+    /**
+     * Scroll (jump immediately) to a given [page].
+     *
+     * Please refer to the sample to learn how to use this API.
+     * @sample androidx.compose.foundation.samples.ScrollToPageSample
+     *
+     * @param page The destination page to scroll to
+     * @param pageOffsetFraction A fraction of the page size that indicates the offset the
+     * destination page will be offset from its snapped position.
+     */
+    suspend fun scrollToPage(page: Int, pageOffsetFraction: Float = 0f) {
+        debugLog { "Scroll from page=$currentPage to page=$page" }
+        awaitScrollDependencies()
+        require(pageOffsetFraction in -0.5..0.5) {
+            "pageOffsetFraction $pageOffsetFraction is not within the range -0.5 to 0.5"
+        }
+        val targetPage = page.coerceInPageRange()
+        scrollPosition.requestPosition(
+            DataIndex(targetPage),
+            (pageAvailableSpace * pageOffsetFraction).roundToInt()
+        )
+        remeasurement?.forceRemeasure()
+    }
+
+    /**
+     * Scroll animate to a given [page]. If the [page] is too far away from [currentPage] we will
+     * not compose all pages in the way. We will pre-jump to a nearer page, compose and animate
+     * the rest of the pages until [page].
+     *
+     * Please refer to the sample to learn how to use this API.
+     * @sample androidx.compose.foundation.samples.AnimateScrollPageSample
+     *
+     * @param page The destination page to scroll to
+     * @param pageOffsetFraction A fraction of the page size that indicates the offset the
+     * destination page will be offset from its snapped position.
+     * @param animationSpec An [AnimationSpec] to move between pages. We'll use a [spring] as the
+     * default animation.
+     */
+    suspend fun animateScrollToPage(
+        page: Int,
+        pageOffsetFraction: Float = 0f,
+        animationSpec: AnimationSpec<Float> = spring(stiffness = Spring.StiffnessMediumLow)
+    ) {
+        if (page == currentPage) return
+        awaitScrollDependencies()
+        require(pageOffsetFraction in -0.5..0.5) {
+            "pageOffsetFraction $pageOffsetFraction is not within the range -0.5 to 0.5"
+        }
+        var currentPosition = currentPage
+        val targetPage = page.coerceInPageRange()
+        animationTargetPage = targetPage
+        // If our future page is too far off, that is, outside of the current viewport
+        val firstVisiblePageIndex = visiblePages.first().index
+        val lastVisiblePageIndex = visiblePages.last().index
+        if (((page > currentPage && page > lastVisiblePageIndex) ||
+                (page < currentPage && page < firstVisiblePageIndex)) &&
+            abs(page - currentPage) >= MaxPagesForAnimateScroll
+        ) {
+            val preJumpPosition = if (page > currentPage) {
+                (page - visiblePages.size).coerceAtLeast(currentPosition)
+            } else {
+                page + visiblePages.size.coerceAtMost(currentPosition)
+            }
+
+            debugLog {
+                "animateScrollToPage with pre-jump to position=$preJumpPosition"
+            }
+
+            // Pre-jump to 1 viewport away from destination page, if possible
+            scrollToPage(preJumpPosition)
+            currentPosition = preJumpPosition
+        }
+
+        val targetOffset = targetPage * pageAvailableSpace
+        val currentOffset = currentPosition * pageAvailableSpace
+        val pageOffsetToSnappedPosition =
+            distanceToSnapPosition + pageOffsetFraction * pageAvailableSpace
+
+        val displacement = targetOffset - currentOffset + pageOffsetToSnappedPosition
+
+        debugLog { "animateScrollToPage $displacement pixels" }
+        animateScrollBy(displacement, animationSpec)
+        animationTargetPage = -1
+    }
+
+    private suspend fun awaitScrollDependencies() {
+        awaitLayoutModifier.waitForFirstLayout()
+    }
+
+    override suspend fun scroll(
+        scrollPriority: MutatePriority,
+        block: suspend ScrollScope.() -> Unit
+    ) {
+        awaitLayoutModifier.waitForFirstLayout()
+        scrollableState.scroll(scrollPriority, block)
+    }
+
+    override fun dispatchRawDelta(delta: Float): Float {
+        return scrollableState.dispatchRawDelta(delta)
+    }
+
+    override val isScrollInProgress: Boolean
+        get() = scrollableState.isScrollInProgress
+
+    override var canScrollForward: Boolean by mutableStateOf(false)
+        private set
+    override var canScrollBackward: Boolean by mutableStateOf(false)
+        private set
+
+    /**
+     *  Updates the state with the new calculated scroll position and consumed scroll.
+     */
+    internal fun applyMeasureResult(result: PagerMeasureResult) {
+        scrollPosition.updateFromMeasureResult(result)
+        scrollToBeConsumed -= result.consumedScroll
+        pagerLayoutInfoState.value = result
+        canScrollForward = result.canScrollForward
+        canScrollBackward = (result.firstVisiblePage?.index ?: 0) != 0 ||
+            result.firstVisiblePageOffset != 0
+        cancelPrefetchIfVisibleItemsChanged(result)
+    }
+
+    private fun Int.coerceInPageRange() = if (pageCount > 0) {
+        coerceIn(0, pageCount - 1)
+    } else {
+        0
+    }
+
+    internal fun updateOnScrollStopped() {
+        settledPageState.value = currentPage
+    }
+
+    private fun performScroll(distance: Float): Float {
+        if (distance < 0 && !canScrollForward || distance > 0 && !canScrollBackward) {
+            return 0f
+        }
+        check(abs(scrollToBeConsumed) <= 0.5f) {
+            "entered drag with non-zero pending scroll: $scrollToBeConsumed"
+        }
+        scrollToBeConsumed += distance
+
+        // scrollToBeConsumed will be consumed synchronously during the forceRemeasure invocation
+        // inside measuring we do scrollToBeConsumed.roundToInt() so there will be no scroll if
+        // we have less than 0.5 pixels
+        if (abs(scrollToBeConsumed) > 0.5f) {
+            val preScrollToBeConsumed = scrollToBeConsumed
+            remeasurement?.forceRemeasure()
+            if (prefetchingEnabled) {
+                notifyPrefetch(preScrollToBeConsumed - scrollToBeConsumed)
+            }
+        }
+
+        // here scrollToBeConsumed is already consumed during the forceRemeasure invocation
+        if (abs(scrollToBeConsumed) <= 0.5f) {
+            // We consumed all of it - we'll hold onto the fractional scroll for later, so report
+            // that we consumed the whole thing
+            return distance
+        } else {
+            val scrollConsumed = distance - scrollToBeConsumed
+            // We did not consume all of it - return the rest to be consumed elsewhere (e.g.,
+            // nested scrolling)
+            scrollToBeConsumed = 0f // We're not consuming the rest, give it back
+            return scrollConsumed
+        }
+    }
+
+    private fun notifyPrefetch(delta: Float) {
+        if (!prefetchingEnabled) {
+            return
+        }
+        val info = layoutInfo
+        if (info.visiblePagesInfo.isNotEmpty()) {
+            val scrollingForward = delta < 0
+            val indexToPrefetch = if (scrollingForward) {
+                info.visiblePagesInfo.last().index + 1
+            } else {
+                info.visiblePagesInfo.first().index - 1
+            }
+            if (indexToPrefetch != this.indexToPrefetch &&
+                indexToPrefetch in 0 until info.pagesCount
+            ) {
+                if (wasScrollingForward != scrollingForward) {
+                    // the scrolling direction has been changed which means the last prefetched
+                    // is not going to be reached anytime soon so it is safer to dispose it.
+                    // if this item is already visible it is safe to call the method anyway
+                    // as it will be no-op
+                    currentPrefetchHandle?.cancel()
+                }
+                this.wasScrollingForward = scrollingForward
+                this.indexToPrefetch = indexToPrefetch
+                currentPrefetchHandle = prefetchState.schedulePrefetch(
+                    indexToPrefetch, premeasureConstraints
+                )
+            }
+        }
+    }
+
+    private fun cancelPrefetchIfVisibleItemsChanged(info: PagerLayoutInfo) {
+        if (indexToPrefetch != -1 && info.visiblePagesInfo.isNotEmpty()) {
+            val expectedPrefetchIndex = if (wasScrollingForward) {
+                info.visiblePagesInfo.last().index + 1
+            } else {
+                info.visiblePagesInfo.first().index - 1
+            }
+            if (indexToPrefetch != expectedPrefetchIndex) {
+                indexToPrefetch = -1
+                currentPrefetchHandle?.cancel()
+                currentPrefetchHandle = null
+            }
+        }
+    }
+
+    companion object {
+        /**
+         * To keep current page and current page offset saved
+         */
+        val Saver: Saver<PagerState, *> = listSaver(
+            save = {
+                listOf(
+                    it.currentPage,
+                    it.currentPageOffsetFraction
+                )
+            },
+            restore = {
+                PagerState(
+                    initialPage = it[0] as Int,
+                    initialPageOffsetFraction = it[1] as Float
+                )
+            }
+        )
+    }
+}
+
+@OptIn(ExperimentalFoundationApi::class)
+internal suspend fun PagerState.animateToNextPage() {
+    if (currentPage + 1 < pageCount) animateScrollToPage(currentPage + 1)
+}
+
+@OptIn(ExperimentalFoundationApi::class)
+internal suspend fun PagerState.animateToPreviousPage() {
+    if (currentPage - 1 >= 0) animateScrollToPage(currentPage - 1)
+}
+
+private const val MinPageOffset = -0.5f
+private const val MaxPageOffset = 0.5f
+internal val DefaultPositionThreshold = 56.dp
+private const val MaxPagesForAnimateScroll = 3
+
+@OptIn(ExperimentalFoundationApi::class)
+internal val EmptyLayoutInfo = object : PagerLayoutInfo {
+    override val visiblePagesInfo: List<PageInfo> = emptyList()
+    override val pagesCount: Int = 0
+    override val pageSize: Int = 0
+    override val pageSpacing: Int = 0
+    override val beforeContentPadding: Int = 0
+    override val afterContentPadding: Int = 0
+    override val viewportSize: IntSize = IntSize.Zero
+    override val orientation: Orientation = Orientation.Horizontal
+    override val viewportStartOffset: Int = 0
+    override val viewportEndOffset: Int = 0
+    override val reverseLayout: Boolean = false
+}
+
+private val UnitDensity = object : Density {
+    override val density: Float = 1f
+    override val fontScale: Float = 1f
+}
+
+internal val SnapAlignmentStartToStart: Density.(layoutSize: Float, itemSize: Float) -> Float =
+    { _, _ -> 0f }
+
+private const val DEBUG = false
+private inline fun debugLog(generateMsg: () -> String) {
+    if (DEBUG) {
+        println("PagerState: ${generateMsg()}")
+    }
+}
\ No newline at end of file
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/lazy/PositionedPage.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/lazy/PositionedPage.kt
new file mode 100644
index 0000000..4f1b26f9
--- /dev/null
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/lazy/PositionedPage.kt
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.foundation.pager.lazy
+
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.ui.layout.Placeable
+import androidx.compose.ui.unit.IntOffset
+
+@OptIn(ExperimentalFoundationApi::class)
+internal class PositionedPage(
+    override val index: Int,
+    override val offset: Int,
+    val key: Any,
+    val orientation: Orientation,
+    val wrappers: MutableList<PagerPlaceableWrapper>,
+    val visualOffset: IntOffset
+) : PageInfo {
+    fun place(scope: Placeable.PlacementScope) = with(scope) {
+        repeat(wrappers.size) { index ->
+            val placeable = wrappers[index].placeable
+            val offset = wrappers[index].offset
+            if (orientation == Orientation.Vertical) {
+                placeable.placeWithLayer(offset + visualOffset)
+            } else {
+                placeable.placeRelativeWithLayer(offset + visualOffset)
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/SelectionContainer.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/SelectionContainer.kt
index 0d4733d..e2e8484 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/SelectionContainer.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/SelectionContainer.kt
@@ -137,7 +137,8 @@
 
     DisposableEffect(manager) {
         onDispose {
-            manager.hideSelectionToolbar()
+            manager.onRelease()
+            manager.hasFocus = false
         }
     }
 }
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/pullrefresh/PullRefreshIndicatorTest.kt b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/pullrefresh/PullRefreshIndicatorTest.kt
index 98399ac..9055b1d 100644
--- a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/pullrefresh/PullRefreshIndicatorTest.kt
+++ b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/pullrefresh/PullRefreshIndicatorTest.kt
@@ -16,6 +16,8 @@
 
 package androidx.compose.material.pullrefresh
 
+import androidx.compose.foundation.gestures.awaitEachGesture
+import androidx.compose.foundation.gestures.awaitFirstDown
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.foundation.lazy.LazyColumn
@@ -26,6 +28,9 @@
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.setValue
 import androidx.compose.ui.Modifier
+import androidx.compose.ui.input.pointer.PointerInputChange
+import androidx.compose.ui.input.pointer.pointerInput
+import androidx.compose.ui.layout.onSizeChanged
 import androidx.compose.ui.platform.testTag
 import androidx.compose.ui.test.assertIsDisplayed
 import androidx.compose.ui.test.assertIsNotDisplayed
@@ -33,8 +38,10 @@
 import androidx.compose.ui.test.junit4.createComposeRule
 import androidx.compose.ui.test.onChild
 import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.performClick
 import androidx.compose.ui.test.performTouchInput
 import androidx.compose.ui.test.swipeDown
+import androidx.compose.ui.unit.IntSize
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.MediumTest
 import com.google.common.truth.Truth.assertThat
@@ -162,6 +169,50 @@
         assertThat(indicatorNode.getUnclippedBoundsInRoot()).isEqualTo(restingBounds)
     }
 
+    // Regression test for b/271777421
+    @Test
+    fun indicatorDoesNotCapturePointerEvents() {
+        var indicatorSize: IntSize? = null
+        lateinit var state: PullRefreshState
+        var downEvent: PointerInputChange? = null
+
+        rule.setContent {
+            state = rememberPullRefreshState(false, {})
+
+            Box {
+                Box(Modifier.fillMaxSize().pointerInput(Unit) {
+                    awaitEachGesture {
+                        downEvent = awaitFirstDown()
+                    }
+                })
+                PullRefreshIndicator(
+                    refreshing = false,
+                    state = state,
+                    modifier = Modifier.onSizeChanged {
+                        // The indicator starts as offset by its negative height in the y direction,
+                        // so work out its height so we can place it inside its normal layout
+                        // bounds
+                        indicatorSize = it
+                    }.testTag(IndicatorTag)
+                )
+            }
+        }
+
+        rule.runOnIdle {
+            // Pull by twice the indicator height (since pull delta is halved) - this will make the
+            // indicator fully visible in its layout bounds, so when we performClick() the indicator
+            // will be visibly inside those coordinates.
+            state.onPull(indicatorSize!!.height.toFloat() * 2)
+        }
+
+        rule.onNodeWithTag(IndicatorTag).performClick()
+        rule.runOnIdle {
+            // The indicator should not have blocked its sibling (placed first, so below) from
+            // seeing touch events.
+            assertThat(downEvent).isNotNull()
+        }
+    }
+
     private val pullRefreshNode get() = rule.onNodeWithTag(PullRefreshTag)
     private val indicatorNode get() = rule.onNodeWithTag(IndicatorTag).onChild()
 }
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/pullrefresh/PullRefreshIndicator.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/pullrefresh/PullRefreshIndicator.kt
index 082cd36..e8c1323 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/pullrefresh/PullRefreshIndicator.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/pullrefresh/PullRefreshIndicator.kt
@@ -21,14 +21,15 @@
 import androidx.compose.animation.core.animateFloatAsState
 import androidx.compose.animation.core.tween
 import androidx.compose.foundation.Canvas
+import androidx.compose.foundation.background
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.foundation.layout.size
 import androidx.compose.foundation.shape.CircleShape
 import androidx.compose.material.CircularProgressIndicator
 import androidx.compose.material.ExperimentalMaterialApi
+import androidx.compose.material.LocalElevationOverlay
 import androidx.compose.material.MaterialTheme
-import androidx.compose.material.Surface
 import androidx.compose.material.contentColorFor
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.Immutable
@@ -37,6 +38,7 @@
 import androidx.compose.runtime.remember
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.shadow
 import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.geometry.Rect
 import androidx.compose.ui.geometry.center
@@ -82,13 +84,19 @@
         derivedStateOf { refreshing || state.position > 0.5f }
     }
 
-    Surface(
+    // Apply an elevation overlay if needed. Note that we aren't using Surface here, as we do not
+    // want its input-blocking behaviour, since the indicator is typically displayed above other
+    // (possibly) interactive content.
+    val elevationOverlay = LocalElevationOverlay.current
+    val color = elevationOverlay?.apply(color = backgroundColor, elevation = Elevation)
+        ?: backgroundColor
+
+    Box(
         modifier = modifier
             .size(IndicatorSize)
-            .pullRefreshIndicatorTransform(state, scale),
-        shape = SpinnerShape,
-        color = backgroundColor,
-        elevation = if (showElevation) Elevation else 0.dp,
+            .pullRefreshIndicatorTransform(state, scale)
+            .shadow(if (showElevation) Elevation else 0.dp, SpinnerShape, clip = true)
+            .background(color = color, shape = SpinnerShape)
     ) {
         Crossfade(
             targetState = refreshing,
diff --git a/compose/runtime/runtime/api/current.ignore b/compose/runtime/runtime/api/current.ignore
index e28d869..1589837 100644
--- a/compose/runtime/runtime/api/current.ignore
+++ b/compose/runtime/runtime/api/current.ignore
@@ -3,3 +3,7 @@
     Added method androidx.compose.runtime.Composer.getCurrentCompositionLocalMap()
 AddedAbstractMethod: androidx.compose.runtime.CompositionContext#getEffectCoroutineContext():
     Added method androidx.compose.runtime.CompositionContext.getEffectCoroutineContext()
+
+
+RemovedClass: androidx.compose.runtime.SnapshotStateKt:
+    Removed class androidx.compose.runtime.SnapshotStateKt
diff --git a/compose/runtime/runtime/api/current.txt b/compose/runtime/runtime/api/current.txt
index e6b9f52..1291a3c 100644
--- a/compose/runtime/runtime/api/current.txt
+++ b/compose/runtime/runtime/api/current.txt
@@ -448,110 +448,6 @@
     method public static <K, V> androidx.compose.runtime.snapshots.SnapshotStateMap<K,V> toMutableStateMap(Iterable<? extends kotlin.Pair<? extends K,? extends V>>);
   }
 
-  public final class SnapshotStateKt {
-    method @androidx.compose.runtime.Composable public static <T> androidx.compose.runtime.State<T> collectAsState(kotlinx.coroutines.flow.StateFlow<? extends T>, optional kotlin.coroutines.CoroutineContext context);
-    method @androidx.compose.runtime.Composable public static <T extends R, R> androidx.compose.runtime.State<R> collectAsState(kotlinx.coroutines.flow.Flow<? extends T>, R? initial, optional kotlin.coroutines.CoroutineContext context);
-    method public static <T> androidx.compose.runtime.State<T> derivedStateOf(kotlin.jvm.functions.Function0<? extends T> calculation);
-    method public static <T> androidx.compose.runtime.State<T> derivedStateOf(androidx.compose.runtime.SnapshotMutationPolicy<T> policy, kotlin.jvm.functions.Function0<? extends T> calculation);
-    method public static inline operator <T> T! getValue(androidx.compose.runtime.State<? extends T>, Object? thisObj, kotlin.reflect.KProperty<?> property);
-    method public static <T> androidx.compose.runtime.snapshots.SnapshotStateList<T> mutableStateListOf();
-    method public static <T> androidx.compose.runtime.snapshots.SnapshotStateList<T> mutableStateListOf(T?... elements);
-    method public static <K, V> androidx.compose.runtime.snapshots.SnapshotStateMap<K,V> mutableStateMapOf();
-    method public static <K, V> androidx.compose.runtime.snapshots.SnapshotStateMap<K,V> mutableStateMapOf(kotlin.Pair<? extends K,? extends V>... pairs);
-    method public static <T> androidx.compose.runtime.MutableState<T> mutableStateOf(T? value, optional androidx.compose.runtime.SnapshotMutationPolicy<T> policy);
-    method public static <T> androidx.compose.runtime.SnapshotMutationPolicy<T> neverEqualPolicy();
-    method @androidx.compose.runtime.Composable public static <T> androidx.compose.runtime.State<T> produceState(T? initialValue, kotlin.jvm.functions.Function2<? super androidx.compose.runtime.ProduceStateScope<T>,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> producer);
-    method @androidx.compose.runtime.Composable public static <T> androidx.compose.runtime.State<T> produceState(T? initialValue, Object? key1, kotlin.jvm.functions.Function2<? super androidx.compose.runtime.ProduceStateScope<T>,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> producer);
-    method @androidx.compose.runtime.Composable public static <T> androidx.compose.runtime.State<T> produceState(T? initialValue, Object? key1, Object? key2, kotlin.jvm.functions.Function2<? super androidx.compose.runtime.ProduceStateScope<T>,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> producer);
-    method @androidx.compose.runtime.Composable public static <T> androidx.compose.runtime.State<T> produceState(T? initialValue, Object? key1, Object? key2, Object? key3, kotlin.jvm.functions.Function2<? super androidx.compose.runtime.ProduceStateScope<T>,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> producer);
-    method @androidx.compose.runtime.Composable public static <T> androidx.compose.runtime.State<T> produceState(T? initialValue, Object![]? keys, kotlin.jvm.functions.Function2<? super androidx.compose.runtime.ProduceStateScope<T>,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> producer);
-    method public static <T> androidx.compose.runtime.SnapshotMutationPolicy<T> referentialEqualityPolicy();
-    method @androidx.compose.runtime.Composable public static <T> androidx.compose.runtime.State<T> rememberUpdatedState(T? newValue);
-    method public static inline operator <T> void setValue(androidx.compose.runtime.MutableState<T>, Object? thisObj, kotlin.reflect.KProperty<?> property, T? value);
-    method public static <T> kotlinx.coroutines.flow.Flow<T> snapshotFlow(kotlin.jvm.functions.Function0<? extends T> block);
-    method public static <T> androidx.compose.runtime.SnapshotMutationPolicy<T> structuralEqualityPolicy();
-    method public static <T> androidx.compose.runtime.snapshots.SnapshotStateList<T> toMutableStateList(java.util.Collection<? extends T>);
-    method public static <K, V> androidx.compose.runtime.snapshots.SnapshotStateMap<K,V> toMutableStateMap(Iterable<? extends kotlin.Pair<? extends K,? extends V>>);
-  }
-
-  public final class SnapshotStateKt {
-    method @androidx.compose.runtime.Composable public static <T> androidx.compose.runtime.State<T> collectAsState(kotlinx.coroutines.flow.StateFlow<? extends T>, optional kotlin.coroutines.CoroutineContext context);
-    method @androidx.compose.runtime.Composable public static <T extends R, R> androidx.compose.runtime.State<R> collectAsState(kotlinx.coroutines.flow.Flow<? extends T>, R? initial, optional kotlin.coroutines.CoroutineContext context);
-    method public static <T> androidx.compose.runtime.State<T> derivedStateOf(kotlin.jvm.functions.Function0<? extends T> calculation);
-    method public static <T> androidx.compose.runtime.State<T> derivedStateOf(androidx.compose.runtime.SnapshotMutationPolicy<T> policy, kotlin.jvm.functions.Function0<? extends T> calculation);
-    method public static inline operator <T> T! getValue(androidx.compose.runtime.State<? extends T>, Object? thisObj, kotlin.reflect.KProperty<?> property);
-    method public static <T> androidx.compose.runtime.snapshots.SnapshotStateList<T> mutableStateListOf();
-    method public static <T> androidx.compose.runtime.snapshots.SnapshotStateList<T> mutableStateListOf(T?... elements);
-    method public static <K, V> androidx.compose.runtime.snapshots.SnapshotStateMap<K,V> mutableStateMapOf();
-    method public static <K, V> androidx.compose.runtime.snapshots.SnapshotStateMap<K,V> mutableStateMapOf(kotlin.Pair<? extends K,? extends V>... pairs);
-    method public static <T> androidx.compose.runtime.MutableState<T> mutableStateOf(T? value, optional androidx.compose.runtime.SnapshotMutationPolicy<T> policy);
-    method public static <T> androidx.compose.runtime.SnapshotMutationPolicy<T> neverEqualPolicy();
-    method @androidx.compose.runtime.Composable public static <T> androidx.compose.runtime.State<T> produceState(T? initialValue, kotlin.jvm.functions.Function2<? super androidx.compose.runtime.ProduceStateScope<T>,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> producer);
-    method @androidx.compose.runtime.Composable public static <T> androidx.compose.runtime.State<T> produceState(T? initialValue, Object? key1, kotlin.jvm.functions.Function2<? super androidx.compose.runtime.ProduceStateScope<T>,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> producer);
-    method @androidx.compose.runtime.Composable public static <T> androidx.compose.runtime.State<T> produceState(T? initialValue, Object? key1, Object? key2, kotlin.jvm.functions.Function2<? super androidx.compose.runtime.ProduceStateScope<T>,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> producer);
-    method @androidx.compose.runtime.Composable public static <T> androidx.compose.runtime.State<T> produceState(T? initialValue, Object? key1, Object? key2, Object? key3, kotlin.jvm.functions.Function2<? super androidx.compose.runtime.ProduceStateScope<T>,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> producer);
-    method @androidx.compose.runtime.Composable public static <T> androidx.compose.runtime.State<T> produceState(T? initialValue, Object![]? keys, kotlin.jvm.functions.Function2<? super androidx.compose.runtime.ProduceStateScope<T>,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> producer);
-    method public static <T> androidx.compose.runtime.SnapshotMutationPolicy<T> referentialEqualityPolicy();
-    method @androidx.compose.runtime.Composable public static <T> androidx.compose.runtime.State<T> rememberUpdatedState(T? newValue);
-    method public static inline operator <T> void setValue(androidx.compose.runtime.MutableState<T>, Object? thisObj, kotlin.reflect.KProperty<?> property, T? value);
-    method public static <T> kotlinx.coroutines.flow.Flow<T> snapshotFlow(kotlin.jvm.functions.Function0<? extends T> block);
-    method public static <T> androidx.compose.runtime.SnapshotMutationPolicy<T> structuralEqualityPolicy();
-    method public static <T> androidx.compose.runtime.snapshots.SnapshotStateList<T> toMutableStateList(java.util.Collection<? extends T>);
-    method public static <K, V> androidx.compose.runtime.snapshots.SnapshotStateMap<K,V> toMutableStateMap(Iterable<? extends kotlin.Pair<? extends K,? extends V>>);
-  }
-
-  public final class SnapshotStateKt {
-    method @androidx.compose.runtime.Composable public static <T> androidx.compose.runtime.State<T> collectAsState(kotlinx.coroutines.flow.StateFlow<? extends T>, optional kotlin.coroutines.CoroutineContext context);
-    method @androidx.compose.runtime.Composable public static <T extends R, R> androidx.compose.runtime.State<R> collectAsState(kotlinx.coroutines.flow.Flow<? extends T>, R? initial, optional kotlin.coroutines.CoroutineContext context);
-    method public static <T> androidx.compose.runtime.State<T> derivedStateOf(kotlin.jvm.functions.Function0<? extends T> calculation);
-    method public static <T> androidx.compose.runtime.State<T> derivedStateOf(androidx.compose.runtime.SnapshotMutationPolicy<T> policy, kotlin.jvm.functions.Function0<? extends T> calculation);
-    method public static inline operator <T> T! getValue(androidx.compose.runtime.State<? extends T>, Object? thisObj, kotlin.reflect.KProperty<?> property);
-    method public static <T> androidx.compose.runtime.snapshots.SnapshotStateList<T> mutableStateListOf();
-    method public static <T> androidx.compose.runtime.snapshots.SnapshotStateList<T> mutableStateListOf(T?... elements);
-    method public static <K, V> androidx.compose.runtime.snapshots.SnapshotStateMap<K,V> mutableStateMapOf();
-    method public static <K, V> androidx.compose.runtime.snapshots.SnapshotStateMap<K,V> mutableStateMapOf(kotlin.Pair<? extends K,? extends V>... pairs);
-    method public static <T> androidx.compose.runtime.MutableState<T> mutableStateOf(T? value, optional androidx.compose.runtime.SnapshotMutationPolicy<T> policy);
-    method public static <T> androidx.compose.runtime.SnapshotMutationPolicy<T> neverEqualPolicy();
-    method @androidx.compose.runtime.Composable public static <T> androidx.compose.runtime.State<T> produceState(T? initialValue, kotlin.jvm.functions.Function2<? super androidx.compose.runtime.ProduceStateScope<T>,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> producer);
-    method @androidx.compose.runtime.Composable public static <T> androidx.compose.runtime.State<T> produceState(T? initialValue, Object? key1, kotlin.jvm.functions.Function2<? super androidx.compose.runtime.ProduceStateScope<T>,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> producer);
-    method @androidx.compose.runtime.Composable public static <T> androidx.compose.runtime.State<T> produceState(T? initialValue, Object? key1, Object? key2, kotlin.jvm.functions.Function2<? super androidx.compose.runtime.ProduceStateScope<T>,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> producer);
-    method @androidx.compose.runtime.Composable public static <T> androidx.compose.runtime.State<T> produceState(T? initialValue, Object? key1, Object? key2, Object? key3, kotlin.jvm.functions.Function2<? super androidx.compose.runtime.ProduceStateScope<T>,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> producer);
-    method @androidx.compose.runtime.Composable public static <T> androidx.compose.runtime.State<T> produceState(T? initialValue, Object![]? keys, kotlin.jvm.functions.Function2<? super androidx.compose.runtime.ProduceStateScope<T>,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> producer);
-    method public static <T> androidx.compose.runtime.SnapshotMutationPolicy<T> referentialEqualityPolicy();
-    method @androidx.compose.runtime.Composable public static <T> androidx.compose.runtime.State<T> rememberUpdatedState(T? newValue);
-    method public static inline operator <T> void setValue(androidx.compose.runtime.MutableState<T>, Object? thisObj, kotlin.reflect.KProperty<?> property, T? value);
-    method public static <T> kotlinx.coroutines.flow.Flow<T> snapshotFlow(kotlin.jvm.functions.Function0<? extends T> block);
-    method public static <T> androidx.compose.runtime.SnapshotMutationPolicy<T> structuralEqualityPolicy();
-    method public static <T> androidx.compose.runtime.snapshots.SnapshotStateList<T> toMutableStateList(java.util.Collection<? extends T>);
-    method public static <K, V> androidx.compose.runtime.snapshots.SnapshotStateMap<K,V> toMutableStateMap(Iterable<? extends kotlin.Pair<? extends K,? extends V>>);
-  }
-
-  public final class SnapshotStateKt {
-    method @androidx.compose.runtime.Composable public static <T> androidx.compose.runtime.State<T> collectAsState(kotlinx.coroutines.flow.StateFlow<? extends T>, optional kotlin.coroutines.CoroutineContext context);
-    method @androidx.compose.runtime.Composable public static <T extends R, R> androidx.compose.runtime.State<R> collectAsState(kotlinx.coroutines.flow.Flow<? extends T>, R? initial, optional kotlin.coroutines.CoroutineContext context);
-    method public static <T> androidx.compose.runtime.State<T> derivedStateOf(kotlin.jvm.functions.Function0<? extends T> calculation);
-    method public static <T> androidx.compose.runtime.State<T> derivedStateOf(androidx.compose.runtime.SnapshotMutationPolicy<T> policy, kotlin.jvm.functions.Function0<? extends T> calculation);
-    method public static inline operator <T> T! getValue(androidx.compose.runtime.State<? extends T>, Object? thisObj, kotlin.reflect.KProperty<?> property);
-    method public static <T> androidx.compose.runtime.snapshots.SnapshotStateList<T> mutableStateListOf();
-    method public static <T> androidx.compose.runtime.snapshots.SnapshotStateList<T> mutableStateListOf(T?... elements);
-    method public static <K, V> androidx.compose.runtime.snapshots.SnapshotStateMap<K,V> mutableStateMapOf();
-    method public static <K, V> androidx.compose.runtime.snapshots.SnapshotStateMap<K,V> mutableStateMapOf(kotlin.Pair<? extends K,? extends V>... pairs);
-    method public static <T> androidx.compose.runtime.MutableState<T> mutableStateOf(T? value, optional androidx.compose.runtime.SnapshotMutationPolicy<T> policy);
-    method public static <T> androidx.compose.runtime.SnapshotMutationPolicy<T> neverEqualPolicy();
-    method @androidx.compose.runtime.Composable public static <T> androidx.compose.runtime.State<T> produceState(T? initialValue, kotlin.jvm.functions.Function2<? super androidx.compose.runtime.ProduceStateScope<T>,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> producer);
-    method @androidx.compose.runtime.Composable public static <T> androidx.compose.runtime.State<T> produceState(T? initialValue, Object? key1, kotlin.jvm.functions.Function2<? super androidx.compose.runtime.ProduceStateScope<T>,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> producer);
-    method @androidx.compose.runtime.Composable public static <T> androidx.compose.runtime.State<T> produceState(T? initialValue, Object? key1, Object? key2, kotlin.jvm.functions.Function2<? super androidx.compose.runtime.ProduceStateScope<T>,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> producer);
-    method @androidx.compose.runtime.Composable public static <T> androidx.compose.runtime.State<T> produceState(T? initialValue, Object? key1, Object? key2, Object? key3, kotlin.jvm.functions.Function2<? super androidx.compose.runtime.ProduceStateScope<T>,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> producer);
-    method @androidx.compose.runtime.Composable public static <T> androidx.compose.runtime.State<T> produceState(T? initialValue, Object![]? keys, kotlin.jvm.functions.Function2<? super androidx.compose.runtime.ProduceStateScope<T>,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> producer);
-    method public static <T> androidx.compose.runtime.SnapshotMutationPolicy<T> referentialEqualityPolicy();
-    method @androidx.compose.runtime.Composable public static <T> androidx.compose.runtime.State<T> rememberUpdatedState(T? newValue);
-    method public static inline operator <T> void setValue(androidx.compose.runtime.MutableState<T>, Object? thisObj, kotlin.reflect.KProperty<?> property, T? value);
-    method public static <T> kotlinx.coroutines.flow.Flow<T> snapshotFlow(kotlin.jvm.functions.Function0<? extends T> block);
-    method public static <T> androidx.compose.runtime.SnapshotMutationPolicy<T> structuralEqualityPolicy();
-    method public static <T> androidx.compose.runtime.snapshots.SnapshotStateList<T> toMutableStateList(java.util.Collection<? extends T>);
-    method public static <K, V> androidx.compose.runtime.snapshots.SnapshotStateMap<K,V> toMutableStateMap(Iterable<? extends kotlin.Pair<? extends K,? extends V>>);
-  }
-
   @androidx.compose.runtime.StableMarker @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY}) public @interface Stable {
   }
 
diff --git a/compose/runtime/runtime/api/public_plus_experimental_current.txt b/compose/runtime/runtime/api/public_plus_experimental_current.txt
index 892930c..b6000ab 100644
--- a/compose/runtime/runtime/api/public_plus_experimental_current.txt
+++ b/compose/runtime/runtime/api/public_plus_experimental_current.txt
@@ -497,110 +497,6 @@
     method public static <K, V> androidx.compose.runtime.snapshots.SnapshotStateMap<K,V> toMutableStateMap(Iterable<? extends kotlin.Pair<? extends K,? extends V>>);
   }
 
-  public final class SnapshotStateKt {
-    method @androidx.compose.runtime.Composable public static <T> androidx.compose.runtime.State<T> collectAsState(kotlinx.coroutines.flow.StateFlow<? extends T>, optional kotlin.coroutines.CoroutineContext context);
-    method @androidx.compose.runtime.Composable public static <T extends R, R> androidx.compose.runtime.State<R> collectAsState(kotlinx.coroutines.flow.Flow<? extends T>, R? initial, optional kotlin.coroutines.CoroutineContext context);
-    method public static <T> androidx.compose.runtime.State<T> derivedStateOf(kotlin.jvm.functions.Function0<? extends T> calculation);
-    method public static <T> androidx.compose.runtime.State<T> derivedStateOf(androidx.compose.runtime.SnapshotMutationPolicy<T> policy, kotlin.jvm.functions.Function0<? extends T> calculation);
-    method public static inline operator <T> T! getValue(androidx.compose.runtime.State<? extends T>, Object? thisObj, kotlin.reflect.KProperty<?> property);
-    method public static <T> androidx.compose.runtime.snapshots.SnapshotStateList<T> mutableStateListOf();
-    method public static <T> androidx.compose.runtime.snapshots.SnapshotStateList<T> mutableStateListOf(T?... elements);
-    method public static <K, V> androidx.compose.runtime.snapshots.SnapshotStateMap<K,V> mutableStateMapOf();
-    method public static <K, V> androidx.compose.runtime.snapshots.SnapshotStateMap<K,V> mutableStateMapOf(kotlin.Pair<? extends K,? extends V>... pairs);
-    method public static <T> androidx.compose.runtime.MutableState<T> mutableStateOf(T? value, optional androidx.compose.runtime.SnapshotMutationPolicy<T> policy);
-    method public static <T> androidx.compose.runtime.SnapshotMutationPolicy<T> neverEqualPolicy();
-    method @androidx.compose.runtime.Composable public static <T> androidx.compose.runtime.State<T> produceState(T? initialValue, kotlin.jvm.functions.Function2<? super androidx.compose.runtime.ProduceStateScope<T>,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> producer);
-    method @androidx.compose.runtime.Composable public static <T> androidx.compose.runtime.State<T> produceState(T? initialValue, Object? key1, kotlin.jvm.functions.Function2<? super androidx.compose.runtime.ProduceStateScope<T>,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> producer);
-    method @androidx.compose.runtime.Composable public static <T> androidx.compose.runtime.State<T> produceState(T? initialValue, Object? key1, Object? key2, kotlin.jvm.functions.Function2<? super androidx.compose.runtime.ProduceStateScope<T>,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> producer);
-    method @androidx.compose.runtime.Composable public static <T> androidx.compose.runtime.State<T> produceState(T? initialValue, Object? key1, Object? key2, Object? key3, kotlin.jvm.functions.Function2<? super androidx.compose.runtime.ProduceStateScope<T>,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> producer);
-    method @androidx.compose.runtime.Composable public static <T> androidx.compose.runtime.State<T> produceState(T? initialValue, Object![]? keys, kotlin.jvm.functions.Function2<? super androidx.compose.runtime.ProduceStateScope<T>,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> producer);
-    method public static <T> androidx.compose.runtime.SnapshotMutationPolicy<T> referentialEqualityPolicy();
-    method @androidx.compose.runtime.Composable public static <T> androidx.compose.runtime.State<T> rememberUpdatedState(T? newValue);
-    method public static inline operator <T> void setValue(androidx.compose.runtime.MutableState<T>, Object? thisObj, kotlin.reflect.KProperty<?> property, T? value);
-    method public static <T> kotlinx.coroutines.flow.Flow<T> snapshotFlow(kotlin.jvm.functions.Function0<? extends T> block);
-    method public static <T> androidx.compose.runtime.SnapshotMutationPolicy<T> structuralEqualityPolicy();
-    method public static <T> androidx.compose.runtime.snapshots.SnapshotStateList<T> toMutableStateList(java.util.Collection<? extends T>);
-    method public static <K, V> androidx.compose.runtime.snapshots.SnapshotStateMap<K,V> toMutableStateMap(Iterable<? extends kotlin.Pair<? extends K,? extends V>>);
-  }
-
-  public final class SnapshotStateKt {
-    method @androidx.compose.runtime.Composable public static <T> androidx.compose.runtime.State<T> collectAsState(kotlinx.coroutines.flow.StateFlow<? extends T>, optional kotlin.coroutines.CoroutineContext context);
-    method @androidx.compose.runtime.Composable public static <T extends R, R> androidx.compose.runtime.State<R> collectAsState(kotlinx.coroutines.flow.Flow<? extends T>, R? initial, optional kotlin.coroutines.CoroutineContext context);
-    method public static <T> androidx.compose.runtime.State<T> derivedStateOf(kotlin.jvm.functions.Function0<? extends T> calculation);
-    method public static <T> androidx.compose.runtime.State<T> derivedStateOf(androidx.compose.runtime.SnapshotMutationPolicy<T> policy, kotlin.jvm.functions.Function0<? extends T> calculation);
-    method public static inline operator <T> T! getValue(androidx.compose.runtime.State<? extends T>, Object? thisObj, kotlin.reflect.KProperty<?> property);
-    method public static <T> androidx.compose.runtime.snapshots.SnapshotStateList<T> mutableStateListOf();
-    method public static <T> androidx.compose.runtime.snapshots.SnapshotStateList<T> mutableStateListOf(T?... elements);
-    method public static <K, V> androidx.compose.runtime.snapshots.SnapshotStateMap<K,V> mutableStateMapOf();
-    method public static <K, V> androidx.compose.runtime.snapshots.SnapshotStateMap<K,V> mutableStateMapOf(kotlin.Pair<? extends K,? extends V>... pairs);
-    method public static <T> androidx.compose.runtime.MutableState<T> mutableStateOf(T? value, optional androidx.compose.runtime.SnapshotMutationPolicy<T> policy);
-    method public static <T> androidx.compose.runtime.SnapshotMutationPolicy<T> neverEqualPolicy();
-    method @androidx.compose.runtime.Composable public static <T> androidx.compose.runtime.State<T> produceState(T? initialValue, kotlin.jvm.functions.Function2<? super androidx.compose.runtime.ProduceStateScope<T>,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> producer);
-    method @androidx.compose.runtime.Composable public static <T> androidx.compose.runtime.State<T> produceState(T? initialValue, Object? key1, kotlin.jvm.functions.Function2<? super androidx.compose.runtime.ProduceStateScope<T>,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> producer);
-    method @androidx.compose.runtime.Composable public static <T> androidx.compose.runtime.State<T> produceState(T? initialValue, Object? key1, Object? key2, kotlin.jvm.functions.Function2<? super androidx.compose.runtime.ProduceStateScope<T>,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> producer);
-    method @androidx.compose.runtime.Composable public static <T> androidx.compose.runtime.State<T> produceState(T? initialValue, Object? key1, Object? key2, Object? key3, kotlin.jvm.functions.Function2<? super androidx.compose.runtime.ProduceStateScope<T>,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> producer);
-    method @androidx.compose.runtime.Composable public static <T> androidx.compose.runtime.State<T> produceState(T? initialValue, Object![]? keys, kotlin.jvm.functions.Function2<? super androidx.compose.runtime.ProduceStateScope<T>,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> producer);
-    method public static <T> androidx.compose.runtime.SnapshotMutationPolicy<T> referentialEqualityPolicy();
-    method @androidx.compose.runtime.Composable public static <T> androidx.compose.runtime.State<T> rememberUpdatedState(T? newValue);
-    method public static inline operator <T> void setValue(androidx.compose.runtime.MutableState<T>, Object? thisObj, kotlin.reflect.KProperty<?> property, T? value);
-    method public static <T> kotlinx.coroutines.flow.Flow<T> snapshotFlow(kotlin.jvm.functions.Function0<? extends T> block);
-    method public static <T> androidx.compose.runtime.SnapshotMutationPolicy<T> structuralEqualityPolicy();
-    method public static <T> androidx.compose.runtime.snapshots.SnapshotStateList<T> toMutableStateList(java.util.Collection<? extends T>);
-    method public static <K, V> androidx.compose.runtime.snapshots.SnapshotStateMap<K,V> toMutableStateMap(Iterable<? extends kotlin.Pair<? extends K,? extends V>>);
-  }
-
-  public final class SnapshotStateKt {
-    method @androidx.compose.runtime.Composable public static <T> androidx.compose.runtime.State<T> collectAsState(kotlinx.coroutines.flow.StateFlow<? extends T>, optional kotlin.coroutines.CoroutineContext context);
-    method @androidx.compose.runtime.Composable public static <T extends R, R> androidx.compose.runtime.State<R> collectAsState(kotlinx.coroutines.flow.Flow<? extends T>, R? initial, optional kotlin.coroutines.CoroutineContext context);
-    method public static <T> androidx.compose.runtime.State<T> derivedStateOf(kotlin.jvm.functions.Function0<? extends T> calculation);
-    method public static <T> androidx.compose.runtime.State<T> derivedStateOf(androidx.compose.runtime.SnapshotMutationPolicy<T> policy, kotlin.jvm.functions.Function0<? extends T> calculation);
-    method public static inline operator <T> T! getValue(androidx.compose.runtime.State<? extends T>, Object? thisObj, kotlin.reflect.KProperty<?> property);
-    method public static <T> androidx.compose.runtime.snapshots.SnapshotStateList<T> mutableStateListOf();
-    method public static <T> androidx.compose.runtime.snapshots.SnapshotStateList<T> mutableStateListOf(T?... elements);
-    method public static <K, V> androidx.compose.runtime.snapshots.SnapshotStateMap<K,V> mutableStateMapOf();
-    method public static <K, V> androidx.compose.runtime.snapshots.SnapshotStateMap<K,V> mutableStateMapOf(kotlin.Pair<? extends K,? extends V>... pairs);
-    method public static <T> androidx.compose.runtime.MutableState<T> mutableStateOf(T? value, optional androidx.compose.runtime.SnapshotMutationPolicy<T> policy);
-    method public static <T> androidx.compose.runtime.SnapshotMutationPolicy<T> neverEqualPolicy();
-    method @androidx.compose.runtime.Composable public static <T> androidx.compose.runtime.State<T> produceState(T? initialValue, kotlin.jvm.functions.Function2<? super androidx.compose.runtime.ProduceStateScope<T>,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> producer);
-    method @androidx.compose.runtime.Composable public static <T> androidx.compose.runtime.State<T> produceState(T? initialValue, Object? key1, kotlin.jvm.functions.Function2<? super androidx.compose.runtime.ProduceStateScope<T>,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> producer);
-    method @androidx.compose.runtime.Composable public static <T> androidx.compose.runtime.State<T> produceState(T? initialValue, Object? key1, Object? key2, kotlin.jvm.functions.Function2<? super androidx.compose.runtime.ProduceStateScope<T>,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> producer);
-    method @androidx.compose.runtime.Composable public static <T> androidx.compose.runtime.State<T> produceState(T? initialValue, Object? key1, Object? key2, Object? key3, kotlin.jvm.functions.Function2<? super androidx.compose.runtime.ProduceStateScope<T>,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> producer);
-    method @androidx.compose.runtime.Composable public static <T> androidx.compose.runtime.State<T> produceState(T? initialValue, Object![]? keys, kotlin.jvm.functions.Function2<? super androidx.compose.runtime.ProduceStateScope<T>,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> producer);
-    method public static <T> androidx.compose.runtime.SnapshotMutationPolicy<T> referentialEqualityPolicy();
-    method @androidx.compose.runtime.Composable public static <T> androidx.compose.runtime.State<T> rememberUpdatedState(T? newValue);
-    method public static inline operator <T> void setValue(androidx.compose.runtime.MutableState<T>, Object? thisObj, kotlin.reflect.KProperty<?> property, T? value);
-    method public static <T> kotlinx.coroutines.flow.Flow<T> snapshotFlow(kotlin.jvm.functions.Function0<? extends T> block);
-    method public static <T> androidx.compose.runtime.SnapshotMutationPolicy<T> structuralEqualityPolicy();
-    method public static <T> androidx.compose.runtime.snapshots.SnapshotStateList<T> toMutableStateList(java.util.Collection<? extends T>);
-    method public static <K, V> androidx.compose.runtime.snapshots.SnapshotStateMap<K,V> toMutableStateMap(Iterable<? extends kotlin.Pair<? extends K,? extends V>>);
-  }
-
-  public final class SnapshotStateKt {
-    method @androidx.compose.runtime.Composable public static <T> androidx.compose.runtime.State<T> collectAsState(kotlinx.coroutines.flow.StateFlow<? extends T>, optional kotlin.coroutines.CoroutineContext context);
-    method @androidx.compose.runtime.Composable public static <T extends R, R> androidx.compose.runtime.State<R> collectAsState(kotlinx.coroutines.flow.Flow<? extends T>, R? initial, optional kotlin.coroutines.CoroutineContext context);
-    method public static <T> androidx.compose.runtime.State<T> derivedStateOf(kotlin.jvm.functions.Function0<? extends T> calculation);
-    method public static <T> androidx.compose.runtime.State<T> derivedStateOf(androidx.compose.runtime.SnapshotMutationPolicy<T> policy, kotlin.jvm.functions.Function0<? extends T> calculation);
-    method public static inline operator <T> T! getValue(androidx.compose.runtime.State<? extends T>, Object? thisObj, kotlin.reflect.KProperty<?> property);
-    method public static <T> androidx.compose.runtime.snapshots.SnapshotStateList<T> mutableStateListOf();
-    method public static <T> androidx.compose.runtime.snapshots.SnapshotStateList<T> mutableStateListOf(T?... elements);
-    method public static <K, V> androidx.compose.runtime.snapshots.SnapshotStateMap<K,V> mutableStateMapOf();
-    method public static <K, V> androidx.compose.runtime.snapshots.SnapshotStateMap<K,V> mutableStateMapOf(kotlin.Pair<? extends K,? extends V>... pairs);
-    method public static <T> androidx.compose.runtime.MutableState<T> mutableStateOf(T? value, optional androidx.compose.runtime.SnapshotMutationPolicy<T> policy);
-    method public static <T> androidx.compose.runtime.SnapshotMutationPolicy<T> neverEqualPolicy();
-    method @androidx.compose.runtime.Composable public static <T> androidx.compose.runtime.State<T> produceState(T? initialValue, kotlin.jvm.functions.Function2<? super androidx.compose.runtime.ProduceStateScope<T>,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> producer);
-    method @androidx.compose.runtime.Composable public static <T> androidx.compose.runtime.State<T> produceState(T? initialValue, Object? key1, kotlin.jvm.functions.Function2<? super androidx.compose.runtime.ProduceStateScope<T>,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> producer);
-    method @androidx.compose.runtime.Composable public static <T> androidx.compose.runtime.State<T> produceState(T? initialValue, Object? key1, Object? key2, kotlin.jvm.functions.Function2<? super androidx.compose.runtime.ProduceStateScope<T>,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> producer);
-    method @androidx.compose.runtime.Composable public static <T> androidx.compose.runtime.State<T> produceState(T? initialValue, Object? key1, Object? key2, Object? key3, kotlin.jvm.functions.Function2<? super androidx.compose.runtime.ProduceStateScope<T>,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> producer);
-    method @androidx.compose.runtime.Composable public static <T> androidx.compose.runtime.State<T> produceState(T? initialValue, Object![]? keys, kotlin.jvm.functions.Function2<? super androidx.compose.runtime.ProduceStateScope<T>,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> producer);
-    method public static <T> androidx.compose.runtime.SnapshotMutationPolicy<T> referentialEqualityPolicy();
-    method @androidx.compose.runtime.Composable public static <T> androidx.compose.runtime.State<T> rememberUpdatedState(T? newValue);
-    method public static inline operator <T> void setValue(androidx.compose.runtime.MutableState<T>, Object? thisObj, kotlin.reflect.KProperty<?> property, T? value);
-    method public static <T> kotlinx.coroutines.flow.Flow<T> snapshotFlow(kotlin.jvm.functions.Function0<? extends T> block);
-    method public static <T> androidx.compose.runtime.SnapshotMutationPolicy<T> structuralEqualityPolicy();
-    method public static <T> androidx.compose.runtime.snapshots.SnapshotStateList<T> toMutableStateList(java.util.Collection<? extends T>);
-    method public static <K, V> androidx.compose.runtime.snapshots.SnapshotStateMap<K,V> toMutableStateMap(Iterable<? extends kotlin.Pair<? extends K,? extends V>>);
-  }
-
   @androidx.compose.runtime.StableMarker @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY}) public @interface Stable {
   }
 
diff --git a/compose/runtime/runtime/api/restricted_current.ignore b/compose/runtime/runtime/api/restricted_current.ignore
index e28d869..1589837 100644
--- a/compose/runtime/runtime/api/restricted_current.ignore
+++ b/compose/runtime/runtime/api/restricted_current.ignore
@@ -3,3 +3,7 @@
     Added method androidx.compose.runtime.Composer.getCurrentCompositionLocalMap()
 AddedAbstractMethod: androidx.compose.runtime.CompositionContext#getEffectCoroutineContext():
     Added method androidx.compose.runtime.CompositionContext.getEffectCoroutineContext()
+
+
+RemovedClass: androidx.compose.runtime.SnapshotStateKt:
+    Removed class androidx.compose.runtime.SnapshotStateKt
diff --git a/compose/runtime/runtime/api/restricted_current.txt b/compose/runtime/runtime/api/restricted_current.txt
index 39b95b6..8af600b 100644
--- a/compose/runtime/runtime/api/restricted_current.txt
+++ b/compose/runtime/runtime/api/restricted_current.txt
@@ -484,110 +484,6 @@
     method public static <K, V> androidx.compose.runtime.snapshots.SnapshotStateMap<K,V> toMutableStateMap(Iterable<? extends kotlin.Pair<? extends K,? extends V>>);
   }
 
-  public final class SnapshotStateKt {
-    method @androidx.compose.runtime.Composable public static <T> androidx.compose.runtime.State<T> collectAsState(kotlinx.coroutines.flow.StateFlow<? extends T>, optional kotlin.coroutines.CoroutineContext context);
-    method @androidx.compose.runtime.Composable public static <T extends R, R> androidx.compose.runtime.State<R> collectAsState(kotlinx.coroutines.flow.Flow<? extends T>, R? initial, optional kotlin.coroutines.CoroutineContext context);
-    method public static <T> androidx.compose.runtime.State<T> derivedStateOf(kotlin.jvm.functions.Function0<? extends T> calculation);
-    method public static <T> androidx.compose.runtime.State<T> derivedStateOf(androidx.compose.runtime.SnapshotMutationPolicy<T> policy, kotlin.jvm.functions.Function0<? extends T> calculation);
-    method public static inline operator <T> T! getValue(androidx.compose.runtime.State<? extends T>, Object? thisObj, kotlin.reflect.KProperty<?> property);
-    method public static <T> androidx.compose.runtime.snapshots.SnapshotStateList<T> mutableStateListOf();
-    method public static <T> androidx.compose.runtime.snapshots.SnapshotStateList<T> mutableStateListOf(T?... elements);
-    method public static <K, V> androidx.compose.runtime.snapshots.SnapshotStateMap<K,V> mutableStateMapOf();
-    method public static <K, V> androidx.compose.runtime.snapshots.SnapshotStateMap<K,V> mutableStateMapOf(kotlin.Pair<? extends K,? extends V>... pairs);
-    method public static <T> androidx.compose.runtime.MutableState<T> mutableStateOf(T? value, optional androidx.compose.runtime.SnapshotMutationPolicy<T> policy);
-    method public static <T> androidx.compose.runtime.SnapshotMutationPolicy<T> neverEqualPolicy();
-    method @androidx.compose.runtime.Composable public static <T> androidx.compose.runtime.State<T> produceState(T? initialValue, kotlin.jvm.functions.Function2<? super androidx.compose.runtime.ProduceStateScope<T>,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> producer);
-    method @androidx.compose.runtime.Composable public static <T> androidx.compose.runtime.State<T> produceState(T? initialValue, Object? key1, kotlin.jvm.functions.Function2<? super androidx.compose.runtime.ProduceStateScope<T>,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> producer);
-    method @androidx.compose.runtime.Composable public static <T> androidx.compose.runtime.State<T> produceState(T? initialValue, Object? key1, Object? key2, kotlin.jvm.functions.Function2<? super androidx.compose.runtime.ProduceStateScope<T>,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> producer);
-    method @androidx.compose.runtime.Composable public static <T> androidx.compose.runtime.State<T> produceState(T? initialValue, Object? key1, Object? key2, Object? key3, kotlin.jvm.functions.Function2<? super androidx.compose.runtime.ProduceStateScope<T>,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> producer);
-    method @androidx.compose.runtime.Composable public static <T> androidx.compose.runtime.State<T> produceState(T? initialValue, Object![]? keys, kotlin.jvm.functions.Function2<? super androidx.compose.runtime.ProduceStateScope<T>,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> producer);
-    method public static <T> androidx.compose.runtime.SnapshotMutationPolicy<T> referentialEqualityPolicy();
-    method @androidx.compose.runtime.Composable public static <T> androidx.compose.runtime.State<T> rememberUpdatedState(T? newValue);
-    method public static inline operator <T> void setValue(androidx.compose.runtime.MutableState<T>, Object? thisObj, kotlin.reflect.KProperty<?> property, T? value);
-    method public static <T> kotlinx.coroutines.flow.Flow<T> snapshotFlow(kotlin.jvm.functions.Function0<? extends T> block);
-    method public static <T> androidx.compose.runtime.SnapshotMutationPolicy<T> structuralEqualityPolicy();
-    method public static <T> androidx.compose.runtime.snapshots.SnapshotStateList<T> toMutableStateList(java.util.Collection<? extends T>);
-    method public static <K, V> androidx.compose.runtime.snapshots.SnapshotStateMap<K,V> toMutableStateMap(Iterable<? extends kotlin.Pair<? extends K,? extends V>>);
-  }
-
-  public final class SnapshotStateKt {
-    method @androidx.compose.runtime.Composable public static <T> androidx.compose.runtime.State<T> collectAsState(kotlinx.coroutines.flow.StateFlow<? extends T>, optional kotlin.coroutines.CoroutineContext context);
-    method @androidx.compose.runtime.Composable public static <T extends R, R> androidx.compose.runtime.State<R> collectAsState(kotlinx.coroutines.flow.Flow<? extends T>, R? initial, optional kotlin.coroutines.CoroutineContext context);
-    method public static <T> androidx.compose.runtime.State<T> derivedStateOf(kotlin.jvm.functions.Function0<? extends T> calculation);
-    method public static <T> androidx.compose.runtime.State<T> derivedStateOf(androidx.compose.runtime.SnapshotMutationPolicy<T> policy, kotlin.jvm.functions.Function0<? extends T> calculation);
-    method public static inline operator <T> T! getValue(androidx.compose.runtime.State<? extends T>, Object? thisObj, kotlin.reflect.KProperty<?> property);
-    method public static <T> androidx.compose.runtime.snapshots.SnapshotStateList<T> mutableStateListOf();
-    method public static <T> androidx.compose.runtime.snapshots.SnapshotStateList<T> mutableStateListOf(T?... elements);
-    method public static <K, V> androidx.compose.runtime.snapshots.SnapshotStateMap<K,V> mutableStateMapOf();
-    method public static <K, V> androidx.compose.runtime.snapshots.SnapshotStateMap<K,V> mutableStateMapOf(kotlin.Pair<? extends K,? extends V>... pairs);
-    method public static <T> androidx.compose.runtime.MutableState<T> mutableStateOf(T? value, optional androidx.compose.runtime.SnapshotMutationPolicy<T> policy);
-    method public static <T> androidx.compose.runtime.SnapshotMutationPolicy<T> neverEqualPolicy();
-    method @androidx.compose.runtime.Composable public static <T> androidx.compose.runtime.State<T> produceState(T? initialValue, kotlin.jvm.functions.Function2<? super androidx.compose.runtime.ProduceStateScope<T>,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> producer);
-    method @androidx.compose.runtime.Composable public static <T> androidx.compose.runtime.State<T> produceState(T? initialValue, Object? key1, kotlin.jvm.functions.Function2<? super androidx.compose.runtime.ProduceStateScope<T>,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> producer);
-    method @androidx.compose.runtime.Composable public static <T> androidx.compose.runtime.State<T> produceState(T? initialValue, Object? key1, Object? key2, kotlin.jvm.functions.Function2<? super androidx.compose.runtime.ProduceStateScope<T>,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> producer);
-    method @androidx.compose.runtime.Composable public static <T> androidx.compose.runtime.State<T> produceState(T? initialValue, Object? key1, Object? key2, Object? key3, kotlin.jvm.functions.Function2<? super androidx.compose.runtime.ProduceStateScope<T>,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> producer);
-    method @androidx.compose.runtime.Composable public static <T> androidx.compose.runtime.State<T> produceState(T? initialValue, Object![]? keys, kotlin.jvm.functions.Function2<? super androidx.compose.runtime.ProduceStateScope<T>,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> producer);
-    method public static <T> androidx.compose.runtime.SnapshotMutationPolicy<T> referentialEqualityPolicy();
-    method @androidx.compose.runtime.Composable public static <T> androidx.compose.runtime.State<T> rememberUpdatedState(T? newValue);
-    method public static inline operator <T> void setValue(androidx.compose.runtime.MutableState<T>, Object? thisObj, kotlin.reflect.KProperty<?> property, T? value);
-    method public static <T> kotlinx.coroutines.flow.Flow<T> snapshotFlow(kotlin.jvm.functions.Function0<? extends T> block);
-    method public static <T> androidx.compose.runtime.SnapshotMutationPolicy<T> structuralEqualityPolicy();
-    method public static <T> androidx.compose.runtime.snapshots.SnapshotStateList<T> toMutableStateList(java.util.Collection<? extends T>);
-    method public static <K, V> androidx.compose.runtime.snapshots.SnapshotStateMap<K,V> toMutableStateMap(Iterable<? extends kotlin.Pair<? extends K,? extends V>>);
-  }
-
-  public final class SnapshotStateKt {
-    method @androidx.compose.runtime.Composable public static <T> androidx.compose.runtime.State<T> collectAsState(kotlinx.coroutines.flow.StateFlow<? extends T>, optional kotlin.coroutines.CoroutineContext context);
-    method @androidx.compose.runtime.Composable public static <T extends R, R> androidx.compose.runtime.State<R> collectAsState(kotlinx.coroutines.flow.Flow<? extends T>, R? initial, optional kotlin.coroutines.CoroutineContext context);
-    method public static <T> androidx.compose.runtime.State<T> derivedStateOf(kotlin.jvm.functions.Function0<? extends T> calculation);
-    method public static <T> androidx.compose.runtime.State<T> derivedStateOf(androidx.compose.runtime.SnapshotMutationPolicy<T> policy, kotlin.jvm.functions.Function0<? extends T> calculation);
-    method public static inline operator <T> T! getValue(androidx.compose.runtime.State<? extends T>, Object? thisObj, kotlin.reflect.KProperty<?> property);
-    method public static <T> androidx.compose.runtime.snapshots.SnapshotStateList<T> mutableStateListOf();
-    method public static <T> androidx.compose.runtime.snapshots.SnapshotStateList<T> mutableStateListOf(T?... elements);
-    method public static <K, V> androidx.compose.runtime.snapshots.SnapshotStateMap<K,V> mutableStateMapOf();
-    method public static <K, V> androidx.compose.runtime.snapshots.SnapshotStateMap<K,V> mutableStateMapOf(kotlin.Pair<? extends K,? extends V>... pairs);
-    method public static <T> androidx.compose.runtime.MutableState<T> mutableStateOf(T? value, optional androidx.compose.runtime.SnapshotMutationPolicy<T> policy);
-    method public static <T> androidx.compose.runtime.SnapshotMutationPolicy<T> neverEqualPolicy();
-    method @androidx.compose.runtime.Composable public static <T> androidx.compose.runtime.State<T> produceState(T? initialValue, kotlin.jvm.functions.Function2<? super androidx.compose.runtime.ProduceStateScope<T>,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> producer);
-    method @androidx.compose.runtime.Composable public static <T> androidx.compose.runtime.State<T> produceState(T? initialValue, Object? key1, kotlin.jvm.functions.Function2<? super androidx.compose.runtime.ProduceStateScope<T>,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> producer);
-    method @androidx.compose.runtime.Composable public static <T> androidx.compose.runtime.State<T> produceState(T? initialValue, Object? key1, Object? key2, kotlin.jvm.functions.Function2<? super androidx.compose.runtime.ProduceStateScope<T>,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> producer);
-    method @androidx.compose.runtime.Composable public static <T> androidx.compose.runtime.State<T> produceState(T? initialValue, Object? key1, Object? key2, Object? key3, kotlin.jvm.functions.Function2<? super androidx.compose.runtime.ProduceStateScope<T>,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> producer);
-    method @androidx.compose.runtime.Composable public static <T> androidx.compose.runtime.State<T> produceState(T? initialValue, Object![]? keys, kotlin.jvm.functions.Function2<? super androidx.compose.runtime.ProduceStateScope<T>,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> producer);
-    method public static <T> androidx.compose.runtime.SnapshotMutationPolicy<T> referentialEqualityPolicy();
-    method @androidx.compose.runtime.Composable public static <T> androidx.compose.runtime.State<T> rememberUpdatedState(T? newValue);
-    method public static inline operator <T> void setValue(androidx.compose.runtime.MutableState<T>, Object? thisObj, kotlin.reflect.KProperty<?> property, T? value);
-    method public static <T> kotlinx.coroutines.flow.Flow<T> snapshotFlow(kotlin.jvm.functions.Function0<? extends T> block);
-    method public static <T> androidx.compose.runtime.SnapshotMutationPolicy<T> structuralEqualityPolicy();
-    method public static <T> androidx.compose.runtime.snapshots.SnapshotStateList<T> toMutableStateList(java.util.Collection<? extends T>);
-    method public static <K, V> androidx.compose.runtime.snapshots.SnapshotStateMap<K,V> toMutableStateMap(Iterable<? extends kotlin.Pair<? extends K,? extends V>>);
-  }
-
-  public final class SnapshotStateKt {
-    method @androidx.compose.runtime.Composable public static <T> androidx.compose.runtime.State<T> collectAsState(kotlinx.coroutines.flow.StateFlow<? extends T>, optional kotlin.coroutines.CoroutineContext context);
-    method @androidx.compose.runtime.Composable public static <T extends R, R> androidx.compose.runtime.State<R> collectAsState(kotlinx.coroutines.flow.Flow<? extends T>, R? initial, optional kotlin.coroutines.CoroutineContext context);
-    method public static <T> androidx.compose.runtime.State<T> derivedStateOf(kotlin.jvm.functions.Function0<? extends T> calculation);
-    method public static <T> androidx.compose.runtime.State<T> derivedStateOf(androidx.compose.runtime.SnapshotMutationPolicy<T> policy, kotlin.jvm.functions.Function0<? extends T> calculation);
-    method public static inline operator <T> T! getValue(androidx.compose.runtime.State<? extends T>, Object? thisObj, kotlin.reflect.KProperty<?> property);
-    method public static <T> androidx.compose.runtime.snapshots.SnapshotStateList<T> mutableStateListOf();
-    method public static <T> androidx.compose.runtime.snapshots.SnapshotStateList<T> mutableStateListOf(T?... elements);
-    method public static <K, V> androidx.compose.runtime.snapshots.SnapshotStateMap<K,V> mutableStateMapOf();
-    method public static <K, V> androidx.compose.runtime.snapshots.SnapshotStateMap<K,V> mutableStateMapOf(kotlin.Pair<? extends K,? extends V>... pairs);
-    method public static <T> androidx.compose.runtime.MutableState<T> mutableStateOf(T? value, optional androidx.compose.runtime.SnapshotMutationPolicy<T> policy);
-    method public static <T> androidx.compose.runtime.SnapshotMutationPolicy<T> neverEqualPolicy();
-    method @androidx.compose.runtime.Composable public static <T> androidx.compose.runtime.State<T> produceState(T? initialValue, kotlin.jvm.functions.Function2<? super androidx.compose.runtime.ProduceStateScope<T>,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> producer);
-    method @androidx.compose.runtime.Composable public static <T> androidx.compose.runtime.State<T> produceState(T? initialValue, Object? key1, kotlin.jvm.functions.Function2<? super androidx.compose.runtime.ProduceStateScope<T>,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> producer);
-    method @androidx.compose.runtime.Composable public static <T> androidx.compose.runtime.State<T> produceState(T? initialValue, Object? key1, Object? key2, kotlin.jvm.functions.Function2<? super androidx.compose.runtime.ProduceStateScope<T>,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> producer);
-    method @androidx.compose.runtime.Composable public static <T> androidx.compose.runtime.State<T> produceState(T? initialValue, Object? key1, Object? key2, Object? key3, kotlin.jvm.functions.Function2<? super androidx.compose.runtime.ProduceStateScope<T>,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> producer);
-    method @androidx.compose.runtime.Composable public static <T> androidx.compose.runtime.State<T> produceState(T? initialValue, Object![]? keys, kotlin.jvm.functions.Function2<? super androidx.compose.runtime.ProduceStateScope<T>,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> producer);
-    method public static <T> androidx.compose.runtime.SnapshotMutationPolicy<T> referentialEqualityPolicy();
-    method @androidx.compose.runtime.Composable public static <T> androidx.compose.runtime.State<T> rememberUpdatedState(T? newValue);
-    method public static inline operator <T> void setValue(androidx.compose.runtime.MutableState<T>, Object? thisObj, kotlin.reflect.KProperty<?> property, T? value);
-    method public static <T> kotlinx.coroutines.flow.Flow<T> snapshotFlow(kotlin.jvm.functions.Function0<? extends T> block);
-    method public static <T> androidx.compose.runtime.SnapshotMutationPolicy<T> structuralEqualityPolicy();
-    method public static <T> androidx.compose.runtime.snapshots.SnapshotStateList<T> toMutableStateList(java.util.Collection<? extends T>);
-    method public static <K, V> androidx.compose.runtime.snapshots.SnapshotStateMap<K,V> toMutableStateMap(Iterable<? extends kotlin.Pair<? extends K,? extends V>>);
-  }
-
   @androidx.compose.runtime.StableMarker @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY}) public @interface Stable {
   }
 
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 60f3519..149bfbb 100644
--- a/glance/glance-appwidget/integration-tests/demos/src/main/AndroidManifest.xml
+++ b/glance/glance-appwidget/integration-tests/demos/src/main/AndroidManifest.xml
@@ -151,6 +151,19 @@
         </receiver>
 
         <receiver
+            android:name="androidx.glance.appwidget.demos.RippleAppWidgetReceiver"
+            android:label="@string/ripple_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>
+
+        <receiver
             android:name="androidx.glance.appwidget.demos.VerticalGridAppWidgetReceiver"
             android:label="@string/grid_widget_name"
             android:enabled="@bool/glance_appwidget_available"
diff --git a/glance/glance-appwidget/integration-tests/demos/src/main/java/androidx/glance/appwidget/demos/RippleAppWidget.kt b/glance/glance-appwidget/integration-tests/demos/src/main/java/androidx/glance/appwidget/demos/RippleAppWidget.kt
new file mode 100644
index 0000000..8202282
--- /dev/null
+++ b/glance/glance-appwidget/integration-tests/demos/src/main/java/androidx/glance/appwidget/demos/RippleAppWidget.kt
@@ -0,0 +1,189 @@
+/*
+ * 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.Context
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.unit.dp
+import androidx.glance.GlanceId
+import androidx.glance.GlanceModifier
+import androidx.glance.Image
+import androidx.glance.ImageProvider
+import androidx.glance.action.clickable
+import androidx.glance.appwidget.GlanceAppWidget
+import androidx.glance.appwidget.GlanceAppWidgetReceiver
+import androidx.glance.appwidget.cornerRadius
+import androidx.glance.appwidget.provideContent
+import androidx.glance.background
+import androidx.glance.color.ColorProvider
+import androidx.glance.layout.Alignment
+import androidx.glance.layout.Box
+import androidx.glance.layout.Column
+import androidx.glance.layout.ContentScale
+import androidx.glance.layout.Spacer
+import androidx.glance.layout.fillMaxSize
+import androidx.glance.layout.fillMaxWidth
+import androidx.glance.layout.height
+import androidx.glance.layout.padding
+import androidx.glance.layout.size
+import androidx.glance.text.FontWeight
+import androidx.glance.text.Text
+import androidx.glance.text.TextAlign
+import androidx.glance.text.TextStyle
+
+/**
+ * Sample AppWidget to showcase the ripples.
+ * Note: Rounded corners are supported in S+
+ */
+class RippleAppWidget : GlanceAppWidget() {
+    private val columnBgColorsA = listOf(Color(0xffA2BDF2), Color(0xff5087EF))
+    private val columnBgColorsB = listOf(Color(0xFFBD789C), Color(0xFF880E4F))
+    private val boxColors = listOf(Color(0xffF7A998), Color(0xffFA5F3D))
+
+    override suspend fun provideGlance(
+        context: Context,
+        id: GlanceId
+    ) = provideContent {
+        RippleDemoContent()
+    }
+
+    @Composable
+    private fun RippleDemoContent() {
+        var count by remember { mutableStateOf(0) }
+        var type by remember { mutableStateOf(ContentScale.Fit) }
+        var columnBgColors by remember { mutableStateOf(columnBgColorsA) }
+
+        Column(
+            horizontalAlignment = Alignment.CenterHorizontally,
+            modifier = GlanceModifier.fillMaxSize().padding(8.dp)
+                .cornerRadius(20.dp)
+                .background(ColorProvider(day = columnBgColors[0], night = columnBgColors[1]))
+                .clickable {
+                    columnBgColors = when (columnBgColors[0]) {
+                        columnBgColorsA[0] -> columnBgColorsB
+                        else -> columnBgColorsA
+                    }
+                }
+        ) {
+            Text(
+                text = "Content Scale: ${type.asString()}, Image / Box click count: $count",
+                modifier = GlanceModifier.padding(5.dp)
+            )
+            // A drawable image with rounded corners and a click modifier.
+            OutlinedButtonUsingImage(text = "Toggle content scale", onClick = {
+                type = when (type) {
+                    ContentScale.Crop -> ContentScale.FillBounds
+                    ContentScale.FillBounds -> ContentScale.Fit
+                    else -> ContentScale.Crop
+                }
+            })
+            Spacer(GlanceModifier.size(5.dp))
+            Text(
+                text = "Image in a clickable box with rounded corners",
+                modifier = GlanceModifier.padding(5.dp)
+            )
+            ImageInClickableBoxWithRoundedCorners(contentScale = type, onClick = { count++ })
+            Spacer(GlanceModifier.size(5.dp))
+            Text(
+                text = "Rounded corner image in a clickable box",
+                modifier = GlanceModifier.padding(5.dp)
+            )
+            RoundedImageInClickableBox(contentScale = type, onClick = { count++ })
+        }
+    }
+    @Composable
+    private fun ImageInClickableBoxWithRoundedCorners(
+        contentScale: ContentScale,
+        onClick: () -> Unit
+    ) {
+        Box(
+            modifier = GlanceModifier
+                .height(100.dp)
+                .background(ColorProvider(day = boxColors[0], night = boxColors[1]))
+                .cornerRadius(25.dp)
+                .clickable(onClick)
+        ) {
+            Image(
+                provider = ImageProvider(R.drawable.compose),
+                contentDescription = "Image sample in a box with rounded corners",
+                contentScale = contentScale,
+                modifier = GlanceModifier.fillMaxSize()
+            )
+        }
+    }
+
+    @Composable
+    private fun RoundedImageInClickableBox(contentScale: ContentScale, onClick: () -> Unit) {
+        Box(
+            modifier = GlanceModifier
+                .height(100.dp)
+                .background(ColorProvider(day = boxColors[0], night = boxColors[1]))
+                .clickable(onClick)
+        ) {
+            Image(
+                provider = ImageProvider(R.drawable.compose),
+                contentDescription = "Image sample with rounded corners",
+                contentScale = contentScale,
+                modifier = GlanceModifier.fillMaxSize().cornerRadius(25.dp)
+            )
+        }
+    }
+
+    @Composable
+    fun OutlinedButtonUsingImage(
+        text: String,
+        onClick: () -> Unit,
+    ) {
+        Box(
+            modifier = GlanceModifier.height(40.dp).fillMaxWidth(),
+            contentAlignment = Alignment.Center
+        ) {
+            // Demonstrates a button with rounded outline using a clickable image. Alternatively,
+            // such button can also be created using Box + Text by adding background image, corner
+            // radius and click modifiers to the box.
+            Image(
+                provider = ImageProvider(R.drawable.ic_outlined_button),
+                contentDescription = "Outlined button sample",
+                // Radius value matched with the border in the outline image so that the ripple
+                // matches it (in versions that support cornerRadius modifier).
+                modifier = GlanceModifier.fillMaxSize().cornerRadius(20.dp).clickable(onClick)
+            )
+            Text(
+                text = text,
+                style = TextStyle(fontWeight = FontWeight.Medium, textAlign = TextAlign.Center),
+                modifier = GlanceModifier.background(Color.Transparent)
+            )
+        }
+    }
+
+    private fun ContentScale.asString(): String =
+        when (this) {
+            ContentScale.Fit -> "Fit"
+            ContentScale.FillBounds -> "Fill Bounds"
+            ContentScale.Crop -> "Crop"
+            else -> "Unknown content scale"
+        }
+}
+
+class RippleAppWidgetReceiver : GlanceAppWidgetReceiver() {
+    override val glanceAppWidget: GlanceAppWidget = RippleAppWidget()
+}
\ No newline at end of file
diff --git a/glance/glance-appwidget/integration-tests/demos/src/main/res/drawable/ic_outlined_button.xml b/glance/glance-appwidget/integration-tests/demos/src/main/res/drawable/ic_outlined_button.xml
new file mode 100644
index 0000000..ee58ce0
--- /dev/null
+++ b/glance/glance-appwidget/integration-tests/demos/src/main/res/drawable/ic_outlined_button.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright 2023 The Android Open Source Project
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+  -->
+<shape xmlns:android="http://schemas.android.com/apk/res/android">
+    <solid android:color="@android:color/transparent" />
+    <corners android:radius="20dp"/>
+    <stroke android:width="1dp" android:color="#000000" />
+    <padding android:left="1dp" android:top="1dp" android:right="1dp"
+        android:bottom="1dp" />
+</shape>
\ 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 3f8a5df..1bf0d9a 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,6 +29,7 @@
     <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="ripple_widget_name">Ripple Widget</string>
     <string name="grid_widget_name">Vertical Grid Widget</string>
     <string name="default_state_widget_name">Default State Widget</string>
     <string name="progress_indicator_widget_name">ProgressBar Widget</string>
diff --git a/glance/glance-appwidget/src/androidAndroidTest/kotlin/androidx/glance/appwidget/GlanceAppWidgetReceiverTest.kt b/glance/glance-appwidget/src/androidAndroidTest/kotlin/androidx/glance/appwidget/GlanceAppWidgetReceiverTest.kt
index 8f20d20..818ac07 100644
--- a/glance/glance-appwidget/src/androidAndroidTest/kotlin/androidx/glance/appwidget/GlanceAppWidgetReceiverTest.kt
+++ b/glance/glance-appwidget/src/androidAndroidTest/kotlin/androidx/glance/appwidget/GlanceAppWidgetReceiverTest.kt
@@ -32,6 +32,7 @@
 import android.widget.CompoundButton
 import android.widget.FrameLayout
 import android.widget.ImageView
+import android.widget.ImageView.ScaleType
 import android.widget.LinearLayout
 import android.widget.RadioButton
 import android.widget.TextView
@@ -488,8 +489,14 @@
 
         mHostRule.startHost()
 
-        mHostRule.onUnboxedHostView<TextView> { textView ->
-            assertThat(textView.background).isNotNull()
+        mHostRule.onUnboxedHostView<FrameLayout> { box ->
+            assertThat(box.notGoneChildCount).isEqualTo(2)
+            val (boxedImage, boxedText) = box.notGoneChildren.toList()
+            val image = boxedImage.getTargetView<ImageView>()
+            val text = boxedText.getTargetView<TextView>()
+            assertThat(image.drawable).isNotNull()
+            assertThat(image.scaleType).isEqualTo(ScaleType.FIT_XY)
+            assertThat(text.background).isNull()
         }
     }
 
@@ -511,6 +518,30 @@
             val image = boxedImage.getTargetView<ImageView>()
             val text = boxedText.getTargetView<TextView>()
             assertThat(image.drawable).isNotNull()
+            assertThat(image.scaleType).isEqualTo(ScaleType.FIT_CENTER)
+            assertThat(text.background).isNull()
+        }
+    }
+
+    @Test
+    fun drawableCropBackground() {
+        TestGlanceAppWidget.uiDefinition = {
+            Text(
+                "Some useful text",
+                modifier = GlanceModifier.fillMaxWidth().height(220.dp)
+                    .background(ImageProvider(R.drawable.oval), contentScale = ContentScale.Crop)
+            )
+        }
+
+        mHostRule.startHost()
+
+        mHostRule.onUnboxedHostView<FrameLayout> { box ->
+            assertThat(box.notGoneChildCount).isEqualTo(2)
+            val (boxedImage, boxedText) = box.notGoneChildren.toList()
+            val image = boxedImage.getTargetView<ImageView>()
+            val text = boxedText.getTargetView<TextView>()
+            assertThat(image.drawable).isNotNull()
+            assertThat(image.scaleType).isEqualTo(ScaleType.CENTER_CROP)
             assertThat(text.background).isNull()
         }
     }
diff --git a/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/NormalizeCompositionTree.kt b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/NormalizeCompositionTree.kt
index 5336388..40a865e 100644
--- a/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/NormalizeCompositionTree.kt
+++ b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/NormalizeCompositionTree.kt
@@ -17,7 +17,6 @@
 
 import android.util.Log
 import androidx.compose.ui.unit.dp
-import androidx.glance.AndroidResourceImageProvider
 import androidx.glance.BackgroundModifier
 import androidx.glance.Emittable
 import androidx.glance.EmittableButton
@@ -33,7 +32,6 @@
 import androidx.glance.extractModifier
 import androidx.glance.findModifier
 import androidx.glance.layout.Alignment
-import androidx.glance.layout.ContentScale
 import androidx.glance.layout.EmittableBox
 import androidx.glance.layout.HeightModifier
 import androidx.glance.layout.WidthModifier
@@ -189,36 +187,63 @@
     if (this is EmittableLazyListItem || this is EmittableSizeBox) return this
 
     var target = this
-
-    // We only need to add a background image view if the background is a Bitmap, or a
-    // drawable resource with non-default content scale. Otherwise, we can set the background
-    // directly on the target element in ApplyModifiers.kt.
-    val (bgModifier, notBgModifier) = target.modifier.extractModifier<BackgroundModifier>()
-    val addBackground = bgModifier?.imageProvider != null &&
-        (bgModifier.imageProvider !is AndroidResourceImageProvider ||
-            bgModifier.contentScale != ContentScale.FillBounds)
-
-    // Add a ripple for every element with an action that does not have already have a built in
-    // ripple.
-    notBgModifier.warnIfMultipleClickableActions()
-    val (actionModifier, notBgOrActionModifier) = notBgModifier.extractModifier<ActionModifier>()
-    val addRipple = actionModifier != null && !hasBuiltinRipple()
     val isButton = target is EmittableButton
-    if (!addBackground && !addRipple && !isButton) return target
 
-    // Hoist the size and action modifiers to the wrapping Box, then set the target element to fill
-    // the given space. doNotUnsetAction() prevents the views within the Box from being made
-    // clickable.
-    val (sizeModifiers, nonSizeModifiers) = notBgOrActionModifier.extractSizeModifiers()
-    val boxModifiers = mutableListOf<GlanceModifier?>(sizeModifiers, actionModifier)
-    val targetModifiers = mutableListOf<GlanceModifier?>(
-        nonSizeModifiers.fillMaxSize()
-    )
-
-    // If we don't need to emulate the background, add the background modifier back to the target.
-    if (!addBackground) {
-        targetModifiers += bgModifier
+    val shouldWrapTargetInABox = target.modifier.any {
+        // Background images (i.e. BitMap or drawable resources) are emulated by placing the image
+        // before the target in the wrapper box. This allows us to support content scale as well as
+        // additional image features (e.g. color filters) on background images.
+        (it is BackgroundModifier && it.imageProvider != null) ||
+            // Ripples are implemented by placing a drawable after the target in the wrapper box.
+            (it is ActionModifier && !hasBuiltinRipple()) ||
+            // Buttons are implemented using a background drawable with rounded corners and an
+            // EmittableText.
+            isButton
     }
+    if (!shouldWrapTargetInABox) return target
+
+    // Hoisted modifiers are subtracted from the target one by one and added to the box and the
+    // remaining modifiers are applied to the target.
+    val boxModifiers = mutableListOf<GlanceModifier?>()
+    val targetModifiers = mutableListOf<GlanceModifier?>()
+    var backgroundImage: EmittableImage? = null
+    var rippleImage: EmittableImage? = null
+
+    // bgModifier.imageProvider is converted to an actual image but bgModifier.colorProvider is
+    // applied back to the target. Note: We could have hoisted the bg color to box instead of
+    // adding it back to the target, but for buttons, we also add an outline background to the box.
+    val (bgModifier, targetModifiersMinusBg) = target.modifier.extractModifier<BackgroundModifier>()
+    if (bgModifier != null) {
+        if (bgModifier.imageProvider != null) {
+            backgroundImage = EmittableImage().apply {
+                modifier = GlanceModifier.fillMaxSize()
+                provider = bgModifier.imageProvider
+                contentScale = bgModifier.contentScale
+            }
+        } else { // is a background color modifier
+            targetModifiers += bgModifier
+        }
+    }
+
+    // Action modifiers are hoisted on the wrapping box and a ripple image is added to the
+    // foreground if the target doesn't have it built-in.
+    targetModifiersMinusBg.warnIfMultipleClickableActions()
+    val (actionModifier, targetModifiersMinusAction) =
+        targetModifiersMinusBg.extractModifier<ActionModifier>()
+    boxModifiers += actionModifier
+    if (actionModifier != null && !hasBuiltinRipple()) {
+        rippleImage = EmittableImage().apply {
+            modifier = GlanceModifier.fillMaxSize()
+            provider = ImageProvider(R.drawable.glance_ripple)
+        }
+    }
+
+    // Hoist the size and corner radius modifiers to the wrapping Box, then set the target element
+    // to fill the given space.
+    val (sizeAndCornerModifiers, targetModifiersMinusSizeAndCornerRadius) =
+        targetModifiersMinusAction.extractSizeAndCornerRadiusModifiers()
+    boxModifiers += sizeAndCornerModifiers
+    targetModifiers += targetModifiersMinusSizeAndCornerRadius.fillMaxSize()
 
     // If this is a button, set the necessary modifiers on the wrapping Box.
     if (target is EmittableButton) {
@@ -234,20 +259,9 @@
         modifier = boxModifiers.collect()
         if (isButton) contentAlignment = Alignment.Center
 
-        if (addBackground && bgModifier != null) {
-            children += EmittableImage().apply {
-                modifier = GlanceModifier.fillMaxSize()
-                provider = bgModifier.imageProvider
-                contentScale = bgModifier.contentScale
-            }
-        }
+        backgroundImage?.let { children += it }
         children += target.apply { modifier = targetModifiers.collect() }
-        if (addRipple) {
-            children += EmittableImage().apply {
-                modifier = GlanceModifier.fillMaxSize()
-                provider = ImageProvider(R.drawable.glance_ripple)
-            }
-        }
+        rippleImage?.let { children += it }
     }
 }
 
@@ -262,13 +276,15 @@
 )
 
 /**
- * Split the [GlanceModifier] into one that contains the [WidthModifier]s and [HeightModifier]s and
- * one that contains the rest.
+ * Split the [GlanceModifier] into one that contains the [WidthModifier]s, [HeightModifier]s and
+ * and [CornerRadiusModifier]s and one that contains the rest.
  */
-private fun GlanceModifier.extractSizeModifiers() =
-    if (any { it is WidthModifier || it is HeightModifier }) {
+private fun GlanceModifier.extractSizeAndCornerRadiusModifiers() =
+    if (any { it is WidthModifier || it is HeightModifier || it is CornerRadiusModifier }) {
         foldIn(ExtractedSizeModifiers()) { acc, modifier ->
-            if (modifier is WidthModifier || modifier is HeightModifier) {
+            if (modifier is WidthModifier ||
+                modifier is HeightModifier ||
+                modifier is CornerRadiusModifier) {
                 acc.copy(sizeModifiers = acc.sizeModifiers.then(modifier))
             } else {
                 acc.copy(nonSizeModifiers = acc.nonSizeModifiers.then(modifier))
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 2383b5d9..31189e2 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -98,7 +98,7 @@
 checkerframework = { module = "org.checkerframework:checker-qual", version = "2.5.3" }
 checkmark = { module = "net.saff.checkmark:checkmark", version = "0.1.6" }
 constraintLayout = { module = "androidx.constraintlayout:constraintlayout", version = "2.0.1"}
-dackka = { module = "com.google.devsite:dackka", version = "1.3.0" }
+dackka = { module = "com.google.devsite:dackka", version = "1.3.1" }
 dagger = { module = "com.google.dagger:dagger", version.ref = "dagger" }
 daggerCompiler = { module = "com.google.dagger:dagger-compiler", version.ref = "dagger" }
 dexmakerMockito = { module = "com.linkedin.dexmaker:dexmaker-mockito", version.ref = "dexmaker" }
diff --git a/graphics/graphics-core/src/androidTest/java/androidx/graphics/lowlatency/SingleBufferedCanvasRendererV29Test.kt b/graphics/graphics-core/src/androidTest/java/androidx/graphics/lowlatency/SingleBufferedCanvasRendererV29Test.kt
index 736b93e..5ecc43e 100644
--- a/graphics/graphics-core/src/androidTest/java/androidx/graphics/lowlatency/SingleBufferedCanvasRendererV29Test.kt
+++ b/graphics/graphics-core/src/androidTest/java/androidx/graphics/lowlatency/SingleBufferedCanvasRendererV29Test.kt
@@ -41,6 +41,7 @@
 import org.junit.Assert.assertFalse
 import org.junit.Assert.assertNotNull
 import org.junit.Assert.assertTrue
+import org.junit.Ignore
 import org.junit.Test
 import org.junit.runner.RunWith
 
@@ -352,6 +353,7 @@
         }
     }
 
+    @Ignore("b/274099885")
     @SdkSuppress(minSdkVersion = Build.VERSION_CODES.Q)
     @Test
     fun testBatchedRenders() {
diff --git a/paging/paging-rxjava2/api/current.ignore b/paging/paging-rxjava2/api/current.ignore
index 4c496cc..0e1e678 100644
--- a/paging/paging-rxjava2/api/current.ignore
+++ b/paging/paging-rxjava2/api/current.ignore
@@ -1,3 +1,7 @@
 // Baseline format: 1.0
 ParameterNameChange: androidx.paging.rxjava2.RxPagingSource#load(androidx.paging.PagingSource.LoadParams<Key>, kotlin.coroutines.Continuation<? super androidx.paging.PagingSource.LoadResult<Key,Value>>) parameter #1:
     Attempted to remove parameter name from parameter arg2 in androidx.paging.rxjava2.RxPagingSource.load
+
+
+RemovedClass: androidx.paging.rxjava2.PagingRx:
+    Removed class androidx.paging.rxjava2.PagingRx
diff --git a/paging/paging-rxjava2/api/current.txt b/paging/paging-rxjava2/api/current.txt
index 0b95aeb..9a9398b 100644
--- a/paging/paging-rxjava2/api/current.txt
+++ b/paging/paging-rxjava2/api/current.txt
@@ -38,15 +38,6 @@
     method @CheckResult public static <T, R> androidx.paging.PagingData<R> map(androidx.paging.PagingData<T>, kotlin.jvm.functions.Function1<? super T,? extends io.reactivex.Single<R>> transform);
   }
 
-  public final class PagingRx {
-    method @CheckResult public static <T> androidx.paging.PagingData<T> filter(androidx.paging.PagingData<T>, kotlin.jvm.functions.Function1<? super T,? extends io.reactivex.Single<java.lang.Boolean>> predicate);
-    method @CheckResult public static <T, R> androidx.paging.PagingData<R> flatMap(androidx.paging.PagingData<T>, kotlin.jvm.functions.Function1<? super T,? extends io.reactivex.Single<java.lang.Iterable<R>>> transform);
-    method public static <Key, Value> io.reactivex.Flowable<androidx.paging.PagingData<Value>> getFlowable(androidx.paging.Pager<Key,Value>);
-    method public static <Key, Value> io.reactivex.Observable<androidx.paging.PagingData<Value>> getObservable(androidx.paging.Pager<Key,Value>);
-    method @CheckResult public static <T extends R, R> androidx.paging.PagingData<R> insertSeparators(androidx.paging.PagingData<T>, kotlin.jvm.functions.Function2<? super T,? super T,? extends io.reactivex.Maybe<R>> generator);
-    method @CheckResult public static <T, R> androidx.paging.PagingData<R> map(androidx.paging.PagingData<T>, kotlin.jvm.functions.Function1<? super T,? extends io.reactivex.Single<R>> transform);
-  }
-
   public abstract class RxPagingSource<Key, Value> extends androidx.paging.PagingSource<Key,Value> {
     ctor public RxPagingSource();
     method public final suspend Object? load(androidx.paging.PagingSource.LoadParams<Key> params, kotlin.coroutines.Continuation<? super androidx.paging.PagingSource.LoadResult<Key,Value>>);
diff --git a/paging/paging-rxjava2/api/public_plus_experimental_current.txt b/paging/paging-rxjava2/api/public_plus_experimental_current.txt
index 7179ce2..acadd7c 100644
--- a/paging/paging-rxjava2/api/public_plus_experimental_current.txt
+++ b/paging/paging-rxjava2/api/public_plus_experimental_current.txt
@@ -40,17 +40,6 @@
     method @CheckResult public static <T, R> androidx.paging.PagingData<R> map(androidx.paging.PagingData<T>, kotlin.jvm.functions.Function1<? super T,? extends io.reactivex.Single<R>> transform);
   }
 
-  public final class PagingRx {
-    method @kotlinx.coroutines.ExperimentalCoroutinesApi public static <T> io.reactivex.Observable<androidx.paging.PagingData<T>> cachedIn(io.reactivex.Observable<androidx.paging.PagingData<T>>, kotlinx.coroutines.CoroutineScope scope);
-    method @kotlinx.coroutines.ExperimentalCoroutinesApi public static <T> io.reactivex.Flowable<androidx.paging.PagingData<T>> cachedIn(io.reactivex.Flowable<androidx.paging.PagingData<T>>, kotlinx.coroutines.CoroutineScope scope);
-    method @CheckResult public static <T> androidx.paging.PagingData<T> filter(androidx.paging.PagingData<T>, kotlin.jvm.functions.Function1<? super T,? extends io.reactivex.Single<java.lang.Boolean>> predicate);
-    method @CheckResult public static <T, R> androidx.paging.PagingData<R> flatMap(androidx.paging.PagingData<T>, kotlin.jvm.functions.Function1<? super T,? extends io.reactivex.Single<java.lang.Iterable<R>>> transform);
-    method public static <Key, Value> io.reactivex.Flowable<androidx.paging.PagingData<Value>> getFlowable(androidx.paging.Pager<Key,Value>);
-    method public static <Key, Value> io.reactivex.Observable<androidx.paging.PagingData<Value>> getObservable(androidx.paging.Pager<Key,Value>);
-    method @CheckResult public static <T extends R, R> androidx.paging.PagingData<R> insertSeparators(androidx.paging.PagingData<T>, kotlin.jvm.functions.Function2<? super T,? super T,? extends io.reactivex.Maybe<R>> generator);
-    method @CheckResult public static <T, R> androidx.paging.PagingData<R> map(androidx.paging.PagingData<T>, kotlin.jvm.functions.Function1<? super T,? extends io.reactivex.Single<R>> transform);
-  }
-
   public abstract class RxPagingSource<Key, Value> extends androidx.paging.PagingSource<Key,Value> {
     ctor public RxPagingSource();
     method public final suspend Object? load(androidx.paging.PagingSource.LoadParams<Key> params, kotlin.coroutines.Continuation<? super androidx.paging.PagingSource.LoadResult<Key,Value>>);
diff --git a/paging/paging-rxjava2/api/restricted_current.ignore b/paging/paging-rxjava2/api/restricted_current.ignore
index 4c496cc..0e1e678 100644
--- a/paging/paging-rxjava2/api/restricted_current.ignore
+++ b/paging/paging-rxjava2/api/restricted_current.ignore
@@ -1,3 +1,7 @@
 // Baseline format: 1.0
 ParameterNameChange: androidx.paging.rxjava2.RxPagingSource#load(androidx.paging.PagingSource.LoadParams<Key>, kotlin.coroutines.Continuation<? super androidx.paging.PagingSource.LoadResult<Key,Value>>) parameter #1:
     Attempted to remove parameter name from parameter arg2 in androidx.paging.rxjava2.RxPagingSource.load
+
+
+RemovedClass: androidx.paging.rxjava2.PagingRx:
+    Removed class androidx.paging.rxjava2.PagingRx
diff --git a/paging/paging-rxjava2/api/restricted_current.txt b/paging/paging-rxjava2/api/restricted_current.txt
index 0b95aeb..9a9398b 100644
--- a/paging/paging-rxjava2/api/restricted_current.txt
+++ b/paging/paging-rxjava2/api/restricted_current.txt
@@ -38,15 +38,6 @@
     method @CheckResult public static <T, R> androidx.paging.PagingData<R> map(androidx.paging.PagingData<T>, kotlin.jvm.functions.Function1<? super T,? extends io.reactivex.Single<R>> transform);
   }
 
-  public final class PagingRx {
-    method @CheckResult public static <T> androidx.paging.PagingData<T> filter(androidx.paging.PagingData<T>, kotlin.jvm.functions.Function1<? super T,? extends io.reactivex.Single<java.lang.Boolean>> predicate);
-    method @CheckResult public static <T, R> androidx.paging.PagingData<R> flatMap(androidx.paging.PagingData<T>, kotlin.jvm.functions.Function1<? super T,? extends io.reactivex.Single<java.lang.Iterable<R>>> transform);
-    method public static <Key, Value> io.reactivex.Flowable<androidx.paging.PagingData<Value>> getFlowable(androidx.paging.Pager<Key,Value>);
-    method public static <Key, Value> io.reactivex.Observable<androidx.paging.PagingData<Value>> getObservable(androidx.paging.Pager<Key,Value>);
-    method @CheckResult public static <T extends R, R> androidx.paging.PagingData<R> insertSeparators(androidx.paging.PagingData<T>, kotlin.jvm.functions.Function2<? super T,? super T,? extends io.reactivex.Maybe<R>> generator);
-    method @CheckResult public static <T, R> androidx.paging.PagingData<R> map(androidx.paging.PagingData<T>, kotlin.jvm.functions.Function1<? super T,? extends io.reactivex.Single<R>> transform);
-  }
-
   public abstract class RxPagingSource<Key, Value> extends androidx.paging.PagingSource<Key,Value> {
     ctor public RxPagingSource();
     method public final suspend Object? load(androidx.paging.PagingSource.LoadParams<Key> params, kotlin.coroutines.Continuation<? super androidx.paging.PagingSource.LoadResult<Key,Value>>);
diff --git a/paging/paging-rxjava3/api/current.ignore b/paging/paging-rxjava3/api/current.ignore
index 6159e34..451c35d 100644
--- a/paging/paging-rxjava3/api/current.ignore
+++ b/paging/paging-rxjava3/api/current.ignore
@@ -1,3 +1,7 @@
 // Baseline format: 1.0
 ParameterNameChange: androidx.paging.rxjava3.RxPagingSource#load(androidx.paging.PagingSource.LoadParams<Key>, kotlin.coroutines.Continuation<? super androidx.paging.PagingSource.LoadResult<Key,Value>>) parameter #1:
     Attempted to remove parameter name from parameter arg2 in androidx.paging.rxjava3.RxPagingSource.load
+
+
+RemovedClass: androidx.paging.rxjava3.PagingRx:
+    Removed class androidx.paging.rxjava3.PagingRx
diff --git a/paging/paging-rxjava3/api/current.txt b/paging/paging-rxjava3/api/current.txt
index 7af17b8..b77cbbe 100644
--- a/paging/paging-rxjava3/api/current.txt
+++ b/paging/paging-rxjava3/api/current.txt
@@ -10,15 +10,6 @@
     method @CheckResult public static <T, R> androidx.paging.PagingData<R> map(androidx.paging.PagingData<T>, kotlin.jvm.functions.Function1<? super T,? extends io.reactivex.rxjava3.core.Single<R>> transform);
   }
 
-  public final class PagingRx {
-    method @CheckResult public static <T> androidx.paging.PagingData<T> filter(androidx.paging.PagingData<T>, kotlin.jvm.functions.Function1<? super T,? extends io.reactivex.rxjava3.core.Single<java.lang.Boolean>> predicate);
-    method @CheckResult public static <T, R> androidx.paging.PagingData<R> flatMap(androidx.paging.PagingData<T>, kotlin.jvm.functions.Function1<? super T,? extends io.reactivex.rxjava3.core.Single<java.lang.Iterable<R>>> transform);
-    method public static <Key, Value> io.reactivex.rxjava3.core.Flowable<androidx.paging.PagingData<Value>> getFlowable(androidx.paging.Pager<Key,Value>);
-    method public static <Key, Value> io.reactivex.rxjava3.core.Observable<androidx.paging.PagingData<Value>> getObservable(androidx.paging.Pager<Key,Value>);
-    method @CheckResult public static <T extends R, R> androidx.paging.PagingData<R> insertSeparators(androidx.paging.PagingData<T>, kotlin.jvm.functions.Function2<? super T,? super T,? extends io.reactivex.rxjava3.core.Maybe<R>> generator);
-    method @CheckResult public static <T, R> androidx.paging.PagingData<R> map(androidx.paging.PagingData<T>, kotlin.jvm.functions.Function1<? super T,? extends io.reactivex.rxjava3.core.Single<R>> transform);
-  }
-
   @Deprecated public final class RxPagedListBuilder<Key, Value> {
     ctor @Deprecated public RxPagedListBuilder(kotlin.jvm.functions.Function0<? extends androidx.paging.PagingSource<Key,Value>> pagingSourceFactory, androidx.paging.PagedList.Config config);
     ctor @Deprecated public RxPagedListBuilder(kotlin.jvm.functions.Function0<? extends androidx.paging.PagingSource<Key,Value>> pagingSourceFactory, int pageSize);
diff --git a/paging/paging-rxjava3/api/public_plus_experimental_current.txt b/paging/paging-rxjava3/api/public_plus_experimental_current.txt
index 6ed617d..68770f0 100644
--- a/paging/paging-rxjava3/api/public_plus_experimental_current.txt
+++ b/paging/paging-rxjava3/api/public_plus_experimental_current.txt
@@ -12,17 +12,6 @@
     method @CheckResult public static <T, R> androidx.paging.PagingData<R> map(androidx.paging.PagingData<T>, kotlin.jvm.functions.Function1<? super T,? extends io.reactivex.rxjava3.core.Single<R>> transform);
   }
 
-  public final class PagingRx {
-    method @kotlinx.coroutines.ExperimentalCoroutinesApi public static <T> io.reactivex.rxjava3.core.Observable<androidx.paging.PagingData<T>> cachedIn(io.reactivex.rxjava3.core.Observable<androidx.paging.PagingData<T>>, kotlinx.coroutines.CoroutineScope scope);
-    method @kotlinx.coroutines.ExperimentalCoroutinesApi public static <T> io.reactivex.rxjava3.core.Flowable<androidx.paging.PagingData<T>> cachedIn(io.reactivex.rxjava3.core.Flowable<androidx.paging.PagingData<T>>, kotlinx.coroutines.CoroutineScope scope);
-    method @CheckResult public static <T> androidx.paging.PagingData<T> filter(androidx.paging.PagingData<T>, kotlin.jvm.functions.Function1<? super T,? extends io.reactivex.rxjava3.core.Single<java.lang.Boolean>> predicate);
-    method @CheckResult public static <T, R> androidx.paging.PagingData<R> flatMap(androidx.paging.PagingData<T>, kotlin.jvm.functions.Function1<? super T,? extends io.reactivex.rxjava3.core.Single<java.lang.Iterable<R>>> transform);
-    method public static <Key, Value> io.reactivex.rxjava3.core.Flowable<androidx.paging.PagingData<Value>> getFlowable(androidx.paging.Pager<Key,Value>);
-    method public static <Key, Value> io.reactivex.rxjava3.core.Observable<androidx.paging.PagingData<Value>> getObservable(androidx.paging.Pager<Key,Value>);
-    method @CheckResult public static <T extends R, R> androidx.paging.PagingData<R> insertSeparators(androidx.paging.PagingData<T>, kotlin.jvm.functions.Function2<? super T,? super T,? extends io.reactivex.rxjava3.core.Maybe<R>> generator);
-    method @CheckResult public static <T, R> androidx.paging.PagingData<R> map(androidx.paging.PagingData<T>, kotlin.jvm.functions.Function1<? super T,? extends io.reactivex.rxjava3.core.Single<R>> transform);
-  }
-
   @Deprecated public final class RxPagedListBuilder<Key, Value> {
     ctor @Deprecated public RxPagedListBuilder(kotlin.jvm.functions.Function0<? extends androidx.paging.PagingSource<Key,Value>> pagingSourceFactory, androidx.paging.PagedList.Config config);
     ctor @Deprecated public RxPagedListBuilder(kotlin.jvm.functions.Function0<? extends androidx.paging.PagingSource<Key,Value>> pagingSourceFactory, int pageSize);
diff --git a/paging/paging-rxjava3/api/restricted_current.ignore b/paging/paging-rxjava3/api/restricted_current.ignore
index 6159e34..451c35d 100644
--- a/paging/paging-rxjava3/api/restricted_current.ignore
+++ b/paging/paging-rxjava3/api/restricted_current.ignore
@@ -1,3 +1,7 @@
 // Baseline format: 1.0
 ParameterNameChange: androidx.paging.rxjava3.RxPagingSource#load(androidx.paging.PagingSource.LoadParams<Key>, kotlin.coroutines.Continuation<? super androidx.paging.PagingSource.LoadResult<Key,Value>>) parameter #1:
     Attempted to remove parameter name from parameter arg2 in androidx.paging.rxjava3.RxPagingSource.load
+
+
+RemovedClass: androidx.paging.rxjava3.PagingRx:
+    Removed class androidx.paging.rxjava3.PagingRx
diff --git a/paging/paging-rxjava3/api/restricted_current.txt b/paging/paging-rxjava3/api/restricted_current.txt
index 7af17b8..b77cbbe 100644
--- a/paging/paging-rxjava3/api/restricted_current.txt
+++ b/paging/paging-rxjava3/api/restricted_current.txt
@@ -10,15 +10,6 @@
     method @CheckResult public static <T, R> androidx.paging.PagingData<R> map(androidx.paging.PagingData<T>, kotlin.jvm.functions.Function1<? super T,? extends io.reactivex.rxjava3.core.Single<R>> transform);
   }
 
-  public final class PagingRx {
-    method @CheckResult public static <T> androidx.paging.PagingData<T> filter(androidx.paging.PagingData<T>, kotlin.jvm.functions.Function1<? super T,? extends io.reactivex.rxjava3.core.Single<java.lang.Boolean>> predicate);
-    method @CheckResult public static <T, R> androidx.paging.PagingData<R> flatMap(androidx.paging.PagingData<T>, kotlin.jvm.functions.Function1<? super T,? extends io.reactivex.rxjava3.core.Single<java.lang.Iterable<R>>> transform);
-    method public static <Key, Value> io.reactivex.rxjava3.core.Flowable<androidx.paging.PagingData<Value>> getFlowable(androidx.paging.Pager<Key,Value>);
-    method public static <Key, Value> io.reactivex.rxjava3.core.Observable<androidx.paging.PagingData<Value>> getObservable(androidx.paging.Pager<Key,Value>);
-    method @CheckResult public static <T extends R, R> androidx.paging.PagingData<R> insertSeparators(androidx.paging.PagingData<T>, kotlin.jvm.functions.Function2<? super T,? super T,? extends io.reactivex.rxjava3.core.Maybe<R>> generator);
-    method @CheckResult public static <T, R> androidx.paging.PagingData<R> map(androidx.paging.PagingData<T>, kotlin.jvm.functions.Function1<? super T,? extends io.reactivex.rxjava3.core.Single<R>> transform);
-  }
-
   @Deprecated public final class RxPagedListBuilder<Key, Value> {
     ctor @Deprecated public RxPagedListBuilder(kotlin.jvm.functions.Function0<? extends androidx.paging.PagingSource<Key,Value>> pagingSourceFactory, androidx.paging.PagedList.Config config);
     ctor @Deprecated public RxPagedListBuilder(kotlin.jvm.functions.Function0<? extends androidx.paging.PagingSource<Key,Value>> pagingSourceFactory, int pageSize);
diff --git a/room/room-ktx/build.gradle b/room/room-ktx/build.gradle
index 466e365..5e4a868 100644
--- a/room/room-ktx/build.gradle
+++ b/room/room-ktx/build.gradle
@@ -30,12 +30,12 @@
     api(libs.kotlinCoroutinesAndroid)
     testImplementation(libs.junit)
     testImplementation(libs.mockitoCore4)
-    testImplementation(libs.truth)
+    testImplementation(project(":internal-testutils-kmp"))
     testImplementation("androidx.lifecycle:lifecycle-livedata-core:2.0.0")
     testImplementation(libs.testRunner)
     testImplementation(libs.kotlinCoroutinesTest)
 
-    androidTestImplementation(libs.truth)
+    androidTestImplementation(project(":internal-testutils-kmp"))
     androidTestImplementation(libs.testRunner)
     androidTestImplementation(libs.kotlinCoroutinesTest)
 }
diff --git a/room/room-ktx/src/androidTest/java/androidx/room/CoroutineRoomCancellationTest.kt b/room/room-ktx/src/androidTest/java/androidx/room/CoroutineRoomCancellationTest.kt
index aaa8ba9..97dcc0d 100644
--- a/room/room-ktx/src/androidTest/java/androidx/room/CoroutineRoomCancellationTest.kt
+++ b/room/room-ktx/src/androidTest/java/androidx/room/CoroutineRoomCancellationTest.kt
@@ -18,10 +18,10 @@
 
 import android.database.sqlite.SQLiteException
 import android.os.CancellationSignal
+import androidx.kruth.assertThat
 import androidx.sqlite.db.SupportSQLiteOpenHelper
 import androidx.test.filters.SdkSuppress
 import androidx.test.filters.SmallTest
-import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.DelicateCoroutinesApi
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -32,7 +32,6 @@
 import kotlinx.coroutines.test.StandardTestDispatcher
 import kotlinx.coroutines.test.TestScope
 import kotlinx.coroutines.test.runCurrent
-import org.junit.Assert.assertTrue
 import org.junit.Assert.fail
 import org.junit.Test
 import java.util.concurrent.Callable
@@ -135,7 +134,7 @@
                     }
                 )
             } catch (exception: Throwable) {
-                assertTrue(exception is SQLiteException)
+                assertThat(exception).isInstanceOf<SQLiteException>()
             }
         }
 
diff --git a/room/room-ktx/src/test/java/androidx/room/CoroutinesRoomTest.kt b/room/room-ktx/src/test/java/androidx/room/CoroutinesRoomTest.kt
index 46e43ec..08dc8c5 100644
--- a/room/room-ktx/src/test/java/androidx/room/CoroutinesRoomTest.kt
+++ b/room/room-ktx/src/test/java/androidx/room/CoroutinesRoomTest.kt
@@ -16,8 +16,8 @@
 
 package androidx.room
 
+import androidx.kruth.assertThat
 import androidx.sqlite.db.SupportSQLiteOpenHelper
-import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.async
 import kotlinx.coroutines.flow.first
@@ -56,11 +56,11 @@
         }
         yield(); yield() // yield for async and flow
 
-        assertThat(invalidationTracker.observers.size).isEqualTo(1)
+        assertThat(invalidationTracker.observers).hasSize(1)
         assertThat(callableExecuted).isTrue()
 
         assertThat(job.await()).isEqualTo(expectedResult)
-        assertThat(invalidationTracker.observers.isEmpty()).isTrue()
+        assertThat(invalidationTracker.observers).isEmpty()
     }
 
     // Use runBlocking dispatcher as query dispatchers, keeps the tests consistent.
diff --git a/room/room-ktx/src/test/java/androidx/room/MigrationTest.kt b/room/room-ktx/src/test/java/androidx/room/MigrationTest.kt
index 162f50b..f49e8ce 100644
--- a/room/room-ktx/src/test/java/androidx/room/MigrationTest.kt
+++ b/room/room-ktx/src/test/java/androidx/room/MigrationTest.kt
@@ -21,11 +21,11 @@
 import android.database.sqlite.SQLiteTransactionListener
 import android.os.CancellationSignal
 import android.util.Pair
+import androidx.kruth.assertThat
 import androidx.room.migration.Migration
 import androidx.sqlite.db.SupportSQLiteDatabase
 import androidx.sqlite.db.SupportSQLiteQuery
 import androidx.sqlite.db.SupportSQLiteStatement
-import com.google.common.truth.Truth.assertThat
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.junit.runners.JUnit4
diff --git a/slidingpanelayout/slidingpanelayout/build.gradle b/slidingpanelayout/slidingpanelayout/build.gradle
index c1e7fc2..6a29dca 100644
--- a/slidingpanelayout/slidingpanelayout/build.gradle
+++ b/slidingpanelayout/slidingpanelayout/build.gradle
@@ -19,7 +19,7 @@
     androidTestImplementation(libs.kotlinStdlib)
     androidTestImplementation(libs.truth)
     androidTestImplementation(project(':internal-testutils-runtime'))
-    androidTestImplementation(project(":window:window-testing"))
+    androidTestImplementation("androidx.window:window-testing:1.0.0")
 }
 
 androidx {
diff --git a/tv/integration-tests/demos/src/main/java/androidx/tv/integration/demos/FeaturedCarousel.kt b/tv/integration-tests/demos/src/main/java/androidx/tv/integration/demos/FeaturedCarousel.kt
index deeafa0..6a5007f 100644
--- a/tv/integration-tests/demos/src/main/java/androidx/tv/integration/demos/FeaturedCarousel.kt
+++ b/tv/integration-tests/demos/src/main/java/androidx/tv/integration/demos/FeaturedCarousel.kt
@@ -112,22 +112,22 @@
 
     val carouselState = remember { CarouselState() }
     Carousel(
-        slideCount = backgrounds.size,
+        itemCount = backgrounds.size,
         carouselState = carouselState,
         modifier = modifier
             .height(300.dp)
             .fillMaxWidth(),
         carouselIndicator = {
             CarouselDefaults.IndicatorRow(
-                slideCount = backgrounds.size,
-                activeSlideIndex = carouselState.activeSlideIndex,
+                itemCount = backgrounds.size,
+                activeItemIndex = carouselState.activeItemIndex,
                 modifier = Modifier
                     .align(Alignment.BottomEnd)
                     .padding(16.dp),
             )
         }
     ) { itemIndex ->
-        CarouselSlide(
+        CarouselItem(
             background = {
                 Box(
                     modifier = Modifier
diff --git a/tv/samples/src/main/java/androidx/tv/samples/CarouselSamples.kt b/tv/samples/src/main/java/androidx/tv/samples/CarouselSamples.kt
index ab964bd..aba7109 100644
--- a/tv/samples/src/main/java/androidx/tv/samples/CarouselSamples.kt
+++ b/tv/samples/src/main/java/androidx/tv/samples/CarouselSamples.kt
@@ -17,7 +17,6 @@
 package androidx.tv.samples
 
 import androidx.annotation.Sampled
-import androidx.compose.animation.ExperimentalAnimationApi
 import androidx.compose.foundation.background
 import androidx.compose.foundation.border
 import androidx.compose.foundation.layout.Box
@@ -45,7 +44,7 @@
 import androidx.tv.material3.CarouselState
 import androidx.tv.material3.ExperimentalTvMaterial3Api
 
-@OptIn(ExperimentalTvMaterial3Api::class, ExperimentalAnimationApi::class)
+@OptIn(ExperimentalTvMaterial3Api::class)
 @Sampled
 @Composable
 fun SimpleCarousel() {
@@ -56,12 +55,12 @@
     )
 
     Carousel(
-        slideCount = backgrounds.size,
+        itemCount = backgrounds.size,
         modifier = Modifier
             .height(300.dp)
             .fillMaxWidth(),
     ) { itemIndex ->
-        CarouselSlide(
+        CarouselItem(
             background = {
                 Box(
                     modifier = Modifier
@@ -91,7 +90,7 @@
     }
 }
 
-@OptIn(ExperimentalTvMaterial3Api::class, ExperimentalAnimationApi::class)
+@OptIn(ExperimentalTvMaterial3Api::class)
 @Sampled
 @Composable
 fun CarouselIndicatorWithRectangleShape() {
@@ -103,15 +102,15 @@
     val carouselState = remember { CarouselState() }
 
     Carousel(
-        slideCount = backgrounds.size,
+        itemCount = backgrounds.size,
         modifier = Modifier
             .height(300.dp)
             .fillMaxWidth(),
         carouselState = carouselState,
         carouselIndicator = {
             CarouselDefaults.IndicatorRow(
-                slideCount = backgrounds.size,
-                activeSlideIndex = carouselState.activeSlideIndex,
+                itemCount = backgrounds.size,
+                activeItemIndex = carouselState.activeItemIndex,
                 modifier = Modifier
                     .align(Alignment.BottomEnd)
                     .padding(16.dp),
@@ -130,7 +129,7 @@
             )
         }
     ) { itemIndex ->
-        CarouselSlide(
+        CarouselItem(
             background = {
                 Box(
                     modifier = Modifier
diff --git a/tv/tv-material/api/public_plus_experimental_current.txt b/tv/tv-material/api/public_plus_experimental_current.txt
index 38f62bb..b0fe368 100644
--- a/tv/tv-material/api/public_plus_experimental_current.txt
+++ b/tv/tv-material/api/public_plus_experimental_current.txt
@@ -25,38 +25,38 @@
   }
 
   @androidx.tv.material3.ExperimentalTvMaterial3Api public final class CarouselDefaults {
-    method @androidx.compose.runtime.Composable @androidx.tv.material3.ExperimentalTvMaterial3Api public void IndicatorRow(int slideCount, int activeSlideIndex, optional androidx.compose.ui.Modifier modifier, optional float spacing, optional kotlin.jvm.functions.Function1<? super java.lang.Boolean,kotlin.Unit> indicator);
+    method @androidx.compose.runtime.Composable @androidx.tv.material3.ExperimentalTvMaterial3Api public void IndicatorRow(int itemCount, int activeItemIndex, optional androidx.compose.ui.Modifier modifier, optional float spacing, optional kotlin.jvm.functions.Function1<? super java.lang.Boolean,kotlin.Unit> indicator);
     method @androidx.compose.runtime.Composable public androidx.compose.animation.ContentTransform getContentTransform();
     property @androidx.compose.runtime.Composable public final androidx.compose.animation.ContentTransform contentTransform;
     field public static final androidx.tv.material3.CarouselDefaults INSTANCE;
-    field public static final long TimeToDisplaySlideMillis = 5000L; // 0x1388L
+    field public static final long TimeToDisplayItemMillis = 5000L; // 0x1388L
+  }
+
+  @androidx.tv.material3.ExperimentalTvMaterial3Api public final class CarouselItemDefaults {
+    method @androidx.compose.runtime.Composable public androidx.compose.animation.ContentTransform getContentTransformEndToStart();
+    method @androidx.compose.runtime.Composable public androidx.compose.animation.ContentTransform getContentTransformLeftToRight();
+    method @androidx.compose.runtime.Composable public androidx.compose.animation.ContentTransform getContentTransformRightToLeft();
+    method @androidx.compose.runtime.Composable public androidx.compose.animation.ContentTransform getContentTransformStartToEnd();
+    property @androidx.compose.runtime.Composable public final androidx.compose.animation.ContentTransform contentTransformEndToStart;
+    property @androidx.compose.runtime.Composable public final androidx.compose.animation.ContentTransform contentTransformLeftToRight;
+    property @androidx.compose.runtime.Composable public final androidx.compose.animation.ContentTransform contentTransformRightToLeft;
+    property @androidx.compose.runtime.Composable public final androidx.compose.animation.ContentTransform contentTransformStartToEnd;
+    field public static final androidx.tv.material3.CarouselItemDefaults INSTANCE;
   }
 
   public final class CarouselKt {
-    method @androidx.compose.runtime.Composable @androidx.tv.material3.ExperimentalTvMaterial3Api public static void Carousel(int slideCount, optional androidx.compose.ui.Modifier modifier, optional androidx.tv.material3.CarouselState carouselState, optional long autoScrollDurationMillis, optional androidx.compose.animation.ContentTransform contentTransformForward, optional androidx.compose.animation.ContentTransform contentTransformBackward, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit> carouselIndicator, kotlin.jvm.functions.Function2<? super androidx.tv.material3.CarouselScope,? super java.lang.Integer,kotlin.Unit> content);
+    method @androidx.compose.runtime.Composable @androidx.tv.material3.ExperimentalTvMaterial3Api public static void Carousel(int itemCount, optional androidx.compose.ui.Modifier modifier, optional androidx.tv.material3.CarouselState carouselState, optional long autoScrollDurationMillis, optional androidx.compose.animation.ContentTransform contentTransformStartToEnd, optional androidx.compose.animation.ContentTransform contentTransformEndToStart, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit> carouselIndicator, kotlin.jvm.functions.Function2<? super androidx.tv.material3.CarouselScope,? super java.lang.Integer,kotlin.Unit> content);
   }
 
   @androidx.tv.material3.ExperimentalTvMaterial3Api public final class CarouselScope {
-    method @androidx.compose.runtime.Composable @androidx.tv.material3.ExperimentalTvMaterial3Api public void CarouselSlide(optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit> background, optional androidx.compose.animation.ContentTransform contentTransformForward, optional androidx.compose.animation.ContentTransform contentTransformBackward, kotlin.jvm.functions.Function0<kotlin.Unit> content);
-  }
-
-  @androidx.tv.material3.ExperimentalTvMaterial3Api public final class CarouselSlideDefaults {
-    method @androidx.compose.runtime.Composable public androidx.compose.animation.ContentTransform getContentTransformBackward();
-    method @androidx.compose.runtime.Composable public androidx.compose.animation.ContentTransform getContentTransformForward();
-    method @androidx.compose.runtime.Composable public androidx.compose.animation.ContentTransform getContentTransformLeftToRight();
-    method @androidx.compose.runtime.Composable public androidx.compose.animation.ContentTransform getContentTransformRightToLeft();
-    property @androidx.compose.runtime.Composable public final androidx.compose.animation.ContentTransform contentTransformBackward;
-    property @androidx.compose.runtime.Composable public final androidx.compose.animation.ContentTransform contentTransformForward;
-    property @androidx.compose.runtime.Composable public final androidx.compose.animation.ContentTransform contentTransformLeftToRight;
-    property @androidx.compose.runtime.Composable public final androidx.compose.animation.ContentTransform contentTransformRightToLeft;
-    field public static final androidx.tv.material3.CarouselSlideDefaults INSTANCE;
+    method @androidx.compose.runtime.Composable @androidx.tv.material3.ExperimentalTvMaterial3Api public void CarouselItem(optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit> background, optional androidx.compose.animation.ContentTransform contentTransformStartToEnd, optional androidx.compose.animation.ContentTransform contentTransformEndToStart, kotlin.jvm.functions.Function0<kotlin.Unit> content);
   }
 
   @androidx.compose.runtime.Stable @androidx.tv.material3.ExperimentalTvMaterial3Api public final class CarouselState {
-    ctor public CarouselState(optional int initialActiveSlideIndex);
-    method public int getActiveSlideIndex();
-    method public androidx.tv.material3.ScrollPauseHandle pauseAutoScroll(int slideIndex);
-    property public final int activeSlideIndex;
+    ctor public CarouselState(optional int initialActiveItemIndex);
+    method public int getActiveItemIndex();
+    method public androidx.tv.material3.ScrollPauseHandle pauseAutoScroll(int itemIndex);
+    property public final int activeItemIndex;
   }
 
   @androidx.compose.runtime.Immutable @androidx.tv.material3.ExperimentalTvMaterial3Api public final class ClickableSurfaceBorder {
diff --git a/tv/tv-material/src/androidTest/java/androidx/tv/material3/CarouselScopeTest.kt b/tv/tv-material/src/androidTest/java/androidx/tv/material3/CarouselScopeTest.kt
index 6fdeaa9..10080e0 100644
--- a/tv/tv-material/src/androidTest/java/androidx/tv/material3/CarouselScopeTest.kt
+++ b/tv/tv-material/src/androidTest/java/androidx/tv/material3/CarouselScopeTest.kt
@@ -52,9 +52,9 @@
 
     @OptIn(ExperimentalTvMaterial3Api::class, ExperimentalAnimationApi::class)
     @Test
-    fun carouselSlide_parentContainerGainsFocused_onBackPress() {
+    fun carouselItem_parentContainerGainsFocused_onBackPress() {
         val containerBoxTag = "container-box"
-        val carouselSlideTag = "carousel-slide"
+        val carouselItemTag = "carousel-item"
 
         rule.setContent {
             val carouselState = remember { CarouselState() }
@@ -68,8 +68,8 @@
                     .focusable()
             ) {
                 CarouselScope(carouselState = carouselState)
-                    .CarouselSlide(
-                        modifier = Modifier.testTag(carouselSlideTag),
+                    .CarouselItem(
+                        modifier = Modifier.testTag(carouselItemTag),
                         background = {
                             Box(
                                 modifier = Modifier
@@ -82,7 +82,7 @@
         }
 
         // Request focus for Carousel Item on start
-        rule.onNodeWithTag(carouselSlideTag)
+        rule.onNodeWithTag(carouselItemTag)
             .performSemanticsAction(SemanticsActions.RequestFocus)
         rule.waitForIdle()
 
diff --git a/tv/tv-material/src/androidTest/java/androidx/tv/material3/CarouselTest.kt b/tv/tv-material/src/androidTest/java/androidx/tv/material3/CarouselTest.kt
index d9c5a2f..7582b2c 100644
--- a/tv/tv-material/src/androidTest/java/androidx/tv/material3/CarouselTest.kt
+++ b/tv/tv-material/src/androidTest/java/androidx/tv/material3/CarouselTest.kt
@@ -71,7 +71,7 @@
 import org.junit.Rule
 import org.junit.Test
 
-private const val delayBetweenSlides = 2500L
+private const val delayBetweenItems = 2500L
 private const val animationTime = 900L
 
 @OptIn(ExperimentalTvMaterial3Api::class)
@@ -89,10 +89,10 @@
 
         rule.onNodeWithText("Text 1").assertIsDisplayed()
 
-        rule.mainClock.advanceTimeBy(delayBetweenSlides)
+        rule.mainClock.advanceTimeBy(delayBetweenItems)
         rule.onNodeWithText("Text 2").assertIsDisplayed()
 
-        rule.mainClock.advanceTimeBy(delayBetweenSlides)
+        rule.mainClock.advanceTimeBy(delayBetweenItems)
         rule.onNodeWithText("Text 3").assertIsDisplayed()
     }
 
@@ -111,7 +111,7 @@
             .onParent()
             .performSemanticsAction(SemanticsActions.RequestFocus)
 
-        rule.mainClock.advanceTimeBy(delayBetweenSlides)
+        rule.mainClock.advanceTimeBy(delayBetweenItems)
 
         rule.onNodeWithText("Text 2").assertDoesNotExist()
         rule.onNodeWithText("Text 1").onParent().assertIsFocused()
@@ -130,7 +130,7 @@
         rule.onNodeWithText("Text 1").assertIsDisplayed()
         rule.onNodeWithText("Text 1").onParent().assertIsNotFocused()
 
-        rule.mainClock.advanceTimeBy(delayBetweenSlides)
+        rule.mainClock.advanceTimeBy(delayBetweenItems)
 
         rule.onNodeWithText("Text 2").assertDoesNotExist()
         rule.onNodeWithText("Text 1").assertIsDisplayed()
@@ -154,17 +154,17 @@
         rule.onNodeWithText("Text 1").assertIsDisplayed()
         rule.onNodeWithText("Text 1").onParent().assertIsNotFocused()
 
-        rule.mainClock.advanceTimeBy(delayBetweenSlides)
+        rule.mainClock.advanceTimeBy(delayBetweenItems)
 
         // pause handle has not been resumed, so Text 1 should still be on the screen.
         rule.onNodeWithText("Text 2").assertDoesNotExist()
         rule.onNodeWithText("Text 1").assertIsDisplayed()
 
         rule.runOnIdle { pauseHandle?.resumeAutoScroll() }
-        rule.mainClock.advanceTimeBy(delayBetweenSlides)
+        rule.mainClock.advanceTimeBy(delayBetweenItems)
 
         // pause handle has been resumed, so Text 2 should be on the screen after
-        // delayBetweenSlides + animationTime
+        // delayBetweenItems + animationTime
         rule.onNodeWithText("Text 1").assertDoesNotExist()
         rule.onNodeWithText("Text 2").assertIsDisplayed()
     }
@@ -192,23 +192,23 @@
         rule.onNodeWithText("Text 1").assertIsDisplayed()
         rule.onNodeWithText("Text 1").onParent().assertIsNotFocused()
 
-        rule.mainClock.advanceTimeBy(delayBetweenSlides)
+        rule.mainClock.advanceTimeBy(delayBetweenItems)
 
         // pause handles have not been resumed, so Text 1 should still be on the screen.
         rule.onNodeWithText("Text 2").assertDoesNotExist()
         rule.onNodeWithText("Text 1").assertIsDisplayed()
 
         rule.runOnIdle { pauseHandle1?.resumeAutoScroll() }
-        rule.mainClock.advanceTimeBy(delayBetweenSlides)
+        rule.mainClock.advanceTimeBy(delayBetweenItems)
 
         // Second pause handle has not been resumed, so Text 1 should still be on the screen.
         rule.onNodeWithText("Text 2").assertDoesNotExist()
         rule.onNodeWithText("Text 1").assertIsDisplayed()
 
         rule.runOnIdle { pauseHandle2?.resumeAutoScroll() }
-        rule.mainClock.advanceTimeBy(delayBetweenSlides)
+        rule.mainClock.advanceTimeBy(delayBetweenItems)
         // All pause handles have been resumed, so Text 2 should be on the screen after
-        // delayBetweenSlides + animationTime
+        // delayBetweenItems + animationTime
         rule.onNodeWithText("Text 1").assertDoesNotExist()
         rule.onNodeWithText("Text 2").assertIsDisplayed()
     }
@@ -236,7 +236,7 @@
         rule.onNodeWithText("Text 1").assertIsDisplayed()
         rule.onNodeWithText("Text 1").onParent().assertIsNotFocused()
 
-        rule.mainClock.advanceTimeBy(delayBetweenSlides)
+        rule.mainClock.advanceTimeBy(delayBetweenItems)
 
         // pause handles have not been resumed, so Text 1 should still be on the screen.
         rule.onNodeWithText("Text 2").assertDoesNotExist()
@@ -245,7 +245,7 @@
         rule.runOnIdle { pauseHandle1?.resumeAutoScroll() }
         // subsequent call to resume should be ignored
         rule.runOnIdle { pauseHandle1?.resumeAutoScroll() }
-        rule.mainClock.advanceTimeBy(delayBetweenSlides)
+        rule.mainClock.advanceTimeBy(delayBetweenItems)
 
         // Second pause handle has not been resumed, so Text 1 should still be on the screen.
         rule.onNodeWithText("Text 2").assertDoesNotExist()
@@ -270,7 +270,7 @@
         rule.onNodeWithText("Card").performSemanticsAction(SemanticsActions.RequestFocus)
         rule.onNodeWithText("Card").assertIsFocused()
 
-        rule.mainClock.advanceTimeBy(delayBetweenSlides)
+        rule.mainClock.advanceTimeBy(delayBetweenItems)
         rule.onNodeWithText("Text 1").assertDoesNotExist()
         rule.onNodeWithText("Text 2").assertIsDisplayed()
     }
@@ -280,7 +280,7 @@
     fun carousel_pagerIndicatorDisplayed() {
         rule.setContent {
             SampleCarousel {
-                SampleCarouselSlide(index = it)
+                SampleCarouselItem(index = it)
             }
         }
 
@@ -292,7 +292,7 @@
     fun carousel_withAnimatedContent_successfulTransition() {
         rule.setContent {
             SampleCarousel {
-                SampleCarouselSlide(index = it) {
+                SampleCarouselItem(index = it) {
                     Column {
                         BasicText(text = "Text ${it + 1}")
                         BasicText(text = "PLAY")
@@ -313,7 +313,7 @@
     fun carousel_withAnimatedContent_successfulFocusIn() {
         rule.setContent {
             SampleCarousel {
-                SampleCarouselSlide(index = it)
+                SampleCarouselItem(index = it)
             }
         }
 
@@ -321,7 +321,7 @@
         rule.onNodeWithTag("pager")
             .performSemanticsAction(SemanticsActions.RequestFocus)
 
-        // current slide overlay render delay
+        // current item overlay render delay
         rule.mainClock.advanceTimeBy(animationTime, false)
         rule.mainClock.advanceTimeBy(animationTime, false)
         rule.mainClock.advanceTimeByFrame()
@@ -367,14 +367,14 @@
 
     @OptIn(ExperimentalAnimationApi::class)
     @Test
-    fun carousel_withCarouselSlide_parentContainerGainsFocus_onBackPress() {
+    fun carousel_withCarouselItem_parentContainerGainsFocus_onBackPress() {
         rule.setContent {
             Box(modifier = Modifier
                 .testTag("box-container")
                 .fillMaxSize()
                 .focusable()) {
                 SampleCarousel {
-                    SampleCarouselSlide(index = it)
+                    SampleCarouselItem(index = it)
                 }
             }
         }
@@ -432,10 +432,10 @@
                             .testTag("featured-carousel")
                             .border(2.dp, Color.Black),
                         carouselState = remember { CarouselState() },
-                        slideCount = 3,
-                        autoScrollDurationMillis = delayBetweenSlides
+                        itemCount = 3,
+                        autoScrollDurationMillis = delayBetweenItems
                     ) {
-                        SampleCarouselSlide(index = it) {
+                        SampleCarouselItem(index = it) {
                             Box {
                                 Column(modifier = Modifier.align(Alignment.BottomStart)) {
                                     BasicText(text = "carousel-frame")
@@ -493,10 +493,10 @@
 
     @OptIn(ExperimentalAnimationApi::class)
     @Test
-    fun carousel_zeroSlideCount_shouldNotCrash() {
+    fun carousel_zeroItemCount_shouldNotCrash() {
         val testTag = "emptyCarousel"
         rule.setContent {
-            Carousel(slideCount = 0, modifier = Modifier.testTag(testTag)) {}
+            Carousel(itemCount = 0, modifier = Modifier.testTag(testTag)) {}
         }
 
         rule.onNodeWithTag(testTag).assertExists()
@@ -504,10 +504,10 @@
 
     @OptIn(ExperimentalAnimationApi::class)
     @Test
-    fun carousel_oneSlideCount_shouldNotCrash() {
+    fun carousel_oneItemCount_shouldNotCrash() {
         val testTag = "emptyCarousel"
         rule.setContent {
-            Carousel(slideCount = 1, modifier = Modifier.testTag(testTag)) {}
+            Carousel(itemCount = 1, modifier = Modifier.testTag(testTag)) {}
         }
 
         rule.onNodeWithTag(testTag).assertExists()
@@ -536,20 +536,20 @@
         rule.mainClock.advanceTimeByFrame()
         rule.waitForIdle()
 
-        // Check that slide 1 is in view and button 1 has focus
+        // Check that item 1 is in view and button 1 has focus
         rule.onNodeWithText("Button-1").assertIsDisplayed()
         rule.onNodeWithText("Button-1").assertIsFocused()
 
-        // press dpad right to scroll to next slide
+        // press dpad right to scroll to next item
         performKeyPress(NativeKeyEvent.KEYCODE_DPAD_RIGHT)
 
-        // Wait for slide to load
+        // Wait for item to load
         rule.mainClock.advanceTimeByFrame()
         rule.waitForIdle()
         rule.mainClock.advanceTimeBy(animationTime, false)
         rule.waitForIdle()
 
-        // Check that slide 2 is in view and button 2 has focus
+        // Check that item 2 is in view and button 2 has focus
         rule.onNodeWithText("Button-2").assertIsDisplayed()
         // TODO: Fix button 2 isn't gaining focus
         // rule.onNodeWithText("Button-2").assertIsFocused()
@@ -557,16 +557,16 @@
         // Check if the first focusable element in parent has focus
         rule.onNodeWithText("Row-button-1").assertIsNotFocused()
 
-        // press dpad left to scroll to previous slide
+        // press dpad left to scroll to previous item
         performKeyPress(NativeKeyEvent.KEYCODE_DPAD_LEFT)
 
-        // Wait for slide to load
+        // Wait for item to load
         rule.mainClock.advanceTimeByFrame()
         rule.waitForIdle()
         rule.mainClock.advanceTimeBy(animationTime, false)
         rule.waitForIdle()
 
-        // Check that slide 1 is in view and button 1 has focus
+        // Check that item 1 is in view and button 1 has focus
         rule.onNodeWithText("Button-1").assertIsDisplayed()
         rule.onNodeWithText("Button-1").assertIsFocused()
     }
@@ -592,8 +592,8 @@
                     }
                 }
 
-                SampleCarousel(carouselState = carouselState, slideCount = 20) {
-                    SampleCarouselSlide(modifier = Modifier.testTag("slide-$it"), index = it)
+                SampleCarousel(carouselState = carouselState, itemCount = 20) {
+                    SampleCarouselItem(modifier = Modifier.testTag("item-$it"), index = it)
                 }
             }
         }
@@ -602,9 +602,9 @@
         rule.onNodeWithTag("pager").performSemanticsAction(SemanticsActions.RequestFocus)
         rule.waitForIdle()
 
-        val slideProgression = listOf(6, 3, -4, 3, -6, 5, 3)
+        val itemProgression = listOf(6, 3, -4, 3, -6, 5, 3)
 
-        slideProgression.forEach {
+        itemProgression.forEach {
             if (it < 0) {
                 performKeyPress(NativeKeyEvent.KEYCODE_DPAD_LEFT, it * -1)
             } else {
@@ -614,20 +614,20 @@
 
         rule.mainClock.advanceTimeBy(animationTime)
 
-        val finalSlide = slideProgression.sum()
-        rule.onNodeWithText("Play $finalSlide").assertIsFocused()
+        val finalItem = itemProgression.sum()
+        rule.onNodeWithText("Play $finalItem").assertIsFocused()
 
         performKeyPress(NativeKeyEvent.KEYCODE_DPAD_RIGHT, 3)
 
         rule.mainClock.advanceTimeBy((animationTime) * 3)
 
-        rule.onNodeWithText("Play ${finalSlide + 3}").assertIsFocused()
+        rule.onNodeWithText("Play ${finalItem + 3}").assertIsFocused()
     }
 
     @Test
     fun carousel_manualScrolling_onDpadLongPress() {
         rule.setContent {
-            SampleCarousel(slideCount = 6) { index ->
+            SampleCarousel(itemCount = 6) { index ->
                 SampleButton("Button ${index + 1}")
             }
         }
@@ -641,31 +641,31 @@
         rule.mainClock.advanceTimeByFrame()
         rule.waitForIdle()
 
-        // Assert that Button 1 from first slide is focused
+        // Assert that Button 1 from first item is focused
         rule.onNodeWithText("Button 1").assertIsFocused()
 
         // Trigger dpad right key long press
         performLongKeyPress(rule, NativeKeyEvent.KEYCODE_DPAD_RIGHT)
 
-        // Advance time and trigger recomposition to switch to next slide
+        // Advance time and trigger recomposition to switch to next item
         rule.mainClock.advanceTimeByFrame()
         rule.waitForIdle()
-        rule.mainClock.advanceTimeBy(delayBetweenSlides, false)
+        rule.mainClock.advanceTimeBy(delayBetweenItems, false)
         rule.waitForIdle()
 
-        // Assert that Button 2 from second slide is focused
+        // Assert that Button 2 from second item is focused
         rule.onNodeWithText("Button 2").assertIsFocused()
 
         // Trigger dpad left key long press
         performLongKeyPress(rule, NativeKeyEvent.KEYCODE_DPAD_LEFT)
 
-        // Advance time and trigger recomposition to switch to previous slide
-        rule.mainClock.advanceTimeBy(delayBetweenSlides, false)
+        // Advance time and trigger recomposition to switch to previous item
+        rule.mainClock.advanceTimeBy(delayBetweenItems, false)
         rule.waitForIdle()
         rule.mainClock.advanceTimeByFrame()
         rule.waitForIdle()
 
-        // Assert that Button 1 from first slide is focused
+        // Assert that Button 1 from first item is focused
         rule.onNodeWithText("Button 1").assertIsFocused()
     }
 
@@ -681,37 +681,37 @@
         rule.onNodeWithTag("pager")
             .performSemanticsAction(SemanticsActions.RequestFocus)
 
-        // current slide overlay render delay
+        // current item overlay render delay
         rule.mainClock.advanceTimeBy(animationTime, false)
         rule.mainClock.advanceTimeBy(animationTime, false)
         rule.mainClock.advanceTimeByFrame()
 
-        // Assert that slide 1 is in view
+        // Assert that item 1 is in view
         rule.onNodeWithText("Button 1").assertIsDisplayed()
 
         // advance time
-        rule.mainClock.advanceTimeBy(delayBetweenSlides + animationTime, false)
+        rule.mainClock.advanceTimeBy(delayBetweenItems + animationTime, false)
         rule.mainClock.advanceTimeByFrame()
 
         // go right once
         performKeyPress(NativeKeyEvent.KEYCODE_DPAD_RIGHT)
 
-        // Wait for slide to load
+        // Wait for item to load
         rule.mainClock.advanceTimeBy(animationTime)
         rule.mainClock.advanceTimeByFrame()
 
-        // Assert that slide 2 is in view
+        // Assert that item 2 is in view
         rule.onNodeWithText("Button 2").assertIsDisplayed()
 
         // go left once
         performKeyPress(NativeKeyEvent.KEYCODE_DPAD_LEFT)
 
-        // Wait for slide to load
-        rule.mainClock.advanceTimeBy(delayBetweenSlides)
+        // Wait for item to load
+        rule.mainClock.advanceTimeBy(delayBetweenItems)
         rule.mainClock.advanceTimeBy(animationTime)
         rule.mainClock.advanceTimeByFrame()
 
-        // Assert that slide 1 is in view
+        // Assert that item 1 is in view
         rule.onNodeWithText("Button 1").assertIsDisplayed()
     }
 
@@ -731,70 +731,70 @@
         rule.onNodeWithTag("pager")
             .performSemanticsAction(SemanticsActions.RequestFocus)
 
-        // current slide overlay render delay
+        // current item overlay render delay
         rule.mainClock.advanceTimeBy(animationTime, false)
         rule.mainClock.advanceTimeBy(animationTime, false)
         rule.mainClock.advanceTimeByFrame()
 
-        // Assert that slide 1 is in view
+        // Assert that item 1 is in view
         rule.onNodeWithText("Button 1").assertIsDisplayed()
 
         // advance time
-        rule.mainClock.advanceTimeBy(delayBetweenSlides + animationTime, false)
+        rule.mainClock.advanceTimeBy(delayBetweenItems + animationTime, false)
         rule.mainClock.advanceTimeByFrame()
 
         // go right once
         performKeyPress(NativeKeyEvent.KEYCODE_DPAD_LEFT)
 
-        // Wait for slide to load
+        // Wait for item to load
         rule.mainClock.advanceTimeBy(animationTime)
         rule.mainClock.advanceTimeByFrame()
 
-        // Assert that slide 2 is in view
+        // Assert that item 2 is in view
         rule.onNodeWithText("Button 2").assertIsDisplayed()
 
         // go left once
         performKeyPress(NativeKeyEvent.KEYCODE_DPAD_RIGHT)
 
-        // Wait for slide to load
-        rule.mainClock.advanceTimeBy(delayBetweenSlides + animationTime, false)
+        // Wait for item to load
+        rule.mainClock.advanceTimeBy(delayBetweenItems + animationTime, false)
         rule.mainClock.advanceTimeByFrame()
 
-        // Assert that slide 1 is in view
+        // Assert that item 1 is in view
         rule.onNodeWithText("Button 1").assertIsDisplayed()
     }
 
     @Test
-    fun carousel_slideCountChangesDuringAnimation_shouldNotCrash() {
-        val slideDisplayDurationMs: Long = 100
-        var slideChanges = 0
-        // number of slides will fall from 4 to 2, but 4 slide transitions should happen without a
+    fun carousel_itemCountChangesDuringAnimation_shouldNotCrash() {
+        val itemDisplayDurationMs: Long = 100
+        var itemChanges = 0
+        // number of items will fall from 4 to 2, but 4 item transitions should happen without a
         // crash
-        val minSuccessfulSlideChanges = 4
+        val minSuccessfulItemChanges = 4
         rule.setContent {
-            var slideCount by remember { mutableStateOf(4) }
+            var itemCount by remember { mutableStateOf(4) }
             LaunchedEffect(Unit) {
-                while (slideCount >= 2) {
-                    delay(slideDisplayDurationMs)
-                    slideCount--
+                while (itemCount >= 2) {
+                    delay(itemDisplayDurationMs)
+                    itemCount--
                 }
             }
             SampleCarousel(
-                slideCount = slideCount,
-                timeToDisplaySlideMillis = slideDisplayDurationMs
+                itemCount = itemCount,
+                timeToDisplayItemMillis = itemDisplayDurationMs
             ) { index ->
-                if (index >= slideCount) {
-                    // slideIndex requested should not be greater than slideCount. User could be
+                if (index >= itemCount) {
+                    // itemIndex requested should not be greater than itemCount. User could be
                     // using a data-structure that could throw an IndexOutOfBoundsException.
-                    // This can happen when the slideCount changes during the transition between
-                    // slides.
-                    throw Exception("Index is larger, index=$index, slideCount=$slideCount")
+                    // This can happen when the itemCount changes during the transition between
+                    // items.
+                    throw Exception("Index is larger, index=$index, itemCount=$itemCount")
                 }
-                slideChanges++
+                itemChanges++
             }
         }
 
-        rule.waitUntil(timeoutMillis = 5000) { slideChanges > minSuccessfulSlideChanges }
+        rule.waitUntil(timeoutMillis = 5000) { itemChanges > minSuccessfulItemChanges }
     }
 }
 
@@ -802,8 +802,8 @@
 @Composable
 private fun SampleCarousel(
     carouselState: CarouselState = remember { CarouselState() },
-    slideCount: Int = 3,
-    timeToDisplaySlideMillis: Long = delayBetweenSlides,
+    itemCount: Int = 3,
+    timeToDisplayItemMillis: Long = delayBetweenItems,
     content: @Composable CarouselScope.(index: Int) -> Unit
 ) {
     Carousel(
@@ -813,16 +813,16 @@
             .height(200.dp)
             .testTag("pager"),
         carouselState = carouselState,
-        slideCount = slideCount,
-        autoScrollDurationMillis = timeToDisplaySlideMillis,
+        itemCount = itemCount,
+        autoScrollDurationMillis = timeToDisplayItemMillis,
         carouselIndicator = {
             CarouselDefaults.IndicatorRow(
                 modifier = Modifier
                     .align(Alignment.BottomEnd)
                     .padding(16.dp)
                     .testTag("indicator"),
-                activeSlideIndex = carouselState.activeSlideIndex,
-                slideCount = slideCount
+                activeItemIndex = carouselState.activeItemIndex,
+                itemCount = itemCount
             )
         },
         content = { content(it) },
@@ -831,16 +831,16 @@
 
 @OptIn(ExperimentalTvMaterial3Api::class, ExperimentalAnimationApi::class)
 @Composable
-private fun CarouselScope.SampleCarouselSlide(
+private fun CarouselScope.SampleCarouselItem(
     index: Int,
     modifier: Modifier = Modifier,
-    contentTransformForward: ContentTransform =
-        CarouselSlideDefaults.contentTransformForward,
+    contentTransformStartToEnd: ContentTransform =
+        CarouselItemDefaults.contentTransformStartToEnd,
     content: (@Composable () -> Unit) = { SampleButton("Play $index") },
 ) {
-    CarouselSlide(
+    CarouselItem(
         modifier = modifier,
-        contentTransformForward = contentTransformForward,
+        contentTransformStartToEnd = contentTransformStartToEnd,
         background = {
             Box(
                 modifier = Modifier
diff --git a/tv/tv-material/src/main/java/androidx/tv/material3/Carousel.kt b/tv/tv-material/src/main/java/androidx/tv/material3/Carousel.kt
index 442c810..8f5661b 100644
--- a/tv/tv-material/src/main/java/androidx/tv/material3/Carousel.kt
+++ b/tv/tv-material/src/main/java/androidx/tv/material3/Carousel.kt
@@ -78,34 +78,33 @@
  * @sample androidx.tv.samples.CarouselIndicatorWithRectangleShape
  *
  * @param modifier Modifier applied to the Carousel.
- * @param slideCount total number of slides present in the carousel.
+ * @param itemCount total number of items present in the carousel.
  * @param carouselState state associated with this carousel.
- * @param autoScrollDurationMillis duration for which slide should be visible before moving to
- * the next slide.
- * @param contentTransformForward animation transform applied when we are moving forward in the
- * carousel while scrolling
- * @param contentTransformBackward animation transform applied when we are moving backward in the
- * carousel while scrolling
- * in the next slide
- * @param carouselIndicator indicator showing the position of the current slide among all slides.
- * @param content defines the slides for a given index.
+ * @param autoScrollDurationMillis duration for which item should be visible before moving to
+ * the next item.
+ * @param contentTransformStartToEnd animation transform applied when we are moving from start to
+ * end in the carousel while scrolling to the next item
+ * @param contentTransformEndToStart animation transform applied when we are moving from end to
+ * start in the carousel while scrolling to the next item
+ * @param carouselIndicator indicator showing the position of the current item among all items.
+ * @param content defines the items for a given index.
  */
 @Suppress("IllegalExperimentalApiUsage")
-@OptIn(ExperimentalComposeUiApi::class, ExperimentalAnimationApi::class)
+@OptIn(ExperimentalComposeUiApi::class)
 @ExperimentalTvMaterial3Api
 @Composable
 fun Carousel(
-    slideCount: Int,
+    itemCount: Int,
     modifier: Modifier = Modifier,
     carouselState: CarouselState = remember { CarouselState() },
-    autoScrollDurationMillis: Long = CarouselDefaults.TimeToDisplaySlideMillis,
-    contentTransformForward: ContentTransform = CarouselDefaults.contentTransform,
-    contentTransformBackward: ContentTransform = CarouselDefaults.contentTransform,
+    autoScrollDurationMillis: Long = CarouselDefaults.TimeToDisplayItemMillis,
+    contentTransformStartToEnd: ContentTransform = CarouselDefaults.contentTransform,
+    contentTransformEndToStart: ContentTransform = CarouselDefaults.contentTransform,
     carouselIndicator:
     @Composable BoxScope.() -> Unit = {
         CarouselDefaults.IndicatorRow(
-            slideCount = slideCount,
-            activeSlideIndex = carouselState.activeSlideIndex,
+            itemCount = itemCount,
+            activeItemIndex = carouselState.activeItemIndex,
             modifier = Modifier
                 .align(Alignment.BottomEnd)
                 .padding(16.dp),
@@ -113,7 +112,7 @@
     },
     content: @Composable CarouselScope.(index: Int) -> Unit
 ) {
-    CarouselStateUpdater(carouselState, slideCount)
+    CarouselStateUpdater(carouselState, itemCount)
     var focusState: FocusState? by remember { mutableStateOf(null) }
     val focusManager = LocalFocusManager.current
     val isLtr = LocalLayoutDirection.current == LayoutDirection.Ltr
@@ -122,7 +121,7 @@
 
     AutoScrollSideEffect(
         autoScrollDurationMillis = autoScrollDurationMillis,
-        slideCount = slideCount,
+        itemCount = itemCount,
         carouselState = carouselState,
         doAutoScroll = shouldPerformAutoScroll(focusState),
         onAutoScrollChange = { isAutoScrollActive = it })
@@ -142,21 +141,21 @@
             carouselState = carouselState,
             outerBoxFocusRequester = carouselOuterBoxFocusRequester,
             focusManager = focusManager,
-            slideCount = slideCount,
+            itemCount = itemCount,
             isLtr = isLtr,
         )
         .focusable()
     ) {
         AnimatedContent(
-            targetState = carouselState.activeSlideIndex,
+            targetState = carouselState.activeItemIndex,
             transitionSpec = {
                 if (carouselState.isMovingBackward) {
-                    contentTransformBackward
+                    contentTransformEndToStart
                 } else {
-                    contentTransformForward
+                    contentTransformStartToEnd
                 }
             }
-        ) { activeSlideIndex ->
+        ) { activeItemIndex ->
             LaunchedEffect(Unit) {
                 this@AnimatedContent.onAnimationCompletion {
                     // Outer box is focused
@@ -166,13 +165,13 @@
                     }
                 }
             }
-            // it is possible for the slideCount to have changed during the transition.
-            // This can cause the slideIndex to be greater than or equal to slideCount and cause
-            // IndexOutOfBoundsException. Guarding against this by checking against slideCount
+            // it is possible for the itemCount to have changed during the transition.
+            // This can cause the itemIndex to be greater than or equal to itemCount and cause
+            // IndexOutOfBoundsException. Guarding against this by checking against itemCount
             // before invoking.
-            if (slideCount > 0) {
+            if (itemCount > 0) {
                 CarouselScope(carouselState = carouselState)
-                    .content(if (activeSlideIndex < slideCount) activeSlideIndex else 0)
+                    .content(if (activeItemIndex < itemCount) activeItemIndex else 0)
             }
         }
         this.carouselIndicator()
@@ -197,13 +196,13 @@
 @Composable
 private fun AutoScrollSideEffect(
     autoScrollDurationMillis: Long,
-    slideCount: Int,
+    itemCount: Int,
     carouselState: CarouselState,
     doAutoScroll: Boolean,
     onAutoScrollChange: (isAutoScrollActive: Boolean) -> Unit = {},
 ) {
-    // Needed to ensure that the code within LaunchedEffect receives updates to the slideCount.
-    val updatedSlideCount by rememberUpdatedState(newValue = slideCount)
+    // Needed to ensure that the code within LaunchedEffect receives updates to the itemCount.
+    val updatedItemCount by rememberUpdatedState(newValue = itemCount)
     if (doAutoScroll) {
         LaunchedEffect(carouselState) {
             while (true) {
@@ -213,7 +212,7 @@
                     snapshotFlow { carouselState.activePauseHandlesCount }
                         .first { pauseHandleCount -> pauseHandleCount == 0 }
                 }
-                carouselState.moveToNextSlide(updatedSlideCount)
+                carouselState.moveToNextItem(updatedItemCount)
             }
         }
     }
@@ -226,7 +225,7 @@
     carouselState: CarouselState,
     outerBoxFocusRequester: FocusRequester,
     focusManager: FocusManager,
-    slideCount: Int,
+    itemCount: Int,
     isLtr: Boolean
 ): Modifier = onKeyEvent {
     // Ignore KeyUp action type
@@ -234,20 +233,20 @@
         return@onKeyEvent KeyEventPropagation.ContinuePropagation
     }
 
-    val showPreviousSlideAndGetKeyEventPropagation = {
-        if (carouselState.isFirstSlide()) {
+    val showPreviousItemAndGetKeyEventPropagation = {
+        if (carouselState.isFirstItem()) {
             KeyEventPropagation.ContinuePropagation
         } else {
-            carouselState.moveToPreviousSlide(slideCount)
+            carouselState.moveToPreviousItem(itemCount)
             outerBoxFocusRequester.requestFocus()
             KeyEventPropagation.StopPropagation
         }
     }
-    val showNextSlideAndGetKeyEventPropagation = {
-        if (carouselState.isLastSlide(slideCount)) {
+    val showNextItemAndGetKeyEventPropagation = {
+        if (carouselState.isLastItem(itemCount)) {
             KeyEventPropagation.ContinuePropagation
         } else {
-            carouselState.moveToNextSlide(slideCount)
+            carouselState.moveToNextItem(itemCount)
             outerBoxFocusRequester.requestFocus()
             KeyEventPropagation.StopPropagation
         }
@@ -266,9 +265,9 @@
             }
 
             if (isLtr) {
-                showPreviousSlideAndGetKeyEventPropagation()
+                showPreviousItemAndGetKeyEventPropagation()
             } else {
-                showNextSlideAndGetKeyEventPropagation()
+                showNextItemAndGetKeyEventPropagation()
             }
         }
 
@@ -279,9 +278,9 @@
             }
 
             if (isLtr) {
-                showNextSlideAndGetKeyEventPropagation()
+                showNextItemAndGetKeyEventPropagation()
             } else {
-                showPreviousSlideAndGetKeyEventPropagation()
+                showPreviousItemAndGetKeyEventPropagation()
             }
         }
 
@@ -291,31 +290,31 @@
 
 @OptIn(ExperimentalTvMaterial3Api::class)
 @Composable
-private fun CarouselStateUpdater(carouselState: CarouselState, slideCount: Int) {
-    LaunchedEffect(carouselState, slideCount) {
-        if (slideCount != 0) {
-            carouselState.activeSlideIndex = floorMod(carouselState.activeSlideIndex, slideCount)
+private fun CarouselStateUpdater(carouselState: CarouselState, itemCount: Int) {
+    LaunchedEffect(carouselState, itemCount) {
+        if (itemCount != 0) {
+            carouselState.activeItemIndex = floorMod(carouselState.activeItemIndex, itemCount)
         }
     }
 }
 
 /**
- * State of the Carousel which allows the user to specify the first slide that is shown when the
+ * State of the Carousel which allows the user to specify the first item that is shown when the
  * Carousel is instantiated in the constructor.
  *
  * It also provides the user with support to pause and resume the auto-scroll behaviour of the
  * Carousel.
- * @param initialActiveSlideIndex the index of the first active slide
+ * @param initialActiveItemIndex the index of the first active item
  */
 @Stable
 @ExperimentalTvMaterial3Api
-class CarouselState(initialActiveSlideIndex: Int = 0) {
+class CarouselState(initialActiveItemIndex: Int = 0) {
     internal var activePauseHandlesCount by mutableStateOf(0)
 
     /**
-     * The index of the slide that is currently displayed by the carousel
+     * The index of the item that is currently displayed by the carousel
      */
-    var activeSlideIndex by mutableStateOf(initialActiveSlideIndex)
+    var activeItemIndex by mutableStateOf(initialActiveItemIndex)
         internal set
 
     /**
@@ -327,38 +326,38 @@
 
     /**
      * Pauses the auto-scrolling behaviour of Carousel.
-     * The pause request is ignored if [slideIndex] is not the current slide that is visible.
+     * The pause request is ignored if [itemIndex] is not the current item that is visible.
      * Returns a [ScrollPauseHandle] that can be used to resume
      */
-    fun pauseAutoScroll(slideIndex: Int): ScrollPauseHandle {
-        if (this.activeSlideIndex != slideIndex) {
+    fun pauseAutoScroll(itemIndex: Int): ScrollPauseHandle {
+        if (this.activeItemIndex != itemIndex) {
             return NoOpScrollPauseHandle
         }
         return ScrollPauseHandleImpl(this)
     }
 
-    internal fun isFirstSlide() = activeSlideIndex == 0
+    internal fun isFirstItem() = activeItemIndex == 0
 
-    internal fun isLastSlide(slideCount: Int) = activeSlideIndex == slideCount - 1
+    internal fun isLastItem(itemCount: Int) = activeItemIndex == itemCount - 1
 
-    internal fun moveToPreviousSlide(slideCount: Int) {
-        // No slides available for carousel
-        if (slideCount == 0) return
+    internal fun moveToPreviousItem(itemCount: Int) {
+        // No items available for carousel
+        if (itemCount == 0) return
 
         isMovingBackward = true
 
-        // Go to previous slide
-        activeSlideIndex = floorMod(activeSlideIndex - 1, slideCount)
+        // Go to previous item
+        activeItemIndex = floorMod(activeItemIndex - 1, itemCount)
     }
 
-    internal fun moveToNextSlide(slideCount: Int) {
-        // No slides available for carousel
-        if (slideCount == 0) return
+    internal fun moveToNextItem(itemCount: Int) {
+        // No items available for carousel
+        if (itemCount == 0) return
 
         isMovingBackward = false
 
-        // Go to next slide
-        activeSlideIndex = floorMod(activeSlideIndex + 1, slideCount)
+        // Go to next item
+        activeItemIndex = floorMod(activeItemIndex + 1, itemCount)
     }
 }
 
@@ -403,34 +402,33 @@
 @ExperimentalTvMaterial3Api
 object CarouselDefaults {
     /**
-     * Default time for which the slide is visible to the user.
+     * Default time for which the item is visible to the user.
      */
-    const val TimeToDisplaySlideMillis: Long = 5000
+    const val TimeToDisplayItemMillis: Long = 5000
 
     /**
      * Transition applied when bringing it into view and removing it from the view
      */
-    @OptIn(ExperimentalAnimationApi::class)
     val contentTransform: ContentTransform
     @Composable get() =
         fadeIn(animationSpec = tween(100))
             .with(fadeOut(animationSpec = tween(100)))
 
     /**
-     * An indicator showing the position of the current active slide among the slides of the
+     * An indicator showing the position of the current active item among the items of the
      * carousel.
      *
-     * @param slideCount total number of slides in the carousel
-     * @param activeSlideIndex the current active slide index
+     * @param itemCount total number of items in the carousel
+     * @param activeItemIndex the current active item index
      * @param modifier Modifier applied to the indicators' container
      * @param spacing spacing between the indicator dots
-     * @param indicator indicator dot representing each slide in the carousel
+     * @param indicator indicator dot representing each item in the carousel
      */
     @ExperimentalTvMaterial3Api
     @Composable
     fun IndicatorRow(
-        slideCount: Int,
-        activeSlideIndex: Int,
+        itemCount: Int,
+        activeItemIndex: Int,
         modifier: Modifier = Modifier,
         spacing: Dp = 8.dp,
         indicator: @Composable (isActive: Boolean) -> Unit = { isActive ->
@@ -451,8 +449,8 @@
             verticalAlignment = Alignment.CenterVertically,
             modifier = modifier,
         ) {
-            repeat(slideCount) {
-                val isActive = it == activeSlideIndex
+            repeat(itemCount) {
+                val isActive = it == activeItemIndex
                 indicator(isActive = isActive)
             }
         }
diff --git a/tv/tv-material/src/main/java/androidx/tv/material3/CarouselSlide.kt b/tv/tv-material/src/main/java/androidx/tv/material3/CarouselItem.kt
similarity index 86%
rename from tv/tv-material/src/main/java/androidx/tv/material3/CarouselSlide.kt
rename to tv/tv-material/src/main/java/androidx/tv/material3/CarouselItem.kt
index 162f167..14650f7 100644
--- a/tv/tv-material/src/main/java/androidx/tv/material3/CarouselSlide.kt
+++ b/tv/tv-material/src/main/java/androidx/tv/material3/CarouselItem.kt
@@ -48,10 +48,10 @@
  * - a [background] layer that is rendered as soon as the composable is visible.
  * - a [content] layer that is rendered on top of the [background]
  *
- * @param background composable defining the background of the slide
- * @param slideIndex current active slide index of the carousel
- * @param modifier modifier applied to the CarouselSlide
- * @param contentTransform content transform to be applied to the content of the slide when
+ * @param background composable defining the background of the item
+ * @param itemIndex current active item index of the carousel
+ * @param modifier modifier applied to the CarouselItem
+ * @param contentTransform content transform to be applied to the content of the item when
  * scrolling
  * @param content composable defining the content displayed on top of the background
  */
@@ -59,12 +59,12 @@
 @OptIn(ExperimentalAnimationApi::class, ExperimentalComposeUiApi::class)
 @ExperimentalTvMaterial3Api
 @Composable
-internal fun CarouselSlide(
-    slideIndex: Int,
+internal fun CarouselItem(
+    itemIndex: Int,
     modifier: Modifier = Modifier,
     background: @Composable () -> Unit = {},
     contentTransform: ContentTransform =
-        CarouselSlideDefaults.contentTransformForward,
+        CarouselItemDefaults.contentTransformStartToEnd,
     content: @Composable () -> Unit,
 ) {
     var containerBoxFocusState: FocusState? by remember { mutableStateOf(null) }
@@ -73,7 +73,7 @@
 
     var isVisible by remember { mutableStateOf(false) }
 
-    DisposableEffect(slideIndex) {
+    DisposableEffect(itemIndex) {
         isVisible = true
         onDispose { isVisible = false }
     }
@@ -112,13 +112,11 @@
 }
 
 @ExperimentalTvMaterial3Api
-object CarouselSlideDefaults {
+object CarouselItemDefaults {
     /**
      * Transform the content from right to left
      */
     // Keeping this as public so that users can access it directly without the isLTR helper
-    @Suppress("IllegalExperimentalApiUsage")
-    @OptIn(ExperimentalAnimationApi::class)
     val contentTransformRightToLeft: ContentTransform
         @Composable get() =
             slideInHorizontally { it * 4 }
@@ -128,8 +126,6 @@
      * Transform the content from left to right
      */
     // Keeping this as public so that users can access it directly without the isLTR helper
-    @Suppress("IllegalExperimentalApiUsage")
-    @OptIn(ExperimentalAnimationApi::class)
     val contentTransformLeftToRight: ContentTransform
         @Composable get() =
             slideInHorizontally()
@@ -138,9 +134,7 @@
     /**
      * Content transform applied when moving forward taking isLTR into account
      */
-    @Suppress("IllegalExperimentalApiUsage")
-    @OptIn(ExperimentalAnimationApi::class)
-    val contentTransformForward
+    val contentTransformStartToEnd
         @Composable get() =
             if (isLtr())
                 contentTransformRightToLeft
@@ -150,9 +144,7 @@
     /**
      * Content transform applied when moving backward taking isLTR into account
      */
-    @Suppress("IllegalExperimentalApiUsage")
-    @OptIn(ExperimentalAnimationApi::class)
-    val contentTransformBackward
+    val contentTransformEndToStart
         @Composable get() =
             if (isLtr())
                 contentTransformLeftToRight
diff --git a/tv/tv-material/src/main/java/androidx/tv/material3/CarouselScope.kt b/tv/tv-material/src/main/java/androidx/tv/material3/CarouselScope.kt
index 838e1e6..665ceb9 100644
--- a/tv/tv-material/src/main/java/androidx/tv/material3/CarouselScope.kt
+++ b/tv/tv-material/src/main/java/androidx/tv/material3/CarouselScope.kt
@@ -17,51 +17,48 @@
 package androidx.tv.material3
 
 import androidx.compose.animation.ContentTransform
-import androidx.compose.animation.ExperimentalAnimationApi
 import androidx.compose.runtime.Composable
 import androidx.compose.ui.Modifier
 
 /**
- * CarouselScope provides a [CarouselScope.CarouselSlide] function which you can use to
- * provide the slide's animation, background and the inner content.
+ * CarouselScope provides a [CarouselScope.CarouselItem] function which you can use to
+ * provide the carousel item's animation, background and the inner content.
  */
 @ExperimentalTvMaterial3Api
 class CarouselScope @OptIn(ExperimentalTvMaterial3Api::class)
 internal constructor(private val carouselState: CarouselState) {
     /**
-     * [CarouselScope.CarouselSlide] can be used to define a slide's animation, background, and
-     * content. Using this is optional and you can choose to define your own CarouselSlide from
+     * [CarouselScope.CarouselItem] can be used to define a item's animation, background, and
+     * content. Using this is optional and you can choose to define your own CarouselItem from
      * scratch
      *
-     * @param modifier modifier applied to the CarouselSlide
-     * @param background composable defining the background of the slide
-     * @param contentTransformForward content transform to be applied to the content of the slide
+     * @param modifier modifier applied to the CarouselItem
+     * @param background composable defining the background of the item
+     * @param contentTransformStartToEnd content transform to be applied to the content of the item
      * when scrolling forward in the carousel
-     * @param contentTransformBackward content transform to be applied to the content of the slide
+     * @param contentTransformEndToStart content transform to be applied to the content of the item
      * when scrolling backward in the carousel
      * @param content composable defining the content displayed on top of the background
      */
     @Composable
-    @Suppress("IllegalExperimentalApiUsage")
-    @OptIn(ExperimentalAnimationApi::class)
     @ExperimentalTvMaterial3Api
-    fun CarouselSlide(
+    fun CarouselItem(
         modifier: Modifier = Modifier,
         background: @Composable () -> Unit = {},
-        contentTransformForward: ContentTransform =
-            CarouselSlideDefaults.contentTransformForward,
-        contentTransformBackward: ContentTransform =
-            CarouselSlideDefaults.contentTransformBackward,
+        contentTransformStartToEnd: ContentTransform =
+            CarouselItemDefaults.contentTransformStartToEnd,
+        contentTransformEndToStart: ContentTransform =
+            CarouselItemDefaults.contentTransformEndToStart,
         content: @Composable () -> Unit
     ) {
-        CarouselSlide(
+        CarouselItem(
             background = background,
-            slideIndex = carouselState.activeSlideIndex,
+            itemIndex = carouselState.activeItemIndex,
             contentTransform =
             if (carouselState.isMovingBackward)
-                contentTransformBackward
+                contentTransformEndToStart
             else
-                contentTransformForward,
+                contentTransformStartToEnd,
             modifier = modifier,
             content = content,
         )
diff --git a/wear/protolayout/protolayout-material/build.gradle b/wear/protolayout/protolayout-material/build.gradle
index fdd52a9..3421707 100644
--- a/wear/protolayout/protolayout-material/build.gradle
+++ b/wear/protolayout/protolayout-material/build.gradle
@@ -18,6 +18,7 @@
 
 plugins {
     id("AndroidXPlugin")
+    id("kotlin-android")
     id("com.android.library")
 }
 
diff --git a/wear/protolayout/protolayout-material/src/test/java/androidx/wear/protolayout/material/ButtonColorsTest.java b/wear/protolayout/protolayout-material/src/test/java/androidx/wear/protolayout/material/ButtonColorsTest.java
new file mode 100644
index 0000000..fcbf8b2
--- /dev/null
+++ b/wear/protolayout/protolayout-material/src/test/java/androidx/wear/protolayout/material/ButtonColorsTest.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.wear.protolayout.material;
+
+import static androidx.wear.protolayout.ColorBuilders.argb;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.wear.protolayout.ColorBuilders.ColorProp;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.annotation.internal.DoNotInstrument;
+
+@RunWith(AndroidJUnit4.class)
+@DoNotInstrument
+public class ButtonColorsTest {
+    private static final int ARGB_BACKGROUND_COLOR = 0x12345678;
+    private static final int ARGB_CONTENT_COLOR = 0x11223344;
+    private static final ColorProp BACKGROUND_COLOR = argb(ARGB_BACKGROUND_COLOR);
+    private static final ColorProp CONTENT_COLOR = argb(ARGB_CONTENT_COLOR);
+    private static final Colors COLORS = new Colors(0x123, 0x234, 0x345, 0x456);
+
+    @Test
+    public void testCreateButtonColorsFromArgb() {
+        ButtonColors buttonColors = new ButtonColors(ARGB_BACKGROUND_COLOR, ARGB_CONTENT_COLOR);
+
+        assertThat(buttonColors.getBackgroundColor().getArgb())
+                .isEqualTo(BACKGROUND_COLOR.getArgb());
+        assertThat(buttonColors.getContentColor().getArgb()).isEqualTo(CONTENT_COLOR.getArgb());
+    }
+
+    @Test
+    public void testCreateButtonColorsFromColorProp() {
+        ButtonColors buttonColors = new ButtonColors(BACKGROUND_COLOR, CONTENT_COLOR);
+
+        assertThat(buttonColors.getBackgroundColor().getArgb())
+                .isEqualTo(BACKGROUND_COLOR.getArgb());
+        assertThat(buttonColors.getContentColor().getArgb()).isEqualTo(CONTENT_COLOR.getArgb());
+    }
+
+    @Test
+    public void testCreateButtonColorsFromHelperPrimary() {
+        ButtonColors buttonColors = ButtonColors.primaryButtonColors(COLORS);
+
+        assertThat(buttonColors.getBackgroundColor().getArgb()).isEqualTo(COLORS.getPrimary());
+        assertThat(buttonColors.getContentColor().getArgb()).isEqualTo(COLORS.getOnPrimary());
+    }
+
+    @Test
+    public void testCreateButtonColorsFromHelperSurface() {
+        ButtonColors buttonColors = ButtonColors.secondaryButtonColors(COLORS);
+
+        assertThat(buttonColors.getBackgroundColor().getArgb()).isEqualTo(COLORS.getSurface());
+        assertThat(buttonColors.getContentColor().getArgb()).isEqualTo(COLORS.getOnSurface());
+    }
+}
diff --git a/wear/protolayout/protolayout-material/src/test/java/androidx/wear/protolayout/material/ButtonTest.java b/wear/protolayout/protolayout-material/src/test/java/androidx/wear/protolayout/material/ButtonTest.java
new file mode 100644
index 0000000..1cdede2
--- /dev/null
+++ b/wear/protolayout/protolayout-material/src/test/java/androidx/wear/protolayout/material/ButtonTest.java
@@ -0,0 +1,355 @@
+/*
+ * 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.wear.protolayout.material;
+
+import static androidx.wear.protolayout.ColorBuilders.argb;
+import static androidx.wear.protolayout.DimensionBuilders.dp;
+import static androidx.wear.protolayout.material.ButtonDefaults.DEFAULT_SIZE;
+import static androidx.wear.protolayout.material.ButtonDefaults.EXTRA_LARGE_SIZE;
+import static androidx.wear.protolayout.material.ButtonDefaults.LARGE_SIZE;
+import static androidx.wear.protolayout.material.ButtonDefaults.PRIMARY_COLORS;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import android.content.Context;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.wear.protolayout.ActionBuilders.LaunchAction;
+import androidx.wear.protolayout.DimensionBuilders.DpProp;
+import androidx.wear.protolayout.LayoutElementBuilders.Box;
+import androidx.wear.protolayout.LayoutElementBuilders.Column;
+import androidx.wear.protolayout.LayoutElementBuilders.LayoutElement;
+import androidx.wear.protolayout.ModifiersBuilders.Clickable;
+import androidx.wear.protolayout.ModifiersBuilders.ElementMetadata;
+import androidx.wear.protolayout.ModifiersBuilders.Modifiers;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.annotation.internal.DoNotInstrument;
+
+@RunWith(AndroidJUnit4.class)
+@DoNotInstrument
+public class ButtonTest {
+    private static final String RESOURCE_ID = "icon";
+    private static final String TEXT = "ABC";
+    private static final String CONTENT_DESCRIPTION = "clickable button";
+    private static final Clickable CLICKABLE =
+            new Clickable.Builder()
+                    .setOnClick(new LaunchAction.Builder().build())
+                    .setId("action_id")
+                    .build();
+    private static final Context CONTEXT = ApplicationProvider.getApplicationContext();
+    private static final LayoutElement CONTENT =
+            new Text.Builder(CONTEXT, "ABC").setColor(argb(0)).build();
+
+    @Test
+    public void testButtonCustomAddedContentNoContentDesc() {
+        Button button = new Button.Builder(CONTEXT, CLICKABLE).setCustomContent(CONTENT).build();
+
+        assertButton(
+                button,
+                DEFAULT_SIZE,
+                new ButtonColors(Colors.PRIMARY, 0),
+                null,
+                Button.METADATA_TAG_CUSTOM_CONTENT,
+                null,
+                null,
+                null,
+                CONTENT);
+    }
+
+    @Test
+    public void testButtonCustom() {
+        DpProp mSize = LARGE_SIZE;
+        ButtonColors mButtonColors = new ButtonColors(0x11223344, 0);
+
+        Button button =
+                new Button.Builder(CONTEXT, CLICKABLE)
+                        .setCustomContent(CONTENT)
+                        .setSize(mSize)
+                        .setButtonColors(mButtonColors)
+                        .setContentDescription(CONTENT_DESCRIPTION)
+                        .build();
+
+        assertButton(
+                button,
+                mSize,
+                mButtonColors,
+                CONTENT_DESCRIPTION,
+                Button.METADATA_TAG_CUSTOM_CONTENT,
+                null,
+                null,
+                null,
+                CONTENT);
+    }
+
+    @Test
+    public void testButtonSetIcon() {
+
+        Button button =
+                new Button.Builder(CONTEXT, CLICKABLE)
+                        .setIconContent(RESOURCE_ID)
+                        .setContentDescription(CONTENT_DESCRIPTION)
+                        .build();
+
+        assertButton(
+                button,
+                DEFAULT_SIZE,
+                PRIMARY_COLORS,
+                CONTENT_DESCRIPTION,
+                Button.METADATA_TAG_ICON,
+                null,
+                RESOURCE_ID,
+                null,
+                null);
+    }
+
+    @Test
+    public void testButtonSetIconSetSize() {
+        Button button =
+                new Button.Builder(CONTEXT, CLICKABLE)
+                        .setIconContent(RESOURCE_ID)
+                        .setSize(LARGE_SIZE)
+                        .setContentDescription(CONTENT_DESCRIPTION)
+                        .build();
+
+        assertButton(
+                button,
+                LARGE_SIZE,
+                PRIMARY_COLORS,
+                CONTENT_DESCRIPTION,
+                Button.METADATA_TAG_ICON,
+                null,
+                RESOURCE_ID,
+                null,
+                null);
+    }
+
+    @Test
+    public void testButtonSetIconCustomSize() {
+        DpProp mSize = dp(36);
+
+        Button button =
+                new Button.Builder(CONTEXT, CLICKABLE)
+                        .setIconContent(RESOURCE_ID, mSize)
+                        .setContentDescription(CONTENT_DESCRIPTION)
+                        .build();
+
+        assertButton(
+                button,
+                DEFAULT_SIZE,
+                PRIMARY_COLORS,
+                CONTENT_DESCRIPTION,
+                Button.METADATA_TAG_ICON,
+                null,
+                RESOURCE_ID,
+                null,
+                null);
+    }
+
+    @Test
+    public void testButtonSetText() {
+        Button button =
+                new Button.Builder(CONTEXT, CLICKABLE)
+                        .setTextContent(TEXT)
+                        .setContentDescription(CONTENT_DESCRIPTION)
+                        .build();
+
+        assertButton(
+                button,
+                DEFAULT_SIZE,
+                PRIMARY_COLORS,
+                CONTENT_DESCRIPTION,
+                Button.METADATA_TAG_TEXT,
+                TEXT,
+                null,
+                null,
+                null);
+    }
+
+    @Test
+    public void testButtonSetTextSetSize() {
+        Button button =
+                new Button.Builder(CONTEXT, CLICKABLE)
+                        .setTextContent(TEXT)
+                        .setContentDescription(CONTENT_DESCRIPTION)
+                        .setSize(EXTRA_LARGE_SIZE)
+                        .build();
+
+        assertButton(
+                button,
+                EXTRA_LARGE_SIZE,
+                PRIMARY_COLORS,
+                CONTENT_DESCRIPTION,
+                Button.METADATA_TAG_TEXT,
+                TEXT,
+                null,
+                null,
+                null);
+    }
+
+    @Test
+    public void testWrongElementForButton() {
+        Column box = new Column.Builder().build();
+
+        assertThat(Button.fromLayoutElement(box)).isNull();
+    }
+
+    @Test
+    public void testWrongBoxForButton() {
+        Box box = new Box.Builder().build();
+
+        assertThat(Button.fromLayoutElement(box)).isNull();
+    }
+
+    @Test
+    public void testWrongTagForButton() {
+        Box box =
+                new Box.Builder()
+                        .setModifiers(
+                                new Modifiers.Builder()
+                                        .setMetadata(
+                                                new ElementMetadata.Builder()
+                                                        .setTagData("test".getBytes(UTF_8))
+                                                        .build())
+                                        .build())
+                        .build();
+
+        assertThat(Button.fromLayoutElement(box)).isNull();
+    }
+
+    private void assertButton(
+            @NonNull Button actualButton,
+            @NonNull DpProp expectedSize,
+            @NonNull ButtonColors expectedButtonColors,
+            @Nullable String expectedContentDescription,
+            @NonNull String expectedMetadataTag,
+            @Nullable String expectedTextContent,
+            @Nullable String expectedIconContent,
+            @Nullable String expectedImageContent,
+            @Nullable LayoutElement expectedCustomContent) {
+        assertButtonIsEqual(
+                actualButton,
+                expectedSize,
+                expectedButtonColors,
+                expectedContentDescription,
+                expectedMetadataTag,
+                expectedTextContent,
+                expectedIconContent,
+                expectedImageContent,
+                expectedCustomContent);
+
+        assertFromLayoutElementButtonIsEqual(
+                actualButton,
+                expectedSize,
+                expectedButtonColors,
+                expectedContentDescription,
+                expectedMetadataTag,
+                expectedTextContent,
+                expectedIconContent,
+                expectedImageContent,
+                expectedCustomContent);
+
+        assertThat(Button.fromLayoutElement(actualButton)).isEqualTo(actualButton);
+    }
+
+    private void assertButtonIsEqual(
+            @NonNull Button actualButton,
+            @NonNull DpProp expectedSize,
+            @NonNull ButtonColors expectedButtonColors,
+            @Nullable String expectedContentDescription,
+            @NonNull String expectedMetadataTag,
+            @Nullable String expectedTextContent,
+            @Nullable String expectedIconContent,
+            @Nullable String expectedImageContent,
+            @Nullable LayoutElement expectedCustomContent) {
+        // Mandatory
+        assertThat(actualButton.getMetadataTag()).isEqualTo(expectedMetadataTag);
+        assertThat(actualButton.getClickable().toProto()).isEqualTo(CLICKABLE.toProto());
+        assertThat(actualButton.getSize().toContainerDimensionProto())
+                .isEqualTo(expectedSize.toContainerDimensionProto());
+        assertThat(actualButton.getButtonColors().getBackgroundColor().getArgb())
+                .isEqualTo(expectedButtonColors.getBackgroundColor().getArgb());
+        assertThat(actualButton.getButtonColors().getContentColor().getArgb())
+                .isEqualTo(expectedButtonColors.getContentColor().getArgb());
+
+        // Nullable
+        if (expectedContentDescription == null) {
+            assertThat(actualButton.getContentDescription()).isNull();
+        } else {
+            assertThat(actualButton.getContentDescription().toString())
+                    .isEqualTo(expectedContentDescription);
+        }
+
+        if (expectedTextContent == null) {
+            assertThat(actualButton.getTextContent()).isNull();
+        } else {
+            assertThat(actualButton.getTextContent()).isEqualTo(expectedTextContent);
+        }
+
+        if (expectedIconContent == null) {
+            assertThat(actualButton.getIconContent()).isNull();
+        } else {
+            assertThat(actualButton.getIconContent()).isEqualTo(expectedIconContent);
+        }
+
+        if (expectedImageContent == null) {
+            assertThat(actualButton.getImageContent()).isNull();
+        } else {
+            assertThat(actualButton.getImageContent()).isEqualTo(expectedImageContent);
+        }
+
+        if (expectedCustomContent == null) {
+            assertThat(actualButton.getCustomContent()).isNull();
+        } else {
+            assertThat(actualButton.getCustomContent().toLayoutElementProto())
+                    .isEqualTo(expectedCustomContent.toLayoutElementProto());
+        }
+    }
+
+    private void assertFromLayoutElementButtonIsEqual(
+            @NonNull Button button,
+            @NonNull DpProp expectedSize,
+            @NonNull ButtonColors expectedButtonColors,
+            @Nullable String expectedContentDescription,
+            @NonNull String expectedMetadataTag,
+            @Nullable String expectedTextContent,
+            @Nullable String expectedIconContent,
+            @Nullable String expectedImageContent,
+            @Nullable LayoutElement expectedCustomContent) {
+        Box box = new Box.Builder().addContent(button).build();
+
+        Button newButton = Button.fromLayoutElement(box.getContents().get(0));
+
+        assertThat(newButton).isNotNull();
+        assertButtonIsEqual(
+                newButton,
+                expectedSize,
+                expectedButtonColors,
+                expectedContentDescription,
+                expectedMetadataTag,
+                expectedTextContent,
+                expectedIconContent,
+                expectedImageContent,
+                expectedCustomContent);
+    }
+}
diff --git a/wear/protolayout/protolayout-material/src/test/java/androidx/wear/protolayout/material/ChipColorsTest.java b/wear/protolayout/protolayout-material/src/test/java/androidx/wear/protolayout/material/ChipColorsTest.java
new file mode 100644
index 0000000..5bf4319
--- /dev/null
+++ b/wear/protolayout/protolayout-material/src/test/java/androidx/wear/protolayout/material/ChipColorsTest.java
@@ -0,0 +1,115 @@
+/*
+ * 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.wear.protolayout.material;
+
+import static androidx.wear.protolayout.ColorBuilders.argb;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.wear.protolayout.ColorBuilders.ColorProp;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.annotation.internal.DoNotInstrument;
+
+@RunWith(AndroidJUnit4.class)
+@DoNotInstrument
+public class ChipColorsTest {
+    private static final int ARGB_BACKGROUND_COLOR = 0x12345678;
+    private static final int ARGB_CONTENT_COLOR = 0x11223344;
+    private static final int ARGB_SECONDARY_CONTENT_COLOR = 0x11223355;
+    private static final int ARGB_ICON_COLOR = 0x11223366;
+    private static final ColorProp BACKGROUND_COLOR = argb(ARGB_BACKGROUND_COLOR);
+    private static final ColorProp CONTENT_COLOR = argb(ARGB_CONTENT_COLOR);
+    private static final ColorProp ICON_COLOR = argb(ARGB_ICON_COLOR);
+    private static final ColorProp SECONDARY_CONTENT_COLOR = argb(ARGB_SECONDARY_CONTENT_COLOR);
+    private static final Colors COLORS = new Colors(0x123, 0x234, 0x345, 0x456);
+
+    @Test
+    public void testCreateChipColorsFromArgb() {
+        ChipColors chipColors = new ChipColors(ARGB_BACKGROUND_COLOR, ARGB_CONTENT_COLOR);
+
+        assertThat(chipColors.getBackgroundColor().getArgb()).isEqualTo(BACKGROUND_COLOR.getArgb());
+        assertThat(chipColors.getIconColor().getArgb()).isEqualTo(CONTENT_COLOR.getArgb());
+        assertThat(chipColors.getContentColor().getArgb()).isEqualTo(CONTENT_COLOR.getArgb());
+        assertThat(chipColors.getSecondaryContentColor().getArgb())
+                .isEqualTo(CONTENT_COLOR.getArgb());
+    }
+
+    @Test
+    public void testCreateChipColorsFromColorProp() {
+        ChipColors chipColors = new ChipColors(BACKGROUND_COLOR, CONTENT_COLOR);
+
+        assertThat(chipColors.getBackgroundColor().getArgb()).isEqualTo(BACKGROUND_COLOR.getArgb());
+        assertThat(chipColors.getIconColor().getArgb()).isEqualTo(CONTENT_COLOR.getArgb());
+        assertThat(chipColors.getContentColor().getArgb()).isEqualTo(CONTENT_COLOR.getArgb());
+        assertThat(chipColors.getSecondaryContentColor().getArgb())
+                .isEqualTo(CONTENT_COLOR.getArgb());
+    }
+
+    @Test
+    public void testCreateChipColorsFullFromArgb() {
+        ChipColors chipColors =
+                new ChipColors(
+                        ARGB_BACKGROUND_COLOR,
+                        ARGB_ICON_COLOR,
+                        ARGB_CONTENT_COLOR,
+                        ARGB_SECONDARY_CONTENT_COLOR);
+
+        assertThat(chipColors.getBackgroundColor().getArgb()).isEqualTo(BACKGROUND_COLOR.getArgb());
+        assertThat(chipColors.getIconColor().getArgb()).isEqualTo(ICON_COLOR.getArgb());
+        assertThat(chipColors.getContentColor().getArgb()).isEqualTo(CONTENT_COLOR.getArgb());
+        assertThat(chipColors.getSecondaryContentColor().getArgb())
+                .isEqualTo(SECONDARY_CONTENT_COLOR.getArgb());
+    }
+
+    @Test
+    public void testCreateChipColorsFullFromColorProp() {
+        ChipColors chipColors =
+                new ChipColors(
+                        BACKGROUND_COLOR, ICON_COLOR, CONTENT_COLOR, SECONDARY_CONTENT_COLOR);
+
+        assertThat(chipColors.getBackgroundColor().getArgb()).isEqualTo(BACKGROUND_COLOR.getArgb());
+        assertThat(chipColors.getIconColor().getArgb()).isEqualTo(ICON_COLOR.getArgb());
+        assertThat(chipColors.getContentColor().getArgb()).isEqualTo(CONTENT_COLOR.getArgb());
+        assertThat(chipColors.getSecondaryContentColor().getArgb())
+                .isEqualTo(SECONDARY_CONTENT_COLOR.getArgb());
+    }
+
+    @Test
+    public void testCreateChipColorsFromHelperPrimary() {
+        ChipColors chipColors = ChipColors.primaryChipColors(COLORS);
+
+        assertThat(chipColors.getBackgroundColor().getArgb()).isEqualTo(COLORS.getPrimary());
+        assertThat(chipColors.getIconColor().getArgb()).isEqualTo(COLORS.getOnPrimary());
+        assertThat(chipColors.getContentColor().getArgb()).isEqualTo(COLORS.getOnPrimary());
+        assertThat(chipColors.getSecondaryContentColor().getArgb())
+                .isEqualTo(COLORS.getOnPrimary());
+    }
+
+    @Test
+    public void testCreateChipColorsFromHelperSurface() {
+        ChipColors chipColors = ChipColors.secondaryChipColors(COLORS);
+
+        assertThat(chipColors.getBackgroundColor().getArgb()).isEqualTo(COLORS.getSurface());
+        assertThat(chipColors.getIconColor().getArgb()).isEqualTo(COLORS.getOnSurface());
+        assertThat(chipColors.getContentColor().getArgb()).isEqualTo(COLORS.getOnSurface());
+        assertThat(chipColors.getSecondaryContentColor().getArgb())
+                .isEqualTo(COLORS.getOnSurface());
+    }
+}
diff --git a/wear/protolayout/protolayout-material/src/test/java/androidx/wear/protolayout/material/ChipTest.java b/wear/protolayout/protolayout-material/src/test/java/androidx/wear/protolayout/material/ChipTest.java
new file mode 100644
index 0000000..316b904
--- /dev/null
+++ b/wear/protolayout/protolayout-material/src/test/java/androidx/wear/protolayout/material/ChipTest.java
@@ -0,0 +1,319 @@
+/*
+ * 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.wear.protolayout.material;
+
+import static androidx.wear.protolayout.ColorBuilders.argb;
+import static androidx.wear.protolayout.DimensionBuilders.dp;
+import static androidx.wear.protolayout.LayoutElementBuilders.HORIZONTAL_ALIGN_CENTER;
+import static androidx.wear.protolayout.LayoutElementBuilders.HORIZONTAL_ALIGN_START;
+import static androidx.wear.protolayout.material.Utils.areChipColorsEqual;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import android.content.Context;
+import android.graphics.Color;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.wear.protolayout.ActionBuilders.LaunchAction;
+import androidx.wear.protolayout.ColorBuilders.ColorProp;
+import androidx.wear.protolayout.DeviceParametersBuilders.DeviceParameters;
+import androidx.wear.protolayout.DimensionBuilders.DpProp;
+import androidx.wear.protolayout.LayoutElementBuilders.Box;
+import androidx.wear.protolayout.LayoutElementBuilders.Column;
+import androidx.wear.protolayout.LayoutElementBuilders.HorizontalAlignment;
+import androidx.wear.protolayout.LayoutElementBuilders.LayoutElement;
+import androidx.wear.protolayout.LayoutElementBuilders.Row;
+import androidx.wear.protolayout.ModifiersBuilders.Clickable;
+import androidx.wear.protolayout.ModifiersBuilders.ElementMetadata;
+import androidx.wear.protolayout.ModifiersBuilders.Modifiers;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.annotation.internal.DoNotInstrument;
+
+@RunWith(AndroidJUnit4.class)
+@DoNotInstrument
+public class ChipTest {
+    private static final String MAIN_TEXT = "Primary text";
+    private static final Clickable CLICKABLE =
+            new Clickable.Builder()
+                    .setOnClick(new LaunchAction.Builder().build())
+                    .setId("action_id")
+                    .build();
+    private static final DeviceParameters DEVICE_PARAMETERS =
+            new DeviceParameters.Builder().setScreenWidthDp(192).setScreenHeightDp(192).build();
+    private static final Context CONTEXT = ApplicationProvider.getApplicationContext();
+    private static final DpProp EXPECTED_WIDTH =
+            dp(
+                    DEVICE_PARAMETERS.getScreenWidthDp()
+                            * (100 - 2 * ChipDefaults.DEFAULT_MARGIN_PERCENT)
+                            / 100);
+
+    @Test
+    public void testChip() {
+        String contentDescription = "Chip";
+        Chip chip =
+                new Chip.Builder(CONTEXT, CLICKABLE, DEVICE_PARAMETERS)
+                        .setPrimaryLabelContent(MAIN_TEXT)
+                        .setHorizontalAlignment(HORIZONTAL_ALIGN_CENTER)
+                        .setContentDescription(contentDescription)
+                        .build();
+        assertChip(
+                chip,
+                HORIZONTAL_ALIGN_CENTER,
+                ChipDefaults.PRIMARY_COLORS,
+                contentDescription,
+                Chip.METADATA_TAG_TEXT,
+                MAIN_TEXT,
+                null,
+                null,
+                null);
+    }
+
+    @Test
+    public void testFullChipColors() {
+        ChipColors colors = new ChipColors(Color.YELLOW, Color.WHITE, Color.BLUE, Color.MAGENTA);
+        String secondaryLabel = "Label";
+        Chip chip =
+                new Chip.Builder(CONTEXT, CLICKABLE, DEVICE_PARAMETERS)
+                        .setChipColors(colors)
+                        .setPrimaryLabelContent(MAIN_TEXT)
+                        .setSecondaryLabelContent(secondaryLabel)
+                        .setIconContent("ICON_ID")
+                        .build();
+        assertChip(
+                chip,
+                HORIZONTAL_ALIGN_START,
+                colors,
+                MAIN_TEXT + "\n" + secondaryLabel,
+                Chip.METADATA_TAG_ICON,
+                MAIN_TEXT,
+                secondaryLabel,
+                "ICON_ID",
+                null);
+    }
+
+    @Test
+    public void testChipLeftAligned() {
+        Chip chip =
+                new Chip.Builder(CONTEXT, CLICKABLE, DEVICE_PARAMETERS)
+                        .setHorizontalAlignment(HORIZONTAL_ALIGN_START)
+                        .setPrimaryLabelContent(MAIN_TEXT)
+                        .build();
+        assertChip(
+                chip,
+                HORIZONTAL_ALIGN_START,
+                ChipDefaults.PRIMARY_COLORS,
+                MAIN_TEXT,
+                Chip.METADATA_TAG_TEXT,
+                MAIN_TEXT,
+                null,
+                null,
+                null);
+    }
+
+    @Test
+    public void testChipCustomContent() {
+        ColorProp yellow = argb(Color.YELLOW);
+        ColorProp blue = argb(Color.BLUE);
+        LayoutElement content =
+                new Row.Builder()
+                        .addContent(
+                                new Text.Builder(CONTEXT, "text1")
+                                        .setTypography(Typography.TYPOGRAPHY_TITLE3)
+                                        .setColor(yellow)
+                                        .setItalic(true)
+                                        .build())
+                        .addContent(
+                                new Text.Builder(CONTEXT, "text2")
+                                        .setTypography(Typography.TYPOGRAPHY_TITLE2)
+                                        .setColor(blue)
+                                        .build())
+                        .build();
+
+        String contentDescription = "Custom chip";
+        Chip chip =
+                new Chip.Builder(CONTEXT, CLICKABLE, DEVICE_PARAMETERS)
+                        .setCustomContent(content)
+                        .setHorizontalAlignment(HORIZONTAL_ALIGN_START)
+                        .setContentDescription(contentDescription)
+                        .build();
+
+        assertChip(
+                chip,
+                HORIZONTAL_ALIGN_START,
+                new ChipColors(
+                        ChipDefaults.PRIMARY_COLORS.getBackgroundColor(),
+                        new ColorProp.Builder(0).build()),
+                contentDescription,
+                Chip.METADATA_TAG_CUSTOM_CONTENT,
+                null,
+                null,
+                null,
+                content);
+        assertThat(chip.getCustomContent().toLayoutElementProto())
+                .isEqualTo(content.toLayoutElementProto());
+    }
+
+    private void assertChip(
+            @NonNull Chip actualChip,
+            @HorizontalAlignment int hAlign,
+            @NonNull ChipColors colors,
+            @Nullable String expectedContDesc,
+            @NonNull String expectedMetadata,
+            @Nullable String expectedPrimaryText,
+            @Nullable String expectedLabel,
+            @Nullable String expectedIcon,
+            @Nullable LayoutElement expectedCustomContent) {
+        assertChipIsEqual(
+                actualChip,
+                hAlign,
+                colors,
+                expectedContDesc,
+                expectedMetadata,
+                expectedPrimaryText,
+                expectedLabel,
+                expectedIcon,
+                expectedCustomContent);
+
+        assertFromLayoutElementChipIsEqual(
+                actualChip,
+                hAlign,
+                colors,
+                expectedContDesc,
+                expectedMetadata,
+                expectedPrimaryText,
+                expectedLabel,
+                expectedIcon,
+                expectedCustomContent);
+
+        assertThat(Chip.fromLayoutElement(actualChip)).isEqualTo(actualChip);
+    }
+
+    @Test
+    public void testWrongElement() {
+        Column box = new Column.Builder().build();
+
+        assertThat(Chip.fromLayoutElement(box)).isNull();
+    }
+
+    @Test
+    public void testWrongBox() {
+        Box box = new Box.Builder().build();
+
+        assertThat(Chip.fromLayoutElement(box)).isNull();
+    }
+
+    @Test
+    public void testWrongTag() {
+        Box box =
+                new Box.Builder()
+                        .setModifiers(
+                                new Modifiers.Builder()
+                                        .setMetadata(
+                                                new ElementMetadata.Builder()
+                                                        .setTagData("test".getBytes(UTF_8))
+                                                        .build())
+                                        .build())
+                        .build();
+
+        assertThat(Chip.fromLayoutElement(box)).isNull();
+    }
+
+    private void assertFromLayoutElementChipIsEqual(
+            @NonNull Chip chip,
+            @HorizontalAlignment int hAlign,
+            @NonNull ChipColors colors,
+            @Nullable String expectedContDesc,
+            @NonNull String expectedMetadata,
+            @Nullable String expectedPrimaryText,
+            @Nullable String expectedLabel,
+            @Nullable String expectedIcon,
+            @Nullable LayoutElement expectedCustomContent) {
+        Box box = new Box.Builder().addContent(chip).build();
+
+        Chip newChip = Chip.fromLayoutElement(box.getContents().get(0));
+
+        assertThat(newChip).isNotNull();
+        assertChipIsEqual(
+                newChip,
+                hAlign,
+                colors,
+                expectedContDesc,
+                expectedMetadata,
+                expectedPrimaryText,
+                expectedLabel,
+                expectedIcon,
+                expectedCustomContent);
+    }
+
+    private void assertChipIsEqual(
+            @NonNull Chip actualChip,
+            @HorizontalAlignment int hAlign,
+            @NonNull ChipColors colors,
+            @Nullable String expectedContDesc,
+            @NonNull String expectedMetadata,
+            @Nullable String expectedPrimaryText,
+            @Nullable String expectedLabel,
+            @Nullable String expectedIcon,
+            @Nullable LayoutElement expectedCustomContent) {
+        assertThat(actualChip.getMetadataTag()).isEqualTo(expectedMetadata);
+        assertThat(actualChip.getClickable().toProto()).isEqualTo(CLICKABLE.toProto());
+        assertThat(actualChip.getWidth().toContainerDimensionProto())
+                .isEqualTo(EXPECTED_WIDTH.toContainerDimensionProto());
+        assertThat(actualChip.getHeight().toContainerDimensionProto())
+                .isEqualTo(ChipDefaults.DEFAULT_HEIGHT.toContainerDimensionProto());
+        assertThat(areChipColorsEqual(actualChip.getChipColors(), colors)).isTrue();
+        assertThat(actualChip.getHorizontalAlignment()).isEqualTo(hAlign);
+
+        if (expectedContDesc == null) {
+            assertThat(actualChip.getContentDescription()).isNull();
+        } else {
+            assertThat(actualChip.getContentDescription().toString()).isEqualTo(expectedContDesc);
+        }
+
+        if (expectedPrimaryText == null) {
+            assertThat(actualChip.getPrimaryLabelContent()).isNull();
+        } else {
+            assertThat(actualChip.getPrimaryLabelContent()).isEqualTo(expectedPrimaryText);
+        }
+
+        if (expectedLabel == null) {
+            assertThat(actualChip.getSecondaryLabelContent()).isNull();
+        } else {
+            assertThat(actualChip.getSecondaryLabelContent()).isEqualTo(expectedLabel);
+        }
+
+        if (expectedIcon == null) {
+            assertThat(actualChip.getIconContent()).isNull();
+        } else {
+            assertThat(actualChip.getIconContent()).isEqualTo(expectedIcon);
+        }
+
+        if (expectedCustomContent == null) {
+            assertThat(actualChip.getCustomContent()).isNull();
+        } else {
+            assertThat(actualChip.getCustomContent().toLayoutElementProto())
+                    .isEqualTo(expectedCustomContent.toLayoutElementProto());
+        }
+    }
+}
diff --git a/wear/protolayout/protolayout-material/src/test/java/androidx/wear/protolayout/material/CircularProgressIndicatorTest.java b/wear/protolayout/protolayout-material/src/test/java/androidx/wear/protolayout/material/CircularProgressIndicatorTest.java
new file mode 100644
index 0000000..f956e06
--- /dev/null
+++ b/wear/protolayout/protolayout-material/src/test/java/androidx/wear/protolayout/material/CircularProgressIndicatorTest.java
@@ -0,0 +1,200 @@
+/*
+ * 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.wear.protolayout.material;
+
+import static androidx.wear.protolayout.material.ProgressIndicatorDefaults.DEFAULT_COLORS;
+import static androidx.wear.protolayout.material.ProgressIndicatorDefaults.DEFAULT_END_ANGLE;
+import static androidx.wear.protolayout.material.ProgressIndicatorDefaults.DEFAULT_START_ANGLE;
+import static androidx.wear.protolayout.material.ProgressIndicatorDefaults.DEFAULT_STROKE_WIDTH;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import android.graphics.Color;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.wear.protolayout.LayoutElementBuilders.Box;
+import androidx.wear.protolayout.LayoutElementBuilders.Column;
+import androidx.wear.protolayout.ModifiersBuilders.ElementMetadata;
+import androidx.wear.protolayout.ModifiersBuilders.Modifiers;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.annotation.internal.DoNotInstrument;
+
+@RunWith(AndroidJUnit4.class)
+@DoNotInstrument
+public class CircularProgressIndicatorTest {
+    @Test
+    public void testOpenRingIndicatorDefault() {
+        CircularProgressIndicator circularProgressIndicator =
+                new CircularProgressIndicator.Builder().build();
+
+        assertProgressIndicator(
+                circularProgressIndicator,
+                0,
+                DEFAULT_START_ANGLE,
+                DEFAULT_END_ANGLE,
+                DEFAULT_COLORS,
+                DEFAULT_STROKE_WIDTH.getValue(),
+                null);
+    }
+
+    @Test
+    public void testProgressIndicatorCustom() {
+        float progress = 0.25f;
+        String contentDescription = "60 degrees progress";
+        ProgressIndicatorColors colors = new ProgressIndicatorColors(Color.YELLOW, Color.BLACK);
+        int thickness = 16;
+        float startAngle = -24;
+        float endAngle = 24;
+
+        CircularProgressIndicator circularProgressIndicator =
+                new CircularProgressIndicator.Builder()
+                        .setProgress(progress)
+                        .setStartAngle(startAngle)
+                        .setEndAngle(endAngle)
+                        .setCircularProgressIndicatorColors(colors)
+                        .setStrokeWidth(thickness)
+                        .setContentDescription(contentDescription)
+                        .build();
+
+        assertProgressIndicator(
+                circularProgressIndicator,
+                progress,
+                startAngle,
+                endAngle,
+                colors,
+                thickness,
+                contentDescription);
+    }
+
+    @Test
+    public void testWrongElement() {
+        Column box = new Column.Builder().build();
+
+        assertThat(CircularProgressIndicator.fromLayoutElement(box)).isNull();
+    }
+
+    @Test
+    public void testWrongBox() {
+        Box box = new Box.Builder().build();
+
+        assertThat(CircularProgressIndicator.fromLayoutElement(box)).isNull();
+    }
+
+    @Test
+    public void testWrongTag() {
+        Box box =
+                new Box.Builder()
+                        .setModifiers(
+                                new Modifiers.Builder()
+                                        .setMetadata(
+                                                new ElementMetadata.Builder()
+                                                        .setTagData("test".getBytes(UTF_8))
+                                                        .build())
+                                        .build())
+                        .build();
+
+        assertThat(CircularProgressIndicator.fromLayoutElement(box)).isNull();
+    }
+
+    private void assertProgressIndicator(
+            @NonNull CircularProgressIndicator actualCircularProgressIndicator,
+            float expectedProgress,
+            float expectedStartAngle,
+            float expectedEndAngle,
+            @NonNull ProgressIndicatorColors expectedColors,
+            float expectedThickness,
+            @Nullable String expectedContentDescription) {
+        assertProgressIndicatorIsEqual(
+                actualCircularProgressIndicator,
+                expectedProgress,
+                expectedStartAngle,
+                expectedEndAngle,
+                expectedColors,
+                expectedThickness,
+                expectedContentDescription);
+
+        Box box = new Box.Builder().addContent(actualCircularProgressIndicator).build();
+
+        CircularProgressIndicator newCpi =
+                CircularProgressIndicator.fromLayoutElement(box.getContents().get(0));
+
+        assertThat(newCpi).isNotNull();
+        assertProgressIndicatorIsEqual(
+                actualCircularProgressIndicator,
+                expectedProgress,
+                expectedStartAngle,
+                expectedEndAngle,
+                expectedColors,
+                expectedThickness,
+                expectedContentDescription);
+
+        assertThat(CircularProgressIndicator.fromLayoutElement(actualCircularProgressIndicator))
+                .isEqualTo(actualCircularProgressIndicator);
+    }
+
+    private void assertProgressIndicatorIsEqual(
+            @NonNull CircularProgressIndicator actualCircularProgressIndicator,
+            float expectedProgress,
+            float expectedStartAngle,
+            float expectedEndAngle,
+            @NonNull ProgressIndicatorColors expectedColors,
+            float expectedThickness,
+            @Nullable String expectedContentDescription) {
+        float total =
+                expectedEndAngle
+                        + (expectedEndAngle <= expectedStartAngle ? 360 : 0)
+                        - expectedStartAngle;
+        assertThat(actualCircularProgressIndicator.getMetadataTag())
+                .isEqualTo(CircularProgressIndicator.METADATA_TAG);
+        assertThat(actualCircularProgressIndicator.getProgress().getValue())
+                .isWithin(0.01f)
+                .of(expectedProgress * total);
+        assertThat(actualCircularProgressIndicator.getStartAngle().getValue())
+                .isWithin(0.01f)
+                .of(expectedStartAngle);
+        assertThat(actualCircularProgressIndicator.getEndAngle().getValue())
+                .isWithin(0.01f)
+                .of(expectedEndAngle);
+        assertThat(
+                        actualCircularProgressIndicator
+                                .getCircularProgressIndicatorColors()
+                                .getIndicatorColor()
+                                .getArgb())
+                .isEqualTo(expectedColors.getIndicatorColor().getArgb());
+        assertThat(
+                        actualCircularProgressIndicator
+                                .getCircularProgressIndicatorColors()
+                                .getTrackColor()
+                                .getArgb())
+                .isEqualTo(expectedColors.getTrackColor().getArgb());
+        assertThat(actualCircularProgressIndicator.getStrokeWidth().getValue())
+                .isEqualTo(expectedThickness);
+
+        if (expectedContentDescription == null) {
+            assertThat(actualCircularProgressIndicator.getContentDescription()).isNull();
+        } else {
+            assertThat(actualCircularProgressIndicator.getContentDescription().toString())
+                    .isEqualTo(expectedContentDescription);
+        }
+    }
+}
diff --git a/wear/protolayout/protolayout-material/src/test/java/androidx/wear/protolayout/material/CompactChipTest.java b/wear/protolayout/protolayout-material/src/test/java/androidx/wear/protolayout/material/CompactChipTest.java
new file mode 100644
index 0000000..f3834d6
--- /dev/null
+++ b/wear/protolayout/protolayout-material/src/test/java/androidx/wear/protolayout/material/CompactChipTest.java
@@ -0,0 +1,125 @@
+/*
+ * 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.wear.protolayout.material;
+
+import static androidx.wear.protolayout.material.Utils.areChipColorsEqual;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import android.content.Context;
+import android.graphics.Color;
+
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.wear.protolayout.ActionBuilders.LaunchAction;
+import androidx.wear.protolayout.DeviceParametersBuilders.DeviceParameters;
+import androidx.wear.protolayout.LayoutElementBuilders.Box;
+import androidx.wear.protolayout.LayoutElementBuilders.Column;
+import androidx.wear.protolayout.ModifiersBuilders.Clickable;
+import androidx.wear.protolayout.ModifiersBuilders.ElementMetadata;
+import androidx.wear.protolayout.ModifiersBuilders.Modifiers;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.annotation.internal.DoNotInstrument;
+
+@RunWith(AndroidJUnit4.class)
+@DoNotInstrument
+public class CompactChipTest {
+    private static final String MAIN_TEXT = "Action";
+    private static final Clickable CLICKABLE =
+            new Clickable.Builder()
+                    .setOnClick(new LaunchAction.Builder().build())
+                    .setId("action_id")
+                    .build();
+    private static final DeviceParameters DEVICE_PARAMETERS =
+            new DeviceParameters.Builder().setScreenWidthDp(192).setScreenHeightDp(192).build();
+    private static final ChipColors COLORS = new ChipColors(Color.YELLOW, Color.BLUE);
+    private static final Context CONTEXT = ApplicationProvider.getApplicationContext();
+
+    @Test
+    public void testCompactChipDefault() {
+        CompactChip compactChip =
+                new CompactChip.Builder(CONTEXT, MAIN_TEXT, CLICKABLE, DEVICE_PARAMETERS).build();
+
+        assertChip(compactChip, ChipDefaults.COMPACT_PRIMARY_COLORS);
+    }
+
+    @Test
+    public void testCompactChipCustomColor() {
+        CompactChip compactChip =
+                new CompactChip.Builder(CONTEXT, MAIN_TEXT, CLICKABLE, DEVICE_PARAMETERS)
+                        .setChipColors(COLORS)
+                        .build();
+
+        assertChip(compactChip, COLORS);
+    }
+
+    @Test
+    public void testWrongElement() {
+        Column box = new Column.Builder().build();
+
+        assertThat(CompactChip.fromLayoutElement(box)).isNull();
+    }
+
+    @Test
+    public void testWrongBox() {
+        Box box = new Box.Builder().build();
+
+        assertThat(CompactChip.fromLayoutElement(box)).isNull();
+    }
+
+    @Test
+    public void testWrongTag() {
+        Box box =
+                new Box.Builder()
+                        .setModifiers(
+                                new Modifiers.Builder()
+                                        .setMetadata(
+                                                new ElementMetadata.Builder()
+                                                        .setTagData("test".getBytes(UTF_8))
+                                                        .build())
+                                        .build())
+                        .build();
+
+        assertThat(CompactChip.fromLayoutElement(box)).isNull();
+    }
+
+    private void assertChip(CompactChip actualCompactChip, ChipColors colors) {
+        assertChipIsEqual(actualCompactChip, colors);
+        assertFromLayoutElementChipIsEqual(actualCompactChip, colors);
+        assertThat(CompactChip.fromLayoutElement(actualCompactChip)).isEqualTo(actualCompactChip);
+    }
+
+    private void assertChipIsEqual(CompactChip actualCompactChip, ChipColors colors) {
+        assertThat(actualCompactChip.getMetadataTag()).isEqualTo(CompactChip.METADATA_TAG);
+        assertThat(actualCompactChip.getClickable().toProto()).isEqualTo(CLICKABLE.toProto());
+        assertThat(areChipColorsEqual(actualCompactChip.getChipColors(), colors)).isTrue();
+        assertThat(actualCompactChip.getText()).isEqualTo(MAIN_TEXT);
+    }
+
+    private void assertFromLayoutElementChipIsEqual(CompactChip chip, ChipColors colors) {
+        Box box = new Box.Builder().addContent(chip).build();
+
+        CompactChip newChip = CompactChip.fromLayoutElement(box.getContents().get(0));
+
+        assertThat(newChip).isNotNull();
+        assertChipIsEqual(newChip, colors);
+    }
+}
diff --git a/wear/protolayout/protolayout-material/src/test/java/androidx/wear/protolayout/material/ProgressIndicatorColorsTest.java b/wear/protolayout/protolayout-material/src/test/java/androidx/wear/protolayout/material/ProgressIndicatorColorsTest.java
new file mode 100644
index 0000000..485a4f1
--- /dev/null
+++ b/wear/protolayout/protolayout-material/src/test/java/androidx/wear/protolayout/material/ProgressIndicatorColorsTest.java
@@ -0,0 +1,71 @@
+/*
+ * 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.wear.protolayout.material;
+
+import static androidx.wear.protolayout.ColorBuilders.argb;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.wear.protolayout.ColorBuilders.ColorProp;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.annotation.internal.DoNotInstrument;
+
+@RunWith(AndroidJUnit4.class)
+@DoNotInstrument
+public class ProgressIndicatorColorsTest {
+    private static final int ARGB_BACKGROUND_COLOR = 0x12345678;
+    private static final int ARGB_CONTENT_COLOR = 0x11223344;
+    private static final ColorProp TRACK_COLOR = argb(ARGB_BACKGROUND_COLOR);
+    private static final ColorProp INDICATOR_COLOR = argb(ARGB_CONTENT_COLOR);
+    private static final Colors COLORS = new Colors(0x123, 0x234, 0x345, 0x456);
+
+    @Test
+    public void testCreateProgressIndicatorColorsFromArgb() {
+        ProgressIndicatorColors progressIndicatorColors =
+                new ProgressIndicatorColors(ARGB_CONTENT_COLOR, ARGB_BACKGROUND_COLOR);
+
+        assertThat(progressIndicatorColors.getTrackColor().getArgb())
+                .isEqualTo(TRACK_COLOR.getArgb());
+        assertThat(progressIndicatorColors.getIndicatorColor().getArgb())
+                .isEqualTo(INDICATOR_COLOR.getArgb());
+    }
+
+    @Test
+    public void testCreateProgressIndicatorColorsFromColorProp() {
+        ProgressIndicatorColors progressIndicatorColors =
+                new ProgressIndicatorColors(INDICATOR_COLOR, TRACK_COLOR);
+
+        assertThat(progressIndicatorColors.getTrackColor().getArgb())
+                .isEqualTo(TRACK_COLOR.getArgb());
+        assertThat(progressIndicatorColors.getIndicatorColor().getArgb())
+                .isEqualTo(INDICATOR_COLOR.getArgb());
+    }
+
+    @Test
+    public void testCreateProgressIndicatorColorsFromHelper() {
+        ProgressIndicatorColors progressIndicatorColors =
+                ProgressIndicatorColors.progressIndicatorColors(COLORS);
+
+        assertThat(progressIndicatorColors.getTrackColor().getArgb())
+                .isEqualTo(COLORS.getSurface());
+        assertThat(progressIndicatorColors.getIndicatorColor().getArgb())
+                .isEqualTo(COLORS.getPrimary());
+    }
+}
diff --git a/wear/protolayout/protolayout-material/src/test/java/androidx/wear/protolayout/material/TextTest.java b/wear/protolayout/protolayout-material/src/test/java/androidx/wear/protolayout/material/TextTest.java
new file mode 100644
index 0000000..3f424d0
--- /dev/null
+++ b/wear/protolayout/protolayout-material/src/test/java/androidx/wear/protolayout/material/TextTest.java
@@ -0,0 +1,197 @@
+/*
+ * 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.wear.protolayout.material;
+
+import static androidx.wear.protolayout.ColorBuilders.argb;
+import static androidx.wear.protolayout.LayoutElementBuilders.FONT_WEIGHT_BOLD;
+import static androidx.wear.protolayout.LayoutElementBuilders.FONT_WEIGHT_MEDIUM;
+import static androidx.wear.protolayout.LayoutElementBuilders.FONT_WEIGHT_NORMAL;
+import static androidx.wear.protolayout.LayoutElementBuilders.TEXT_ALIGN_END;
+import static androidx.wear.protolayout.LayoutElementBuilders.TEXT_OVERFLOW_ELLIPSIZE_END;
+import static androidx.wear.protolayout.material.Typography.TYPOGRAPHY_BODY1;
+import static androidx.wear.protolayout.material.Typography.TYPOGRAPHY_CAPTION2;
+import static androidx.wear.protolayout.material.Typography.TYPOGRAPHY_TITLE1;
+import static androidx.wear.protolayout.material.Typography.getFontStyleBuilder;
+import static androidx.wear.protolayout.material.Typography.getLineHeightForTypography;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import android.content.Context;
+import android.graphics.Color;
+
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.wear.protolayout.LayoutElementBuilders.Box;
+import androidx.wear.protolayout.LayoutElementBuilders.Column;
+import androidx.wear.protolayout.LayoutElementBuilders.FontStyle;
+import androidx.wear.protolayout.ModifiersBuilders.Background;
+import androidx.wear.protolayout.ModifiersBuilders.ElementMetadata;
+import androidx.wear.protolayout.ModifiersBuilders.Modifiers;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.annotation.internal.DoNotInstrument;
+
+@RunWith(AndroidJUnit4.class)
+@DoNotInstrument
+public class TextTest {
+
+    public static final int NUM_OF_FONT_STYLE_CONST = 12;
+    private static final Context CONTEXT = ApplicationProvider.getApplicationContext();
+
+    @Test
+    public void testTypography_incorrectTypography_negativeValue() {
+        assertThrows(IllegalArgumentException.class, () -> getFontStyleBuilder(-1, CONTEXT));
+    }
+
+    @Test
+    public void testTypography_incorrectTypography_positiveValue() {
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> getFontStyleBuilder(NUM_OF_FONT_STYLE_CONST + 1, CONTEXT));
+    }
+
+    @Test
+    public void testLineHeight_incorrectTypography_negativeValue() {
+        assertThrows(IllegalArgumentException.class, () -> getLineHeightForTypography(-1));
+    }
+
+    @Test
+    public void testLineHeight_incorrectTypography_positiveValue() {
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> getLineHeightForTypography(NUM_OF_FONT_STYLE_CONST + 1));
+    }
+
+    @Test
+    public void testTypography_body1() {
+        FontStyle fontStyle = getFontStyleBuilder(TYPOGRAPHY_BODY1, CONTEXT).build();
+        assertFontStyle(fontStyle, 16, FONT_WEIGHT_NORMAL, 0.01f, 20, TYPOGRAPHY_BODY1);
+    }
+
+    @Test
+    public void testTypography_caption2() {
+        FontStyle fontStyle = getFontStyleBuilder(TYPOGRAPHY_CAPTION2, CONTEXT).build();
+        assertFontStyle(fontStyle, 12, FONT_WEIGHT_MEDIUM, 0.01f, 16, TYPOGRAPHY_CAPTION2);
+    }
+
+    @Test
+    public void testWrongElement() {
+        Column box = new Column.Builder().build();
+
+        assertThat(Text.fromLayoutElement(box)).isNull();
+    }
+
+    @Test
+    public void testWrongBox() {
+        Box box = new Box.Builder().build();
+
+        assertThat(Text.fromLayoutElement(box)).isNull();
+    }
+
+    @Test
+    public void testWrongTag() {
+        Box box =
+                new Box.Builder()
+                    .setModifiers(
+                        new Modifiers.Builder()
+                            .setMetadata(
+                                new ElementMetadata.Builder()
+                                    .setTagData("test".getBytes(UTF_8))
+                                    .build())
+                            .build())
+                    .build();
+
+        assertThat(Text.fromLayoutElement(box)).isNull();
+    }
+
+    @Test
+    public void testText() {
+        String textContent = "Testing text.";
+        Modifiers modifiers =
+                new Modifiers.Builder()
+                        .setBackground(new Background.Builder().setColor(argb(Color.BLUE)).build())
+                        .build();
+        int color = Color.YELLOW;
+        Text text =
+                new Text.Builder(CONTEXT, textContent)
+                        .setItalic(true)
+                        .setColor(argb(color))
+                        .setTypography(TYPOGRAPHY_TITLE1)
+                        .setUnderline(true)
+                        .setMaxLines(2)
+                        .setModifiers(modifiers)
+                        .setOverflow(TEXT_OVERFLOW_ELLIPSIZE_END)
+                        .setMultilineAlignment(TEXT_ALIGN_END)
+                        .setWeight(FONT_WEIGHT_BOLD)
+                        .build();
+
+        FontStyle expectedFontStyle =
+                getFontStyleBuilder(TYPOGRAPHY_TITLE1, CONTEXT)
+                        .setItalic(true)
+                        .setUnderline(true)
+                        .setColor(argb(color))
+                        .setWeight(FONT_WEIGHT_BOLD)
+                        .build();
+
+        assertTextIsEqual(text, textContent, modifiers, color, expectedFontStyle);
+
+        Box box = new Box.Builder().addContent(text).build();
+        Text newText = Text.fromLayoutElement(box.getContents().get(0));
+        assertThat(newText).isNotNull();
+        assertTextIsEqual(newText, textContent, modifiers, color, expectedFontStyle);
+
+        assertThat(Text.fromLayoutElement(text)).isEqualTo(text);
+    }
+
+    private void assertTextIsEqual(
+            Text actualText,
+            String expectedTextContent,
+            Modifiers expectedModifiers,
+            int expectedColor,
+            FontStyle expectedFontStyle) {
+        assertThat(actualText.getFontStyle().toProto()).isEqualTo(expectedFontStyle.toProto());
+        assertThat(actualText.getText()).isEqualTo(expectedTextContent);
+        assertThat(actualText.getColor().getArgb()).isEqualTo(expectedColor);
+        assertThat(actualText.getOverflow()).isEqualTo(TEXT_OVERFLOW_ELLIPSIZE_END);
+        assertThat(actualText.getMultilineAlignment()).isEqualTo(TEXT_ALIGN_END);
+        assertThat(actualText.getMaxLines()).isEqualTo(2);
+        assertThat(actualText.getLineHeight())
+                .isEqualTo(getLineHeightForTypography(TYPOGRAPHY_TITLE1).getValue());
+    }
+
+    private void assertFontStyle(
+            FontStyle actualFontStyle,
+            int expectedSize,
+            int expectedWeight,
+            float expectedLetterSpacing,
+            float expectedLineHeight,
+            int typographyCode) {
+        assertThat(actualFontStyle.getSize()).isNotNull();
+        assertThat(actualFontStyle.getWeight()).isNotNull();
+        assertThat(actualFontStyle.getLetterSpacing()).isNotNull();
+        assertThat(actualFontStyle.getSize().getValue()).isEqualTo(expectedSize);
+        assertThat(actualFontStyle.getWeight().getValue()).isEqualTo(expectedWeight);
+        assertThat(actualFontStyle.getLetterSpacing().getValue()).isEqualTo(expectedLetterSpacing);
+        assertThat(getLineHeightForTypography(typographyCode).getValue())
+                .isEqualTo(expectedLineHeight);
+    }
+}
diff --git a/wear/protolayout/protolayout-material/src/test/java/androidx/wear/protolayout/material/TitleChipTest.java b/wear/protolayout/protolayout-material/src/test/java/androidx/wear/protolayout/material/TitleChipTest.java
new file mode 100644
index 0000000..408484f
--- /dev/null
+++ b/wear/protolayout/protolayout-material/src/test/java/androidx/wear/protolayout/material/TitleChipTest.java
@@ -0,0 +1,137 @@
+/*
+ * 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.wear.protolayout.material;
+
+import static androidx.wear.protolayout.DimensionBuilders.dp;
+import static androidx.wear.protolayout.material.Utils.areChipColorsEqual;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import android.content.Context;
+import android.graphics.Color;
+
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.wear.protolayout.ActionBuilders.LaunchAction;
+import androidx.wear.protolayout.DeviceParametersBuilders.DeviceParameters;
+import androidx.wear.protolayout.DimensionBuilders.DpProp;
+import androidx.wear.protolayout.LayoutElementBuilders.Box;
+import androidx.wear.protolayout.LayoutElementBuilders.Column;
+import androidx.wear.protolayout.ModifiersBuilders.Clickable;
+import androidx.wear.protolayout.ModifiersBuilders.ElementMetadata;
+import androidx.wear.protolayout.ModifiersBuilders.Modifiers;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.annotation.internal.DoNotInstrument;
+
+@RunWith(AndroidJUnit4.class)
+@DoNotInstrument
+public class TitleChipTest {
+    private static final String MAIN_TEXT = "Action";
+    private static final Clickable CLICKABLE =
+            new Clickable.Builder()
+                    .setOnClick(new LaunchAction.Builder().build())
+                    .setId("action_id")
+                    .build();
+    private static final DeviceParameters DEVICE_PARAMETERS =
+            new DeviceParameters.Builder().setScreenWidthDp(192).setScreenHeightDp(192).build();
+    private static final ChipColors COLORS = new ChipColors(Color.YELLOW, Color.BLUE);
+    private static final Context CONTEXT = ApplicationProvider.getApplicationContext();
+    private static final DpProp EXPECTED_WIDTH =
+            dp(
+                    DEVICE_PARAMETERS.getScreenWidthDp()
+                            * (100 - 2 * ChipDefaults.DEFAULT_MARGIN_PERCENT)
+                            / 100);
+
+    @Test
+    public void testTitleChipDefault() {
+        TitleChip titleChip =
+                new TitleChip.Builder(CONTEXT, MAIN_TEXT, CLICKABLE, DEVICE_PARAMETERS).build();
+
+        assertChip(titleChip, ChipDefaults.TITLE_PRIMARY_COLORS, EXPECTED_WIDTH);
+    }
+
+    @Test
+    public void testTitleChipCustom() {
+        DpProp width = dp(150);
+        TitleChip titleChip =
+                new TitleChip.Builder(CONTEXT, MAIN_TEXT, CLICKABLE, DEVICE_PARAMETERS)
+                        .setChipColors(COLORS)
+                        .setWidth(width)
+                        .build();
+
+        assertChip(titleChip, COLORS, width);
+    }
+
+    @Test
+    public void testWrongElement() {
+        Column box = new Column.Builder().build();
+
+        assertThat(TitleChip.fromLayoutElement(box)).isNull();
+    }
+
+    @Test
+    public void testWrongBox() {
+        Box box = new Box.Builder().build();
+
+        assertThat(TitleChip.fromLayoutElement(box)).isNull();
+    }
+
+    @Test
+    public void testWrongTag() {
+        Box box =
+                new Box.Builder()
+                        .setModifiers(
+                                new Modifiers.Builder()
+                                        .setMetadata(
+                                                new ElementMetadata.Builder()
+                                                        .setTagData("test".getBytes(UTF_8))
+                                                        .build())
+                                        .build())
+                        .build();
+
+        assertThat(TitleChip.fromLayoutElement(box)).isNull();
+    }
+
+    private void assertChip(TitleChip actualTitleChip, ChipColors colors, DpProp width) {
+        assertChipIsEqual(actualTitleChip, colors, width);
+        assertFromLayoutElementChipIsEqual(actualTitleChip, colors, width);
+        assertThat(TitleChip.fromLayoutElement(actualTitleChip)).isEqualTo(actualTitleChip);
+    }
+
+    private void assertChipIsEqual(TitleChip actualTitleChip, ChipColors colors, DpProp width) {
+        assertThat(actualTitleChip.getMetadataTag()).isEqualTo(TitleChip.METADATA_TAG);
+        assertThat(actualTitleChip.getClickable().toProto()).isEqualTo(CLICKABLE.toProto());
+        assertThat(actualTitleChip.getWidth().toContainerDimensionProto())
+                .isEqualTo(width.toContainerDimensionProto());
+        assertThat(areChipColorsEqual(actualTitleChip.getChipColors(), colors)).isTrue();
+        assertThat(actualTitleChip.getText()).isEqualTo(MAIN_TEXT);
+    }
+
+    private void assertFromLayoutElementChipIsEqual(
+            TitleChip chip, ChipColors colors, DpProp width) {
+        Box box = new Box.Builder().addContent(chip).build();
+
+        TitleChip newChip = TitleChip.fromLayoutElement(box.getContents().get(0));
+
+        assertThat(newChip).isNotNull();
+        assertChipIsEqual(newChip, colors, width);
+    }
+}
diff --git a/wear/protolayout/protolayout-material/src/test/java/androidx/wear/protolayout/material/Utils.java b/wear/protolayout/protolayout-material/src/test/java/androidx/wear/protolayout/material/Utils.java
new file mode 100644
index 0000000..363a204
--- /dev/null
+++ b/wear/protolayout/protolayout-material/src/test/java/androidx/wear/protolayout/material/Utils.java
@@ -0,0 +1,37 @@
+/*
+ * 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.wear.protolayout.material;
+
+import androidx.annotation.Dimension;
+
+public final class Utils {
+    /** Returns true if the given ChipColors have the same colored content. */
+    static boolean areChipColorsEqual(ChipColors colors1, ChipColors colors2) {
+        return colors1.getBackgroundColor().getArgb() == colors2.getBackgroundColor().getArgb()
+                && colors1.getContentColor().getArgb() == colors2.getContentColor().getArgb()
+                && colors1.getSecondaryContentColor().getArgb()
+                        == colors2.getSecondaryContentColor().getArgb()
+                && colors1.getIconColor().getArgb() == colors2.getIconColor().getArgb();
+    }
+
+    @Dimension(unit = Dimension.DP)
+    public static int pxToDp(int px, float scale) {
+        return (int) ((px - 0.5f) / scale);
+    }
+
+    private Utils() {}
+}
diff --git a/wear/protolayout/protolayout-material/src/test/java/androidx/wear/protolayout/material/layouts/EdgeContentLayoutTest.java b/wear/protolayout/protolayout-material/src/test/java/androidx/wear/protolayout/material/layouts/EdgeContentLayoutTest.java
new file mode 100644
index 0000000..c673b23
--- /dev/null
+++ b/wear/protolayout/protolayout-material/src/test/java/androidx/wear/protolayout/material/layouts/EdgeContentLayoutTest.java
@@ -0,0 +1,227 @@
+/*
+ * 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.wear.protolayout.material.layouts;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import android.content.Context;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.wear.protolayout.DeviceParametersBuilders.DeviceParameters;
+import androidx.wear.protolayout.LayoutElementBuilders.Box;
+import androidx.wear.protolayout.LayoutElementBuilders.Column;
+import androidx.wear.protolayout.LayoutElementBuilders.LayoutElement;
+import androidx.wear.protolayout.ModifiersBuilders.ElementMetadata;
+import androidx.wear.protolayout.ModifiersBuilders.Modifiers;
+import androidx.wear.protolayout.material.CircularProgressIndicator;
+import androidx.wear.protolayout.material.Text;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.annotation.internal.DoNotInstrument;
+
+@RunWith(AndroidJUnit4.class)
+@DoNotInstrument
+public class EdgeContentLayoutTest {
+    private static final Context CONTEXT = ApplicationProvider.getApplicationContext();
+    private static final DeviceParameters DEVICE_PARAMETERS =
+            new DeviceParameters.Builder().setScreenWidthDp(192).setScreenHeightDp(192).build();
+    private static final Text PRIMARY_LABEL = new Text.Builder(CONTEXT, "Primary label").build();
+    private static final Text SECONDARY_LABEL =
+            new Text.Builder(CONTEXT, "Secondary label").build();
+
+    @Test
+    public void testAll() {
+        LayoutElement content = new Box.Builder().build();
+        CircularProgressIndicator progressIndicator =
+                new CircularProgressIndicator.Builder().build();
+        EdgeContentLayout layout =
+                new EdgeContentLayout.Builder(DEVICE_PARAMETERS)
+                        .setContent(content)
+                        .setEdgeContent(progressIndicator)
+                        .setPrimaryLabelTextContent(PRIMARY_LABEL)
+                        .setSecondaryLabelTextContent(SECONDARY_LABEL)
+                        .build();
+
+        assertLayout(layout, progressIndicator, content, PRIMARY_LABEL, SECONDARY_LABEL);
+    }
+
+    @Test
+    public void testContentOnly() {
+        LayoutElement content = new Box.Builder().build();
+        EdgeContentLayout layout =
+                new EdgeContentLayout.Builder(DEVICE_PARAMETERS).setContent(content).build();
+
+        assertLayout(layout, null, content, null, null);
+    }
+
+    @Test
+    public void testIndicatorOnly() {
+        CircularProgressIndicator progressIndicator =
+                new CircularProgressIndicator.Builder().build();
+        EdgeContentLayout layout =
+                new EdgeContentLayout.Builder(DEVICE_PARAMETERS)
+                        .setEdgeContent(progressIndicator)
+                        .build();
+
+        assertLayout(layout, progressIndicator, null, null, null);
+    }
+
+    @Test
+    public void testEmpty() {
+        EdgeContentLayout layout = new EdgeContentLayout.Builder(DEVICE_PARAMETERS).build();
+
+        assertLayout(layout, null, null, null, null);
+    }
+
+    @Test
+    public void testWrongElement() {
+        Column box = new Column.Builder().build();
+
+        assertThat(EdgeContentLayout.fromLayoutElement(box)).isNull();
+    }
+
+    @Test
+    public void testWrongBox() {
+        Box box = new Box.Builder().build();
+
+        assertThat(EdgeContentLayout.fromLayoutElement(box)).isNull();
+    }
+
+    @Test
+    public void testWrongTag() {
+        Box box =
+                new Box.Builder()
+                        .setModifiers(
+                                new Modifiers.Builder()
+                                        .setMetadata(
+                                                new ElementMetadata.Builder()
+                                                        .setTagData("test".getBytes(UTF_8))
+                                                        .build())
+                                        .build())
+                        .build();
+
+        assertThat(EdgeContentLayout.fromLayoutElement(box)).isNull();
+    }
+
+    @Test
+    public void testWrongLengthTag() {
+        Box box =
+                new Box.Builder()
+                        .setModifiers(
+                                new Modifiers.Builder()
+                                        .setMetadata(
+                                                new ElementMetadata.Builder()
+                                                        .setTagData(
+                                                                EdgeContentLayout
+                                                                        .METADATA_TAG_PREFIX
+                                                                        .getBytes(UTF_8))
+                                                        .build())
+                                        .build())
+                        .build();
+
+        assertThat(EdgeContentLayout.fromLayoutElement(box)).isNull();
+    }
+
+    private void assertLayout(
+            @NonNull EdgeContentLayout actualLayout,
+            @Nullable LayoutElement expectedProgressIndicator,
+            @Nullable LayoutElement expectedContent,
+            @Nullable LayoutElement expectedPrimaryLabel,
+            @Nullable LayoutElement expectedSecondaryLabel) {
+        assertLayoutIsEqual(
+                actualLayout,
+                expectedProgressIndicator,
+                expectedContent,
+                expectedPrimaryLabel,
+                expectedSecondaryLabel);
+
+        Box box = new Box.Builder().addContent(actualLayout).build();
+
+        EdgeContentLayout newLayout = EdgeContentLayout.fromLayoutElement(box.getContents().get(0));
+
+        assertThat(newLayout).isNotNull();
+        assertLayoutIsEqual(
+                newLayout,
+                expectedProgressIndicator,
+                expectedContent,
+                expectedPrimaryLabel,
+                expectedSecondaryLabel);
+
+        assertThat(EdgeContentLayout.fromLayoutElement(actualLayout)).isEqualTo(actualLayout);
+    }
+
+    private void assertLayoutIsEqual(
+            @NonNull EdgeContentLayout actualLayout,
+            @Nullable LayoutElement expectedProgressIndicator,
+            @Nullable LayoutElement expectedContent,
+            @Nullable LayoutElement expectedPrimaryLabel,
+            @Nullable LayoutElement expectedSecondaryLabel) {
+        byte[] expectedMetadata = EdgeContentLayout.METADATA_TAG_BASE.clone();
+
+        if (expectedProgressIndicator == null) {
+            assertThat(actualLayout.getEdgeContent()).isNull();
+        } else {
+            assertThat(actualLayout.getEdgeContent().toLayoutElementProto())
+                    .isEqualTo(expectedProgressIndicator.toLayoutElementProto());
+            expectedMetadata[EdgeContentLayout.FLAG_INDEX] =
+                    (byte)
+                            (expectedMetadata[EdgeContentLayout.FLAG_INDEX]
+                                    | EdgeContentLayout.EDGE_CONTENT_PRESENT);
+        }
+
+        if (expectedContent == null) {
+            assertThat(actualLayout.getContent()).isNull();
+        } else {
+            assertThat(actualLayout.getContent().toLayoutElementProto())
+                    .isEqualTo(expectedContent.toLayoutElementProto());
+            expectedMetadata[EdgeContentLayout.FLAG_INDEX] =
+                    (byte)
+                            (expectedMetadata[EdgeContentLayout.FLAG_INDEX]
+                                    | EdgeContentLayout.CONTENT_PRESENT);
+        }
+
+        if (expectedPrimaryLabel == null) {
+            assertThat(actualLayout.getPrimaryLabelTextContent()).isNull();
+        } else {
+            assertThat(actualLayout.getPrimaryLabelTextContent().toLayoutElementProto())
+                    .isEqualTo(expectedPrimaryLabel.toLayoutElementProto());
+            expectedMetadata[EdgeContentLayout.FLAG_INDEX] =
+                    (byte)
+                            (expectedMetadata[EdgeContentLayout.FLAG_INDEX]
+                                    | EdgeContentLayout.PRIMARY_LABEL_PRESENT);
+        }
+
+        if (expectedSecondaryLabel == null) {
+            assertThat(actualLayout.getSecondaryLabelTextContent()).isNull();
+        } else {
+            assertThat(actualLayout.getSecondaryLabelTextContent().toLayoutElementProto())
+                    .isEqualTo(expectedSecondaryLabel.toLayoutElementProto());
+            expectedMetadata[EdgeContentLayout.FLAG_INDEX] =
+                    (byte)
+                            (expectedMetadata[EdgeContentLayout.FLAG_INDEX]
+                                    | EdgeContentLayout.SECONDARY_LABEL_PRESENT);
+        }
+
+        assertThat(actualLayout.getMetadataTag()).isEqualTo(expectedMetadata);
+    }
+}
diff --git a/wear/protolayout/protolayout-material/src/test/java/androidx/wear/protolayout/material/layouts/MultiButtonLayoutTest.java b/wear/protolayout/protolayout-material/src/test/java/androidx/wear/protolayout/material/layouts/MultiButtonLayoutTest.java
new file mode 100644
index 0000000..085b61d
--- /dev/null
+++ b/wear/protolayout/protolayout-material/src/test/java/androidx/wear/protolayout/material/layouts/MultiButtonLayoutTest.java
@@ -0,0 +1,168 @@
+/*
+ * 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.wear.protolayout.material.layouts;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import android.content.Context;
+
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.wear.protolayout.ActionBuilders.LaunchAction;
+import androidx.wear.protolayout.LayoutElementBuilders.Box;
+import androidx.wear.protolayout.LayoutElementBuilders.Column;
+import androidx.wear.protolayout.ModifiersBuilders.Clickable;
+import androidx.wear.protolayout.ModifiersBuilders.ElementMetadata;
+import androidx.wear.protolayout.ModifiersBuilders.Modifiers;
+import androidx.wear.protolayout.material.Button;
+import androidx.wear.protolayout.material.ButtonDefaults;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.annotation.internal.DoNotInstrument;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+@RunWith(AndroidJUnit4.class)
+@DoNotInstrument
+public class MultiButtonLayoutTest {
+    private static final Context CONTEXT = ApplicationProvider.getApplicationContext();
+    private static final Clickable CLICKABLE =
+            new Clickable.Builder()
+                    .setOnClick(new LaunchAction.Builder().build())
+                    .setId("action_id")
+                    .build();
+
+    @Test
+    public void test_1button() {
+        Button button1 =
+                new Button.Builder(CONTEXT, CLICKABLE)
+                        .setTextContent("1")
+                        .setSize(ButtonDefaults.EXTRA_LARGE_SIZE)
+                        .build();
+
+        MultiButtonLayout layout =
+                new MultiButtonLayout.Builder().addButtonContent(button1).build();
+
+        assertLayout(layout, Arrays.asList(button1));
+    }
+
+    @Test
+    public void test_2buttons() {
+        Button button1 = new Button.Builder(CONTEXT, CLICKABLE).setTextContent("1").build();
+        Button button2 = new Button.Builder(CONTEXT, CLICKABLE).setTextContent("2").build();
+
+        MultiButtonLayout layout =
+                new MultiButtonLayout.Builder()
+                        .addButtonContent(button1)
+                        .addButtonContent(button2)
+                        .build();
+
+        assertLayout(layout, Arrays.asList(button1, button2));
+    }
+
+    @Test
+    public void test_5buttons() {
+        List<Button> buttons = new ArrayList<>();
+        int size = 5;
+        for (int i = 0; i < size; i++) {
+            buttons.add(new Button.Builder(CONTEXT, CLICKABLE).setTextContent("" + i).build());
+        }
+
+        MultiButtonLayout.Builder layoutBuilder = new MultiButtonLayout.Builder();
+        for (Button b : buttons) {
+            layoutBuilder.addButtonContent(b);
+        }
+        layoutBuilder.setFiveButtonDistribution(
+                MultiButtonLayout.FIVE_BUTTON_DISTRIBUTION_TOP_HEAVY);
+        MultiButtonLayout layout = layoutBuilder.build();
+
+        assertLayout(layout, buttons);
+        assertThat(layout.getFiveButtonDistribution())
+                .isEqualTo(MultiButtonLayout.FIVE_BUTTON_DISTRIBUTION_TOP_HEAVY);
+    }
+
+    @Test
+    public void test_too_many_button() {
+        Button button = new Button.Builder(CONTEXT, CLICKABLE).setTextContent("1").build();
+        MultiButtonLayout.Builder layoutBuilder = new MultiButtonLayout.Builder();
+        for (int i = 0; i < LayoutDefaults.MULTI_BUTTON_MAX_NUMBER + 1; i++) {
+            layoutBuilder.addButtonContent(button);
+        }
+
+        assertThrows(IllegalArgumentException.class, layoutBuilder::build);
+    }
+
+    @Test
+    public void testWrongElement() {
+        Column box = new Column.Builder().build();
+
+        assertThat(MultiButtonLayout.fromLayoutElement(box)).isNull();
+    }
+
+    @Test
+    public void testWrongBox() {
+        Box box = new Box.Builder().build();
+
+        assertThat(MultiButtonLayout.fromLayoutElement(box)).isNull();
+    }
+
+    @Test
+    public void testWrongTag() {
+        Box box =
+                new Box.Builder()
+                    .setModifiers(
+                        new Modifiers.Builder()
+                            .setMetadata(
+                                new ElementMetadata.Builder()
+                                    .setTagData("test".getBytes(UTF_8))
+                                    .build())
+                            .build())
+                    .build();
+
+        assertThat(MultiButtonLayout.fromLayoutElement(box)).isNull();
+    }
+
+    private void assertLayout(MultiButtonLayout actualLayout, List<Button> expectedButtons) {
+        assertLayoutIsEqual(actualLayout, expectedButtons);
+
+        Box box = new Box.Builder().addContent(actualLayout).build();
+
+        MultiButtonLayout newLayout = MultiButtonLayout.fromLayoutElement(box.getContents().get(0));
+
+        assertThat(newLayout).isNotNull();
+        assertLayoutIsEqual(newLayout, expectedButtons);
+
+        assertThat(MultiButtonLayout.fromLayoutElement(actualLayout)).isEqualTo(actualLayout);
+    }
+
+    private void assertLayoutIsEqual(MultiButtonLayout actualLayout, List<Button> expectedButtons) {
+        int size = expectedButtons.size();
+        assertThat(actualLayout.getMetadataTag()).isEqualTo(MultiButtonLayout.METADATA_TAG);
+        assertThat(actualLayout.getButtonContents()).hasSize(size);
+        for (int i = 0; i < size; i++) {
+            assertThat(actualLayout.getButtonContents().get(i).toLayoutElementProto())
+                    .isEqualTo(expectedButtons.get(i).toLayoutElementProto());
+        }
+    }
+}
diff --git a/wear/protolayout/protolayout-material/src/test/java/androidx/wear/protolayout/material/layouts/MultiSlotLayoutTest.java b/wear/protolayout/protolayout-material/src/test/java/androidx/wear/protolayout/material/layouts/MultiSlotLayoutTest.java
new file mode 100644
index 0000000..db1e4c7
--- /dev/null
+++ b/wear/protolayout/protolayout-material/src/test/java/androidx/wear/protolayout/material/layouts/MultiSlotLayoutTest.java
@@ -0,0 +1,107 @@
+/*
+ * 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.wear.protolayout.material.layouts;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.wear.protolayout.LayoutElementBuilders.Box;
+import androidx.wear.protolayout.LayoutElementBuilders.Column;
+import androidx.wear.protolayout.LayoutElementBuilders.LayoutElement;
+import androidx.wear.protolayout.LayoutElementBuilders.Row;
+import androidx.wear.protolayout.ModifiersBuilders.ElementMetadata;
+import androidx.wear.protolayout.ModifiersBuilders.Modifiers;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.annotation.internal.DoNotInstrument;
+
+@RunWith(AndroidJUnit4.class)
+@DoNotInstrument
+public class MultiSlotLayoutTest {
+
+    @Test
+    public void test() {
+        LayoutElement content1 = new Box.Builder().build();
+        LayoutElement content2 = new Row.Builder().build();
+        float spacerWidth = 10f;
+
+        MultiSlotLayout layout =
+                new MultiSlotLayout.Builder()
+                        .addSlotContent(content1)
+                        .addSlotContent(content2)
+                        .setHorizontalSpacerWidth(spacerWidth)
+                        .build();
+
+        assertLayoutIsEqual(content1, content2, spacerWidth, layout);
+
+        Box box = new Box.Builder().addContent(layout).build();
+
+        MultiSlotLayout newLayout = MultiSlotLayout.fromLayoutElement(box.getContents().get(0));
+
+        assertThat(newLayout).isNotNull();
+        assertLayoutIsEqual(content1, content2, spacerWidth, newLayout);
+
+        assertThat(MultiSlotLayout.fromLayoutElement(layout)).isEqualTo(layout);
+    }
+
+    @Test
+    public void testWrongElement() {
+        Column box = new Column.Builder().build();
+
+        assertThat(MultiSlotLayout.fromLayoutElement(box)).isNull();
+    }
+
+    @Test
+    public void testWrongBox() {
+        Box box = new Box.Builder().build();
+
+        assertThat(MultiSlotLayout.fromLayoutElement(box)).isNull();
+    }
+
+    @Test
+    public void testWrongTag() {
+        Box box =
+                new Box.Builder()
+                    .setModifiers(
+                        new Modifiers.Builder()
+                            .setMetadata(
+                                new ElementMetadata.Builder()
+                                    .setTagData("test".getBytes(UTF_8))
+                                    .build())
+                            .build())
+                        .build();
+
+        assertThat(MultiSlotLayout.fromLayoutElement(box)).isNull();
+    }
+
+    private void assertLayoutIsEqual(
+            LayoutElement content1,
+            LayoutElement content2,
+            float spacerWidth,
+            MultiSlotLayout layout) {
+        assertThat(layout.getSlotContents()).hasSize(2);
+        assertThat(layout.getMetadataTag()).isEqualTo(MultiSlotLayout.METADATA_TAG);
+        assertThat(layout.getSlotContents().get(0).toLayoutElementProto())
+                .isEqualTo(content1.toLayoutElementProto());
+        assertThat(layout.getSlotContents().get(1).toLayoutElementProto())
+                .isEqualTo(content2.toLayoutElementProto());
+        assertThat(layout.getHorizontalSpacerWidth()).isEqualTo(spacerWidth);
+    }
+}
diff --git a/wear/protolayout/protolayout-material/src/test/java/androidx/wear/protolayout/material/layouts/PrimaryLayoutTest.java b/wear/protolayout/protolayout-material/src/test/java/androidx/wear/protolayout/material/layouts/PrimaryLayoutTest.java
new file mode 100644
index 0000000..6669cda
--- /dev/null
+++ b/wear/protolayout/protolayout-material/src/test/java/androidx/wear/protolayout/material/layouts/PrimaryLayoutTest.java
@@ -0,0 +1,274 @@
+/*
+ * 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.wear.protolayout.material.layouts;
+
+import static androidx.wear.protolayout.material.layouts.LayoutDefaults.DEFAULT_VERTICAL_SPACER_HEIGHT;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import android.content.Context;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.wear.protolayout.ActionBuilders.LaunchAction;
+import androidx.wear.protolayout.DeviceParametersBuilders.DeviceParameters;
+import androidx.wear.protolayout.LayoutElementBuilders.Box;
+import androidx.wear.protolayout.LayoutElementBuilders.Column;
+import androidx.wear.protolayout.LayoutElementBuilders.LayoutElement;
+import androidx.wear.protolayout.ModifiersBuilders.Clickable;
+import androidx.wear.protolayout.ModifiersBuilders.ElementMetadata;
+import androidx.wear.protolayout.ModifiersBuilders.Modifiers;
+import androidx.wear.protolayout.material.CompactChip;
+import androidx.wear.protolayout.material.Text;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.annotation.internal.DoNotInstrument;
+
+@RunWith(AndroidJUnit4.class)
+@DoNotInstrument
+public class PrimaryLayoutTest {
+    private static final Clickable CLICKABLE =
+            new Clickable.Builder()
+                    .setOnClick(new LaunchAction.Builder().build())
+                    .setId("action_id")
+                    .build();
+    private static final Context CONTEXT = ApplicationProvider.getApplicationContext();
+    private static final DeviceParameters DEVICE_PARAMETERS =
+            new DeviceParameters.Builder().setScreenWidthDp(192).setScreenHeightDp(192).build();
+    private static final LayoutElement CONTENT = new Box.Builder().build();
+    private static final CompactChip PRIMARY_CHIP =
+            new CompactChip.Builder(CONTEXT, "Compact", CLICKABLE, DEVICE_PARAMETERS).build();
+    private static final Text PRIMARY_LABEL = new Text.Builder(CONTEXT, "Primary label").build();
+    private static final Text SECONDARY_LABEL =
+            new Text.Builder(CONTEXT, "Secondary label").build();
+
+    @Test
+    public void testOnlyContent() {
+        PrimaryLayout layout =
+                new PrimaryLayout.Builder(DEVICE_PARAMETERS).setContent(CONTENT).build();
+
+        assertLayout(DEFAULT_VERTICAL_SPACER_HEIGHT.getValue(), layout, CONTENT, null, null, null);
+    }
+
+    @Test
+    public void testContentChip() {
+        PrimaryLayout layout =
+                new PrimaryLayout.Builder(DEVICE_PARAMETERS)
+                        .setContent(CONTENT)
+                        .setPrimaryChipContent(PRIMARY_CHIP)
+                        .build();
+
+        assertLayout(
+                DEFAULT_VERTICAL_SPACER_HEIGHT.getValue(),
+                layout,
+                CONTENT,
+                PRIMARY_CHIP,
+                null,
+                null);
+    }
+
+    @Test
+    public void testContentPrimaryLabel() {
+        PrimaryLayout layout =
+                new PrimaryLayout.Builder(DEVICE_PARAMETERS)
+                        .setContent(CONTENT)
+                        .setPrimaryLabelTextContent(PRIMARY_LABEL)
+                        .build();
+
+        assertLayout(
+                DEFAULT_VERTICAL_SPACER_HEIGHT.getValue(),
+                layout,
+                CONTENT,
+                null,
+                PRIMARY_LABEL,
+                null);
+    }
+
+    @Test
+    public void testContentSecondaryLabel() {
+        PrimaryLayout layout =
+                new PrimaryLayout.Builder(DEVICE_PARAMETERS)
+                        .setContent(CONTENT)
+                        .setSecondaryLabelTextContent(SECONDARY_LABEL)
+                        .build();
+
+        assertLayout(
+                DEFAULT_VERTICAL_SPACER_HEIGHT.getValue(),
+                layout,
+                CONTENT,
+                null,
+                null,
+                SECONDARY_LABEL);
+    }
+
+    @Test
+    public void testAll() {
+        float height = 12;
+        PrimaryLayout layout =
+                new PrimaryLayout.Builder(DEVICE_PARAMETERS)
+                        .setContent(CONTENT)
+                        .setPrimaryChipContent(PRIMARY_CHIP)
+                        .setPrimaryLabelTextContent(PRIMARY_LABEL)
+                        .setSecondaryLabelTextContent(SECONDARY_LABEL)
+                        .setVerticalSpacerHeight(height)
+                        .build();
+
+        assertLayout(height, layout, CONTENT, PRIMARY_CHIP, PRIMARY_LABEL, SECONDARY_LABEL);
+    }
+
+    @Test
+    public void testWrongElement() {
+        Column box = new Column.Builder().build();
+
+        assertThat(PrimaryLayout.fromLayoutElement(box)).isNull();
+    }
+
+    @Test
+    public void testWrongBox() {
+        Box box = new Box.Builder().build();
+
+        assertThat(PrimaryLayout.fromLayoutElement(box)).isNull();
+    }
+
+    @Test
+    public void testWrongTag() {
+        Box box =
+                new Box.Builder()
+                        .setModifiers(
+                                new Modifiers.Builder()
+                                        .setMetadata(
+                                                new ElementMetadata.Builder()
+                                                        .setTagData("test".getBytes(UTF_8))
+                                                        .build())
+                                        .build())
+                        .build();
+
+        assertThat(PrimaryLayout.fromLayoutElement(box)).isNull();
+    }
+
+    @Test
+    public void testWrongLengthTag() {
+        Box box =
+                new Box.Builder()
+                        .setModifiers(
+                                new Modifiers.Builder()
+                                        .setMetadata(
+                                                new ElementMetadata.Builder()
+                                                        .setTagData(
+                                                                PrimaryLayout.METADATA_TAG_PREFIX
+                                                                        .getBytes(UTF_8))
+                                                        .build())
+                                        .build())
+                        .build();
+
+        assertThat(PrimaryLayout.fromLayoutElement(box)).isNull();
+    }
+
+    private void assertLayout(
+            float height,
+            @NonNull PrimaryLayout actualLayout,
+            @Nullable LayoutElement expectedContent,
+            @Nullable LayoutElement expectedPrimaryChip,
+            @Nullable LayoutElement expectedPrimaryLabel,
+            @Nullable LayoutElement expectedSecondaryLabel) {
+        assertLayoutIsEqual(
+                height,
+                actualLayout,
+                expectedContent,
+                expectedPrimaryChip,
+                expectedPrimaryLabel,
+                expectedSecondaryLabel);
+
+        Box box = new Box.Builder().addContent(actualLayout).build();
+
+        PrimaryLayout newLayout = PrimaryLayout.fromLayoutElement(box.getContents().get(0));
+
+        assertThat(newLayout).isNotNull();
+        assertLayoutIsEqual(
+                height,
+                newLayout,
+                expectedContent,
+                expectedPrimaryChip,
+                expectedPrimaryLabel,
+                expectedSecondaryLabel);
+
+        assertThat(PrimaryLayout.fromLayoutElement(actualLayout)).isEqualTo(actualLayout);
+    }
+
+    private void assertLayoutIsEqual(
+            float height,
+            @NonNull PrimaryLayout actualLayout,
+            @Nullable LayoutElement expectedContent,
+            @Nullable LayoutElement expectedPrimaryChip,
+            @Nullable LayoutElement expectedPrimaryLabel,
+            @Nullable LayoutElement expectedSecondaryLabel) {
+        byte[] expectedMetadata = PrimaryLayout.METADATA_TAG_BASE.clone();
+
+        if (expectedContent == null) {
+            assertThat(actualLayout.getContent()).isNull();
+        } else {
+            assertThat(actualLayout.getContent().toLayoutElementProto())
+                    .isEqualTo(expectedContent.toLayoutElementProto());
+            expectedMetadata[PrimaryLayout.FLAG_INDEX] =
+                    (byte)
+                            (expectedMetadata[PrimaryLayout.FLAG_INDEX]
+                                    | PrimaryLayout.CONTENT_PRESENT);
+        }
+
+        if (expectedPrimaryChip == null) {
+            assertThat(actualLayout.getPrimaryChipContent()).isNull();
+        } else {
+            assertThat(actualLayout.getPrimaryChipContent().toLayoutElementProto())
+                    .isEqualTo(expectedPrimaryChip.toLayoutElementProto());
+            expectedMetadata[PrimaryLayout.FLAG_INDEX] =
+                    (byte)
+                            (expectedMetadata[PrimaryLayout.FLAG_INDEX]
+                                    | PrimaryLayout.CHIP_PRESENT);
+        }
+
+        assertThat(actualLayout.getVerticalSpacerHeight()).isEqualTo(height);
+
+        if (expectedPrimaryLabel == null) {
+            assertThat(actualLayout.getPrimaryLabelTextContent()).isNull();
+        } else {
+            assertThat(actualLayout.getPrimaryLabelTextContent().toLayoutElementProto())
+                    .isEqualTo(expectedPrimaryLabel.toLayoutElementProto());
+            expectedMetadata[PrimaryLayout.FLAG_INDEX] =
+                    (byte)
+                            (expectedMetadata[PrimaryLayout.FLAG_INDEX]
+                                    | PrimaryLayout.PRIMARY_LABEL_PRESENT);
+        }
+
+        if (expectedSecondaryLabel == null) {
+            assertThat(actualLayout.getSecondaryLabelTextContent()).isNull();
+        } else {
+            assertThat(actualLayout.getSecondaryLabelTextContent().toLayoutElementProto())
+                    .isEqualTo(expectedSecondaryLabel.toLayoutElementProto());
+            expectedMetadata[PrimaryLayout.FLAG_INDEX] =
+                    (byte)
+                            (expectedMetadata[PrimaryLayout.FLAG_INDEX]
+                                    | PrimaryLayout.SECONDARY_LABEL_PRESENT);
+        }
+
+        assertThat(actualLayout.getMetadataTag()).isEqualTo(expectedMetadata);
+    }
+}
diff --git a/wear/watchface/watchface-complications-data/src/main/java/androidx/wear/watchface/complications/data/ComplicationDataExpressionEvaluator.kt b/wear/watchface/watchface-complications-data/src/main/java/androidx/wear/watchface/complications/data/ComplicationDataExpressionEvaluator.kt
index a4cb503..f496e85 100644
--- a/wear/watchface/watchface-complications-data/src/main/java/androidx/wear/watchface/complications/data/ComplicationDataExpressionEvaluator.kt
+++ b/wear/watchface/watchface-complications-data/src/main/java/androidx/wear/watchface/complications/data/ComplicationDataExpressionEvaluator.kt
@@ -29,12 +29,10 @@
 import androidx.wear.protolayout.expression.pipeline.sensor.SensorGateway
 import java.util.concurrent.Executor
 import kotlin.coroutines.ContinuationInterceptor
-import kotlin.coroutines.CoroutineContext
 import kotlinx.coroutines.CoroutineDispatcher
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.asCoroutineDispatcher
-import kotlinx.coroutines.asExecutor
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.asStateFlow
@@ -96,9 +94,10 @@
 
         /**
          * @see ComplicationDataExpressionEvaluator.init, [executor] is used in place of
-         *   `coroutineScope`.
+         *   `coroutineScope`, and defaults to an immediate main-thread [Executor].
          */
-        fun init(executor: Executor) {
+        @JvmOverloads
+        fun init(executor: Executor = CoroutineScope(Dispatchers.Main.immediate).asExecutor()) {
             evaluator.init(CoroutineScope(executor.asCoroutineDispatcher()))
             evaluator.data
                 .filterNotNull()
@@ -129,14 +128,12 @@
      *
      * This needs to be called exactly once.
      *
-     * @param coroutineScope used for background evaluation
+     * @param coroutineScope used for background evaluation, must be single threaded
      */
-    fun init(coroutineScope: CoroutineScope = CoroutineScope(Dispatchers.Main)) {
+    fun init(coroutineScope: CoroutineScope = CoroutineScope(Dispatchers.Main.immediate)) {
         // Add all the receivers before we start binding them because binding can synchronously
         // trigger the receiver, which would update the data before all the fields are evaluated.
-        initStateReceivers(coroutineScope)
-        initEvaluator()
-        monitorState(coroutineScope)
+        Initializer(coroutineScope).init()
     }
 
     /**
@@ -149,115 +146,115 @@
         if (this::evaluator.isInitialized) evaluator.close()
     }
 
-    /** Adds [ComplicationEvaluationResultReceiver]s to [state]. */
-    private fun initStateReceivers(coroutineScope: CoroutineScope) {
-        val receivers = mutableSetOf<ComplicationEvaluationResultReceiver<out Any>>()
-
-        if (unevaluatedData.hasRangedValueExpression()) {
-            unevaluatedData.rangedValueExpression
-                ?.buildReceiver(
-                    coroutineScope,
-                    expressionTrimmer = { setRangedValueExpression(null) },
-                    setter = { setRangedValue(it) },
-                )
-                ?.let { receivers += it }
-        }
-        if (unevaluatedData.hasLongText()) {
-            unevaluatedData.longText
-                ?.buildReceiver(coroutineScope) { setLongText(it) }
-                ?.let { receivers += it }
-        }
-        if (unevaluatedData.hasLongTitle()) {
-            unevaluatedData.longTitle
-                ?.buildReceiver(coroutineScope) { setLongTitle(it) }
-                ?.let { receivers += it }
-        }
-        if (unevaluatedData.hasShortText()) {
-            unevaluatedData.shortText
-                ?.buildReceiver(coroutineScope) { setShortText(it) }
-                ?.let { receivers += it }
-        }
-        if (unevaluatedData.hasShortTitle()) {
-            unevaluatedData.shortTitle
-                ?.buildReceiver(coroutineScope) { setShortTitle(it) }
-                ?.let { receivers += it }
-        }
-        if (unevaluatedData.hasContentDescription()) {
-            unevaluatedData.contentDescription
-                ?.buildReceiver(coroutineScope) { setContentDescription(it) }
-                ?.let { receivers += it }
+    private inner class Initializer(val coroutineScope: CoroutineScope) {
+        fun init() {
+            initStateReceivers()
+            initEvaluator()
+            monitorState()
         }
 
-        state.value = State(unevaluatedData, receivers)
-    }
+        /** Adds [ComplicationEvaluationResultReceiver]s to [state]. */
+        private fun initStateReceivers() {
+            val receivers = mutableSetOf<ComplicationEvaluationResultReceiver<out Any>>()
 
-    private fun DynamicFloat.buildReceiver(
-        coroutineScope: CoroutineScope,
-        expressionTrimmer: WireComplicationData.Builder.() -> WireComplicationData.Builder,
-        setter: WireComplicationData.Builder.(Float) -> WireComplicationData.Builder,
-    ) =
-        ComplicationEvaluationResultReceiver(
-            setter = {
-                if (!keepExpression) expressionTrimmer(this)
-                setter(this, it)
-            },
-            binder = { receiver ->
-                evaluator.bind(
-                    this@buildReceiver,
-                    coroutineScope.coroutineContext.asExecutor(),
-                    receiver
-                )
-            },
-        )
-
-    private fun WireComplicationText.buildReceiver(
-        coroutineScope: CoroutineScope,
-        setter: WireComplicationData.Builder.(WireComplicationText) -> WireComplicationData.Builder,
-    ) =
-        expression?.let { expression ->
-            ComplicationEvaluationResultReceiver<String>(
-                setter = {
-                    setter(
-                        if (keepExpression) {
-                            WireComplicationText(it, expression)
-                        } else {
-                            WireComplicationText(it)
-                        }
+            if (unevaluatedData.hasRangedValueExpression()) {
+                unevaluatedData.rangedValueExpression
+                    ?.buildReceiver(
+                        expressionTrimmer = { setRangedValueExpression(null) },
+                        setter = { setRangedValue(it) },
                     )
+                    ?.let { receivers += it }
+            }
+            if (unevaluatedData.hasLongText()) {
+                unevaluatedData.longText?.buildReceiver { setLongText(it) }?.let { receivers += it }
+            }
+            if (unevaluatedData.hasLongTitle()) {
+                unevaluatedData.longTitle
+                    ?.buildReceiver { setLongTitle(it) }
+                    ?.let { receivers += it }
+            }
+            if (unevaluatedData.hasShortText()) {
+                unevaluatedData.shortText
+                    ?.buildReceiver { setShortText(it) }
+                    ?.let { receivers += it }
+            }
+            if (unevaluatedData.hasShortTitle()) {
+                unevaluatedData.shortTitle
+                    ?.buildReceiver { setShortTitle(it) }
+                    ?.let { receivers += it }
+            }
+            if (unevaluatedData.hasContentDescription()) {
+                unevaluatedData.contentDescription
+                    ?.buildReceiver { setContentDescription(it) }
+                    ?.let { receivers += it }
+            }
+
+            state.value = State(unevaluatedData, receivers)
+        }
+
+        private fun DynamicFloat.buildReceiver(
+            expressionTrimmer: WireComplicationData.Builder.() -> WireComplicationData.Builder,
+            setter: WireComplicationData.Builder.(Float) -> WireComplicationData.Builder,
+        ) =
+            ComplicationEvaluationResultReceiver(
+                setter = {
+                    if (!keepExpression) expressionTrimmer(this)
+                    setter(this, it)
                 },
                 binder = { receiver ->
-                    evaluator.bind(
-                        expression,
-                        ULocale.getDefault(),
-                        coroutineScope.coroutineContext.asExecutor(),
-                        receiver
-                    )
+                    evaluator.bind(this@buildReceiver, coroutineScope.asExecutor(), receiver)
                 },
             )
+
+        private fun WireComplicationText.buildReceiver(
+            setter:
+                WireComplicationData.Builder.(WireComplicationText) -> WireComplicationData.Builder,
+        ) =
+            expression?.let { expression ->
+                ComplicationEvaluationResultReceiver<String>(
+                    setter = {
+                        setter(
+                            if (keepExpression) {
+                                WireComplicationText(it, expression)
+                            } else {
+                                WireComplicationText(it)
+                            }
+                        )
+                    },
+                    binder = { receiver ->
+                        evaluator.bind(
+                            expression,
+                            ULocale.getDefault(),
+                            coroutineScope.asExecutor(),
+                            receiver
+                        )
+                    },
+                )
+            }
+
+        /** Initializes the internal [DynamicTypeEvaluator] if there are pending receivers. */
+        private fun initEvaluator() {
+            if (state.value.pending.isEmpty()) return
+            evaluator =
+                DynamicTypeEvaluator(
+                    /* platformDataSourcesInitiallyEnabled = */ true,
+                    sensorGateway,
+                    stateStore,
+                )
+            for (receiver in state.value.pending) receiver.bind()
+            for (receiver in state.value.pending) receiver.startEvaluation()
+            evaluator.enablePlatformDataSources()
         }
 
-    /** Initializes the internal [DynamicTypeEvaluator] if there are pending receivers. */
-    private fun initEvaluator() {
-        if (state.value.pending.isEmpty()) return
-        evaluator =
-            DynamicTypeEvaluator(
-                /* platformDataSourcesInitiallyEnabled = */ true,
-                sensorGateway,
-                stateStore,
-            )
-        for (receiver in state.value.pending) receiver.bind()
-        for (receiver in state.value.pending) receiver.startEvaluation()
-        evaluator.enablePlatformDataSources()
-    }
-
-    /** Monitors [state] changes and updates [data]. */
-    private fun monitorState(coroutineScope: CoroutineScope) {
-        state
-            .onEach {
-                if (it.invalid.isNotEmpty()) _data.value = INVALID_DATA
-                else if (it.pending.isEmpty() && it.preUpdateCount == 0) _data.value = it.data
-            }
-            .launchIn(coroutineScope)
+        /** Monitors [state] changes and updates [data]. */
+        private fun monitorState() {
+            state
+                .onEach {
+                    if (it.invalid.isNotEmpty()) _data.value = INVALID_DATA
+                    else if (it.pending.isEmpty() && it.preUpdateCount == 0) _data.value = it.data
+                }
+                .launchIn(coroutineScope)
+        }
     }
 
     /**
@@ -359,5 +356,15 @@
     }
 }
 
-internal fun CoroutineContext.asExecutor(): Executor =
-    (get(ContinuationInterceptor) as CoroutineDispatcher).asExecutor()
+/**
+ * Replacement for CoroutineDispatcher.asExecutor extension due to
+ * https://github.com/Kotlin/kotlinx.coroutines/pull/3683.
+ */
+internal fun CoroutineScope.asExecutor() = Executor { runnable ->
+    val dispatcher = coroutineContext[ContinuationInterceptor] as CoroutineDispatcher
+    if (dispatcher.isDispatchNeeded(coroutineContext)) {
+        dispatcher.dispatch(coroutineContext, runnable)
+    } else {
+        runnable.run()
+    }
+}
diff --git a/wear/watchface/watchface-complications-data/src/test/java/androidx/wear/watchface/complications/data/ComplicationDataExpressionEvaluatorTest.kt b/wear/watchface/watchface-complications-data/src/test/java/androidx/wear/watchface/complications/data/ComplicationDataExpressionEvaluatorTest.kt
index 279a8b0..1ee050f 100644
--- a/wear/watchface/watchface-complications-data/src/test/java/androidx/wear/watchface/complications/data/ComplicationDataExpressionEvaluatorTest.kt
+++ b/wear/watchface/watchface-complications-data/src/test/java/androidx/wear/watchface/complications/data/ComplicationDataExpressionEvaluatorTest.kt
@@ -16,12 +16,12 @@
 
 package androidx.wear.watchface.complications.data
 
+import android.os.Handler
+import android.os.Looper
 import android.support.wearable.complications.ComplicationData as WireComplicationData
 import android.support.wearable.complications.ComplicationText as WireComplicationText
 import android.util.Log
-import androidx.core.content.ContextCompat
 import androidx.core.util.Consumer
-import androidx.test.core.app.ApplicationProvider.getApplicationContext
 import androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat
 import androidx.wear.protolayout.expression.DynamicBuilders.DynamicString
 import androidx.wear.protolayout.expression.StateEntryBuilders.StateEntryValue
@@ -30,6 +30,7 @@
 import androidx.wear.watchface.complications.data.ComplicationDataExpressionEvaluator.Companion.hasExpression
 import com.google.common.truth.Expect
 import com.google.common.truth.Truth.assertThat
+import java.util.concurrent.Executor
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.flow.SharingStarted
@@ -45,7 +46,6 @@
 import org.mockito.kotlin.times
 import org.mockito.kotlin.verify
 import org.robolectric.shadows.ShadowLog
-import org.robolectric.shadows.ShadowLooper.runUiThreadTasks
 
 @RunWith(SharedRobolectricTestRunner::class)
 class ComplicationDataExpressionEvaluatorTest {
@@ -69,7 +69,6 @@
     fun data_noExpression_setToUnevaluated() {
         ComplicationDataExpressionEvaluator(DATA_WITH_NO_EXPRESSION).use { evaluator ->
             evaluator.init()
-            runUiThreadTasks()
 
             assertThat(evaluator.data.value).isEqualTo(DATA_WITH_NO_EXPRESSION)
         }
@@ -219,16 +218,14 @@
                     evaluator.data
                         .filterNotNull()
                         .shareIn(
-                            CoroutineScope(Dispatchers.Main),
+                            CoroutineScope(Dispatchers.Main.immediate),
                             SharingStarted.Eagerly,
                             replay = 10,
                         )
                 evaluator.init()
-                runUiThreadTasks() // Ensures data sharing started.
 
                 for (state in scenario.states) {
                     stateStore.setStateEntryValues(state)
-                    runUiThreadTasks() // Ensures data sharing ended.
                 }
 
                 expect
@@ -241,6 +238,17 @@
 
     @Test
     fun data_keepExpression_doesNotTrimUnevaluatedExpression() {
+        class MainExecutor : Executor {
+            private val handler = Handler(Looper.getMainLooper())
+
+            override fun execute(runnable: Runnable) {
+                if (handler.looper == Looper.myLooper()) {
+                    runnable.run()
+                } else {
+                    handler.post(runnable)
+                }
+            }
+        }
         val expressed =
             WireComplicationData.Builder(WireComplicationData.TYPE_NO_DATA)
                 .setRangedValueExpression(DynamicFloat.constant(1f))
@@ -252,7 +260,6 @@
                 .build()
         ComplicationDataExpressionEvaluator(expressed, keepExpression = true).use { evaluator ->
             evaluator.init()
-            runUiThreadTasks()
 
             assertThat(evaluator.data.value)
                 .isEqualTo(
@@ -334,11 +341,7 @@
     fun compat_notInitialized_listenerNotInvoked() {
         ComplicationDataExpressionEvaluator.Compat.Builder(DATA_WITH_NO_EXPRESSION, listener)
             .build()
-            .use {
-                runUiThreadTasks()
-
-                verify(listener, never()).accept(any())
-            }
+            .use { verify(listener, never()).accept(any()) }
     }
 
     @Test
@@ -346,13 +349,31 @@
         ComplicationDataExpressionEvaluator.Compat.Builder(DATA_WITH_NO_EXPRESSION, listener)
             .build()
             .use { evaluator ->
-                evaluator.init(ContextCompat.getMainExecutor(getApplicationContext()))
-                runUiThreadTasks()
+                evaluator.init()
 
                 verify(listener, times(1)).accept(DATA_WITH_NO_EXPRESSION)
             }
     }
 
+    @Test
+    fun compat_expression_listenerInvokedWithEvaluatedData() {
+        val data =
+            WireComplicationData.Builder(WireComplicationData.TYPE_NO_DATA)
+                .setRangedValueExpression(DynamicFloat.constant(1f))
+                .build()
+        ComplicationDataExpressionEvaluator.Compat.Builder(data, listener).build().use { evaluator
+            ->
+            evaluator.init()
+
+            verify(listener, times(1))
+                .accept(
+                    WireComplicationData.Builder(WireComplicationData.TYPE_NO_DATA)
+                        .setRangedValue(1f)
+                        .build()
+                )
+        }
+    }
+
     private companion object {
         val DATA_WITH_NO_EXPRESSION =
             WireComplicationData.Builder(WireComplicationData.TYPE_NO_DATA)
diff --git a/window/extensions/extensions/build.gradle b/window/extensions/extensions/build.gradle
index 6d72848..bccb7ae 100644
--- a/window/extensions/extensions/build.gradle
+++ b/window/extensions/extensions/build.gradle
@@ -26,7 +26,7 @@
     api(libs.kotlinStdlib)
     implementation("androidx.annotation:annotation:1.3.0")
     implementation("androidx.annotation:annotation-experimental:1.1.0")
-    implementation(project(":window:extensions:core:core"))
+    implementation("androidx.window.extensions.core:core:1.0.0-beta01")
 
     testImplementation(libs.robolectric)
     testImplementation(libs.testExtJunit)
diff --git a/window/window/build.gradle b/window/window/build.gradle
index 23e165d..32d38d8 100644
--- a/window/window/build.gradle
+++ b/window/window/build.gradle
@@ -48,10 +48,10 @@
     implementation("androidx.annotation:annotation:1.3.0")
     implementation("androidx.collection:collection:1.1.0")
     implementation("androidx.core:core:1.8.0")
-    implementation(project(":window:extensions:core:core"))
 
+    implementation("androidx.window.extensions.core:core:1.0.0-beta01")
     compileOnly(project(":window:sidecar:sidecar"))
-    compileOnly(project(":window:extensions:extensions"))
+    compileOnly("androidx.window.extensions:extensions:1.1.0-beta01")
 
     testImplementation(libs.testCore)
     testImplementation(libs.testRunner)
@@ -61,9 +61,9 @@
     testImplementation(libs.mockitoCore4)
     testImplementation(libs.mockitoKotlin4)
     testImplementation(libs.kotlinCoroutinesTest)
+    testImplementation("androidx.window.extensions.core:core:1.0.0-beta01")
     testImplementation(compileOnly(project(":window:sidecar:sidecar")))
-    testImplementation(compileOnly(project(":window:extensions:extensions")))
-    testImplementation(implementation(project(":window:extensions:core:core")))
+    testImplementation(compileOnly("androidx.window.extensions:extensions:1.1.0-beta01"))
 
     androidTestImplementation(libs.testCore)
     androidTestImplementation(libs.kotlinTestJunit)
@@ -77,9 +77,9 @@
     androidTestImplementation(libs.multidex)
     androidTestImplementation(libs.truth)
     androidTestImplementation(libs.junit) // Needed for Assert.assertThrows
-    androidTestImplementation(compileOnly(project(":window:extensions:extensions")))
+    androidTestImplementation("androidx.window.extensions.core:core:1.0.0-beta01")
     androidTestImplementation(compileOnly(project(":window:sidecar:sidecar")))
-    androidTestImplementation(implementation(project(":window:extensions:core:core")))
+    androidTestImplementation(compileOnly("androidx.window.extensions:extensions:1.1.0-beta01"))
 }
 
 androidx {