Fix focus-search crash when TvLazyColumn contains empty TvLazyRow
When beyond-bounds modifier is called during focus search, it uses
last() instead of the safer lastOrNull() call when looking through
elements to add to the focus-search interval.
Updated the code to use lastOrNull and create an interval with
only one element to avoid the crash.
Bug: b/260299091
Test: Added unit test
Change-Id: I237c6ac5cfee4104aceda95b1c4a3e767db3689c
diff --git a/tv/integration-tests/demos/src/main/java/androidx/tv/integration/demos/LazyRowsAndColumns.kt b/tv/integration-tests/demos/src/main/java/androidx/tv/integration/demos/LazyRowsAndColumns.kt
index 3de22a2..72f62aa 100644
--- a/tv/integration-tests/demos/src/main/java/androidx/tv/integration/demos/LazyRowsAndColumns.kt
+++ b/tv/integration-tests/demos/src/main/java/androidx/tv/integration/demos/LazyRowsAndColumns.kt
@@ -35,16 +35,14 @@
@Composable
fun LazyRowsAndColumns() {
TvLazyColumn(verticalArrangement = Arrangement.spacedBy(20.dp)) {
- repeat((0 until rowsCount).count()) {
- item { SampleLazyRow() }
- }
+ items(rowsCount) { SampleLazyRow() }
}
}
@Composable
fun SampleLazyRow() {
val colors = listOf(Color.Red, Color.Magenta, Color.Green, Color.Yellow, Color.Blue, Color.Cyan)
- val backgroundColors = (0 until columnsCount).map { colors.random() }
+ val backgroundColors = List(columnsCount) { colors.random() }
TvLazyRow(horizontalArrangement = Arrangement.spacedBy(10.dp)) {
backgroundColors.forEach { backgroundColor ->
diff --git a/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/grid/LazyGridTest.kt b/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/grid/LazyGridTest.kt
index 336c09e..e320788 100644
--- a/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/grid/LazyGridTest.kt
+++ b/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/grid/LazyGridTest.kt
@@ -1098,6 +1098,7 @@
}
internal fun ComposeContentTestRule.keyPress(keyCode: Int, numberOfPresses: Int = 1) {
- for (index in 0 until numberOfPresses)
+ repeat(numberOfPresses) {
InstrumentationRegistry.getInstrumentation().sendKeyDownUpSync(keyCode)
-}
+ }
+}
\ No newline at end of file
diff --git a/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/list/LazyListBeyondBoundsTest.kt b/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/list/LazyListBeyondBoundsTest.kt
index 2a8e3e2..55f74eb 100644
--- a/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/list/LazyListBeyondBoundsTest.kt
+++ b/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/list/LazyListBeyondBoundsTest.kt
@@ -16,11 +16,19 @@
package androidx.tv.foundation.lazy.list
+import androidx.compose.foundation.focusable
import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.text.BasicText
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
+import androidx.compose.ui.focus.FocusRequester
+import androidx.compose.ui.focus.focusRequester
+import androidx.compose.ui.input.key.NativeKeyEvent
import androidx.compose.ui.layout.BeyondBoundsLayout
import androidx.compose.ui.layout.BeyondBoundsLayout.LayoutDirection.Companion.Above
import androidx.compose.ui.layout.BeyondBoundsLayout.LayoutDirection.Companion.After
@@ -38,7 +46,9 @@
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.LayoutDirection.Ltr
import androidx.compose.ui.unit.LayoutDirection.Rtl
+import androidx.compose.ui.unit.dp
import androidx.test.filters.MediumTest
+import androidx.tv.foundation.lazy.grid.keyPress
import com.google.common.truth.Truth.assertThat
import org.junit.Rule
import org.junit.Test
@@ -460,6 +470,39 @@
}
}
+ @Test
+ fun emptyRowInColumn_focusSearchDoesNotCrash() {
+ val buttonFocusRequester = FocusRequester()
+ rule.setContent {
+ Column {
+ BasicText(
+ text = "Outer button",
+ Modifier.focusRequester(buttonFocusRequester).focusable())
+
+ TvLazyColumn {
+ items(3) {
+ if (it == 2) {
+ // intentional empty row
+ TvLazyRow(
+ Modifier
+ .height(200.dp)
+ .width(2000.dp)) {}
+ } else {
+ TvLazyRow {
+ items(30) {
+ Box(Modifier.size(200.dp)) { BasicText(text = it.toString()) }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ rule.runOnIdle { buttonFocusRequester.requestFocus() }
+ rule.keyPress(NativeKeyEvent.KEYCODE_DPAD_DOWN)
+ }
+
private fun ComposeContentTestRule.setLazyContent(
size: Dp,
firstVisibleItem: Int,
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazyBeyondBoundsModifier.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazyBeyondBoundsModifier.kt
index c4777fb..7868efd 100644
--- a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazyBeyondBoundsModifier.kt
+++ b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazyBeyondBoundsModifier.kt
@@ -86,7 +86,7 @@
// We use a new interval each time because this function is re-entrant.
var interval = beyondBoundsInfo.addInterval(
state.firstVisibleItemIndex,
- state.layoutInfo.visibleItemsInfo.last().index
+ state.layoutInfo.visibleItemsInfo.lastOrNull()?.index ?: state.firstVisibleItemIndex
)
var found: T? = null