diff --git a/.github/workflows/release-gate.yml b/.github/workflows/release-gate.yml
index 3a34828fb..cd804c534 100644
--- a/.github/workflows/release-gate.yml
+++ b/.github/workflows/release-gate.yml
@@ -22,11 +22,11 @@ jobs:
- name: Validate release test matrix
run: python3 scripts/validate_release_cases.py
- - name: Compile debug Kotlin
- run: ./gradlew :app:compileDebugKotlin --stacktrace
+ - name: Compile debug Kotlin (playDebug)
+ run: ./gradlew :app:compilePlayDebugKotlin --stacktrace
- - name: Run debug unit tests
- run: ./gradlew :app:testDebugUnitTest --stacktrace
+ - name: Run debug unit tests (playDebug)
+ run: ./gradlew :app:testPlayDebugUnitTest --stacktrace
benchmark-smoke:
if: github.event_name == 'workflow_dispatch'
diff --git a/app/src/main/kotlin/com/arflix/tv/MainActivity.kt b/app/src/main/kotlin/com/arflix/tv/MainActivity.kt
index cf455ab72..7ea2b02a8 100644
--- a/app/src/main/kotlin/com/arflix/tv/MainActivity.kt
+++ b/app/src/main/kotlin/com/arflix/tv/MainActivity.kt
@@ -3,6 +3,7 @@ package com.arflix.tv
import android.os.Bundle
import android.view.ViewTreeObserver
import android.view.WindowManager
+import com.arflix.tv.R
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.viewModels
@@ -25,6 +26,7 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.systemBarsPadding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
@@ -133,11 +135,18 @@ class MainActivity : ComponentActivity() {
// Instead, let the splash dismiss immediately and show our Compose loading screen
installSplashScreen()
+ // Detect device type before super.onCreate().
+ // The splash screen's postSplashScreenTheme is Theme.ArflixTV.Mobile (no fullscreen)
+ // which is correct for phones/tablets. On TV we override to the fullscreen Leanback theme.
+ val initialDeviceType = detectDeviceType(this)
+ if (initialDeviceType == DeviceType.TV) {
+ setTheme(R.style.Theme_ArflixTV)
+ }
+
super.onCreate(savedInstanceState)
pendingLauncherRequest = parseLauncherRequest(intent)
// Set orientation based on device type
- val initialDeviceType = detectDeviceType(this)
requestedOrientation = when (initialDeviceType) {
DeviceType.TV -> ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
DeviceType.TABLET -> ActivityInfo.SCREEN_ORIENTATION_SENSOR
@@ -147,11 +156,30 @@ class MainActivity : ComponentActivity() {
// Keep screen on during playback
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
- // Immersive fullscreen mode
+ // All devices use edge-to-edge (setDecorFitsSystemWindows=false).
+ // TV hides the bars; mobile keeps them visible and Compose handles
+ // insets via systemBarsPadding() in the root layout.
WindowCompat.setDecorFitsSystemWindows(window, false)
- WindowInsetsControllerCompat(window, window.decorView).apply {
- systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
- hide(WindowInsetsCompat.Type.systemBars())
+ if (initialDeviceType == DeviceType.TV) {
+ WindowInsetsControllerCompat(window, window.decorView).apply {
+ systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
+ hide(WindowInsetsCompat.Type.systemBars())
+ }
+ } else {
+ // Clear any FLAG_FULLSCREEN the Leanback theme may have set
+ @Suppress("DEPRECATION")
+ window.clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN)
+ // Transparent bars — the dark app background shows through them.
+ // White (light) icons are used since the background is dark.
+ @Suppress("DEPRECATION")
+ window.statusBarColor = android.graphics.Color.TRANSPARENT
+ @Suppress("DEPRECATION")
+ window.navigationBarColor = android.graphics.Color.TRANSPARENT
+ WindowInsetsControllerCompat(window, window.decorView).apply {
+ show(WindowInsetsCompat.Type.systemBars())
+ isAppearanceLightStatusBars = false // white icons on dark bg
+ isAppearanceLightNavigationBars = false // white icons on dark bg
+ }
}
setContent {
@@ -217,9 +245,13 @@ class MainActivity : ComponentActivity() {
override fun onWindowFocusChanged(hasFocus: Boolean) {
super.onWindowFocusChanged(hasFocus)
if (hasFocus) {
- // Re-apply immersive mode when window regains focus
- WindowInsetsControllerCompat(window, window.decorView).apply {
- hide(WindowInsetsCompat.Type.systemBars())
+ // Re-apply immersive mode only for TV when window regains focus.
+ // Mobile fullscreen is managed per-screen (e.g. player).
+ val currentDeviceType = detectDeviceType(this)
+ if (currentDeviceType == DeviceType.TV) {
+ WindowInsetsControllerCompat(window, window.decorView).apply {
+ hide(WindowInsetsCompat.Type.systemBars())
+ }
}
}
}
@@ -393,6 +425,7 @@ fun ArflixApp(
Column(
modifier = Modifier
.fillMaxSize()
+ // Background fills edge-to-edge (including behind transparent bars)
.background(
brush = Brush.linearGradient(
colors = listOf(
@@ -402,6 +435,11 @@ fun ArflixApp(
)
)
)
+ // On mobile, push content between the status bar and navigation bar.
+ // Applied AFTER background so the gradient fills behind the bars.
+ // systemBarsPadding() reads live WindowInsets, so it automatically
+ // becomes 0 when the player hides the bars.
+ .then(if (isMobile) Modifier.systemBarsPadding() else Modifier)
) {
Box(modifier = Modifier.weight(1f)) {
AppNavigation(
diff --git a/app/src/main/kotlin/com/arflix/tv/ui/screens/player/PlayerScreen.kt b/app/src/main/kotlin/com/arflix/tv/ui/screens/player/PlayerScreen.kt
index b6dc293a5..fe7bb2bfe 100644
--- a/app/src/main/kotlin/com/arflix/tv/ui/screens/player/PlayerScreen.kt
+++ b/app/src/main/kotlin/com/arflix/tv/ui/screens/player/PlayerScreen.kt
@@ -180,6 +180,26 @@ fun PlayerScreen(
val latestUiState by rememberUpdatedState(uiState)
val focusManager = LocalFocusManager.current
val coroutineScope = rememberCoroutineScope()
+ val deviceType = LocalDeviceType.current
+
+ // On mobile, enable immersive fullscreen for the player and restore system bars on exit.
+ // TV is always in fullscreen so no change is needed there.
+ DisposableEffect(Unit) {
+ val activity = context as? android.app.Activity
+ val window = activity?.window
+ if (window != null && deviceType != com.arflix.tv.util.DeviceType.TV) {
+ val controller = androidx.core.view.WindowInsetsControllerCompat(window, window.decorView)
+ controller.systemBarsBehavior =
+ androidx.core.view.WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
+ controller.hide(androidx.core.view.WindowInsetsCompat.Type.systemBars())
+ }
+ onDispose {
+ if (window != null && deviceType != com.arflix.tv.util.DeviceType.TV) {
+ val controller = androidx.core.view.WindowInsetsControllerCompat(window, window.decorView)
+ controller.show(androidx.core.view.WindowInsetsCompat.Type.systemBars())
+ }
+ }
+ }
var isPlaying by remember { mutableStateOf(false) }
var isBuffering by remember { mutableStateOf(true) }
diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml
index d46af5826..f0fd858d8 100644
--- a/app/src/main/res/values/themes.xml
+++ b/app/src/main/res/values/themes.xml
@@ -5,11 +5,13 @@
- @color/background_dark
- @mipmap/ic_launcher
- 1000
- - @style/Theme.ArflixTV
+
+ - @style/Theme.ArflixTV.Mobile
- @color/background_dark
-
+
+
+
+
+
diff --git a/gradlew b/gradlew
old mode 100644
new mode 100755