Merge "Add test log" into androidx-main
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/FtlRunner.kt b/buildSrc/private/src/main/kotlin/androidx/build/FtlRunner.kt
index b805725..5c45e4e 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/FtlRunner.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/FtlRunner.kt
@@ -83,6 +83,14 @@
     @get:Option(option = "pullScreenshots", description = "true if screenshots should be pulled")
     abstract val pullScreenshots: Property<String>
 
+    @get:Optional
+    @get:Input
+    @get:Option(
+        option = "instrumentationArgs",
+        description = "instrumentation arguments to pass to FTL test runner"
+    )
+    abstract val instrumentationArgs: Property<String>
+
     @get:Input
     abstract val device: Property<String>
 
@@ -142,6 +150,8 @@
                     if (shouldPull) {
                         "/sdcard/Android/data/${apkPackageName.get()}/cache/androidx_screenshots"
                     } else null,
+                    if (instrumentationArgs.isPresent) "--environment-variables" else null,
+                    if (instrumentationArgs.isPresent) instrumentationArgs.get() else null,
                 )
             )
         }
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/params/DynamicRangesCompat.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/params/DynamicRangesCompat.java
new file mode 100644
index 0000000..e7d3d64
--- /dev/null
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/params/DynamicRangesCompat.java
@@ -0,0 +1,166 @@
+/*
+ * 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.camera2.internal.compat.params;
+
+import android.hardware.camera2.CameraCharacteristics;
+import android.hardware.camera2.params.DynamicRangeProfiles;
+import android.os.Build;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+import androidx.camera.camera2.internal.compat.CameraCharacteristicsCompat;
+import androidx.camera.core.DynamicRange;
+import androidx.core.util.Preconditions;
+
+import java.util.Set;
+
+/**
+ * Helper for accessing features in DynamicRangeProfiles in a backwards compatible fashion.
+ */
+@RequiresApi(21)
+public final class DynamicRangesCompat {
+
+    private final DynamicRangeProfilesCompatImpl mImpl;
+
+    DynamicRangesCompat(@NonNull DynamicRangeProfilesCompatImpl impl) {
+        mImpl = impl;
+    }
+
+    /**
+     * Returns a set of supported {@link DynamicRange} that can be referenced in a single
+     * capture request.
+     *
+     * <p>For example if a particular 10-bit output capable device returns (STANDARD,
+     * HLG10, HDR10) as result from calling {@link #getSupportedDynamicRanges()} and
+     * getProfileCaptureRequestConstraints(long) returns (STANDARD, HLG10) when given an argument
+     * of STANDARD. This means that the corresponding camera device will only accept and process
+     * capture requests that reference outputs configured using HDR10 dynamic range or
+     * alternatively some combination of STANDARD and HLG10. However trying to queue capture
+     * requests to outputs that reference both HDR10 and STANDARD/HLG10 will result in
+     * IllegalArgumentException.
+     *
+     * <p>The list will be empty in case there are no constraints for the given dynamic range.
+     *
+     * @param dynamicRange The dynamic range that will be checked for constraints
+     * @return non-modifiable set of dynamic ranges
+     * @throws IllegalArgumentException If the dynamic range argument is not within the set
+     * returned by {@link #getSupportedDynamicRanges()}.
+     */
+    @NonNull
+    public Set<DynamicRange> getDynamicRangeCaptureRequestConstraints(
+            @NonNull DynamicRange dynamicRange) {
+        return mImpl.getDynamicRangeCaptureRequestConstraints(dynamicRange);
+    }
+
+    /**
+     * Returns a set of supported dynamic ranges.
+     *
+     * @return a non-modifiable set of dynamic ranges.
+     */
+    @NonNull
+    public Set<DynamicRange> getSupportedDynamicRanges() {
+        return mImpl.getSupportedDynamicRanges();
+    }
+
+    /**
+     * Checks whether a given dynamic range is suitable for latency sensitive use cases.
+     *
+     * <p>Due to internal lookahead logic, camera outputs configured with some dynamic range
+     * profiles may experience additional latency greater than 3 buffers. Using camera outputs
+     * with such dynamic ranges for latency sensitive use cases such as camera preview is not
+     * recommended. Dynamic ranges that have such extra streaming delay are typically utilized for
+     * scenarios such as offscreen video recording.
+     *
+     * @param dynamicRange The dynamic range to check for extra latency
+     * @return {@code true} if the given profile is not suitable for latency sensitive use cases,
+     * {@code false} otherwise.
+     * @throws IllegalArgumentException If the dynamic range argument is not within the set
+     * returned by {@link #getSupportedDynamicRanges()}.
+     */
+    public boolean isExtraLatencyPresent(@NonNull DynamicRange dynamicRange) {
+        return mImpl.isExtraLatencyPresent(dynamicRange);
+    }
+
+    /**
+     * Returns a {@link DynamicRangesCompat} using the capabilities derived from the provided
+     * characteristics.
+     *
+     * @param characteristics the characteristics used to derive dynamic range information.
+     * @return a {@link DynamicRangesCompat} object.
+     */
+    @NonNull
+    public static DynamicRangesCompat fromCameraCharacteristics(
+            @NonNull CameraCharacteristicsCompat characteristics) {
+        DynamicRangesCompat rangesCompat = null;
+        if (Build.VERSION.SDK_INT >= 33) {
+            rangesCompat = toDynamicRangesCompat(characteristics.get(
+                    CameraCharacteristics.REQUEST_AVAILABLE_DYNAMIC_RANGE_PROFILES));
+        }
+
+        return (rangesCompat == null) ? DynamicRangesCompatBaseImpl.COMPAT_INSTANCE : rangesCompat;
+    }
+
+    /**
+     * Creates an instance from a framework android.hardware.camera2.params.DynamicRangeProfiles
+     * object.
+     *
+     * @param dynamicRangeProfiles a {@link android.hardware.camera2.params.DynamicRangeProfiles).
+     * @return an equivalent {@link DynamicRangesCompat} object.
+     */
+    @Nullable
+    @RequiresApi(33)
+    public static DynamicRangesCompat toDynamicRangesCompat(
+            @Nullable DynamicRangeProfiles dynamicRangeProfiles) {
+        if (dynamicRangeProfiles == null) {
+            return null;
+        }
+
+        Preconditions.checkState(Build.VERSION.SDK_INT >= 33, "DynamicRangeProfiles can only be "
+                + "converted to DynamicRangesCompat on API 33 or higher.");
+
+        return new DynamicRangesCompat(new DynamicRangesCompatApi33Impl(dynamicRangeProfiles));
+    }
+
+    /**
+     * Returns the underlying framework
+     * {@link android.hardware.camera2.params.DynamicRangeProfiles).
+     *
+     * @return the underlying {@link android.hardware.camera2.params.DynamicRangeProfiles).
+     */
+    @NonNull
+    @RequiresApi(33)
+    public DynamicRangeProfiles toDynamicRangeProfiles() {
+        Preconditions.checkState(Build.VERSION.SDK_INT >= 33, "DynamicRangesCompat can only be "
+                + "converted to DynamicRangeProfiles on API 33 or higher.");
+        return Preconditions.checkNotNull(mImpl.unwrap());
+    }
+
+    interface DynamicRangeProfilesCompatImpl {
+        @NonNull
+        Set<DynamicRange> getDynamicRangeCaptureRequestConstraints(
+                @NonNull DynamicRange dynamicRange);
+
+        @NonNull
+        Set<DynamicRange> getSupportedDynamicRanges();
+
+        boolean isExtraLatencyPresent(@NonNull DynamicRange dynamicRange);
+
+        @Nullable
+        DynamicRangeProfiles unwrap();
+    }
+}
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/params/DynamicRangesCompatApi33Impl.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/params/DynamicRangesCompatApi33Impl.java
new file mode 100644
index 0000000..1c9a492
--- /dev/null
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/params/DynamicRangesCompatApi33Impl.java
@@ -0,0 +1,171 @@
+/*
+ * 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.camera2.internal.compat.params;
+
+import static android.hardware.camera2.params.DynamicRangeProfiles.DOLBY_VISION_10B_HDR_OEM;
+import static android.hardware.camera2.params.DynamicRangeProfiles.DOLBY_VISION_10B_HDR_OEM_PO;
+import static android.hardware.camera2.params.DynamicRangeProfiles.DOLBY_VISION_10B_HDR_REF;
+import static android.hardware.camera2.params.DynamicRangeProfiles.DOLBY_VISION_10B_HDR_REF_PO;
+import static android.hardware.camera2.params.DynamicRangeProfiles.DOLBY_VISION_8B_HDR_OEM;
+import static android.hardware.camera2.params.DynamicRangeProfiles.DOLBY_VISION_8B_HDR_OEM_PO;
+import static android.hardware.camera2.params.DynamicRangeProfiles.DOLBY_VISION_8B_HDR_REF;
+import static android.hardware.camera2.params.DynamicRangeProfiles.DOLBY_VISION_8B_HDR_REF_PO;
+import static android.hardware.camera2.params.DynamicRangeProfiles.HDR10;
+import static android.hardware.camera2.params.DynamicRangeProfiles.HDR10_PLUS;
+import static android.hardware.camera2.params.DynamicRangeProfiles.HLG10;
+import static android.hardware.camera2.params.DynamicRangeProfiles.STANDARD;
+
+import android.hardware.camera2.params.DynamicRangeProfiles;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+import androidx.camera.core.DynamicRange;
+import androidx.core.util.Preconditions;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+@RequiresApi(33)
+class DynamicRangesCompatApi33Impl implements DynamicRangesCompat.DynamicRangeProfilesCompatImpl {
+    private static final Map<Long, DynamicRange> PROFILE_TO_DR_MAP = new HashMap<>();
+    private static final Map<DynamicRange, List<Long>> DR_TO_PROFILE_MAP = new HashMap<>();
+
+    static {
+        // SDR
+        PROFILE_TO_DR_MAP.put(STANDARD, DynamicRange.SDR);
+        DR_TO_PROFILE_MAP.put(DynamicRange.SDR, Collections.singletonList(STANDARD));
+
+        // HLG
+        PROFILE_TO_DR_MAP.put(HLG10,
+                new DynamicRange(DynamicRange.FORMAT_HLG, DynamicRange.BIT_DEPTH_10_BIT));
+        DR_TO_PROFILE_MAP.put(PROFILE_TO_DR_MAP.get(HLG10), Collections.singletonList(HLG10));
+
+        // HDR10
+        DynamicRange hdr10 = new DynamicRange(DynamicRange.FORMAT_HDR10,
+                DynamicRange.BIT_DEPTH_10_BIT);
+        PROFILE_TO_DR_MAP.put(HDR10, hdr10);
+        DR_TO_PROFILE_MAP.put(hdr10, Collections.singletonList(HDR10));
+
+        // HDR10+
+        DynamicRange hdr10Plus = new DynamicRange(DynamicRange.FORMAT_HDR10_PLUS,
+                DynamicRange.BIT_DEPTH_10_BIT);
+        PROFILE_TO_DR_MAP.put(HDR10_PLUS, hdr10Plus);
+        DR_TO_PROFILE_MAP.put(hdr10Plus, Collections.singletonList(HDR10_PLUS));
+
+        // Dolby Vision 10-bit
+        DynamicRange dolbyVision10Bit = new DynamicRange(DynamicRange.FORMAT_DOLBY_VISION,
+                DynamicRange.BIT_DEPTH_10_BIT);
+        // A list of the Camera2 10-bit dolby vision profiles ordered by priority. Any API that
+        // takes a DynamicRange with dolby vision format will attempt to convert to these
+        // profiles in order, using the first one that is supported. We will need to add a
+        // mechanism for choosing between these
+        List<Long> dolbyVision10BitProfilesOrdered = Arrays.asList(DOLBY_VISION_10B_HDR_OEM,
+                DOLBY_VISION_10B_HDR_OEM_PO, DOLBY_VISION_10B_HDR_REF, DOLBY_VISION_10B_HDR_REF_PO);
+        for (Long profile : dolbyVision10BitProfilesOrdered) {
+            PROFILE_TO_DR_MAP.put(profile, dolbyVision10Bit);
+        }
+        DR_TO_PROFILE_MAP.put(dolbyVision10Bit, dolbyVision10BitProfilesOrdered);
+
+        // Dolby vision 8-bit
+        DynamicRange dolbyVision8Bit = new DynamicRange(DynamicRange.FORMAT_DOLBY_VISION,
+                DynamicRange.BIT_DEPTH_8_BIT);
+        List<Long> dolbyVision8BitProfilesOrdered = Arrays.asList(DOLBY_VISION_8B_HDR_OEM,
+                DOLBY_VISION_8B_HDR_OEM_PO, DOLBY_VISION_8B_HDR_REF, DOLBY_VISION_8B_HDR_REF_PO);
+        for (Long profile : dolbyVision8BitProfilesOrdered) {
+            PROFILE_TO_DR_MAP.put(profile, dolbyVision8Bit);
+        }
+        DR_TO_PROFILE_MAP.put(dolbyVision8Bit, dolbyVision8BitProfilesOrdered);
+    }
+
+    private final DynamicRangeProfiles mDynamicRangeProfiles;
+
+    DynamicRangesCompatApi33Impl(@NonNull Object dynamicRangeProfiles) {
+        mDynamicRangeProfiles = (DynamicRangeProfiles) dynamicRangeProfiles;
+    }
+
+    @NonNull
+    @Override
+    public Set<DynamicRange> getDynamicRangeCaptureRequestConstraints(
+            @NonNull DynamicRange dynamicRange) {
+        Long dynamicRangeProfile = dynamicRangeToFirstSupportedProfile(dynamicRange);
+        Preconditions.checkArgument(dynamicRangeProfile != null,
+                "DynamicRange is not supported: " + dynamicRange);
+        return profileSetToDynamicRangeSet(
+                mDynamicRangeProfiles.getProfileCaptureRequestConstraints(dynamicRangeProfile));
+    }
+
+    @NonNull
+    @Override
+    public Set<DynamicRange> getSupportedDynamicRanges() {
+        return profileSetToDynamicRangeSet(mDynamicRangeProfiles.getSupportedProfiles());
+    }
+
+    @Override
+    public boolean isExtraLatencyPresent(@NonNull DynamicRange dynamicRange) {
+        Long dynamicRangeProfile = dynamicRangeToFirstSupportedProfile(dynamicRange);
+        Preconditions.checkArgument(dynamicRangeProfile != null,
+                "DynamicRange is not supported: " + dynamicRange);
+        return mDynamicRangeProfiles.isExtraLatencyPresent(dynamicRangeProfile);
+    }
+
+    @Nullable
+    @Override
+    public DynamicRangeProfiles unwrap() {
+        return mDynamicRangeProfiles;
+    }
+
+    @NonNull
+    private static DynamicRange profileToDynamicRange(long profile) {
+        return Preconditions.checkNotNull(PROFILE_TO_DR_MAP.get(profile),
+                "Dynamic range profile cannot be converted to a DynamicRange object: " + profile);
+    }
+
+    @Nullable
+    private Long dynamicRangeToFirstSupportedProfile(@NonNull DynamicRange dynamicRange) {
+        List<Long> orderedProfiles =
+                Preconditions.checkNotNull(DR_TO_PROFILE_MAP.get(dynamicRange),
+                        "DynamicRange object does not have an associated camera2 profile: "
+                                + dynamicRange);
+        Set<Long> supportedList = mDynamicRangeProfiles.getSupportedProfiles();
+        for (Long profile : orderedProfiles) {
+            if (supportedList.contains(profile)) {
+                return profile;
+            }
+        }
+
+        // No profile supported
+        return null;
+    }
+
+    @NonNull
+    private static Set<DynamicRange> profileSetToDynamicRangeSet(@NonNull Set<Long> profileSet) {
+        if (profileSet.isEmpty()) {
+            return Collections.emptySet();
+        }
+        Set<DynamicRange> dynamicRangeSet = new HashSet<>(profileSet.size());
+        for (long profile : profileSet) {
+            dynamicRangeSet.add(profileToDynamicRange(profile));
+        }
+        return Collections.unmodifiableSet(dynamicRangeSet);
+    }
+}
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/params/DynamicRangesCompatBaseImpl.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/params/DynamicRangesCompatBaseImpl.java
new file mode 100644
index 0000000..312978f
--- /dev/null
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/params/DynamicRangesCompatBaseImpl.java
@@ -0,0 +1,64 @@
+/*
+ * 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.camera2.internal.compat.params;
+
+import android.hardware.camera2.params.DynamicRangeProfiles;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+import androidx.camera.core.DynamicRange;
+import androidx.core.util.Preconditions;
+
+import java.util.Collections;
+import java.util.Set;
+
+@RequiresApi(21)
+class DynamicRangesCompatBaseImpl implements DynamicRangesCompat.DynamicRangeProfilesCompatImpl {
+
+    static final DynamicRangesCompat COMPAT_INSTANCE =
+            new DynamicRangesCompat(new DynamicRangesCompatBaseImpl());
+
+    private static final Set<DynamicRange> SDR_ONLY = Collections.singleton(DynamicRange.SDR);
+    @NonNull
+    @Override
+    public Set<DynamicRange> getDynamicRangeCaptureRequestConstraints(
+            @NonNull DynamicRange dynamicRange) {
+        Preconditions.checkArgument(DynamicRange.SDR.equals(dynamicRange),
+                "DynamicRange is not supported: " + dynamicRange);
+        return SDR_ONLY;
+    }
+
+    @NonNull
+    @Override
+    public Set<DynamicRange> getSupportedDynamicRanges() {
+        return SDR_ONLY;
+    }
+
+    @Override
+    public boolean isExtraLatencyPresent(@NonNull DynamicRange dynamicRange) {
+        Preconditions.checkArgument(DynamicRange.SDR.equals(dynamicRange),
+                "DynamicRange is not supported: " + dynamicRange);
+        return false;
+    }
+
+    @Nullable
+    @Override
+    public DynamicRangeProfiles unwrap() {
+        return null;
+    }
+}
diff --git a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/compat/params/DynamicRangesCompatTest.kt b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/compat/params/DynamicRangesCompatTest.kt
new file mode 100644
index 0000000..686debe
--- /dev/null
+++ b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/compat/params/DynamicRangesCompatTest.kt
@@ -0,0 +1,360 @@
+/*
+ * 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.internal.compat.params
+
+import android.hardware.camera2.CameraCharacteristics
+import android.hardware.camera2.params.DynamicRangeProfiles
+import android.hardware.camera2.params.DynamicRangeProfiles.DOLBY_VISION_10B_HDR_OEM
+import android.hardware.camera2.params.DynamicRangeProfiles.DOLBY_VISION_8B_HDR_OEM
+import android.hardware.camera2.params.DynamicRangeProfiles.HDR10
+import android.hardware.camera2.params.DynamicRangeProfiles.HDR10_PLUS
+import android.hardware.camera2.params.DynamicRangeProfiles.HLG10
+import android.hardware.camera2.params.DynamicRangeProfiles.STANDARD
+import android.os.Build
+import androidx.annotation.RequiresApi
+import androidx.camera.camera2.internal.compat.CameraCharacteristicsCompat
+import androidx.camera.core.DynamicRange
+import androidx.camera.core.DynamicRange.BIT_DEPTH_10_BIT
+import androidx.camera.core.DynamicRange.BIT_DEPTH_8_BIT
+import androidx.camera.core.DynamicRange.FORMAT_DOLBY_VISION
+import androidx.camera.core.DynamicRange.FORMAT_HDR10
+import androidx.camera.core.DynamicRange.FORMAT_HDR10_PLUS
+import androidx.camera.core.DynamicRange.FORMAT_HLG
+import androidx.camera.core.DynamicRange.SDR
+import com.google.common.truth.Truth.assertThat
+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
+import org.robolectric.shadow.api.Shadow
+import org.robolectric.shadows.ShadowCameraCharacteristics
+
+@RunWith(RobolectricTestRunner::class)
+@DoNotInstrument
+@Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
+class DynamicRangesCompatTest {
+
+    companion object {
+        val HLG10_UNCONSTRAINED by lazy {
+            DynamicRangeProfiles(longArrayOf(HLG10, 0, 0))
+        }
+
+        val HLG10_CONSTRAINED by lazy {
+            DynamicRangeProfiles(
+                longArrayOf(
+                    HLG10, HLG10, LATENCY_NONE
+                )
+            )
+        }
+
+        val HLG10_SDR_CONSTRAINED by lazy {
+            DynamicRangeProfiles(
+                longArrayOf(
+                    HLG10, HLG10 or STANDARD, LATENCY_NONE
+                )
+            )
+        }
+
+        val HLG10_HDR10_CONSTRAINED by lazy {
+            DynamicRangeProfiles(
+                longArrayOf(
+                    HLG10, HLG10 or HDR10, LATENCY_NONE,
+                    HDR10, HDR10 or HLG10, LATENCY_NONE
+                )
+            )
+        }
+
+        val HDR10_UNCONSTRAINED by lazy {
+            DynamicRangeProfiles(
+                longArrayOf(
+                    HLG10, CONSTRAINTS_NONE, LATENCY_NONE, // HLG is mandated
+                    HDR10, CONSTRAINTS_NONE, LATENCY_NONE
+                )
+            )
+        }
+
+        val HDR10_PLUS_UNCONSTRAINED by lazy {
+            DynamicRangeProfiles(
+                longArrayOf(
+                    HLG10, CONSTRAINTS_NONE, LATENCY_NONE, // HLG is mandated
+                    HDR10_PLUS, CONSTRAINTS_NONE, LATENCY_NONE
+                )
+            )
+        }
+
+        val DOLBY_VISION_10B_UNCONSTRAINED by lazy {
+            DynamicRangeProfiles(
+                longArrayOf(
+                    HLG10, CONSTRAINTS_NONE, LATENCY_NONE, // HLG is mandated
+                    DOLBY_VISION_10B_HDR_OEM, CONSTRAINTS_NONE, LATENCY_NONE
+                )
+            )
+        }
+
+        val DOLBY_VISION_10B_UNCONSTRAINED_SLOW by lazy {
+            DynamicRangeProfiles(
+                longArrayOf(
+                    HLG10, CONSTRAINTS_NONE, LATENCY_NONE, // HLG is mandated
+                    DOLBY_VISION_10B_HDR_OEM, CONSTRAINTS_NONE, LATENCY_NON_ZERO
+                )
+            )
+        }
+
+        val DOLBY_VISION_8B_UNCONSTRAINED by lazy {
+            DynamicRangeProfiles(
+                longArrayOf(
+                    DOLBY_VISION_8B_HDR_OEM, CONSTRAINTS_NONE, LATENCY_NONE
+                )
+            )
+        }
+
+        private const val LATENCY_NONE = 0L
+        private const val LATENCY_NON_ZERO = 3L
+        private const val CONSTRAINTS_NONE = 0L
+
+        val DYNAMIC_RANGE_HLG10 = DynamicRange(FORMAT_HLG, BIT_DEPTH_10_BIT)
+        val DYNAMIC_RANGE_HDR10 = DynamicRange(FORMAT_HDR10, BIT_DEPTH_10_BIT)
+        val DYNAMIC_RANGE_HDR10_PLUS = DynamicRange(FORMAT_HDR10_PLUS, BIT_DEPTH_10_BIT)
+        val DYNAMIC_RANGE_DOLBY_VISION_10B = DynamicRange(FORMAT_DOLBY_VISION, BIT_DEPTH_10_BIT)
+        val DYNAMIC_RANGE_DOLBY_VISION_8B = DynamicRange(FORMAT_DOLBY_VISION, BIT_DEPTH_8_BIT)
+    }
+
+    @Config(minSdk = Build.VERSION_CODES.TIRAMISU)
+    @Test
+    fun canWrapAndUnwrapDynamicRangeProfiles() {
+        val dynamicRangesCompat = DynamicRangesCompat.toDynamicRangesCompat(HLG10_UNCONSTRAINED)
+
+        assertThat(dynamicRangesCompat).isNotNull()
+        assertThat(dynamicRangesCompat?.toDynamicRangeProfiles()).isEqualTo(HLG10_UNCONSTRAINED)
+    }
+
+    @Config(minSdk = Build.VERSION_CODES.TIRAMISU)
+    @Test
+    fun canSupportDynamicRangeFromHlg10Profile() {
+        val dynamicRangesCompat = DynamicRangesCompat.toDynamicRangesCompat(HLG10_UNCONSTRAINED)
+        assertThat(dynamicRangesCompat?.supportedDynamicRanges).contains(DYNAMIC_RANGE_HLG10)
+    }
+
+    @Config(minSdk = Build.VERSION_CODES.TIRAMISU)
+    @Test
+    fun canSupportDynamicRangeFromHdr10Profile() {
+        val dynamicRangesCompat = DynamicRangesCompat.toDynamicRangesCompat(HDR10_UNCONSTRAINED)
+        assertThat(dynamicRangesCompat?.supportedDynamicRanges).contains(DYNAMIC_RANGE_HDR10)
+    }
+
+    @Config(minSdk = Build.VERSION_CODES.TIRAMISU)
+    @Test
+    fun canSupportDynamicRangeFromHdr10PlusProfile() {
+        val dynamicRangesCompat =
+            DynamicRangesCompat.toDynamicRangesCompat(HDR10_PLUS_UNCONSTRAINED)
+        assertThat(dynamicRangesCompat?.supportedDynamicRanges).contains(DYNAMIC_RANGE_HDR10_PLUS)
+    }
+
+    @Config(minSdk = Build.VERSION_CODES.TIRAMISU)
+    @Test
+    fun canSupportDynamicRangeFromDolbyVision10bProfile() {
+        val dynamicRangesCompat =
+            DynamicRangesCompat.toDynamicRangesCompat(DOLBY_VISION_10B_UNCONSTRAINED)
+        assertThat(dynamicRangesCompat?.supportedDynamicRanges).contains(
+            DYNAMIC_RANGE_DOLBY_VISION_10B
+        )
+    }
+
+    @Config(minSdk = Build.VERSION_CODES.TIRAMISU)
+    @Test
+    fun canSupportDynamicRangeFromDolbyVision8bProfile() {
+        val dynamicRangesCompat =
+            DynamicRangesCompat.toDynamicRangesCompat(DOLBY_VISION_8B_UNCONSTRAINED)
+        assertThat(dynamicRangesCompat?.supportedDynamicRanges).contains(
+            DYNAMIC_RANGE_DOLBY_VISION_8B
+        )
+    }
+
+    @Config(minSdk = Build.VERSION_CODES.TIRAMISU)
+    @Test
+    fun canProduceConcurrentDynamicRangeConstraints() {
+        val hlg10ConstrainedWrapped = DynamicRangesCompat.toDynamicRangesCompat(HLG10_CONSTRAINED)
+        assertThat(
+            hlg10ConstrainedWrapped?.getDynamicRangeCaptureRequestConstraints(SDR)
+        ).containsExactly(SDR)
+        assertThat(
+            hlg10ConstrainedWrapped?.getDynamicRangeCaptureRequestConstraints(
+                DYNAMIC_RANGE_HLG10
+            )
+        ).containsExactly(DYNAMIC_RANGE_HLG10)
+
+        val hlg10SdrConstrainedWrapped =
+            DynamicRangesCompat.toDynamicRangesCompat(HLG10_SDR_CONSTRAINED)
+        assertThat(
+            hlg10SdrConstrainedWrapped?.getDynamicRangeCaptureRequestConstraints(SDR)
+        ).containsExactly(SDR, DYNAMIC_RANGE_HLG10)
+        assertThat(
+            hlg10SdrConstrainedWrapped?.getDynamicRangeCaptureRequestConstraints(
+                DYNAMIC_RANGE_HLG10
+            )
+        ).containsExactly(DYNAMIC_RANGE_HLG10, SDR)
+
+        val hlg10Hdr10ConstrainedWrapped =
+            DynamicRangesCompat.toDynamicRangesCompat(HLG10_HDR10_CONSTRAINED)
+        assertThat(
+            hlg10Hdr10ConstrainedWrapped?.getDynamicRangeCaptureRequestConstraints(SDR)
+        ).containsExactly(SDR)
+        assertThat(
+            hlg10Hdr10ConstrainedWrapped?.getDynamicRangeCaptureRequestConstraints(
+                DYNAMIC_RANGE_HLG10
+            )
+        ).containsExactly(DYNAMIC_RANGE_HLG10, DYNAMIC_RANGE_HDR10)
+        assertThat(
+            hlg10Hdr10ConstrainedWrapped?.getDynamicRangeCaptureRequestConstraints(
+                DYNAMIC_RANGE_HDR10
+            )
+        ).containsExactly(DYNAMIC_RANGE_HDR10, DYNAMIC_RANGE_HLG10)
+    }
+
+    @Config(minSdk = Build.VERSION_CODES.TIRAMISU)
+    @Test
+    fun producesDynamicRangeWithCorrectLatency() {
+        val dynamicRangesCompat =
+            DynamicRangesCompat.toDynamicRangesCompat(DOLBY_VISION_10B_UNCONSTRAINED_SLOW)
+        assertThat(dynamicRangesCompat?.isExtraLatencyPresent(SDR)).isFalse()
+        assertThat(
+            dynamicRangesCompat?.isExtraLatencyPresent(
+                DYNAMIC_RANGE_DOLBY_VISION_10B
+            )
+        ).isTrue()
+    }
+
+    @Config(minSdk = Build.VERSION_CODES.TIRAMISU)
+    @Test
+    fun canProduceDynamicRangeWithoutConstraints() {
+        val dynamicRangesCompat = DynamicRangesCompat.toDynamicRangesCompat(HLG10_UNCONSTRAINED)
+        assertThat(
+            dynamicRangesCompat?.getDynamicRangeCaptureRequestConstraints(DYNAMIC_RANGE_HLG10)
+        ).isEmpty()
+    }
+
+    @Config(minSdk = Build.VERSION_CODES.TIRAMISU)
+    @Test
+    fun canProduceDynamicRangesCompatFromCharacteristics() {
+        val characteristics = newCameraCharacteristicsCompat()
+        Shadow.extract<ShadowCameraCharacteristics>(
+            characteristics.toCameraCharacteristics()
+        ).addDynamicRangeProfiles(HLG10_CONSTRAINED)
+
+        val dynamicRangesCompat = DynamicRangesCompat.fromCameraCharacteristics(characteristics)
+
+        assertThat(dynamicRangesCompat.toDynamicRangeProfiles()).isEqualTo(HLG10_CONSTRAINED)
+    }
+
+    @Test
+    fun alwaysSupportsOnlySdrWithoutDynamicRangeProfilesInCharacteristics() {
+        val characteristics = newCameraCharacteristicsCompat()
+
+        val dynamicRangesCompat = DynamicRangesCompat.fromCameraCharacteristics(characteristics)
+
+        assertThat(dynamicRangesCompat.supportedDynamicRanges).containsExactly(SDR)
+        assertThat(
+            dynamicRangesCompat.getDynamicRangeCaptureRequestConstraints(SDR)
+        ).containsExactly(SDR)
+    }
+
+    @Test
+    fun unsupportedDynamicRangeAlwaysThrowsException() {
+        val characteristics = newCameraCharacteristicsCompat()
+
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+            Shadow.extract<ShadowCameraCharacteristics>(
+                characteristics.toCameraCharacteristics()
+            ).addDynamicRangeProfiles(DOLBY_VISION_8B_UNCONSTRAINED)
+        }
+
+        val dynamicRangesCompat = DynamicRangesCompat.fromCameraCharacteristics(characteristics)
+
+        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
+            assertThat(dynamicRangesCompat.supportedDynamicRanges).containsExactly(SDR)
+        } else {
+            assertThat(dynamicRangesCompat.supportedDynamicRanges).containsExactly(
+                SDR, DYNAMIC_RANGE_DOLBY_VISION_8B
+            )
+        }
+
+        assertThrows(IllegalArgumentException::class.java) {
+            dynamicRangesCompat.getDynamicRangeCaptureRequestConstraints(
+                DYNAMIC_RANGE_DOLBY_VISION_10B
+            )
+        }
+
+        assertThrows(IllegalArgumentException::class.java) {
+            dynamicRangesCompat.isExtraLatencyPresent(
+                DYNAMIC_RANGE_DOLBY_VISION_10B
+            )
+        }
+    }
+
+    @Test
+    fun sdrHasNoExtraLatency() {
+        val characteristics = newCameraCharacteristicsCompat()
+
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+            Shadow.extract<ShadowCameraCharacteristics>(
+                characteristics.toCameraCharacteristics()
+            ).addDynamicRangeProfiles(HLG10_CONSTRAINED)
+        }
+
+        val dynamicRangesCompat = DynamicRangesCompat.fromCameraCharacteristics(characteristics)
+
+        assertThat(dynamicRangesCompat.isExtraLatencyPresent(SDR)).isFalse()
+    }
+
+    @Test
+    fun sdrHasSdrConstraint_whenConcurrentDynamicRangesNotSupported() {
+        val characteristics = newCameraCharacteristicsCompat()
+
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+            Shadow.extract<ShadowCameraCharacteristics>(
+                characteristics.toCameraCharacteristics()
+            ).addDynamicRangeProfiles(HLG10_CONSTRAINED)
+        }
+
+        val dynamicRangesCompat = DynamicRangesCompat.fromCameraCharacteristics(characteristics)
+
+        assertThat(dynamicRangesCompat.getDynamicRangeCaptureRequestConstraints(SDR))
+            .containsExactly(SDR)
+    }
+}
+
+private const val CAMERA_ID = "0"
+
+@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
+fun newCameraCharacteristicsCompat(): CameraCharacteristicsCompat {
+    return CameraCharacteristicsCompat.toCameraCharacteristicsCompat(
+        ShadowCameraCharacteristics.newCameraCharacteristics(),
+        CAMERA_ID
+    )
+}
+
+@RequiresApi(Build.VERSION_CODES.TIRAMISU)
+fun ShadowCameraCharacteristics.addDynamicRangeProfiles(
+    dynamicRangeProfiles: DynamicRangeProfiles
+) {
+    set(
+        CameraCharacteristics.REQUEST_AVAILABLE_DYNAMIC_RANGE_PROFILES,
+        dynamicRangeProfiles
+    )
+}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/DynamicRange.java b/camera/camera-core/src/main/java/androidx/camera/core/DynamicRange.java
new file mode 100644
index 0000000..62c1199
--- /dev/null
+++ b/camera/camera-core/src/main/java/androidx/camera/core/DynamicRange.java
@@ -0,0 +1,176 @@
+/*
+ * 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;
+
+import androidx.annotation.IntDef;
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+import androidx.annotation.RestrictTo;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/** A representation of the dynamic range of an image. */
+@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public final class DynamicRange {
+    /** Standard Dynamic Range (SDR) format. */
+    public static final int FORMAT_SDR = 0;
+
+    //------------------------------------------------------------------------------//
+    //                            HDR Formats                                       //
+    //------------------------------------------------------------------------------//
+    /**
+     * An unspecified High Dynamic Range (HDR) format which allows the device to determine the
+     * underlying dynamic range format.
+     */
+    public static final int FORMAT_HDR_UNSPECIFIED = 1;
+    /** Hybrid Log Gamma (HLG) dynamic range format. */
+    public static final int FORMAT_HLG = FORMAT_HDR_UNSPECIFIED + 1;
+    /** HDR10 dynamic range format. */
+    public static final int FORMAT_HDR10 = FORMAT_HDR_UNSPECIFIED + 2;
+    /** HDR10+ dynamic range format. */
+    public static final int FORMAT_HDR10_PLUS = FORMAT_HDR_UNSPECIFIED + 3;
+    /** Dolby Vision dynamic range format. */
+    public static final int FORMAT_DOLBY_VISION = FORMAT_HDR_UNSPECIFIED + 4;
+    //------------------------------------------------------------------------------//
+
+    /** Bit depth is unspecified and may be determined automatically by the device. */
+    public static final int BIT_DEPTH_UNSPECIFIED = 0;
+    /** Eight-bit bit depth. */
+    public static final int BIT_DEPTH_8_BIT = 8;
+    /** Ten-bit bit depth. */
+    public static final int BIT_DEPTH_10_BIT = 10;
+
+    @RestrictTo(RestrictTo.Scope.LIBRARY)
+    @IntDef({FORMAT_SDR, FORMAT_HDR_UNSPECIFIED, FORMAT_HLG, FORMAT_HDR10, FORMAT_HDR10_PLUS,
+            FORMAT_DOLBY_VISION})
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface DynamicRangeFormat {
+    }
+
+    @RestrictTo(RestrictTo.Scope.LIBRARY)
+    @IntDef({BIT_DEPTH_UNSPECIFIED, BIT_DEPTH_8_BIT, BIT_DEPTH_10_BIT})
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface BitDepth {
+    }
+
+    /** A dynamic range representing 8-bit standard dynamic range (SDR). */
+    @NonNull
+    public static final DynamicRange SDR = new DynamicRange(FORMAT_SDR, BIT_DEPTH_8_BIT);
+
+    /**
+     * A dynamic range representing 10-bit high dynamic range (HDR) with unspecified format.
+     *
+     * <p>The HDR format is unspecified, and may defer to device defaults
+     * when used to select a dynamic range.
+     */
+    @NonNull
+    public static final DynamicRange HDR_UNSPECIFIED_10_BIT =
+            new DynamicRange(FORMAT_HDR_UNSPECIFIED, BIT_DEPTH_10_BIT);
+
+    private final @DynamicRangeFormat int mFormat;
+    private final @BitDepth int mBitDepth;
+
+    /**
+     * Creates a dynamic range representation from a format and bit depth.
+     *
+     * <p>This constructor is left public for testing purposes. It does not do any verification that
+     * the provided arguments are a valid combination of format and bit depth.
+     *
+     * @param format   The dynamic range format.
+     * @param bitDepth The bit depth.
+     */
+    public DynamicRange(
+            @DynamicRangeFormat int format,
+            @BitDepth int bitDepth) {
+        mFormat = format;
+        mBitDepth = bitDepth;
+    }
+
+    /**
+     * Returns the dynamic range format.
+     *
+     * @return The dynamic range format. Possible values are {@link #FORMAT_SDR},
+     * {@link #FORMAT_HLG}, {@link #FORMAT_HDR10}, {@link #FORMAT_HDR10_PLUS}, or
+     * {@link #FORMAT_DOLBY_VISION}.
+     */
+    @DynamicRangeFormat
+    public int getFormat() {
+        return mFormat;
+    }
+
+    /**
+     * Returns the bit depth used by this dynamic range configuration.
+     *
+     * <p>Common values are {@link #BIT_DEPTH_8_BIT}, such as for {@link #FORMAT_SDR} or
+     * {@link #BIT_DEPTH_10_BIT}, such as for {@link #FORMAT_HDR10}.
+     *
+     * @return The bit depth. Possible values are {@link #BIT_DEPTH_8_BIT},
+     * {@link #BIT_DEPTH_10_BIT}, or {@link #BIT_DEPTH_UNSPECIFIED}.
+     */
+    @BitDepth
+    public int getBitDepth() {
+        return mBitDepth;
+    }
+
+    @NonNull
+    @Override
+    public String toString() {
+        return "DynamicRange@" + Integer.toHexString(System.identityHashCode(this)) + "{"
+                + "format=" + getFormatLabel(mFormat) + ", "
+                + "bitDepth=" + mBitDepth
+                + "}";
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (o == this) {
+            return true;
+        }
+        if (o instanceof DynamicRange) {
+            DynamicRange that = (DynamicRange) o;
+            return this.mFormat == that.getFormat()
+                    && this.mBitDepth == that.getBitDepth();
+        }
+        return false;
+    }
+
+    @Override
+    public int hashCode() {
+        int hashCode = 1;
+        hashCode *= 1000003;
+        hashCode ^= mFormat;
+        hashCode *= 1000003;
+        hashCode ^= mBitDepth;
+        return hashCode;
+    }
+
+    @NonNull
+    private static String getFormatLabel(@DynamicRangeFormat int format) {
+        switch (format) {
+            case FORMAT_SDR: return "FORMAT_SDR";
+            case FORMAT_HDR_UNSPECIFIED: return "FORMAT_HDR_UNSPECIFIED";
+            case FORMAT_HLG: return "FORMAT_HLG";
+            case FORMAT_HDR10: return "FORMAT_HDR10";
+            case FORMAT_HDR10_PLUS: return "FORMAT_HDR10_PLUS";
+            case FORMAT_DOLBY_VISION: return "FORMAT_DOLBY_VISION";
+        }
+
+        return "<Unknown>";
+    }
+}
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/DynamicRangeTest.kt b/camera/camera-core/src/test/java/androidx/camera/core/DynamicRangeTest.kt
new file mode 100644
index 0000000..c89e49a
--- /dev/null
+++ b/camera/camera-core/src/test/java/androidx/camera/core/DynamicRangeTest.kt
@@ -0,0 +1,47 @@
+/*
+ * 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
+
+import android.os.Build
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.RobolectricTestRunner
+import org.robolectric.annotation.Config
+import org.robolectric.annotation.internal.DoNotInstrument
+
+@RunWith(RobolectricTestRunner::class)
+@DoNotInstrument
+@Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
+class DynamicRangeTest {
+
+    @Test
+    fun canCreateUnspecifiedDynamicRange() {
+        val dynamicRange = DynamicRange(
+            DynamicRange.FORMAT_HDR_UNSPECIFIED,
+            DynamicRange.BIT_DEPTH_UNSPECIFIED
+        )
+        assertThat(dynamicRange.format).isEqualTo(DynamicRange.FORMAT_HDR_UNSPECIFIED)
+        assertThat(dynamicRange.bitDepth).isEqualTo(DynamicRange.BIT_DEPTH_UNSPECIFIED)
+    }
+
+    @Test
+    fun sdrDynamicRange_is8Bit() {
+        assertThat(DynamicRange.SDR.format).isEqualTo(DynamicRange.FORMAT_SDR)
+        assertThat(DynamicRange.SDR.bitDepth).isEqualTo(DynamicRange.BIT_DEPTH_8_BIT)
+    }
+}
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 3963e6a..7dc5a90 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
@@ -159,7 +159,8 @@
     private static final String SURFACE_UPDATE_KEY =
             "androidx.camera.video.VideoCapture.streamUpdate";
     private static final Defaults DEFAULT_CONFIG = new Defaults();
-    private static final boolean ENABLE_SURFACE_PROCESSING_BY_QUIRK;
+    @VisibleForTesting
+    static boolean sEnableSurfaceProcessingByQuirk;
     private static final boolean USE_TEMPLATE_PREVIEW_BY_QUIRK;
 
     static {
@@ -175,7 +176,7 @@
                 DeviceQuirks.get(ExtraSupportedResolutionQuirk.class) != null;
         USE_TEMPLATE_PREVIEW_BY_QUIRK =
                 hasPreviewStretchQuirk || hasPreviewDelayQuirk || hasImageCaptureFailedQuirk;
-        ENABLE_SURFACE_PROCESSING_BY_QUIRK =
+        sEnableSurfaceProcessingByQuirk =
                 hasPreviewDelayQuirk || hasImageCaptureFailedQuirk
                         || hasVideoQualityQuirkAndWorkaroundBySurfaceProcessing
                         || hasExtraSupportedResolutionQuirk;
@@ -383,7 +384,6 @@
 
     /**
      * {@inheritDoc}
-     *
      */
     @SuppressWarnings("unchecked")
     @RestrictTo(Scope.LIBRARY_GROUP)
@@ -409,7 +409,6 @@
 
     /**
      * {@inheritDoc}
-     *
      */
     @Override
     @RestrictTo(Scope.LIBRARY_GROUP)
@@ -420,7 +419,6 @@
 
     /**
      * {@inheritDoc}
-     *
      */
     @RestrictTo(Scope.LIBRARY_GROUP)
     @Override
@@ -447,7 +445,6 @@
 
     /**
      * {@inheritDoc}
-     *
      */
     @RestrictTo(Scope.LIBRARY_GROUP)
     @Override
@@ -468,7 +465,6 @@
 
     /**
      * {@inheritDoc}
-     *
      */
     @RestrictTo(Scope.LIBRARY_GROUP)
     @NonNull
@@ -483,7 +479,6 @@
 
     /**
      * {@inheritDoc}
-     *
      */
     @NonNull
     @RestrictTo(Scope.LIBRARY_GROUP)
@@ -563,8 +558,7 @@
         VideoEncoderInfo videoEncoderInfo = getVideoEncoderInfo(config.getVideoEncoderInfoFinder(),
                 videoCapabilities, mediaSpec, resolution, targetFpsRange);
         mCropRect = calculateCropRect(resolution, videoEncoderInfo);
-        boolean shouldMirror = camera.getHasTransform() && isMirroringRequired(camera);
-        mNode = createNodeIfNeeded(isCropNeeded(mCropRect, resolution), shouldMirror);
+        mNode = createNodeIfNeeded(camera, mCropRect, resolution);
         // Choose Timebase based on the whether the buffer is copied.
         Timebase timebase;
         if (mNode != null || !camera.getHasTransform()) {
@@ -588,7 +582,7 @@
                     camera.getHasTransform(),
                     mCropRect,
                     getRelativeRotation(camera, isMirroringRequired(camera)),
-                    shouldMirror);
+                    shouldMirror(camera));
             cameraEdge.addOnInvalidatedListener(onSurfaceInvalidated);
             mCameraEdge = cameraEdge;
             SurfaceProcessorNode.OutConfig outConfig =
@@ -700,7 +694,6 @@
      *
      * <p>These values may be overridden by the implementation. They only provide a minimum set of
      * defaults that are implementation independent.
-     *
      */
     @RestrictTo(Scope.LIBRARY_GROUP)
     public static final class Defaults implements ConfigProvider<VideoCaptureConfig<?>> {
@@ -819,9 +812,13 @@
     }
 
     @Nullable
-    private SurfaceProcessorNode createNodeIfNeeded(boolean isCropNeeded, boolean mirroring) {
-        if (getEffect() != null || ENABLE_SURFACE_PROCESSING_BY_QUIRK || isCropNeeded
-                || mirroring) {
+    private SurfaceProcessorNode createNodeIfNeeded(@NonNull CameraInternal camera,
+            @NonNull Rect cropRect,
+            @NonNull Size resolution) {
+        if (getEffect() != null
+                || shouldEnableSurfaceProcessingByQuirk(camera)
+                || shouldCrop(cropRect, resolution)
+                || shouldMirror(camera)) {
             Logger.d(TAG, "Surface processing is enabled.");
             return new SurfaceProcessorNode(requireNonNull(getCamera()),
                     getEffect() != null ? getEffect().createSurfaceProcessorInternal() :
@@ -957,11 +954,23 @@
         }
     }
 
-    private static boolean isCropNeeded(@NonNull Rect cropRect, @NonNull Size resolution) {
+    private boolean shouldMirror(@NonNull CameraInternal camera) {
+        // Stream is always mirrored during buffer copy. If there has been a buffer copy, it
+        // means the input stream is already mirrored. Otherwise, mirror it as needed.
+        return camera.getHasTransform() && isMirroringRequired(camera);
+    }
+
+    private static boolean shouldCrop(@NonNull Rect cropRect, @NonNull Size resolution) {
         return resolution.getWidth() != cropRect.width()
                 || resolution.getHeight() != cropRect.height();
     }
 
+    private static boolean shouldEnableSurfaceProcessingByQuirk(@NonNull CameraInternal camera) {
+        // If there has been a buffer copy, it means the surface processing is already enabled on
+        // input stream. Otherwise, enable it as needed.
+        return camera.getHasTransform() && sEnableSurfaceProcessingByQuirk;
+    }
+
     private static int alignDown(int length, int alignment,
             @NonNull Range<Integer> supportedLength) {
         return align(true, length, alignment, supportedLength);
@@ -1300,7 +1309,6 @@
 
         /**
          * {@inheritDoc}
-         *
          */
         @RestrictTo(Scope.LIBRARY_GROUP)
         @Override
@@ -1311,7 +1319,6 @@
 
         /**
          * {@inheritDoc}
-         *
          */
         @RestrictTo(Scope.LIBRARY_GROUP)
         @NonNull
@@ -1383,7 +1390,6 @@
          * setTargetAspectRatio is not supported on VideoCapture
          *
          * <p>To set aspect ratio, see {@link Recorder.Builder#setAspectRatio(int)}.
-         *
          */
         @RestrictTo(Scope.LIBRARY_GROUP)
         @NonNull
@@ -1450,7 +1456,6 @@
          * setTargetResolution is not supported on VideoCapture
          *
          * <p>To set resolution, see {@link Recorder.Builder#setQualitySelector(QualitySelector)}.
-         *
          */
         @RestrictTo(Scope.LIBRARY_GROUP)
         @NonNull
diff --git a/camera/camera-video/src/test/java/androidx/camera/video/VideoCaptureTest.kt b/camera/camera-video/src/test/java/androidx/camera/video/VideoCaptureTest.kt
index 39694a7..c03128a 100644
--- a/camera/camera-video/src/test/java/androidx/camera/video/VideoCaptureTest.kt
+++ b/camera/camera-video/src/test/java/androidx/camera/video/VideoCaptureTest.kt
@@ -753,6 +753,42 @@
     }
 
     @Test
+    fun hasSurfaceProcessingQuirk_nodeIsNeeded() {
+        // Arrange.
+        VideoCapture.sEnableSurfaceProcessingByQuirk = true
+        setupCamera()
+        createCameraUseCaseAdapter()
+
+        // Act.
+        val videoCapture = createVideoCapture()
+        addAndAttachUseCases(videoCapture)
+
+        // Assert.
+        assertThat(videoCapture.node).isNotNull()
+
+        // Clean-up.
+        VideoCapture.sEnableSurfaceProcessingByQuirk = false
+    }
+
+    @Test
+    fun hasSurfaceProcessingQuirkButNoCameraTransform_nodeIsNotNeeded() {
+        // Arrange.
+        VideoCapture.sEnableSurfaceProcessingByQuirk = true
+        setupCamera(hasTransform = false)
+        createCameraUseCaseAdapter()
+
+        // Act.
+        val videoCapture = createVideoCapture()
+        addAndAttachUseCases(videoCapture)
+
+        // Assert.
+        assertThat(videoCapture.node).isNull()
+
+        // Clean-up.
+        VideoCapture.sEnableSurfaceProcessingByQuirk = false
+    }
+
+    @Test
     fun defaultMirrorModeIsOff() {
         val videoCapture = createVideoCapture()
         assertThat(videoCapture.mirrorMode).isEqualTo(MIRROR_MODE_OFF)
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text2/BasicTextField2Test.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text2/BasicTextField2Test.kt
index 58d013b..d7cf9d2 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text2/BasicTextField2Test.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text2/BasicTextField2Test.kt
@@ -49,6 +49,7 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.MediumTest
 import com.google.common.truth.Truth.assertThat
+import org.junit.Ignore
 import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -230,6 +231,7 @@
         }
     }
 
+    @Ignore // b/273412941
     @Test
     fun textField_focus_doesNotShowSoftwareKeyboard_ifDisabled() {
         val state = TextFieldState()
diff --git a/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text2/BasicTextField2.kt b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text2/BasicTextField2.kt
index 4b7cfa32..31dcd3d0 100644
--- a/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text2/BasicTextField2.kt
+++ b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text2/BasicTextField2.kt
@@ -59,11 +59,9 @@
 import androidx.compose.ui.semantics.setText
 import androidx.compose.ui.text.TextLayoutResult
 import androidx.compose.ui.text.TextPainter
-import androidx.compose.ui.text.TextRange
 import androidx.compose.ui.text.TextStyle
 import androidx.compose.ui.text.input.ImeAction
 import androidx.compose.ui.text.input.KeyboardType
-import androidx.compose.ui.text.input.TextFieldValue
 import androidx.compose.ui.unit.Density
 import kotlin.math.roundToInt
 
@@ -165,20 +163,12 @@
         if (!enabled) this.disabled()
 
         setText { text ->
-            // If the action is performed while in an active text editing session, treat this like
-            // an IME command and update the text by going through the buffer. This keeps the buffer
-            // state consistent if other IME commands are performed before the next recomposition,
-            // and is used for the testing code path.
-            textInputSessionState.value?.let {
-                state.editProcessor.update(
-                    listOf(
-                        DeleteAllCommand,
-                        CommitTextCommand(text, 1)
-                    )
+            state.editProcessor.update(
+                listOf(
+                    DeleteAllCommand,
+                    CommitTextCommand(text, 1)
                 )
-            } ?: run {
-                state.editProcessor.reset(TextFieldValue(text.text, TextRange(text.text.length)))
-            }
+            )
             true
         }
         onClick {
diff --git a/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text2/input/ApplyEditCommand.kt b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text2/input/ApplyEditCommand.kt
index 0a1412a..685449b 100644
--- a/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text2/input/ApplyEditCommand.kt
+++ b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text2/input/ApplyEditCommand.kt
@@ -174,15 +174,17 @@
 private fun EditingBuffer.applyDeleteSurroundingTextCommand(
     deleteSurroundingTextCommand: DeleteSurroundingTextCommand
 ) {
-    delete(
-        selectionEnd,
-        minOf(selectionEnd + deleteSurroundingTextCommand.lengthAfterCursor, length)
-    )
+    // calculate the end with safe addition since lengthAfterCursor can be set to e.g. Int.MAX
+    // by the input
+    val end = selectionEnd.addExactOrElse(deleteSurroundingTextCommand.lengthAfterCursor) { length }
+    delete(selectionEnd, minOf(end, length))
 
-    delete(
-        maxOf(0, selectionStart - deleteSurroundingTextCommand.lengthBeforeCursor),
-        selectionStart
-    )
+    // calculate the start with safe subtraction since lengthBeforeCursor can be set to e.g.
+    // Int.MAX by the input
+    val start = selectionStart.subtractExactOrElse(
+        deleteSurroundingTextCommand.lengthBeforeCursor
+    ) { 0 }
+    delete(maxOf(0, start), selectionStart)
 }
 
 private fun EditingBuffer.applyBackspaceCommand() {
diff --git a/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text2/input/MathUItils.kt b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text2/input/MathUItils.kt
new file mode 100644
index 0000000..9ac4025
--- /dev/null
+++ b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text2/input/MathUItils.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.text2.input
+
+/**
+ * Adds [this] and [right], and if an overflow occurs returns result of [defaultValue].
+ */
+internal inline fun Int.addExactOrElse(right: Int, defaultValue: () -> Int): Int {
+    val result = this + right
+    // HD 2-12 Overflow iff both arguments have the opposite sign of the result
+    return if (this xor result and (right xor result) < 0) defaultValue() else result
+}
+
+/**
+ * Subtracts [right] from [this], and if an overflow occurs returns result of [defaultValue].
+ */
+internal fun Int.subtractExactOrElse(right: Int, defaultValue: () -> Int): Int {
+    val result = this - right
+    // HD 2-12 Overflow iff the arguments have different signs and
+    // the sign of the result is different from the sign of x
+    return if (this xor right and (this xor result) < 0) defaultValue() else result
+}
\ No newline at end of file
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/textfield/OutlinedTextFieldScreenshotTest.kt b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/textfield/OutlinedTextFieldScreenshotTest.kt
index ac4def2..c40c277 100644
--- a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/textfield/OutlinedTextFieldScreenshotTest.kt
+++ b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/textfield/OutlinedTextFieldScreenshotTest.kt
@@ -25,6 +25,7 @@
 import androidx.compose.material.GOLDEN_MATERIAL
 import androidx.compose.material.Icon
 import androidx.compose.material.LocalContentColor
+import androidx.compose.material.LocalTextStyle
 import androidx.compose.material.OutlinedTextField
 import androidx.compose.material.Text
 import androidx.compose.material.icons.Icons
@@ -44,7 +45,6 @@
 import androidx.compose.ui.test.performTouchInput
 import androidx.compose.ui.test.swipeLeft
 import androidx.compose.ui.text.TextRange
-import androidx.compose.ui.text.TextStyle
 import androidx.compose.ui.text.input.TextFieldValue
 import androidx.compose.ui.text.style.TextAlign
 import androidx.compose.ui.unit.LayoutDirection
@@ -476,7 +476,7 @@
                 value = TextFieldValue(text = text, selection = TextRange(text.length)),
                 onValueChange = {},
                 modifier = Modifier.width(300.dp).testTag(TextFieldTag),
-                textStyle = TextStyle(textAlign = TextAlign.Center),
+                textStyle = LocalTextStyle.current.copy(textAlign = TextAlign.Center),
                 singleLine = true
             )
         }
@@ -492,7 +492,7 @@
                 value = TextFieldValue(text = text, selection = TextRange(text.length)),
                 onValueChange = {},
                 modifier = Modifier.fillMaxWidth().testTag(TextFieldTag),
-                textStyle = TextStyle(textAlign = TextAlign.End),
+                textStyle = LocalTextStyle.current.copy(textAlign = TextAlign.End),
                 singleLine = true
             )
         }
diff --git a/compose/material/material/src/androidMain/kotlin/androidx/compose/material/DefaultPlatformTextStyle.android.kt b/compose/material/material/src/androidMain/kotlin/androidx/compose/material/DefaultPlatformTextStyle.android.kt
new file mode 100644
index 0000000..2e65f0c
--- /dev/null
+++ b/compose/material/material/src/androidMain/kotlin/androidx/compose/material/DefaultPlatformTextStyle.android.kt
@@ -0,0 +1,27 @@
+/*
+ * 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.material
+
+import androidx.compose.ui.text.PlatformTextStyle
+
+private const val DefaultIncludeFontPadding = true
+
+@Suppress("DEPRECATION")
+private val DefaultPlatformTextStyle = PlatformTextStyle(
+    includeFontPadding = DefaultIncludeFontPadding
+)
+internal actual fun defaultPlatformTextStyle(): PlatformTextStyle? = DefaultPlatformTextStyle
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Text.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Text.kt
index aec0946..9398dcd1 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Text.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Text.kt
@@ -347,7 +347,7 @@
  *
  * @see ProvideTextStyle
  */
-val LocalTextStyle = compositionLocalOf(structuralEqualityPolicy()) { TextStyle.Default }
+val LocalTextStyle = compositionLocalOf(structuralEqualityPolicy()) { DefaultTextStyle }
 
 // TODO: b/156598010 remove this and replace with fold definition on the backing CompositionLocal
 /**
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Typography.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Typography.kt
index 63e208c..492072d 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Typography.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Typography.kt
@@ -18,6 +18,7 @@
 
 import androidx.compose.runtime.Immutable
 import androidx.compose.runtime.staticCompositionLocalOf
+import androidx.compose.ui.text.PlatformTextStyle
 import androidx.compose.ui.text.TextStyle
 import androidx.compose.ui.text.font.FontFamily
 import androidx.compose.ui.text.font.FontWeight
@@ -117,67 +118,67 @@
      */
     constructor(
         defaultFontFamily: FontFamily = FontFamily.Default,
-        h1: TextStyle = TextStyle(
+        h1: TextStyle = DefaultTextStyle.copy(
             fontWeight = FontWeight.Light,
             fontSize = 96.sp,
             letterSpacing = (-1.5).sp
         ),
-        h2: TextStyle = TextStyle(
+        h2: TextStyle = DefaultTextStyle.copy(
             fontWeight = FontWeight.Light,
             fontSize = 60.sp,
             letterSpacing = (-0.5).sp
         ),
-        h3: TextStyle = TextStyle(
+        h3: TextStyle = DefaultTextStyle.copy(
             fontWeight = FontWeight.Normal,
             fontSize = 48.sp,
             letterSpacing = 0.sp
         ),
-        h4: TextStyle = TextStyle(
+        h4: TextStyle = DefaultTextStyle.copy(
             fontWeight = FontWeight.Normal,
             fontSize = 34.sp,
             letterSpacing = 0.25.sp
         ),
-        h5: TextStyle = TextStyle(
+        h5: TextStyle = DefaultTextStyle.copy(
             fontWeight = FontWeight.Normal,
             fontSize = 24.sp,
             letterSpacing = 0.sp
         ),
-        h6: TextStyle = TextStyle(
+        h6: TextStyle = DefaultTextStyle.copy(
             fontWeight = FontWeight.Medium,
             fontSize = 20.sp,
             letterSpacing = 0.15.sp
         ),
-        subtitle1: TextStyle = TextStyle(
+        subtitle1: TextStyle = DefaultTextStyle.copy(
             fontWeight = FontWeight.Normal,
             fontSize = 16.sp,
             letterSpacing = 0.15.sp
         ),
-        subtitle2: TextStyle = TextStyle(
+        subtitle2: TextStyle = DefaultTextStyle.copy(
             fontWeight = FontWeight.Medium,
             fontSize = 14.sp,
             letterSpacing = 0.1.sp
         ),
-        body1: TextStyle = TextStyle(
+        body1: TextStyle = DefaultTextStyle.copy(
             fontWeight = FontWeight.Normal,
             fontSize = 16.sp,
             letterSpacing = 0.5.sp
         ),
-        body2: TextStyle = TextStyle(
+        body2: TextStyle = DefaultTextStyle.copy(
             fontWeight = FontWeight.Normal,
             fontSize = 14.sp,
             letterSpacing = 0.25.sp
         ),
-        button: TextStyle = TextStyle(
+        button: TextStyle = DefaultTextStyle.copy(
             fontWeight = FontWeight.Medium,
             fontSize = 14.sp,
             letterSpacing = 1.25.sp
         ),
-        caption: TextStyle = TextStyle(
+        caption: TextStyle = DefaultTextStyle.copy(
             fontWeight = FontWeight.Normal,
             fontSize = 12.sp,
             letterSpacing = 0.4.sp
         ),
-        overline: TextStyle = TextStyle(
+        overline: TextStyle = DefaultTextStyle.copy(
             fontWeight = FontWeight.Normal,
             fontSize = 10.sp,
             letterSpacing = 1.5.sp
@@ -284,6 +285,15 @@
     return if (fontFamily != null) this else copy(fontFamily = default)
 }
 
+internal val DefaultTextStyle = TextStyle.Default.copy(
+    platformStyle = defaultPlatformTextStyle()
+)
+
+/**
+ * Returns Default [PlatformTextStyle].
+ */
+internal expect fun defaultPlatformTextStyle(): PlatformTextStyle?
+
 /**
  * This CompositionLocal holds on to the current definition of typography for this application as
  * described by the Material spec. You can read the values in it when creating custom components
diff --git a/compose/material/material/src/desktopMain/kotlin/androidx/compose/material/DefaultPlatformTextStyle.desktop.kt b/compose/material/material/src/desktopMain/kotlin/androidx/compose/material/DefaultPlatformTextStyle.desktop.kt
new file mode 100644
index 0000000..d8b6ef6
--- /dev/null
+++ b/compose/material/material/src/desktopMain/kotlin/androidx/compose/material/DefaultPlatformTextStyle.desktop.kt
@@ -0,0 +1,21 @@
+/*
+ * 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.material
+
+import androidx.compose.ui.text.PlatformTextStyle
+
+internal actual fun defaultPlatformTextStyle(): PlatformTextStyle? = null
\ No newline at end of file
diff --git a/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/TimePickerSamples.kt b/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/TimePickerSamples.kt
index 94a3911..180c2ec 100644
--- a/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/TimePickerSamples.kt
+++ b/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/TimePickerSamples.kt
@@ -22,17 +22,17 @@
 import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.IntrinsicSize
 import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.RowScope
 import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.foundation.layout.height
-import androidx.compose.foundation.layout.offset
 import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
 import androidx.compose.foundation.layout.width
-import androidx.compose.material.Button
 import androidx.compose.material.icons.Icons
 import androidx.compose.material.icons.outlined.Keyboard
 import androidx.compose.material.icons.outlined.Schedule
+import androidx.compose.material3.Button
 import androidx.compose.material3.ExperimentalMaterial3Api
 import androidx.compose.material3.Icon
 import androidx.compose.material3.IconButton
@@ -54,10 +54,13 @@
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.platform.LocalConfiguration
+import androidx.compose.ui.semantics.isContainer
+import androidx.compose.ui.semantics.semantics
 import androidx.compose.ui.tooling.preview.Preview
 import androidx.compose.ui.unit.dp
 import androidx.compose.ui.window.Dialog
 import androidx.compose.ui.window.DialogProperties
+import androidx.compose.ui.zIndex
 import java.text.SimpleDateFormat
 import java.util.Calendar
 import java.util.Locale
@@ -179,28 +182,43 @@
                 showTimePicker = false
             },
             toggle = {
-                if (configuration.screenHeightDp > 540) {
-                    IconButton(
-                        modifier = Modifier.offset(x = -(8.dp)),
-                        onClick = { showingPicker.value = !showingPicker.value }) {
-                        val icon = if (showingPicker.value) {
-                            Icons.Outlined.Keyboard
-                        } else {
-                            Icons.Outlined.Schedule
-                        }
-                        Icon(
-                            icon,
-                            contentDescription = if (showingPicker.value) {
-                                "Switch to Text Input"
+                if (configuration.screenHeightDp > 400) {
+                    // Make this take the entire viewport. This will guarantee that Screen readers
+                    // focus the toggle first.
+                    Box(
+                        Modifier
+                            .fillMaxSize()
+                            .semantics { isContainer = true }
+                    ) {
+                        IconButton(
+                            modifier = Modifier
+                                // This is a workaround so that the Icon comes up first
+                                // in the talkback traversal order. So that users of a11y
+                                // services can use the text input. When talkback traversal
+                                // order is customizable we can remove this.
+                                .size(64.dp, 72.dp)
+                                .align(Alignment.BottomStart)
+                                .zIndex(5f),
+                            onClick = { showingPicker.value = !showingPicker.value }) {
+                            val icon = if (showingPicker.value) {
+                                Icons.Outlined.Keyboard
                             } else {
-                                "Switch to Touch Input"
+                                Icons.Outlined.Schedule
                             }
-                        )
+                            Icon(
+                                icon,
+                                contentDescription = if (showingPicker.value) {
+                                    "Switch to Text Input"
+                                } else {
+                                    "Switch to Touch Input"
+                                }
+                            )
+                        }
                     }
                 }
             }
         ) {
-            if (showingPicker.value && configuration.screenHeightDp > 540) {
+            if (showingPicker.value && configuration.screenHeightDp > 400) {
                 TimePicker(state = state)
             } else {
                 TimeInput(state = state)
@@ -214,7 +232,7 @@
     title: String = "Select Time",
     onCancel: () -> Unit,
     onConfirm: () -> Unit,
-    toggle: @Composable RowScope.() -> Unit = {},
+    toggle: @Composable () -> Unit = {},
     content: @Composable () -> Unit,
 ) {
     Dialog(
@@ -226,11 +244,13 @@
             tonalElevation = 6.dp,
             modifier = Modifier
                 .width(IntrinsicSize.Min)
+                .height(IntrinsicSize.Min)
                 .background(
                     shape = MaterialTheme.shapes.extraLarge,
                     color = MaterialTheme.colorScheme.surface
                 ),
         ) {
+            toggle()
             Column(
                 modifier = Modifier.padding(24.dp),
                 horizontalAlignment = Alignment.CenterHorizontally
@@ -248,7 +268,6 @@
                         .height(40.dp)
                         .fillMaxWidth()
                 ) {
-                    toggle()
                     Spacer(modifier = Modifier.weight(1f))
                     TextButton(
                         onClick = onCancel
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/BottomSheetScaffoldTest.kt b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/BottomSheetScaffoldTest.kt
index 0651b91..5dc6547 100644
--- a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/BottomSheetScaffoldTest.kt
+++ b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/BottomSheetScaffoldTest.kt
@@ -21,7 +21,6 @@
 import android.content.res.Configuration
 import android.os.Build
 import androidx.activity.ComponentActivity
-import androidx.annotation.RequiresApi
 import androidx.compose.foundation.background
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.PaddingValues
@@ -583,7 +582,7 @@
         }
     }
 
-    @RequiresApi(Build.VERSION_CODES.O)
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
     @Test
     fun bottomSheetScaffold_slotsPositionedAppropriately() {
         val topBarHeight = 56.dp
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/TimePickerTest.kt b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/TimePickerTest.kt
index b26f237..6fb42e6 100644
--- a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/TimePickerTest.kt
+++ b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/TimePickerTest.kt
@@ -17,17 +17,20 @@
 package androidx.compose.material3
 
 import android.content.Context
+import android.content.res.Resources
 import android.os.Build
 import android.text.format.DateFormat
-import androidx.compose.ui.platform.LocalContext
 import androidx.compose.ui.ExperimentalComposeUiApi
 import androidx.compose.ui.input.key.Key
+import androidx.compose.ui.platform.LocalContext
 import androidx.compose.ui.semantics.Role
 import androidx.compose.ui.semantics.SemanticsProperties
 import androidx.compose.ui.semantics.SemanticsProperties.SelectableGroup
 import androidx.compose.ui.test.ExperimentalTestApi
 import androidx.compose.ui.test.SemanticsMatcher.Companion.expectValue
 import androidx.compose.ui.test.SemanticsMatcher.Companion.keyIsDefined
+import androidx.compose.ui.test.SemanticsNodeInteraction
+import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
 import androidx.compose.ui.test.assert
 import androidx.compose.ui.test.assertAll
 import androidx.compose.ui.test.assertCountEquals
@@ -49,6 +52,7 @@
 import androidx.compose.ui.test.onAllNodesWithContentDescription
 import androidx.compose.ui.test.onAllNodesWithText
 import androidx.compose.ui.test.onChildren
+import androidx.compose.ui.test.onFirst
 import androidx.compose.ui.test.onLast
 import androidx.compose.ui.test.onNodeWithContentDescription
 import androidx.compose.ui.test.onNodeWithText
@@ -61,6 +65,7 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.MediumTest
 import androidx.test.filters.SdkSuppress
+import androidx.test.platform.app.InstrumentationRegistry
 import com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn
 import com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession
 import com.android.dx.mockito.inline.extended.MockedMethod
@@ -85,9 +90,15 @@
             TimePicker(state)
         }
 
-        rule.onAllNodesWithText("23").assertCountEquals(1)
+       rule.onNodeWithTimeValue(
+           number = 2,
+           selection = Selection.Hour,
+       ).assertIsSelected()
 
-        rule.onNodeWithText("02").assertIsSelected()
+        rule.onNodeWithTimeValue(
+            number = 23,
+            selection = Selection.Minute,
+        ).assertExists()
 
         rule.onNodeWithText("AM").assertExists()
 
@@ -101,9 +112,14 @@
             TimePicker(state)
         }
 
-        rule.onNodeWithText("23").performClick()
+        rule.onNodeWithTimeValue(
+            number = 23,
+            selection = Selection.Minute,
+        ).performClick()
 
-        rule.onNodeWithText("55").assertExists()
+        rule.runOnIdle {
+            assertThat(state.selection).isEqualTo(Selection.Minute)
+        }
     }
 
     @Test
@@ -113,7 +129,7 @@
             TimePicker(state)
         }
 
-        rule.onNodeWithText("6").performClick()
+        rule.onNodeWithTimeValue(number = 6, selection = Selection.Hour).performClick()
 
         // shows 06 in display
         rule.onNodeWithText("06").assertExists()
@@ -479,7 +495,7 @@
         }
 
         repeat(24) { number ->
-            rule.onNodeWithText(number.toString()).performClick()
+            rule.onNodeWithTimeValue(number, Selection.Hour, is24Hour = true).performClick()
             rule.runOnIdle {
                 state.selection = Selection.Hour
                 assertThat(state.hour).isEqualTo(number)
@@ -506,11 +522,78 @@
                 else -> number
             }
 
-            rule.onNodeWithText("$hour").performClick()
+            rule.onNodeWithTimeValue(hour, Selection.Hour).performClick()
             rule.runOnIdle {
                 state.selection = Selection.Hour
                 assertThat(state.hour).isEqualTo(number)
             }
         }
     }
+
+    @Test
+    fun clockFace_24HourMinutes_everyValue() {
+        val state = TimePickerState(initialHour = 10, initialMinute = 23, is24Hour = true)
+        state.selection = Selection.Minute
+        rule.setMaterialContent(lightColorScheme()) {
+            ClockFace(state, TimePickerDefaults.colors())
+        }
+
+        repeat(11) { number ->
+            rule.onNodeWithTimeValue(
+                number * 5,
+                Selection.Minute,
+                is24Hour = true
+            ).performClick()
+            rule.runOnIdle {
+                assertThat(state.minute).isEqualTo(number * 5)
+            }
+        }
+    }
+
+    @Test
+    fun clockFace_12HourMinutes_everyValue() {
+        val state = TimePickerState(initialHour = 10, initialMinute = 23, is24Hour = false)
+        state.selection = Selection.Minute
+        rule.setMaterialContent(lightColorScheme()) {
+            ClockFace(state, TimePickerDefaults.colors())
+        }
+
+        repeat(11) { number ->
+            rule.onNodeWithTimeValue(number * 5, Selection.Minute).performClick()
+            rule.runOnIdle {
+                assertThat(state.minute).isEqualTo(number * 5)
+            }
+        }
+    }
+
+    private fun contentDescriptionForValue(
+        resources: Resources,
+        selection: Selection,
+        is24Hour: Boolean,
+        number: Int
+    ): String {
+
+        val id = if (selection == Selection.Minute) {
+            R.string.time_picker_minute_suffix
+        } else if (is24Hour) {
+            R.string.time_picker_hour_24h_suffix
+        } else {
+            R.string.time_picker_hour_suffix
+        }
+
+        return resources.getString(id, number)
+    }
+
+    private fun SemanticsNodeInteractionsProvider.onNodeWithTimeValue(
+        number: Int,
+        selection: Selection,
+        is24Hour: Boolean = false,
+    ): SemanticsNodeInteraction = onAllNodesWithContentDescription(
+        contentDescriptionForValue(
+            InstrumentationRegistry.getInstrumentation().context.resources,
+            selection,
+            is24Hour,
+            number
+        )
+    ).onFirst()
 }
diff --git a/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/DefaultPlatformTextStyle.android.kt b/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/DefaultPlatformTextStyle.android.kt
new file mode 100644
index 0000000..f3ed0e3
--- /dev/null
+++ b/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/DefaultPlatformTextStyle.android.kt
@@ -0,0 +1,27 @@
+/*
+ * 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.material3
+
+import androidx.compose.ui.text.PlatformTextStyle
+
+private const val DefaultIncludeFontPadding = true
+
+@Suppress("DEPRECATION")
+private val DefaultPlatformTextStyle = PlatformTextStyle(
+    includeFontPadding = DefaultIncludeFontPadding
+)
+internal actual fun defaultPlatformTextStyle(): PlatformTextStyle? = DefaultPlatformTextStyle
\ No newline at end of file
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/DefaultPlatformTextStyle.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/DefaultPlatformTextStyle.kt
new file mode 100644
index 0000000..256aae5
--- /dev/null
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/DefaultPlatformTextStyle.kt
@@ -0,0 +1,24 @@
+/*
+ * 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.material3
+
+import androidx.compose.ui.text.PlatformTextStyle
+
+/**
+ * Returns Default [PlatformTextStyle].
+ */
+internal expect fun defaultPlatformTextStyle(): PlatformTextStyle?
\ No newline at end of file
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Text.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Text.kt
index bfdc8a6..c418cf7 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Text.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Text.kt
@@ -18,6 +18,7 @@
 
 import androidx.compose.foundation.text.BasicText
 import androidx.compose.foundation.text.InlineTextContent
+import androidx.compose.material3.tokens.DefaultTextStyle
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.CompositionLocalProvider
 import androidx.compose.runtime.compositionLocalOf
@@ -343,7 +344,7 @@
  *
  * @see ProvideTextStyle
  */
-val LocalTextStyle = compositionLocalOf(structuralEqualityPolicy()) { TextStyle.Default }
+val LocalTextStyle = compositionLocalOf(structuralEqualityPolicy()) { DefaultTextStyle }
 
 // TODO(b/156598010): remove this and replace with fold definition on the backing CompositionLocal
 /**
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/TimePicker.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/TimePicker.kt
index 665d9fe..a7ccf61 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/TimePicker.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/TimePicker.kt
@@ -116,14 +116,19 @@
 import androidx.compose.ui.layout.Measurable
 import androidx.compose.ui.layout.MeasureResult
 import androidx.compose.ui.layout.MeasureScope
+import androidx.compose.ui.layout.boundsInParent
 import androidx.compose.ui.layout.layoutId
+import androidx.compose.ui.layout.onGloballyPositioned
 import androidx.compose.ui.layout.onSizeChanged
 import androidx.compose.ui.platform.InspectorInfo
 import androidx.compose.ui.platform.InspectorValueInfo
 import androidx.compose.ui.platform.LocalDensity
 import androidx.compose.ui.platform.debugInspectorInfo
 import androidx.compose.ui.semantics.Role
+import androidx.compose.ui.semantics.clearAndSetSemantics
 import androidx.compose.ui.semantics.contentDescription
+import androidx.compose.ui.semantics.isContainer
+import androidx.compose.ui.semantics.onClick
 import androidx.compose.ui.semantics.role
 import androidx.compose.ui.semantics.selectableGroup
 import androidx.compose.ui.semantics.selected
@@ -563,9 +568,8 @@
         DpOffset(offsetX, offsetY)
     }
 
-    internal val values by derivedStateOf {
-        if (selection == Selection.Minute) Minutes else Hours
-    }
+    internal var center by mutableStateOf(IntOffset.Zero)
+    internal val values get() = if (selection == Selection.Minute) Minutes else Hours
 
     internal var selection by mutableStateOf(Selection.Hour)
     internal var isAfternoonToggle by mutableStateOf(initialHour > 12 && !is24Hour)
@@ -588,6 +592,12 @@
         hourAngle = RadiansPerHour * hour % 12 - FullCircle / 4
     }
 
+    internal fun moveSelector(x: Float, y: Float, maxDist: Float) {
+        if (selection == Selection.Hour && is24hour) {
+            isInnerCircle = dist(x, y, center.x, center.y) < maxDist
+        }
+    }
+
     internal fun isSelected(value: Int): Boolean =
         if (selection == Selection.Minute) {
             value == minute
@@ -655,6 +665,17 @@
         currentAngle.animateTo(targetValue.second, tween(200))
     }
 
+    internal suspend fun onTap(x: Float, y: Float, maxDist: Float) {
+        update(atan(y - center.y, x - center.x), true)
+        moveSelector(x, y, maxDist)
+
+        if (selection == Selection.Hour) {
+            selection = Selection.Minute
+        } else {
+            settle()
+        }
+    }
+
     companion object {
         /**
          * The default [Saver] implementation for [TimePickerState].
@@ -811,7 +832,7 @@
     )
 
     Box(
-        modifier = modifier,
+        modifier = modifier.clearAndSetSemantics { },
         contentAlignment = Alignment.Center
     ) {
         Text(
@@ -861,13 +882,13 @@
         shape = TimeSelectorContainerShape.toShape(),
         color = containerColor,
     ) {
-        val valueContentDescription = getString(
+        val valueContentDescription =
             numberContentDescription(
                 selection = selection,
-                is24Hour = state.is24hour
-            ),
-            value
-        )
+                is24Hour = state.is24hour,
+                number = value
+            )
+
         Box(contentAlignment = Alignment.Center) {
             Text(
                 modifier = Modifier.semantics { contentDescription = valueContentDescription },
@@ -886,6 +907,7 @@
             .background(shape = CircleShape, color = colors.clockDialColor)
             .size(ClockDialContainerSize)
             .semantics {
+                isContainer = false
                 selectableGroup()
             },
         targetState = state.values,
@@ -902,13 +924,12 @@
                 LocalContentColor provides colors.clockDialContentColor(false)
             ) {
                 repeat(screen.size) {
-                    val outerValue = if (!state.is24hour) screen[it] else screen[it] % 12
-                    ClockText(
-                        is24Hour = state.is24hour,
-                        selection = state.selection,
-                        value = outerValue,
-                        selected = state.isSelected(it)
-                    )
+                    val outerValue = if (!state.is24hour || state.selection == Selection.Minute) {
+                        screen[it]
+                    } else {
+                        screen[it] % 12
+                    }
+                    ClockText(state = state, value = outerValue)
                 }
 
                 if (state.selection == Selection.Hour && state.is24hour) {
@@ -921,12 +942,7 @@
                     ) {
                         repeat(ExtraHours.size) {
                             val innerValue = ExtraHours[it]
-                            ClockText(
-                                is24Hour = true,
-                                selection = state.selection,
-                                value = innerValue,
-                                selected = state.isSelected(it % 11)
-                            )
+                            ClockText(state = state, value = innerValue)
                         }
                     }
                 }
@@ -1003,37 +1019,24 @@
 }) {
     var offsetX by remember { mutableStateOf(0f) }
     var offsetY by remember { mutableStateOf(0f) }
-    var center by remember { mutableStateOf(IntOffset.Zero) }
+    val center by remember { mutableStateOf(IntOffset.Zero) }
     val scope = rememberCoroutineScope()
     val maxDist = with(LocalDensity.current) { MaxDistance.toPx() }
-    fun moveSelector(x: Float, y: Float) {
-        if (state.selection == Selection.Hour && state.is24hour) {
-            state.isInnerCircle = dist(x, y, center.x, center.y) < maxDist
-        }
-    }
+
     Modifier
-        .onSizeChanged { center = it.center }
-        .pointerInput(state, maxDist, center) {
+        .onSizeChanged { state.center = it.center }
+        .pointerInput(state, center, maxDist) {
             detectTapGestures(
                 onPress = {
                     offsetX = it.x
                     offsetY = it.y
                 },
                 onTap = {
-                    scope.launch {
-                        state.update(atan(it.y - center.y, it.x - center.x), true)
-                        moveSelector(it.x, it.y)
-
-                        if (state.selection == Selection.Hour) {
-                            state.selection = Selection.Minute
-                        } else {
-                            state.settle()
-                        }
-                    }
+                    scope.launch { state.onTap(it.x, it.y, maxDist) }
                 },
             )
         }
-        .pointerInput(state, maxDist, center) {
+        .pointerInput(state, center, maxDist) {
             detectDragGestures(onDragEnd = {
                 scope.launch {
                     if (state.selection == Selection.Hour) {
@@ -1047,47 +1050,56 @@
                 scope.launch {
                     offsetX += dragAmount.x
                     offsetY += dragAmount.y
-                    state.update(atan(offsetY - center.y, offsetX - center.x))
+                    state.update(atan(offsetY - state.center.y, offsetX - state.center.x))
                 }
-                moveSelector(offsetX, offsetY)
+                state.moveSelector(offsetX, offsetY, maxDist)
             }
         }
 }
 
 @Composable
-private fun ClockText(
-    is24Hour: Boolean,
-    selected: Boolean,
-    selection: Selection,
-    value: Int
-) {
+private fun ClockText(state: TimePickerState, value: Int) {
     val style = MaterialTheme.typography.fromToken(ClockDialLabelTextFont).let {
-        remember(it) {
-            copyAndSetFontPadding(style = it, false)
-        }
+        copyAndSetFontPadding(style = it, false)
     }
 
-    val contentDescription = getString(
+    val maxDist = with(LocalDensity.current) { MaxDistance.toPx() }
+    var center by remember { mutableStateOf(Offset.Zero) }
+    val scope = rememberCoroutineScope()
+    val contentDescription =
         numberContentDescription(
-            selection = selection,
-            is24Hour = is24Hour
-        ),
-        value
-    )
+            selection = state.selection,
+            is24Hour = state.is24hour,
+            number = value
+        )
+
+    val text = value.toLocalString(minDigits = 1)
+    val selected = if (state.selection == Selection.Minute) {
+        state.minute.toLocalString(minDigits = 1) == text
+    } else {
+        state.hour.toLocalString(minDigits = 1) == text
+    }
 
     Box(
         contentAlignment = Alignment.Center,
         modifier = Modifier
             .minimumInteractiveComponentSize()
             .size(MinimumInteractiveSize)
+            .onGloballyPositioned { center = it.boundsInParent().center }
             .focusable()
-            .semantics {
+            .semantics(mergeDescendants = true) {
+                onClick {
+                    scope.launch { state.onTap(center.x, center.y, maxDist) }
+                    true
+                }
                 this.selected = selected
-                this.contentDescription = contentDescription
             }
     ) {
         Text(
-            text = value.toLocalString(minDigits = 1),
+            modifier = Modifier.clearAndSetSemantics {
+                this.contentDescription = contentDescription
+            },
+            text = text,
             style = style,
         )
     }
@@ -1288,16 +1300,20 @@
 
 @Composable
 @ReadOnlyComposable
-private fun numberContentDescription(selection: Selection, is24Hour: Boolean): Strings {
-    if (selection == Selection.Minute) {
-        return Strings.TimePickerMinuteSuffix
+internal fun numberContentDescription(
+    selection: Selection,
+    is24Hour: Boolean,
+    number: Int
+): String {
+    val id = if (selection == Selection.Minute) {
+        Strings.TimePickerMinuteSuffix
+    } else if (is24Hour) {
+        Strings.TimePicker24HourSuffix
+    } else {
+        Strings.TimePickerHourSuffix
     }
 
-    if (is24Hour) {
-        return Strings.TimePicker24HourSuffix
-    }
-
-    return Strings.TimePickerHourSuffix
+    return getString(id, number)
 }
 
 private fun valuesForAnimation(current: Float, new: Float): Pair<Float, Float> {
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/TypographyTokens.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/TypographyTokens.kt
index 403ced0..089c598 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/TypographyTokens.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/TypographyTokens.kt
@@ -18,11 +18,12 @@
 
 package androidx.compose.material3.tokens
 
+import androidx.compose.material3.defaultPlatformTextStyle
 import androidx.compose.ui.text.TextStyle
 
 internal object TypographyTokens {
     val BodyLarge =
-        TextStyle(
+        DefaultTextStyle.copy(
             fontFamily = TypeScaleTokens.BodyLargeFont,
             fontWeight = TypeScaleTokens.BodyLargeWeight,
             fontSize = TypeScaleTokens.BodyLargeSize,
@@ -30,7 +31,7 @@
             letterSpacing = TypeScaleTokens.BodyLargeTracking,
         )
     val BodyMedium =
-        TextStyle(
+        DefaultTextStyle.copy(
             fontFamily = TypeScaleTokens.BodyMediumFont,
             fontWeight = TypeScaleTokens.BodyMediumWeight,
             fontSize = TypeScaleTokens.BodyMediumSize,
@@ -38,7 +39,7 @@
             letterSpacing = TypeScaleTokens.BodyMediumTracking,
         )
     val BodySmall =
-        TextStyle(
+        DefaultTextStyle.copy(
             fontFamily = TypeScaleTokens.BodySmallFont,
             fontWeight = TypeScaleTokens.BodySmallWeight,
             fontSize = TypeScaleTokens.BodySmallSize,
@@ -46,7 +47,7 @@
             letterSpacing = TypeScaleTokens.BodySmallTracking,
         )
     val DisplayLarge =
-        TextStyle(
+        DefaultTextStyle.copy(
             fontFamily = TypeScaleTokens.DisplayLargeFont,
             fontWeight = TypeScaleTokens.DisplayLargeWeight,
             fontSize = TypeScaleTokens.DisplayLargeSize,
@@ -54,7 +55,7 @@
             letterSpacing = TypeScaleTokens.DisplayLargeTracking,
         )
     val DisplayMedium =
-        TextStyle(
+        DefaultTextStyle.copy(
             fontFamily = TypeScaleTokens.DisplayMediumFont,
             fontWeight = TypeScaleTokens.DisplayMediumWeight,
             fontSize = TypeScaleTokens.DisplayMediumSize,
@@ -62,7 +63,7 @@
             letterSpacing = TypeScaleTokens.DisplayMediumTracking,
         )
     val DisplaySmall =
-        TextStyle(
+        DefaultTextStyle.copy(
             fontFamily = TypeScaleTokens.DisplaySmallFont,
             fontWeight = TypeScaleTokens.DisplaySmallWeight,
             fontSize = TypeScaleTokens.DisplaySmallSize,
@@ -70,7 +71,7 @@
             letterSpacing = TypeScaleTokens.DisplaySmallTracking,
         )
     val HeadlineLarge =
-        TextStyle(
+        DefaultTextStyle.copy(
             fontFamily = TypeScaleTokens.HeadlineLargeFont,
             fontWeight = TypeScaleTokens.HeadlineLargeWeight,
             fontSize = TypeScaleTokens.HeadlineLargeSize,
@@ -78,7 +79,7 @@
             letterSpacing = TypeScaleTokens.HeadlineLargeTracking,
         )
     val HeadlineMedium =
-        TextStyle(
+        DefaultTextStyle.copy(
             fontFamily = TypeScaleTokens.HeadlineMediumFont,
             fontWeight = TypeScaleTokens.HeadlineMediumWeight,
             fontSize = TypeScaleTokens.HeadlineMediumSize,
@@ -86,7 +87,7 @@
             letterSpacing = TypeScaleTokens.HeadlineMediumTracking,
         )
     val HeadlineSmall =
-        TextStyle(
+        DefaultTextStyle.copy(
             fontFamily = TypeScaleTokens.HeadlineSmallFont,
             fontWeight = TypeScaleTokens.HeadlineSmallWeight,
             fontSize = TypeScaleTokens.HeadlineSmallSize,
@@ -94,7 +95,7 @@
             letterSpacing = TypeScaleTokens.HeadlineSmallTracking,
         )
     val LabelLarge =
-        TextStyle(
+        DefaultTextStyle.copy(
             fontFamily = TypeScaleTokens.LabelLargeFont,
             fontWeight = TypeScaleTokens.LabelLargeWeight,
             fontSize = TypeScaleTokens.LabelLargeSize,
@@ -102,7 +103,7 @@
             letterSpacing = TypeScaleTokens.LabelLargeTracking,
         )
     val LabelMedium =
-        TextStyle(
+        DefaultTextStyle.copy(
             fontFamily = TypeScaleTokens.LabelMediumFont,
             fontWeight = TypeScaleTokens.LabelMediumWeight,
             fontSize = TypeScaleTokens.LabelMediumSize,
@@ -110,7 +111,7 @@
             letterSpacing = TypeScaleTokens.LabelMediumTracking,
         )
     val LabelSmall =
-        TextStyle(
+        DefaultTextStyle.copy(
             fontFamily = TypeScaleTokens.LabelSmallFont,
             fontWeight = TypeScaleTokens.LabelSmallWeight,
             fontSize = TypeScaleTokens.LabelSmallSize,
@@ -118,7 +119,7 @@
             letterSpacing = TypeScaleTokens.LabelSmallTracking,
         )
     val TitleLarge =
-        TextStyle(
+        DefaultTextStyle.copy(
             fontFamily = TypeScaleTokens.TitleLargeFont,
             fontWeight = TypeScaleTokens.TitleLargeWeight,
             fontSize = TypeScaleTokens.TitleLargeSize,
@@ -126,7 +127,7 @@
             letterSpacing = TypeScaleTokens.TitleLargeTracking,
         )
     val TitleMedium =
-        TextStyle(
+        DefaultTextStyle.copy(
             fontFamily = TypeScaleTokens.TitleMediumFont,
             fontWeight = TypeScaleTokens.TitleMediumWeight,
             fontSize = TypeScaleTokens.TitleMediumSize,
@@ -134,11 +135,13 @@
             letterSpacing = TypeScaleTokens.TitleMediumTracking,
         )
     val TitleSmall =
-        TextStyle(
+        DefaultTextStyle.copy(
             fontFamily = TypeScaleTokens.TitleSmallFont,
             fontWeight = TypeScaleTokens.TitleSmallWeight,
             fontSize = TypeScaleTokens.TitleSmallSize,
             lineHeight = TypeScaleTokens.TitleSmallLineHeight,
             letterSpacing = TypeScaleTokens.TitleSmallTracking,
         )
-}
\ No newline at end of file
+}
+
+internal val DefaultTextStyle = TextStyle.Default.copy(platformStyle = defaultPlatformTextStyle())
diff --git a/compose/material3/material3/src/desktopMain/kotlin/androidx/compose/material/DefaultPlatformTextStyle.desktop.kt b/compose/material3/material3/src/desktopMain/kotlin/androidx/compose/material/DefaultPlatformTextStyle.desktop.kt
new file mode 100644
index 0000000..3e06cac
--- /dev/null
+++ b/compose/material3/material3/src/desktopMain/kotlin/androidx/compose/material/DefaultPlatformTextStyle.desktop.kt
@@ -0,0 +1,21 @@
+/*
+ * 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.material3
+
+import androidx.compose.ui.text.PlatformTextStyle
+
+internal actual fun defaultPlatformTextStyle(): PlatformTextStyle? = null
\ No newline at end of file
diff --git a/core/core/src/main/java/androidx/core/app/NotificationCompat.java b/core/core/src/main/java/androidx/core/app/NotificationCompat.java
index 124fa91..355b4f0 100644
--- a/core/core/src/main/java/androidx/core/app/NotificationCompat.java
+++ b/core/core/src/main/java/androidx/core/app/NotificationCompat.java
@@ -4280,12 +4280,14 @@
              * where the platform doesn't support the MIME type, the original text provided in the
              * constructor will be used.
              *
-             * @param dataMimeType The MIME type of the content
+             * @param dataMimeType The MIME type of the content. See
+             * {@link android.graphics.ImageDecoder#isMimeTypeSupported(String)}
+             * for a list of supported image MIME types.
              * @param dataUri The uri containing the content whose type is given by the MIME type.
              * <p class="note">
+             * Notification Listeners including the System UI need permission to access the
+             * data the Uri points to. The recommended ways to do this are:
              * <ol>
-             *   <li>Notification Listeners including the System UI need permission to access the
-             *       data the Uri points to. The recommended ways to do this are:</li>
              *   <li>Store the data in your own ContentProvider, making sure that other apps have
              *       the correct permission to access your provider. The preferred mechanism for
              *       providing access is to use per-URI permissions which are temporary and only
diff --git a/graphics/graphics-core/src/androidTest/java/androidx/graphics/MultiBufferedCanvasRendererTest.kt b/graphics/graphics-core/src/androidTest/java/androidx/graphics/MultiBufferedCanvasRendererTest.kt
index e0ca037c..85138d1 100644
--- a/graphics/graphics-core/src/androidTest/java/androidx/graphics/MultiBufferedCanvasRendererTest.kt
+++ b/graphics/graphics-core/src/androidTest/java/androidx/graphics/MultiBufferedCanvasRendererTest.kt
@@ -19,7 +19,6 @@
 import android.graphics.Bitmap
 import android.graphics.Color
 import android.graphics.ColorSpace
-import android.graphics.Paint
 import android.graphics.RenderNode
 import android.os.Build
 import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -109,19 +108,15 @@
         val renderNode = RenderNode("node").apply {
             setPosition(0, 0, TEST_WIDTH, TEST_HEIGHT)
             val canvas = beginRecording()
-            val paint = Paint()
-            val widthF = TEST_WIDTH.toFloat()
-            val heightF = TEST_HEIGHT.toFloat()
-            val halfWidth = widthF / 2f
-            val halfHeight = heightF / 2f
-            canvas.drawRect(0f, 0f, halfWidth, halfHeight,
-                paint.apply { color = Color.RED })
-            canvas.drawRect(halfWidth, 0f, widthF, halfHeight,
-                paint.apply { color = Color.YELLOW })
-            canvas.drawRect(0f, halfHeight, halfWidth, heightF,
-                paint.apply { color = Color.GREEN })
-            canvas.drawRect(halfWidth, halfHeight, widthF, heightF,
-                paint.apply { color = Color.BLUE })
+            drawSquares(
+                canvas,
+                TEST_WIDTH,
+                TEST_HEIGHT,
+                Color.RED,
+                Color.YELLOW,
+                Color.GREEN,
+                Color.BLUE
+            )
             endRecording()
         }
         val executor = Executors.newSingleThreadExecutor()
diff --git a/graphics/graphics-core/src/androidTest/java/androidx/graphics/TestUtils.kt b/graphics/graphics-core/src/androidTest/java/androidx/graphics/TestUtils.kt
index 645a199..7958b18 100644
--- a/graphics/graphics-core/src/androidTest/java/androidx/graphics/TestUtils.kt
+++ b/graphics/graphics-core/src/androidTest/java/androidx/graphics/TestUtils.kt
@@ -17,8 +17,39 @@
 package androidx.graphics
 
 import android.graphics.Bitmap
+import android.graphics.Canvas
+import android.graphics.Paint
+import android.opengl.EGL14
+import androidx.graphics.opengl.egl.EGLConfigAttributes
+import androidx.graphics.opengl.egl.EGLManager
+import androidx.graphics.opengl.egl.EGLSpec
+import androidx.graphics.opengl.egl.EGLVersion
 import org.junit.Assert
 
+fun drawSquares(
+    canvas: Canvas,
+    width: Int,
+    height: Int,
+    topLeft: Int,
+    topRight: Int,
+    bottomLeft: Int,
+    bottomRight: Int
+) {
+    val paint = Paint()
+    val widthF = width.toFloat()
+    val heightF = height.toFloat()
+    val halfWidth = widthF / 2f
+    val halfHeight = heightF / 2f
+    canvas.drawRect(0f, 0f, halfWidth, halfHeight,
+        paint.apply { color = topLeft })
+    canvas.drawRect(halfWidth, 0f, widthF, halfHeight,
+        paint.apply { color = topRight })
+    canvas.drawRect(0f, halfHeight, halfWidth, heightF,
+        paint.apply { color = bottomLeft })
+    canvas.drawRect(halfWidth, halfHeight, widthF, heightF,
+        paint.apply { color = bottomRight })
+}
+
 fun Bitmap.verifyQuadrants(
     topLeft: Int,
     topRight: Int,
@@ -44,4 +75,48 @@
     Assert.assertEquals(bottomRight, getPixel(width - 2, height / 2 + 2))
     Assert.assertEquals(bottomRight, getPixel(width - 2, height - 2))
     Assert.assertEquals(bottomRight, getPixel(width / 2 + 2, height - 2))
+}
+
+fun Bitmap.isAllColor(targetColor: Int): Boolean {
+    for (i in 0 until width) {
+        for (j in 0 until height) {
+            if (getPixel(i, j) != targetColor) {
+                return false
+            }
+        }
+    }
+    return true
+}
+
+fun withEgl(block: (egl: EGLManager) -> Unit) {
+    val egl = createAndSetupEGLManager(EGLSpec.V14)
+    try {
+        block(egl)
+    } finally {
+        releaseEGLManager(egl)
+    }
+}
+
+// Helper method to create and initialize an EGLManager
+private fun createAndSetupEGLManager(eglSpec: EGLSpec = EGLSpec.V14): EGLManager {
+    val egl = EGLManager(eglSpec)
+    Assert.assertEquals(EGLVersion.Unknown, egl.eglVersion)
+    Assert.assertEquals(EGL14.EGL_NO_CONTEXT, egl.eglContext)
+
+    egl.initialize()
+
+    val config = egl.loadConfig(EGLConfigAttributes.RGBA_8888)
+    if (config == null) {
+        Assert.fail("Config 888 should be supported")
+    }
+
+    egl.createContext(config!!)
+    return egl
+}
+
+// Helper method to release EGLManager
+private fun releaseEGLManager(egl: EGLManager) {
+    egl.release()
+    Assert.assertEquals(EGLVersion.Unknown, egl.eglVersion)
+    Assert.assertEquals(EGL14.EGL_NO_CONTEXT, egl.eglContext)
 }
\ No newline at end of file
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
new file mode 100644
index 0000000..c0d0a6a
--- /dev/null
+++ b/graphics/graphics-core/src/androidTest/java/androidx/graphics/lowlatency/SingleBufferedCanvasRendererV29Test.kt
@@ -0,0 +1,418 @@
+/*
+ * 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.graphics.lowlatency
+
+import android.graphics.Bitmap
+import android.graphics.Canvas
+import android.graphics.Color
+import android.graphics.ColorSpace
+import android.hardware.HardwareBuffer
+import android.os.Build
+import androidx.annotation.RequiresApi
+import androidx.graphics.drawSquares
+import androidx.graphics.isAllColor
+import androidx.graphics.opengl.egl.supportsNativeAndroidFence
+import androidx.graphics.surface.SurfaceControlCompat
+import androidx.graphics.surface.SurfaceControlCompat.Companion.BUFFER_TRANSFORM_IDENTITY
+import androidx.graphics.verifyQuadrants
+import androidx.graphics.withEgl
+import androidx.hardware.SyncFenceCompat
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SdkSuppress
+import androidx.test.filters.SmallTest
+import java.util.concurrent.CountDownLatch
+import java.util.concurrent.Executors
+import java.util.concurrent.TimeUnit
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertNotNull
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+@SmallTest
+class SingleBufferedCanvasRendererV29Test {
+
+    companion object {
+        const val TEST_WIDTH = 20
+        const val TEST_HEIGHT = 20
+    }
+
+    data class RectColors(
+        val topLeft: Int,
+        val topRight: Int,
+        val bottomLeft: Int,
+        val bottomRight: Int
+    )
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.Q)
+    @Test
+    fun testRenderFrameRotate0() {
+        testRenderWithTransform(
+            BUFFER_TRANSFORM_IDENTITY,
+            RectColors(
+                topLeft = Color.RED,
+                topRight = Color.YELLOW,
+                bottomRight = Color.BLUE,
+                bottomLeft = Color.GREEN
+            ),
+            RectColors(
+                topLeft = Color.RED,
+                topRight = Color.YELLOW,
+                bottomRight = Color.BLUE,
+                bottomLeft = Color.GREEN
+            )
+        )
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.Q)
+    @Test
+    fun testRenderFrameRotate90() {
+        testRenderWithTransform(
+            SurfaceControlCompat.BUFFER_TRANSFORM_ROTATE_90,
+            RectColors(
+                topLeft = Color.RED,
+                topRight = Color.YELLOW,
+                bottomRight = Color.BLUE,
+                bottomLeft = Color.GREEN
+            ),
+            RectColors(
+                topLeft = Color.YELLOW,
+                topRight = Color.BLUE,
+                bottomRight = Color.GREEN,
+                bottomLeft = Color.RED
+            )
+        )
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.Q)
+    @Test
+    fun testRenderFrameRotate180() {
+        testRenderWithTransform(
+            SurfaceControlCompat.BUFFER_TRANSFORM_ROTATE_180,
+            RectColors(
+                topLeft = Color.RED,
+                topRight = Color.YELLOW,
+                bottomRight = Color.BLUE,
+                bottomLeft = Color.GREEN
+            ),
+            RectColors(
+                topLeft = Color.BLUE,
+                topRight = Color.GREEN,
+                bottomRight = Color.RED,
+                bottomLeft = Color.YELLOW
+            )
+        )
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.Q)
+    @Test
+    fun testRenderFrameRotate270() {
+        testRenderWithTransform(
+            SurfaceControlCompat.BUFFER_TRANSFORM_ROTATE_270,
+            RectColors(
+                topLeft = Color.RED,
+                topRight = Color.YELLOW,
+                bottomRight = Color.BLUE,
+                bottomLeft = Color.GREEN
+            ),
+            RectColors(
+                topLeft = Color.GREEN,
+                topRight = Color.RED,
+                bottomRight = Color.YELLOW,
+                bottomLeft = Color.BLUE
+            )
+        )
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.Q)
+    @Test
+    fun testClearRenderer() {
+        val transformer = BufferTransformer().apply {
+            computeTransform(TEST_WIDTH, TEST_HEIGHT, BUFFER_TRANSFORM_IDENTITY)
+        }
+        val executor = Executors.newSingleThreadExecutor()
+        val firstRenderLatch = CountDownLatch(1)
+        val clearLatch = CountDownLatch(2)
+        var buffer: HardwareBuffer? = null
+        val renderer = SingleBufferedCanvasRendererV29(
+            TEST_WIDTH,
+            TEST_HEIGHT,
+            transformer,
+            executor,
+            object : SingleBufferedCanvasRenderer.RenderCallbacks<Unit> {
+                override fun render(canvas: Canvas, width: Int, height: Int, param: Unit) {
+                    canvas.drawColor(Color.RED)
+                }
+
+                override fun onBufferReady(
+                    hardwareBuffer: HardwareBuffer,
+                    syncFenceCompat: SyncFenceCompat?
+                ) {
+                    syncFenceCompat?.awaitForever()
+                    buffer = hardwareBuffer
+                    firstRenderLatch.countDown()
+                    clearLatch.countDown()
+                }
+            }).apply {
+                // See: b/236394768 Workaround for ANGLE issue where FBOs with HardwareBuffer
+                // attachments are not executed until a glReadPixels call is made
+                forceFlush.set(true)
+            }
+        try {
+            renderer.render(Unit)
+            firstRenderLatch.await(3000, TimeUnit.MILLISECONDS)
+            renderer.clear()
+            assertTrue(clearLatch.await(3000, TimeUnit.MILLISECONDS))
+            assertNotNull(buffer)
+            val colorSpace = ColorSpace.get(ColorSpace.Named.LINEAR_SRGB)
+            val bitmap = Bitmap.wrapHardwareBuffer(buffer!!, colorSpace)
+                ?.copy(Bitmap.Config.ARGB_8888, false)
+            assertNotNull(bitmap)
+            assertTrue(bitmap!!.isAllColor(Color.TRANSPARENT))
+        } finally {
+            val latch = CountDownLatch(1)
+            renderer.release(true) {
+                executor.shutdownNow()
+                latch.countDown()
+            }
+            assertTrue(latch.await(3000, TimeUnit.MILLISECONDS))
+        }
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.Q)
+    @Test
+    fun testCancelPending() {
+        val transformer = BufferTransformer().apply {
+            computeTransform(TEST_WIDTH, TEST_HEIGHT, BUFFER_TRANSFORM_IDENTITY)
+        }
+        val executor = Executors.newSingleThreadExecutor()
+        var buffer: HardwareBuffer? = null
+        val initialDrawLatch = CountDownLatch(1)
+        val bufferReadyLatch = CountDownLatch(1)
+        val waitForRequestLatch = CountDownLatch(1)
+
+        var drawCancelledRequestLatch: CountDownLatch? = null
+        val renderer = SingleBufferedCanvasRendererV29(
+            TEST_WIDTH,
+            TEST_HEIGHT,
+            transformer,
+            executor,
+            object : SingleBufferedCanvasRenderer.RenderCallbacks<Int> {
+                override fun render(canvas: Canvas, width: Int, height: Int, param: Int) {
+                    canvas.drawColor(param)
+                    initialDrawLatch.countDown()
+                }
+
+                override fun onBufferReady(
+                    hardwareBuffer: HardwareBuffer,
+                    syncFenceCompat: SyncFenceCompat?
+                ) {
+                    syncFenceCompat?.awaitForever()
+                    buffer = hardwareBuffer
+                    bufferReadyLatch.countDown()
+                    drawCancelledRequestLatch?.countDown()
+                }
+            })
+        try {
+            renderer.render(Color.RED)
+            assertTrue(initialDrawLatch.await(3000, TimeUnit.MILLISECONDS))
+
+            executor.execute {
+                waitForRequestLatch.await()
+            }
+
+            drawCancelledRequestLatch = CountDownLatch(2)
+            renderer.render(Color.GREEN)
+            renderer.render(Color.YELLOW)
+            renderer.cancelPending()
+            waitForRequestLatch.countDown()
+
+            assertTrue(bufferReadyLatch.await(3000, TimeUnit.MILLISECONDS))
+            // Because the requests were cancelled this latch should not be signalled
+            assertFalse(drawCancelledRequestLatch.await(1000, TimeUnit.MILLISECONDS))
+            assertNotNull(buffer)
+            val colorSpace = ColorSpace.get(ColorSpace.Named.LINEAR_SRGB)
+            val bitmap = Bitmap.wrapHardwareBuffer(buffer!!, colorSpace)
+                ?.copy(Bitmap.Config.ARGB_8888, false)
+            assertNotNull(bitmap)
+            assertTrue(bitmap!!.isAllColor(Color.RED))
+        } finally {
+            val latch = CountDownLatch(1)
+            renderer.release(true) {
+                executor.shutdownNow()
+                latch.countDown()
+            }
+            assertTrue(latch.await(3000, TimeUnit.MILLISECONDS))
+        }
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.Q)
+    @Test
+    fun testMultiReleasesDoesNotCrash() {
+        val transformer = BufferTransformer()
+        val executor = Executors.newSingleThreadExecutor()
+        val renderer = SingleBufferedCanvasRendererV29(
+            TEST_WIDTH,
+            TEST_HEIGHT,
+            transformer,
+            executor,
+            object : SingleBufferedCanvasRenderer.RenderCallbacks<Void> {
+                override fun render(canvas: Canvas, width: Int, height: Int, param: Void) {
+                    // NO-OP
+                }
+
+                override fun onBufferReady(
+                    hardwareBuffer: HardwareBuffer,
+                    syncFenceCompat: SyncFenceCompat?
+                ) {
+                    // NO-OP
+                }
+            })
+        try {
+            val latch = CountDownLatch(1)
+            renderer.release(true) {
+                executor.shutdownNow()
+                latch.countDown()
+            }
+            assertTrue(latch.await(3000, TimeUnit.MILLISECONDS))
+            renderer.release(true)
+        } finally {
+            if (!executor.isShutdown) {
+                executor.shutdownNow()
+            }
+        }
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.Q)
+    @Test
+    fun testRendererVisibleFlag() {
+        var supportsNativeAndroidFence = false
+        withEgl { eglManager ->
+            supportsNativeAndroidFence = eglManager.supportsNativeAndroidFence()
+        }
+        if (!supportsNativeAndroidFence) {
+            return
+        }
+        val transformer = BufferTransformer().apply {
+            computeTransform(TEST_WIDTH, TEST_HEIGHT, BUFFER_TRANSFORM_IDENTITY)
+        }
+        val executor = Executors.newSingleThreadExecutor()
+        var syncFenceNull = false
+        var drawLatch: CountDownLatch? = null
+        val renderer = SingleBufferedCanvasRendererV29(
+            TEST_WIDTH,
+            TEST_HEIGHT,
+            transformer,
+            executor,
+            object : SingleBufferedCanvasRenderer.RenderCallbacks<Int> {
+                override fun render(canvas: Canvas, width: Int, height: Int, param: Int) {
+                    canvas.drawColor(param)
+                }
+
+                override fun onBufferReady(
+                    hardwareBuffer: HardwareBuffer,
+                    syncFenceCompat: SyncFenceCompat?
+                ) {
+                    syncFenceNull = syncFenceCompat == null
+                    syncFenceCompat?.awaitForever()
+                    drawLatch?.countDown()
+                }
+            })
+        try {
+            renderer.isVisible = false
+            drawLatch = CountDownLatch(1)
+            renderer.render(Color.RED)
+            assertTrue(drawLatch.await(3000, TimeUnit.MILLISECONDS))
+            assertFalse(syncFenceNull)
+
+            renderer.isVisible = true
+            drawLatch = CountDownLatch(1)
+            renderer.render(Color.BLUE)
+            assertTrue(drawLatch.await(3000, TimeUnit.MILLISECONDS))
+            assertTrue(syncFenceNull)
+        } finally {
+            val latch = CountDownLatch(1)
+            renderer.release(true) {
+                executor.shutdownNow()
+                latch.countDown()
+            }
+            assertTrue(latch.await(3000, TimeUnit.MILLISECONDS))
+        }
+    }
+
+    @RequiresApi(Build.VERSION_CODES.Q)
+    private fun testRenderWithTransform(
+        transform: Int,
+        actualColors: RectColors,
+        expectedColors: RectColors
+    ) {
+        val transformer = BufferTransformer()
+        transformer.computeTransform(TEST_WIDTH, TEST_HEIGHT, transform)
+        val executor = Executors.newSingleThreadExecutor()
+        var buffer: HardwareBuffer? = null
+        val renderLatch = CountDownLatch(1)
+        val renderer = SingleBufferedCanvasRendererV29(
+            TEST_WIDTH,
+            TEST_HEIGHT,
+            transformer,
+            executor,
+            object : SingleBufferedCanvasRenderer.RenderCallbacks<Int> {
+                override fun render(canvas: Canvas, width: Int, height: Int, param: Int) {
+                    drawSquares(
+                        canvas,
+                        width,
+                        height,
+                        actualColors.topLeft,
+                        actualColors.topRight,
+                        actualColors.bottomLeft,
+                        actualColors.bottomRight
+                    )
+                }
+
+                override fun onBufferReady(
+                    hardwareBuffer: HardwareBuffer,
+                    syncFenceCompat: SyncFenceCompat?
+                ) {
+                    syncFenceCompat?.awaitForever()
+                    buffer = hardwareBuffer
+                    renderLatch.countDown()
+                }
+            })
+        try {
+            renderer.render(0)
+            assertTrue(renderLatch.await(3000, TimeUnit.MILLISECONDS))
+            assertNotNull(buffer)
+            val colorSpace = ColorSpace.get(ColorSpace.Named.LINEAR_SRGB)
+            val bitmap = Bitmap.wrapHardwareBuffer(buffer!!, colorSpace)
+                ?.copy(Bitmap.Config.ARGB_8888, false)
+            assertNotNull(bitmap)
+            bitmap!!.verifyQuadrants(
+                expectedColors.topLeft,
+                expectedColors.topRight,
+                expectedColors.bottomLeft,
+                expectedColors.bottomRight
+            )
+        } finally {
+            val latch = CountDownLatch(1)
+            renderer.release(true) {
+                executor.shutdownNow()
+                latch.countDown()
+            }
+            assertTrue(latch.await(3000, TimeUnit.MILLISECONDS))
+        }
+    }
+}
\ No newline at end of file
diff --git a/graphics/graphics-core/src/androidTest/java/androidx/graphics/opengl/GLRendererTest.kt b/graphics/graphics-core/src/androidTest/java/androidx/graphics/opengl/GLRendererTest.kt
index fbb3d71..a03d42d 100644
--- a/graphics/graphics-core/src/androidTest/java/androidx/graphics/opengl/GLRendererTest.kt
+++ b/graphics/graphics-core/src/androidTest/java/androidx/graphics/opengl/GLRendererTest.kt
@@ -40,6 +40,7 @@
 import androidx.annotation.RequiresApi
 import androidx.annotation.WorkerThread
 import androidx.graphics.SurfaceTextureRenderer
+import androidx.graphics.isAllColor
 import androidx.graphics.lowlatency.LineRenderer
 import androidx.graphics.lowlatency.Rectangle
 import androidx.hardware.SyncFenceCompat
@@ -949,8 +950,6 @@
                 val surface = Surface(surfaceTexture)
                 val canvas = surface.lockCanvas(null)
                 canvas.save()
-                // GL is flipped vertically from Android's canvas so flip the canvas here
-                canvas.scale(1f, -1f, width / 2f, height / 2f)
                 val paint = Paint()
                 // top left
                 canvas.drawRect(0f, 0f, width / 2f, height / 2f,
@@ -1056,9 +1055,6 @@
         val frameHandler = Handler(frameHandlerThread.looper)
         val renderNode = RenderNode("node").apply {
             setPosition(0, 0, width, height)
-            scaleY = -1f
-            pivotX = width / 2f
-            pivotY = height / 2f
             val canvas = beginRecording()
             val paint = Paint()
             // top left
@@ -1343,17 +1339,6 @@
         assertTrue(copyLatch.await(3000, TimeUnit.MILLISECONDS))
     }
 
-    private fun Bitmap.isAllColor(targetColor: Int): Boolean {
-        for (i in 0 until width) {
-            for (j in 0 until height) {
-                if (getPixel(i, j) != targetColor) {
-                    return false
-                }
-            }
-        }
-        return true
-    }
-
     private fun genTexture(): Int {
         val buffer = IntArray(1)
         GLES20.glGenTextures(1, buffer, 0)
diff --git a/graphics/graphics-core/src/androidTest/java/androidx/graphics/opengl/SyncStrategyTest.kt b/graphics/graphics-core/src/androidTest/java/androidx/graphics/opengl/SyncStrategyTest.kt
index 09d8470..482afc6 100644
--- a/graphics/graphics-core/src/androidTest/java/androidx/graphics/opengl/SyncStrategyTest.kt
+++ b/graphics/graphics-core/src/androidTest/java/androidx/graphics/opengl/SyncStrategyTest.kt
@@ -16,19 +16,15 @@
 
 package androidx.graphics.opengl
 
-import android.opengl.EGL14
 import android.os.Build
 import androidx.annotation.RequiresApi
 import androidx.graphics.lowlatency.FrontBufferSyncStrategy
 import androidx.graphics.lowlatency.FrontBufferUtils
-import androidx.graphics.opengl.egl.EGLConfigAttributes
-import androidx.graphics.opengl.egl.EGLManager
 import androidx.graphics.opengl.egl.EGLSpec
-import androidx.graphics.opengl.egl.EGLVersion
 import androidx.graphics.opengl.egl.supportsNativeAndroidFence
+import androidx.graphics.withEgl
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
-import org.junit.Assert
 import org.junit.Assert.assertTrue
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -101,37 +97,4 @@
             }
         }
     }
-
-    private fun withEgl(block: (egl: EGLManager) -> Unit) {
-        val egl = createAndSetupEGLManager(EGLSpec.V14)
-        try {
-            block(egl)
-        } finally {
-            releaseEGLManager(egl)
-        }
-    }
-
-    // Helper method to create and initialize an EGLManager
-    fun createAndSetupEGLManager(eglSpec: EGLSpec = EGLSpec.V14): EGLManager {
-        val egl = EGLManager(eglSpec)
-        Assert.assertEquals(EGLVersion.Unknown, egl.eglVersion)
-        Assert.assertEquals(EGL14.EGL_NO_CONTEXT, egl.eglContext)
-
-        egl.initialize()
-
-        val config = egl.loadConfig(EGLConfigAttributes.RGBA_8888)
-        if (config == null) {
-            Assert.fail("Config 888 should be supported")
-        }
-
-        egl.createContext(config!!)
-        return egl
-    }
-
-    // Helper method to release EGLManager
-    fun releaseEGLManager(egl: EGLManager) {
-        egl.release()
-        Assert.assertEquals(EGLVersion.Unknown, egl.eglVersion)
-        Assert.assertEquals(EGL14.EGL_NO_CONTEXT, egl.eglContext)
-    }
 }
\ No newline at end of file
diff --git a/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/ParamQueue.kt b/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/ParamQueue.kt
index a0b1765..167bd13 100644
--- a/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/ParamQueue.kt
+++ b/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/ParamQueue.kt
@@ -80,4 +80,8 @@
             mParams.add(param)
         }
     }
+
+    fun count(): Int = mLock.withLock { mParams.size }
+
+    fun isEmpty() = count() == 0
 }
\ No newline at end of file
diff --git a/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/SingleBufferedCanvasRenderer.kt b/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/SingleBufferedCanvasRenderer.kt
new file mode 100644
index 0000000..8a66107
--- /dev/null
+++ b/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/SingleBufferedCanvasRenderer.kt
@@ -0,0 +1,88 @@
+/*
+ * 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.graphics.lowlatency
+
+import android.graphics.Canvas
+import android.hardware.HardwareBuffer
+import android.os.Build
+import androidx.annotation.RequiresApi
+import androidx.annotation.WorkerThread
+import androidx.hardware.SyncFenceCompat
+import java.util.concurrent.Executor
+
+/**
+ * Interface to provide an abstraction around implementations for a low latency hardware
+ * accelerated [Canvas] that provides a [HardwareBuffer] with the [Canvas] rendered scene
+ */
+internal interface SingleBufferedCanvasRenderer<T> {
+
+    interface RenderCallbacks<T> {
+        @WorkerThread
+        fun render(canvas: Canvas, width: Int, height: Int, param: T)
+
+        @WorkerThread
+        fun onBufferReady(hardwareBuffer: HardwareBuffer, syncFenceCompat: SyncFenceCompat?)
+    }
+
+    /**
+     * Render into the [HardwareBuffer] with the given parameter and bounds
+     */
+    fun render(param: T)
+
+    /**
+     * Flag to indicate whether or not the contents of the [SingleBufferedCanvasRenderer] are visible.
+     * This is used to help internal state to determine appropriate synchronization
+     */
+    var isVisible: Boolean
+
+    /**
+     * Releases resources associated with [SingleBufferedCanvasRenderer] instance. Attempts to
+     * use this object after it is closed will be ignored
+     */
+    fun release(cancelPending: Boolean, onReleaseComplete: (() -> Unit)? = null)
+
+    /**
+     * Clear the contents of the [HardwareBuffer]
+     */
+    fun clear()
+
+    /**
+     * Cancel all pending render requests
+     */
+    fun cancelPending()
+
+    companion object {
+
+        @RequiresApi(Build.VERSION_CODES.Q)
+        fun <T> create(
+            width: Int,
+            height: Int,
+            bufferTransformer: BufferTransformer,
+            executor: Executor,
+            bufferReadyListener: RenderCallbacks<T>
+        ): SingleBufferedCanvasRenderer<T> {
+            // TODO return different instance for corresponding platform version
+            return SingleBufferedCanvasRendererV29(
+                width,
+                height,
+                bufferTransformer,
+                executor,
+                bufferReadyListener
+            )
+        }
+    }
+}
\ No newline at end of file
diff --git a/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/SingleBufferedCanvasRendererV29.kt b/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/SingleBufferedCanvasRendererV29.kt
new file mode 100644
index 0000000..042ecb3
--- /dev/null
+++ b/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/SingleBufferedCanvasRendererV29.kt
@@ -0,0 +1,407 @@
+/*
+ * 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.graphics.lowlatency
+
+import android.graphics.RenderNode
+import android.graphics.SurfaceTexture
+import android.hardware.HardwareBuffer
+import android.opengl.GLES20
+import android.opengl.Matrix
+import android.os.Build
+import android.os.Handler
+import android.os.Looper
+import android.util.Log
+import androidx.annotation.RequiresApi
+import androidx.annotation.WorkerThread
+import androidx.graphics.SurfaceTextureRenderer
+import androidx.graphics.lowlatency.FrontBufferUtils.Companion.obtainHardwareBufferUsageFlags
+import androidx.graphics.opengl.FrameBuffer
+import androidx.graphics.opengl.FrameBufferRenderer
+import androidx.graphics.opengl.GLRenderer
+import androidx.graphics.opengl.QuadTextureRenderer
+import androidx.graphics.opengl.egl.EGLManager
+import androidx.graphics.opengl.egl.EGLSpec
+import androidx.hardware.SyncFenceCompat
+import java.nio.IntBuffer
+import java.util.concurrent.Executor
+import java.util.concurrent.atomic.AtomicBoolean
+import java.util.concurrent.atomic.AtomicInteger
+
+@RequiresApi(Build.VERSION_CODES.Q)
+internal class SingleBufferedCanvasRendererV29<T>(
+    private val width: Int,
+    private val height: Int,
+    private val bufferTransformer: BufferTransformer,
+    private val executor: Executor,
+    private val callbacks: SingleBufferedCanvasRenderer.RenderCallbacks<T>,
+) : SingleBufferedCanvasRenderer<T> {
+
+    private val mMainHandler = Handler(Looper.myLooper() ?: Looper.getMainLooper())
+
+    private val mRenderNode = RenderNode("renderNode").apply {
+        setPosition(
+            0,
+            0,
+            this@SingleBufferedCanvasRendererV29.width,
+            this@SingleBufferedCanvasRendererV29.height
+        )
+    }
+
+    /**
+     * Runnable used to execute the request to render batched parameters
+     */
+    private val mRenderPendingRunnable = Runnable { renderPendingParameters() }
+
+    /**
+     * Runnable used to execute the request to clear buffer content on screen
+     */
+    private val mClearContentsRunnable = Runnable {
+        mFrameBufferRenderer.clear()
+        obtainFrameBufferTarget().requestRender()
+    }
+
+    /**
+     * SurfaceTextureRenderer used to render contents of a RenderNode into a SurfaceTexture
+     * that is then rendered into a HardwareBuffer for consumption
+     */
+    private val mSurfaceTextureRenderer = SurfaceTextureRenderer(
+            mRenderNode,
+            width,
+            height,
+            mMainHandler
+        ) { texture ->
+            mSurfaceTexture = texture
+            obtainFrameBufferTarget().requestRender()
+        }
+
+    /**
+     * Helper method to request the provided RenderNode content to be drawn on the texture
+     * rendering thread
+     */
+    internal fun dispatchRenderTextureRequest() {
+        executor.execute(mRenderPendingRunnable)
+    }
+
+    /**
+     * Helper method to request clearing the contents of the destination HardwareBuffer
+     */
+    private fun dispatchClearRequest() {
+        executor.execute(mClearContentsRunnable)
+    }
+
+    /**
+     * Maximum number of pending renders to the SurfaceTexture before we queue up parameters
+     * and wait for the consumer to catch up. Some devices have very fast input sampling rates
+     * which make the producing side much faster than the consuming side. We batch the pending
+     * parameters and when the consuming side catches up, we batch and render all the pending
+     * parameters into the SurfaceTexture that then gets drawn into the destination HardwareBuffer.
+     * This ensures we don't drop any attempts to render.
+     */
+    private val mMaxPendingBuffers = 2
+
+    /**
+     * Keep track of the number of pending renders of the source SurfaceTexture to the destination
+     * HardwareBuffer
+     */
+    private val mPendingBuffers = AtomicInteger(0)
+
+    /**
+     * Source SurfaceTexture that the destination of content to be rendered from the provided
+     * RenderNode
+     */
+    private var mSurfaceTexture: SurfaceTexture? = null
+
+    /**
+     * HardwareBuffer flags for front buffered rendering
+     */
+    private val mHardwareBufferUsageFlags = obtainHardwareBufferUsageFlags()
+
+    // ---------- GLThread ------
+
+    /**
+     * [FrontBufferSyncStrategy] used for [FrameBufferRenderer] to conditionally decide
+     * when to create a [SyncFenceCompat] for transaction calls.
+     */
+    private val mFrontBufferSyncStrategy = FrontBufferSyncStrategy(mHardwareBufferUsageFlags)
+
+    /**
+     * Shader that handles rendering a texture as a quad into the destination
+     */
+    private var mQuadRenderer: QuadTextureRenderer? = null
+
+    /**
+     * Texture id of the SurfaceTexture that is to be rendered
+     */
+    private var mTextureId: Int = -1
+
+    /**
+     * Scratch buffer used for gen/delete texture operations
+     */
+    private val buffer = IntArray(1)
+
+    // ---------- GLThread ------
+
+    private val mFrameBufferRenderer = FrameBufferRenderer(
+        object : FrameBufferRenderer.RenderCallback {
+
+            private val mMVPMatrix = FloatArray(16)
+            private val mProjection = FloatArray(16)
+
+            private fun obtainQuadRenderer(): QuadTextureRenderer =
+                mQuadRenderer ?: QuadTextureRenderer().apply {
+                    GLES20.glGenTextures(1, buffer, 0)
+                    mTextureId = buffer[0]
+                    mSurfaceTexture?.let { texture ->
+                        texture.attachToGLContext(mTextureId)
+                        setSurfaceTexture(texture)
+                    }
+                    mQuadRenderer = this
+                }
+
+            override fun obtainFrameBuffer(egl: EGLSpec): FrameBuffer {
+                return mFrontBufferLayer ?: FrameBuffer(
+                    egl,
+                    HardwareBuffer.create(
+                        bufferTransformer.glWidth,
+                        bufferTransformer.glHeight,
+                        HardwareBuffer.RGBA_8888,
+                        1,
+                        mHardwareBufferUsageFlags
+                    )
+                ).also { mFrontBufferLayer = it }
+            }
+
+            @RequiresApi(Build.VERSION_CODES.S)
+            override fun onDraw(eglManager: EGLManager) {
+                mSurfaceTexture?.let { texture ->
+                    val bufferWidth = bufferTransformer.glWidth
+                    val bufferHeight = bufferTransformer.glHeight
+                    GLES20.glViewport(0, 0, bufferWidth, bufferHeight)
+                    Matrix.orthoM(
+                        mMVPMatrix,
+                        0,
+                        0f,
+                        bufferWidth.toFloat(),
+                        0f,
+                        bufferHeight.toFloat(),
+                        -1f,
+                        1f
+                    )
+
+                    Matrix.multiplyMM(mProjection, 0, mMVPMatrix, 0, bufferTransformer.transform, 0)
+                    // texture.updateTexImage is called within QuadTextureRenderer#draw
+                    obtainQuadRenderer().draw(mProjection, width.toFloat(), height.toFloat())
+                    texture.releaseTexImage()
+                }
+            }
+
+            override fun onDrawComplete(
+                frameBuffer: FrameBuffer,
+                syncFenceCompat: SyncFenceCompat?
+            ) {
+                if (forceFlush.get()) {
+                    // See b/236394768. On some ANGLE versions, attempting to do a glClear + flush
+                    // does not actually flush pixels to FBOs with HardwareBuffer attachments
+                    // For testing purposes when verifying clear use cases, do a GPU readback
+                    // to actually executed any pending clear operations to verify output
+                    GLES20.glReadPixels(0, 0, 1, 1, GLES20.GL_RGBA, GLES20.GL_UNSIGNED_BYTE,
+                        IntBuffer.wrap(IntArray(1)))
+                }
+
+                val state = mState.get()
+                if (state != RELEASED) {
+                    executor.execute {
+                        callbacks.onBufferReady(frameBuffer.hardwareBuffer, syncFenceCompat)
+                    }
+                }
+                val pending = mPendingBuffers.decrementAndGet()
+                if (state == PENDING_RELEASE && pending <= 0) {
+                    mMainHandler.post(::tearDown)
+                } else {
+                    // After rendering see if there are additional queued content to render
+                    // If all images within the SurfaceTexture are pending being drawn into the
+                    // destination HardwareBuffer, they are queued up to be batch rendered after
+                    // texture image has been released
+                    dispatchRenderTextureRequest()
+                }
+            }
+        },
+        mFrontBufferSyncStrategy
+    )
+
+    /**
+     * [GLRenderer] used to render contents of the SurfaceTexture into a HardwareBuffer
+     */
+    private val mGLRenderer = GLRenderer().apply { start() }
+
+    /**
+     * Thread safe queue of parameters to be consumed in on the texture render thread that are
+     * provided in [SingleBufferedCanvasRenderer.render]
+     */
+    private val mParams = ParamQueue<T>()
+
+    /**
+     * State to determine if [release] has been called on this [SingleBufferedCanvasRendererV29]
+     * instance. If true, all subsequent operations are a no-op
+     */
+    private val mState = AtomicInteger(ACTIVE)
+
+    /**
+     * Pending release callback to be invoked when the renderer is torn down
+     */
+    private var mReleaseComplete: (() -> Unit)? = null
+
+    /**
+     * Flag to maintain visibility state on the main thread
+     */
+    private var mIsVisible = false
+
+    private fun isReleased(): Boolean {
+        val state = mState.get()
+        return state == RELEASED || state == PENDING_RELEASE
+    }
+
+    @WorkerThread
+    internal fun renderPendingParameters() {
+        val pending = mPendingBuffers.get()
+        // If there are pending requests to draw and we are not waiting on the consuming side
+        // to catch up, then render content in the RenderNode and issue a request to draw into
+        // the SurfaceTexture
+        if (pending < mMaxPendingBuffers) {
+            val params = mParams.release()
+            if (params.isNotEmpty()) {
+                val canvas = mRenderNode.beginRecording()
+                for (p in params) {
+                    callbacks.render(canvas, width, height, p)
+                }
+                mRenderNode.endRecording()
+                mPendingBuffers.incrementAndGet()
+                mSurfaceTextureRenderer.renderFrame()
+            }
+        }
+    }
+
+    override var isVisible: Boolean
+        set(value) {
+            mGLRenderer.execute {
+                mFrontBufferSyncStrategy.isVisible = value
+            }
+            mIsVisible = value
+        }
+        get() = mFrontBufferSyncStrategy.isVisible
+
+    private var mFrontBufferLayer: FrameBuffer? = null
+
+    private fun obtainFrameBufferTarget(): GLRenderer.RenderTarget =
+        mFrameBufferTarget ?: mGLRenderer.createRenderTarget(width, height, mFrameBufferRenderer)
+            .also { mFrameBufferTarget = it }
+
+    private var mFrameBufferTarget: GLRenderer.RenderTarget? = null
+
+    override fun render(param: T) {
+        ifNotReleased {
+            mParams.add(param)
+            if (mPendingBuffers.get() < mMaxPendingBuffers) {
+                dispatchRenderTextureRequest()
+            }
+        }
+    }
+
+    override fun release(cancelPending: Boolean, onReleaseComplete: (() -> Unit)?) {
+        ifNotReleased {
+            mReleaseComplete = onReleaseComplete
+            if (cancelPending || !isPendingRendering()) {
+                mState.set(RELEASED)
+                cancelPending()
+                tearDown()
+            } else {
+                mState.set(PENDING_RELEASE)
+            }
+        }
+    }
+
+    private fun isPendingRendering() = mParams.isEmpty() || mPendingBuffers.get() > 0
+
+    internal fun tearDown() {
+        mFrameBufferTarget?.detach(true) {
+            // GL Thread
+            mQuadRenderer?.release()
+            if (mTextureId != -1) {
+                buffer[0] = mTextureId
+                GLES20.glDeleteTextures(1, buffer, 0)
+                mTextureId = -1
+            }
+        }
+        mGLRenderer.stop(false)
+        mSurfaceTexture?.let { texture ->
+            if (!texture.isReleased) {
+                texture.release()
+            }
+        }
+        mRenderNode.discardDisplayList()
+        mSurfaceTextureRenderer.release()
+
+        mReleaseComplete?.let { callback ->
+            mMainHandler.post(callback)
+        }
+    }
+
+    override fun cancelPending() {
+        ifNotReleased {
+            mParams.clear()
+        }
+    }
+
+    override fun clear() {
+        ifNotReleased {
+            dispatchClearRequest()
+        }
+    }
+
+    private inline fun ifNotReleased(block: () -> Unit) {
+        if (!isReleased()) {
+            block()
+        } else {
+            Log.w(TAG, "Attempt to use already released renderer")
+        }
+    }
+
+    // See: b/236394768. Some test emulator instances have not picked up the fix so
+    // apply a workaround here for testing purposes
+    internal val forceFlush = AtomicBoolean(false)
+
+    private companion object {
+
+        /**
+         * Indicates the renderer is an in active state and can render content
+         */
+        const val ACTIVE = 0
+
+        /**
+         * Indicates the renderer is released and is no longer in a valid state to render content
+         */
+        const val RELEASED = 1
+
+        /**
+         * Indicates the renderer is completing rendering of current pending frames but not accepting
+         * new requests to render
+         */
+        const val PENDING_RELEASE = 2
+
+        const val TAG = "SingleBufferedCanvasV29"
+    }
+}
\ No newline at end of file
diff --git a/graphics/graphics-core/src/main/java/androidx/graphics/opengl/QuadTextureRenderer.kt b/graphics/graphics-core/src/main/java/androidx/graphics/opengl/QuadTextureRenderer.kt
index c45e203..f838ec4 100644
--- a/graphics/graphics-core/src/main/java/androidx/graphics/opengl/QuadTextureRenderer.kt
+++ b/graphics/graphics-core/src/main/java/androidx/graphics/opengl/QuadTextureRenderer.kt
@@ -260,7 +260,7 @@
             void main(void)
             {
                 gl_Position = $uVPMatrix * $aPosition;
-                $vTexCoord = vec2($tVPMatrix * vec4($aTexCoord, 1.0, 1.0));
+                $vTexCoord = vec2($tVPMatrix * vec4($aTexCoord.x, 1.0 - $aTexCoord.y, 1.0, 1.0));
             }
             """
 
diff --git a/health/health-services-client/OWNERS b/health/health-services-client/OWNERS
index 7079ccb..ff2eed8 100644
--- a/health/health-services-client/OWNERS
+++ b/health/health-services-client/OWNERS
@@ -1,5 +1,5 @@
 # Bug component: 1126127
-aakanksha@google.com
 jlannin@google.com
 shobana@google.com
 smskelley@google.com
+sutterlin@google.com
diff --git a/libraryversions.toml b/libraryversions.toml
index e2f8957..eb751d2 100644
--- a/libraryversions.toml
+++ b/libraryversions.toml
@@ -16,7 +16,7 @@
 BLUETOOTH = "1.0.0-alpha01"
 BROWSER = "1.6.0-alpha01"
 BUILDSRC_TESTS = "1.0.0-alpha01"
-CAMERA = "1.3.0-alpha04"
+CAMERA = "1.3.0-alpha05"
 CAMERA_PIPE = "1.0.0-alpha01"
 CARDVIEW = "1.1.0-alpha01"
 CAR_APP = "1.4.0-alpha01"
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/compat/XConverters.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/compat/XConverters.kt
index 0c6ab9a..317de2c 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/compat/XConverters.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/compat/XConverters.kt
@@ -31,7 +31,6 @@
 import androidx.room.compiler.processing.XType
 import androidx.room.compiler.processing.XTypeElement
 import androidx.room.compiler.processing.XVariableElement
-import androidx.room.compiler.processing.compat.XConverters.toXProcessing
 import androidx.room.compiler.processing.javac.JavacAnnotation
 import androidx.room.compiler.processing.javac.JavacAnnotationValue
 import androidx.room.compiler.processing.javac.JavacElement
@@ -54,7 +53,9 @@
 import androidx.room.compiler.processing.ksp.KspProcessingEnv
 import androidx.room.compiler.processing.ksp.KspType
 import androidx.room.compiler.processing.ksp.KspTypeElement
+import androidx.room.compiler.processing.ksp.synthetic.KspSyntheticContinuationParameterElement
 import androidx.room.compiler.processing.ksp.synthetic.KspSyntheticPropertyMethodElement
+import androidx.room.compiler.processing.ksp.synthetic.KspSyntheticReceiverParameterElement
 import com.google.devtools.ksp.symbol.KSAnnotated
 import com.google.devtools.ksp.symbol.KSAnnotation
 import com.google.devtools.ksp.symbol.KSClassDeclaration
@@ -171,7 +172,14 @@
     fun XTypeElement.toKS(): KSClassDeclaration = (this as KspTypeElement).declaration
 
     @JvmStatic
-    fun XElement.toKS(): KSAnnotated = (this as KspElement).declaration
+    fun XElement.toKS(): KSAnnotated =
+        when (this) {
+            is KspElement -> this.declaration
+            is KspSyntheticPropertyMethodElement -> this.field.declaration
+            is KspSyntheticContinuationParameterElement -> this.enclosingElement.declaration
+            is KspSyntheticReceiverParameterElement -> this.enclosingElement.declaration
+            else -> error("Don't know how to convert element of type '${this::class}' to KSP")
+        }
 
     @JvmStatic
     fun XExecutableElement.toKS(): KSFunctionDeclaration =
@@ -249,6 +257,9 @@
         return when (this) {
             is JavacElement -> this.env
             is KspElement -> this.env
+            is KspSyntheticContinuationParameterElement -> this.env
+            is KspSyntheticPropertyMethodElement -> this.env
+            is KspSyntheticReceiverParameterElement -> this.env
             else -> error("Unexpected element: $this")
         }
     }
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/synthetic/KspSyntheticContinuationParameterElement.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/synthetic/KspSyntheticContinuationParameterElement.kt
index 9fe597a..620f91f 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/synthetic/KspSyntheticContinuationParameterElement.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/synthetic/KspSyntheticContinuationParameterElement.kt
@@ -39,7 +39,7 @@
  * this is what KAPT generates and Room needs it as long as it generates java code.
  */
 internal class KspSyntheticContinuationParameterElement(
-    private val env: KspProcessingEnv,
+    val env: KspProcessingEnv,
     override val enclosingElement: KspMethodElement
 ) : XExecutableParameterElement,
     XEquality,
diff --git a/text/text/src/androidTest/java/androidx/compose/ui/text/android/FontPaddingTest.kt b/text/text/src/androidTest/java/androidx/compose/ui/text/android/FontPaddingTest.kt
index f7e466f..d84f481 100644
--- a/text/text/src/androidTest/java/androidx/compose/ui/text/android/FontPaddingTest.kt
+++ b/text/text/src/androidTest/java/androidx/compose/ui/text/android/FontPaddingTest.kt
@@ -18,10 +18,8 @@
 
 import android.graphics.Typeface
 import android.text.TextPaint
-import android.os.Build
 import androidx.core.content.res.ResourcesCompat
 import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.test.filters.SdkSuppress
 import androidx.test.filters.SmallTest
 import androidx.test.platform.app.InstrumentationRegistry
 import androidx.testutils.fonts.R
@@ -181,12 +179,7 @@
     }
 
     @Test
-    @SdkSuppress(maxSdkVersion = 33) // b/262909049: Failing on SDK 34
     fun tallTypefaceTextIsTwiceTheHeightOfLatinTypefaceTextMultiLine() {
-        if (Build.VERSION.SDK_INT == 33 && Build.VERSION.CODENAME != "REL") {
-            return // b/262909049: Do not run this test on pre-release Android U.
-        }
-
         val latinLayout = TextLayout(latinTextMultiLine, typeface = latinTypeface)
         val tallLayout = TextLayout(tallTextMultiLine, typeface = tallTypeface)
 
@@ -212,7 +205,8 @@
             charSequence = text,
             textPaint = textPaint,
             includePadding = includePadding,
-            fallbackLineSpacing = false
+            fallbackLineSpacing = false,
+            width = fontSize * 1.5f
         )
     }
-}
\ No newline at end of file
+}
diff --git a/text/text/src/androidTest/java/androidx/compose/ui/text/android/TextLayoutTest.kt b/text/text/src/androidTest/java/androidx/compose/ui/text/android/TextLayoutTest.kt
index 2621b1e..4de574f 100644
--- a/text/text/src/androidTest/java/androidx/compose/ui/text/android/TextLayoutTest.kt
+++ b/text/text/src/androidTest/java/androidx/compose/ui/text/android/TextLayoutTest.kt
@@ -53,7 +53,7 @@
 
     @Test
     fun constructor_default_values() {
-        val textLayout = TextLayout(charSequence = "", textPaint = TextPaint())
+        val textLayout = TextLayout(charSequence = "", textPaint = TextPaint(), width = 0f)
         val frameworkLayout = textLayout.layout
 
         assertThat(frameworkLayout.width).isEqualTo(0)
diff --git a/text/text/src/main/java/androidx/compose/ui/text/android/TextLayout.kt b/text/text/src/main/java/androidx/compose/ui/text/android/TextLayout.kt
index cb51b971..d55d854 100644
--- a/text/text/src/main/java/androidx/compose/ui/text/android/TextLayout.kt
+++ b/text/text/src/main/java/androidx/compose/ui/text/android/TextLayout.kt
@@ -114,7 +114,7 @@
 @InternalPlatformTextApi
 class TextLayout constructor(
     charSequence: CharSequence,
-    width: Float = 0.0f,
+    width: Float,
     textPaint: TextPaint,
     @TextLayoutAlignment alignment: Int = DEFAULT_ALIGNMENT,
     ellipsize: TextUtils.TruncateAt? = null,
diff --git a/tv/tv-material/build.gradle b/tv/tv-material/build.gradle
index 6b59668..2978d14 100644
--- a/tv/tv-material/build.gradle
+++ b/tv/tv-material/build.gradle
@@ -50,6 +50,7 @@
     androidTestImplementation(project(":compose:ui:ui-test"))
     androidTestImplementation(project(":compose:ui:ui-test-junit4"))
     androidTestImplementation(project(":compose:test-utils"))
+    androidTestImplementation(project(":test:screenshot:screenshot"))
     androidTestImplementation(libs.testRunner)
 
     samples(project(":tv:tv-samples"))
@@ -60,6 +61,8 @@
     defaultConfig {
         minSdkVersion 21
     }
+    sourceSets.androidTest.assets.srcDirs +=
+             project.rootDir.absolutePath + "/../../golden/tv/compose/material3"
 }
 
 androidx {
diff --git a/tv/tv-material/src/androidTest/java/androidx/tv/material3/GoldenCommon.kt b/tv/tv-material/src/androidTest/java/androidx/tv/material3/GoldenCommon.kt
new file mode 100644
index 0000000..49b8130
--- /dev/null
+++ b/tv/tv-material/src/androidTest/java/androidx/tv/material3/GoldenCommon.kt
@@ -0,0 +1,19 @@
+/*
+ * 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.tv.material3
+
+internal const val TV_GOLDEN_MATERIAL3 = "tv/compose/material3"
\ No newline at end of file
diff --git a/tv/tv-material/src/androidTest/java/androidx/tv/material3/TabRowTest.kt b/tv/tv-material/src/androidTest/java/androidx/tv/material3/TabRowTest.kt
index fd811f2..198e493 100644
--- a/tv/tv-material/src/androidTest/java/androidx/tv/material3/TabRowTest.kt
+++ b/tv/tv-material/src/androidTest/java/androidx/tv/material3/TabRowTest.kt
@@ -16,6 +16,7 @@
 
 package androidx.tv.material3
 
+import android.os.Build
 import androidx.compose.foundation.background
 import androidx.compose.foundation.border
 import androidx.compose.foundation.focusable
@@ -34,6 +35,7 @@
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
 import androidx.compose.runtime.setValue
+import androidx.compose.testutils.assertAgainstGolden
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.focus.FocusRequester
 import androidx.compose.ui.focus.focusRequester
@@ -42,20 +44,52 @@
 import androidx.compose.ui.platform.testTag
 import androidx.compose.ui.test.assertIsDisplayed
 import androidx.compose.ui.test.assertIsFocused
+import androidx.compose.ui.test.captureToImage
 import androidx.compose.ui.test.junit4.createComposeRule
 import androidx.compose.ui.test.onNodeWithTag
 import androidx.compose.ui.test.onNodeWithText
 import androidx.compose.ui.unit.DpRect
 import androidx.compose.ui.unit.dp
+import androidx.test.filters.SdkSuppress
 import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.screenshot.AndroidXScreenshotTestRule
 import org.junit.Rule
 import org.junit.Test
 
 class TabRowTest {
-
     @get:Rule
     val rule = createComposeRule()
 
+    @get:Rule
+    val screenshotRule = AndroidXScreenshotTestRule(TV_GOLDEN_MATERIAL3)
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+    @Test
+    fun tabRow_pillIndicatorScreenshot() {
+        val tabs = constructTabs(count = 3)
+        val testTag = "TabRowTestTag"
+
+        setContent(
+            tabs = tabs,
+            contentBuilder = {
+                Box {
+                    var selectedTabIndex by remember { mutableStateOf(0) }
+                    TabRowSample(
+                        tabs = tabs,
+                        modifier = Modifier.testTag(testTag),
+                        selectedTabIndex = selectedTabIndex,
+                        onFocus = { selectedTabIndex = it }
+                    )
+                }
+            }
+        )
+
+        rule
+            .onNodeWithTag(testTag)
+            .captureToImage()
+            .assertAgainstGolden(screenshotRule, "tab_row_pill_indicator_default")
+    }
+
     @Test
     fun tabRow_shouldNotCrashWithOnly1Tab() {
         val tabs = constructTabs(count = 1)
@@ -233,6 +267,7 @@
 private fun TabRowSample(
     tabs: List<String>,
     selectedTabIndex: Int,
+    modifier: Modifier = Modifier,
     onFocus: (index: Int) -> Unit = {},
     onClick: (index: Int) -> Unit = onFocus,
     buildTab: @Composable ((index: Int, tab: String) -> Unit) = @Composable { index, tab ->
@@ -272,6 +307,7 @@
         if (indicator != null) {
             TabRow(
                 selectedTabIndex = selectedTabIndex,
+                modifier = modifier,
                 indicator = indicator,
                 separator = { Spacer(modifier = Modifier.width(12.dp)) },
             ) {
@@ -280,6 +316,7 @@
         } else {
             TabRow(
                 selectedTabIndex = selectedTabIndex,
+                modifier = modifier,
                 separator = { Spacer(modifier = Modifier.width(12.dp)) },
             ) {
                 tabs.forEachIndexed { index, tab -> buildTab(index, tab) }
diff --git a/wear/compose/compose-foundation/api/current.txt b/wear/compose/compose-foundation/api/current.txt
index 384b168..117b84c 100644
--- a/wear/compose/compose-foundation/api/current.txt
+++ b/wear/compose/compose-foundation/api/current.txt
@@ -171,6 +171,30 @@
     property public final androidx.compose.ui.text.font.FontWeight? fontWeight;
   }
 
+  public final class ExpandableItemsDefaults {
+    method @androidx.compose.runtime.Composable public void Chevron(float progress, long color, optional androidx.compose.ui.Modifier modifier, optional float strokeWidth);
+    method public androidx.compose.animation.core.AnimationSpec<java.lang.Float> getCollapseAnimationSpec();
+    method public androidx.compose.animation.core.AnimationSpec<java.lang.Float> getExpandAnimationSpec();
+    property public final androidx.compose.animation.core.AnimationSpec<java.lang.Float> collapseAnimationSpec;
+    property public final androidx.compose.animation.core.AnimationSpec<java.lang.Float> expandAnimationSpec;
+    field public static final androidx.wear.compose.foundation.ExpandableItemsDefaults INSTANCE;
+  }
+
+  public final class ExpandableItemsState {
+    method public float getExpandProgress();
+    method public boolean isExpanded();
+    method public void setExpanded(boolean);
+    method public void toggle();
+    property public final float expandProgress;
+    property public final boolean expanded;
+  }
+
+  public final class ExpandableKt {
+    method public static void expandableItem(androidx.wear.compose.foundation.lazy.ScalingLazyListScope, androidx.wear.compose.foundation.ExpandableItemsState state, optional Object? key, kotlin.jvm.functions.Function1<? super java.lang.Boolean,kotlin.Unit> content);
+    method public static void expandableItems(androidx.wear.compose.foundation.lazy.ScalingLazyListScope, androidx.wear.compose.foundation.ExpandableItemsState state, int count, optional kotlin.jvm.functions.Function1<? super java.lang.Integer,?>? key, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.layout.BoxScope,? super java.lang.Integer,kotlin.Unit> itemContent);
+    method @androidx.compose.runtime.Composable public static androidx.wear.compose.foundation.ExpandableItemsState rememberExpandableItemsState(optional boolean initiallyExpanded, optional androidx.compose.animation.core.AnimationSpec<java.lang.Float> expandAnimationSpec, optional androidx.compose.animation.core.AnimationSpec<java.lang.Float> collapseAnimationSpec);
+  }
+
 }
 
 package androidx.wear.compose.foundation.lazy {
diff --git a/wear/compose/compose-foundation/api/public_plus_experimental_current.txt b/wear/compose/compose-foundation/api/public_plus_experimental_current.txt
index eda89f7..abaaf5d 100644
--- a/wear/compose/compose-foundation/api/public_plus_experimental_current.txt
+++ b/wear/compose/compose-foundation/api/public_plus_experimental_current.txt
@@ -171,6 +171,30 @@
     property public final androidx.compose.ui.text.font.FontWeight? fontWeight;
   }
 
+  public final class ExpandableItemsDefaults {
+    method @androidx.compose.runtime.Composable public void Chevron(float progress, long color, optional androidx.compose.ui.Modifier modifier, optional float strokeWidth);
+    method public androidx.compose.animation.core.AnimationSpec<java.lang.Float> getCollapseAnimationSpec();
+    method public androidx.compose.animation.core.AnimationSpec<java.lang.Float> getExpandAnimationSpec();
+    property public final androidx.compose.animation.core.AnimationSpec<java.lang.Float> collapseAnimationSpec;
+    property public final androidx.compose.animation.core.AnimationSpec<java.lang.Float> expandAnimationSpec;
+    field public static final androidx.wear.compose.foundation.ExpandableItemsDefaults INSTANCE;
+  }
+
+  public final class ExpandableItemsState {
+    method public float getExpandProgress();
+    method public boolean isExpanded();
+    method public void setExpanded(boolean);
+    method public void toggle();
+    property public final float expandProgress;
+    property public final boolean expanded;
+  }
+
+  public final class ExpandableKt {
+    method public static void expandableItem(androidx.wear.compose.foundation.lazy.ScalingLazyListScope, androidx.wear.compose.foundation.ExpandableItemsState state, optional Object? key, kotlin.jvm.functions.Function1<? super java.lang.Boolean,kotlin.Unit> content);
+    method public static void expandableItems(androidx.wear.compose.foundation.lazy.ScalingLazyListScope, androidx.wear.compose.foundation.ExpandableItemsState state, int count, optional kotlin.jvm.functions.Function1<? super java.lang.Integer,?>? key, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.layout.BoxScope,? super java.lang.Integer,kotlin.Unit> itemContent);
+    method @androidx.compose.runtime.Composable public static androidx.wear.compose.foundation.ExpandableItemsState rememberExpandableItemsState(optional boolean initiallyExpanded, optional androidx.compose.animation.core.AnimationSpec<java.lang.Float> expandAnimationSpec, optional androidx.compose.animation.core.AnimationSpec<java.lang.Float> collapseAnimationSpec);
+  }
+
   @kotlin.RequiresOptIn(message="This Wear Foundation API is experimental and is likely to change or to be removed in" + " the future.") @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) public @interface ExperimentalWearFoundationApi {
   }
 
diff --git a/wear/compose/compose-foundation/api/restricted_current.txt b/wear/compose/compose-foundation/api/restricted_current.txt
index 384b168..117b84c 100644
--- a/wear/compose/compose-foundation/api/restricted_current.txt
+++ b/wear/compose/compose-foundation/api/restricted_current.txt
@@ -171,6 +171,30 @@
     property public final androidx.compose.ui.text.font.FontWeight? fontWeight;
   }
 
+  public final class ExpandableItemsDefaults {
+    method @androidx.compose.runtime.Composable public void Chevron(float progress, long color, optional androidx.compose.ui.Modifier modifier, optional float strokeWidth);
+    method public androidx.compose.animation.core.AnimationSpec<java.lang.Float> getCollapseAnimationSpec();
+    method public androidx.compose.animation.core.AnimationSpec<java.lang.Float> getExpandAnimationSpec();
+    property public final androidx.compose.animation.core.AnimationSpec<java.lang.Float> collapseAnimationSpec;
+    property public final androidx.compose.animation.core.AnimationSpec<java.lang.Float> expandAnimationSpec;
+    field public static final androidx.wear.compose.foundation.ExpandableItemsDefaults INSTANCE;
+  }
+
+  public final class ExpandableItemsState {
+    method public float getExpandProgress();
+    method public boolean isExpanded();
+    method public void setExpanded(boolean);
+    method public void toggle();
+    property public final float expandProgress;
+    property public final boolean expanded;
+  }
+
+  public final class ExpandableKt {
+    method public static void expandableItem(androidx.wear.compose.foundation.lazy.ScalingLazyListScope, androidx.wear.compose.foundation.ExpandableItemsState state, optional Object? key, kotlin.jvm.functions.Function1<? super java.lang.Boolean,kotlin.Unit> content);
+    method public static void expandableItems(androidx.wear.compose.foundation.lazy.ScalingLazyListScope, androidx.wear.compose.foundation.ExpandableItemsState state, int count, optional kotlin.jvm.functions.Function1<? super java.lang.Integer,?>? key, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.layout.BoxScope,? super java.lang.Integer,kotlin.Unit> itemContent);
+    method @androidx.compose.runtime.Composable public static androidx.wear.compose.foundation.ExpandableItemsState rememberExpandableItemsState(optional boolean initiallyExpanded, optional androidx.compose.animation.core.AnimationSpec<java.lang.Float> expandAnimationSpec, optional androidx.compose.animation.core.AnimationSpec<java.lang.Float> collapseAnimationSpec);
+  }
+
 }
 
 package androidx.wear.compose.foundation.lazy {
diff --git a/wear/compose/compose-material/samples/src/main/java/androidx/wear/compose/material/samples/ExpandableSample.kt b/wear/compose/compose-foundation/samples/src/main/java/androidx/wear/compose/foundation/samples/ExpandableSample.kt
similarity index 92%
rename from wear/compose/compose-material/samples/src/main/java/androidx/wear/compose/material/samples/ExpandableSample.kt
rename to wear/compose/compose-foundation/samples/src/main/java/androidx/wear/compose/foundation/samples/ExpandableSample.kt
index afa936f..3769f31 100644
--- a/wear/compose/compose-material/samples/src/main/java/androidx/wear/compose/material/samples/ExpandableSample.kt
+++ b/wear/compose/compose-foundation/samples/src/main/java/androidx/wear/compose/foundation/samples/ExpandableSample.kt
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package androidx.wear.compose.material.samples
+package androidx.wear.compose.foundation.samples
 
 import androidx.annotation.Sampled
 import androidx.compose.foundation.layout.Spacer
@@ -25,15 +25,15 @@
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.unit.dp
+import androidx.wear.compose.foundation.ExpandableItemsDefaults
+import androidx.wear.compose.foundation.expandableItem
+import androidx.wear.compose.foundation.expandableItems
 import androidx.wear.compose.foundation.lazy.ScalingLazyColumn
+import androidx.wear.compose.foundation.rememberExpandableItemsState
 import androidx.wear.compose.material.Chip
-import androidx.wear.compose.material.ExpandableItemsDefaults
 import androidx.wear.compose.material.MaterialTheme
 import androidx.wear.compose.material.OutlinedChip
 import androidx.wear.compose.material.Text
-import androidx.wear.compose.material.expandableItem
-import androidx.wear.compose.material.expandableItems
-import androidx.wear.compose.material.rememberExpandableItemsState
 
 @Sampled
 @Composable
diff --git a/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/Expandable.kt b/wear/compose/compose-foundation/src/commonMain/kotlin/androidx/wear/compose/foundation/Expandable.kt
similarity index 95%
rename from wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/Expandable.kt
rename to wear/compose/compose-foundation/src/commonMain/kotlin/androidx/wear/compose/foundation/Expandable.kt
index 47cffe6..1fafbf8 100644
--- a/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/Expandable.kt
+++ b/wear/compose/compose-foundation/src/commonMain/kotlin/androidx/wear/compose/foundation/Expandable.kt
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package androidx.wear.compose.material
+package androidx.wear.compose.foundation
 
 import androidx.compose.animation.core.Animatable
 import androidx.compose.animation.core.AnimationSpec
@@ -45,10 +45,10 @@
  * Create and [remember] an [ExpandableItemsState]
  *
  * Example of an expandable list:
- * @sample androidx.wear.compose.material.samples.ExpandableWithItemsSample
+ * @sample androidx.wear.compose.foundation.samples.ExpandableWithItemsSample
  *
  * Example of an expandable text:
- * @sample androidx.wear.compose.material.samples.ExpandableTextSample
+ * @sample androidx.wear.compose.foundation.samples.ExpandableTextSample
  *
  * @param initiallyExpanded The initial value of the state.
  * @param expandAnimationSpec The [AnimationSpec] to use when showing the extra information.
@@ -70,7 +70,7 @@
  * Adds a series of items, that will be expanded/collapsed according to the [ExpandableItemsState]
  *
  * Example of an expandable list:
- * @sample androidx.wear.compose.material.samples.ExpandableWithItemsSample
+ * @sample androidx.wear.compose.foundation.samples.ExpandableWithItemsSample
  *
  * @param state The [ExpandableItemsState] connected to these items to.
  * @param count The number of items
@@ -115,7 +115,7 @@
  * Adds a single item, that will be expanded/collapsed according to the [ExpandableItemsState].
  *
  * Example of an expandable text:
- * @sample androidx.wear.compose.material.samples.ExpandableTextSample
+ * @sample androidx.wear.compose.foundation.samples.ExpandableTextSample
  *
  * The item should support two levels of information display (for example, a text showing a few
  * lines in the collapsed state, and more in the expanded state)
@@ -229,16 +229,16 @@
      *
      * @param progress The point in the animation we are displaying this chevron in. 0f means pointing
      * downward, 1f means pointing upward.
+     * @param color The color to draw this chevron on.
      * @param modifier Modifier to be applied to the AnimatableChevron. This can be used to provide a
      * content description for accessibility.
-     * @param color The color to draw this chevron on.
      * @param strokeWidth The stroke width used to draw this chevron.
      */
     @Composable
     public fun Chevron(
         progress: Float,
+        color: Color,
         modifier: Modifier = Modifier,
-        color: Color = MaterialTheme.colors.onBackground,
         strokeWidth: Dp = 3.dp
     ) {
         Box(
diff --git a/wear/compose/compose-material/api/current.txt b/wear/compose/compose-material/api/current.txt
index 1be733d..761dc1e8 100644
--- a/wear/compose/compose-material/api/current.txt
+++ b/wear/compose/compose-material/api/current.txt
@@ -184,30 +184,6 @@
     method @Deprecated public static void curvedText(androidx.wear.compose.foundation.CurvedScope, String text, optional androidx.wear.compose.foundation.CurvedModifier modifier, optional long background, optional long color, optional long fontSize, optional androidx.wear.compose.foundation.CurvedTextStyle? style, optional androidx.wear.compose.foundation.CurvedDirection.Angular? angularDirection, optional int overflow);
   }
 
-  public final class ExpandableItemsDefaults {
-    method @androidx.compose.runtime.Composable public void Chevron(float progress, optional androidx.compose.ui.Modifier modifier, optional long color, optional float strokeWidth);
-    method public androidx.compose.animation.core.AnimationSpec<java.lang.Float> getCollapseAnimationSpec();
-    method public androidx.compose.animation.core.AnimationSpec<java.lang.Float> getExpandAnimationSpec();
-    property public final androidx.compose.animation.core.AnimationSpec<java.lang.Float> collapseAnimationSpec;
-    property public final androidx.compose.animation.core.AnimationSpec<java.lang.Float> expandAnimationSpec;
-    field public static final androidx.wear.compose.material.ExpandableItemsDefaults INSTANCE;
-  }
-
-  public final class ExpandableItemsState {
-    method public float getExpandProgress();
-    method public boolean isExpanded();
-    method public void setExpanded(boolean);
-    method public void toggle();
-    property public final float expandProgress;
-    property public final boolean expanded;
-  }
-
-  public final class ExpandableKt {
-    method public static void expandableItem(androidx.wear.compose.foundation.lazy.ScalingLazyListScope, androidx.wear.compose.material.ExpandableItemsState state, optional Object? key, kotlin.jvm.functions.Function1<? super java.lang.Boolean,kotlin.Unit> content);
-    method public static void expandableItems(androidx.wear.compose.foundation.lazy.ScalingLazyListScope, androidx.wear.compose.material.ExpandableItemsState state, int count, optional kotlin.jvm.functions.Function1<? super java.lang.Integer,?>? key, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.layout.BoxScope,? super java.lang.Integer,kotlin.Unit> itemContent);
-    method @androidx.compose.runtime.Composable public static androidx.wear.compose.material.ExpandableItemsState rememberExpandableItemsState(optional boolean initiallyExpanded, optional androidx.compose.animation.core.AnimationSpec<java.lang.Float> expandAnimationSpec, optional androidx.compose.animation.core.AnimationSpec<java.lang.Float> collapseAnimationSpec);
-  }
-
   public final class HorizontalPageIndicatorKt {
     method @androidx.compose.runtime.Composable public static void HorizontalPageIndicator(androidx.wear.compose.material.PageIndicatorState pageIndicatorState, optional androidx.compose.ui.Modifier modifier, optional int indicatorStyle, optional long selectedColor, optional long unselectedColor, optional float indicatorSize, optional float spacing, optional androidx.compose.ui.graphics.Shape indicatorShape);
   }
diff --git a/wear/compose/compose-material/api/public_plus_experimental_current.txt b/wear/compose/compose-material/api/public_plus_experimental_current.txt
index c1255f3..3dd2192 100644
--- a/wear/compose/compose-material/api/public_plus_experimental_current.txt
+++ b/wear/compose/compose-material/api/public_plus_experimental_current.txt
@@ -185,30 +185,6 @@
     method @Deprecated public static void curvedText(androidx.wear.compose.foundation.CurvedScope, String text, optional androidx.wear.compose.foundation.CurvedModifier modifier, optional long background, optional long color, optional long fontSize, optional androidx.wear.compose.foundation.CurvedTextStyle? style, optional androidx.wear.compose.foundation.CurvedDirection.Angular? angularDirection, optional int overflow);
   }
 
-  public final class ExpandableItemsDefaults {
-    method @androidx.compose.runtime.Composable public void Chevron(float progress, optional androidx.compose.ui.Modifier modifier, optional long color, optional float strokeWidth);
-    method public androidx.compose.animation.core.AnimationSpec<java.lang.Float> getCollapseAnimationSpec();
-    method public androidx.compose.animation.core.AnimationSpec<java.lang.Float> getExpandAnimationSpec();
-    property public final androidx.compose.animation.core.AnimationSpec<java.lang.Float> collapseAnimationSpec;
-    property public final androidx.compose.animation.core.AnimationSpec<java.lang.Float> expandAnimationSpec;
-    field public static final androidx.wear.compose.material.ExpandableItemsDefaults INSTANCE;
-  }
-
-  public final class ExpandableItemsState {
-    method public float getExpandProgress();
-    method public boolean isExpanded();
-    method public void setExpanded(boolean);
-    method public void toggle();
-    property public final float expandProgress;
-    property public final boolean expanded;
-  }
-
-  public final class ExpandableKt {
-    method public static void expandableItem(androidx.wear.compose.foundation.lazy.ScalingLazyListScope, androidx.wear.compose.material.ExpandableItemsState state, optional Object? key, kotlin.jvm.functions.Function1<? super java.lang.Boolean,kotlin.Unit> content);
-    method public static void expandableItems(androidx.wear.compose.foundation.lazy.ScalingLazyListScope, androidx.wear.compose.material.ExpandableItemsState state, int count, optional kotlin.jvm.functions.Function1<? super java.lang.Integer,?>? key, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.layout.BoxScope,? super java.lang.Integer,kotlin.Unit> itemContent);
-    method @androidx.compose.runtime.Composable public static androidx.wear.compose.material.ExpandableItemsState rememberExpandableItemsState(optional boolean initiallyExpanded, optional androidx.compose.animation.core.AnimationSpec<java.lang.Float> expandAnimationSpec, optional androidx.compose.animation.core.AnimationSpec<java.lang.Float> collapseAnimationSpec);
-  }
-
   @kotlin.RequiresOptIn(message="This Wear Material API is experimental and is likely to change or to be removed in" + " the future.") @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) public @interface ExperimentalWearMaterialApi {
   }
 
diff --git a/wear/compose/compose-material/api/restricted_current.txt b/wear/compose/compose-material/api/restricted_current.txt
index 1be733d..761dc1e8 100644
--- a/wear/compose/compose-material/api/restricted_current.txt
+++ b/wear/compose/compose-material/api/restricted_current.txt
@@ -184,30 +184,6 @@
     method @Deprecated public static void curvedText(androidx.wear.compose.foundation.CurvedScope, String text, optional androidx.wear.compose.foundation.CurvedModifier modifier, optional long background, optional long color, optional long fontSize, optional androidx.wear.compose.foundation.CurvedTextStyle? style, optional androidx.wear.compose.foundation.CurvedDirection.Angular? angularDirection, optional int overflow);
   }
 
-  public final class ExpandableItemsDefaults {
-    method @androidx.compose.runtime.Composable public void Chevron(float progress, optional androidx.compose.ui.Modifier modifier, optional long color, optional float strokeWidth);
-    method public androidx.compose.animation.core.AnimationSpec<java.lang.Float> getCollapseAnimationSpec();
-    method public androidx.compose.animation.core.AnimationSpec<java.lang.Float> getExpandAnimationSpec();
-    property public final androidx.compose.animation.core.AnimationSpec<java.lang.Float> collapseAnimationSpec;
-    property public final androidx.compose.animation.core.AnimationSpec<java.lang.Float> expandAnimationSpec;
-    field public static final androidx.wear.compose.material.ExpandableItemsDefaults INSTANCE;
-  }
-
-  public final class ExpandableItemsState {
-    method public float getExpandProgress();
-    method public boolean isExpanded();
-    method public void setExpanded(boolean);
-    method public void toggle();
-    property public final float expandProgress;
-    property public final boolean expanded;
-  }
-
-  public final class ExpandableKt {
-    method public static void expandableItem(androidx.wear.compose.foundation.lazy.ScalingLazyListScope, androidx.wear.compose.material.ExpandableItemsState state, optional Object? key, kotlin.jvm.functions.Function1<? super java.lang.Boolean,kotlin.Unit> content);
-    method public static void expandableItems(androidx.wear.compose.foundation.lazy.ScalingLazyListScope, androidx.wear.compose.material.ExpandableItemsState state, int count, optional kotlin.jvm.functions.Function1<? super java.lang.Integer,?>? key, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.layout.BoxScope,? super java.lang.Integer,kotlin.Unit> itemContent);
-    method @androidx.compose.runtime.Composable public static androidx.wear.compose.material.ExpandableItemsState rememberExpandableItemsState(optional boolean initiallyExpanded, optional androidx.compose.animation.core.AnimationSpec<java.lang.Float> expandAnimationSpec, optional androidx.compose.animation.core.AnimationSpec<java.lang.Float> collapseAnimationSpec);
-  }
-
   public final class HorizontalPageIndicatorKt {
     method @androidx.compose.runtime.Composable public static void HorizontalPageIndicator(androidx.wear.compose.material.PageIndicatorState pageIndicatorState, optional androidx.compose.ui.Modifier modifier, optional int indicatorStyle, optional long selectedColor, optional long unselectedColor, optional float indicatorSize, optional float spacing, optional androidx.compose.ui.graphics.Shape indicatorShape);
   }
diff --git a/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/ExpandableDemo.kt b/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/ExpandableDemo.kt
index 39f1e3c..e40b68a 100644
--- a/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/ExpandableDemo.kt
+++ b/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/ExpandableDemo.kt
@@ -29,18 +29,18 @@
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.unit.dp
+import androidx.wear.compose.foundation.ExpandableItemsDefaults
+import androidx.wear.compose.foundation.ExpandableItemsState
+import androidx.wear.compose.foundation.expandableItem
+import androidx.wear.compose.foundation.expandableItems
 import androidx.wear.compose.foundation.lazy.ScalingLazyColumn
 import androidx.wear.compose.foundation.lazy.ScalingLazyListScope
+import androidx.wear.compose.foundation.rememberExpandableItemsState
 import androidx.wear.compose.material.Chip
 import androidx.wear.compose.material.ChipDefaults
-import androidx.wear.compose.material.ExpandableItemsDefaults
 import androidx.wear.compose.material.MaterialTheme
 import androidx.wear.compose.material.OutlinedChip
-import androidx.wear.compose.material.ExpandableItemsState
 import androidx.wear.compose.material.Text
-import androidx.wear.compose.material.rememberExpandableItemsState
-import androidx.wear.compose.material.expandableItem
-import androidx.wear.compose.material.expandableItems
 
 @Composable
 fun ExpandableListItems() {
diff --git a/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/FoundationDemos.kt b/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/FoundationDemos.kt
index 49fb373..c0f9761 100644
--- a/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/FoundationDemos.kt
+++ b/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/FoundationDemos.kt
@@ -24,6 +24,8 @@
 import androidx.wear.compose.foundation.samples.CurvedFonts
 import androidx.wear.compose.foundation.samples.CurvedRowAndColumn
 import androidx.wear.compose.foundation.samples.CurvedWeight
+import androidx.wear.compose.foundation.samples.ExpandableTextSample
+import androidx.wear.compose.foundation.samples.ExpandableWithItemsSample
 import androidx.wear.compose.foundation.samples.HierarchicalFocusCoordinatorSample
 import androidx.wear.compose.foundation.samples.OversizeComposable
 import androidx.wear.compose.foundation.samples.ScalingLazyColumnEdgeAnchoredAndAnimatedScrollTo
@@ -35,6 +37,15 @@
 val WearFoundationDemos = DemoCategory(
     "Foundation",
     listOf(
+        DemoCategory(
+            "Expandables",
+            listOf(
+                ComposableDemo("Items in SLC") { ExpandableListItems() },
+                ComposableDemo("Expandable Text") { ExpandableText() },
+                ComposableDemo("Items Sample") { ExpandableWithItemsSample() },
+                ComposableDemo("Text Sample") { ExpandableTextSample() },
+            )
+        ),
         DemoCategory("CurvedLayout", listOf(
             ComposableDemo("Curved Row") { CurvedWorldDemo() },
             ComposableDemo("Curved Row and Column") { CurvedRowAndColumn() },
diff --git a/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/MaterialDemos.kt b/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/MaterialDemos.kt
index baf4417..45ecbf2 100644
--- a/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/MaterialDemos.kt
+++ b/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/MaterialDemos.kt
@@ -50,8 +50,6 @@
 import androidx.wear.compose.material.samples.CurvedTextDemo
 import androidx.wear.compose.material.samples.CurvedTextProviderDemo
 import androidx.wear.compose.material.samples.EdgeSwipeForSwipeToDismiss
-import androidx.wear.compose.material.samples.ExpandableTextSample
-import androidx.wear.compose.material.samples.ExpandableWithItemsSample
 import androidx.wear.compose.material.samples.FixedFontSize
 import androidx.wear.compose.material.samples.HorizontalPageIndicatorSample
 import androidx.wear.compose.material.samples.IndeterminateCircularProgressIndicator
@@ -130,15 +128,6 @@
     "Material",
     listOf(
         DemoCategory(
-            "Expandables",
-            listOf(
-                ComposableDemo("Items in SLC") { ExpandableListItems() },
-                ComposableDemo("Expandable Text") { ExpandableText() },
-                ComposableDemo("Items Sample") { ExpandableWithItemsSample() },
-                ComposableDemo("Text Sample") { ExpandableTextSample() },
-            )
-        ),
-        DemoCategory(
             "ScrollAway",
             listOf(
                 ComposableDemo("Column") { ScrollAwayColumnDemo() },
diff --git a/wear/protolayout/protolayout/api/current.txt b/wear/protolayout/protolayout/api/current.txt
index b4afdb2..f0977b7d 100644
--- a/wear/protolayout/protolayout/api/current.txt
+++ b/wear/protolayout/protolayout/api/current.txt
@@ -180,7 +180,9 @@
     method public static androidx.wear.protolayout.DimensionBuilders.DpProp dp(@Dimension(unit=androidx.annotation.Dimension.DP) float);
     method public static androidx.wear.protolayout.DimensionBuilders.EmProp em(int);
     method public static androidx.wear.protolayout.DimensionBuilders.EmProp em(float);
+    method public static androidx.wear.protolayout.DimensionBuilders.ExpandedDimensionProp expand();
     method public static androidx.wear.protolayout.DimensionBuilders.SpProp sp(@Dimension(unit=androidx.annotation.Dimension.SP) float);
+    method public static androidx.wear.protolayout.DimensionBuilders.WrappedDimensionProp wrap();
   }
 
   public static interface DimensionBuilders.ContainerDimension {
@@ -220,6 +222,16 @@
     method public androidx.wear.protolayout.DimensionBuilders.EmProp.Builder setValue(float);
   }
 
+  public static final class DimensionBuilders.ExpandedDimensionProp implements androidx.wear.protolayout.DimensionBuilders.ContainerDimension androidx.wear.protolayout.DimensionBuilders.ImageDimension {
+    method public float getLayoutWeight();
+  }
+
+  public static final class DimensionBuilders.ExpandedDimensionProp.Builder implements androidx.wear.protolayout.DimensionBuilders.ContainerDimension.Builder androidx.wear.protolayout.DimensionBuilders.ImageDimension.Builder {
+    ctor public DimensionBuilders.ExpandedDimensionProp.Builder();
+    method public androidx.wear.protolayout.DimensionBuilders.ExpandedDimensionProp build();
+    method public androidx.wear.protolayout.DimensionBuilders.ExpandedDimensionProp.Builder setLayoutWeight(float);
+  }
+
   public static interface DimensionBuilders.ImageDimension {
   }
 
@@ -256,6 +268,16 @@
     method public androidx.wear.protolayout.DimensionBuilders.SpacerDimension build();
   }
 
+  public static final class DimensionBuilders.WrappedDimensionProp implements androidx.wear.protolayout.DimensionBuilders.ContainerDimension {
+    method @Dimension(unit=androidx.annotation.Dimension.DP) public float getMinimumSizeDp();
+  }
+
+  public static final class DimensionBuilders.WrappedDimensionProp.Builder implements androidx.wear.protolayout.DimensionBuilders.ContainerDimension.Builder {
+    ctor public DimensionBuilders.WrappedDimensionProp.Builder();
+    method public androidx.wear.protolayout.DimensionBuilders.WrappedDimensionProp build();
+    method public androidx.wear.protolayout.DimensionBuilders.WrappedDimensionProp.Builder setMinimumSizeDp(@Dimension(unit=androidx.annotation.Dimension.DP) float);
+  }
+
   public final class LayoutElementBuilders {
     field public static final int ARC_ANCHOR_CENTER = 2; // 0x2
     field public static final int ARC_ANCHOR_END = 3; // 0x3
diff --git a/wear/protolayout/protolayout/api/public_plus_experimental_current.txt b/wear/protolayout/protolayout/api/public_plus_experimental_current.txt
index 19bb4e0..0a6f7bf 100644
--- a/wear/protolayout/protolayout/api/public_plus_experimental_current.txt
+++ b/wear/protolayout/protolayout/api/public_plus_experimental_current.txt
@@ -192,7 +192,9 @@
     method public static androidx.wear.protolayout.DimensionBuilders.DpProp dp(@Dimension(unit=androidx.annotation.Dimension.DP) float);
     method public static androidx.wear.protolayout.DimensionBuilders.EmProp em(int);
     method public static androidx.wear.protolayout.DimensionBuilders.EmProp em(float);
+    method public static androidx.wear.protolayout.DimensionBuilders.ExpandedDimensionProp expand();
     method public static androidx.wear.protolayout.DimensionBuilders.SpProp sp(@Dimension(unit=androidx.annotation.Dimension.SP) float);
+    method public static androidx.wear.protolayout.DimensionBuilders.WrappedDimensionProp wrap();
   }
 
   public static interface DimensionBuilders.ContainerDimension {
@@ -232,6 +234,16 @@
     method public androidx.wear.protolayout.DimensionBuilders.EmProp.Builder setValue(float);
   }
 
+  public static final class DimensionBuilders.ExpandedDimensionProp implements androidx.wear.protolayout.DimensionBuilders.ContainerDimension androidx.wear.protolayout.DimensionBuilders.ImageDimension {
+    method public float getLayoutWeight();
+  }
+
+  public static final class DimensionBuilders.ExpandedDimensionProp.Builder implements androidx.wear.protolayout.DimensionBuilders.ContainerDimension.Builder androidx.wear.protolayout.DimensionBuilders.ImageDimension.Builder {
+    ctor public DimensionBuilders.ExpandedDimensionProp.Builder();
+    method public androidx.wear.protolayout.DimensionBuilders.ExpandedDimensionProp build();
+    method public androidx.wear.protolayout.DimensionBuilders.ExpandedDimensionProp.Builder setLayoutWeight(float);
+  }
+
   public static interface DimensionBuilders.ImageDimension {
   }
 
@@ -268,6 +280,16 @@
     method public androidx.wear.protolayout.DimensionBuilders.SpacerDimension build();
   }
 
+  public static final class DimensionBuilders.WrappedDimensionProp implements androidx.wear.protolayout.DimensionBuilders.ContainerDimension {
+    method @Dimension(unit=androidx.annotation.Dimension.DP) public float getMinimumSizeDp();
+  }
+
+  public static final class DimensionBuilders.WrappedDimensionProp.Builder implements androidx.wear.protolayout.DimensionBuilders.ContainerDimension.Builder {
+    ctor public DimensionBuilders.WrappedDimensionProp.Builder();
+    method public androidx.wear.protolayout.DimensionBuilders.WrappedDimensionProp build();
+    method public androidx.wear.protolayout.DimensionBuilders.WrappedDimensionProp.Builder setMinimumSizeDp(@Dimension(unit=androidx.annotation.Dimension.DP) float);
+  }
+
   public final class LayoutElementBuilders {
     field public static final int ARC_ANCHOR_CENTER = 2; // 0x2
     field public static final int ARC_ANCHOR_END = 3; // 0x3
diff --git a/wear/protolayout/protolayout/api/restricted_current.txt b/wear/protolayout/protolayout/api/restricted_current.txt
index b4afdb2..f0977b7d 100644
--- a/wear/protolayout/protolayout/api/restricted_current.txt
+++ b/wear/protolayout/protolayout/api/restricted_current.txt
@@ -180,7 +180,9 @@
     method public static androidx.wear.protolayout.DimensionBuilders.DpProp dp(@Dimension(unit=androidx.annotation.Dimension.DP) float);
     method public static androidx.wear.protolayout.DimensionBuilders.EmProp em(int);
     method public static androidx.wear.protolayout.DimensionBuilders.EmProp em(float);
+    method public static androidx.wear.protolayout.DimensionBuilders.ExpandedDimensionProp expand();
     method public static androidx.wear.protolayout.DimensionBuilders.SpProp sp(@Dimension(unit=androidx.annotation.Dimension.SP) float);
+    method public static androidx.wear.protolayout.DimensionBuilders.WrappedDimensionProp wrap();
   }
 
   public static interface DimensionBuilders.ContainerDimension {
@@ -220,6 +222,16 @@
     method public androidx.wear.protolayout.DimensionBuilders.EmProp.Builder setValue(float);
   }
 
+  public static final class DimensionBuilders.ExpandedDimensionProp implements androidx.wear.protolayout.DimensionBuilders.ContainerDimension androidx.wear.protolayout.DimensionBuilders.ImageDimension {
+    method public float getLayoutWeight();
+  }
+
+  public static final class DimensionBuilders.ExpandedDimensionProp.Builder implements androidx.wear.protolayout.DimensionBuilders.ContainerDimension.Builder androidx.wear.protolayout.DimensionBuilders.ImageDimension.Builder {
+    ctor public DimensionBuilders.ExpandedDimensionProp.Builder();
+    method public androidx.wear.protolayout.DimensionBuilders.ExpandedDimensionProp build();
+    method public androidx.wear.protolayout.DimensionBuilders.ExpandedDimensionProp.Builder setLayoutWeight(float);
+  }
+
   public static interface DimensionBuilders.ImageDimension {
   }
 
@@ -256,6 +268,16 @@
     method public androidx.wear.protolayout.DimensionBuilders.SpacerDimension build();
   }
 
+  public static final class DimensionBuilders.WrappedDimensionProp implements androidx.wear.protolayout.DimensionBuilders.ContainerDimension {
+    method @Dimension(unit=androidx.annotation.Dimension.DP) public float getMinimumSizeDp();
+  }
+
+  public static final class DimensionBuilders.WrappedDimensionProp.Builder implements androidx.wear.protolayout.DimensionBuilders.ContainerDimension.Builder {
+    ctor public DimensionBuilders.WrappedDimensionProp.Builder();
+    method public androidx.wear.protolayout.DimensionBuilders.WrappedDimensionProp build();
+    method public androidx.wear.protolayout.DimensionBuilders.WrappedDimensionProp.Builder setMinimumSizeDp(@Dimension(unit=androidx.annotation.Dimension.DP) float);
+  }
+
   public final class LayoutElementBuilders {
     field public static final int ARC_ANCHOR_CENTER = 2; // 0x2
     field public static final int ARC_ANCHOR_END = 3; // 0x3
diff --git a/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/DimensionBuilders.java b/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/DimensionBuilders.java
index 4cae976..68392b2 100644
--- a/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/DimensionBuilders.java
+++ b/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/DimensionBuilders.java
@@ -35,6 +35,9 @@
 public final class DimensionBuilders {
     private DimensionBuilders() {}
 
+    private static final ExpandedDimensionProp EXPAND = new ExpandedDimensionProp.Builder().build();
+    private static final WrappedDimensionProp WRAP = new WrappedDimensionProp.Builder().build();
+
     /** Shortcut for building a {@link DpProp} using a measurement in DP. */
     @NonNull
     public static DpProp dp(@Dimension(unit = DP) float valueDp) {
@@ -66,6 +69,28 @@
     }
 
     /**
+     * Shortcut for building an {@link ExpandedDimensionProp} that will expand to the size of its
+     * parent.
+     *
+     * @since 1.0
+     */
+    @NonNull
+    public static ExpandedDimensionProp expand() {
+        return EXPAND;
+    }
+
+    /**
+     * Shortcut for building an {@link WrappedDimensionProp} that will shrink to the size of its
+     * children.
+     *
+     * @since 1.0
+     */
+    @NonNull
+    public static WrappedDimensionProp wrap() {
+        return WRAP;
+    }
+
+    /**
      * A type for linear dimensions, measured in dp.
      *
      * @since 1.0
@@ -373,7 +398,6 @@
      *
      * @since 1.0
      */
-    @RestrictTo(Scope.LIBRARY_GROUP)
     public static final class ExpandedDimensionProp implements ContainerDimension, ImageDimension {
         private final DimensionProto.ExpandedDimensionProp mImpl;
         @Nullable private final Fingerprint mFingerprint;
@@ -472,7 +496,6 @@
      *
      * @since 1.0
      */
-    @RestrictTo(Scope.LIBRARY_GROUP)
     public static final class WrappedDimensionProp implements ContainerDimension {
         private final DimensionProto.WrappedDimensionProp mImpl;
         @Nullable private final Fingerprint mFingerprint;