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;