diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/core/Timestamps.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/core/Timestamps.kt index 05b64a36c38cd..a64cb8a1a310f 100644 --- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/core/Timestamps.kt +++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/core/Timestamps.kt @@ -25,18 +25,22 @@ import javax.inject.Singleton /** A nanosecond timestamp */ @JvmInline public value class TimestampNs constructor(public val value: Long) { - public operator fun minus(other: TimestampNs): DurationNs = DurationNs(value - other.value) + public inline operator fun minus(other: TimestampNs): DurationNs = + DurationNs(value - other.value) - public operator fun plus(other: DurationNs): TimestampNs = TimestampNs(value + other.value) + public inline operator fun plus(other: DurationNs): TimestampNs = + TimestampNs(value + other.value) } @JvmInline public value class DurationNs(public val value: Long) { - public operator fun minus(other: DurationNs): DurationNs = DurationNs(value - other.value) + public inline operator fun minus(other: DurationNs): DurationNs = + DurationNs(value - other.value) - public operator fun plus(other: DurationNs): DurationNs = DurationNs(value + other.value) + public inline operator fun plus(other: DurationNs): DurationNs = DurationNs(value + other.value) - public operator fun plus(other: TimestampNs): TimestampNs = TimestampNs(value + other.value) + public inline operator fun plus(other: TimestampNs): TimestampNs = + TimestampNs(value + other.value) public operator fun compareTo(other: DurationNs): Int { return if (value == other.value) { @@ -49,7 +53,7 @@ public value class DurationNs(public val value: Long) { } public companion object { - public fun fromMs(durationMs: Long): DurationNs = DurationNs(durationMs * 1_000_000L) + public inline fun fromMs(durationMs: Long): DurationNs = DurationNs(durationMs * 1_000_000L) } } @@ -63,17 +67,18 @@ public class SystemTimeSource @Inject constructor() : TimeSource { } public object Timestamps { - public fun now(timeSource: TimeSource): TimestampNs = timeSource.now() + public inline fun now(timeSource: TimeSource): TimestampNs = timeSource.now() - public fun DurationNs.formatNs(): String = "$this ns" + public inline fun DurationNs.formatNs(): String = "$this ns" - public fun DurationNs.formatMs(decimals: Int = 3): String = + public inline fun DurationNs.formatMs(decimals: Int = 3): String = "%.${decimals}f ms".format(null, this.value / 1_000_000.0) - public fun TimestampNs.formatNs(): String = "$this ns" + public inline fun TimestampNs.formatNs(): String = "$this ns" - public fun TimestampNs.formatMs(): String = "${this.value / 1_000_000} ms" + public inline fun TimestampNs.formatMs(): String = "${this.value / 1_000_000} ms" - public fun TimestampNs.measureNow(timeSource: TimeSource = SystemTimeSource()): DurationNs = - now(timeSource).minus(this) + public inline fun TimestampNs.measureNow( + timeSource: TimeSource = SystemTimeSource() + ): DurationNs = now(timeSource) - this } diff --git a/work/work-runtime/api/current.txt b/work/work-runtime/api/current.txt index 35b839b4e3966..8bc927adecb8f 100644 --- a/work/work-runtime/api/current.txt +++ b/work/work-runtime/api/current.txt @@ -171,8 +171,9 @@ package androidx.work { method @InaccessibleFromKotlin public java.util.Map getKeyValueMap(); method public long getLong(String key, long defaultValue); method public long[]? getLongArray(String key); + method public String?[]? getNullableStringArray(String key); method public String? getString(String key); - method public String[]? getStringArray(String key); + method @Deprecated public String[]? getStringArray(String key); method public boolean hasKeyWithValueOfType(String key, Class klass); method public byte[] toByteArray(); property public java.util.Map keyValueMap; diff --git a/work/work-runtime/api/restricted_current.txt b/work/work-runtime/api/restricted_current.txt index 35b839b4e3966..8bc927adecb8f 100644 --- a/work/work-runtime/api/restricted_current.txt +++ b/work/work-runtime/api/restricted_current.txt @@ -171,8 +171,9 @@ package androidx.work { method @InaccessibleFromKotlin public java.util.Map getKeyValueMap(); method public long getLong(String key, long defaultValue); method public long[]? getLongArray(String key); + method public String?[]? getNullableStringArray(String key); method public String? getString(String key); - method public String[]? getStringArray(String key); + method @Deprecated public String[]? getStringArray(String key); method public boolean hasKeyWithValueOfType(String key, Class klass); method public byte[] toByteArray(); property public java.util.Map keyValueMap; diff --git a/work/work-runtime/src/androidTest/java/androidx/work/DataTest.kt b/work/work-runtime/src/androidTest/java/androidx/work/DataTest.kt index de7a61759450e..6078944343cea 100644 --- a/work/work-runtime/src/androidTest/java/androidx/work/DataTest.kt +++ b/work/work-runtime/src/androidTest/java/androidx/work/DataTest.kt @@ -18,9 +18,11 @@ package androidx.work import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest +import org.junit.Assert.assertArrayEquals import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse import org.junit.Assert.assertNotNull +import org.junit.Assert.assertNull import org.junit.Assert.assertTrue import org.junit.Test import org.junit.runner.RunWith @@ -47,4 +49,61 @@ class DataTest { assertFalse(data.hasKeyWithValueOfType("nothing")) assertFalse(data.hasKeyWithValueOfType("two")) } + + @Test + fun getNullableStringArray_withNullValues() { + val array = arrayOf("foo", null, "bar") + val data = workDataOf("array" to array) + val result = data.getNullableStringArray("array") + assertNotNull(result) + assertArrayEquals(array, result) + } + + @Test + fun getNullableStringArray_noNullValues() { + val array = arrayOf("foo", "bar", "baz") + val data = workDataOf("array" to array) + val result = data.getNullableStringArray("array") + assertNotNull(result) + assertArrayEquals(array, result) + } + + @Test + fun getNullableStringArray_allNulls() { + val array = arrayOf(null, null, null) + val data = workDataOf("array" to array) + val result = data.getNullableStringArray("array") + assertNotNull(result) + assertArrayEquals(array, result) + } + + @Test + fun getNullableStringArray_emptyArray() { + val array = arrayOf() + val data = workDataOf("array" to array) + val result = data.getNullableStringArray("array") + assertNotNull(result) + assertArrayEquals(array, result) + } + + @Test + fun getNullableStringArray_keyNotFound() { + val data = workDataOf() + val result = data.getNullableStringArray("nonexistent_key") + assertNull(result) + } + + @Test + fun getNullableStringArray_wrongType() { + val data = workDataOf("array" to 123) + val result = data.getNullableStringArray("array") + assertNull(result) + } + + @Test + fun getNullableStringArray_wrongArrayType() { + val data = workDataOf("array" to intArrayOf(1, 2, 3)) + val result = data.getNullableStringArray("array") + assertNull(result) + } } diff --git a/work/work-runtime/src/androidTest/java/androidx/work/impl/WorkContinuationImplTestKt.kt b/work/work-runtime/src/androidTest/java/androidx/work/impl/WorkContinuationImplTestKt.kt index 46780fcc60d75..c99e0a5fa40ef 100644 --- a/work/work-runtime/src/androidTest/java/androidx/work/impl/WorkContinuationImplTestKt.kt +++ b/work/work-runtime/src/androidTest/java/androidx/work/impl/WorkContinuationImplTestKt.kt @@ -85,7 +85,8 @@ class WorkContinuationImplTestKt { it.state == WorkInfo.State.SUCCEEDED } assertThat(info.outputData.size()).isEqualTo(2) - assertThat(info.outputData.getStringArray(stringTag)).isEqualTo(arrayOf("hello")) + assertThat(info.outputData.getNullableStringArray(stringTag)) + .isEqualTo(arrayOf("hello")) val intArray = info.outputData.getIntArray(intTag)!!.sortedArray() assertThat(intArray).isEqualTo(intArrayOf(1, 3)) } diff --git a/work/work-runtime/src/main/java/androidx/work/Data_.kt b/work/work-runtime/src/main/java/androidx/work/Data_.kt index 9d151b07882fc..7190550261b03 100644 --- a/work/work-runtime/src/main/java/androidx/work/Data_.kt +++ b/work/work-runtime/src/main/java/androidx/work/Data_.kt @@ -56,14 +56,19 @@ public class Data { return if (value is T) value else defaultValue } - private inline fun getTypedArray( + private inline fun getTypedArray( key: String, constructor: (size: Int, init: (index: Int) -> T) -> TArray, ): TArray? { val value = values[key] - return if (value is Array<*> && value.isArrayOf()) - constructor(value.size) { i -> value[i] as T } - else null + if (value is Array<*>) { + try { + return constructor(value.size) { i -> value[i] as T } + } catch (e: ClassCastException) { + // Fall-through to return null if the cast fails + } + } + return null } /** @@ -184,8 +189,24 @@ public class Data { * @param key The key for the argument * @return The value specified by the key if it exists; `null` otherwise */ + @Deprecated( + message = + "Use getNullableStringArray(key) instead. This method does not correctly handle " + + "String arrays which may contain null elements.", + replaceWith = ReplaceWith("getNullableStringArray(key)"), + ) public fun getStringArray(key: String): Array? = getTypedArray(key, ::Array) + /** + * Gets the string array value for the given key. The array may contain `null` elements. + * + * @param key The key for the argument + * @return The value specified by the key if it exists; `null` otherwise + */ + // null elements are supported to match the setter + @SuppressLint("NullableCollection", "NullableCollectionElement") + public fun getNullableStringArray(key: String): Array? = getTypedArray(key, ::Array) + public val keyValueMap: Map /** * Gets all the values in this Data object. diff --git a/xr/compose/compose/api/current.txt b/xr/compose/compose/api/current.txt index c75b03f260ec8..18f344f76a2db 100644 --- a/xr/compose/compose/api/current.txt +++ b/xr/compose/compose/api/current.txt @@ -304,7 +304,7 @@ package androidx.xr.compose.subspace { } public final class SpatialColumnKt { - method @KotlinOnly @androidx.compose.runtime.Composable @androidx.xr.compose.subspace.SubspaceComposable public static void SpatialColumn(optional androidx.xr.compose.subspace.layout.SubspaceModifier modifier, optional androidx.xr.compose.subspace.layout.SpatialAlignment alignment, optional androidx.xr.compose.subspace.layout.SpatialArrangement.Vertical verticalArrangement, kotlin.jvm.functions.Function1 content); + method @KotlinOnly @androidx.compose.runtime.Composable @androidx.xr.compose.subspace.SubspaceComposable public static inline void SpatialColumn(optional androidx.xr.compose.subspace.layout.SubspaceModifier modifier, optional androidx.xr.compose.subspace.layout.SpatialAlignment alignment, optional androidx.xr.compose.subspace.layout.SpatialArrangement.Vertical verticalArrangement, kotlin.jvm.functions.Function1 content); method @BytecodeOnly @androidx.compose.runtime.Composable @androidx.xr.compose.subspace.SubspaceComposable public static void SpatialColumn(androidx.xr.compose.subspace.layout.SubspaceModifier?, androidx.xr.compose.subspace.layout.SpatialAlignment?, androidx.xr.compose.subspace.layout.SpatialArrangement.Vertical?, kotlin.jvm.functions.Function3, androidx.compose.runtime.Composer?, int, int); } @@ -1036,7 +1036,9 @@ package androidx.xr.compose.subspace.layout { method @KotlinOnly @androidx.compose.runtime.Composable @androidx.xr.compose.subspace.SubspaceComposable public static inline void SubspaceLayout(optional androidx.xr.compose.subspace.layout.SubspaceModifier modifier, androidx.xr.compose.subspace.layout.SubspaceMeasurePolicy measurePolicy); method @BytecodeOnly @androidx.compose.runtime.Composable @androidx.xr.compose.subspace.SubspaceComposable public static void SubspaceLayout(androidx.xr.compose.subspace.layout.SubspaceModifier?, androidx.xr.compose.subspace.layout.SubspaceMeasurePolicy, androidx.compose.runtime.Composer?, int, int); method @KotlinOnly @androidx.compose.runtime.Composable @androidx.xr.compose.subspace.SubspaceComposable public static inline void SubspaceLayout(kotlin.jvm.functions.Function0 content, optional androidx.xr.compose.subspace.layout.SubspaceModifier modifier, androidx.xr.compose.subspace.layout.SubspaceMeasurePolicy measurePolicy); + method @KotlinOnly @androidx.compose.runtime.Composable @androidx.xr.compose.subspace.SubspaceComposable public static inline void SubspaceLayout(kotlin.jvm.functions.Function0 content, String coreEntityName, optional androidx.xr.compose.subspace.layout.SubspaceModifier modifier, androidx.xr.compose.subspace.layout.SubspaceMeasurePolicy measurePolicy); method @BytecodeOnly @androidx.compose.runtime.Composable @androidx.xr.compose.subspace.SubspaceComposable public static void SubspaceLayout(kotlin.jvm.functions.Function2, androidx.xr.compose.subspace.layout.SubspaceModifier?, androidx.xr.compose.subspace.layout.SubspaceMeasurePolicy, androidx.compose.runtime.Composer?, int, int); + method @BytecodeOnly @androidx.compose.runtime.Composable @androidx.xr.compose.subspace.SubspaceComposable public static void SubspaceLayout(kotlin.jvm.functions.Function2, String, androidx.xr.compose.subspace.layout.SubspaceModifier?, androidx.xr.compose.subspace.layout.SubspaceMeasurePolicy, androidx.compose.runtime.Composer?, int, int); } public interface SubspaceMeasurable { @@ -1108,8 +1110,10 @@ package androidx.xr.compose.subspace.layout { public abstract static class SubspacePlaceable.SubspacePlacementScope { ctor public SubspacePlaceable.SubspacePlacementScope(); method @InaccessibleFromKotlin public androidx.xr.compose.subspace.layout.SubspaceLayoutCoordinates? getCoordinates(); + method @InaccessibleFromKotlin protected abstract androidx.compose.ui.unit.LayoutDirection getParentLayoutDirection(); method public final void place(androidx.xr.compose.subspace.layout.SubspacePlaceable, androidx.xr.runtime.math.Pose pose); property public androidx.xr.compose.subspace.layout.SubspaceLayoutCoordinates? coordinates; + property protected abstract androidx.compose.ui.unit.LayoutDirection parentLayoutDirection; } } diff --git a/xr/compose/compose/api/restricted_current.txt b/xr/compose/compose/api/restricted_current.txt index 1021a1aeb8ca2..d9417cac065b4 100644 --- a/xr/compose/compose/api/restricted_current.txt +++ b/xr/compose/compose/api/restricted_current.txt @@ -314,8 +314,12 @@ package androidx.xr.compose.subspace { } public final class SpatialColumnKt { - method @KotlinOnly @androidx.compose.runtime.Composable @androidx.xr.compose.subspace.SubspaceComposable public static void SpatialColumn(optional androidx.xr.compose.subspace.layout.SubspaceModifier modifier, optional androidx.xr.compose.subspace.layout.SpatialAlignment alignment, optional androidx.xr.compose.subspace.layout.SpatialArrangement.Vertical verticalArrangement, kotlin.jvm.functions.Function1 content); + method @KotlinOnly @androidx.compose.runtime.Composable @androidx.xr.compose.subspace.SubspaceComposable public static inline void SpatialColumn(optional androidx.xr.compose.subspace.layout.SubspaceModifier modifier, optional androidx.xr.compose.subspace.layout.SpatialAlignment alignment, optional androidx.xr.compose.subspace.layout.SpatialArrangement.Vertical verticalArrangement, kotlin.jvm.functions.Function1 content); method @BytecodeOnly @androidx.compose.runtime.Composable @androidx.xr.compose.subspace.SubspaceComposable public static void SpatialColumn(androidx.xr.compose.subspace.layout.SubspaceModifier?, androidx.xr.compose.subspace.layout.SpatialAlignment?, androidx.xr.compose.subspace.layout.SpatialArrangement.Vertical?, kotlin.jvm.functions.Function3, androidx.compose.runtime.Composer?, int, int); + method @InaccessibleFromKotlin @kotlin.PublishedApi internal static androidx.xr.compose.subspace.layout.SubspaceMeasurePolicy getDefaultSpatialColumnMeasurePolicy(); + method @KotlinOnly @androidx.compose.runtime.Composable @kotlin.PublishedApi internal static androidx.xr.compose.subspace.layout.SubspaceMeasurePolicy spatialColumnMeasurePolicy(androidx.xr.compose.subspace.layout.SpatialAlignment alignment, androidx.xr.compose.subspace.layout.SpatialArrangement.Vertical verticalArrangement); + method @BytecodeOnly @androidx.compose.runtime.Composable @kotlin.PublishedApi internal static androidx.xr.compose.subspace.layout.SubspaceMeasurePolicy spatialColumnMeasurePolicy(androidx.xr.compose.subspace.layout.SpatialAlignment, androidx.xr.compose.subspace.layout.SpatialArrangement.Vertical, androidx.compose.runtime.Composer?, int); + property @kotlin.PublishedApi internal static androidx.xr.compose.subspace.layout.SubspaceMeasurePolicy DefaultSpatialColumnMeasurePolicy; } @androidx.compose.foundation.layout.LayoutScopeMarker public interface SpatialColumnScope { @@ -325,6 +329,12 @@ package androidx.xr.compose.subspace { method @BytecodeOnly public static androidx.xr.compose.subspace.layout.SubspaceModifier! weight$default(androidx.xr.compose.subspace.SpatialColumnScope!, androidx.xr.compose.subspace.layout.SubspaceModifier!, float, boolean, int, Object!); } + @kotlin.PublishedApi internal final class SpatialColumnScopeInstance implements androidx.xr.compose.subspace.SpatialColumnScope { + method public androidx.xr.compose.subspace.layout.SubspaceModifier align(androidx.xr.compose.subspace.layout.SubspaceModifier, androidx.xr.compose.subspace.layout.SpatialAlignment.Depth alignment); + method public androidx.xr.compose.subspace.layout.SubspaceModifier align(androidx.xr.compose.subspace.layout.SubspaceModifier, androidx.xr.compose.subspace.layout.SpatialAlignment.Horizontal alignment); + method public androidx.xr.compose.subspace.layout.SubspaceModifier weight(androidx.xr.compose.subspace.layout.SubspaceModifier, float weight, boolean fill); + } + public final class SpatialCurvedRowDefaults { method @BytecodeOnly public float getCurveRadius-D9Ej5fM(); property public androidx.compose.ui.unit.Dp curveRadius; @@ -1049,7 +1059,9 @@ package androidx.xr.compose.subspace.layout { method @KotlinOnly @androidx.compose.runtime.Composable @androidx.xr.compose.subspace.SubspaceComposable public static inline void SubspaceLayout(optional androidx.xr.compose.subspace.layout.SubspaceModifier modifier, androidx.xr.compose.subspace.layout.SubspaceMeasurePolicy measurePolicy); method @BytecodeOnly @androidx.compose.runtime.Composable @androidx.xr.compose.subspace.SubspaceComposable public static void SubspaceLayout(androidx.xr.compose.subspace.layout.SubspaceModifier?, androidx.xr.compose.subspace.layout.SubspaceMeasurePolicy, androidx.compose.runtime.Composer?, int, int); method @KotlinOnly @androidx.compose.runtime.Composable @androidx.xr.compose.subspace.SubspaceComposable public static inline void SubspaceLayout(kotlin.jvm.functions.Function0 content, optional androidx.xr.compose.subspace.layout.SubspaceModifier modifier, androidx.xr.compose.subspace.layout.SubspaceMeasurePolicy measurePolicy); + method @KotlinOnly @androidx.compose.runtime.Composable @androidx.xr.compose.subspace.SubspaceComposable public static inline void SubspaceLayout(kotlin.jvm.functions.Function0 content, String coreEntityName, optional androidx.xr.compose.subspace.layout.SubspaceModifier modifier, androidx.xr.compose.subspace.layout.SubspaceMeasurePolicy measurePolicy); method @BytecodeOnly @androidx.compose.runtime.Composable @androidx.xr.compose.subspace.SubspaceComposable public static void SubspaceLayout(kotlin.jvm.functions.Function2, androidx.xr.compose.subspace.layout.SubspaceModifier?, androidx.xr.compose.subspace.layout.SubspaceMeasurePolicy, androidx.compose.runtime.Composer?, int, int); + method @BytecodeOnly @androidx.compose.runtime.Composable @androidx.xr.compose.subspace.SubspaceComposable public static void SubspaceLayout(kotlin.jvm.functions.Function2, String, androidx.xr.compose.subspace.layout.SubspaceModifier?, androidx.xr.compose.subspace.layout.SubspaceMeasurePolicy, androidx.compose.runtime.Composer?, int, int); method @BytecodeOnly @androidx.compose.runtime.Composable @kotlin.PublishedApi internal static androidx.xr.compose.subspace.layout.OpaqueEntity rememberOpaqueEntity(kotlin.jvm.functions.Function1, androidx.compose.runtime.Composer?, int); method @KotlinOnly @androidx.compose.runtime.Composable @kotlin.PublishedApi internal static androidx.xr.compose.subspace.layout.OpaqueEntity rememberOpaqueEntity(kotlin.jvm.functions.Function1 entityFactory); } @@ -1123,8 +1135,10 @@ package androidx.xr.compose.subspace.layout { public abstract static class SubspacePlaceable.SubspacePlacementScope { ctor public SubspacePlaceable.SubspacePlacementScope(); method @InaccessibleFromKotlin public androidx.xr.compose.subspace.layout.SubspaceLayoutCoordinates? getCoordinates(); + method @InaccessibleFromKotlin protected abstract androidx.compose.ui.unit.LayoutDirection getParentLayoutDirection(); method public final void place(androidx.xr.compose.subspace.layout.SubspacePlaceable, androidx.xr.runtime.math.Pose pose); property public androidx.xr.compose.subspace.layout.SubspaceLayoutCoordinates? coordinates; + property protected abstract androidx.compose.ui.unit.LayoutDirection parentLayoutDirection; } } diff --git a/xr/compose/compose/src/main/kotlin/androidx/xr/compose/subspace/SpatialColumn.kt b/xr/compose/compose/src/main/kotlin/androidx/xr/compose/subspace/SpatialColumn.kt index 8c83ca125abc1..7f0b2c3b0e876 100644 --- a/xr/compose/compose/src/main/kotlin/androidx/xr/compose/subspace/SpatialColumn.kt +++ b/xr/compose/compose/src/main/kotlin/androidx/xr/compose/subspace/SpatialColumn.kt @@ -22,8 +22,6 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.unit.Density import androidx.compose.ui.util.fastRoundToInt -import androidx.xr.compose.platform.LocalSession -import androidx.xr.compose.subspace.layout.CoreGroupEntity import androidx.xr.compose.subspace.layout.SpatialAlignment import androidx.xr.compose.subspace.layout.SpatialArrangement import androidx.xr.compose.subspace.layout.SubspaceLayout @@ -38,7 +36,6 @@ import androidx.xr.compose.unit.VolumeConstraints import androidx.xr.runtime.math.Pose import androidx.xr.runtime.math.Quaternion import androidx.xr.runtime.math.Vector3 -import androidx.xr.scenecore.GroupEntity /** * A layout composable that arranges its children in a vertical sequence. @@ -52,24 +49,47 @@ import androidx.xr.scenecore.GroupEntity */ @Composable @SubspaceComposable -public fun SpatialColumn( +public inline fun SpatialColumn( modifier: SubspaceModifier = SubspaceModifier, alignment: SpatialAlignment = SpatialAlignment.Center, verticalArrangement: SpatialArrangement.Vertical = SpatialArrangement.Center, - content: @Composable @SubspaceComposable SpatialColumnScope.() -> Unit, + crossinline content: @Composable @SubspaceComposable SpatialColumnScope.() -> Unit, ) { - val session = checkNotNull(LocalSession.current) { "session must be initialized" } - val coreGroupEntity = remember { - CoreGroupEntity(GroupEntity.create(session, name = "SpatialColumn", pose = Pose.Identity)) - } + val measurePolicy = + spatialColumnMeasurePolicy(alignment = alignment, verticalArrangement = verticalArrangement) + SubspaceLayout( modifier = modifier, content = { SpatialColumnScopeInstance.content() }, - coreEntity = coreGroupEntity, - measurePolicy = SpatialColumnMeasurePolicy(alignment, verticalArrangement), + coreEntityName = "SpatialColumn", + measurePolicy = measurePolicy, ) } +@PublishedApi +internal val DefaultSpatialColumnMeasurePolicy: SubspaceMeasurePolicy = + SpatialColumnMeasurePolicy( + alignment = SpatialAlignment.Center, + verticalArrangement = SpatialArrangement.Center, + ) + +@PublishedApi +@Composable +internal fun spatialColumnMeasurePolicy( + alignment: SpatialAlignment, + verticalArrangement: SpatialArrangement.Vertical, +): SubspaceMeasurePolicy = + if (alignment == SpatialAlignment.Center && verticalArrangement == SpatialArrangement.Center) { + DefaultSpatialColumnMeasurePolicy + } else { + remember(alignment, verticalArrangement) { + SpatialColumnMeasurePolicy( + alignment = alignment, + verticalArrangement = verticalArrangement, + ) + } + } + /** * Measure policy for [SpatialColumn] layouts. Handles the measurement and placement of children in * a vertical sequence. @@ -255,6 +275,7 @@ public interface SpatialColumnScope { } /** Default implementation of the [SpatialColumnScope] interface. */ +@PublishedApi internal object SpatialColumnScopeInstance : SpatialColumnScope { override fun SubspaceModifier.weight(weight: Float, fill: Boolean): SubspaceModifier { require(weight > 0.0) { "invalid weight $weight; must be greater than zero" } diff --git a/xr/compose/compose/src/main/kotlin/androidx/xr/compose/subspace/layout/SubspaceLayout.kt b/xr/compose/compose/src/main/kotlin/androidx/xr/compose/subspace/layout/SubspaceLayout.kt index 7b879753beff8..ec0e8f49cb413 100644 --- a/xr/compose/compose/src/main/kotlin/androidx/xr/compose/subspace/layout/SubspaceLayout.kt +++ b/xr/compose/compose/src/main/kotlin/androidx/xr/compose/subspace/layout/SubspaceLayout.kt @@ -38,8 +38,8 @@ import androidx.xr.scenecore.GroupEntity /** * [SubspaceLayout] is the main component for laying out leaf nodes with zero children. * - * The measurement, layout and intrinsic measurement behaviours of this layout will be defined by - * the [SubspaceMeasurePolicy] instance. See [SubspaceMeasurePolicy] for more details. + * The measurement, layout and intrinsic measurement behaviors of this layout will be defined by the + * [SubspaceMeasurePolicy] instance. See [SubspaceMeasurePolicy] for more details. * * Example: * ```kotlin @@ -80,8 +80,8 @@ public inline fun SubspaceLayout( * [SubspaceLayout] is the main core component for layout. It can be used to measure and position * zero or more layout children. * - * The measurement, layout and intrinsic measurement behaviours of this layout will be defined by - * the [SubspaceMeasurePolicy] instance. See [SubspaceMeasurePolicy] for more details. + * The measurement, layout and intrinsic measurement behaviors of this layout will be defined by the + * [SubspaceMeasurePolicy] instance. See [SubspaceMeasurePolicy] for more details. * * Example: * ```kotlin @@ -132,6 +132,69 @@ public inline fun SubspaceLayout( } } +/** + * [SubspaceLayout] is the main core component for layout. It can be used to measure and position + * zero or more layout children. + * + * The measurement, layout and intrinsic measurement behaviors of this layout will be defined by the + * [SubspaceMeasurePolicy] instance. See [SubspaceMeasurePolicy] for more details. + * + * Example: + * ```kotlin + * fun MyLayout( + * modifier: SubspaceModifier = SubspaceModifier, + * content: @SubspaceComposable @Composable () -> Unit) { + * SubspaceLayout(content = content, modifier = modifier) { + * measurables, constraints -> + * val placeables = measurables.map { it.measure(constraints) } + * layout(constraints.maxWidth, constraints.maxHeight, constraints.maxDepth) { + * placeables.forEach { it.place(Pose.Identity) } + * } + * } + * } + * ``` + * + * @param modifier SubspaceModifier to apply during layout + * @param content the child composables to be laid out. + * @param coreEntityName A name for the underlying [androidx.xr.scenecore.GroupEntity] that is + * created to host the content of this layout. This name is used for debugging and identification + * purposes; it will appear in scene graph inspectors, making it easier to correlate this + * composable with its corresponding node in the 3D scene. + * @param measurePolicy a policy defining the measurement and positioning of the layout. + */ +@Suppress("ComposableLambdaParameterPosition", "NOTHING_TO_INLINE") +@SubspaceComposable +@Composable +public inline fun SubspaceLayout( + crossinline content: @Composable @SubspaceComposable () -> Unit, + coreEntityName: String, + modifier: SubspaceModifier = SubspaceModifier, + measurePolicy: SubspaceMeasurePolicy, +) { + check(currentComposer.applier.current is ComposeSubspaceNode) { + "SubspaceComposable functions are expected to be used within the context of a " + + "Subspace composition. Please ensure that this component is in a Subspace or " + + " is a child of another SubspaceComposable." + } + + val coreEntity = rememberOpaqueEntity { + GroupEntity.create(session = this, name = coreEntityName) + } + val compositionLocalMap = currentComposer.currentCompositionLocalMap + CompositionLocalProvider(LocalOpaqueEntity provides coreEntity) { + ComposeNode>( + factory = ComposeSubspaceNode.Constructor, + update = { + set(compositionLocalMap, SetCompositionLocalMap) + set(measurePolicy, SetMeasurePolicy) + set(coreEntity, SetCoreEntity) + set(modifier, SetModifier) + }, + content = content, + ) + } +} + /** Creates a [CoreGroupEntity] that is automatically disposed of when it leaves the composition. */ @Composable @PublishedApi @@ -146,8 +209,8 @@ internal fun rememberOpaqueEntity( * [SubspaceLayout] is the main core component for layout for "leaf" nodes. It can be used to * measure and position zero children. * - * The measurement, layout and intrinsic measurement behaviours of this layout will be defined by - * the [SubspaceMeasurePolicy] instance. See [SubspaceMeasurePolicy] for more details. + * The measurement, layout and intrinsic measurement behaviors of this layout will be defined by the + * [SubspaceMeasurePolicy] instance. See [SubspaceMeasurePolicy] for more details. * * @param modifier SubspaceModifier to apply during layout. * @param coreEntity SceneCore Entity being placed in this layout. This parameter is generally not @@ -187,8 +250,8 @@ internal inline fun SubspaceLayout( * [SubspaceLayout] is the main core component for layout. It can be used to measure and position * zero or more layout children. * - * The measurement, layout and intrinsic measurement behaviours of this layout will be defined by - * the [SubspaceMeasurePolicy] instance. See [SubspaceMeasurePolicy] for more details. + * The measurement, layout and intrinsic measurement behaviors of this layout will be defined by the + * [SubspaceMeasurePolicy] instance. See [SubspaceMeasurePolicy] for more details. * * @param modifier SubspaceModifier to apply during layout * @param coreEntity SceneCore Entity being placed in this layout. This parameter is generally not @@ -208,9 +271,8 @@ internal inline fun SubspaceLayout( measurePolicy: SubspaceMeasurePolicy, ) { - val session = checkNotNull(LocalSession.current) { "session must be initialized" } val coreGroupEntity = - coreEntity ?: remember { CoreGroupEntity(GroupEntity.create(session, name = "Entity")) } + coreEntity ?: rememberOpaqueEntity { GroupEntity.create(this, name = "Entity") } check(currentComposer.applier.current is ComposeSubspaceNode) { "SubspaceComposable functions are expected to be used within the context of a " + diff --git a/xr/compose/compose/src/main/kotlin/androidx/xr/compose/subspace/layout/SubspacePlaceable.kt b/xr/compose/compose/src/main/kotlin/androidx/xr/compose/subspace/layout/SubspacePlaceable.kt index 4987e17421acd..d43c35260a1bd 100644 --- a/xr/compose/compose/src/main/kotlin/androidx/xr/compose/subspace/layout/SubspacePlaceable.kt +++ b/xr/compose/compose/src/main/kotlin/androidx/xr/compose/subspace/layout/SubspacePlaceable.kt @@ -16,12 +16,13 @@ package androidx.xr.compose.subspace.layout +import androidx.annotation.RestrictTo +import androidx.compose.ui.unit.LayoutDirection import androidx.xr.runtime.math.Pose /** * A [SubspacePlaceable] corresponds to a child layout that can be positioned by its parent layout. - * Most [SubspacePlaceables][SubspacePlaceable] are the result of a [SubspaceMeasurable.measure] - * call. + * Most [SubspacePlaceable] are the result of a [SubspaceMeasurable.measure] call. * * Based on [androidx.compose.ui.layout.Placeable]. */ @@ -43,6 +44,13 @@ public abstract class SubspacePlaceable { /** Receiver scope that permits explicit placement of a [SubspacePlaceable]. */ public abstract class SubspacePlacementScope { + /** + * Keeps the layout direction of the parent of the subspace placeable that is being placed + * using current [SubspacePlacementScope]. Used to support automatic position mirroring for + * convenient RTL support in custom layouts. + */ + protected abstract val parentLayoutDirection: LayoutDirection + /** * The [SubspaceLayoutCoordinates] of this layout, if known or `null` if the layout hasn't * been placed yet. @@ -50,9 +58,53 @@ public abstract class SubspacePlaceable { public open val coordinates: SubspaceLayoutCoordinates? get() = null - /** Place a [SubspacePlaceable] at the [Pose] in its parent's coordinate system. */ + /** + * Place a [SubspacePlaceable] at the [Pose] in its parent's coordinate system. + * + * @param pose The pose of the layout. + */ public fun SubspacePlaceable.place(pose: Pose) { placeAt(pose) } + + /** + * Place a [SubspacePlaceable] at the [Pose] in its parent's coordinate system with auto + * mirrored position along YZ plane if parent layout direction is [LayoutDirection.Rtl]. + * + * If the [parentLayoutDirection] is [LayoutDirection.Rtl], this function calculates a new + * pose by mirroring the original [pose] across the YZ plane. This ensures that layouts + * designed for LTR behave intuitively when the locale is switched to RTL. + * + * The mirroring transformation involves: + * 1. Translation: The `x` component of the translation vector is negated (`x -> -x`). This + * moves the object from the right side to the left side, or vice versa. + * 2. Rotation: The `y` and `z` components of the underlying quaternion are negated. This + * has the effect of reversing the direction of yaw (rotation around the Y-axis) and roll + * (rotation around the Z-axis), while leaving pitch (rotation around the X-axis) + * unchanged. + * + * For example, a pose that places an object 20 dp to the left and yawed 30 degrees to the + * right will be transformed to place the object 20 dp to the right and yawed 30 degrees to + * the left. + * + * If the layout direction is LTR, the original [pose] is used without modification. + * + * This API is not mirroring the internal mesh geometry of 3D models. This function only + * affects pose of the layout. + * + * @param pose The pose of the layout. + */ + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) + public fun SubspacePlaceable.placeRelative(pose: Pose) { + var newPose = pose + if (parentLayoutDirection == LayoutDirection.Rtl) { + newPose = + Pose( + translation = pose.translation.copy(x = -pose.translation.x), + rotation = pose.rotation.copy(y = -pose.rotation.y, z = -pose.rotation.z), + ) + } + placeAt(newPose) + } } } diff --git a/xr/compose/compose/src/main/kotlin/androidx/xr/compose/subspace/node/SubspaceLayoutModifierNodeCoordinator.kt b/xr/compose/compose/src/main/kotlin/androidx/xr/compose/subspace/node/SubspaceLayoutModifierNodeCoordinator.kt index 7e42ef0f5a99d..0ef171353dee8 100644 --- a/xr/compose/compose/src/main/kotlin/androidx/xr/compose/subspace/node/SubspaceLayoutModifierNodeCoordinator.kt +++ b/xr/compose/compose/src/main/kotlin/androidx/xr/compose/subspace/node/SubspaceLayoutModifierNodeCoordinator.kt @@ -16,6 +16,7 @@ package androidx.xr.compose.subspace.node +import androidx.compose.ui.unit.LayoutDirection import androidx.xr.compose.subspace.layout.LayoutSubspaceMeasureScope import androidx.xr.compose.subspace.layout.ParentLayoutParamsAdjustable import androidx.xr.compose.subspace.layout.SubspaceLayoutCoordinates @@ -121,6 +122,9 @@ internal class SubspaceLayoutModifierNodeCoordinator( logger?.nodePlaced(layoutModifierNode, pose) subspaceMeasureResult?.placeChildren( object : SubspacePlacementScope() { + override val parentLayoutDirection = + this@SubspaceLayoutModifierNodeCoordinator.layoutNode?.layoutDirection + ?: LayoutDirection.Ltr public override val coordinates = this@SubspaceLayoutModifierNodeCoordinator } ) diff --git a/xr/compose/compose/src/main/kotlin/androidx/xr/compose/subspace/node/SubspaceLayoutNode.kt b/xr/compose/compose/src/main/kotlin/androidx/xr/compose/subspace/node/SubspaceLayoutNode.kt index 4ba7b6e5a74cb..e08daffd4a0f9 100644 --- a/xr/compose/compose/src/main/kotlin/androidx/xr/compose/subspace/node/SubspaceLayoutNode.kt +++ b/xr/compose/compose/src/main/kotlin/androidx/xr/compose/subspace/node/SubspaceLayoutNode.kt @@ -507,6 +507,7 @@ internal class SubspaceLayoutNode : ComposeSubspaceNode { subspaceMeasureResult?.placeChildren( object : SubspacePlacementScope() { + override val parentLayoutDirection = this@SubspaceLayoutNode.layoutDirection override val coordinates = this@SubspaceMeasurableLayout } ) diff --git a/xr/compose/compose/src/test/kotlin/androidx/xr/compose/subspace/layout/SubspacePlaceableTest.kt b/xr/compose/compose/src/test/kotlin/androidx/xr/compose/subspace/layout/SubspacePlaceableTest.kt new file mode 100644 index 0000000000000..dd79865a7342e --- /dev/null +++ b/xr/compose/compose/src/test/kotlin/androidx/xr/compose/subspace/layout/SubspacePlaceableTest.kt @@ -0,0 +1,87 @@ +/* + * Copyright 2024 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.xr.compose.subspace.layout + +import androidx.compose.ui.unit.LayoutDirection +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.xr.runtime.math.Pose +import androidx.xr.runtime.math.Quaternion +import androidx.xr.runtime.math.Vector3 +import com.google.common.truth.Truth.assertThat +import kotlin.test.assertNotNull +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class SubspacePlaceableTest { + + private class TestSubspacePlaceable : SubspacePlaceable() { + var placedPose: Pose? = null + + override fun placeAt(pose: Pose) { + placedPose = pose + } + } + + private class TestSubspacePlacementScope(override val parentLayoutDirection: LayoutDirection) : + SubspacePlaceable.SubspacePlacementScope() + + @Test + fun place_callsPlaceAtWithCorrectPose() { + val placeable = TestSubspacePlaceable() + val scope = TestSubspacePlacementScope(LayoutDirection.Ltr) + val pose = Pose() + + with(scope) { placeable.place(pose) } + + assertThat(placeable.placedPose).isEqualTo(pose) + } + + @Test + fun placeRelative_ltr_callsPlaceAtWithOriginalPose() { + val placeable = TestSubspacePlaceable() + val scope = TestSubspacePlacementScope(LayoutDirection.Ltr) + val pose = Pose(translation = Vector3(1f, 2f, 3f)) + + with(scope) { placeable.placeRelative(pose) } + + assertThat(placeable.placedPose).isEqualTo(pose) + } + + @Test + fun placeRelative_rtl_callsPlaceAtWithMirroredPose() { + val placeable = TestSubspacePlaceable() + val scope = TestSubspacePlacementScope(LayoutDirection.Rtl) + val originalPose = + Pose(translation = Vector3(1f, 2f, 3f), rotation = Quaternion(0.1f, 0.2f, 0.3f, 0.9f)) + + with(scope) { placeable.placeRelative(originalPose) } + + val placedPose = assertNotNull(placeable.placedPose) + + // Translation x is negated + assertThat(placedPose.translation.x).isEqualTo(-originalPose.translation.x) + assertThat(placedPose.translation.y).isEqualTo(originalPose.translation.y) + assertThat(placedPose.translation.z).isEqualTo(originalPose.translation.z) + + // Rotation y and z components are negated. + assertThat(placedPose.rotation.x).isWithin(1e-6f).of(originalPose.rotation.x) + assertThat(placedPose.rotation.y).isWithin(1e-6f).of(-originalPose.rotation.y) + assertThat(placedPose.rotation.z).isWithin(1e-6f).of(-originalPose.rotation.z) + assertThat(placedPose.rotation.w).isWithin(1e-6f).of(originalPose.rotation.w) + } +} diff --git a/xr/runtime/runtime/src/main/kotlin/androidx/xr/runtime/ServiceLoaderExt.kt b/xr/runtime/runtime/src/main/kotlin/androidx/xr/runtime/ServiceLoaderExt.kt index 1cd2a6aee6ad5..bc3c3ee8210cf 100644 --- a/xr/runtime/runtime/src/main/kotlin/androidx/xr/runtime/ServiceLoaderExt.kt +++ b/xr/runtime/runtime/src/main/kotlin/androidx/xr/runtime/ServiceLoaderExt.kt @@ -17,10 +17,12 @@ package androidx.xr.runtime import android.app.Activity +import android.companion.virtual.VirtualDeviceManager import android.content.Context import android.content.pm.ActivityInfo import android.content.pm.PackageManager import android.os.Build +import androidx.annotation.RequiresApi import androidx.xr.runtime.internal.Feature import androidx.xr.runtime.internal.Service import androidx.xr.runtime.manifest.FEATURE_XR_API_OPENXR @@ -70,6 +72,7 @@ internal fun loadProviders( } private const val REQUIRED_DISPLAY_CATEGORY_XR_PROJECTED = "xr_projected" +private const val PROJECTED_DEVICE_NAME = "ProjectionDevice" private fun hasXrProjectedDisplayCategory(activityInfo: ActivityInfo): Boolean { // TODO b/460536048 - Remove reflection once requiredDisplayCategory is public in SDK 36 @@ -110,6 +113,18 @@ internal fun isProjectedActivity(context: Context): Boolean { } } +// TODO: b/458737779 - Implement tests when the test rule is available +/** Returns whether the provided context is the Projected device context. */ +@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE) +internal fun isProjectedDeviceContext(context: Context): Boolean = + getVirtualDevice(context)?.name?.startsWith(PROJECTED_DEVICE_NAME) == true + +@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE) +private fun getVirtualDevice(context: Context) = + context.getSystemService(VirtualDeviceManager::class.java).virtualDevices.find { + it.deviceId == context.deviceId + } + /** * Returns the first service provider from [providers] that has its requirements satisfied by the * [features] supported by the current device. @@ -117,15 +132,21 @@ internal fun isProjectedActivity(context: Context): Boolean { internal fun selectProvider(providers: List, features: Set): S? = providers.firstOrNull { features.containsAll(it.requirements) } -/** Returns the set of features available for the current activity on this device. */ -internal fun getDeviceActivityFeatures(context: Context): Set { +/** Returns the set of features available for the current context associated with the device. */ +internal fun getDeviceContextFeatures(context: Context): Set { // Short-circuit for unit tests environments. if (Build.FINGERPRINT.contains("robolectric")) return emptySet() val features = mutableSetOf(Feature.FULLSTACK) val packageManager = context.packageManager - if (isProjectedActivity(context)) { + if (context is Activity && isProjectedActivity(context)) { + features.add(Feature.PROJECTED) + } else if ( + // TODO: b/458737779 - Implement tests when the test rule is available + Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE && + isProjectedDeviceContext(context) + ) { features.add(Feature.PROJECTED) } diff --git a/xr/runtime/runtime/src/main/kotlin/androidx/xr/runtime/Session.kt b/xr/runtime/runtime/src/main/kotlin/androidx/xr/runtime/Session.kt index 02490e418a04d..2a5de7afd4632 100644 --- a/xr/runtime/runtime/src/main/kotlin/androidx/xr/runtime/Session.kt +++ b/xr/runtime/runtime/src/main/kotlin/androidx/xr/runtime/Session.kt @@ -233,7 +233,7 @@ public constructor( return SessionCreateSuccess(activitySessionMap[activity]!!) } - val features = getDeviceActivityFeatures(activity) + val features = getDeviceContextFeatures(activity) val runtimes = mutableListOf() diff --git a/xr/runtime/runtime/src/test/kotlin/androidx/xr/runtime/ServiceLoaderExtTest.kt b/xr/runtime/runtime/src/test/kotlin/androidx/xr/runtime/ServiceLoaderExtTest.kt index e018adf049354..4ada17784d684 100644 --- a/xr/runtime/runtime/src/test/kotlin/androidx/xr/runtime/ServiceLoaderExtTest.kt +++ b/xr/runtime/runtime/src/test/kotlin/androidx/xr/runtime/ServiceLoaderExtTest.kt @@ -93,41 +93,41 @@ class ServiceLoaderExtTest { } @Test - fun getDeviceActivityFeatures_onRobolectric_returnsEmptySet() { - assertThat(getDeviceActivityFeatures(ApplicationProvider.getApplicationContext())).isEmpty() + fun getDeviceContextFeatures_onRobolectric_returnsEmptySet() { + assertThat(getDeviceContextFeatures(ApplicationProvider.getApplicationContext())).isEmpty() } @Test - fun getDeviceActivityFeatures_notOnRobolectric_addsFullStack() { + fun getDeviceContextFeatures_notOnRobolectric_addsFullStack() { ShadowBuild.setFingerprint("a_real_device") - assertThat(getDeviceActivityFeatures(ApplicationProvider.getApplicationContext())) + assertThat(getDeviceContextFeatures(ApplicationProvider.getApplicationContext())) .containsExactly(Feature.FULLSTACK) } @Test - fun getDeviceActivityFeatures_onOpenXrDevice_addsOpenXr() { + fun getDeviceContextFeatures_onOpenXrDevice_addsOpenXr() { ShadowBuild.setFingerprint("a_real_device") val context: Context = ApplicationProvider.getApplicationContext() shadowOf(context.packageManager) .setSystemFeature(FEATURE_XR_API_OPENXR, /* supported= */ true) - assertThat(getDeviceActivityFeatures(context)).contains(Feature.OPEN_XR) + assertThat(getDeviceContextFeatures(context)).contains(Feature.OPEN_XR) } @Test - fun getDeviceActivityFeatures_onSpatialDevice_addsSpatial() { + fun getDeviceContextFeatures_onSpatialDevice_addsSpatial() { ShadowBuild.setFingerprint("a_real_device") val context: Context = ApplicationProvider.getApplicationContext() shadowOf(context.packageManager) .setSystemFeature(FEATURE_XR_API_SPATIAL, /* supported= */ true) - assertThat(getDeviceActivityFeatures(context)).contains(Feature.SPATIAL) + assertThat(getDeviceContextFeatures(context)).contains(Feature.SPATIAL) } @Test @Config(sdk = [Build.VERSION_CODES.UPSIDE_DOWN_CAKE]) - fun getDeviceActivityFeatures_onProjectedActivity_addsProjected() { + fun getDeviceContextFeatures_onProjectedActivity_addsProjected() { ShadowBuild.setFingerprint("a_real_device") val activity = Robolectric.buildActivity(Activity::class.java).create().get() val activityInfo = ActivityInfo() @@ -141,14 +141,14 @@ class ServiceLoaderExtTest { shadowOf(activity.packageManager).installPackage(packageInfo) - assertThat(getDeviceActivityFeatures(activity)).contains(Feature.PROJECTED) + assertThat(getDeviceContextFeatures(activity)).contains(Feature.PROJECTED) } @Test - fun getDeviceActivityFeatures_onNonProjectedActivity_doesNotAddProjected() { + fun getDeviceContextFeatures_onNonProjectedActivity_doesNotAddProjected() { ShadowBuild.setFingerprint("a_real_device") - assertThat(getDeviceActivityFeatures(ApplicationProvider.getApplicationContext())) + assertThat(getDeviceContextFeatures(ApplicationProvider.getApplicationContext())) .doesNotContain(Feature.PROJECTED) }