Use BringIntoViewResponder to bring element into view.

By default, scrollable-containers ensure that the focused element
is brought into view. We need to bring the entire FeaturedCarousel
or ImmersiveList into view even when a sub-composable is in focus.

The earlier approach called BringIntoViewRequester.bringIntoView.
This change uses a custom BringIntoViewResponder to intercept the
request to bringIntoView and return the entire FeaturedCarousel
or ImmersiveList rectangle rather than only the rectangle of the
focused sub-composable.

Test: Existing tests cover this change.
Relnote: NA
Change-Id: I232436a2c7ee3a72fdac762463c5167470964400
diff --git a/tv/tv-material/api/current.txt b/tv/tv-material/api/current.txt
index 85bc920..0c788a4 100644
--- a/tv/tv-material/api/current.txt
+++ b/tv/tv-material/api/current.txt
@@ -1,6 +1,9 @@
 // Signature format: 4.0
 package androidx.tv.material {
 
+  public final class BringIntoViewIfChildrenAreFocusedKt {
+  }
+
   public final class ContentColorKt {
     method public static androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.ui.graphics.Color> getLocalContentColor();
     property public static final androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.ui.graphics.Color> LocalContentColor;
diff --git a/tv/tv-material/api/public_plus_experimental_current.txt b/tv/tv-material/api/public_plus_experimental_current.txt
index 05824aa..1b4f018 100644
--- a/tv/tv-material/api/public_plus_experimental_current.txt
+++ b/tv/tv-material/api/public_plus_experimental_current.txt
@@ -1,6 +1,9 @@
 // Signature format: 4.0
 package androidx.tv.material {
 
+  public final class BringIntoViewIfChildrenAreFocusedKt {
+  }
+
   public final class ContentColorKt {
     method public static androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.ui.graphics.Color> getLocalContentColor();
     property public static final androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.ui.graphics.Color> LocalContentColor;
diff --git a/tv/tv-material/api/restricted_current.txt b/tv/tv-material/api/restricted_current.txt
index 85bc920..0c788a4 100644
--- a/tv/tv-material/api/restricted_current.txt
+++ b/tv/tv-material/api/restricted_current.txt
@@ -1,6 +1,9 @@
 // Signature format: 4.0
 package androidx.tv.material {
 
+  public final class BringIntoViewIfChildrenAreFocusedKt {
+  }
+
   public final class ContentColorKt {
     method public static androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.ui.graphics.Color> getLocalContentColor();
     property public static final androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.ui.graphics.Color> LocalContentColor;
diff --git a/tv/tv-material/src/main/java/androidx/tv/material/BringIntoViewIfChildrenAreFocused.kt b/tv/tv-material/src/main/java/androidx/tv/material/BringIntoViewIfChildrenAreFocused.kt
new file mode 100644
index 0000000..0c4e5a8
--- /dev/null
+++ b/tv/tv-material/src/main/java/androidx/tv/material/BringIntoViewIfChildrenAreFocused.kt
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.tv.material
+
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.relocation.BringIntoViewResponder
+import androidx.compose.foundation.relocation.bringIntoViewResponder
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.composed
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.geometry.Rect
+import androidx.compose.ui.layout.onSizeChanged
+import androidx.compose.ui.platform.debugInspectorInfo
+
+@Suppress("IllegalExperimentalApiUsage") // TODO (b/233188423): Address before moving to beta
+@OptIn(ExperimentalFoundationApi::class)
+internal fun Modifier.bringIntoViewIfChildrenAreFocused(): Modifier = composed(
+    inspectorInfo = debugInspectorInfo { name = "bringIntoViewIfChildrenAreFocused" },
+    factory = {
+        var myRect: Rect = Rect.Zero
+        this
+            .onSizeChanged {
+                myRect = Rect(Offset.Zero, Offset(it.width.toFloat(), it.height.toFloat()))
+            }
+            .bringIntoViewResponder(
+                remember {
+                    object : BringIntoViewResponder {
+                        // return the current rectangle and ignoring the child rectangle received.
+                        @ExperimentalFoundationApi
+                        override fun calculateRectForParent(localRect: Rect): Rect = myRect
+
+                        // The container is not expected to be scrollable. Hence the child is
+                        // already in view with respect to the container.
+                        @ExperimentalFoundationApi
+                        override suspend fun bringChildIntoView(localRect: () -> Rect?) {}
+                    }
+                }
+            )
+    }
+)
\ No newline at end of file
diff --git a/tv/tv-material/src/main/java/androidx/tv/material/carousel/Carousel.kt b/tv/tv-material/src/main/java/androidx/tv/material/carousel/Carousel.kt
index 6200545..61500e9 100644
--- a/tv/tv-material/src/main/java/androidx/tv/material/carousel/Carousel.kt
+++ b/tv/tv-material/src/main/java/androidx/tv/material/carousel/Carousel.kt
@@ -58,6 +58,7 @@
 import androidx.compose.ui.unit.LayoutDirection
 import androidx.compose.ui.unit.dp
 import androidx.tv.material.ExperimentalTvMaterialApi
+import androidx.tv.material.bringIntoViewIfChildrenAreFocused
 import java.lang.Math.floorMod
 import kotlinx.coroutines.delay
 import kotlinx.coroutines.flow.first
@@ -111,7 +112,9 @@
         carouselState,
         focusState,
         onAutoScrollChange = { isAutoScrollActive = it })
+
     Box(modifier = modifier
+        .bringIntoViewIfChildrenAreFocused()
         .focusRequester(carouselOuterBoxFocusRequester)
         .onFocusChanged {
             focusState = it
diff --git a/tv/tv-material/src/main/java/androidx/tv/material/carousel/CarouselItem.kt b/tv/tv-material/src/main/java/androidx/tv/material/carousel/CarouselItem.kt
index 6f34d1e..c676109 100644
--- a/tv/tv-material/src/main/java/androidx/tv/material/carousel/CarouselItem.kt
+++ b/tv/tv-material/src/main/java/androidx/tv/material/carousel/CarouselItem.kt
@@ -25,14 +25,11 @@
 import androidx.compose.foundation.ExperimentalFoundationApi
 import androidx.compose.foundation.focusable
 import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.relocation.BringIntoViewRequester
-import androidx.compose.foundation.relocation.bringIntoViewRequester
 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
-import androidx.compose.runtime.rememberCoroutineScope
 import androidx.compose.runtime.setValue
 import androidx.compose.runtime.snapshotFlow
 import androidx.compose.ui.Alignment
@@ -45,7 +42,6 @@
 import androidx.tv.material.ExperimentalTvMaterialApi
 import kotlinx.coroutines.delay
 import kotlinx.coroutines.flow.first
-import kotlinx.coroutines.launch
 
 /**
  * This composable is intended for use in Carousel.
@@ -77,33 +73,19 @@
     val overlayVisible = remember { MutableTransitionState(initialState = false) }
     var focusState: FocusState? by remember { mutableStateOf(null) }
     val focusManager = LocalFocusManager.current
-    val bringIntoViewRequester = remember { BringIntoViewRequester() }
-    val coroutineScope = rememberCoroutineScope()
 
     LaunchedEffect(overlayVisible) {
         overlayVisible.onAnimationCompletion {
             // slide has loaded completely.
-            if (focusState?.isFocused == true) {
-                // Using bringIntoViewRequester here instead of in Carousel.kt as when the focusable
-                // item is within an animation, bringIntoView scrolls excessively and loses focus.
-                // b/241591211
-                // By using bringIntoView inside the snapshotFlow, we ensure that the focusable has
-                // completed animating into position.
-                bringIntoViewRequester.bringIntoView()
-                focusManager.moveFocus(FocusDirection.Enter)
-            }
+            if (focusState?.isFocused == true) { focusManager.moveFocus(FocusDirection.Enter) }
         }
     }
 
     Box(modifier = modifier
-        .bringIntoViewRequester(bringIntoViewRequester)
         .onFocusChanged {
             focusState = it
             if (it.isFocused && overlayVisible.isIdle && overlayVisible.currentState) {
-                coroutineScope.launch {
-                    bringIntoViewRequester.bringIntoView()
-                    focusManager.moveFocus(FocusDirection.Enter)
-                }
+                focusManager.moveFocus(FocusDirection.Enter)
             }
         }
         .focusable()) {
diff --git a/tv/tv-material/src/main/java/androidx/tv/material/immersivelist/ImmersiveList.kt b/tv/tv-material/src/main/java/androidx/tv/material/immersivelist/ImmersiveList.kt
index 54acbd9..e750f38 100644
--- a/tv/tv-material/src/main/java/androidx/tv/material/immersivelist/ImmersiveList.kt
+++ b/tv/tv-material/src/main/java/androidx/tv/material/immersivelist/ImmersiveList.kt
@@ -26,18 +26,14 @@
 import androidx.compose.animation.fadeIn
 import androidx.compose.animation.fadeOut
 import androidx.compose.animation.with
-import androidx.compose.foundation.ExperimentalFoundationApi
 import androidx.compose.foundation.focusable
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.BoxScope
-import androidx.compose.foundation.relocation.BringIntoViewRequester
-import androidx.compose.foundation.relocation.bringIntoViewRequester
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.Immutable
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
-import androidx.compose.runtime.rememberCoroutineScope
 import androidx.compose.runtime.setValue
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.ExperimentalComposeUiApi
@@ -46,7 +42,7 @@
 import androidx.compose.ui.focus.onFocusChanged
 import androidx.compose.ui.platform.LocalFocusManager
 import androidx.tv.material.ExperimentalTvMaterialApi
-import kotlinx.coroutines.launch
+import androidx.tv.material.bringIntoViewIfChildrenAreFocused
 
 /**
  * Immersive List consists of a list with multiple items and a background that displays content
@@ -56,13 +52,13 @@
  * [ImmersiveListBackgroundScope.AnimatedVisibility].
  *
  * @param background Composable defining the background to be displayed for a given item's
- * index.
+ * index. `listHasFocus` argument can be used to hide the background when the list is not in focus
  * @param modifier applied to Immersive List.
  * @param listAlignment Alignment of the List with respect to the Immersive List.
  * @param list composable defining the list of items that has to be rendered.
  */
 @Suppress("IllegalExperimentalApiUsage")
-@OptIn(ExperimentalComposeUiApi::class, ExperimentalFoundationApi::class)
+@OptIn(ExperimentalComposeUiApi::class)
 @ExperimentalTvMaterialApi
 @Composable
 fun ImmersiveList(
@@ -74,19 +70,8 @@
 ) {
     var currentItemIndex by remember { mutableStateOf(0) }
     var listHasFocus by remember { mutableStateOf(false) }
-    val bringIntoViewRequester = remember { BringIntoViewRequester() }
-    val coroutineScope = rememberCoroutineScope()
 
-    Box(modifier
-            .bringIntoViewRequester(bringIntoViewRequester)
-            .onFocusChanged {
-                if (it.isFocused) {
-                    coroutineScope.launch {
-                        bringIntoViewRequester.bringIntoView()
-                    }
-                }
-            }
-    ) {
+    Box(modifier.bringIntoViewIfChildrenAreFocused()) {
         ImmersiveListBackgroundScope(this).background(currentItemIndex, listHasFocus)
 
         val focusManager = LocalFocusManager.current
@@ -151,7 +136,8 @@
             enter,
             exit,
             label,
-            content)
+            content
+        )
     }
 
     /**
@@ -205,4 +191,4 @@
         return onFocusChanged { if (it.hasFocus || it.isFocused) { onFocused(index) } }
             .focusable()
     }
-}
\ No newline at end of file
+}