feat: add presentation-app code

Test: NA

Change-Id: I049551d6496e013df2a1aecd3e7fe5b76e7b2b62
diff --git a/tv/integration-tests/presentation/README.md b/tv/integration-tests/presentation/README.md
new file mode 100644
index 0000000..eb86b24
--- /dev/null
+++ b/tv/integration-tests/presentation/README.md
@@ -0,0 +1,14 @@
+# Presentation app
+
+## Setup
+
+* Uncomment the `coil` and `gson` libraries dependency additions from the `build.gradle` file.
+* Create the `data.json` file in `presentation/src/main/assets` directory and add the content from
+  this link: go/compose-tv-presentation-app-data
+
+> If you are not a Googler and want to use this app for
+> testing, you will have to create the `data.json` file by following the schema mentioned in the
+`Data.kt` file
+
+* Uncomment the function content and imports from
+  `presentation/src/main/java/androidx/tv/integration/presentation/ExternalLibs.kt` file
diff --git a/tv/integration-tests/presentation/build.gradle b/tv/integration-tests/presentation/build.gradle
index b712865..85dc901 100644
--- a/tv/integration-tests/presentation/build.gradle
+++ b/tv/integration-tests/presentation/build.gradle
@@ -32,6 +32,10 @@
     implementation(project(":compose:material3:material3"))
     implementation(project(":navigation:navigation-runtime"))
     implementation(project(":profileinstaller:profileinstaller"))
+    implementation "androidx.compose.material:material-icons-extended:1.3.1"
+
+//    implementation 'io.coil-kt:coil-compose:2.2.2'
+//    implementation 'com.google.code.gson:gson:2.8.9'
 
     implementation(project(":tv:tv-foundation"))
     implementation(project(":tv:tv-material"))
@@ -75,3 +79,8 @@
                 using project(":lifecycle:lifecycle-viewmodel-savedstate")
     }
 }
+
+repositories {
+    mavenCentral()
+    google()
+}
diff --git a/tv/integration-tests/presentation/src/main/assets/.gitignore b/tv/integration-tests/presentation/src/main/assets/.gitignore
new file mode 100644
index 0000000..114ea57
--- /dev/null
+++ b/tv/integration-tests/presentation/src/main/assets/.gitignore
@@ -0,0 +1 @@
+data.json
\ No newline at end of file
diff --git a/tv/integration-tests/presentation/src/main/java/androidx/tv/integration/presentation/AlignmentCenter.kt b/tv/integration-tests/presentation/src/main/java/androidx/tv/integration/presentation/AlignmentCenter.kt
new file mode 100644
index 0000000..9284cd6
--- /dev/null
+++ b/tv/integration-tests/presentation/src/main/java/androidx/tv/integration/presentation/AlignmentCenter.kt
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.tv.integration.presentation
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.RowScope
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+
+@Composable
+fun AlignmentCenter(
+    modifier: Modifier = Modifier,
+    horizontalAxis: Boolean = false,
+    verticalAxis: Boolean = false,
+    content: @Composable RowScope.() -> Unit
+) {
+    Row(
+        modifier = modifier.fillMaxWidth(),
+        horizontalArrangement = if (horizontalAxis) Arrangement.Center else Arrangement.Start,
+        verticalAlignment = if (verticalAxis) Alignment.CenterVertically else Alignment.Top,
+    ) {
+        content()
+    }
+}
diff --git a/tv/integration-tests/presentation/src/main/java/androidx/tv/integration/presentation/App.kt b/tv/integration-tests/presentation/src/main/java/androidx/tv/integration/presentation/App.kt
new file mode 100644
index 0000000..4078a99
--- /dev/null
+++ b/tv/integration-tests/presentation/src/main/java/androidx/tv/integration/presentation/App.kt
@@ -0,0 +1,157 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.tv.integration.presentation
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.MutableState
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+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.graphics.Color
+import androidx.compose.ui.input.key.Key
+import androidx.compose.ui.input.key.key
+import androidx.compose.ui.input.key.nativeKeyCode
+import androidx.compose.ui.input.key.onKeyEvent
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.zIndex
+import androidx.tv.foundation.lazy.list.TvLazyColumn
+import androidx.tv.material3.ExperimentalTvMaterial3Api
+import androidx.tv.material3.ModalNavigationDrawer
+
+val pageColor = Color(0xff18171a)
+
+@OptIn(ExperimentalTvMaterial3Api::class, ExperimentalComposeUiApi::class)
+@Composable
+fun App() {
+    val tabs = listOf("Home", "Movies", "Shows")
+    var selectedTabIndex by remember { mutableStateOf(0) }
+    val homePageFr = remember { FocusRequester() }
+    val moviesPageFr = remember { FocusRequester() }
+    val showsPageFr = remember { FocusRequester() }
+
+    val tabRow = @Composable {
+        AppTabRow(
+            tabs = tabs,
+            selectedTabIndex = selectedTabIndex,
+            onSelectedTabIndexChange = { selectedTabIndex = it },
+            modifier = Modifier
+                .zIndex(100f)
+                .onKeyEvent {
+                    if (it.key.nativeKeyCode == Key.DirectionDown.nativeKeyCode) {
+                        val fr = when (selectedTabIndex) {
+                            0 -> homePageFr
+                            1 -> moviesPageFr
+                            2 -> showsPageFr
+                            else -> null
+                        }
+                        fr?.requestFocus()
+                        true
+                    } else
+                    false
+                }
+        )
+    }
+
+    val homepage = @Composable {
+        TvLazyColumn(
+            modifier = Modifier
+                .fillMaxSize()
+                .focusRequester(homePageFr)
+                .background(pageColor)
+        ) {
+            item {
+                FeaturedCarousel(
+//                    modifier = Modifier.focusRequester(homePageFr)
+                )
+                AppSpacer(height = 50.dp)
+            }
+            movieCollections.forEach { movieCollection ->
+                item {
+                    AppLazyRow(
+                        title = movieCollection.label,
+                        items = movieCollection.items,
+                        drawItem = { movie, _, modifier ->
+                            ImageCard(
+                                movie,
+                                aspectRatio = 2f / 3,
+                                modifier = modifier
+                            )
+                        }
+                    )
+                    AppSpacer(height = 35.dp)
+                }
+            }
+        }
+    }
+
+    val moviesPage = @Composable {
+        TvLazyColumn(
+            modifier = Modifier
+                .fillMaxSize()
+                .background(pageColor)
+        ) {
+            item {
+                AppImmersiveList(Modifier.focusRequester(moviesPageFr))
+            }
+        }
+    }
+
+    val showsPage = @Composable {
+        TvLazyColumn(
+            modifier = Modifier
+                .fillMaxSize()
+                .background(pageColor)
+        ) {
+            item {
+                ShowsGrid(Modifier.focusRequester(showsPageFr))
+            }
+        }
+    }
+
+    val activePage: MutableState<(@Composable () -> Unit)> = remember(selectedTabIndex) {
+        mutableStateOf(
+            when (selectedTabIndex) {
+                0 -> homepage
+                1 -> moviesPage
+                2 -> showsPage
+                else -> homepage
+            }
+        )
+    }
+
+    ModalNavigationDrawer(
+        drawerContent = {
+            Sidebar(
+                selectedIndex = selectedTabIndex,
+                onIndexChange = { selectedTabIndex = it }
+            )
+        }
+    ) {
+        Box(modifier = Modifier.fillMaxSize()) {
+            activePage.value()
+            tabRow()
+        }
+    }
+}
diff --git a/tv/integration-tests/presentation/src/main/java/androidx/tv/integration/presentation/AppButton.kt b/tv/integration-tests/presentation/src/main/java/androidx/tv/integration/presentation/AppButton.kt
new file mode 100644
index 0000000..9857021
--- /dev/null
+++ b/tv/integration-tests/presentation/src/main/java/androidx/tv/integration/presentation/AppButton.kt
@@ -0,0 +1,71 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.tv.integration.presentation
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.border
+import androidx.compose.foundation.focusable
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.Icon
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.focus.onFocusChanged
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import androidx.tv.material3.Text
+
+@Composable
+fun AppButton(
+    text: String,
+    icon: ImageVector,
+    modifier: Modifier = Modifier,
+) {
+    var isFocused by remember { mutableStateOf(false) }
+
+    Row(
+        modifier = modifier
+            .border(2.dp, if (isFocused) Color.White else Color.Transparent, RoundedCornerShape(50))
+            .padding(4.dp)
+            .background(Color.White.copy(alpha = 0.9f), RoundedCornerShape(50))
+            .padding(top = 5.dp, bottom = 5.dp, start = 10.dp, end = 15.dp)
+            .onFocusChanged {
+                isFocused = it.isFocused
+            }
+            .focusable(),
+        horizontalArrangement = Arrangement.spacedBy(0.dp),
+        verticalAlignment = Alignment.CenterVertically
+    ) {
+        Icon(
+            imageVector = icon,
+            contentDescription = null,
+            modifier = Modifier.size(25.dp),
+            tint = Color.Black
+        )
+        Text(text = text, fontSize = 12.sp)
+    }
+}
diff --git a/tv/integration-tests/presentation/src/main/java/androidx/tv/integration/presentation/AppImmersiveList.kt b/tv/integration-tests/presentation/src/main/java/androidx/tv/integration/presentation/AppImmersiveList.kt
new file mode 100644
index 0000000..b087bc3
--- /dev/null
+++ b/tv/integration-tests/presentation/src/main/java/androidx/tv/integration/presentation/AppImmersiveList.kt
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.tv.integration.presentation
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.height
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+import androidx.tv.material3.ExperimentalTvMaterial3Api
+import androidx.tv.material3.ImmersiveList
+
+@OptIn(ExperimentalTvMaterial3Api::class)
+@Composable
+fun AppImmersiveList(modifier: Modifier = Modifier) {
+    ImmersiveList(
+        modifier = modifier
+            .height(500.dp)
+            .fillMaxSize(),
+        background = { index, _ ->
+            Box(modifier = Modifier.fillMaxSize()) {
+                val movie = topPicksForYou[index]
+                LandscapeImageBackground(movie)
+            }
+        }
+    ) {
+        AppLazyRow(
+            title = "",
+            items = topPicksForYou,
+            modifier = Modifier
+        ) { movie, index, modifier ->
+            ImageCard(
+                movie,
+//                aspectRatio = 2f / 3,
+                modifier = modifier.immersiveListItem(index)
+            )
+        }
+    }
+}
diff --git a/tv/integration-tests/presentation/src/main/java/androidx/tv/integration/presentation/AppLazyRow.kt b/tv/integration-tests/presentation/src/main/java/androidx/tv/integration/presentation/AppLazyRow.kt
new file mode 100644
index 0000000..455dc59
--- /dev/null
+++ b/tv/integration-tests/presentation/src/main/java/androidx/tv/integration/presentation/AppLazyRow.kt
@@ -0,0 +1,71 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.tv.integration.presentation
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.padding
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.focus.onFocusChanged
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import androidx.tv.foundation.lazy.list.TvLazyRow
+import androidx.tv.material3.Text
+
+@Composable
+fun AppLazyRow(
+    title: String,
+    items: List<Movie>,
+    modifier: Modifier = Modifier,
+    drawItem: @Composable (movie: Movie, index: Int, modifier: Modifier) -> Unit
+) {
+    val paddingLeft = 58.dp
+    var hasFocus by remember { mutableStateOf(false) }
+
+    Column(modifier = modifier.onFocusChanged { hasFocus = it.hasFocus }) {
+        Text(
+            text = title,
+            color = if (hasFocus) Color.White else Color.White.copy(alpha = 0.8f),
+            fontSize = 14.sp,
+            modifier = Modifier.padding(start = paddingLeft)
+        )
+
+        AppSpacer(height = 12.dp)
+
+        TvLazyRow(
+            contentPadding = PaddingValues(horizontal = paddingLeft),
+            horizontalArrangement = Arrangement.spacedBy(20.dp)
+        ) {
+            items.forEachIndexed { index, movie ->
+                item {
+                    drawItem(
+                        movie = movie,
+                        index = index,
+                        modifier = Modifier
+                    )
+                }
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/tv/integration-tests/presentation/src/main/java/androidx/tv/integration/presentation/AppSpacer.kt b/tv/integration-tests/presentation/src/main/java/androidx/tv/integration/presentation/AppSpacer.kt
new file mode 100644
index 0000000..c8a57ec
--- /dev/null
+++ b/tv/integration-tests/presentation/src/main/java/androidx/tv/integration/presentation/AppSpacer.kt
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.tv.integration.presentation
+
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.width
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.Dp
+
+@Composable
+fun AppSpacer(
+    width: Dp? = null,
+    height: Dp? = null
+) {
+    var modifier: Modifier = Modifier
+    if (width != null) {
+        modifier = modifier.width(width)
+    }
+    if (height != null) {
+        modifier = modifier.height(height)
+    }
+    Spacer(modifier)
+}
diff --git a/tv/integration-tests/presentation/src/main/java/androidx/tv/integration/presentation/AppTabRow.kt b/tv/integration-tests/presentation/src/main/java/androidx/tv/integration/presentation/AppTabRow.kt
new file mode 100644
index 0000000..b1a566e
--- /dev/null
+++ b/tv/integration-tests/presentation/src/main/java/androidx/tv/integration/presentation/AppTabRow.kt
@@ -0,0 +1,80 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.tv.integration.presentation
+
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.width
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import androidx.tv.material3.ExperimentalTvMaterial3Api
+import androidx.tv.material3.LocalContentColor
+import androidx.tv.material3.Tab
+import androidx.tv.material3.TabDefaults
+import androidx.tv.material3.TabRow
+import androidx.tv.material3.Text
+
+@OptIn(ExperimentalTvMaterial3Api::class)
+@Composable
+fun AppTabRow(
+    tabs: List<String>,
+    selectedTabIndex: Int,
+    onSelectedTabIndexChange: (Int) -> Unit,
+    modifier: Modifier = Modifier
+) {
+    AlignmentCenter(horizontalAxis = true) {
+        FocusGroup {
+            TabRow(
+                selectedTabIndex = selectedTabIndex,
+                separator = { Spacer(modifier = Modifier.width(4.dp)) },
+                modifier = modifier.padding(top = 20.dp),
+//                indicator = @Composable { tabPositions ->
+//                    tabPositions.getOrNull(selectedTabIndex)?.let {
+//                        TabRowDefaults.PillIndicator(
+//                            currentTabPosition = it,
+//                            inactiveColor = Color(0xFFE5E1E6),
+//                        )
+//                    }
+//                }
+            ) {
+                tabs.forEachIndexed { index, tabLabel ->
+                    val tabModifier = Modifier.restorableFocus()
+                    val firstTabModifier = Modifier.initiallyFocused()
+                    val isFirstTab = index == 0
+
+                    Tab(
+                        selected = selectedTabIndex == index,
+                        onFocus = { onSelectedTabIndexChange(index) },
+                        colors = TabDefaults.pillIndicatorTabColors(
+                            contentColor = LocalContentColor.current,
+//                            selectedContentColor = Color(0xFF313033),
+                        ),
+                        modifier = if (isFirstTab) firstTabModifier else tabModifier
+                    ) {
+                        Text(
+                            text = tabLabel,
+                            fontSize = 12.sp,
+                            modifier = Modifier.padding(horizontal = 16.dp, vertical = 6.dp)
+                        )
+                    }
+                }
+            }
+        }
+    }
+}
diff --git a/tv/integration-tests/presentation/src/main/java/androidx/tv/integration/presentation/BringIntoViewIfChildrenAreFocused.kt b/tv/integration-tests/presentation/src/main/java/androidx/tv/integration/presentation/BringIntoViewIfChildrenAreFocused.kt
new file mode 100644
index 0000000..2ad15c7
--- /dev/null
+++ b/tv/integration-tests/presentation/src/main/java/androidx/tv/integration/presentation/BringIntoViewIfChildrenAreFocused.kt
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.tv.integration.presentation
+
+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
+
+@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/integration-tests/presentation/src/main/java/androidx/tv/integration/presentation/Data.kt b/tv/integration-tests/presentation/src/main/java/androidx/tv/integration/presentation/Data.kt
new file mode 100644
index 0000000..afe48bf
--- /dev/null
+++ b/tv/integration-tests/presentation/src/main/java/androidx/tv/integration/presentation/Data.kt
@@ -0,0 +1,63 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.tv.integration.presentation
+
+data class MovieImage(val url: String, val aspect: String)
+data class Movie(val name: String, val images: List<MovieImage>, val root: String)
+data class MovieCollection(val label: String, val items: List<Movie>)
+data class RootData(
+    val data: List<MovieCollection>,
+    val featuredCarouselMovies: List<String>,
+    val commonDescription: String
+)
+
+var movieCollections = listOf<MovieCollection>()
+var topPicksForYou = listOf<Movie>()
+var allMovies = listOf<Movie>()
+var featuredCarouselMovies = listOf<Movie>()
+var commonDescription = ""
+
+val Movie.description: String
+    get() = commonDescription
+
+fun getMovieImageUrl(
+    movie: Movie,
+    aspect: String = "orientation/backdrop_16x9"
+): String =
+    movie
+        .images
+        .find { image -> image.aspect == aspect }?.url ?: movie.images.first().url
+
+fun initializeData(rootData: RootData) {
+    commonDescription = rootData.commonDescription
+    movieCollections = rootData.data
+    topPicksForYou = movieCollections[3].items
+    allMovies = movieCollections.flatMap { it.items }.reversed()
+    featuredCarouselMovies = run {
+        val titles = rootData.featuredCarouselMovies
+        val previousTitles = mutableListOf<String>()
+
+        movieCollections.flatMap { it.items }.filter {
+            if (previousTitles.contains(it.name)) {
+                false
+            } else {
+                previousTitles.add(it.name)
+                titles.contains(it.name)
+            }
+        }
+    }
+}
diff --git a/tv/integration-tests/presentation/src/main/java/androidx/tv/integration/presentation/ExternalLibs.kt b/tv/integration-tests/presentation/src/main/java/androidx/tv/integration/presentation/ExternalLibs.kt
new file mode 100644
index 0000000..4a497f2
--- /dev/null
+++ b/tv/integration-tests/presentation/src/main/java/androidx/tv/integration/presentation/ExternalLibs.kt
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.tv.integration.presentation
+
+import android.util.Log
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.layout.ContentScale
+// import coil.compose.AsyncImage
+// import com.google.gson.Gson
+
+fun getRootDataFromJson(jsonData: String): RootData {
+    Log.d("LOL", "getRootDataFromJson: $jsonData")
+//    return Gson().fromJson(jsonData, RootData::class.java)
+    return RootData(listOf(), listOf(), "")
+}
+
+@Composable
+fun AppAsyncImage(
+    imageUrl: String,
+    modifier: Modifier = Modifier,
+    contentScale: ContentScale = ContentScale.Fit,
+    alignment: Alignment = Alignment.Center,
+    contentDescription: String? = null
+) {
+    Log.d(
+        "LOL",
+        "AppAsyncImage: $imageUrl, $modifier, $contentScale, $alignment, $contentDescription"
+    )
+//    AsyncImage(
+//        model = imageUrl,
+//        contentScale = contentScale,
+//        alignment = alignment,
+//        modifier = modifier,
+//        contentDescription = contentDescription
+//    )
+}
\ No newline at end of file
diff --git a/tv/integration-tests/presentation/src/main/java/androidx/tv/integration/presentation/FeaturedCarousel.kt b/tv/integration-tests/presentation/src/main/java/androidx/tv/integration/presentation/FeaturedCarousel.kt
new file mode 100644
index 0000000..33e5b16
--- /dev/null
+++ b/tv/integration-tests/presentation/src/main/java/androidx/tv/integration/presentation/FeaturedCarousel.kt
@@ -0,0 +1,121 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.tv.integration.presentation
+
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.width
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.outlined.ArrowRight
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import androidx.tv.material3.Carousel
+import androidx.tv.material3.CarouselDefaults
+import androidx.tv.material3.CarouselScope
+import androidx.tv.material3.CarouselState
+import androidx.tv.material3.ExperimentalTvMaterial3Api
+import androidx.tv.material3.Text
+
+@OptIn(ExperimentalTvMaterial3Api::class)
+@Composable
+fun FeaturedCarousel(
+    movies: List<Movie> = featuredCarouselMovies,
+    modifier: Modifier = Modifier
+) {
+    val carouselState: CarouselState = remember { CarouselState() }
+    val slidesCount = movies.size
+
+    Carousel(
+        itemCount = slidesCount,
+        carouselState = carouselState,
+        modifier = modifier
+            .height(340.dp)
+            .fillMaxWidth(),
+        carouselIndicator = {
+            CarouselDefaults.IndicatorRow(
+                itemCount = slidesCount,
+                activeItemIndex = carouselState.activeItemIndex,
+                modifier = Modifier
+                    .align(Alignment.BottomEnd)
+                    .padding(end = 58.dp, bottom = 16.dp),
+            )
+        }
+    ) { itemIndex ->
+        val movie = movies[itemIndex]
+
+        CarouselSlide(
+            title = movie.name,
+            description = movie.description,
+            background = {
+                LandscapeImageBackground(movie)
+            },
+            actions = {
+                AppButton(
+                    text = "Watch on YouTube",
+                    icon = Icons.Outlined.ArrowRight,
+                )
+            },
+        )
+    }
+}
+
+@OptIn(ExperimentalTvMaterial3Api::class)
+@Composable
+private fun CarouselScope.CarouselSlide(
+    title: String,
+    description: String,
+    background: @Composable () -> Unit,
+    actions: @Composable () -> Unit
+) {
+    CarouselItem(
+        background = {
+            background()
+        },
+        modifier = Modifier
+    ) {
+        Column(
+            modifier = Modifier.padding(start = 58.dp, top = 150.dp)
+        ) {
+            Text(
+                text = title,
+                color = Color.White,
+                fontSize = 40.sp
+            )
+
+            AppSpacer(height = 16.dp)
+
+            Text(
+                text = description,
+                color = Color.White,
+                fontSize = 16.sp,
+                lineHeight = 24.sp,
+                modifier = Modifier.width(500.dp),
+            )
+
+            AppSpacer(height = 15.dp)
+
+            actions()
+        }
+    }
+}
diff --git a/tv/integration-tests/presentation/src/main/java/androidx/tv/integration/presentation/FocusGroup.kt b/tv/integration-tests/presentation/src/main/java/androidx/tv/integration/presentation/FocusGroup.kt
new file mode 100644
index 0000000..928dcf9
--- /dev/null
+++ b/tv/integration-tests/presentation/src/main/java/androidx/tv/integration/presentation/FocusGroup.kt
@@ -0,0 +1,179 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.tv.integration.presentation
+
+import android.annotation.SuppressLint
+import android.util.Log
+import androidx.compose.foundation.focusable
+import androidx.compose.foundation.layout.Box
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.MutableState
+import androidx.compose.runtime.Stable
+import androidx.compose.runtime.currentCompositeKeyHash
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberUpdatedState
+import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.ui.ExperimentalComposeUiApi
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.focus.FocusDirection
+import androidx.compose.ui.focus.FocusRequester
+import androidx.compose.ui.focus.focusRequester
+import androidx.compose.ui.focus.onFocusChanged
+import androidx.compose.ui.platform.LocalFocusManager
+
+/**
+ * Composable container that provides modifier extensions to allow focus to be restored to the
+ * element that was previously focused within the TvFocusGroup.
+ *
+ * @param modifier the modifier to apply to this group.
+ * @param content the content that is present within the group and can use focus-group modifier
+ * extensions.
+ */
+@OptIn(ExperimentalComposeUiApi::class)
+@Composable
+fun FocusGroup(
+    modifier: Modifier = Modifier,
+    content: @Composable FocusGroupScope.() -> Unit
+) {
+    val focusManager = LocalFocusManager.current
+    val focusGroupKeyHash = currentCompositeKeyHash
+
+    // TODO: Is this the intended way to call rememberSaveable
+    //  with key set to parentHash?
+    val previousFocusedItemHash: MutableState<Int?> = rememberSaveable(
+        key = focusGroupKeyHash.toString()
+    ) {
+        mutableStateOf(null)
+    }
+
+    val state = FocusGroupState(previousFocusedItemHash = previousFocusedItemHash)
+
+    Box(
+        modifier = modifier
+            .onFocusChanged {
+                if (it.isFocused) {
+                    if (state.noRecordedState()) {
+                        focusManager.moveFocus(FocusDirection.Enter)
+                    } else {
+                        if (state.focusRequester != FocusRequester.Default) {
+                            try {
+                                state.focusRequester.requestFocus()
+                            } catch (e: Exception) {
+                                Log.w("TvFocusGroup", "TvFocusGroup: Failed to request focus", e)
+                            }
+                        } else {
+                            focusManager.moveFocus(FocusDirection.Enter)
+                        }
+                    }
+                }
+            }
+            .focusable(),
+        content = { FocusGroupScope(state).content() }
+    )
+}
+
+/**
+ * Scope containing the modifier extensions to be used within [FocusGroup].
+ */
+class FocusGroupScope internal constructor(private val state: FocusGroupState) {
+    private var currentFocusableIdIndex = 0
+
+    private fun generateUniqueFocusableId(): Int = currentFocusableIdIndex++
+
+    /**
+     * Modifier that records if the item was in focus before it moved out of the group. When focus
+     * enters the [FocusGroup], the item will be returned focus.
+     */
+    @SuppressLint("ComposableModifierFactory")
+    @Composable
+    fun Modifier.restorableFocus(): Modifier =
+        this.restorableFocus(focusId = rememberSaveable { generateUniqueFocusableId() })
+
+    /**
+     * Modifier that marks the current composable as the item to gain focus initially when focus
+     * enters the [FocusGroup]. When focus enters the [FocusGroup], the item will be returned focus.
+     */
+    @SuppressLint("ComposableModifierFactory")
+    @Composable
+    fun Modifier.initiallyFocused(): Modifier {
+        val focusId = rememberSaveable { generateUniqueFocusableId() }
+        if (state.noRecordedState()) {
+            state.recordFocusedItemHash(focusId)
+        }
+        return this.restorableFocus(focusId)
+    }
+
+    @SuppressLint("ComposableModifierFactory")
+    @OptIn(ExperimentalComposeUiApi::class)
+    @Composable
+    private fun Modifier.restorableFocus(focusId: Int): Modifier {
+        val focusRequester = remember { FocusRequester() }
+        var isFocused = remember { false }
+        val isCurrentlyFocused by rememberUpdatedState(isFocused)
+        val focusManager = LocalFocusManager.current
+        state.associatedWith(focusId, focusRequester)
+        DisposableEffect(Unit) {
+            onDispose {
+                state.clearDisposedFocusRequester(focusId)
+                if (isCurrentlyFocused) {
+                    focusManager.moveFocus(FocusDirection.Exit)
+                    focusManager.moveFocus(FocusDirection.Enter)
+                }
+            }
+        }
+
+        return this
+            .focusRequester(focusRequester)
+            .onFocusChanged {
+                isFocused = it.isFocused || it.hasFocus
+                if (isFocused) {
+                    state.recordFocusedItemHash(focusId)
+                    state.associatedWith(focusId, focusRequester)
+                }
+            }
+    }
+}
+
+@Stable
+internal class FocusGroupState(
+    private var previousFocusedItemHash: MutableState<Int?>
+) {
+    internal var focusRequester: FocusRequester = FocusRequester.Default
+        private set
+
+    internal fun recordFocusedItemHash(itemHash: Int) {
+        previousFocusedItemHash.value = itemHash
+    }
+
+    internal fun clearDisposedFocusRequester(itemHash: Int) {
+        if (previousFocusedItemHash.value == itemHash) {
+            focusRequester = FocusRequester.Default
+        }
+    }
+
+    internal fun associatedWith(itemHash: Int, focusRequester: FocusRequester) {
+        if (previousFocusedItemHash.value == itemHash) {
+            this.focusRequester = focusRequester
+        }
+    }
+
+    internal fun noRecordedState(): Boolean =
+        previousFocusedItemHash.value == null && focusRequester == FocusRequester.Default
+}
diff --git a/tv/integration-tests/presentation/src/main/java/androidx/tv/integration/presentation/ImageCard.kt b/tv/integration-tests/presentation/src/main/java/androidx/tv/integration/presentation/ImageCard.kt
new file mode 100644
index 0000000..450a9a0
--- /dev/null
+++ b/tv/integration-tests/presentation/src/main/java/androidx/tv/integration/presentation/ImageCard.kt
@@ -0,0 +1,92 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.tv.integration.presentation
+
+import androidx.compose.animation.animateColorAsState
+import androidx.compose.animation.core.animateFloatAsState
+import androidx.compose.foundation.border
+import androidx.compose.foundation.focusable
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.aspectRatio
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.draw.scale
+import androidx.compose.ui.focus.onFocusChanged
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import androidx.tv.material3.Text
+
+@Composable
+fun ImageCard(
+    movie: Movie,
+    aspectRatio: Float = 16f / 9,
+    customCardWidth: Dp? = null,
+    modifier: Modifier = Modifier,
+) {
+    val aspect = if (aspectRatio == 16f / 9)
+        "orientation/vod_art_16x9"
+    else
+        "orientation/vod_art_2x3"
+    val scaleMax = if (aspectRatio == 16f / 9) 1.1f else 1.025f
+    val cardWidth = customCardWidth ?: if (aspectRatio == 16f / 9) 200.dp else 172.dp
+
+    var isFocused by remember { mutableStateOf(false) }
+    val shape = RoundedCornerShape(12.dp)
+    val borderColor by animateColorAsState(
+        targetValue = if (isFocused) Color.White.copy(alpha = 0.8f) else Color.Transparent
+    )
+    val scale by animateFloatAsState(targetValue = if (isFocused) scaleMax else 1f)
+
+    Column(
+        modifier = Modifier
+            .width(cardWidth)
+            .scale(scale)
+    ) {
+        Box(
+            modifier = modifier
+                .fillMaxWidth()
+                .aspectRatio(aspectRatio)
+                .border(2.dp, borderColor, shape)
+                .clip(shape)
+                .onFocusChanged { isFocused = it.isFocused }
+                .focusable()
+        ) {
+            AppAsyncImage(imageUrl = getMovieImageUrl(movie, aspect = aspect))
+        }
+        androidx.compose.foundation.layout.Spacer(modifier = Modifier.height(6.dp))
+        Text(
+            text = movie.name,
+            color = Color.White.copy(alpha = 0.8f),
+            fontSize = 12.sp,
+            textAlign = TextAlign.Center,
+            modifier = Modifier.fillMaxWidth()
+        )
+    }
+}
diff --git a/tv/integration-tests/presentation/src/main/java/androidx/tv/integration/presentation/LandscapeImageBackground.kt b/tv/integration-tests/presentation/src/main/java/androidx/tv/integration/presentation/LandscapeImageBackground.kt
new file mode 100644
index 0000000..18a9b33
--- /dev/null
+++ b/tv/integration-tests/presentation/src/main/java/androidx/tv/integration/presentation/LandscapeImageBackground.kt
@@ -0,0 +1,85 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.tv.integration.presentation
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Brush
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.layout.onGloballyPositioned
+
+@Composable
+fun LandscapeImageBackground(movie: Movie, aspect: String = "orientation/iconic_16x9") {
+    val navigationGradient = Brush.verticalGradient(
+        colors = listOf(pageColor, Color.Transparent),
+        startY = 0f,
+        endY = 200f
+    )
+    var height by remember {
+        mutableStateOf(0f)
+    }
+
+    val navigationGradientBottom = Brush.verticalGradient(
+        colors = listOf(
+            Color.Transparent,
+            pageColor
+        ),
+        startY = 50f,
+        endY = height,
+    )
+
+    val horizontalGradient = Brush.horizontalGradient(
+        colors = listOf(pageColor, Color.Transparent),
+        startX = 1400f,
+        endX = 900f,
+    )
+
+    Box(
+        modifier = Modifier
+            .fillMaxSize()
+            .onGloballyPositioned { height = it.size.height.toFloat() }
+    ) {
+        AppAsyncImage(
+            imageUrl = getMovieImageUrl(
+                movie = movie,
+                aspect = aspect
+            ),
+            contentScale = ContentScale.FillWidth,
+            alignment = Alignment.Center,
+            modifier = Modifier.fillMaxWidth(),
+            contentDescription = null
+        )
+
+        Box(
+            modifier = Modifier
+                .matchParentSize()
+                .background(navigationGradientBottom)
+                .background(navigationGradient)
+                .background(horizontalGradient)
+        )
+    }
+}
diff --git a/tv/integration-tests/presentation/src/main/java/androidx/tv/integration/presentation/MainActivity.kt b/tv/integration-tests/presentation/src/main/java/androidx/tv/integration/presentation/MainActivity.kt
index 6306306..83c3ad1 100644
--- a/tv/integration-tests/presentation/src/main/java/androidx/tv/integration/presentation/MainActivity.kt
+++ b/tv/integration-tests/presentation/src/main/java/androidx/tv/integration/presentation/MainActivity.kt
@@ -19,13 +19,19 @@
 import android.os.Bundle
 import androidx.activity.ComponentActivity
 import androidx.activity.compose.setContent
-import androidx.compose.foundation.text.BasicText
 
 class MainActivity : ComponentActivity() {
     override fun onCreate(savedInstanceState: Bundle?) {
+        // Create the "data.json" file in "presentation/src/main/assets" directory and add the
+        // content from this link: go/compose-tv-presentation-app-data
+        val jsonData = assets.readAssetsFile("data.json")
+        val deserializedData = getRootDataFromJson(jsonData)
+
+        initializeData(deserializedData)
+
         super.onCreate(savedInstanceState)
         setContent {
-            BasicText("Hi")
+            App()
         }
     }
 }
\ No newline at end of file
diff --git a/tv/integration-tests/presentation/src/main/java/androidx/tv/integration/presentation/ShowsGrid.kt b/tv/integration-tests/presentation/src/main/java/androidx/tv/integration/presentation/ShowsGrid.kt
new file mode 100644
index 0000000..4dfbf0b
--- /dev/null
+++ b/tv/integration-tests/presentation/src/main/java/androidx/tv/integration/presentation/ShowsGrid.kt
@@ -0,0 +1,117 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.tv.integration.presentation
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.OutlinedTextField
+import androidx.compose.material3.TextFieldDefaults
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.unit.dp
+import androidx.tv.foundation.lazy.grid.TvGridCells
+import androidx.tv.foundation.lazy.grid.TvLazyHorizontalGrid
+import androidx.tv.material3.Text
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun ShowsGrid(modifier: Modifier = Modifier) {
+    var keyword by remember { mutableStateOf("") }
+    val movies by remember(keyword) {
+        mutableStateOf(allMovies.filter { movie -> movie.name.contains(keyword) })
+    }
+    Column(
+        modifier = Modifier
+            .height(520.dp)
+            .padding(top = 70.dp),
+    ) {
+        Box(
+            modifier = Modifier
+                .padding(horizontal = 58.dp)
+                .fillMaxWidth()
+        ) {
+            OutlinedTextField(
+                value = keyword,
+                onValueChange = { keyword = it },
+                placeholder = {
+                    Text(text = "Search", color = Color.White)
+                },
+                modifier = modifier
+                    .fillMaxWidth()
+                    .align(Alignment.Center),
+                colors = TextFieldDefaults.outlinedTextFieldColors(
+                    focusedTextColor = Color.White,
+                    unfocusedTextColor = Color.White,
+                    focusedBorderColor = Color.White,
+                    unfocusedBorderColor = Color.White.copy(alpha = 0.5f)
+                )
+            )
+        }
+
+        AppSpacer(height = 20.dp)
+
+        if (movies.isEmpty()) {
+            AppSpacer(height = 20.dp)
+            Box(modifier = Modifier.fillMaxWidth()) {
+                Text(
+                    text = "No movies matched",
+                    modifier = Modifier.align(Alignment.Center),
+                    color = Color.White,
+                )
+            }
+        }
+
+        FocusGroup {
+            TvLazyHorizontalGrid(
+                rows = TvGridCells.Fixed(3),
+                contentPadding = PaddingValues(horizontal = 58.dp),
+                verticalArrangement = Arrangement.spacedBy(10.dp),
+                modifier = Modifier
+                    .fillMaxSize()
+                    .bringIntoViewIfChildrenAreFocused(),
+            ) {
+                items(movies.size) {
+                    val movie = movies[it]
+                    val isFirstItem = it == 0
+                    val itemModifier = Modifier.restorableFocus()
+                    val firstItemModifier = Modifier.initiallyFocused()
+
+                    Box(modifier = Modifier.padding(end = 30.dp)) {
+                        ImageCard(
+                            movie,
+                            customCardWidth = 150.dp,
+                            modifier = if (isFirstItem) firstItemModifier else itemModifier
+                        )
+                    }
+                }
+            }
+        }
+    }
+}
diff --git a/tv/integration-tests/presentation/src/main/java/androidx/tv/integration/presentation/Sidebar.kt b/tv/integration-tests/presentation/src/main/java/androidx/tv/integration/presentation/Sidebar.kt
new file mode 100644
index 0000000..882b27b
--- /dev/null
+++ b/tv/integration-tests/presentation/src/main/java/androidx/tv/integration/presentation/Sidebar.kt
@@ -0,0 +1,131 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.tv.integration.presentation
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.focusable
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxHeight
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.offset
+import androidx.compose.foundation.layout.width
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.outlined.Home
+import androidx.compose.material.icons.outlined.Movie
+import androidx.compose.material.icons.outlined.Tv
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.IconButtonDefaults
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.focus.FocusRequester
+import androidx.compose.ui.focus.focusRequester
+import androidx.compose.ui.focus.onFocusChanged
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.unit.dp
+import androidx.tv.material3.ExperimentalTvMaterial3Api
+import androidx.tv.material3.Icon
+
+@OptIn(ExperimentalTvMaterial3Api::class)
+@Composable
+fun Sidebar(
+    selectedIndex: Int,
+    onIndexChange: (index: Int) -> Unit,
+) {
+    val fr = remember { FocusRequester() }
+    val drawIcon: @Composable (
+        imageVector: ImageVector,
+        index: Int,
+        modifier: Modifier
+    ) -> Unit =
+        { imageVector, index, modifier ->
+            var isFocused by remember { mutableStateOf(false) }
+            val isSelected = selectedIndex == index
+
+            IconButton(
+                onClick = { },
+                modifier = modifier
+                    .onFocusChanged {
+                        isFocused = it.isFocused
+                        if (it.isFocused) {
+                            onIndexChange(index)
+                        }
+                    }
+                    .focusRequester(if (index == 0) fr else FocusRequester()),
+                colors = IconButtonDefaults.filledIconButtonColors(
+                    containerColor =
+                    if (isSelected && isFocused)
+                        Color.White
+                    else
+                        Color.Transparent,
+                )
+            ) {
+                Box(modifier = Modifier) {
+                    Icon(
+                        imageVector = imageVector,
+                        tint = if (isSelected && isFocused) pageColor else Color.White,
+                        contentDescription = null,
+                    )
+                    if (isSelected) {
+                        Box(
+                            modifier = Modifier
+                                .width(10.dp)
+                                .height(3.dp)
+                                .offset(y = 4.dp)
+                                .align(Alignment.BottomCenter)
+                                .background(Color.Red)
+                        )
+                    }
+                }
+            }
+        }
+
+    FocusGroup {
+        Column(
+            modifier = Modifier
+                .width(60.dp)
+                .fillMaxHeight()
+                .background(pageColor)
+                .focusable(false),
+            horizontalAlignment = Alignment.CenterHorizontally,
+            verticalArrangement = Arrangement.Center,
+        ) {
+            drawIcon(
+                imageVector = Icons.Outlined.Home,
+                index = 0,
+                modifier = Modifier.initiallyFocused(),
+            )
+            drawIcon(
+                imageVector = Icons.Outlined.Movie,
+                index = 1,
+                modifier = Modifier.restorableFocus(),
+            )
+            drawIcon(
+                imageVector = Icons.Outlined.Tv,
+                index = 2,
+                modifier = Modifier.restorableFocus(),
+            )
+        }
+    }
+}
diff --git a/tv/integration-tests/presentation/src/main/java/androidx/tv/integration/presentation/readAssetsFile.kt b/tv/integration-tests/presentation/src/main/java/androidx/tv/integration/presentation/readAssetsFile.kt
new file mode 100644
index 0000000..18708dd
--- /dev/null
+++ b/tv/integration-tests/presentation/src/main/java/androidx/tv/integration/presentation/readAssetsFile.kt
@@ -0,0 +1,22 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.tv.integration.presentation
+
+import android.content.res.AssetManager
+
+fun AssetManager.readAssetsFile(fileName: String): String =
+    open(fileName).bufferedReader().use { it.readText() }
\ No newline at end of file