Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions .github/workflows/release-gate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
54 changes: 46 additions & 8 deletions app/src/main/kotlin/com/arflix/tv/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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 {
Expand Down Expand Up @@ -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())
}
}
}
}
Expand Down Expand Up @@ -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(
Expand All @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) }
Expand Down
17 changes: 15 additions & 2 deletions app/src/main/res/values/themes.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,13 @@
<item name="windowSplashScreenBackground">@color/background_dark</item>
<item name="windowSplashScreenAnimatedIcon">@mipmap/ic_launcher</item>
<item name="windowSplashScreenAnimationDuration">1000</item>
<item name="postSplashScreenTheme">@style/Theme.ArflixTV</item>
<!-- Post-splash transitions to Mobile theme (no fullscreen).
TV overrides this in MainActivity.onCreate() before super. -->
<item name="postSplashScreenTheme">@style/Theme.ArflixTV.Mobile</item>
<item name="android:windowBackground">@color/background_dark</item>
</style>

<!-- ARVIO TV Theme - Arctic Fuse 2 Inspired -->
<!-- ARVIO TV Theme - Arctic Fuse 2 Inspired (TV devices only) -->
<style name="Theme.ArflixTV" parent="@style/Theme.Leanback">
<item name="android:windowBackground">@color/background_dark</item>
<item name="android:colorBackground">@color/background_dark</item>
Expand All @@ -19,4 +21,15 @@
<item name="android:windowFullscreen">true</item>
<item name="android:windowContentOverlay">@null</item>
</style>

<!-- ARVIO Mobile Theme - Normal app bar behaviour (phones/tablets) -->
<style name="Theme.ArflixTV.Mobile" parent="@style/Theme.ArflixTV">
<!-- Override the TV fullscreen flag so status bar and nav bar are visible -->
<item name="android:windowFullscreen">false</item>
<!-- Transparent bars — the dark app background shows through naturally -->
<item name="android:statusBarColor">@android:color/transparent</item>
<item name="android:navigationBarColor">@android:color/transparent</item>
<item name="android:windowDrawsSystemBarBackgrounds">true</item>
</style>
</resources>

Empty file modified gradlew
100644 → 100755
Empty file.
Loading