Merge "Update proto docs for class name" into androidx-main
diff --git a/compose/foundation/foundation-layout/api/current.txt b/compose/foundation/foundation-layout/api/current.txt
index 4339957..c8e0c04 100644
--- a/compose/foundation/foundation-layout/api/current.txt
+++ b/compose/foundation/foundation-layout/api/current.txt
@@ -111,12 +111,6 @@
method @androidx.compose.runtime.Stable public androidx.compose.ui.Modifier weight(androidx.compose.ui.Modifier, float weight, optional boolean fill);
}
- @androidx.compose.foundation.layout.LayoutScopeMarker @androidx.compose.runtime.Immutable @kotlin.jvm.JvmDefaultWithCompatibility public interface FlowColumnScope extends androidx.compose.foundation.layout.ColumnScope {
- }
-
- @androidx.compose.foundation.layout.LayoutScopeMarker @androidx.compose.runtime.Immutable @kotlin.jvm.JvmDefaultWithCompatibility public interface FlowRowScope extends androidx.compose.foundation.layout.RowScope {
- }
-
public final class IntrinsicKt {
method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier height(androidx.compose.ui.Modifier, androidx.compose.foundation.layout.IntrinsicSize intrinsicSize);
method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier requiredHeight(androidx.compose.ui.Modifier, androidx.compose.foundation.layout.IntrinsicSize intrinsicSize);
diff --git a/compose/foundation/foundation-layout/api/public_plus_experimental_current.txt b/compose/foundation/foundation-layout/api/public_plus_experimental_current.txt
index 3e7c323..3d72dbe 100644
--- a/compose/foundation/foundation-layout/api/public_plus_experimental_current.txt
+++ b/compose/foundation/foundation-layout/api/public_plus_experimental_current.txt
@@ -114,7 +114,7 @@
@kotlin.RequiresOptIn(message="The API of this layout is experimental and is likely to change in the future.") @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) public @interface ExperimentalLayoutApi {
}
- @androidx.compose.foundation.layout.LayoutScopeMarker @androidx.compose.runtime.Immutable @kotlin.jvm.JvmDefaultWithCompatibility public interface FlowColumnScope extends androidx.compose.foundation.layout.ColumnScope {
+ @androidx.compose.foundation.layout.ExperimentalLayoutApi @androidx.compose.foundation.layout.LayoutScopeMarker @androidx.compose.runtime.Immutable public interface FlowColumnScope extends androidx.compose.foundation.layout.ColumnScope {
}
public final class FlowLayoutKt {
@@ -122,7 +122,7 @@
method @androidx.compose.foundation.layout.ExperimentalLayoutApi @androidx.compose.runtime.Composable public static inline void FlowRow(optional androidx.compose.ui.Modifier modifier, optional androidx.compose.foundation.layout.Arrangement.Horizontal horizontalArrangement, optional androidx.compose.foundation.layout.Arrangement.Vertical verticalArrangement, optional int maxItemsInEachRow, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.FlowRowScope,kotlin.Unit> content);
}
- @androidx.compose.foundation.layout.LayoutScopeMarker @androidx.compose.runtime.Immutable @kotlin.jvm.JvmDefaultWithCompatibility public interface FlowRowScope extends androidx.compose.foundation.layout.RowScope {
+ @androidx.compose.foundation.layout.ExperimentalLayoutApi @androidx.compose.foundation.layout.LayoutScopeMarker @androidx.compose.runtime.Immutable public interface FlowRowScope extends androidx.compose.foundation.layout.RowScope {
}
public final class IntrinsicKt {
diff --git a/compose/foundation/foundation-layout/api/restricted_current.txt b/compose/foundation/foundation-layout/api/restricted_current.txt
index 2b75f19..03cdfb3 100644
--- a/compose/foundation/foundation-layout/api/restricted_current.txt
+++ b/compose/foundation/foundation-layout/api/restricted_current.txt
@@ -114,17 +114,11 @@
method @androidx.compose.runtime.Stable public androidx.compose.ui.Modifier weight(androidx.compose.ui.Modifier, float weight, optional boolean fill);
}
- @androidx.compose.foundation.layout.LayoutScopeMarker @androidx.compose.runtime.Immutable @kotlin.jvm.JvmDefaultWithCompatibility public interface FlowColumnScope extends androidx.compose.foundation.layout.ColumnScope {
- }
-
public final class FlowLayoutKt {
method @androidx.compose.runtime.Composable @kotlin.PublishedApi internal static androidx.compose.ui.layout.MeasurePolicy columnMeasurementHelper(androidx.compose.foundation.layout.Arrangement.Vertical verticalArrangement, androidx.compose.foundation.layout.Arrangement.Horizontal horizontalArrangement, int maxItemsInMainAxis);
method @androidx.compose.runtime.Composable @kotlin.PublishedApi internal static androidx.compose.ui.layout.MeasurePolicy rowMeasurementHelper(androidx.compose.foundation.layout.Arrangement.Horizontal horizontalArrangement, androidx.compose.foundation.layout.Arrangement.Vertical verticalArrangement, int maxItemsInMainAxis);
}
- @androidx.compose.foundation.layout.LayoutScopeMarker @androidx.compose.runtime.Immutable @kotlin.jvm.JvmDefaultWithCompatibility public interface FlowRowScope extends androidx.compose.foundation.layout.RowScope {
- }
-
public final class IntrinsicKt {
method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier height(androidx.compose.ui.Modifier, androidx.compose.foundation.layout.IntrinsicSize intrinsicSize);
method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier requiredHeight(androidx.compose.ui.Modifier, androidx.compose.foundation.layout.IntrinsicSize intrinsicSize);
diff --git a/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/FlowLayout.kt b/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/FlowLayout.kt
index 5d58064..cdec1d5 100644
--- a/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/FlowLayout.kt
+++ b/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/FlowLayout.kt
@@ -126,7 +126,7 @@
*/
@LayoutScopeMarker
@Immutable
-@JvmDefaultWithCompatibility
+@ExperimentalLayoutApi
interface FlowRowScope : RowScope
/**
@@ -134,11 +134,13 @@
*/
@LayoutScopeMarker
@Immutable
-@JvmDefaultWithCompatibility
+@ExperimentalLayoutApi
interface FlowColumnScope : ColumnScope
+@OptIn(ExperimentalLayoutApi::class)
internal object FlowRowScopeInstance : RowScope by RowScopeInstance, FlowRowScope
+@OptIn(ExperimentalLayoutApi::class)
internal object FlowColumnScopeInstance : ColumnScope by ColumnScopeInstance, FlowColumnScope
private fun getVerticalArrangement(verticalArrangement: Arrangement.Vertical):
diff --git a/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/Size.kt b/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/Size.kt
index 78210e2..b0a80c7 100644
--- a/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/Size.kt
+++ b/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/Size.kt
@@ -38,7 +38,6 @@
import androidx.compose.ui.unit.constrain
import androidx.compose.ui.unit.constrainHeight
import androidx.compose.ui.unit.constrainWidth
-import androidx.compose.ui.unit.dp
import kotlin.math.roundToInt
/**
@@ -768,12 +767,12 @@
private val Density.targetConstraints: Constraints
get() {
val maxWidth = if (maxWidth != Dp.Unspecified) {
- maxWidth.coerceAtLeast(0.dp).roundToPx()
+ maxWidth.roundToPx().coerceAtLeast(0)
} else {
Constraints.Infinity
}
val maxHeight = if (maxHeight != Dp.Unspecified) {
- maxHeight.coerceAtLeast(0.dp).roundToPx()
+ maxHeight.roundToPx().coerceAtLeast(0)
} else {
Constraints.Infinity
}
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutTest.kt
index d0fc90e..603ac21 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutTest.kt
@@ -94,7 +94,10 @@
@Test
fun measureAndPlaceTwoItems() {
val itemProvider = itemProvider({ 2 }) { index ->
- Box(Modifier.fillMaxSize().testTag("$index"))
+ Box(
+ Modifier
+ .fillMaxSize()
+ .testTag("$index"))
}
rule.setContent {
LazyLayout(itemProvider) {
@@ -118,8 +121,14 @@
@Test
fun measureAndPlaceMultipleLayoutsInOneItem() {
val itemProvider = itemProvider({ 1 }) { index ->
- Box(Modifier.fillMaxSize().testTag("${index}x0"))
- Box(Modifier.fillMaxSize().testTag("${index}x1"))
+ Box(
+ Modifier
+ .fillMaxSize()
+ .testTag("${index}x0"))
+ Box(
+ Modifier
+ .fillMaxSize()
+ .testTag("${index}x1"))
}
rule.setContent {
@@ -143,7 +152,10 @@
@Test
fun updatingitemProvider() {
var itemProvider by mutableStateOf(itemProvider({ 1 }) { index ->
- Box(Modifier.fillMaxSize().testTag("$index"))
+ Box(
+ Modifier
+ .fillMaxSize()
+ .testTag("$index"))
})
rule.setContent {
@@ -166,7 +178,10 @@
rule.runOnIdle {
itemProvider = itemProvider({ 2 }) { index ->
- Box(Modifier.fillMaxSize().testTag("$index"))
+ Box(
+ Modifier
+ .fillMaxSize()
+ .testTag("$index"))
}
}
@@ -178,7 +193,10 @@
fun stateBaseditemProvider() {
var itemCount by mutableStateOf(1)
val itemProvider = itemProvider({ itemCount }) { index ->
- Box(Modifier.fillMaxSize().testTag("$index"))
+ Box(
+ Modifier
+ .fillMaxSize()
+ .testTag("$index"))
}
rule.setContent {
@@ -228,7 +246,11 @@
}
}
val itemProvider = itemProvider({ 1 }) { index ->
- Box(Modifier.fillMaxSize().testTag("$index").then(modifier))
+ Box(
+ Modifier
+ .fillMaxSize()
+ .testTag("$index")
+ .then(modifier))
}
var needToCompose by mutableStateOf(false)
val prefetchState = LazyLayoutPrefetchState()
@@ -335,13 +357,15 @@
fun nodeIsReusedWithoutExtraRemeasure() {
var indexToCompose by mutableStateOf<Int?>(0)
var remeasuresCount = 0
- val modifier = Modifier.layout { measurable, constraints ->
- val placeable = measurable.measure(constraints)
- remeasuresCount++
- layout(placeable.width, placeable.height) {
- placeable.place(0, 0)
+ val modifier = Modifier
+ .layout { measurable, constraints ->
+ val placeable = measurable.measure(constraints)
+ remeasuresCount++
+ layout(placeable.width, placeable.height) {
+ placeable.place(0, 0)
+ }
}
- }.fillMaxSize()
+ .fillMaxSize()
val itemProvider = itemProvider({ 2 }) {
Box(modifier)
}
@@ -376,6 +400,52 @@
}
@Test
+ fun nodeIsReusedWhenRemovedFirst() {
+ var itemCount by mutableStateOf(1)
+ var remeasuresCount = 0
+ val modifier = Modifier
+ .layout { measurable, constraints ->
+ val placeable = measurable.measure(constraints)
+ remeasuresCount++
+ layout(placeable.width, placeable.height) {
+ placeable.place(0, 0)
+ }
+ }
+ .fillMaxSize()
+ val itemProvider = itemProvider({ itemCount }) {
+ Box(modifier)
+ }
+
+ rule.setContent {
+ LazyLayout(itemProvider) { constraints ->
+ val node = if (itemCount == 1) {
+ measure(0, constraints).first()
+ } else {
+ null
+ }
+ layout(10, 10) {
+ node?.place(0, 0)
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ assertThat(remeasuresCount).isEqualTo(1)
+ // node will be kept for reuse
+ itemCount = 0
+ }
+
+ rule.runOnIdle {
+ // node should be now reused
+ itemCount = 1
+ }
+
+ rule.runOnIdle {
+ assertThat(remeasuresCount).isEqualTo(1)
+ }
+ }
+
+ @Test
fun regularCompositionIsUsedInPrefetchTimeCalculation() {
val itemProvider = itemProvider({ 1 }) {
Box(Modifier.fillMaxSize())
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Background.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Background.kt
index 910e79f..748d6b1 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Background.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Background.kt
@@ -89,7 +89,7 @@
)
private class BackgroundElement(
- private val color: Color? = null,
+ private val color: Color = Color.Unspecified,
private val brush: Brush? = null,
private val alpha: Float,
private val shape: Shape,
@@ -116,7 +116,7 @@
}
override fun hashCode(): Int {
- var result = color?.hashCode() ?: 0
+ var result = color.hashCode()
result = 31 * result + (brush?.hashCode() ?: 0)
result = 31 * result + alpha.hashCode()
result = 31 * result + shape.hashCode()
@@ -133,7 +133,7 @@
}
private class BackgroundNode(
- var color: Color?,
+ var color: Color,
var brush: Brush?,
var alpha: Float,
var shape: Shape,
@@ -155,7 +155,7 @@
}
private fun ContentDrawScope.drawRect() {
- color?.let { drawRect(color = it) }
+ if (color != Color.Unspecified) drawRect(color = color)
brush?.let { drawRect(brush = it, alpha = alpha) }
}
@@ -166,7 +166,7 @@
} else {
shape.createOutline(size, layoutDirection, this)
}
- color?.let { drawOutline(outline, color = it) }
+ if (color != Color.Unspecified) drawOutline(outline, color = color)
brush?.let { drawOutline(outline, brush = it, alpha = alpha) }
lastOutline = outline
lastSize = size
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutItemContentFactory.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutItemContentFactory.kt
index 4023a81..9b61189 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutItemContentFactory.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutItemContentFactory.kt
@@ -19,6 +19,7 @@
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.ReusableContentHost
import androidx.compose.runtime.Stable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
@@ -95,13 +96,11 @@
val index = itemProvider.findIndexByKey(key, lastKnownIndex).also {
lastKnownIndex = it
}
-
- if (index < itemProvider.itemCount) {
- val key = itemProvider.getKey(index)
- if (key == this.key) {
- StableSaveProvider(StableValue(saveableStateHolder), StableValue(key)) {
- itemProvider.Item(index)
- }
+ val indexIsUpToDate =
+ index < itemProvider.itemCount && itemProvider.getKey(index) == key
+ ReusableContentHost(active = indexIsUpToDate) {
+ StableSaveProvider(StableValue(saveableStateHolder), StableValue(key)) {
+ itemProvider.Item(index)
}
}
DisposableEffect(key) {
diff --git a/compose/material/material/api/current.txt b/compose/material/material/api/current.txt
index 4eea647..c9c5c53 100644
--- a/compose/material/material/api/current.txt
+++ b/compose/material/material/api/current.txt
@@ -7,7 +7,8 @@
}
public final class AndroidMenu_androidKt {
- method @androidx.compose.runtime.Composable public static void DropdownMenu(boolean expanded, kotlin.jvm.functions.Function0<kotlin.Unit> onDismissRequest, optional androidx.compose.ui.Modifier modifier, optional long offset, optional androidx.compose.ui.window.PopupProperties properties, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit> content);
+ method @androidx.compose.runtime.Composable public static void DropdownMenu(boolean expanded, kotlin.jvm.functions.Function0<kotlin.Unit> onDismissRequest, optional androidx.compose.ui.Modifier modifier, optional long offset, optional androidx.compose.foundation.ScrollState scrollState, optional androidx.compose.ui.window.PopupProperties properties, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit> content);
+ method @Deprecated @androidx.compose.runtime.Composable public static void DropdownMenu(boolean expanded, kotlin.jvm.functions.Function0<? extends kotlin.Unit> onDismissRequest, optional androidx.compose.ui.Modifier modifier, optional long offset, optional androidx.compose.ui.window.PopupProperties properties, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,? extends kotlin.Unit> content);
method @androidx.compose.runtime.Composable public static void DropdownMenuItem(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> content);
}
diff --git a/compose/material/material/api/public_plus_experimental_current.txt b/compose/material/material/api/public_plus_experimental_current.txt
index d9fe905..feb60c4b 100644
--- a/compose/material/material/api/public_plus_experimental_current.txt
+++ b/compose/material/material/api/public_plus_experimental_current.txt
@@ -7,7 +7,8 @@
}
public final class AndroidMenu_androidKt {
- method @androidx.compose.runtime.Composable public static void DropdownMenu(boolean expanded, kotlin.jvm.functions.Function0<kotlin.Unit> onDismissRequest, optional androidx.compose.ui.Modifier modifier, optional long offset, optional androidx.compose.ui.window.PopupProperties properties, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit> content);
+ method @androidx.compose.runtime.Composable public static void DropdownMenu(boolean expanded, kotlin.jvm.functions.Function0<kotlin.Unit> onDismissRequest, optional androidx.compose.ui.Modifier modifier, optional long offset, optional androidx.compose.foundation.ScrollState scrollState, optional androidx.compose.ui.window.PopupProperties properties, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit> content);
+ method @Deprecated @androidx.compose.runtime.Composable public static void DropdownMenu(boolean expanded, kotlin.jvm.functions.Function0<? extends kotlin.Unit> onDismissRequest, optional androidx.compose.ui.Modifier modifier, optional long offset, optional androidx.compose.ui.window.PopupProperties properties, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,? extends kotlin.Unit> content);
method @androidx.compose.runtime.Composable public static void DropdownMenuItem(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> content);
}
@@ -426,7 +427,7 @@
}
@androidx.compose.material.ExperimentalMaterialApi @kotlin.jvm.JvmDefaultWithCompatibility public interface ExposedDropdownMenuBoxScope {
- method @androidx.compose.runtime.Composable public default void ExposedDropdownMenu(boolean expanded, kotlin.jvm.functions.Function0<kotlin.Unit> onDismissRequest, optional androidx.compose.ui.Modifier modifier, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit> content);
+ method @androidx.compose.runtime.Composable public default void ExposedDropdownMenu(boolean expanded, kotlin.jvm.functions.Function0<kotlin.Unit> onDismissRequest, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.foundation.ScrollState scrollState, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit> content);
method public androidx.compose.ui.Modifier exposedDropdownSize(androidx.compose.ui.Modifier, optional boolean matchTextFieldWidth);
}
diff --git a/compose/material/material/api/restricted_current.txt b/compose/material/material/api/restricted_current.txt
index 4eea647..c9c5c53 100644
--- a/compose/material/material/api/restricted_current.txt
+++ b/compose/material/material/api/restricted_current.txt
@@ -7,7 +7,8 @@
}
public final class AndroidMenu_androidKt {
- method @androidx.compose.runtime.Composable public static void DropdownMenu(boolean expanded, kotlin.jvm.functions.Function0<kotlin.Unit> onDismissRequest, optional androidx.compose.ui.Modifier modifier, optional long offset, optional androidx.compose.ui.window.PopupProperties properties, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit> content);
+ method @androidx.compose.runtime.Composable public static void DropdownMenu(boolean expanded, kotlin.jvm.functions.Function0<kotlin.Unit> onDismissRequest, optional androidx.compose.ui.Modifier modifier, optional long offset, optional androidx.compose.foundation.ScrollState scrollState, optional androidx.compose.ui.window.PopupProperties properties, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit> content);
+ method @Deprecated @androidx.compose.runtime.Composable public static void DropdownMenu(boolean expanded, kotlin.jvm.functions.Function0<? extends kotlin.Unit> onDismissRequest, optional androidx.compose.ui.Modifier modifier, optional long offset, optional androidx.compose.ui.window.PopupProperties properties, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,? extends kotlin.Unit> content);
method @androidx.compose.runtime.Composable public static void DropdownMenuItem(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> content);
}
diff --git a/compose/material/material/integration-tests/material-catalog/src/main/java/androidx/compose/material/catalog/library/model/Examples.kt b/compose/material/material/integration-tests/material-catalog/src/main/java/androidx/compose/material/catalog/library/model/Examples.kt
index ccbd6ff..30fa679 100644
--- a/compose/material/material/integration-tests/material-catalog/src/main/java/androidx/compose/material/catalog/library/model/Examples.kt
+++ b/compose/material/material/integration-tests/material-catalog/src/main/java/androidx/compose/material/catalog/library/model/Examples.kt
@@ -50,6 +50,7 @@
import androidx.compose.material.samples.LeadingIconTabs
import androidx.compose.material.samples.LinearProgressIndicatorSample
import androidx.compose.material.samples.MenuSample
+import androidx.compose.material.samples.MenuWithScrollStateSample
import androidx.compose.material.samples.ModalBottomSheetSample
import androidx.compose.material.samples.ModalDrawerSample
import androidx.compose.material.samples.NavigationRailBottomAlignSample
@@ -398,6 +399,13 @@
MenuSample()
},
Example(
+ name = ::MenuWithScrollStateSample.name,
+ description = MenusExampleDescription,
+ sourceUrl = MenusExampleSourceUrl
+ ) {
+ MenuWithScrollStateSample()
+ },
+ Example(
name = ::ExposedDropdownMenuSample.name,
description = MenusExampleDescription,
sourceUrl = MenusExampleSourceUrl
@@ -703,14 +711,18 @@
description = TextFieldsExampleDescription,
sourceUrl = TextFieldsExampleSourceUrl
) {
- TextArea()
+ TextArea()
}
).map {
// By default text field samples are minimal and don't have a `width` modifier to restrict the
// width. As a result, they grow horizontally if enough text is typed. To prevent this behavior
// in Catalog app the code below restricts the width of every text field sample
it.copy(content = {
- Box(Modifier.wrapContentWidth().width(280.dp)) { it.content() }
+ Box(
+ Modifier
+ .wrapContentWidth()
+ .width(280.dp)
+ ) { it.content() }
})
}
diff --git a/compose/material/material/samples/src/main/java/androidx/compose/material/samples/MenuSamples.kt b/compose/material/material/samples/src/main/java/androidx/compose/material/samples/MenuSamples.kt
index bf6f420..f9c6a77 100644
--- a/compose/material/material/samples/src/main/java/androidx/compose/material/samples/MenuSamples.kt
+++ b/compose/material/material/samples/src/main/java/androidx/compose/material/samples/MenuSamples.kt
@@ -20,6 +20,7 @@
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.wrapContentSize
+import androidx.compose.foundation.rememberScrollState
import androidx.compose.material.Divider
import androidx.compose.material.DropdownMenu
import androidx.compose.material.DropdownMenuItem
@@ -29,6 +30,7 @@
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.MoreVert
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
@@ -41,7 +43,9 @@
fun MenuSample() {
var expanded by remember { mutableStateOf(false) }
- Box(modifier = Modifier.fillMaxSize().wrapContentSize(Alignment.TopStart)) {
+ Box(modifier = Modifier
+ .fillMaxSize()
+ .wrapContentSize(Alignment.TopStart)) {
IconButton(onClick = { expanded = true }) {
Icon(Icons.Default.MoreVert, contentDescription = "Localized description")
}
@@ -61,4 +65,37 @@
}
}
}
-}
\ No newline at end of file
+}
+
+@Sampled
+@Composable
+fun MenuWithScrollStateSample() {
+ var expanded by remember { mutableStateOf(false) }
+ val scrollState = rememberScrollState()
+ Box(
+ modifier = Modifier
+ .fillMaxSize()
+ .wrapContentSize(Alignment.TopStart)
+ ) {
+ IconButton(onClick = { expanded = true }) {
+ Icon(Icons.Default.MoreVert, contentDescription = "Localized description")
+ }
+ DropdownMenu(
+ expanded = expanded,
+ onDismissRequest = { expanded = false },
+ scrollState = scrollState
+ ) {
+ repeat(30) {
+ DropdownMenuItem(onClick = { /* Handle item! */ }) {
+ Text("Item ${it + 1}")
+ }
+ }
+ }
+ LaunchedEffect(expanded) {
+ if (expanded) {
+ // Scroll to show the bottom menu items.
+ scrollState.scrollTo(scrollState.maxValue)
+ }
+ }
+ }
+}
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/ExposedDropdownMenuTest.kt b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/ExposedDropdownMenuTest.kt
index 17c6cc8..23300d9 100644
--- a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/ExposedDropdownMenuTest.kt
+++ b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/ExposedDropdownMenuTest.kt
@@ -22,7 +22,9 @@
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.rememberScrollState
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
@@ -35,9 +37,11 @@
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.platform.ComposeView
+import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.assertIsFocused
+import androidx.compose.ui.test.assertIsNotDisplayed
import androidx.compose.ui.test.assertTextContains
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithTag
@@ -336,6 +340,47 @@
// Should not have crashed.
}
+ @Test
+ fun withScrolledContent() {
+ rule.setMaterialContent {
+ Box(Modifier.fillMaxSize()) {
+ ExposedDropdownMenuBox(
+ modifier = Modifier.align(Alignment.Center),
+ expanded = true,
+ onExpandedChange = { }
+ ) {
+ val scrollState = rememberScrollState()
+ TextField(
+ value = "",
+ onValueChange = { },
+ label = { Text("Label") },
+ )
+ ExposedDropdownMenu(
+ expanded = true,
+ onDismissRequest = { },
+ scrollState = scrollState
+ ) {
+ repeat(100) {
+ Box(
+ Modifier
+ .testTag("MenuContent ${it + 1}")
+ .size(with(LocalDensity.current) { 70.toDp() })
+ )
+ }
+ }
+ LaunchedEffect(Unit) {
+ scrollState.scrollTo(scrollState.maxValue)
+ }
+ }
+ }
+ }
+
+ rule.waitForIdle()
+
+ rule.onNodeWithTag("MenuContent 1").assertIsNotDisplayed()
+ rule.onNodeWithTag("MenuContent 100").assertIsDisplayed()
+ }
+
@Composable
fun ExposedDropdownMenuForTest(
expanded: Boolean,
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/MenuTest.kt b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/MenuTest.kt
index ace0f2a..8ad3242 100644
--- a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/MenuTest.kt
+++ b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/MenuTest.kt
@@ -21,6 +21,8 @@
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.requiredSize
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
@@ -29,6 +31,8 @@
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.test.ExperimentalTestApi
+import androidx.compose.ui.test.assertIsDisplayed
+import androidx.compose.ui.test.assertIsNotDisplayed
import androidx.compose.ui.test.hasAnyDescendant
import androidx.compose.ui.test.hasTestTag
import androidx.compose.ui.test.isPopup
@@ -128,6 +132,42 @@
}
@Test
+ fun menu_scrolledContent() {
+ rule.setContent {
+ with(LocalDensity.current) {
+ Box(
+ Modifier
+ .requiredSize(20.toDp())
+ .background(color = Color.Blue)
+ ) {
+ val scrollState = rememberScrollState()
+ DropdownMenu(
+ expanded = true,
+ onDismissRequest = {},
+ scrollState = scrollState
+ ) {
+ repeat(100) {
+ Box(
+ Modifier
+ .testTag("MenuContent ${it + 1}")
+ .size(70.toDp())
+ )
+ }
+ }
+ LaunchedEffect(Unit) {
+ scrollState.scrollTo(scrollState.maxValue)
+ }
+ }
+ }
+ }
+
+ rule.waitForIdle()
+
+ rule.onNodeWithTag("MenuContent 1").assertIsNotDisplayed()
+ rule.onNodeWithTag("MenuContent 100").assertIsDisplayed()
+ }
+
+ @Test
fun menu_positioning_bottomEnd() {
val screenWidth = 500
val screenHeight = 1000
diff --git a/compose/material/material/src/androidMain/kotlin/androidx/compose/material/AndroidMenu.android.kt b/compose/material/material/src/androidMain/kotlin/androidx/compose/material/AndroidMenu.android.kt
index 94c0373..e1bd14d 100644
--- a/compose/material/material/src/androidMain/kotlin/androidx/compose/material/AndroidMenu.android.kt
+++ b/compose/material/material/src/androidMain/kotlin/androidx/compose/material/AndroidMenu.android.kt
@@ -17,11 +17,13 @@
package androidx.compose.material
import androidx.compose.animation.core.MutableTransitionState
+import androidx.compose.foundation.ScrollState
import androidx.compose.foundation.interaction.Interaction
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.RowScope
+import androidx.compose.foundation.rememberScrollState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
@@ -70,6 +72,15 @@
* tapping outside the menu's bounds
* @param offset [DpOffset] to be added to the position of the menu
*/
+@Deprecated(
+ level = DeprecationLevel.HIDDEN,
+ replaceWith = ReplaceWith(
+ expression = "DropdownMenu(expanded,onDismissRequest, modifier, offset, " +
+ "rememberScrollState(), properties, content)",
+ "androidx.compose.foundation.rememberScrollState"
+ ),
+ message = "Replaced by a DropdownMenu function with a ScrollState parameter"
+)
@Composable
fun DropdownMenu(
expanded: Boolean,
@@ -78,6 +89,69 @@
offset: DpOffset = DpOffset(0.dp, 0.dp),
properties: PopupProperties = PopupProperties(focusable = true),
content: @Composable ColumnScope.() -> Unit
+) = DropdownMenu(
+ expanded = expanded,
+ onDismissRequest = onDismissRequest,
+ modifier = modifier,
+ offset = offset,
+ scrollState = rememberScrollState(),
+ properties = properties,
+ content = content
+)
+
+/**
+ * <a href="https://material.io/components/menus#dropdown-menu" class="external" target="_blank">Material Design dropdown menu</a>.
+ *
+ * A dropdown menu is a compact way of displaying multiple choices. It appears upon interaction with
+ * an element (such as an icon or button) or when users perform a specific action.
+ *
+ * 
+ *
+ * A [DropdownMenu] behaves similarly to a [Popup], and will use the position of the parent layout
+ * to position itself on screen. Commonly a [DropdownMenu] will be placed in a [Box] with a sibling
+ * that will be used as the 'anchor'. Note that a [DropdownMenu] by itself will not take up any
+ * space in a layout, as the menu is displayed in a separate window, on top of other content.
+ *
+ * The [content] of a [DropdownMenu] will typically be [DropdownMenuItem]s, as well as custom
+ * content. Using [DropdownMenuItem]s will result in a menu that matches the Material
+ * specification for menus. Also note that the [content] is placed inside a scrollable [Column],
+ * so using a [LazyColumn] as the root layout inside [content] is unsupported.
+ *
+ * [onDismissRequest] will be called when the menu should close - for example when there is a
+ * tap outside the menu, or when the back key is pressed.
+ *
+ * [DropdownMenu] changes its positioning depending on the available space, always trying to be
+ * fully visible. It will try to expand horizontally, depending on layout direction, to the end of
+ * its parent, then to the start of its parent, and then screen end-aligned. Vertically, it will
+ * try to expand to the bottom of its parent, then from the top of its parent, and then screen
+ * top-aligned. An [offset] can be provided to adjust the positioning of the menu for cases when
+ * the layout bounds of its parent do not coincide with its visual bounds. Note the offset will
+ * be applied in the direction in which the menu will decide to expand.
+ *
+ * Example usage:
+ * @sample androidx.compose.material.samples.MenuSample
+ *
+ * Example usage with a [ScrollState] to control the menu items scroll position:
+ * @sample androidx.compose.material.samples.MenuWithScrollStateSample
+ *
+ * @param expanded whether the menu is expanded or not
+ * @param onDismissRequest called when the user requests to dismiss the menu, such as by tapping
+ * outside the menu's bounds
+ * @param modifier [Modifier] to be applied to the menu's content
+ * @param offset [DpOffset] to be added to the position of the menu
+ * @param scrollState a [ScrollState] to used by the menu's content for items vertical scrolling
+ * @param properties [PopupProperties] for further customization of this popup's behavior
+ * @param content the content of this dropdown menu, typically a [DropdownMenuItem]
+ */
+@Composable
+fun DropdownMenu(
+ expanded: Boolean,
+ onDismissRequest: () -> Unit,
+ modifier: Modifier = Modifier,
+ offset: DpOffset = DpOffset(0.dp, 0.dp),
+ scrollState: ScrollState = rememberScrollState(),
+ properties: PopupProperties = PopupProperties(focusable = true),
+ content: @Composable ColumnScope.() -> Unit
) {
val expandedStates = remember { MutableTransitionState(false) }
expandedStates.targetState = expanded
@@ -100,6 +174,7 @@
DropdownMenuContent(
expandedStates = expandedStates,
transformOriginState = transformOriginState,
+ scrollState = scrollState,
modifier = modifier,
content = content
)
diff --git a/compose/material/material/src/androidMain/kotlin/androidx/compose/material/ExposedDropdownMenu.kt b/compose/material/material/src/androidMain/kotlin/androidx/compose/material/ExposedDropdownMenu.kt
index 4a46e56..660f1c2 100644
--- a/compose/material/material/src/androidMain/kotlin/androidx/compose/material/ExposedDropdownMenu.kt
+++ b/compose/material/material/src/androidMain/kotlin/androidx/compose/material/ExposedDropdownMenu.kt
@@ -21,6 +21,7 @@
import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.core.MutableTransitionState
import androidx.compose.animation.core.tween
+import androidx.compose.foundation.ScrollState
import androidx.compose.foundation.gestures.awaitEachGesture
import androidx.compose.foundation.gestures.awaitFirstDown
import androidx.compose.foundation.gestures.waitForUpOrCancellation
@@ -30,6 +31,7 @@
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.rememberScrollState
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowDropDown
import androidx.compose.material.internal.ExposedDropdownMenuPopup
@@ -223,6 +225,7 @@
* @param onDismissRequest Called when the user requests to dismiss the menu, such as by
* tapping outside the menu's bounds
* @param modifier The modifier to apply to this layout
+ * @param scrollState a [ScrollState] to used by the menu's content for items vertical scrolling
* @param content The content of the [ExposedDropdownMenu]
*/
@Composable
@@ -230,6 +233,7 @@
expanded: Boolean,
onDismissRequest: () -> Unit,
modifier: Modifier = Modifier,
+ scrollState: ScrollState = rememberScrollState(),
content: @Composable ColumnScope.() -> Unit
) {
// TODO(b/202810604): use DropdownMenu when PopupProperties constructor is stable
@@ -261,6 +265,7 @@
DropdownMenuContent(
expandedStates = expandedStates,
transformOriginState = transformOriginState,
+ scrollState = scrollState,
modifier = modifier.exposedDropdownSize(),
content = content
)
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Menu.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Menu.kt
index 3e80837..9d3dadbd 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Menu.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Menu.kt
@@ -21,8 +21,9 @@
import androidx.compose.animation.core.animateFloat
import androidx.compose.animation.core.tween
import androidx.compose.animation.core.updateTransition
-import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.foundation.ScrollState
import androidx.compose.foundation.clickable
+import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.IntrinsicSize
@@ -33,14 +34,13 @@
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.sizeIn
import androidx.compose.foundation.layout.width
-import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.ripple.rememberRipple
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.Immutable
-import androidx.compose.runtime.getValue
import androidx.compose.runtime.MutableState
+import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@@ -57,11 +57,11 @@
import kotlin.math.max
import kotlin.math.min
-@Suppress("ModifierParameter")
@Composable
internal fun DropdownMenuContent(
expandedStates: MutableTransitionState<Boolean>,
transformOriginState: MutableState<TransformOrigin>,
+ scrollState: ScrollState,
modifier: Modifier = Modifier,
content: @Composable ColumnScope.() -> Unit
) {
@@ -126,7 +126,7 @@
modifier = modifier
.padding(vertical = DropdownMenuVerticalPadding)
.width(IntrinsicSize.Max)
- .verticalScroll(rememberScrollState()),
+ .verticalScroll(scrollState),
content = content
)
}
diff --git a/compose/material/material/src/desktopMain/kotlin/androidx/compose/material/DesktopMenu.desktop.kt b/compose/material/material/src/desktopMain/kotlin/androidx/compose/material/DesktopMenu.desktop.kt
index 4da69f2..c197e65 100644
--- a/compose/material/material/src/desktopMain/kotlin/androidx/compose/material/DesktopMenu.desktop.kt
+++ b/compose/material/material/src/desktopMain/kotlin/androidx/compose/material/DesktopMenu.desktop.kt
@@ -21,6 +21,8 @@
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.RowScope
+import androidx.compose.foundation.ScrollState
+import androidx.compose.foundation.rememberScrollState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.mutableStateOf
@@ -74,7 +76,15 @@
* @param offset [DpOffset] to be added to the position of the menu
* @param content content lambda
*/
-@Suppress("ModifierParameter")
+@Deprecated(
+ level = DeprecationLevel.HIDDEN,
+ replaceWith = ReplaceWith(
+ expression = "DropdownMenu(expanded,onDismissRequest, focusable, modifier, offset, " +
+ "rememberScrollState(), content)",
+ "androidx.compose.foundation.rememberScrollState"
+ ),
+ message = "Replaced by a DropdownMenu function with a ScrollState parameter"
+)
@Composable
fun DropdownMenu(
expanded: Boolean,
@@ -83,6 +93,64 @@
modifier: Modifier = Modifier,
offset: DpOffset = DpOffset(0.dp, 0.dp),
content: @Composable ColumnScope.() -> Unit
+) = DropdownMenu(
+ expanded = expanded,
+ onDismissRequest = onDismissRequest,
+ focusable = focusable,
+ modifier = modifier,
+ offset = offset,
+ scrollState = rememberScrollState(),
+ content = content
+)
+
+/**
+ * A Material Design [dropdown menu](https://material.io/components/menus#dropdown-menu).
+ *
+ * A [DropdownMenu] behaves similarly to a [Popup], and will use the position of the parent layout
+ * to position itself on screen. Commonly a [DropdownMenu] will be placed in a [Box] with a sibling
+ * that will be used as the 'anchor'. Note that a [DropdownMenu] by itself will not take up any
+ * space in a layout, as the menu is displayed in a separate window, on top of other content.
+ *
+ * The [content] of a [DropdownMenu] will typically be [DropdownMenuItem]s, as well as custom
+ * content. Using [DropdownMenuItem]s will result in a menu that matches the Material
+ * specification for menus. Also note that the [content] is placed inside a scrollable [Column],
+ * so using a [LazyColumn] as the root layout inside [content] is unsupported.
+ *
+ * [onDismissRequest] will be called when the menu should close - for example when there is a
+ * tap outside the menu, or when the back key is pressed.
+ *
+ * [DropdownMenu] changes its positioning depending on the available space, always trying to be
+ * fully visible. It will try to expand horizontally, depending on layout direction, to the end of
+ * its parent, then to the start of its parent, and then screen end-aligned. Vertically, it will
+ * try to expand to the bottom of its parent, then from the top of its parent, and then screen
+ * top-aligned. An [offset] can be provided to adjust the positioning of the menu for cases when
+ * the layout bounds of its parent do not coincide with its visual bounds. Note the offset will
+ * be applied in the direction in which the menu will decide to expand.
+ *
+ * Example usage:
+ * @sample androidx.compose.material.samples.MenuSample
+ *
+ * Example usage with a [ScrollState] to control the menu items scroll position:
+ * @sample androidx.compose.material.samples.MenuWithScrollStateSample
+ *
+ * @param expanded Whether the menu is currently open and visible to the user
+ * @param onDismissRequest Called when the user requests to dismiss the menu, such as by
+ * tapping outside the menu's bounds
+ * @param focusable Whether the dropdown can capture focus
+ * @param modifier [Modifier] to be applied to the menu's content
+ * @param offset [DpOffset] to be added to the position of the menu
+ * @param scrollState a [ScrollState] to used by the menu's content for items vertical scrolling
+ * @param content the content of this dropdown menu, typically a [DropdownMenuItem]
+ */
+@Composable
+fun DropdownMenu(
+ expanded: Boolean,
+ onDismissRequest: () -> Unit,
+ focusable: Boolean = true,
+ modifier: Modifier = Modifier,
+ offset: DpOffset = DpOffset(0.dp, 0.dp),
+ scrollState: ScrollState = rememberScrollState(),
+ content: @Composable ColumnScope.() -> Unit
) {
val expandedStates = remember { MutableTransitionState(false) }
expandedStates.targetState = expanded
@@ -110,6 +178,7 @@
expandedStates = expandedStates,
transformOriginState = transformOriginState,
modifier = modifier,
+ scrollState = scrollState,
content = content
)
}
@@ -164,7 +233,15 @@
* @param onDismissRequest Called when the user requests to dismiss the menu, such as by
* tapping outside the menu's bounds
*/
-@Suppress("ModifierParameter")
+@Deprecated(
+ level = DeprecationLevel.HIDDEN,
+ replaceWith = ReplaceWith(
+ expression = "CursorDropdownMenu(expanded,onDismissRequest, focusable, modifier, " +
+ "rememberScrollState(), content)",
+ "androidx.compose.foundation.rememberScrollState"
+ ),
+ message = "Replaced by a CursorDropdownMenu function with a ScrollState parameter"
+)
@Composable
fun CursorDropdownMenu(
expanded: Boolean,
@@ -172,6 +249,40 @@
focusable: Boolean = true,
modifier: Modifier = Modifier,
content: @Composable ColumnScope.() -> Unit
+) = CursorDropdownMenu(
+ expanded = expanded,
+ onDismissRequest = onDismissRequest,
+ focusable = focusable,
+ modifier = modifier,
+ scrollState = rememberScrollState(),
+ content = content
+)
+
+/**
+ *
+ * A [CursorDropdownMenu] behaves similarly to [Popup] and will use the current position of the mouse
+ * cursor to position itself on screen.
+ *
+ * The [content] of a [CursorDropdownMenu] will typically be [DropdownMenuItem]s, as well as custom
+ * content. Using [DropdownMenuItem]s will result in a menu that matches the Material
+ * specification for menus.
+ *
+ * @param expanded Whether the menu is currently open and visible to the user
+ * @param onDismissRequest Called when the user requests to dismiss the menu, such as by
+ * tapping outside the menu's bounds
+ * @param focusable Whether the dropdown can capture focus
+ * @param modifier [Modifier] to be applied to the menu's content
+ * @param scrollState a [ScrollState] to used by the menu's content for items vertical scrolling
+ * @param content the content of this dropdown menu, typically a [DropdownMenuItem]
+ */
+@Composable
+fun CursorDropdownMenu(
+ expanded: Boolean,
+ onDismissRequest: () -> Unit,
+ focusable: Boolean = true,
+ modifier: Modifier = Modifier,
+ scrollState: ScrollState = rememberScrollState(),
+ content: @Composable ColumnScope.() -> Unit
) {
val expandedStates = remember { MutableTransitionState(false) }
expandedStates.targetState = expanded
@@ -188,6 +299,7 @@
expandedStates = expandedStates,
transformOriginState = transformOriginState,
modifier = modifier,
+ scrollState = scrollState,
content = content
)
}
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/DividerScreenshotTest.kt b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/DividerScreenshotTest.kt
index 7374b08..1f62b814 100644
--- a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/DividerScreenshotTest.kt
+++ b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/DividerScreenshotTest.kt
@@ -31,6 +31,7 @@
import androidx.test.filters.MediumTest
import androidx.test.filters.SdkSuppress
import androidx.test.screenshot.AndroidXScreenshotTestRule
+import org.junit.Assume.assumeFalse
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@@ -64,6 +65,8 @@
@Test
fun darkTheme() {
+ assumeFalse("See b/272301182", Build.VERSION.SDK_INT == 33)
+
composeTestRule.setMaterialContent(darkColorScheme()) {
Column(Modifier.testTag(Tag)) {
Spacer(Modifier.size(10.dp))
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/MenuTest.kt b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/MenuTest.kt
index 0df58c9..26f91f1 100644
--- a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/MenuTest.kt
+++ b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/MenuTest.kt
@@ -146,7 +146,6 @@
}
}
- @OptIn(ExperimentalMaterial3Api::class)
@Test
fun menu_scrolledContent() {
rule.setContent {
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/NavigationBarScreenshotTest.kt b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/NavigationBarScreenshotTest.kt
index 3693245..3c05d96 100644
--- a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/NavigationBarScreenshotTest.kt
+++ b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/NavigationBarScreenshotTest.kt
@@ -21,6 +21,7 @@
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.PressInteraction
import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.height
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Favorite
import androidx.compose.runtime.Composable
@@ -33,6 +34,7 @@
import androidx.compose.ui.test.captureToImage
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.unit.dp
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
import androidx.test.filters.SdkSuppress
@@ -112,6 +114,25 @@
}
@Test
+ fun lightTheme_customHeight() {
+ val interactionSource = MutableInteractionSource()
+
+ var scope: CoroutineScope? = null
+
+ composeTestRule.setMaterialContent(lightColorScheme()) {
+ scope = rememberCoroutineScope()
+ DefaultNavigationBar(interactionSource, Modifier.height(64.dp))
+ }
+
+ assertNavigationBarMatches(
+ scope = scope!!,
+ interactionSource = interactionSource,
+ interaction = null,
+ goldenIdentifier = "navigationBar_lightTheme_customHeight"
+ )
+ }
+
+ @Test
fun darkTheme_defaultColors() {
val interactionSource = MutableInteractionSource()
@@ -211,14 +232,16 @@
*
* @param interactionSource the [MutableInteractionSource] for the first [NavigationBarItem], to
* control its visual state.
+ * @param modifier the [Modifier] applied to the navigation bar
* @param setUnselectedItemsAsDisabled when true, marks unselected items as disabled
*/
@Composable
private fun DefaultNavigationBar(
interactionSource: MutableInteractionSource,
+ modifier: Modifier = Modifier,
setUnselectedItemsAsDisabled: Boolean = false,
) {
- Box(Modifier.semantics(mergeDescendants = true) {}.testTag(Tag)) {
+ Box(modifier.semantics(mergeDescendants = true) {}.testTag(Tag)) {
NavigationBar {
NavigationBarItem(
icon = { Icon(Icons.Filled.Favorite, null) },
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/NavigationBarTest.kt b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/NavigationBarTest.kt
index e9a0168..2f6c21f 100644
--- a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/NavigationBarTest.kt
+++ b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/NavigationBarTest.kt
@@ -19,6 +19,7 @@
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.height
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Favorite
import androidx.compose.material3.tokens.NavigationBarTokens
@@ -266,42 +267,43 @@
@Test
fun navigationBarItemContent_withLabel_sizeAndPosition() {
rule.setMaterialContent(lightColorScheme()) {
- Box {
- NavigationBar {
- NavigationBarItem(
- modifier = Modifier.testTag("item"),
- icon = {
- Icon(Icons.Filled.Favorite, null, Modifier.testTag("icon"))
- },
- label = {
- Text("ItemText")
- },
- selected = true,
- onClick = {}
- )
- }
+ NavigationBar {
+ NavigationBarItem(
+ modifier = Modifier.testTag("item"),
+ icon = {
+ Icon(Icons.Filled.Favorite, null, Modifier.testTag("icon"))
+ },
+ label = {
+ Text("ItemText")
+ },
+ selected = true,
+ onClick = {}
+ )
}
}
val itemBounds = rule.onNodeWithTag("item").getUnclippedBoundsInRoot()
val iconBounds = rule.onNodeWithTag("icon", useUnmergedTree = true)
.getUnclippedBoundsInRoot()
- val textBounds = rule.onNodeWithText("ItemText", useUnmergedTree = true)
- .getUnclippedBoundsInRoot()
- // Distance from the bottom of the item to the text bottom, and from the top of the icon to
- // the top of the item
- val verticalPadding = NavigationBarItemVerticalPadding
-
- val itemBottom = itemBounds.height + itemBounds.top
- // Text bottom should be `verticalPadding` from the bottom of the item
- textBounds.bottom.assertIsEqualTo(itemBottom - verticalPadding)
+ // Distance from the top of the item to the top of the icon for the default height
+ val verticalPadding = 16.dp
rule.onNodeWithTag("icon", useUnmergedTree = true)
- // The icon should be centered in the item
+ // The icon should be horizontally centered in the item
.assertLeftPositionInRootIsEqualTo((itemBounds.width - iconBounds.width) / 2)
// The top of the icon is `verticalPadding` below the top of the item
.assertTopPositionInRootIsEqualTo(itemBounds.top + verticalPadding)
+
+ val iconBottom = iconBounds.top + iconBounds.height
+ // Text should be `IndicatorVerticalPadding + NavigationBarIndicatorToLabelPadding` from the
+ // bottom of the icon
+ rule.onNodeWithText("ItemText", useUnmergedTree = true)
+ .getUnclippedBoundsInRoot()
+ .top
+ .assertIsEqualTo(
+ iconBottom + IndicatorVerticalPadding + NavigationBarIndicatorToLabelPadding
+ )
}
@Test
@@ -367,6 +369,49 @@
}
@Test
+ fun navigationBarItemContent_customHeight_withLabel_sizeAndPosition() {
+ val defaultHeight = NavigationBarTokens.ContainerHeight
+ val customHeight = 64.dp
+
+ rule.setMaterialContent(lightColorScheme()) {
+ NavigationBar(Modifier.height(customHeight)) {
+ NavigationBarItem(
+ modifier = Modifier.testTag("item"),
+ icon = {
+ Icon(Icons.Filled.Favorite, null, Modifier.testTag("icon"))
+ },
+ label = { Text("Label") },
+ selected = true,
+ onClick = {}
+ )
+ }
+ }
+
+ // Vertical padding is removed symmetrically from top and bottom for smaller heights
+ val verticalPadding = 16.dp - (defaultHeight - customHeight) / 2
+
+ val itemBounds = rule.onNodeWithTag("item").getUnclippedBoundsInRoot()
+ val iconBounds = rule.onNodeWithTag("icon", useUnmergedTree = true)
+ .getUnclippedBoundsInRoot()
+
+ rule.onNodeWithTag("icon", useUnmergedTree = true)
+ // The icon should be horizontally centered in the item
+ .assertLeftPositionInRootIsEqualTo((itemBounds.width - iconBounds.width) / 2)
+ // The top of the icon is `verticalPadding` below the top of the item
+ .assertTopPositionInRootIsEqualTo(itemBounds.top + verticalPadding)
+
+ val iconBottom = iconBounds.top + iconBounds.height
+ // Text should be `IndicatorVerticalPadding + NavigationBarIndicatorToLabelPadding` from the
+ // bottom of the item
+ rule.onNodeWithText("Label", useUnmergedTree = true)
+ .getUnclippedBoundsInRoot()
+ .top
+ .assertIsEqualTo(
+ iconBottom + IndicatorVerticalPadding + NavigationBarIndicatorToLabelPadding
+ )
+ }
+
+ @Test
fun navigationBar_selectNewItem() {
rule.setMaterialContent(lightColorScheme()) {
var selectedItem by remember { mutableStateOf(0) }
diff --git a/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/AndroidMenu.android.kt b/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/AndroidMenu.android.kt
index 0acd46a..5948252 100644
--- a/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/AndroidMenu.android.kt
+++ b/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/AndroidMenu.android.kt
@@ -75,7 +75,6 @@
* @param content the content of this dropdown menu, typically a [DropdownMenuItem]
*/
@OptIn(ExperimentalMaterial3Api::class)
-@Suppress("ModifierParameter")
@Deprecated(
level = DeprecationLevel.HIDDEN,
replaceWith = ReplaceWith(
@@ -147,7 +146,6 @@
* @param properties [PopupProperties] for further customization of this popup's behavior
* @param content the content of this dropdown menu, typically a [DropdownMenuItem]
*/
-@Suppress("ModifierParameter")
@Composable
fun DropdownMenu(
expanded: Boolean,
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Menu.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Menu.kt
index 318985d..4b558c6 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Menu.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Menu.kt
@@ -61,7 +61,6 @@
import kotlin.math.max
import kotlin.math.min
-@Suppress("ModifierParameter")
@Composable
internal fun DropdownMenuContent(
expandedStates: MutableTransitionState<Boolean>,
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/NavigationBar.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/NavigationBar.kt
index 2746436..5f1bf664 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/NavigationBar.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/NavigationBar.kt
@@ -535,8 +535,8 @@
* [animationProgress].
*
* When [alwaysShowLabel] is true, the positions do not move. The [iconPlaceable] will be placed
- * near the top of the item and the [labelPlaceable] will be placed near the bottom, according to
- * the spec.
+ * near the top of the item and the [labelPlaceable] will be placed beneath it with padding,
+ * according to the spec.
*
* When [animationProgress] is 1 (representing the selected state), the positions will be the same
* as above.
@@ -573,11 +573,13 @@
): MeasureResult {
val height = constraints.maxHeight
- // Label should be `ItemVerticalPadding` from the bottom
- val labelY = height - labelPlaceable.height - NavigationBarItemVerticalPadding.roundToPx()
+ val contentTotalHeight = iconPlaceable.height + IndicatorVerticalPadding.roundToPx() +
+ NavigationBarIndicatorToLabelPadding.roundToPx() + labelPlaceable.height
+ val contentVerticalPadding = ((height - contentTotalHeight) / 2)
+ .coerceAtLeast(IndicatorVerticalPadding.roundToPx())
- // Icon (when selected) should be `ItemVerticalPadding` from the top
- val selectedIconY = NavigationBarItemVerticalPadding.roundToPx()
+ // Icon (when selected) should be `contentVerticalPadding` from top
+ val selectedIconY = contentVerticalPadding
val unselectedIconY =
if (alwaysShowLabel) selectedIconY else (height - iconPlaceable.height) / 2
@@ -588,6 +590,10 @@
// animationProgress.
val offset = (iconDistance * (1 - animationProgress)).roundToInt()
+ // Label should be fixed padding below icon
+ val labelY = selectedIconY + iconPlaceable.height + IndicatorVerticalPadding.roundToPx() +
+ NavigationBarIndicatorToLabelPadding.roundToPx()
+
val containerWidth = constraints.maxWidth
val labelX = (containerWidth - labelPlaceable.width) / 2
@@ -626,12 +632,13 @@
internal val NavigationBarItemHorizontalPadding: Dp = 8.dp
/*@VisibleForTesting*/
-internal val NavigationBarItemVerticalPadding: Dp = 16.dp
+internal val NavigationBarIndicatorToLabelPadding: Dp = 4.dp
private val IndicatorHorizontalPadding: Dp =
(NavigationBarTokens.ActiveIndicatorWidth - NavigationBarTokens.IconSize) / 2
-private val IndicatorVerticalPadding: Dp =
+/*@VisibleForTesting*/
+internal val IndicatorVerticalPadding: Dp =
(NavigationBarTokens.ActiveIndicatorHeight - NavigationBarTokens.IconSize) / 2
private val IndicatorVerticalOffset: Dp = 12.dp
\ No newline at end of file
diff --git a/compose/runtime/runtime-saveable/build.gradle b/compose/runtime/runtime-saveable/build.gradle
index b4d8073..d23376d 100644
--- a/compose/runtime/runtime-saveable/build.gradle
+++ b/compose/runtime/runtime-saveable/build.gradle
@@ -15,9 +15,8 @@
*/
-import androidx.build.AndroidXComposePlugin
+import androidx.build.KmpPlatformsKt
import androidx.build.LibraryType
-import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins {
id("AndroidXPlugin")
@@ -25,77 +24,54 @@
id("com.android.library")
}
-AndroidXComposePlugin.applyAndConfigureKotlinPlugin(project)
+def desktopEnabled = KmpPlatformsKt.enableDesktop(project)
-dependencies {
+androidXMultiplatform {
+ android()
+ if (desktopEnabled) desktop()
- if(!AndroidXComposePlugin.isMultiplatformEnabled(project)) {
- /* When updating dependencies, make sure to make the an an analogous update in the
- corresponding block below */
- api project(":compose:runtime:runtime")
- api "androidx.annotation:annotation:1.1.0"
-
- implementation(libs.kotlinStdlib)
-
- testImplementation(libs.junit)
- testImplementation(libs.truth)
- testImplementation(libs.testCore)
- testImplementation(libs.testRules)
-
- androidTestImplementation projectOrArtifact(':compose:ui:ui')
- androidTestImplementation projectOrArtifact(":compose:ui:ui-test-junit4")
- androidTestImplementation projectOrArtifact(":compose:test-utils")
- androidTestImplementation "androidx.fragment:fragment:1.3.0"
- androidTestImplementation projectOrArtifact(":activity:activity-compose")
- androidTestImplementation(libs.testUiautomator)
- androidTestImplementation(libs.testCore)
- androidTestImplementation(libs.testRules)
- androidTestImplementation(libs.testRunner)
- androidTestImplementation(libs.espressoCore)
- androidTestImplementation(libs.junit)
- androidTestImplementation(libs.truth)
- androidTestImplementation(libs.dexmakerMockito)
- androidTestImplementation(libs.mockitoCore)
-
- lintPublish(project(":compose:runtime:runtime-saveable-lint"))
-
- samples(projectOrArtifact(":compose:runtime:runtime-saveable:runtime-saveable-samples"))
- }
-}
-
-if(AndroidXComposePlugin.isMultiplatformEnabled(project)) {
- androidXComposeMultiplatform {
- android()
- desktop()
- }
-
- kotlin {
- /* When updating dependencies, make sure to make the an an analogous update in the
- corresponding block above */
- sourceSets {
- commonMain.dependencies {
+ sourceSets {
+ commonMain {
+ dependencies {
implementation(libs.kotlinStdlibCommon)
api project(":compose:runtime:runtime")
}
+ }
- androidMain.dependencies {
+ commonTest {
+ dependencies {
+ }
+ }
+
+ jvmMain {
+ dependencies {
+ }
+ }
+
+
+ androidMain {
+ dependsOn(jvmMain)
+ dependencies {
implementation(libs.kotlinStdlib)
api "androidx.annotation:annotation:1.1.0"
}
+ }
- // TODO(b/214407011): These dependencies leak into instrumented tests as well. If you
- // need to add Robolectric (which must be kept out of androidAndroidTest), use a top
- // level dependencies block instead:
- // `dependencies { testImplementation(libs.robolectric) }`
- androidTest.dependencies {
- implementation(libs.testRules)
- implementation(libs.testRunner)
- implementation(libs.junit)
- implementation(libs.truth)
+ if (desktopEnabled) {
+ desktopMain {
+ dependsOn(jvmMain)
}
+ }
- androidAndroidTest.dependencies {
+ jvmTest {
+ dependencies {
+ }
+ }
+
+ androidAndroidTest {
+ dependsOn(jvmTest)
+ dependencies {
implementation project(':compose:ui:ui')
implementation project(":compose:ui:ui-test-junit4")
implementation project(":compose:test-utils")
@@ -112,10 +88,32 @@
implementation(libs.mockitoCore)
}
}
+
+ // TODO(b/214407011): These dependencies leak into instrumented tests as well. If you
+ // need to add Robolectric (which must be kept out of androidAndroidTest), use a top
+ // level dependencies block instead:
+ // `dependencies { testImplementation(libs.robolectric) }`
+ androidTest {
+ dependsOn(jvmTest)
+ dependencies {
+ implementation(libs.testRules)
+ implementation(libs.testRunner)
+ implementation(libs.junit)
+ implementation(libs.truth)
+ }
+ }
+
+ if (desktopEnabled) {
+ desktopTest {
+ dependsOn(jvmTest)
+ }
+ }
}
- dependencies {
- samples(projectOrArtifact(":compose:runtime:runtime-saveable:runtime-saveable-samples"))
- }
+}
+
+dependencies {
+ samples(projectOrArtifact(":compose:runtime:runtime-saveable:runtime-saveable-samples"))
+ lintPublish(project(":compose:runtime:runtime-saveable-lint"))
}
androidx {
diff --git a/compose/test-utils/build.gradle b/compose/test-utils/build.gradle
index 5b5d26c..46eecc3 100644
--- a/compose/test-utils/build.gradle
+++ b/compose/test-utils/build.gradle
@@ -14,10 +14,8 @@
* limitations under the License.
*/
-import androidx.build.AndroidXComposePlugin
import androidx.build.LibraryType
import androidx.build.Publish
-import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins {
id("AndroidXPlugin")
@@ -25,89 +23,93 @@
id("AndroidXComposePlugin")
}
-AndroidXComposePlugin.applyAndConfigureKotlinPlugin(project)
+def desktopEnabled = false // b/276387374 TODO: KmpPlatformsKt.enableDesktop(project)
-dependencies {
+androidXMultiplatform {
+ android()
+ if (desktopEnabled) desktop()
- if(!AndroidXComposePlugin.isMultiplatformEnabled(project)) {
- /*
- * When updating dependencies, make sure to make the an an analogous update in the
- * corresponding block below
- */
-
- api("androidx.activity:activity:1.2.0")
- api(projectOrArtifact(":compose:ui:ui-test-junit4"))
- api(project(":test:screenshot:screenshot"))
-
- implementation(libs.kotlinStdlibCommon)
- implementation(projectOrArtifact(":compose:runtime:runtime"))
- implementation(projectOrArtifact(":compose:ui:ui-unit"))
- implementation(projectOrArtifact(":compose:ui:ui-graphics"))
- implementation("androidx.activity:activity-compose:1.3.1")
- // old version of common-java8 conflicts with newer version, because both have
- // DefaultLifecycleEventObserver.
- // Outside of androidx this is resolved via constraint added to lifecycle-common,
- // but it doesn't work in androidx.
- // See aosp/1804059
- implementation("androidx.lifecycle:lifecycle-common-java8:2.5.1")
- implementation(libs.testCore)
- implementation(libs.testRules)
-
- // This has stub APIs for access to legacy Android APIs, so we don't want
- // any dependency on this module.
- compileOnly(projectOrArtifact(":compose:ui:ui-android-stubs"))
-
- testImplementation(libs.truth)
-
- androidTestImplementation(libs.truth)
- androidTestImplementation(projectOrArtifact(":compose:material:material"))
- }
-}
-
-if (AndroidXComposePlugin.isMultiplatformEnabled(project)) {
- androidXComposeMultiplatform {
- android()
- }
-
- kotlin {
- /*
- * When updating dependencies, make sure to make the an an analogous update in the
- * corresponding block above
- */
- sourceSets {
- commonMain.dependencies {
+ sourceSets {
+ commonMain {
+ dependencies {
implementation(libs.kotlinStdlibCommon)
implementation(projectOrArtifact(":compose:runtime:runtime"))
implementation(projectOrArtifact(":compose:ui:ui-unit"))
implementation(projectOrArtifact(":compose:ui:ui-graphics"))
implementation(projectOrArtifact(":compose:ui:ui-test-junit4"))
}
+ }
+ androidMain.dependencies {
+ api("androidx.activity:activity:1.2.0")
+ implementation "androidx.activity:activity-compose:1.3.1"
+ api(projectOrArtifact(":compose:ui:ui-test-junit4"))
+ api(project(":test:screenshot:screenshot"))
+ // This has stub APIs for access to legacy Android APIs, so we don't want
+ // any dependency on this module.
+ compileOnly(projectOrArtifact(":compose:ui:ui-android-stubs"))
+ implementation(libs.testCore)
+ implementation(libs.testRules)
+ }
- androidMain.dependencies {
- api("androidx.activity:activity:1.2.0")
- implementation "androidx.activity:activity-compose:1.3.1"
- api(projectOrArtifact(":compose:ui:ui-test-junit4"))
- api(project(":test:screenshot:screenshot"))
- // This has stub APIs for access to legacy Android APIs, so we don't want
- // any dependency on this module.
- compileOnly(projectOrArtifact(":compose:ui:ui-android-stubs"))
- implementation(libs.testCore)
- implementation(libs.testRules)
+ commonTest {
+ dependencies {
}
+ }
- // TODO(b/214407011): These dependencies leak into instrumented tests as well. If you
- // need to add Robolectric (which must be kept out of androidAndroidTest), use a top
- // level dependencies block instead:
- // `dependencies { testImplementation(libs.robolectric) }`
- androidTest.dependencies {
- implementation(libs.truth)
+ jvmMain {
+ dependsOn(commonMain)
+ dependencies {
}
+ }
- androidAndroidTest.dependencies {
+
+ androidMain {
+ dependsOn(jvmMain)
+ dependencies {
+ }
+ }
+
+ if (desktopEnabled) {
+ desktopMain {
+ dependsOn(jvmMain)
+ dependencies {
+ }
+ }
+ }
+
+ jvmTest {
+ dependsOn(commonTest)
+ dependencies {
+ }
+ }
+
+ androidAndroidTest {
+ dependsOn(jvmTest)
+ dependencies {
implementation(libs.truth)
implementation(projectOrArtifact(":compose:material:material"))
}
}
+
+ // TODO(b/214407011): These dependencies leak into instrumented tests as well. If you
+ // need to add Robolectric (which must be kept out of androidAndroidTest), use a top
+ // level dependencies block instead:
+ // `dependencies { testImplementation(libs.robolectric) }`
+ androidTest {
+ dependsOn(jvmTest)
+ dependencies {
+ implementation(libs.truth)
+ }
+ }
+
+ if (desktopEnabled) {
+ desktopTest {
+ dependsOn(jvmTest)
+ dependsOn(desktopMain)
+ dependencies {
+ }
+ }
+ }
}
}
diff --git a/compose/ui/ui-geometry/build.gradle b/compose/ui/ui-geometry/build.gradle
index 1a05daf..15e6e16 100644
--- a/compose/ui/ui-geometry/build.gradle
+++ b/compose/ui/ui-geometry/build.gradle
@@ -14,9 +14,8 @@
* limitations under the License.
*/
-import androidx.build.AndroidXComposePlugin
+import androidx.build.KmpPlatformsKt
import androidx.build.LibraryType
-import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins {
id("AndroidXPlugin")
@@ -24,53 +23,69 @@
id("AndroidXComposePlugin")
}
-AndroidXComposePlugin.applyAndConfigureKotlinPlugin(project)
+def desktopEnabled = KmpPlatformsKt.enableDesktop(project)
-if(!AndroidXComposePlugin.isMultiplatformEnabled(project)) {
- dependencies {
- /*
- * When updating dependencies, make sure to make the an an analogous update in the
- * corresponding block below
- */
+androidXMultiplatform {
+ android()
+ if (desktopEnabled) desktop()
- api("androidx.annotation:annotation:1.1.0")
-
- implementation("androidx.compose.runtime:runtime:1.2.1")
- implementation(project(":compose:ui:ui-util"))
- implementation(libs.kotlinStdlib)
-
- testImplementation(libs.junit)
- testImplementation(libs.truth)
- testImplementation(libs.kotlinTest)
- }
-}
-
-if(AndroidXComposePlugin.isMultiplatformEnabled(project)) {
- androidXComposeMultiplatform {
- android()
- desktop()
- }
-
- kotlin {
- /*
- * When updating dependencies, make sure to make the an an analogous update in the
- * corresponding block above
- */
- sourceSets {
- commonMain.dependencies {
+ sourceSets {
+ commonMain {
+ dependencies {
implementation(libs.kotlinStdlibCommon)
- implementation(project(":compose:runtime:runtime"))
+ implementation("androidx.compose.runtime:runtime:1.2.1")
implementation(project(":compose:ui:ui-util"))
}
- jvmMain.dependencies {
+ }
+
+ commonTest {
+ dependencies {
+ implementation(kotlin("test-junit"))
+ }
+ }
+
+ jvmMain {
+ dependencies {
implementation(libs.kotlinStdlib)
}
- androidMain.dependencies {
+ }
+
+
+ androidMain {
+ dependsOn(jvmMain)
+ dependencies {
api("androidx.annotation:annotation:1.1.0")
}
- commonTest.dependencies {
- implementation(kotlin("test-junit"))
+ }
+
+ if (desktopEnabled) {
+ desktopMain {
+ dependsOn(jvmMain)
+ dependencies {
+ implementation(project(":compose:runtime:runtime"))
+ }
+ }
+ }
+
+ jvmTest {
+ dependencies {
+ }
+ }
+
+ androidAndroidTest {
+ dependsOn(jvmTest)
+ dependencies {
+ }
+ }
+
+ androidTest {
+ dependsOn(jvmTest)
+ }
+
+ if (desktopEnabled) {
+ desktopTest {
+ dependsOn(jvmTest)
}
}
}
diff --git a/compose/ui/ui-graphics/src/androidAndroidTest/kotlin/androidx/compose/ui/graphics/ShaderTest.kt b/compose/ui/ui-graphics/src/androidAndroidTest/kotlin/androidx/compose/ui/graphics/ShaderTest.kt
index 786ed1f..a1c9071 100644
--- a/compose/ui/ui-graphics/src/androidAndroidTest/kotlin/androidx/compose/ui/graphics/ShaderTest.kt
+++ b/compose/ui/ui-graphics/src/androidAndroidTest/kotlin/androidx/compose/ui/graphics/ShaderTest.kt
@@ -28,6 +28,8 @@
import org.junit.Test
import org.junit.runner.RunWith
import kotlin.math.roundToInt
+import org.junit.Assert.assertNotNull
+import org.junit.Assert.assertNull
@SmallTest
@RunWith(AndroidJUnit4::class)
@@ -197,6 +199,53 @@
)
}
+ @Test
+ fun testInvalidWidthBrush() {
+ // Verify that attempts to create a RadialGradient with a width of 0 do not throw
+ // IllegalArgumentExceptions for an invalid radius
+ val brush = Brush.radialGradient(listOf(Color.Red, Color.Blue))
+ val paint = Paint()
+ brush.applyTo(Size(0f, 10f), paint, 1.0f)
+ }
+
+ @Test
+ fun testInvalidHeightBrush() {
+ val brush = Brush.radialGradient(listOf(Color.Red, Color.Blue))
+ val paint = Paint()
+ // Verify that attempts to create a RadialGradient with a height of 0 do not throw
+ // IllegalArgumentExceptions for an invalid radius
+ brush.applyTo(Size(10f, 0f), paint, 1.0f)
+ }
+
+ @Test
+ fun testValidToInvalidWidthBrush() {
+ // Verify that attempts to create a RadialGradient with a non-zero width/height that
+ // is later attempted to be recreated with a zero width remove the shader from the Paint
+ val brush = Brush.radialGradient(listOf(Color.Red, Color.Blue))
+ val paint = Paint()
+ brush.applyTo(Size(10f, 10f), paint, 1.0f)
+
+ assertNotNull(paint.shader)
+
+ brush.applyTo(Size(0f, 10f), paint, 1.0f)
+ assertNull(paint.shader)
+ }
+
+ @Test
+ fun testValidToInvalidHeightBrush() {
+ // Verify that attempts to create a RadialGradient with a non-zero width/height that
+ // is later attempted to be recreated with a zero height remove the shader from the Paint
+ val brush = Brush.radialGradient(listOf(Color.Red, Color.Blue))
+ val paint = Paint()
+
+ brush.applyTo(Size(10f, 10f), paint, 1.0f)
+
+ assertNotNull(paint.shader)
+
+ brush.applyTo(Size(10f, 0f), paint, 1.0f)
+ assertNull(paint.shader)
+ }
+
private fun ImageBitmap.drawInto(
block: DrawScope.() -> Unit
) = CanvasDrawScope().draw(
diff --git a/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/Brush.kt b/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/Brush.kt
index a18e8db..5f7a1d0 100644
--- a/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/Brush.kt
+++ b/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/Brush.kt
@@ -653,8 +653,14 @@
final override fun applyTo(size: Size, p: Paint, alpha: Float) {
var shader = internalShader
if (shader == null || createdSize != size) {
- shader = createShader(size).also { internalShader = it }
- createdSize = size
+ if (size.isEmpty()) {
+ shader = null
+ internalShader = null
+ createdSize = Size.Unspecified
+ } else {
+ shader = createShader(size).also { internalShader = it }
+ createdSize = size
+ }
}
if (p.color != Color.Black) p.color = Color.Black
if (p.shader != shader) p.shader = shader
diff --git a/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/AndroidParagraphTest.kt b/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/AndroidParagraphTest.kt
index 386b478..1aef72f 100644
--- a/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/AndroidParagraphTest.kt
+++ b/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/AndroidParagraphTest.kt
@@ -1348,7 +1348,7 @@
val paragraph = simpleParagraph(
text = "",
style = TextStyle(brush = brush),
- width = 0.0f
+ width = 1.0f
)
assertThat(paragraph.textPaint.shader).isNotNull()
diff --git a/compose/ui/ui-tooling-preview/build.gradle b/compose/ui/ui-tooling-preview/build.gradle
index a9f9236..d6d3a42 100644
--- a/compose/ui/ui-tooling-preview/build.gradle
+++ b/compose/ui/ui-tooling-preview/build.gradle
@@ -14,10 +14,8 @@
* limitations under the License.
*/
-
-import androidx.build.AndroidXComposePlugin
+import androidx.build.KmpPlatformsKt
import androidx.build.LibraryType
-import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins {
id("AndroidXPlugin")
@@ -25,47 +23,91 @@
id("com.android.library")
}
-AndroidXComposePlugin.applyAndConfigureKotlinPlugin(project)
+def desktopEnabled = KmpPlatformsKt.enableDesktop(project)
-dependencies {
- if(!AndroidXComposePlugin.isMultiplatformEnabled(project)) {
- implementation(libs.kotlinStdlib)
- api("androidx.annotation:annotation:1.2.0")
- api("androidx.compose.runtime:runtime:1.2.1")
- testImplementation(libs.junit)
- }
-}
+androidXMultiplatform {
+ android()
+ if (desktopEnabled) desktop()
-if(AndroidXComposePlugin.isMultiplatformEnabled(project)) {
- androidXComposeMultiplatform {
- android()
- desktop()
- }
- kotlin {
- /*
- * When updating dependencies, make sure to make the an an analogous update in the
- * corresponding block above
- */
- sourceSets {
- commonMain.dependencies {
+ sourceSets {
+ commonMain {
+ dependencies {
implementation(libs.kotlinStdlibCommon)
api(project(":compose:runtime:runtime"))
}
+ }
- androidMain.dependencies {
+ commonTest {
+ dependencies {
+
+ }
+ }
+
+ jvmMain {
+ dependsOn(commonMain)
+ dependencies {
+ }
+ }
+
+ if (desktopEnabled) {
+ skikoMain {
+ dependsOn(commonMain)
+ dependencies {
+ api(project(":compose:runtime:runtime"))
+ }
+ }
+ }
+
+ androidMain {
+ dependsOn(jvmMain)
+ dependencies {
api("androidx.annotation:annotation:1.2.0")
}
+ }
- androidTest.dependencies {
+ if (desktopEnabled) {
+ desktopMain {
+ dependsOn(skikoMain)
+ dependsOn(jvmMain)
+ dependencies {
+
+ }
+ }
+ }
+
+ jvmTest {
+ dependsOn(commonTest)
+ dependencies {
+ }
+ }
+
+ androidAndroidTest {
+ dependsOn(jvmTest)
+ dependencies {
+ }
+ }
+
+ androidTest {
+ dependsOn(jvmTest)
+ dependencies {
implementation(libs.junit)
}
}
+
+ if (desktopEnabled) {
+ desktopTest {
+ dependsOn(jvmTest)
+ dependsOn(desktopMain)
+ dependencies {
+
+ }
+ }
+ }
}
}
-
androidx {
name = "Compose Tooling API"
type = LibraryType.PUBLISHED_LIBRARY
diff --git a/compose/ui/ui-unit/build.gradle b/compose/ui/ui-unit/build.gradle
index bfbfed7..627cd87 100644
--- a/compose/ui/ui-unit/build.gradle
+++ b/compose/ui/ui-unit/build.gradle
@@ -14,9 +14,8 @@
* limitations under the License.
*/
-import androidx.build.AndroidXComposePlugin
+import androidx.build.KmpPlatformsKt
import androidx.build.LibraryType
-import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins {
id("AndroidXPlugin")
@@ -24,85 +23,88 @@
id("AndroidXComposePlugin")
}
-AndroidXComposePlugin.applyAndConfigureKotlinPlugin(project)
+def desktopEnabled = KmpPlatformsKt.enableDesktop(project)
-if(!AndroidXComposePlugin.isMultiplatformEnabled(project)) {
- dependencies {
- /*
- * When updating dependencies, make sure to make the an an analogous update in the
- * corresponding block below
- */
+androidXMultiplatform {
+ android()
+ if (desktopEnabled) desktop()
- api(project(":compose:ui:ui-geometry"))
- api("androidx.annotation:annotation:1.1.0")
-
- implementation(libs.kotlinStdlib)
- implementation("androidx.compose.runtime:runtime:1.2.1")
- implementation(project(":compose:ui:ui-util"))
-
- testImplementation(libs.junit)
- testImplementation(libs.truth)
-
- androidTestImplementation(libs.testRules)
- androidTestImplementation(libs.testRunner)
- androidTestImplementation(libs.testExtJunit)
- androidTestImplementation(libs.espressoCore)
- androidTestImplementation(libs.truth)
- androidTestImplementation(libs.kotlinTest)
-
- samples(projectOrArtifact(":compose:ui:ui-unit:ui-unit-samples"))
- }
-}
-
-if(AndroidXComposePlugin.isMultiplatformEnabled(project)) {
- androidXComposeMultiplatform {
- android()
- desktop()
- }
-
- kotlin {
-
- /*
- * When updating dependencies, make sure to make the an an analogous update in the
- * corresponding block above
- */
- sourceSets {
- commonMain.dependencies {
+ sourceSets {
+ commonMain {
+ dependencies {
implementation(libs.kotlinStdlibCommon)
api(project(":compose:ui:ui-geometry"))
implementation(project(":compose:runtime:runtime"))
implementation(project(":compose:ui:ui-util"))
}
- jvmMain.dependencies {
- implementation(libs.kotlinStdlib)
- }
- androidMain.dependencies {
- api("androidx.annotation:annotation:1.1.0")
- }
+ }
- commonTest.dependencies {
+ commonTest {
+ dependencies {
implementation(kotlin("test-junit"))
}
+ }
- // TODO(b/214407011): These dependencies leak into instrumented tests as well. If you
- // need to add Robolectric (which must be kept out of androidAndroidTest), use a top
- // level dependencies block instead:
- // `dependencies { testImplementation(libs.robolectric) }`
- androidTest.dependencies {
- implementation(libs.truth)
+ jvmMain {
+ dependencies {
+ implementation(libs.kotlinStdlib)
}
- androidAndroidTest.dependencies {
+ }
+
+
+ androidMain {
+ dependsOn(jvmMain)
+ dependencies {
+ api("androidx.annotation:annotation:1.1.0")
+ }
+ }
+
+ if (desktopEnabled) {
+ desktopMain {
+ dependsOn(jvmMain)
+ dependencies {
+ implementation(project(":compose:runtime:runtime"))
+ }
+ }
+ }
+
+ jvmTest {
+ dependencies {
+ }
+ }
+
+ androidAndroidTest {
+ dependsOn(jvmTest)
+ dependencies {
implementation(libs.testRules)
implementation(libs.testRunner)
implementation(libs.testExtJunit)
implementation(libs.espressoCore)
}
}
+
+ // TODO(b/214407011): These dependencies leak into instrumented tests as well. If you
+ // need to add Robolectric (which must be kept out of androidAndroidTest), use a top
+ // level dependencies block instead:
+ // `dependencies { testImplementation(libs.robolectric) }`
+ androidTest {
+ dependsOn(jvmTest)
+ dependencies {
+ implementation(libs.truth)
+ }
+ }
+
+ if (desktopEnabled) {
+ desktopTest {
+ dependsOn(jvmTest)
+ }
+ }
}
- dependencies {
- samples(projectOrArtifact(":compose:ui:ui-unit:ui-unit-samples"))
- }
+}
+
+dependencies {
+ samples(projectOrArtifact(":compose:ui:ui-unit:ui-unit-samples"))
}
androidx {
diff --git a/compose/ui/ui-util/build.gradle b/compose/ui/ui-util/build.gradle
index 8eeb75e..4c19723 100644
--- a/compose/ui/ui-util/build.gradle
+++ b/compose/ui/ui-util/build.gradle
@@ -14,9 +14,8 @@
* limitations under the License.
*/
-import androidx.build.AndroidXComposePlugin
+import androidx.build.KmpPlatformsKt
import androidx.build.LibraryType
-import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins {
id("AndroidXPlugin")
@@ -24,59 +23,72 @@
id("AndroidXComposePlugin")
}
-AndroidXComposePlugin.applyAndConfigureKotlinPlugin(project)
+def desktopEnabled = KmpPlatformsKt.enableDesktop(project)
-if(!AndroidXComposePlugin.isMultiplatformEnabled(project)) {
- dependencies {
- /*
- * When updating dependencies, make sure to make the an an analogous update in the
- * corresponding block below
- */
+androidXMultiplatform {
+ android()
+ if (desktopEnabled) desktop()
- implementation(libs.kotlinStdlib)
-
- testImplementation(libs.junit)
- testImplementation(libs.truth)
- testImplementation(libs.kotlinTest)
- }
-}
-
-if(AndroidXComposePlugin.isMultiplatformEnabled(project)) {
- androidXComposeMultiplatform {
- android()
- desktop()
- }
-
- kotlin {
- /*
- * When updating dependencies, make sure to make the an an analogous update in the
- * corresponding block above
- */
- sourceSets {
- commonMain.dependencies {
+ sourceSets {
+ commonMain {
+ dependencies {
implementation(libs.kotlinStdlibCommon)
}
+ }
- jvmMain.dependencies {
- implementation(libs.kotlinStdlib)
- }
-
- androidMain.dependencies {
- implementation(libs.kotlinStdlib)
- }
-
- commonTest.dependencies {
+ commonTest {
+ dependencies {
implementation(kotlin("test-junit"))
}
+ }
- // TODO(b/214407011): These dependencies leak into instrumented tests as well. If you
- // need to add Robolectric (which must be kept out of androidAndroidTest), use a top
- // level dependencies block instead:
- // `dependencies { testImplementation(libs.robolectric) }`
- androidTest.dependencies {
+ jvmMain {
+ dependencies {
+ implementation(libs.kotlinStdlib)
+ }
+ }
+
+
+ androidMain {
+ dependsOn(jvmMain)
+ dependencies {
+ implementation(libs.kotlinStdlib)
+ }
+ }
+
+ if (desktopEnabled) {
+ desktopMain {
+ dependsOn(jvmMain)
+ }
+ }
+
+ jvmTest {
+ dependencies {
+ }
+ }
+
+ androidAndroidTest {
+ dependsOn(jvmTest)
+ dependencies {
+ }
+ }
+
+ // TODO(b/214407011): These dependencies leak into instrumented tests as well. If you
+ // need to add Robolectric (which must be kept out of androidAndroidTest), use a top
+ // level dependencies block instead:
+ // `dependencies { testImplementation(libs.robolectric) }`
+ androidTest {
+ dependsOn(jvmTest)
+ dependencies {
implementation(libs.truth)
}
}
+
+ if (desktopEnabled) {
+ desktopTest {
+ dependsOn(jvmTest)
+ }
+ }
}
}
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/pointer/PointerInteropFilterTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/pointer/PointerInteropFilterTest.kt
index 32fd60c..f761c28 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/pointer/PointerInteropFilterTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/pointer/PointerInteropFilterTest.kt
@@ -4405,38 +4405,3 @@
)
internal typealias PointerEventHandler = (PointerEvent, PointerEventPass, IntSize) -> Unit
-
-private fun PointerEventHandler.invokeOverAllPasses(
- pointerEvent: PointerEvent,
- size: IntSize = IntSize(Int.MAX_VALUE, Int.MAX_VALUE)
-) {
- invokeOverPasses(
- pointerEvent,
- listOf(
- PointerEventPass.Initial,
- PointerEventPass.Main,
- PointerEventPass.Final
- ),
- size = size
- )
-}
-
-private fun PointerEventHandler.invokeOverPasses(
- pointerEvent: PointerEvent,
- vararg pointerEventPasses: PointerEventPass,
- size: IntSize = IntSize(Int.MAX_VALUE, Int.MAX_VALUE)
-) {
- invokeOverPasses(pointerEvent, pointerEventPasses.toList(), size)
-}
-
-private fun PointerEventHandler.invokeOverPasses(
- pointerEvent: PointerEvent,
- pointerEventPasses: List<PointerEventPass>,
- size: IntSize = IntSize(Int.MAX_VALUE, Int.MAX_VALUE)
-) {
- require(pointerEvent.changes.isNotEmpty())
- require(pointerEventPasses.isNotEmpty())
- pointerEventPasses.forEach {
- this.invoke(pointerEvent, it, size)
- }
-}
\ No newline at end of file
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeView.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeView.android.kt
index 497be6e..da63070 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeView.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeView.android.kt
@@ -911,13 +911,20 @@
}
}
- private fun convertMeasureSpec(measureSpec: Int): Pair<Int, Int> {
+ @Suppress("NOTHING_TO_INLINE")
+ private inline operator fun ULong.component1() = (this shr 32).toInt()
+ @Suppress("NOTHING_TO_INLINE")
+ private inline operator fun ULong.component2() = (this and 0xFFFFFFFFUL).toInt()
+
+ private fun pack(a: Int, b: Int) = (a.toULong() shl 32 or b.toULong())
+
+ private fun convertMeasureSpec(measureSpec: Int): ULong {
val mode = MeasureSpec.getMode(measureSpec)
val size = MeasureSpec.getSize(measureSpec)
return when (mode) {
- MeasureSpec.EXACTLY -> size to size
- MeasureSpec.UNSPECIFIED -> 0 to Constraints.Infinity
- MeasureSpec.AT_MOST -> 0 to size
+ MeasureSpec.EXACTLY -> pack(size, size)
+ MeasureSpec.UNSPECIFIED -> pack(0, Constraints.Infinity)
+ MeasureSpec.AT_MOST -> pack(0, size)
else -> throw IllegalStateException()
}
}
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/HitPathTracker.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/HitPathTracker.kt
index 9a7ddac..abd9b6a 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/HitPathTracker.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/HitPathTracker.kt
@@ -376,12 +376,21 @@
@OptIn(ExperimentalComposeUiApi::class)
for ((key, change) in changes) {
- // Filter for changes that are associated with pointer ids that are relevant to this
- // node
- if (key in pointerIds) {
+ val keyValue = key.value
+
+ // Using for (key in pointerIds) causes key to be boxed and create allocations
+ var keyInPointerIds = false
+ for (i in 0..pointerIds.lastIndex) {
+ if (pointerIds[i].value == keyValue) {
+ keyInPointerIds = true
+ break
+ }
+ }
+
+ if (keyInPointerIds) {
// And translate their position relative to the parent coordinates, to give us a
// change local to the PointerInputFilter's coordinates
- val historical = mutableListOf<HistoricalChange>()
+ val historical = ArrayList<HistoricalChange>(change.historical.size)
change.historical.fastForEach {
historical.add(
HistoricalChange(
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNode.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNode.kt
index c9f3e51..aa215e9 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNode.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNode.kt
@@ -66,6 +66,8 @@
*/
private const val DebugChanges = false
+private val DefaultDensity = Density(1f)
+
/**
* An element in the layout hierarchy, built with compose UI.
*/
@@ -627,7 +629,7 @@
/**
* The screen density to be used by this layout.
*/
- override var density: Density = Density(1f)
+ override var density: Density = DefaultDensity
set(value) {
if (field != value) {
field = value
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/NodeChain.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/NodeChain.kt
index 2b2847f..9043a07 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/NodeChain.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/NodeChain.kt
@@ -743,7 +743,8 @@
private fun Modifier.fillVector(
result: MutableVector<Modifier.Element>
): MutableVector<Modifier.Element> {
- val stack = MutableVector<Modifier>(result.size).also { it.add(this) }
+ val capacity = result.size.coerceAtLeast(16)
+ val stack = MutableVector<Modifier>(capacity).also { it.add(this) }
while (stack.isNotEmpty()) {
when (val next = stack.removeAt(stack.size - 1)) {
is CombinedModifier -> {
diff --git a/constraintlayout/constraintlayout-compose/build.gradle b/constraintlayout/constraintlayout-compose/build.gradle
index 639aa60..51a3271 100644
--- a/constraintlayout/constraintlayout-compose/build.gradle
+++ b/constraintlayout/constraintlayout-compose/build.gradle
@@ -14,9 +14,7 @@
* limitations under the License.
*/
-import androidx.build.AndroidXComposePlugin
import androidx.build.LibraryType
-import androidx.build.Publish
plugins {
id("AndroidXPlugin")
@@ -24,77 +22,49 @@
id("AndroidXComposePlugin")
}
-AndroidXComposePlugin.applyAndConfigureKotlinPlugin(project)
+androidXMultiplatform {
+ android()
-dependencies {
- if(!AndroidXComposePlugin.isMultiplatformEnabled(project)) {
- implementation(project(":compose:ui:ui"))
- implementation(project(":compose:ui:ui-unit"))
- implementation(project(":compose:ui:ui-util"))
- implementation(project(":compose:foundation:foundation"))
- implementation(project(":compose:foundation:foundation-layout"))
-
- implementation(project(":constraintlayout:constraintlayout-core"))
-
- androidTestImplementation(project(":compose:material:material"))
- androidTestImplementation(project(":compose:ui:ui-test"))
- androidTestImplementation(project(":compose:ui:ui-test-junit4"))
- androidTestImplementation(project(":compose:ui:ui-test-manifest"))
- androidTestImplementation(project(":activity:activity"))
-
- androidTestImplementation(libs.kotlinTest)
- androidTestImplementation(libs.testRules)
- androidTestImplementation(libs.testRunner)
- androidTestImplementation(libs.junit)
-
- lintPublish(project(":constraintlayout:constraintlayout-compose-lint"))
- }
-}
-
-if(AndroidXComposePlugin.isMultiplatformEnabled(project)) {
- androidXComposeMultiplatform {
- android()
- desktop()
- }
-
- kotlin {
- /*
- * When updating dependencies, make sure to make the an an analogous update in the
- * corresponding block above
- */
- sourceSets {
- commonMain.dependencies {
-// implementation(libs.kotlinStdlibCommon)
-
+ sourceSets {
+ commonMain {
+ dependencies {
implementation(project(":compose:ui:ui"))
- implementation("androidx.compose.ui:ui-unit:1.4.0-beta02")
- implementation("androidx.compose.ui:ui-util:1.4.0-beta02")
- implementation("androidx.compose.foundation:foundation:1.4.0-beta02")
- implementation("androidx.compose.foundation:foundation-layout:1.4.0-beta02")
+ implementation(project(":compose:ui:ui-unit"))
+ implementation(project(":compose:ui:ui-util"))
+ implementation(project(":compose:foundation:foundation"))
+ implementation(project(":compose:foundation:foundation-layout"))
implementation(project(":constraintlayout:constraintlayout-core"))
-
}
+ }
- androidMain.dependencies {
+ commonTest {
+ dependencies {
+ }
+ }
+
+ jvmMain {
+ dependencies {
+ }
+ }
+
+
+ androidMain {
+ dependsOn(commonMain)
+ dependsOn(jvmMain)
+ dependencies {
api("androidx.annotation:annotation:1.1.0")
implementation("androidx.core:core-ktx:1.5.0")
}
+ }
- desktopMain.dependencies {
- implementation(libs.kotlinStdlib)
+ jvmTest {
+ dependencies {
}
+ }
- // TODO(b/214407011): These dependencies leak into instrumented tests as well. If you
- // need to add Robolectric (which must be kept out of androidAndroidTest), use a top
- // level dependencies block instead:
- // `dependencies { testImplementation(libs.robolectric) }`
- androidTest.dependencies {
- implementation(libs.testRules)
- implementation(libs.testRunner)
- implementation(libs.junit)
- }
-
- androidAndroidTest.dependencies {
+ androidAndroidTest {
+ dependsOn(jvmTest)
+ dependencies {
implementation(libs.kotlinTest)
implementation(libs.testRules)
implementation(libs.testRunner)
@@ -107,9 +77,27 @@
implementation(project(":compose:test-utils"))
}
}
+
+
+ // TODO(b/214407011): These dependencies leak into instrumented tests as well. If you
+ // need to add Robolectric (which must be kept out of androidAndroidTest), use a top
+ // level dependencies block instead:
+ // `dependencies { testImplementation(libs.robolectric) }`
+ androidTest {
+ dependsOn(jvmTest)
+ dependencies {
+ implementation(libs.testRules)
+ implementation(libs.testRunner)
+ implementation(libs.junit)
+ }
+ }
}
}
+dependencies {
+ lintPublish(project(":constraintlayout:constraintlayout-compose-lint"))
+}
+
androidx {
name = "Android ConstraintLayout Compose Library"
type = LibraryType.PUBLISHED_LIBRARY
diff --git a/javascriptengine/javascriptengine/src/main/java/androidx/javascriptengine/JavaScriptIsolate.java b/javascriptengine/javascriptengine/src/main/java/androidx/javascriptengine/JavaScriptIsolate.java
index 00afeb8..a4432f0 100644
--- a/javascriptengine/javascriptengine/src/main/java/androidx/javascriptengine/JavaScriptIsolate.java
+++ b/javascriptengine/javascriptengine/src/main/java/androidx/javascriptengine/JavaScriptIsolate.java
@@ -162,13 +162,23 @@
@Override
public void reportResult(String result) {
Objects.requireNonNull(result);
- handleEvaluationResult(mCompleter, result);
+ final long identityToken = Binder.clearCallingIdentity();
+ try {
+ handleEvaluationResult(mCompleter, result);
+ } finally {
+ Binder.restoreCallingIdentity(identityToken);
+ }
}
@Override
public void reportError(@ExecutionErrorTypes int type, String error) {
Objects.requireNonNull(error);
- handleEvaluationError(mCompleter, type, error);
+ final long identityToken = Binder.clearCallingIdentity();
+ try {
+ handleEvaluationError(mCompleter, type, error);
+ } finally {
+ Binder.restoreCallingIdentity(identityToken);
+ }
}
}
diff --git a/navigation/integration-tests/testapp/src/main/java/androidx/navigation/testapp/MainFragment.kt b/navigation/integration-tests/testapp/src/main/java/androidx/navigation/testapp/MainFragment.kt
index 083d17d..e820bfe 100644
--- a/navigation/integration-tests/testapp/src/main/java/androidx/navigation/testapp/MainFragment.kt
+++ b/navigation/integration-tests/testapp/src/main/java/androidx/navigation/testapp/MainFragment.kt
@@ -18,6 +18,7 @@
import android.graphics.Color
import android.os.Bundle
+import android.view.Gravity
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
@@ -27,6 +28,7 @@
import androidx.fragment.app.Fragment
import androidx.navigation.fragment.FragmentNavigatorExtras
import androidx.navigation.fragment.findNavController
+import androidx.transition.Slide
/**
* Fragment used to show how to navigate to another destination
@@ -38,6 +40,8 @@
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
+ enterTransition = Slide(Gravity.RIGHT)
+ exitTransition = Slide(Gravity.LEFT)
return inflater.inflate(R.layout.main_fragment, container, false)
}
diff --git a/navigation/integration-tests/testapp/src/main/res/navigation/nav_main.xml b/navigation/integration-tests/testapp/src/main/res/navigation/nav_main.xml
index cea98bd..4db4d1d 100644
--- a/navigation/integration-tests/testapp/src/main/res/navigation/nav_main.xml
+++ b/navigation/integration-tests/testapp/src/main/res/navigation/nav_main.xml
@@ -23,31 +23,19 @@
android:name=".MainFragment"
android:label="@string/home">
<argument android:name="myarg" android:defaultValue="Home" />
- <action android:id="@+id/next" app:destination="@+id/first_screen"
- app:enterAnim="@anim/nav_default_enter_anim"
- app:exitAnim="@anim/nav_default_exit_anim"
- app:popEnterAnim="@anim/nav_default_pop_enter_anim"
- app:popExitAnim="@anim/nav_default_pop_exit_anim"/>
+ <action android:id="@+id/next" app:destination="@+id/first_screen"/>
</fragment>
<fragment android:id="@+id/first_screen"
android:name="androidx.navigation.testapp.MainFragment"
android:label="@string/first">
<argument android:name="myarg" android:defaultValue="one" />
- <action android:id="@+id/next" app:destination="@+id/next_fragment"
- app:enterAnim="@anim/nav_default_enter_anim"
- app:exitAnim="@anim/nav_default_exit_anim"
- app:popEnterAnim="@anim/nav_default_pop_enter_anim"
- app:popExitAnim="@anim/nav_default_pop_exit_anim"/>
+ <action android:id="@+id/next" app:destination="@+id/next_fragment"/>
</fragment>
<fragment android:id="@+id/next_fragment"
android:name="androidx.navigation.testapp.MainFragment"
android:label="@string/second">
<argument android:name="myarg" android:defaultValue="two" />
- <action android:id="@+id/next" app:destination="@+id/first_screen"
- app:enterAnim="@anim/nav_default_enter_anim"
- app:exitAnim="@anim/nav_default_exit_anim"
- app:popEnterAnim="@anim/nav_default_pop_enter_anim"
- app:popExitAnim="@anim/nav_default_pop_exit_anim"/>
+ <action android:id="@+id/next" app:destination="@+id/first_screen"/>
</fragment>
</navigation>
<dialog
diff --git a/navigation/integration-tests/testapp/src/main/res/navigation/two_pane_navigation.xml b/navigation/integration-tests/testapp/src/main/res/navigation/two_pane_navigation.xml
index f7eb4da..63261cbf 100644
--- a/navigation/integration-tests/testapp/src/main/res/navigation/two_pane_navigation.xml
+++ b/navigation/integration-tests/testapp/src/main/res/navigation/two_pane_navigation.xml
@@ -20,51 +20,31 @@
android:name="androidx.navigation.testapp.MainFragment"
android:label="@string/first">
<argument android:name="myarg" android:defaultValue="one" />
- <action android:id="@+id/next" app:destination="@+id/second_fragment"
- app:enterAnim="@anim/nav_default_enter_anim"
- app:exitAnim="@anim/nav_default_exit_anim"
- app:popEnterAnim="@anim/nav_default_pop_enter_anim"
- app:popExitAnim="@anim/nav_default_pop_exit_anim"/>
+ <action android:id="@+id/next" app:destination="@+id/second_fragment"/>
</fragment>
<fragment android:id="@+id/second_fragment"
android:name="androidx.navigation.testapp.MainFragment"
android:label="@string/second">
<argument android:name="myarg" android:defaultValue="two" />
- <action android:id="@+id/next" app:destination="@+id/third_fragment"
- app:enterAnim="@anim/nav_default_enter_anim"
- app:exitAnim="@anim/nav_default_exit_anim"
- app:popEnterAnim="@anim/nav_default_pop_enter_anim"
- app:popExitAnim="@anim/nav_default_pop_exit_anim"/>
+ <action android:id="@+id/next" app:destination="@+id/third_fragment"/>
</fragment>
<fragment android:id="@+id/third_fragment"
android:name="androidx.navigation.testapp.MainFragment"
android:label="@string/third">
<argument android:name="myarg" android:defaultValue="three" />
- <action android:id="@+id/next" app:destination="@+id/fourth_fragment"
- app:enterAnim="@anim/nav_default_enter_anim"
- app:exitAnim="@anim/nav_default_exit_anim"
- app:popEnterAnim="@anim/nav_default_pop_enter_anim"
- app:popExitAnim="@anim/nav_default_pop_exit_anim"/>
+ <action android:id="@+id/next" app:destination="@+id/fourth_fragment"/>
</fragment>
<fragment android:id="@+id/fourth_fragment"
android:name="androidx.navigation.testapp.MainFragment"
android:label="@string/fourth">
<argument android:name="myarg" android:defaultValue="four" />
- <action android:id="@+id/next" app:destination="@+id/fifth_fragment"
- app:enterAnim="@anim/nav_default_enter_anim"
- app:exitAnim="@anim/nav_default_exit_anim"
- app:popEnterAnim="@anim/nav_default_pop_enter_anim"
- app:popExitAnim="@anim/nav_default_pop_exit_anim"/>
+ <action android:id="@+id/next" app:destination="@+id/fifth_fragment"/>
</fragment>
<fragment android:id="@+id/fifth_fragment"
android:name="androidx.navigation.testapp.MainFragment"
android:label="@string/fifth">
<argument android:name="myarg" android:defaultValue="five" />
- <action android:id="@+id/next" app:destination="@+id/first_fragment"
- app:enterAnim="@anim/nav_default_enter_anim"
- app:exitAnim="@anim/nav_default_exit_anim"
- app:popEnterAnim="@anim/nav_default_pop_enter_anim"
- app:popExitAnim="@anim/nav_default_pop_exit_anim"/>
+ <action android:id="@+id/next" app:destination="@+id/first_fragment"/>
</fragment>
<dialog
android:id="@+id/learn_more"
diff --git a/paging/paging-common/api/current.txt b/paging/paging-common/api/current.txt
index 10ec13b..2070710 100644
--- a/paging/paging-common/api/current.txt
+++ b/paging/paging-common/api/current.txt
@@ -46,7 +46,7 @@
method @AnyThread public void onInvalidated();
}
- public final class InvalidatingPagingSourceFactory<Key, Value> implements kotlin.jvm.functions.Function0<androidx.paging.PagingSource<Key,Value>> {
+ public final class InvalidatingPagingSourceFactory<Key, Value> implements androidx.paging.PagingSourceFactory<Key,Value> {
ctor public InvalidatingPagingSourceFactory(kotlin.jvm.functions.Function0<? extends androidx.paging.PagingSource<Key,Value>> pagingSourceFactory);
method public void invalidate();
method public androidx.paging.PagingSource<Key,Value> invoke();
@@ -411,6 +411,10 @@
public static final class PagingSource.LoadResult.Page.Companion {
}
+ public fun interface PagingSourceFactory<Key, Value> extends kotlin.jvm.functions.Function0<androidx.paging.PagingSource<Key,Value>> {
+ method public operator androidx.paging.PagingSource<Key,Value> invoke();
+ }
+
public final class PagingState<Key, Value> {
ctor public PagingState(java.util.List<androidx.paging.PagingSource.LoadResult.Page<Key,Value>> pages, Integer? anchorPosition, androidx.paging.PagingConfig config, @IntRange(from=0L) int leadingPlaceholderCount);
method public Value? closestItemToPosition(int anchorPosition);
diff --git a/paging/paging-common/api/public_plus_experimental_current.txt b/paging/paging-common/api/public_plus_experimental_current.txt
index 07e6471..a5d3d01 100644
--- a/paging/paging-common/api/public_plus_experimental_current.txt
+++ b/paging/paging-common/api/public_plus_experimental_current.txt
@@ -49,7 +49,7 @@
@kotlin.RequiresOptIn @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) public @interface ExperimentalPagingApi {
}
- public final class InvalidatingPagingSourceFactory<Key, Value> implements kotlin.jvm.functions.Function0<androidx.paging.PagingSource<Key,Value>> {
+ public final class InvalidatingPagingSourceFactory<Key, Value> implements androidx.paging.PagingSourceFactory<Key,Value> {
ctor public InvalidatingPagingSourceFactory(kotlin.jvm.functions.Function0<? extends androidx.paging.PagingSource<Key,Value>> pagingSourceFactory);
method public void invalidate();
method public androidx.paging.PagingSource<Key,Value> invoke();
@@ -415,6 +415,10 @@
public static final class PagingSource.LoadResult.Page.Companion {
}
+ public fun interface PagingSourceFactory<Key, Value> extends kotlin.jvm.functions.Function0<androidx.paging.PagingSource<Key,Value>> {
+ method public operator androidx.paging.PagingSource<Key,Value> invoke();
+ }
+
public final class PagingState<Key, Value> {
ctor public PagingState(java.util.List<androidx.paging.PagingSource.LoadResult.Page<Key,Value>> pages, Integer? anchorPosition, androidx.paging.PagingConfig config, @IntRange(from=0L) int leadingPlaceholderCount);
method public Value? closestItemToPosition(int anchorPosition);
diff --git a/paging/paging-common/api/restricted_current.txt b/paging/paging-common/api/restricted_current.txt
index 10ec13b..2070710 100644
--- a/paging/paging-common/api/restricted_current.txt
+++ b/paging/paging-common/api/restricted_current.txt
@@ -46,7 +46,7 @@
method @AnyThread public void onInvalidated();
}
- public final class InvalidatingPagingSourceFactory<Key, Value> implements kotlin.jvm.functions.Function0<androidx.paging.PagingSource<Key,Value>> {
+ public final class InvalidatingPagingSourceFactory<Key, Value> implements androidx.paging.PagingSourceFactory<Key,Value> {
ctor public InvalidatingPagingSourceFactory(kotlin.jvm.functions.Function0<? extends androidx.paging.PagingSource<Key,Value>> pagingSourceFactory);
method public void invalidate();
method public androidx.paging.PagingSource<Key,Value> invoke();
@@ -411,6 +411,10 @@
public static final class PagingSource.LoadResult.Page.Companion {
}
+ public fun interface PagingSourceFactory<Key, Value> extends kotlin.jvm.functions.Function0<androidx.paging.PagingSource<Key,Value>> {
+ method public operator androidx.paging.PagingSource<Key,Value> invoke();
+ }
+
public final class PagingState<Key, Value> {
ctor public PagingState(java.util.List<androidx.paging.PagingSource.LoadResult.Page<Key,Value>> pages, Integer? anchorPosition, androidx.paging.PagingConfig config, @IntRange(from=0L) int leadingPlaceholderCount);
method public Value? closestItemToPosition(int anchorPosition);
diff --git a/paging/paging-common/src/main/kotlin/androidx/paging/InvalidatingPagingSourceFactory.kt b/paging/paging-common/src/main/kotlin/androidx/paging/InvalidatingPagingSourceFactory.kt
index 93ddb2d..3f74d54 100644
--- a/paging/paging-common/src/main/kotlin/androidx/paging/InvalidatingPagingSourceFactory.kt
+++ b/paging/paging-common/src/main/kotlin/androidx/paging/InvalidatingPagingSourceFactory.kt
@@ -32,7 +32,7 @@
*/
public class InvalidatingPagingSourceFactory<Key : Any, Value : Any>(
private val pagingSourceFactory: () -> PagingSource<Key, Value>
-) : () -> PagingSource<Key, Value> {
+) : PagingSourceFactory<Key, Value> {
@VisibleForTesting
internal val pagingSources = CopyOnWriteArrayList<PagingSource<Key, Value>>()
diff --git a/paging/paging-common/src/main/kotlin/androidx/paging/PagingSourceFactory.kt b/paging/paging-common/src/main/kotlin/androidx/paging/PagingSourceFactory.kt
new file mode 100644
index 0000000..4ed3c7f
--- /dev/null
+++ b/paging/paging-common/src/main/kotlin/androidx/paging/PagingSourceFactory.kt
@@ -0,0 +1,32 @@
+/*
+ * 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.paging
+
+/**
+ * Interface for a factory that generates [PagingSource].
+ *
+ * The factory extending this interface can be used to instantiate a [Pager] as the
+ * pagingSourceFactory.
+ */
+public fun interface PagingSourceFactory<Key : Any, Value : Any> : () -> PagingSource<Key, Value> {
+ /**
+ * Returns a new PagingSource instance.
+ *
+ * This function can be invoked by calling pagingSourceFactory() or pagingSourceFactory::invoke.
+ */
+ public override operator fun invoke(): PagingSource<Key, Value>
+}
\ No newline at end of file
diff --git a/paging/paging-testing/api/current.txt b/paging/paging-testing/api/current.txt
index 066939a..f10ecc1 100644
--- a/paging/paging-testing/api/current.txt
+++ b/paging/paging-testing/api/current.txt
@@ -26,7 +26,7 @@
}
public final class StaticListPagingSourceFactoryKt {
- method public static <Value> kotlin.jvm.functions.Function0<androidx.paging.PagingSource<java.lang.Integer,Value>> asPagingSourceFactory(kotlinx.coroutines.flow.Flow<java.util.List<Value>>, kotlinx.coroutines.CoroutineScope coroutineScope);
+ method public static <Value> androidx.paging.PagingSourceFactory<java.lang.Integer,Value> asPagingSourceFactory(kotlinx.coroutines.flow.Flow<java.util.List<Value>>, kotlinx.coroutines.CoroutineScope coroutineScope);
}
public final class TestPager<Key, Value> {
diff --git a/paging/paging-testing/api/public_plus_experimental_current.txt b/paging/paging-testing/api/public_plus_experimental_current.txt
index 066939a..f10ecc1 100644
--- a/paging/paging-testing/api/public_plus_experimental_current.txt
+++ b/paging/paging-testing/api/public_plus_experimental_current.txt
@@ -26,7 +26,7 @@
}
public final class StaticListPagingSourceFactoryKt {
- method public static <Value> kotlin.jvm.functions.Function0<androidx.paging.PagingSource<java.lang.Integer,Value>> asPagingSourceFactory(kotlinx.coroutines.flow.Flow<java.util.List<Value>>, kotlinx.coroutines.CoroutineScope coroutineScope);
+ method public static <Value> androidx.paging.PagingSourceFactory<java.lang.Integer,Value> asPagingSourceFactory(kotlinx.coroutines.flow.Flow<java.util.List<Value>>, kotlinx.coroutines.CoroutineScope coroutineScope);
}
public final class TestPager<Key, Value> {
diff --git a/paging/paging-testing/api/restricted_current.txt b/paging/paging-testing/api/restricted_current.txt
index 066939a..f10ecc1 100644
--- a/paging/paging-testing/api/restricted_current.txt
+++ b/paging/paging-testing/api/restricted_current.txt
@@ -26,7 +26,7 @@
}
public final class StaticListPagingSourceFactoryKt {
- method public static <Value> kotlin.jvm.functions.Function0<androidx.paging.PagingSource<java.lang.Integer,Value>> asPagingSourceFactory(kotlinx.coroutines.flow.Flow<java.util.List<Value>>, kotlinx.coroutines.CoroutineScope coroutineScope);
+ method public static <Value> androidx.paging.PagingSourceFactory<java.lang.Integer,Value> asPagingSourceFactory(kotlinx.coroutines.flow.Flow<java.util.List<Value>>, kotlinx.coroutines.CoroutineScope coroutineScope);
}
public final class TestPager<Key, Value> {
diff --git a/paging/paging-testing/src/main/java/androidx/paging/testing/StaticListPagingSourceFactory.kt b/paging/paging-testing/src/main/java/androidx/paging/testing/StaticListPagingSourceFactory.kt
index e44216e..3cf9165 100644
--- a/paging/paging-testing/src/main/java/androidx/paging/testing/StaticListPagingSourceFactory.kt
+++ b/paging/paging-testing/src/main/java/androidx/paging/testing/StaticListPagingSourceFactory.kt
@@ -19,6 +19,7 @@
import androidx.paging.InvalidatingPagingSourceFactory
import androidx.paging.PagingSource
import androidx.paging.Pager
+import androidx.paging.PagingSourceFactory
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.launch
@@ -41,7 +42,7 @@
*/
public fun <Value : Any> Flow<@JvmSuppressWildcards List<Value>>.asPagingSourceFactory(
coroutineScope: CoroutineScope
-): () -> PagingSource<Int, Value> {
+): PagingSourceFactory<Int, Value> {
var data: List<Value>? = null
diff --git a/paging/paging-testing/src/test/kotlin/androidx/paging/testing/PagerFlowSnapshotTest.kt b/paging/paging-testing/src/test/kotlin/androidx/paging/testing/PagerFlowSnapshotTest.kt
index 42aaa0b5..312d95b 100644
--- a/paging/paging-testing/src/test/kotlin/androidx/paging/testing/PagerFlowSnapshotTest.kt
+++ b/paging/paging-testing/src/test/kotlin/androidx/paging/testing/PagerFlowSnapshotTest.kt
@@ -20,6 +20,7 @@
import androidx.paging.PagingConfig
import androidx.paging.PagingSource
import androidx.paging.PagingSource.LoadParams
+import androidx.paging.PagingSourceFactory
import androidx.paging.PagingState
import androidx.paging.cachedIn
import androidx.paging.insertSeparators
@@ -2355,9 +2356,9 @@
}
private class WrappedPagingSourceFactory(
- private val factory: () -> PagingSource<Int, Int>,
+ private val factory: PagingSourceFactory<Int, Int>,
private val loadDelay: Long,
-) : () -> PagingSource<Int, Int> {
+) : PagingSourceFactory<Int, Int> {
override fun invoke(): PagingSource<Int, Int> = TestPagingSource(factory(), loadDelay)
}
diff --git a/paging/paging-testing/src/test/kotlin/androidx/paging/testing/StaticListPagingSourceFactoryTest.kt b/paging/paging-testing/src/test/kotlin/androidx/paging/testing/StaticListPagingSourceFactoryTest.kt
index 6a2f2c1..eb28936 100644
--- a/paging/paging-testing/src/test/kotlin/androidx/paging/testing/StaticListPagingSourceFactoryTest.kt
+++ b/paging/paging-testing/src/test/kotlin/androidx/paging/testing/StaticListPagingSourceFactoryTest.kt
@@ -17,8 +17,8 @@
package androidx.paging.testing
import androidx.paging.PagingConfig
-import androidx.paging.PagingSource
import androidx.paging.PagingSource.LoadResult.Page
+import androidx.paging.PagingSourceFactory
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.cancel
@@ -47,7 +47,7 @@
@Test
fun emptyFlow() {
- val factory: () -> PagingSource<Int, Int> =
+ val factory: PagingSourceFactory<Int, Int> =
flowOf<List<Int>>().asPagingSourceFactory(testScope)
val pagingSource = factory()
val pager = TestPager(pagingSource, CONFIG)
@@ -64,7 +64,7 @@
List(20) { it }
)
- val factory: () -> PagingSource<Int, Int> =
+ val factory: PagingSourceFactory<Int, Int> =
flow.asPagingSourceFactory(testScope)
val pagingSource = factory()
val pager = TestPager(pagingSource, CONFIG)
@@ -85,7 +85,7 @@
emit(List(15) { it + 30 }) // second gen
}
- val factory: () -> PagingSource<Int, Int> =
+ val factory: PagingSourceFactory<Int, Int> =
flow.asPagingSourceFactory(testScope)
advanceTimeBy(1000)
@@ -117,7 +117,7 @@
val mutableFlow = MutableSharedFlow<List<Int>>()
val collectionScope = this.backgroundScope
- val factory: () -> PagingSource<Int, Int> =
+ val factory: PagingSourceFactory<Int, Int> =
mutableFlow.asPagingSourceFactory(collectionScope)
mutableFlow.emit(List(10) { it })
@@ -146,10 +146,10 @@
fun multipleFactories_fromSameFlow() = testScope.runTest {
val mutableFlow = MutableSharedFlow<List<Int>>()
- val factory1: () -> PagingSource<Int, Int> =
+ val factory1: PagingSourceFactory<Int, Int> =
mutableFlow.asPagingSourceFactory(testScope.backgroundScope)
- val factory2: () -> PagingSource<Int, Int> =
+ val factory2: PagingSourceFactory<Int, Int> =
mutableFlow.asPagingSourceFactory(testScope.backgroundScope)
mutableFlow.emit(List(10) { it })
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KSTypeExt.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KSTypeExt.kt
index 470c9af..213eeb9 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KSTypeExt.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KSTypeExt.kt
@@ -22,10 +22,10 @@
import com.google.devtools.ksp.symbol.KSDeclaration
import com.google.devtools.ksp.symbol.KSNode
import com.google.devtools.ksp.symbol.KSType
+import com.google.devtools.ksp.symbol.KSTypeAlias
import com.google.devtools.ksp.symbol.KSTypeArgument
import com.google.devtools.ksp.symbol.KSTypeParameter
import com.google.devtools.ksp.symbol.KSTypeReference
-import com.google.devtools.ksp.symbol.Modifier
/**
* Root package comes as <root> instead of "" so we work around it here.
@@ -47,10 +47,12 @@
}
internal fun KSTypeReference.isTypeParameterReference(): Boolean {
- return this.resolve().declaration is KSTypeParameter
+ return this.resolve().isTypeParameter()
}
-fun KSType.isInline() = declaration.modifiers.contains(Modifier.INLINE)
+internal fun KSType.isTypeParameter(): Boolean {
+ return declaration is KSTypeParameter
+}
internal fun KSType.withNullability(nullability: XNullability) = when (nullability) {
XNullability.NULLABLE -> makeNullable()
@@ -59,14 +61,15 @@
}
private fun KSAnnotated.hasAnnotation(qName: String) =
- annotations.any { it.hasQualifiedName(qName) }
+ annotations.any { it.hasQualifiedNameOrAlias(qName) }
-private fun KSAnnotation.hasQualifiedName(qName: String): Boolean {
- return annotationType.resolve().hasQualifiedName(qName)
+private fun KSAnnotation.hasQualifiedNameOrAlias(qName: String): Boolean {
+ return annotationType.resolve().hasQualifiedNameOrAlias(qName)
}
-private fun KSType.hasQualifiedName(qName: String): Boolean {
- return declaration.qualifiedName?.asString() == qName
+private fun KSType.hasQualifiedNameOrAlias(qName: String): Boolean {
+ return declaration.qualifiedName?.asString() == qName ||
+ (declaration as? KSTypeAlias)?.type?.resolve()?.hasQualifiedNameOrAlias(qName) ?: false
}
internal fun KSAnnotated.hasJvmWildcardAnnotation() =
@@ -75,20 +78,55 @@
internal fun KSAnnotated.hasSuppressJvmWildcardAnnotation() =
hasAnnotation(JvmSuppressWildcards::class.java.canonicalName!!)
-private fun KSType.hasAnnotation(qName: String) = annotations.any { it.hasQualifiedName(qName) }
-
-internal fun KSType.hasJvmWildcardAnnotation() =
- hasAnnotation(JvmWildcard::class.java.canonicalName!!)
+// TODO(bcorso): There's a bug in KSP where, after using KSType#asMemberOf() or KSType#replace(),
+// the annotations are removed from the resulting type. However, it turns out that the annotation
+// information is still available in the underlying KotlinType, so we use reflection to get them.
+// See https://github.com/google/ksp/issues/1376.
+private fun KSType.hasAnnotation(qName: String): Boolean {
+ fun String.toFqName(): Any {
+ return Class.forName("org.jetbrains.kotlin.name.FqName")
+ .getConstructor(String::class.java)
+ .newInstance(this)
+ }
+ fun hasAnnotationViaReflection(qName: String): Boolean {
+ val ksType = if (
+ // Note: Technically, we could just make KSTypeWrapper internal and cast to get the
+ // delegate, but since we need to use reflection anyway, just get it via reflection.
+ this.javaClass.canonicalName == "androidx.room.compiler.processing.ksp.KSTypeWrapper") {
+ this.javaClass.methods.find { it.name == "getDelegate" }?.invoke(this)
+ } else {
+ this
+ }
+ val kotlinType =
+ ksType?.javaClass?.methods?.find { it.name == "getKotlinType" }?.invoke(ksType)
+ val kotlinAnnotations =
+ kotlinType?.javaClass
+ ?.methods
+ ?.find { it.name == "getAnnotations" }
+ ?.invoke(kotlinType)
+ return kotlinAnnotations?.javaClass
+ ?.methods
+ ?.find { it.name == "hasAnnotation" }
+ ?.invoke(kotlinAnnotations, qName.toFqName()) == true
+ }
+ return if (annotations.toList().isEmpty()) {
+ // If there are no annotations but KSType#toString() shows annotations, check the underlying
+ // KotlinType for annotations using reflection.
+ toString().startsWith("[") && hasAnnotationViaReflection(qName)
+ } else {
+ annotations.any { it.annotationType.resolve().hasQualifiedNameOrAlias(qName) }
+ }
+}
internal fun KSType.hasSuppressJvmWildcardAnnotation() =
hasAnnotation(JvmSuppressWildcards::class.java.canonicalName!!)
internal fun KSNode.hasSuppressWildcardsAnnotationInHierarchy(): Boolean {
- (this as? KSAnnotated)?.let {
- if (hasSuppressJvmWildcardAnnotation()) {
- return true
- }
- }
- val parent = parent ?: return false
- return parent.hasSuppressWildcardsAnnotationInHierarchy()
- }
\ No newline at end of file
+ (this as? KSAnnotated)?.let {
+ if (hasSuppressJvmWildcardAnnotation()) {
+ return true
+ }
+ }
+ val parent = parent ?: return false
+ return parent.hasSuppressWildcardsAnnotationInHierarchy()
+}
\ No newline at end of file
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KSTypeVarianceResolver.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KSTypeVarianceResolver.kt
index bdade61..d797d50 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KSTypeVarianceResolver.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KSTypeVarianceResolver.kt
@@ -25,6 +25,7 @@
import com.google.devtools.ksp.symbol.KSTypeAlias
import com.google.devtools.ksp.symbol.KSTypeArgument
import com.google.devtools.ksp.symbol.KSTypeParameter
+import com.google.devtools.ksp.symbol.KSTypeReference
import com.google.devtools.ksp.symbol.Modifier
import com.google.devtools.ksp.symbol.Variance
@@ -59,21 +60,57 @@
return type
}
- val resolvedType = if (hasTypeVariables(scope.declarationType())) {
+ // First, replace any type aliases in the type with their actual types
+ return type.replaceTypeAliases()
+ // Next, resolve wildcards based on the scope of the type
+ .resolveWildcards(scope)
+ // Next, apply any additional variance changes based on the @JvmSuppressWildcards or
+ // @JvmWildcard annotations on the resolved type.
+ .applyJvmWildcardAnnotations()
+ // Finally, unwrap any delegate types. (Note: as part of resolving wildcards, we wrap
+ // types/type arguments in delegates to avoid loosing annotation information. However,
+ // those delegates may cause issues later if KSP tries to cast the type/argument to a
+ // particular implementation, so we unwrap them here.
+ .removeWrappers()
+ }
+
+ private fun KSType.replaceTypeAliases(typeStack: ReferenceStack = ReferenceStack()): KSType {
+ if (isError || typeStack.queue.contains(this)) {
+ return this
+ }
+ if (declaration is KSTypeAlias) {
+ return (declaration as KSTypeAlias).type.resolve().replaceTypeAliases(typeStack)
+ }
+ return typeStack.withReference(this) {
+ createWrapper(arguments.map { it.replaceTypeAliases(typeStack) })
+ }
+ }
+
+ private fun KSTypeArgument.replaceTypeAliases(typeStack: ReferenceStack): KSTypeArgument {
+ val type = type?.resolve()
+ if (
+ type == null ||
+ type.isError ||
+ variance == Variance.STAR ||
+ typeStack.queue.contains(type)
+ ) {
+ return this
+ }
+ return createWrapper(type.replaceTypeAliases(typeStack), variance)
+ }
+
+ private fun KSType.resolveWildcards(scope: KSTypeVarianceResolverScope): KSType {
+ return if (hasTypeVariables(scope.declarationType())) {
// If the associated declared type contains type variables that were resolved, e.g.
// using "asMemberOf", then it has special rules about how to resolve the types.
getJavaWildcardWithTypeVariables(
- type = type,
+ type = this,
declarationType = getJavaWildcard(scope.declarationType(), scope),
scope = scope,
)
} else {
- getJavaWildcard(type, scope)
+ getJavaWildcard(this, scope)
}
-
- // As a final pass, we apply variance from any @JvmSuppressWildcards or @JvmWildcard
- // annotations on the resolved type.
- return applyJvmWildcardAnnotations(resolvedType)
}
private fun hasTypeVariables(
@@ -99,15 +136,6 @@
if (type.isError || typeStack.queue.contains(type)) {
return type
}
- if (type.declaration is KSTypeAlias) {
- return getJavaWildcard(
- type = (type.declaration as KSTypeAlias).type.resolve(),
- scope = scope,
- typeStack = typeStack,
- typeArgStack = typeArgStack,
- typeParamStack = typeParamStack,
- )
- }
return typeStack.withReference(type) {
val resolvedTypeArgs =
type.arguments.indices.map { i ->
@@ -120,7 +148,7 @@
typeParamStack = typeParamStack,
)
}
- type.replace(resolvedTypeArgs)
+ type.createWrapper(resolvedTypeArgs)
}
}
@@ -158,10 +186,10 @@
if (typeParamStack.indices.none { i ->
(typeParamStack[i].variance == Variance.CONTRAVARIANT ||
typeArgStack[i].variance == Variance.CONTRAVARIANT) &&
- // The declaration and use site variance is ignored when using @JvmWildcard
- // explicitly on a type.
- !typeArgStack[i].hasJvmWildcardAnnotation()
- }) {
+ // The declaration and use site variance is ignored when using @JvmWildcard
+ // explicitly on a type.
+ !typeArgStack[i].hasJvmWildcardAnnotation()
+ }) {
return false
}
} else {
@@ -217,7 +245,7 @@
} else {
typeArg.variance
}
- return createTypeArgument(resolvedType, resolvedVariance)
+ return typeArg.createWrapper(resolvedType, resolvedVariance)
}
private fun getJavaWildcardWithTypeVariables(
@@ -252,7 +280,7 @@
)
}
}
- type.replace(resolvedTypeArgs)
+ type.createWrapper(resolvedTypeArgs)
}
}
@@ -293,7 +321,7 @@
} else {
typeArg.variance
}
- return createTypeArgument(resolvedType, resolvedVariance)
+ return typeArg.createWrapper(resolvedType, resolvedVariance)
}
private fun getJavaWildcardWithTypeVariablesForOuterType(
@@ -322,26 +350,27 @@
} else {
typeArg.variance
}
- return createTypeArgument(resolvedType, resolvedVariance)
+ return typeArg.createWrapper(resolvedType, resolvedVariance)
}
- private fun applyJvmWildcardAnnotations(
- type: KSType,
+ private fun KSType.applyJvmWildcardAnnotations(
typeStack: ReferenceStack = ReferenceStack(),
+ typeArgStack: List<KSTypeArgument> = emptyList(),
): KSType {
- if (type.isError || typeStack.queue.contains(type)) {
- return type
+ if (isError || typeStack.queue.contains(this)) {
+ return this
}
- return typeStack.withReference(type) {
+ return typeStack.withReference(this) {
val resolvedTypeArgs =
- type.arguments.indices.map { i ->
+ arguments.indices.map { i ->
applyJvmWildcardAnnotations(
- typeArg = type.arguments[i],
- typeParameter = type.declaration.typeParameters[i],
+ typeArg = arguments[i],
+ typeParameter = declaration.typeParameters[i],
+ typeArgStack = typeArgStack,
typeStack = typeStack,
)
}
- type.replace(resolvedTypeArgs)
+ createWrapper(resolvedTypeArgs)
}
}
@@ -349,6 +378,7 @@
typeArg: KSTypeArgument,
typeParameter: KSTypeParameter,
typeStack: ReferenceStack,
+ typeArgStack: List<KSTypeArgument>,
): KSTypeArgument {
val type = typeArg.type?.resolve()
if (
@@ -359,28 +389,107 @@
) {
return typeArg
}
- val resolvedType = applyJvmWildcardAnnotations(
- type = type,
- typeStack = typeStack,
- )
+ val resolvedType = type.applyJvmWildcardAnnotations(typeStack, typeArgStack + typeArg)
val resolvedVariance = when {
typeParameter.variance == Variance.INVARIANT &&
typeArg.variance != Variance.INVARIANT -> typeArg.variance
typeArg.hasJvmWildcardAnnotation() -> typeParameter.variance
- typeStack.queue.any { it.hasSuppressJvmWildcardAnnotation() } ||
+ // We only need to check the first type in the stack for @JvmSuppressWildcards.
+ // Any other @JvmSuppressWildcards usages will be placed on the type arguments rather
+ // than the types, so no need to check the rest of the types.
+ typeStack.queue.first().hasSuppressJvmWildcardAnnotation() ||
typeArg.hasSuppressWildcardsAnnotationInHierarchy() ||
+ typeArgStack.any { it.hasSuppressJvmWildcardAnnotation() } ||
typeParameter.hasSuppressWildcardsAnnotationInHierarchy() -> Variance.INVARIANT
else -> typeArg.variance
}
- return createTypeArgument(resolvedType, resolvedVariance)
+ return typeArg.createWrapper(resolvedType, resolvedVariance)
}
- private fun KSType.isTypeParameter(): Boolean {
- return createTypeReference().isTypeParameterReference()
+ private fun KSTypeArgument.createWrapper(
+ newType: KSType,
+ newVariance: Variance
+ ): KSTypeArgument {
+ return KSTypeArgumentWrapper(
+ delegate = (this as? KSTypeArgumentWrapper)?.delegate ?: this,
+ type = newType.createTypeReference(),
+ variance = newVariance
+ )
}
- private fun createTypeArgument(type: KSType, variance: Variance): KSTypeArgument {
- return resolver.getTypeArgument(type.createTypeReference(), variance)
+ private fun KSType.createWrapper(newArguments: List<KSTypeArgument>): KSType {
+ return KSTypeWrapper(
+ delegate = (this as? KSTypeWrapper)?.delegate ?: this,
+ arguments = newArguments
+ )
+ }
+
+ private fun KSType.removeWrappers(typeStack: ReferenceStack = ReferenceStack()): KSType {
+ if (isError || typeStack.queue.contains(this)) {
+ return this
+ }
+ return typeStack.withReference(this) {
+ val delegateType = (this as? KSTypeWrapper)?.delegate ?: this
+ delegateType.replace(arguments.map { it.removeWrappers(typeStack) })
+ }
+ }
+
+ private fun KSTypeArgument.removeWrappers(
+ typeStack: ReferenceStack = ReferenceStack()
+ ): KSTypeArgument {
+ val type = type?.resolve()
+ if (
+ type == null ||
+ type.isError ||
+ variance == Variance.STAR ||
+ typeStack.queue.contains(type)
+ ) {
+ return this
+ }
+ return resolver.getTypeArgument(
+ type.removeWrappers(typeStack).createTypeReference(),
+ variance
+ )
+ }
+}
+
+/**
+ * A wrapper for creating a new [KSType] that allows arguments of type [KSTypeArgumentWrapper].
+ *
+ * Note: This wrapper acts similar to [KSType#replace(KSTypeArgument)]. However, we can't call
+ * [KSType#replace(KSTypeArgument)] directly when using [KSTypeArgumentWrapper] or we'll get an
+ * [IllegalStateException] since KSP tries to cast to its own implementation of [KSTypeArgument].
+ */
+private class KSTypeWrapper(
+ val delegate: KSType,
+ override val arguments: List<KSTypeArgument>
+) : KSType by delegate {
+ override fun toString() = if (arguments.isNotEmpty()) {
+ "${delegate.toString().substringBefore("<")}<${arguments.joinToString(",")}>"
+ } else {
+ delegate.toString()
+ }
+}
+
+/**
+ * A wrapper for creating a new [KSTypeArgument] that delegates to the original argument for
+ * annotations.
+ *
+ * Note: This wrapper acts similar to [Resolver#getTypeArgument(KSTypeReference, Variance)].
+ * However, we can't call [Resolver#getTypeArgument(KSTypeReference, Variance)] directly because
+ * we'll lose information about annotations (e.g. `@JvmSuppressWildcards`) that were on the original
+ * type argument.
+ */
+private class KSTypeArgumentWrapper(
+ val delegate: KSTypeArgument,
+ override val type: KSTypeReference,
+ override val variance: Variance,
+) : KSTypeArgument by delegate {
+ override fun toString() = when (variance) {
+ Variance.INVARIANT -> "${type.resolve()}"
+ Variance.CONTRAVARIANT -> "in ${type.resolve()}"
+ Variance.COVARIANT -> "out ${type.resolve()}"
+ Variance.STAR -> "*"
}
}
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KSTypeVarianceResolverScope.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KSTypeVarianceResolverScope.kt
index f39ab11..19f546a 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KSTypeVarianceResolverScope.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KSTypeVarianceResolverScope.kt
@@ -27,7 +27,7 @@
* Provides KSType resolution scope for a type.
*/
internal sealed class KSTypeVarianceResolverScope(
- private val annotated: KSAnnotated,
+ val annotated: KSAnnotated,
private val container: KSDeclaration?,
private val asMemberOf: KspType?
) {
@@ -36,8 +36,15 @@
* parameter is in kotlin or the containing class, which inherited the method, is in kotlin.
*/
val needsWildcardResolution: Boolean by lazy {
+ fun nodeForSuppressionCheck(): KSAnnotated? = when (this) {
+ // For property setter and getter methods skip to the enclosing class to check for
+ // suppression annotations to match KAPT.
+ is PropertySetterParameterType,
+ is PropertyGetterMethodReturnType -> annotated.parent?.parent as? KSAnnotated
+ else -> annotated
+ }
(annotated.isInKotlinCode() || container?.isInKotlinCode() == true) &&
- !annotated.hasSuppressWildcardsAnnotationInHierarchy()
+ nodeForSuppressionCheck()?.hasSuppressWildcardsAnnotationInHierarchy() != true
}
private fun KSAnnotated.isInKotlinCode(): Boolean {
diff --git a/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/ksp/KspTypeNamesGoldenTest.kt b/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/ksp/KspTypeNamesGoldenTest.kt
index 2bd5ed4..be3f4bf 100644
--- a/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/ksp/KspTypeNamesGoldenTest.kt
+++ b/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/ksp/KspTypeNamesGoldenTest.kt
@@ -159,6 +159,11 @@
class MyGenericIn<in T>
class MyGenericOut<out T>
class MyGenericMultipleParameters<T1: MyGeneric<*>, T2: MyGeneric<T1>>
+ interface MyInterface
+ typealias MyInterfaceAlias = MyInterface
+ typealias MyGenericAlias = MyGenericIn<MyGenericOut<MyGenericOut<MyType>>>
+ typealias JSW = JvmSuppressWildcards
+ typealias JW = JvmWildcard
typealias MyLambdaTypeAlias = (@JvmWildcard MyType) -> @JvmWildcard MyType
enum class MyEnum {
VAL1,
@@ -341,6 +346,14 @@
sealedListChild: List<GrandParentSealed.Parent2.Child1>,
jvmWildcard: List<@JvmWildcard String>,
suppressJvmWildcard: List<@JvmSuppressWildcards Number>,
+ suppressJvmWildcardsGeneric1: @JvmSuppressWildcards List<MyGenericOut<MyGenericIn<MyGeneric<MyType>>>>,
+ suppressJvmWildcardsGeneric2: List<@JvmSuppressWildcards MyGenericOut<MyGenericIn<MyGeneric<MyType>>>>,
+ suppressJvmWildcardsGeneric3: List<MyGenericOut<@JvmSuppressWildcards MyGenericIn<MyGeneric<MyType>>>>,
+ suppressJvmWildcardsGeneric4: List<MyGenericOut<MyGenericIn<@JvmSuppressWildcards MyGeneric<MyType>>>>,
+ interfaceAlias: List<MyInterfaceAlias>,
+ genericAlias: List<MyGenericAlias>,
+ jvmWildcardTypeAlias: List<@JW String>,
+ suppressJvmWildcardTypeAlias: List<@JSW Number>,
) {
var propWithFinalType: String = ""
var propWithOpenType: Number = 3
@@ -353,6 +366,16 @@
var propSealedListChild: List<GrandParentSealed.Parent2.Child1> = TODO()
@JvmSuppressWildcards
var propWithOpenTypeButSuppressAnnotation: Number = 3
+ var genericVar: List<MyGenericIn<MyGenericOut<MyGenericOut<MyType>>>> = TODO()
+ @JvmSuppressWildcards var suppressJvmWildcardsGenericVar1: List<MyGenericIn<MyGenericOut<MyGenericOut<MyType>>>> = TODO()
+ var suppressJvmWildcardsGenericVar2: @JvmSuppressWildcards List<MyGenericIn<MyGenericOut<MyGenericOut<MyType>>>> = TODO()
+ var suppressJvmWildcardsGenericVar3: List<@JvmSuppressWildcards MyGenericIn<MyGenericOut<MyGenericOut<MyType>>>> = TODO()
+ var suppressJvmWildcardsGenericVar4: List<MyGenericIn<@JvmSuppressWildcards MyGenericOut<MyGenericOut<MyType>>>> = TODO()
+ var suppressJvmWildcardsGenericVar5: List<MyGenericIn<MyGenericOut<@JvmSuppressWildcards MyGenericOut<MyType>>>> = TODO()
+ var interfaceAlias: List<MyInterfaceAlias> = TODO()
+ var genericAlias: List<MyGenericAlias> = TODO()
+ var jvmWildcardTypeAlias: List<@JW String> = TODO()
+ var suppressJvmWildcardTypeAlias: List<@JSW Number> = TODO()
fun list(list: List<*>): List<*> { TODO() }
fun listTypeArg(list: List<R>): List<R> { TODO() }
fun listTypeArgNumber(list: List<Number>): List<Number> { TODO() }
@@ -393,6 +416,43 @@
fun suspendExplicitJvmSuppressWildcard_OnType2(
list: @JvmSuppressWildcards List<Number>
): @JvmSuppressWildcards List<Number> { TODO() }
+ fun interfaceAlias(
+ param: List<MyInterfaceAlias>
+ ): List<MyInterfaceAlias> = TODO()
+ fun explicitJvmSuppressWildcardsOnAlias(
+ param: List<@JvmSuppressWildcards MyInterfaceAlias>,
+ ): List<@JvmSuppressWildcards MyInterfaceAlias> = TODO()
+ fun genericAlias(param: List<MyGenericAlias>): List<MyGenericAlias> = TODO()
+ fun explicitJvmSuppressWildcardsOnGenericAlias(
+ param: List<@JvmSuppressWildcards MyGenericAlias>,
+ ): List<@JvmSuppressWildcards MyGenericAlias> = TODO()
+ fun explicitOutOnInvariant_onType1_WithExplicitJvmSuppressWildcardAlias(
+ list: @JSW MyGeneric<out MyGeneric<MyType>>
+ ): @JSW MyGeneric<out MyGeneric<MyType>> { TODO() }
+ fun explicitOutOnInvariant_onType2_WithExplicitJvmSuppressWildcardAlias(
+ list: @JSW MyGeneric<MyGeneric<out MyType>>
+ ): @JSW MyGeneric<MyGeneric<out MyType>> { TODO() }
+ fun explicitOutOnVariant_onType1(
+ list: List<out List<Number>>
+ ): List<out List<Number>> { TODO() }
+ fun explicitOutOnVariant_onType2(
+ list: List<List<out Number>>
+ ): List<List<out Number>> { TODO() }
+ fun explicitOutOnVariant_onType1_WithExplicitJvmSuppressWildcardAlias(
+ list: @JSW List<out List<Number>>
+ ): @JSW List<out List<Number>> { TODO() }
+ fun explicitOutOnVariant_onType2_WithExplicitJvmSuppressWildcardAlias(
+ list: @JSW List<List<out Number>>
+ ): @JSW List<List<out Number>> { TODO() }
+ fun explicitJvmWildcardTypeAlias(
+ list: List<@JW String>
+ ): List<@JW String> { TODO() }
+ fun explicitJvmSuppressWildcardTypeAlias_OnType(
+ list: List<@JSW Number>
+ ): List<@JSW Number> { TODO() }
+ fun explicitJvmSuppressWildcardTypeAlias_OnType2(
+ list: @JSW List<Number>
+ ): @JSW List<Number> { TODO() }
}
""".trimIndent()
), listOf(className)
diff --git a/testutils/testutils-fonts/build.gradle b/testutils/testutils-fonts/build.gradle
index dffd039..911cf9b 100644
--- a/testutils/testutils-fonts/build.gradle
+++ b/testutils/testutils-fonts/build.gradle
@@ -14,12 +14,20 @@
* limitations under the License.
*/
+
+import androidx.build.KmpPlatformsKt
import androidx.build.LibraryType
plugins {
id("AndroidXPlugin")
id("com.android.library")
- id("kotlin-android")
+}
+
+def desktopEnabled = KmpPlatformsKt.enableDesktop(project)
+
+androidXMultiplatform {
+ android()
+ if (desktopEnabled) desktop()
}
dependencies {
diff --git a/wear/compose/compose-navigation/src/main/java/androidx/wear/compose/navigation/SwipeDismissableNavHost.kt b/wear/compose/compose-navigation/src/main/java/androidx/wear/compose/navigation/SwipeDismissableNavHost.kt
index f541247..1a5e97a 100644
--- a/wear/compose/compose-navigation/src/main/java/androidx/wear/compose/navigation/SwipeDismissableNavHost.kt
+++ b/wear/compose/compose-navigation/src/main/java/androidx/wear/compose/navigation/SwipeDismissableNavHost.kt
@@ -16,6 +16,7 @@
package androidx.wear.compose.navigation
+import android.util.Log
import androidx.activity.compose.LocalOnBackPressedDispatcherOwner
import androidx.compose.foundation.layout.Box
import androidx.compose.runtime.Composable
@@ -171,9 +172,23 @@
// no WearNavigator.Destinations were added to the navigation backstack (be sure to build
// the NavGraph using androidx.wear.compose.navigation.composable) or because the last entry
// was popped prior to navigating (instead, use navigate with popUpTo).
- val current = if (backStack.isNotEmpty()) backStack.last() else throw IllegalArgumentException(
- "The WearNavigator backstack is empty, there is no navigation destination to display."
- )
+ // If the activity is using FLAG_ACTIVITY_NEW_TASK then it also needs to set
+ // FLAG_ACTIVITY_CLEAR_TASK, otherwise the activity will be created twice,
+ // the first of these with an empty backstack.
+ val current = backStack.lastOrNull()
+
+ if (current == null) {
+ val warningText =
+ "Current backstack entry is empty. Please ensure: \n" +
+ "1. The current WearNavigator navigation backstack is not empty (e.g. by using " +
+ "androidx.wear.compose.navigation.composable to build your nav graph). \n" +
+ "2. The last entry is not popped prior to navigation " +
+ "(instead, use navigate with popUpTo). \n" +
+ "3. If the activity uses FLAG_ACTIVITY_NEW_TASK you should also set " +
+ "FLAG_ACTIVITY_CLEAR_TASK to maintain the backstack consistency."
+
+ Log.w(TAG, warningText)
+ }
val swipeState = state.swipeToDismissBoxState
LaunchedEffect(swipeState.currentValue) {
@@ -200,7 +215,7 @@
modifier = Modifier,
hasBackground = previous != null,
backgroundKey = previous?.id ?: SwipeToDismissKeys.Background,
- contentKey = current.id,
+ contentKey = current?.id ?: SwipeToDismissKeys.Content,
content = { isBackground ->
BoxedStackEntryContent(if (isBackground) previous else current, stateHolder, modifier)
}
@@ -279,3 +294,5 @@
}
}
}
+
+private const val TAG = "SwipeDismissableNavHost"
\ No newline at end of file
diff --git a/wear/protolayout/protolayout-expression-pipeline/api/current.txt b/wear/protolayout/protolayout-expression-pipeline/api/current.txt
index 814cdba..18e01b6 100644
--- a/wear/protolayout/protolayout-expression-pipeline/api/current.txt
+++ b/wear/protolayout/protolayout-expression-pipeline/api/current.txt
@@ -56,6 +56,7 @@
public class StateStore {
method public static androidx.wear.protolayout.expression.pipeline.StateStore create(java.util.Map<java.lang.String!,androidx.wear.protolayout.expression.StateEntryBuilders.StateEntryValue!>);
method @UiThread public void setStateEntryValues(java.util.Map<java.lang.String!,androidx.wear.protolayout.expression.StateEntryBuilders.StateEntryValue!>);
+ field public static final int MAX_STATE_ENTRY_COUNT = 100; // 0x64
}
public interface TimeGateway {
diff --git a/wear/protolayout/protolayout-expression-pipeline/api/public_plus_experimental_current.txt b/wear/protolayout/protolayout-expression-pipeline/api/public_plus_experimental_current.txt
index 814cdba..18e01b6 100644
--- a/wear/protolayout/protolayout-expression-pipeline/api/public_plus_experimental_current.txt
+++ b/wear/protolayout/protolayout-expression-pipeline/api/public_plus_experimental_current.txt
@@ -56,6 +56,7 @@
public class StateStore {
method public static androidx.wear.protolayout.expression.pipeline.StateStore create(java.util.Map<java.lang.String!,androidx.wear.protolayout.expression.StateEntryBuilders.StateEntryValue!>);
method @UiThread public void setStateEntryValues(java.util.Map<java.lang.String!,androidx.wear.protolayout.expression.StateEntryBuilders.StateEntryValue!>);
+ field public static final int MAX_STATE_ENTRY_COUNT = 100; // 0x64
}
public interface TimeGateway {
diff --git a/wear/protolayout/protolayout-expression-pipeline/api/restricted_current.txt b/wear/protolayout/protolayout-expression-pipeline/api/restricted_current.txt
index 62aa8aa..6dd8857 100644
--- a/wear/protolayout/protolayout-expression-pipeline/api/restricted_current.txt
+++ b/wear/protolayout/protolayout-expression-pipeline/api/restricted_current.txt
@@ -58,6 +58,7 @@
method public static androidx.wear.protolayout.expression.pipeline.StateStore create(java.util.Map<java.lang.String!,androidx.wear.protolayout.expression.StateEntryBuilders.StateEntryValue!>);
method @UiThread public void setStateEntryValues(java.util.Map<java.lang.String!,androidx.wear.protolayout.expression.StateEntryBuilders.StateEntryValue!>);
method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) @UiThread public void setStateEntryValuesProto(java.util.Map<java.lang.String!,androidx.wear.protolayout.expression.proto.StateEntryProto.StateEntryValue!>);
+ field public static final int MAX_STATE_ENTRY_COUNT = 100; // 0x64
}
public interface TimeGateway {
diff --git a/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/BoundDynamicTypeImpl.java b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/BoundDynamicTypeImpl.java
index 3ed31ac..bbae3dc 100644
--- a/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/BoundDynamicTypeImpl.java
+++ b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/BoundDynamicTypeImpl.java
@@ -16,6 +16,11 @@
package androidx.wear.protolayout.expression.pipeline;
+import android.os.Handler;
+import android.os.Looper;
+
+import androidx.annotation.UiThread;
+
import java.util.List;
/**
@@ -76,6 +81,15 @@
@Override
public void close() {
+ if (Looper.getMainLooper().isCurrentThread()) {
+ closeInternal();
+ } else {
+ new Handler(Looper.getMainLooper()).post(this::closeInternal);
+ }
+ }
+
+ @UiThread
+ private void closeInternal() {
mNodes.stream()
.filter(n -> n instanceof DynamicDataSourceNode)
.forEach(n -> ((DynamicDataSourceNode<?>) n).destroy());
diff --git a/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/DynamicTypeEvaluator.java b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/DynamicTypeEvaluator.java
index 068ce4b..3deccea 100644
--- a/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/DynamicTypeEvaluator.java
+++ b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/DynamicTypeEvaluator.java
@@ -250,8 +250,8 @@
/**
* Gets the quota manager used for limiting the total number of dynamic types in the
- * pipeline, or {@code null} if there are no restriction on the number of dynamic types.
- * If present, the quota manager is used to prevent unreasonably expensive expressions.
+ * pipeline, or {@code null} if there are no restriction on the number of dynamic types. If
+ * present, the quota manager is used to prevent unreasonably expensive expressions.
*/
@Nullable
public QuotaManager getDynamicTypesQuotaManager() {
@@ -303,8 +303,8 @@
MainThreadExecutor uiExecutor = new MainThreadExecutor(uiHandler);
TimeGateway timeGateway = config.getTimeGateway();
if (timeGateway == null) {
- timeGateway = new TimeGatewayImpl(uiHandler);
- ((TimeGatewayImpl) timeGateway).enableUpdates();
+ timeGateway = new TimeGatewayImpl(uiHandler);
+ ((TimeGatewayImpl) timeGateway).enableUpdates();
}
this.mTimeDataSource = new EpochTimePlatformDataSource(uiExecutor, timeGateway);
if (config.getSensorGateway() != null) {
@@ -331,7 +331,7 @@
if (!mDynamicTypesQuotaManager.tryAcquireQuota(boundDynamicType.getDynamicNodeCount())) {
throw new EvaluationException(
"Dynamic type expression limit reached. Try making the dynamic type expression"
- + " shorter or reduce the number of dynamic type expressions.");
+ + " shorter or reduce the number of dynamic type expressions.");
}
return boundDynamicType;
}
diff --git a/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/StateStore.java b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/StateStore.java
index c2ce371..f722bbb 100644
--- a/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/StateStore.java
+++ b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/StateStore.java
@@ -18,6 +18,8 @@
import static java.util.stream.Collectors.toMap;
+import android.annotation.SuppressLint;
+
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
@@ -41,13 +43,27 @@
* must only be used from the UI thread.
*/
public class StateStore {
+ /**
+ * Maximum number for state entries allowed for this {@link StateStore}.
+ *
+ * <p>The ProtoLayout state model is not designed to handle large volumes of layout provided
+ * state. So we limit the number of state entries to keep the on-the-wire size and state
+ * store update times manageable.
+ */
+ @SuppressLint("MinMaxConstant")
+ public static final int MAX_STATE_ENTRY_COUNT = 100;
@NonNull private final Map<String, StateEntryValue> mCurrentState = new ArrayMap<>();
@NonNull
private final Map<String, Set<DynamicTypeValueReceiverWithPreUpdate<StateEntryValue>>>
mRegisteredCallbacks = new ArrayMap<>();
- /** Creates a {@link StateStore}. */
+ /**
+ * Creates a {@link StateStore}.
+ *
+ * @throws IllegalStateException if number of initialState entries is greater than
+ * {@link StateStore#MAX_STATE_ENTRY_COUNT}.
+ */
@NonNull
public static StateStore create(
@NonNull Map<String, StateEntryBuilders.StateEntryValue> initialState) {
@@ -56,6 +72,9 @@
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
public StateStore(@NonNull Map<String, StateEntryValue> initialState) {
+ if (initialState.size() > MAX_STATE_ENTRY_COUNT) {
+ throw stateTooLargeException(initialState.size());
+ }
mCurrentState.putAll(initialState);
}
@@ -63,6 +82,10 @@
* Sets the given state, replacing the current state.
*
* <p>Informs registered listeners of changed values, invalidates removed values.
+ *
+ * @throws IllegalStateException if number of state entries is greater than
+ * {@link StateStore#MAX_STATE_ENTRY_COUNT}. The state will not update and old state entries
+ * will stay in place.
*/
@UiThread
public void setStateEntryValues(
@@ -74,10 +97,18 @@
* Sets the given state, replacing the current state.
*
* <p>Informs registered listeners of changed values, invalidates removed values.
+ *
+ * @throws IllegalStateException if number of state entries is larger than
+ * {@link StateStore#MAX_STATE_ENTRY_COUNT}. The state will not update and old state entries
+ * will stay in place.
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
@UiThread
public void setStateEntryValuesProto(@NonNull Map<String, StateEntryValue> newState) {
+ if (newState.size() > MAX_STATE_ENTRY_COUNT) {
+ throw stateTooLargeException(newState.size());
+ }
+
// Figure out which nodes have actually changed.
Set<String> removedKeys = getRemovedKeys(newState);
Map<String, StateEntryValue> changedEntries = getChangedEntries(newState);
@@ -85,10 +116,9 @@
Stream.concat(removedKeys.stream(), changedEntries.keySet().stream())
.forEach(
key -> {
- for (DynamicTypeValueReceiverWithPreUpdate<StateEntryValue>
- callback :
- mRegisteredCallbacks.getOrDefault(
- key, Collections.emptySet())) {
+ for (DynamicTypeValueReceiverWithPreUpdate<StateEntryValue> callback :
+ mRegisteredCallbacks.getOrDefault(
+ key, Collections.emptySet())) {
callback.onPreUpdate();
}
});
@@ -168,4 +198,12 @@
}
return result;
}
+
+ static IllegalStateException stateTooLargeException(int stateSize) {
+ return new IllegalStateException(
+ String.format(
+ "Too many state entries: %d. The maximum number of allowed state entries "
+ + "is %d.",
+ stateSize, MAX_STATE_ENTRY_COUNT));
+ }
}
diff --git a/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/TimeGatewayImpl.java b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/TimeGatewayImpl.java
index f9f699d..18bae95 100644
--- a/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/TimeGatewayImpl.java
+++ b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/TimeGatewayImpl.java
@@ -138,6 +138,7 @@
}
@Override
+ @UiThread
public void close() {
setUpdatesEnabled(false);
registeredCallbacks.clear();
diff --git a/wear/protolayout/protolayout-expression-pipeline/src/test/java/androidx/wear/protolayout/expression/pipeline/StateStoreTest.java b/wear/protolayout/protolayout-expression-pipeline/src/test/java/androidx/wear/protolayout/expression/pipeline/StateStoreTest.java
index d3a2bb4..ae1630d 100644
--- a/wear/protolayout/protolayout-expression-pipeline/src/test/java/androidx/wear/protolayout/expression/pipeline/StateStoreTest.java
+++ b/wear/protolayout/protolayout-expression-pipeline/src/test/java/androidx/wear/protolayout/expression/pipeline/StateStoreTest.java
@@ -16,6 +16,7 @@
package androidx.wear.protolayout.expression.pipeline;
+import static org.junit.Assert.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
@@ -38,6 +39,9 @@
import org.mockito.InOrder;
import org.mockito.Mockito;
+import java.util.HashMap;
+import java.util.Map;
+
@RunWith(AndroidJUnit4.class)
public class StateStoreTest {
@Rule public Expect mExpect = Expect.create();
@@ -48,6 +52,8 @@
"foo", buildStateEntry("bar"),
"baz", buildStateEntry("foobar")));
+ public StateStoreTest() {}
+
@Test
public void setBuilderApi() {
mStateStoreUnderTest.setStateEntryValues(
@@ -58,6 +64,25 @@
}
@Test
+ public void initState_largeNumberOfEntries_throws() {
+ Map<String, StateEntryBuilders.StateEntryValue> state = new HashMap<>();
+ for (int i = 0; i < StateStore.MAX_STATE_ENTRY_COUNT + 10; i++) {
+ state.put(Integer.toString(i), StateEntryBuilders.StateEntryValue.fromString("baz"));
+ }
+ assertThrows(IllegalStateException.class, () -> StateStore.create(state));
+ }
+
+ @Test
+ public void newState_largeNumberOfEntries_throws() {
+ Map<String, StateEntryBuilders.StateEntryValue> state = new HashMap<>();
+ for (int i = 0; i < StateStore.MAX_STATE_ENTRY_COUNT + 10; i++) {
+ state.put(Integer.toString(i), StateEntryBuilders.StateEntryValue.fromString("baz"));
+ }
+ assertThrows(
+ IllegalStateException.class, () -> mStateStoreUnderTest.setStateEntryValues(state));
+ }
+
+ @Test
public void canReadInitialState() {
mExpect.that(mStateStoreUnderTest.getStateEntryValuesProto("foo"))
.isEqualTo(buildStateEntry("bar"));
@@ -88,8 +113,7 @@
@Test
public void setStateFiresListeners() {
- DynamicTypeValueReceiverWithPreUpdate<StateEntryValue> cb =
- buildStateUpdateCallbackMock();
+ DynamicTypeValueReceiverWithPreUpdate<StateEntryValue> cb = buildStateUpdateCallbackMock();
mStateStoreUnderTest.registerCallback("foo", cb);
mStateStoreUnderTest.setStateEntryValuesProto(
@@ -101,8 +125,7 @@
@Test
public void setStateFiresOnPreStateUpdateFirst() {
- DynamicTypeValueReceiverWithPreUpdate<StateEntryValue> cb =
- buildStateUpdateCallbackMock();
+ DynamicTypeValueReceiverWithPreUpdate<StateEntryValue> cb = buildStateUpdateCallbackMock();
InOrder inOrder = Mockito.inOrder(cb);
@@ -166,8 +189,7 @@
@SuppressWarnings("unchecked")
@Test
public void canUnregisterListeners() {
- DynamicTypeValueReceiverWithPreUpdate<StateEntryValue> cb =
- buildStateUpdateCallbackMock();
+ DynamicTypeValueReceiverWithPreUpdate<StateEntryValue> cb = buildStateUpdateCallbackMock();
mStateStoreUnderTest.registerCallback("foo", cb);
mStateStoreUnderTest.setStateEntryValuesProto(
@@ -183,8 +205,7 @@
}
@SuppressWarnings("unchecked")
- private DynamicTypeValueReceiverWithPreUpdate<StateEntryValue>
- buildStateUpdateCallbackMock() {
+ private DynamicTypeValueReceiverWithPreUpdate<StateEntryValue> buildStateUpdateCallbackMock() {
// This needs an unchecked cast because of the generic; this method just centralizes the
// warning suppression.
return mock(DynamicTypeValueReceiverWithPreUpdate.class);
diff --git a/wear/protolayout/protolayout-renderer/src/test/java/androidx/wear/protolayout/renderer/dynamicdata/ProtoLayoutDynamicDataPipelineTest.java b/wear/protolayout/protolayout-renderer/src/test/java/androidx/wear/protolayout/renderer/dynamicdata/ProtoLayoutDynamicDataPipelineTest.java
index 9b7d7ac..d89a564 100644
--- a/wear/protolayout/protolayout-renderer/src/test/java/androidx/wear/protolayout/renderer/dynamicdata/ProtoLayoutDynamicDataPipelineTest.java
+++ b/wear/protolayout/protolayout-renderer/src/test/java/androidx/wear/protolayout/renderer/dynamicdata/ProtoLayoutDynamicDataPipelineTest.java
@@ -168,7 +168,8 @@
}
@Test
- public void buildPipeline_dpProp_animatable_animationsDisabled_hasStaticValue_assignsEndVal() {
+ public void
+ buildPipeline_dpProp_animatable_animationsDisabled_hasStaticValue_assignsEndValue() {
List<Float> results = new ArrayList<>();
float endValue = 10.0f;
DynamicFloat dynamicFloat = animatableFixedFloat(5.0f, endValue);
@@ -183,7 +184,7 @@
@Test
public void
- buildPipeline_degreesProp_animatable_animationsDisabled_hasStaticValue_assignsEndVal() {
+ buildPipeline_degreesProp_animatable_animationsDisabled_hasStaticValue_assignsEndValue() {
List<Float> results = new ArrayList<>();
float endValue = 10.0f;
DynamicFloat dynamicFloat = animatableFixedFloat(5.0f, endValue);
@@ -217,7 +218,7 @@
@Test
public void
- buildPipeline_colorProp_animatable_animationsDisabled_noStaticValueSet_assignsEndVal() {
+ buildPipeline_colorProp_animatable_animationsDisabled_noStaticValueSet_assignsEndValue() {
List<Integer> results = new ArrayList<>();
DynamicColor dynamicColor = animatableFixedColor(0, 1);
ColorProp colorProp = ColorProp.newBuilder().setDynamicValue(dynamicColor).build();
@@ -1719,8 +1720,7 @@
Repeatable.newBuilder()
.setRepeatMode(
RepeatMode
- .REPEAT_MODE_REVERSE
- )
+ .REPEAT_MODE_REVERSE)
.setIterations(iterations)
.setForwardRepeatOverride(
alternateParameters)
@@ -1813,13 +1813,12 @@
ProtoLayoutDynamicDataPipeline pipeline =
enableAnimations
? new ProtoLayoutDynamicDataPipeline(
- /* sensorGateway= */ null,
+ /* sensorGateway= */ null,
mStateStore,
new FixedQuotaManagerImpl(MAX_VALUE),
new FixedQuotaManagerImpl(MAX_VALUE))
: new ProtoLayoutDynamicDataPipeline(
- /* sensorGateway= */ null,
- mStateStore);
+ /* sensorGateway= */ null, mStateStore);
shadowOf(getMainLooper()).idle();
pipeline.setFullyVisible(true);
@@ -1842,7 +1841,7 @@
AddToListCallback<Float> receiver =
new AddToListCallback<>(results, /* invalidList= */ null);
ProtoLayoutDynamicDataPipeline pipeline =
- new ProtoLayoutDynamicDataPipeline( /* sensorGateway= */ null, mStateStore);
+ new ProtoLayoutDynamicDataPipeline(/* sensorGateway= */ null, mStateStore);
shadowOf(getMainLooper()).idle();
pipeline.setFullyVisible(true);
@@ -1860,7 +1859,7 @@
AddToListCallback<Integer> receiver =
new AddToListCallback<>(results, /* invalidList= */ null);
ProtoLayoutDynamicDataPipeline pipeline =
- new ProtoLayoutDynamicDataPipeline( /* sensorGateway= */ null, mStateStore);
+ new ProtoLayoutDynamicDataPipeline(/* sensorGateway= */ null, mStateStore);
shadowOf(getMainLooper()).idle();
pipeline.setFullyVisible(true);
@@ -1878,7 +1877,7 @@
AddToListCallback<Float> receiver =
new AddToListCallback<>(results, /* invalidList= */ null);
ProtoLayoutDynamicDataPipeline pipeline =
- new ProtoLayoutDynamicDataPipeline( /* sensorGateway= */ null, mStateStore);
+ new ProtoLayoutDynamicDataPipeline(/* sensorGateway= */ null, mStateStore);
shadowOf(getMainLooper()).idle();
pipeline.setFullyVisible(true);
@@ -1896,7 +1895,7 @@
AddToListCallback<Float> receiver =
new AddToListCallback<>(results, /* invalidList= */ null);
ProtoLayoutDynamicDataPipeline pipeline =
- new ProtoLayoutDynamicDataPipeline( /* sensorGateway= */ null, mStateStore);
+ new ProtoLayoutDynamicDataPipeline(/* sensorGateway= */ null, mStateStore);
shadowOf(getMainLooper()).idle();
pipeline.setFullyVisible(true);
@@ -1914,7 +1913,7 @@
AddToListCallback<Integer> receiver =
new AddToListCallback<>(results, /* invalidList= */ null);
ProtoLayoutDynamicDataPipeline pipeline =
- new ProtoLayoutDynamicDataPipeline( /* sensorGateway= */ null, mStateStore);
+ new ProtoLayoutDynamicDataPipeline(/* sensorGateway= */ null, mStateStore);
shadowOf(getMainLooper()).idle();
pipeline.setFullyVisible(true);
diff --git a/wear/protolayout/protolayout-renderer/src/test/java/androidx/wear/protolayout/renderer/inflater/ProtoLayoutInflaterTest.java b/wear/protolayout/protolayout-renderer/src/test/java/androidx/wear/protolayout/renderer/inflater/ProtoLayoutInflaterTest.java
index 9e263bf..9a529c2 100644
--- a/wear/protolayout/protolayout-renderer/src/test/java/androidx/wear/protolayout/renderer/inflater/ProtoLayoutInflaterTest.java
+++ b/wear/protolayout/protolayout-renderer/src/test/java/androidx/wear/protolayout/renderer/inflater/ProtoLayoutInflaterTest.java
@@ -4205,8 +4205,9 @@
private static FadeInTransition.Builder fadeIn(int delay) {
return FadeInTransition.newBuilder()
.setAnimationSpec(
- AnimationSpec.newBuilder().setAnimationParameters(
- AnimationParameters.newBuilder().setDelayMillis(delay)));
+ AnimationSpec.newBuilder()
+ .setAnimationParameters(
+ AnimationParameters.newBuilder().setDelayMillis(delay)));
}
private LayoutElement textFadeInSlideIn(String text) {
diff --git a/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/StateBuilders.java b/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/StateBuilders.java
index 8889ec0..0fb8101 100644
--- a/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/StateBuilders.java
+++ b/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/StateBuilders.java
@@ -149,9 +149,17 @@
return this;
}
+ private static final int MAX_STATE_SIZE = 30;
+
/** Builds an instance from accumulated values. */
@NonNull
public State build() {
+ if (mImpl.getIdToValueMap().size() > MAX_STATE_SIZE) {
+ throw new IllegalStateException(
+ String.format(
+ "State size is too large: %d. Maximum " + "allowed state size is %d.",
+ mImpl.getIdToValueMap().size(), MAX_STATE_SIZE));
+ }
return new State(mImpl.build(), mFingerprint);
}
}
diff --git a/wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/renderer/TileRenderer.java b/wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/renderer/TileRenderer.java
index 184aa81..3fb2267 100644
--- a/wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/renderer/TileRenderer.java
+++ b/wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/renderer/TileRenderer.java
@@ -202,15 +202,18 @@
public View inflate(@NonNull ViewGroup parent) {
String errorMessage =
"This method only works with the deprecated constructors that accept Layout and"
- + " Resources.";
+ + " Resources.";
try {
// Waiting for the result from future for backwards compatibility.
return inflateLayout(
- checkNotNull(mLayout, errorMessage),
- checkNotNull(mResources, errorMessage),
- parent).get(10, TimeUnit.SECONDS);
- } catch (ExecutionException | InterruptedException | CancellationException |
- TimeoutException e) {
+ checkNotNull(mLayout, errorMessage),
+ checkNotNull(mResources, errorMessage),
+ parent)
+ .get(10, TimeUnit.SECONDS);
+ } catch (ExecutionException
+ | InterruptedException
+ | CancellationException
+ | TimeoutException e) {
// Wrap checked exceptions to avoid changing the method signature.
throw new RuntimeException("Rendering tile has not successfully finished.", e);
}
@@ -219,13 +222,12 @@
/**
* Inflates a Tile into {@code parent}.
*
- * @param layout The portion of the Tile to render.
+ * @param layout The portion of the Tile to render.
* @param resources The resources for the Tile.
- * @param parent The view to attach the tile into.
+ * @param parent The view to attach the tile into.
* @return The future with the first child that was inflated. This may be null if the Layout is
- * empty or the top-level LayoutElement has no inner set, or the top-level LayoutElement
- * contains an
- * unsupported inner type.
+ * empty or the top-level LayoutElement has no inner set, or the top-level LayoutElement
+ * contains an unsupported inner type.
*/
@NonNull
public ListenableFuture<View> inflateAsync(
@@ -241,7 +243,6 @@
@NonNull ResourceProto.Resources resources,
@NonNull ViewGroup parent) {
ListenableFuture<Void> result = mInstance.renderAndAttach(layout, resources, parent);
- return FluentFuture.from(result)
- .transform(ignored -> parent.getChildAt(0), mUiExecutor);
+ return FluentFuture.from(result).transform(ignored -> parent.getChildAt(0), mUiExecutor);
}
}
diff --git a/wear/watchface/watchface-complications-data/src/main/java/android/support/wearable/complications/ComplicationData.kt b/wear/watchface/watchface-complications-data/src/main/java/android/support/wearable/complications/ComplicationData.kt
index 8887171d..949c589 100644
--- a/wear/watchface/watchface-complications-data/src/main/java/android/support/wearable/complications/ComplicationData.kt
+++ b/wear/watchface/watchface-complications-data/src/main/java/android/support/wearable/complications/ComplicationData.kt
@@ -1836,13 +1836,13 @@
*
* Returns this Builder to allow chaining.
*/
- fun setListEntryCollection(timelineEntries: Collection<ComplicationData>?) = apply {
- if (timelineEntries == null) {
+ fun setListEntryCollection(listEntries: Collection<ComplicationData>?) = apply {
+ if (listEntries == null) {
fields.remove(EXP_FIELD_LIST_ENTRIES)
} else {
fields.putParcelableArray(
EXP_FIELD_LIST_ENTRIES,
- timelineEntries
+ listEntries
.map { data ->
data.fields.putInt(EXP_FIELD_LIST_ENTRY_TYPE, data.type)
data.fields
diff --git a/wear/watchface/watchface-complications-data/src/main/java/androidx/wear/watchface/complications/data/ComplicationDataExpressionEvaluator.kt b/wear/watchface/watchface-complications-data/src/main/java/androidx/wear/watchface/complications/data/ComplicationDataExpressionEvaluator.kt
index 94db677..ddf43de 100644
--- a/wear/watchface/watchface-complications-data/src/main/java/androidx/wear/watchface/complications/data/ComplicationDataExpressionEvaluator.kt
+++ b/wear/watchface/watchface-complications-data/src/main/java/androidx/wear/watchface/complications/data/ComplicationDataExpressionEvaluator.kt
@@ -18,7 +18,7 @@
import android.icu.util.ULocale
import android.support.wearable.complications.ComplicationData as WireComplicationData
-import android.support.wearable.complications.ComplicationData
+import android.support.wearable.complications.ComplicationData.Companion.TYPE_NO_DATA
import android.support.wearable.complications.ComplicationText as WireComplicationText
import androidx.annotation.MainThread
import androidx.annotation.RestrictTo
@@ -41,17 +41,19 @@
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.emitAll
+import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.flow
+import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.mapNotNull
+import kotlinx.coroutines.flow.merge
import kotlinx.coroutines.flow.update
+import kotlinx.coroutines.flow.updateAndGet
import kotlinx.coroutines.invoke
import kotlinx.coroutines.launch
/**
* Evaluates a [WireComplicationData] with
* [androidx.wear.protolayout.expression.DynamicBuilders.DynamicType] within its fields.
- *
- * Due to [WireComplicationData]'s shallow copy strategy the input is modified in-place.
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
class ComplicationDataExpressionEvaluator(
@@ -65,27 +67,101 @@
*
* The expression is evaluated _separately_ on each flow collection.
*/
- fun evaluate(unevaluatedData: WireComplicationData) =
- flow<WireComplicationData> {
- val state: MutableStateFlow<State> = unevaluatedData.buildState()
- state.value.use {
- val evaluatedData: Flow<WireComplicationData> =
- state
- .mapNotNull {
- when {
- // Emitting INVALID_DATA if there's an invalid receiver.
- it.invalidReceivers.isNotEmpty() -> INVALID_DATA
- // Emitting the data if all pending receivers are done and all
- // pre-updates are satisfied.
- it.pendingReceivers.isEmpty() -> it.data
- // Skipping states that are not ready for be emitted.
- else -> null
- }
- }
- .distinctUntilChanged()
- emitAll(evaluatedData)
+ fun evaluate(unevaluatedData: WireComplicationData): Flow<WireComplicationData> =
+ evaluateTopLevelFields(unevaluatedData)
+ // Combining with fields that are made of WireComplicationData.
+ .combineWithDataList(unevaluatedData.timelineEntries) { entries ->
+ // Timeline entries are set on the built WireComplicationData.
+ WireComplicationData.Builder(
+ this@combineWithDataList.build().apply { setTimelineEntryCollection(entries) }
+ )
}
+ .combineWithDataList(unevaluatedData.listEntries) { setListEntryCollection(it) }
+ // Must be last, as it overwrites INVALID_DATA.
+ .combineWithEvaluatedPlaceholder(unevaluatedData.placeholder)
+ .distinctUntilChanged()
+
+ /** Evaluates "local" fields, excluding fields of type WireComplicationData. */
+ private fun evaluateTopLevelFields(
+ unevaluatedData: WireComplicationData
+ ): Flow<WireComplicationData> = flow {
+ val state: MutableStateFlow<State> = unevaluatedData.buildState()
+ state.value.use {
+ val evaluatedData: Flow<WireComplicationData> =
+ state.mapNotNull {
+ when {
+ // Emitting INVALID_DATA if there's an invalid receiver.
+ it.invalidReceivers.isNotEmpty() -> INVALID_DATA
+ // Emitting the data if all pending receivers are done and all
+ // pre-updates are satisfied.
+ it.pendingReceivers.isEmpty() -> it.data
+ // Skipping states that are not ready for be emitted.
+ else -> null
+ }
+ }
+ emitAll(evaluatedData)
}
+ }
+
+ /**
+ * Combines the receiver with the evaluated version of the provided list.
+ *
+ * If the receiver [Flow] emits [INVALID_DATA] or the input list is null or empty, this does not
+ * mutate the flow and does not wait for the entries to finish evaluating.
+ *
+ * If even one [WireComplicationData] within the provided list is evaluated to [INVALID_DATA],
+ * the output [Flow] becomes [INVALID_DATA] (the receiver [Flow] is ignored).
+ */
+ private fun Flow<WireComplicationData>.combineWithDataList(
+ unevaluatedEntries: List<WireComplicationData>?,
+ setter:
+ WireComplicationData.Builder.(
+ List<WireComplicationData>
+ ) -> WireComplicationData.Builder,
+ ): Flow<WireComplicationData> {
+ if (unevaluatedEntries.isNullOrEmpty()) return this
+ val evaluatedEntriesFlow: Flow<List<WireComplicationData>> =
+ combine(unevaluatedEntries.map { evaluate(it) })
+
+ return this.combine(evaluatedEntriesFlow).map {
+ (data: WireComplicationData, evaluatedEntries: List<WireComplicationData>?) ->
+ // Not mutating if invalid.
+ if (data === INVALID_DATA) return@map data
+ // An entry is invalid, emitting invalid.
+ if (evaluatedEntries.any { it === INVALID_DATA }) return@map INVALID_DATA
+ // All is well, mutating the input.
+ return@map WireComplicationData.Builder(data).setter(evaluatedEntries).build()
+ }
+ }
+
+ /**
+ * Same as [combineWithDataList], but sets the evaluated placeholder ONLY when the receiver
+ * [Flow] emits [TYPE_NO_DATA], or [keepExpression] is true, otherwise clears it and does not
+ * wait for the placeholder to finish evaluating.
+ *
+ * If the placeholder is not required (per the above paragraph), this doesn't wait for it.
+ */
+ private fun Flow<WireComplicationData>.combineWithEvaluatedPlaceholder(
+ unevaluatedPlaceholder: WireComplicationData?
+ ): Flow<WireComplicationData> {
+ if (unevaluatedPlaceholder == null) return this
+ val evaluatedPlaceholderFlow: Flow<WireComplicationData> = evaluate(unevaluatedPlaceholder)
+
+ return this.combine(evaluatedPlaceholderFlow).map {
+ (data: WireComplicationData, evaluatedPlaceholder: WireComplicationData?) ->
+ if (!keepExpression && data.type != TYPE_NO_DATA) {
+ // Clearing the placeholder when data is not TYPE_NO_DATA (it was meant as an
+ // expression fallback).
+ return@map WireComplicationData.Builder(data).setPlaceholder(null).build()
+ }
+ // Placeholder required but invalid, emitting invalid.
+ if (evaluatedPlaceholder === INVALID_DATA) return@map INVALID_DATA
+ // All is well, mutating the input.
+ return@map WireComplicationData.Builder(data)
+ .setPlaceholder(evaluatedPlaceholder)
+ .build()
+ }
+ }
private suspend fun WireComplicationData.buildState() =
MutableStateFlow(State(this)).apply {
@@ -177,7 +253,7 @@
* [ComplicationEvaluationResultReceiver] that are evaluating it.
*/
private inner class State(
- val data: ComplicationData,
+ val data: WireComplicationData,
val pendingReceivers: Set<ComplicationEvaluationResultReceiver<out Any>> = setOf(),
val invalidReceivers: Set<ComplicationEvaluationResultReceiver<out Any>> = setOf(),
val completeReceivers: Set<ComplicationEvaluationResultReceiver<out Any>> = setOf(),
@@ -317,3 +393,35 @@
runnable.run()
}
}
+
+/** Replacement of [kotlinx.coroutines.flow.combine], which doesn't seem to work. */
+internal fun <T> combine(flows: List<Flow<T>>): Flow<List<T>> = flow {
+ data class ValueExists(val value: T?, val exists: Boolean)
+ val latest = MutableStateFlow(List(flows.size) { ValueExists(null, false) })
+ @Suppress("UNCHECKED_CAST") // Flow<List<T?>> -> Flow<List<T>> safe after filtering exists.
+ emitAll(
+ flows
+ .mapIndexed { i, flow -> flow.map { i to it } } // List<Flow<Int, T>> (indexed flows)
+ .merge() // Flow<Int, T>
+ .map { (i, value) ->
+ // Updating latest and returning the current latest.
+ latest.updateAndGet {
+ val newLatest = it.toMutableList()
+ newLatest[i] = ValueExists(value, true)
+ newLatest
+ }
+ } // Flow<List<ValueExists>>
+ // Filtering emissions until we have all values.
+ .filter { values -> values.all { it.exists } }
+ // Flow<List<T>> + defensive copy.
+ .map { values -> values.map { it.value } } as Flow<List<T>>
+ )
+}
+
+/**
+ * Another replacement of [kotlinx.coroutines.flow.combine] which is similar to
+ * `combine(List<Flow<T>>)` but allows different types for each flow.
+ */
+@Suppress("UNCHECKED_CAST")
+internal fun <T1, T2> Flow<T1>.combine(other: Flow<T2>): Flow<Pair<T1, T2>> =
+ combine(listOf(this as Flow<*>, other as Flow<*>)).map { (a, b) -> (a as T1) to (b as T2) }
diff --git a/wear/watchface/watchface-complications-data/src/test/java/androidx/wear/watchface/complications/data/ComplicationDataExpressionEvaluatorTest.kt b/wear/watchface/watchface-complications-data/src/test/java/androidx/wear/watchface/complications/data/ComplicationDataExpressionEvaluatorTest.kt
index c913c25..3a22917 100644
--- a/wear/watchface/watchface-complications-data/src/test/java/androidx/wear/watchface/complications/data/ComplicationDataExpressionEvaluatorTest.kt
+++ b/wear/watchface/watchface-complications-data/src/test/java/androidx/wear/watchface/complications/data/ComplicationDataExpressionEvaluatorTest.kt
@@ -17,6 +17,8 @@
package androidx.wear.watchface.complications.data
import android.support.wearable.complications.ComplicationData as WireComplicationData
+import android.support.wearable.complications.ComplicationData.Companion.TYPE_NO_DATA
+import android.support.wearable.complications.ComplicationData.Companion.TYPE_SHORT_TEXT
import android.support.wearable.complications.ComplicationText as WireComplicationText
import android.util.Log
import androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat
@@ -58,10 +60,7 @@
@Test
fun evaluate_noExpression_returnsUnevaluated() = runBlocking {
- val data =
- WireComplicationData.Builder(WireComplicationData.TYPE_NO_DATA)
- .setRangedValue(10f)
- .build()
+ val data = WireComplicationData.Builder(TYPE_NO_DATA).setRangedValue(10f).build()
val evaluator = ComplicationDataExpressionEvaluator()
@@ -81,7 +80,7 @@
) {
SET_IMMEDIATELY_WHEN_ALL_DATA_AVAILABLE(
expressed =
- WireComplicationData.Builder(WireComplicationData.TYPE_NO_DATA)
+ WireComplicationData.Builder(TYPE_NO_DATA)
.setRangedValueExpression(DynamicFloat.constant(1f))
.setLongText(WireComplicationText(DynamicString.constant("Long Text")))
.setLongTitle(WireComplicationText(DynamicString.constant("Long Title")))
@@ -90,23 +89,29 @@
.setContentDescription(
WireComplicationText(DynamicString.constant("Description"))
)
- .build(),
+ .setPlaceholder(constantData("Placeholder"))
+ .setListEntryCollection(listOf(constantData("List")))
+ .build()
+ .also { it.setTimelineEntryCollection(listOf(constantData("Timeline"))) },
states = listOf(),
evaluated =
listOf(
- WireComplicationData.Builder(WireComplicationData.TYPE_NO_DATA)
+ WireComplicationData.Builder(TYPE_NO_DATA)
.setRangedValue(1f)
.setLongText(WireComplicationText("Long Text"))
.setLongTitle(WireComplicationText("Long Title"))
.setShortText(WireComplicationText("Short Text"))
.setShortTitle(WireComplicationText("Short Title"))
.setContentDescription(WireComplicationText("Description"))
+ .setPlaceholder(evaluatedData("Placeholder"))
+ .setListEntryCollection(listOf(evaluatedData("List")))
.build()
+ .also { it.setTimelineEntryCollection(listOf(evaluatedData("Timeline"))) },
),
),
SET_ONLY_AFTER_ALL_FIELDS_EVALUATED(
expressed =
- WireComplicationData.Builder(WireComplicationData.TYPE_NO_DATA)
+ WireComplicationData.Builder(TYPE_NO_DATA)
.setRangedValueExpression(DynamicFloat.fromState("ranged_value"))
.setLongText(WireComplicationText(DynamicString.fromState("long_text")))
.setLongTitle(WireComplicationText(DynamicString.fromState("long_title")))
@@ -115,7 +120,10 @@
.setContentDescription(
WireComplicationText(DynamicString.fromState("description"))
)
- .build(),
+ .setPlaceholder(stateData("placeholder"))
+ .setListEntryCollection(listOf(stateData("list")))
+ .build()
+ .also { it.setTimelineEntryCollection(listOf(stateData("timeline"))) },
states =
aggregate(
// Each map piles on top of the previous ones.
@@ -124,25 +132,38 @@
mapOf("long_title" to StateEntryValue.fromString("Long Title")),
mapOf("short_text" to StateEntryValue.fromString("Short Text")),
mapOf("short_title" to StateEntryValue.fromString("Short Title")),
- // Only the last one will trigger an evaluated data.
mapOf("description" to StateEntryValue.fromString("Description")),
+ mapOf("placeholder" to StateEntryValue.fromString("Placeholder")),
+ mapOf("list" to StateEntryValue.fromString("List")),
+ mapOf("timeline" to StateEntryValue.fromString("Timeline")),
+ // Only the last one will trigger an evaluated data.
),
evaluated =
listOf(
- INVALID_DATA, // Before state is available.
- WireComplicationData.Builder(WireComplicationData.TYPE_NO_DATA)
+ // Before any state is available.
+ INVALID_DATA,
+ // INVALID_DATA with placeholder, after it's available (and others aren't).
+ WireComplicationData.Builder(INVALID_DATA)
+ .setPlaceholder(evaluatedData("Placeholder"))
+ .build(),
+ // Evaluated data with after everything is available.
+ WireComplicationData.Builder(TYPE_NO_DATA)
.setRangedValue(1f)
.setLongText(WireComplicationText("Long Text"))
.setLongTitle(WireComplicationText("Long Title"))
.setShortText(WireComplicationText("Short Text"))
.setShortTitle(WireComplicationText("Short Title"))
.setContentDescription(WireComplicationText("Description"))
+ // Not trimmed for TYPE_NO_DATA.
+ .setPlaceholder(evaluatedData("Placeholder"))
+ .setListEntryCollection(listOf(evaluatedData("List")))
.build()
+ .also { it.setTimelineEntryCollection(listOf(evaluatedData("Timeline"))) },
),
),
SET_TO_EVALUATED_IF_ALL_FIELDS_VALID(
expressed =
- WireComplicationData.Builder(WireComplicationData.TYPE_SHORT_TEXT)
+ WireComplicationData.Builder(TYPE_SHORT_TEXT)
.setShortTitle(WireComplicationText(DynamicString.fromState("valid")))
.setShortText(WireComplicationText(DynamicString.fromState("valid")))
.build(),
@@ -153,7 +174,7 @@
evaluated =
listOf(
INVALID_DATA, // Before state is available.
- WireComplicationData.Builder(WireComplicationData.TYPE_SHORT_TEXT)
+ WireComplicationData.Builder(TYPE_SHORT_TEXT)
.setShortTitle(WireComplicationText("Valid"))
.setShortText(WireComplicationText("Valid"))
.build(),
@@ -161,7 +182,7 @@
),
SET_TO_NO_DATA_IF_FIRST_STATE_IS_INVALID(
expressed =
- WireComplicationData.Builder(WireComplicationData.TYPE_SHORT_TEXT)
+ WireComplicationData.Builder(TYPE_SHORT_TEXT)
.setShortTitle(WireComplicationText(DynamicString.fromState("valid")))
.setShortText(WireComplicationText(DynamicString.fromState("invalid")))
.build(),
@@ -177,7 +198,7 @@
),
SET_TO_NO_DATA_IF_LAST_STATE_IS_INVALID(
expressed =
- WireComplicationData.Builder(WireComplicationData.TYPE_SHORT_TEXT)
+ WireComplicationData.Builder(TYPE_SHORT_TEXT)
.setShortTitle(WireComplicationText(DynamicString.fromState("valid")))
.setShortText(WireComplicationText(DynamicString.fromState("invalid")))
.build(),
@@ -192,13 +213,43 @@
evaluated =
listOf(
INVALID_DATA, // Before state is available.
- WireComplicationData.Builder(WireComplicationData.TYPE_SHORT_TEXT)
+ WireComplicationData.Builder(TYPE_SHORT_TEXT)
.setShortTitle(WireComplicationText("Valid"))
.setShortText(WireComplicationText("Valid"))
.build(),
INVALID_DATA, // After it was invalidated.
),
),
+ SET_TO_EVALUATED_WITHOUT_PLACEHOLDER_IF_NOT_NO_DATA(
+ expressed =
+ WireComplicationData.Builder(TYPE_SHORT_TEXT)
+ .setShortText(WireComplicationText("Text"))
+ .setPlaceholder(evaluatedData("Placeholder"))
+ .build(),
+ states = listOf(),
+ evaluated =
+ listOf(
+ // No placeholder.
+ WireComplicationData.Builder(TYPE_SHORT_TEXT)
+ .setShortText(WireComplicationText("Text"))
+ .build(),
+ )
+ ),
+ SET_TO_EVALUATED_WITHOUT_PLACEHOLDER_EVEN_IF_PLACEHOLDER_INVALID_IF_NOT_NO_DATA(
+ expressed =
+ WireComplicationData.Builder(TYPE_SHORT_TEXT)
+ .setShortText(WireComplicationText("Text"))
+ .setPlaceholder(stateData("placeholder"))
+ .build(),
+ states = listOf(), // placeholder state not set.
+ evaluated =
+ listOf(
+ // No placeholder.
+ WireComplicationData.Builder(TYPE_SHORT_TEXT)
+ .setShortText(WireComplicationText("Text"))
+ .build(),
+ )
+ ),
}
@Test
@@ -231,7 +282,7 @@
@Test
fun evaluate_cancelled_cleansUp() = runBlocking {
val expressed =
- WireComplicationData.Builder(WireComplicationData.TYPE_NO_DATA)
+ WireComplicationData.Builder(TYPE_NO_DATA)
.setRangedValueExpression(
// Uses TimeGateway, which needs cleaning up.
DynamicInstant.withSecondsPrecision(Instant.EPOCH)
@@ -262,19 +313,22 @@
@Test
fun evaluate_keepExpression_doesNotTrimUnevaluatedExpression() = runBlocking {
val expressed =
- WireComplicationData.Builder(WireComplicationData.TYPE_NO_DATA)
+ WireComplicationData.Builder(TYPE_NO_DATA)
.setRangedValueExpression(DynamicFloat.constant(1f))
.setLongText(WireComplicationText(DynamicString.constant("Long Text")))
.setLongTitle(WireComplicationText(DynamicString.constant("Long Title")))
.setShortText(WireComplicationText(DynamicString.constant("Short Text")))
.setShortTitle(WireComplicationText(DynamicString.constant("Short Title")))
.setContentDescription(WireComplicationText(DynamicString.constant("Description")))
+ .setPlaceholder(constantData("Placeholder"))
+ .setListEntryCollection(listOf(constantData("List")))
.build()
+ .also { it.setTimelineEntryCollection(listOf(constantData("Timeline"))) }
val evaluator = ComplicationDataExpressionEvaluator(keepExpression = true)
assertThat(evaluator.evaluate(expressed).firstOrNull())
.isEqualTo(
- WireComplicationData.Builder(WireComplicationData.TYPE_NO_DATA)
+ WireComplicationData.Builder(TYPE_NO_DATA)
.setRangedValue(1f)
.setRangedValueExpression(DynamicFloat.constant(1f))
.setLongText(
@@ -292,6 +346,29 @@
.setContentDescription(
WireComplicationText("Description", DynamicString.constant("Description"))
)
+ .setPlaceholder(evaluatedWithConstantData("Placeholder"))
+ .setListEntryCollection(listOf(evaluatedWithConstantData("List")))
+ .build()
+ .also {
+ it.setTimelineEntryCollection(listOf(evaluatedWithConstantData("Timeline")))
+ },
+ )
+ }
+
+ @Test
+ fun evaluate_keepExpressionNotNoData_doesNotTrimPlaceholder() = runBlocking {
+ val expressed =
+ WireComplicationData.Builder(TYPE_SHORT_TEXT)
+ .setShortText(WireComplicationText("Text"))
+ .setPlaceholder(evaluatedData("Placeholder"))
+ .build()
+ val evaluator = ComplicationDataExpressionEvaluator(keepExpression = true)
+
+ assertThat(evaluator.evaluate(expressed).firstOrNull())
+ .isEqualTo(
+ WireComplicationData.Builder(TYPE_SHORT_TEXT)
+ .setShortText(WireComplicationText("Text"))
+ .setPlaceholder(evaluatedData("Placeholder"))
.build()
)
}
@@ -300,5 +377,25 @@
/** Converts `[{a: A}, {b: B}, {c: C}]` to `[{a: A}, {a: A, b: B}, {a: A, b: B, c: C}]`. */
fun <K, V> aggregate(vararg maps: Map<K, V>): List<Map<K, V>> =
maps.fold(listOf()) { acc, map -> acc + ((acc.lastOrNull() ?: mapOf()) + map) }
+
+ fun constantData(value: String) =
+ WireComplicationData.Builder(TYPE_NO_DATA)
+ .setLongText(WireComplicationText(DynamicString.constant(value)))
+ .build()
+
+ fun stateData(value: String) =
+ WireComplicationData.Builder(TYPE_NO_DATA)
+ .setLongText(WireComplicationText(DynamicString.fromState(value)))
+ .build()
+
+ fun evaluatedData(value: String) =
+ WireComplicationData.Builder(TYPE_NO_DATA)
+ .setLongText(WireComplicationText(value))
+ .build()
+
+ fun evaluatedWithConstantData(value: String) =
+ WireComplicationData.Builder(TYPE_NO_DATA)
+ .setLongText(WireComplicationText(value, DynamicString.constant(value)))
+ .build()
}
}