Skip to content

Commit

Permalink
Merge branch 'main' into 824-enforce-official-supported-integration-h…
Browse files Browse the repository at this point in the history
…ostname
  • Loading branch information
StaehliJ authored Dec 18, 2024
2 parents ad28c12 + 2ee6651 commit ec7bcfb
Show file tree
Hide file tree
Showing 10 changed files with 329 additions and 651 deletions.
4 changes: 2 additions & 2 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ androidx-test-runner = "1.6.2"
androidx-tv-material = "1.0.0"
coil = "3.0.4"
comscore = "6.11.1"
dependency-analysis-gradle-plugin = "2.6.0"
dependency-analysis-gradle-plugin = "2.6.1"
detekt = "1.23.7"
dokka = "2.0.0-Beta"
guava = "33.3.1-android"
Expand All @@ -26,7 +26,7 @@ junit = "4.13.2"
kotlin = "2.1.0"
kotlinx-coroutines = "1.9.0"
kotlinx-datetime = "0.6.1"
kotlinx-kover = "0.8.3"
kotlinx-kover = "0.9.0"
kotlinx-serialization = "1.7.3"
mockk = "1.13.13"
okhttp = "4.12.0"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,27 @@ fun rememberIsTouchExplorationEnabled(): Boolean {

return isTouchExplorationEnabled
}

/**
* A composable function that returns a boolean indicating whether TalkBack is currently enabled.
*
* This function uses a [DisposableEffect] to register an [AccessibilityManager.AccessibilityStateChangeListener]
* that updates the state of the composable when the accessibility state changes.
*
* @return `true` if TalkBack is enabled, `false` otherwise.
*/
@Composable
fun rememberIsTalkBackEnabled(): Boolean {
val accessibilityManager = LocalContext.current.getSystemService<AccessibilityManager>() ?: return false
val (isTalkBackEnabled, setTalkBackEnabled) = remember {
mutableStateOf(accessibilityManager.isEnabled)
}
DisposableEffect(Unit) {
val l = AccessibilityManager.AccessibilityStateChangeListener(setTalkBackEnabled)
accessibilityManager.addAccessibilityStateChangeListener(l)
onDispose {
accessibilityManager.removeAccessibilityStateChangeListener(l)
}
}
return isTalkBackEnabled
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
/*
* Copyright (c) SRG SSR. All rights reserved.
* License information is available from the LICENSE file.
*/
package ch.srgssr.pillarbox.demo.shared.ui.player

import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.text.BasicText
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.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.tooling.preview.Preview
import kotlinx.coroutines.delay
import kotlin.time.Duration
import kotlin.time.Duration.Companion.ZERO
import kotlin.time.Duration.Companion.seconds

/**
* A class that manages the visibility of controls with a delay.
*
* This class is used to control the visibility of UI elements that should be hidden
* after a certain period of inactivity. The visibility is initially set to [initialVisible]
* and the delay before hiding is set to [initialDelay].
*
* To reset the delay and keep the controls visible, call the [reset] function.
* This will restart the delay timer.
*
* @param initialVisible The initial visibility of the controls.
* @param initialDelay The initial delay before hiding the controls.
*/
class DelayedControlsVisibility internal constructor(initialVisible: Boolean, initialDelay: Duration) {
/**
* Controls visibility.
*/
var visible by mutableStateOf(initialVisible)

/**
* The [delay] after the controls become no more visible.
* Can be reset with [reset] method.
*/
var delay by mutableStateOf(initialDelay)
internal var reset by mutableStateOf<Any?>(null)

/**
* Resets the ongoing delay.
*/
fun reset() {
if (visible && delay > ZERO) {
reset = Any()
}
}
}

/**
* Remembers and controls the visibility of UI elements with a delay.
*
* Initially sets visibility to [initialVisible]. If visible, hides after [initialDelay].
*
* @param initialVisible Initial visibility. Defaults to false.
* @param initialDelay Delay before hiding, if initially visible. Defaults to 3 seconds.
* @return A [DelayedControlsVisibility] instance to control and observe visibility.
*/
@Composable
fun rememberDelayedControlsVisibility(initialVisible: Boolean = false, initialDelay: Duration = DefaultVisibilityDelay): DelayedControlsVisibility {
val visibility = remember(initialVisible, initialDelay) { DelayedControlsVisibility(initialVisible, initialDelay) }
LaunchedEffect(visibility.visible, visibility.delay, visibility.reset) {
if (visibility.visible && visibility.delay > ZERO) {
delay(visibility.delay)
visibility.visible = false
}
}
return visibility
}

/**
* Default visibility delay
*/
val DefaultVisibilityDelay = 3.seconds

@Preview
@Composable
private fun KeepVisibleDelayPreview() {
val visibility = rememberDelayedControlsVisibility(true, 2.seconds)

Column {
Box(
modifier = Modifier
.fillMaxWidth()
.aspectRatio(16 / 9f)
.clickable { visibility.visible = !visibility.visible },
) {
Box(
modifier = Modifier
.fillMaxSize()
.background(color = Color.Green)
)
androidx.compose.animation.AnimatedVisibility(
visible = visibility.visible,
modifier = Modifier.fillMaxSize(),
enter = fadeIn(),
exit = fadeOut(),
) {
Box(modifier = Modifier.background(color = Color.Black.copy(alpha = 0.5f)), contentAlignment = Alignment.Center) {
BasicText(text = "Text to hide", color = { Color.Red })
}
}
}

Row(
modifier = Modifier
.fillMaxWidth()
.background(color = Color.White),
horizontalArrangement = Arrangement.SpaceAround
) {
BasicText(
text = "Show",
modifier = Modifier.clickable {
visibility.visible = true
visibility.reset()
}
)
BasicText(
text = "Toggle",
modifier = Modifier.clickable {
visibility.visible = !visibility.visible
}
)
BasicText(
text = "Hide",
modifier = Modifier.clickable {
visibility.visible = false
}
)
BasicText(
text = "Disable",
modifier = Modifier.clickable {
visibility.delay = ZERO
}
)
BasicText(
text = "Enable",
modifier = Modifier.clickable {
visibility.delay = 2.seconds
}
)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,10 @@ import ch.srgssr.pillarbox.demo.shared.extension.onDpadEvent
import ch.srgssr.pillarbox.demo.shared.ui.components.PillarboxSlider
import ch.srgssr.pillarbox.demo.shared.ui.getFormatter
import ch.srgssr.pillarbox.demo.shared.ui.localTimeFormatter
import ch.srgssr.pillarbox.demo.shared.ui.player.DefaultVisibilityDelay
import ch.srgssr.pillarbox.demo.shared.ui.player.metrics.MetricsOverlay
import ch.srgssr.pillarbox.demo.shared.ui.player.rememberDelayedControlsVisibility
import ch.srgssr.pillarbox.demo.shared.ui.rememberIsTalkBackEnabled
import ch.srgssr.pillarbox.demo.shared.ui.settings.MetricsOverlayOptions
import ch.srgssr.pillarbox.demo.tv.ui.player.compose.controls.PlayerError
import ch.srgssr.pillarbox.demo.tv.ui.player.compose.controls.PlayerPlaybackRow
Expand All @@ -70,11 +73,9 @@ import ch.srgssr.pillarbox.ui.extension.durationAsState
import ch.srgssr.pillarbox.ui.extension.getCurrentChapterAsState
import ch.srgssr.pillarbox.ui.extension.getCurrentCreditAsState
import ch.srgssr.pillarbox.ui.extension.isCurrentMediaItemLiveAsState
import ch.srgssr.pillarbox.ui.extension.isPlayingAsState
import ch.srgssr.pillarbox.ui.extension.playerErrorAsState
import ch.srgssr.pillarbox.ui.widget.DelayedVisibilityState
import ch.srgssr.pillarbox.ui.widget.maintainVisibleOnFocus
import ch.srgssr.pillarbox.ui.widget.player.PlayerSurface
import ch.srgssr.pillarbox.ui.widget.rememberDelayedVisibilityState
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.map
import kotlinx.datetime.Instant
Expand All @@ -92,6 +93,8 @@ import kotlin.time.Duration.Companion.seconds
* @param metricsOverlayEnabled
* @param metricsOverlayOptions
*/

@Suppress("CyclomaticComplexMethod")
@Composable
fun PlayerView(
player: PillarboxExoPlayer,
Expand All @@ -100,17 +103,21 @@ fun PlayerView(
metricsOverlayOptions: MetricsOverlayOptions,
) {
val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed)
val visibilityState = rememberDelayedVisibilityState(player = player, visible = true)

val talkBackEnabled = rememberIsTalkBackEnabled()
val isPlaying by player.isPlayingAsState()
val keepControlDelay = if (!talkBackEnabled && isPlaying) DefaultVisibilityDelay else ZERO
val controlsVisibilityState = rememberDelayedControlsVisibility(initialVisible = true, keepControlDelay)

LaunchedEffect(drawerState.currentValue) {
when (drawerState.currentValue) {
DrawerValue.Closed -> visibilityState.show()
DrawerValue.Open -> visibilityState.hide()
controlsVisibilityState.visible = when (drawerState.currentValue) {
DrawerValue.Closed -> true
DrawerValue.Open -> false
}
}

BackHandler(enabled = visibilityState.isVisible) {
visibilityState.hide()
BackHandler(enabled = controlsVisibilityState.visible) {
controlsVisibilityState.visible = false
}

PlaybackSettingsDrawer(
Expand All @@ -133,7 +140,7 @@ fun PlayerView(
.onDpadEvent(
eventType = KeyEventType.KeyUp,
onEnter = {
visibilityState.show()
controlsVisibilityState.visible = true
true
},
)
Expand All @@ -146,7 +153,7 @@ fun PlayerView(
Column {
ChapterInfo(
player = player,
visibilityState = visibilityState,
controlsVisible = controlsVisibilityState.visible,
)

if (metricsOverlayEnabled) {
Expand All @@ -166,25 +173,27 @@ fun PlayerView(
}
}

if (!visibilityState.isVisible && currentCredit != null) {
if (!controlsVisibilityState.visible && currentCredit != null) {
SkipButton(
modifier = Modifier
.align(Alignment.BottomEnd)
.padding(MaterialTheme.paddings.baseline),
onClick = { player.seekTo(currentCredit?.end ?: 0L) },
)
}

AnimatedVisibility(
visible = visibilityState.isVisible,
visible = controlsVisibilityState.visible,
modifier = Modifier
.fillMaxSize()
.maintainVisibleOnFocus(delayedVisibilityState = visibilityState),
.onFocusChanged {
if (it.isFocused) {
controlsVisibilityState.reset()
}
},
) {
Box {
PlayerPlaybackRow(
player = player,
state = visibilityState,
modifier = Modifier.align(Alignment.Center),
)

Expand Down Expand Up @@ -221,7 +230,7 @@ fun PlayerView(
PlayerTimeRow(
player = player,
onSeek = { value ->
visibilityState.resetAutoHide()
controlsVisibilityState.reset()
player.seekTo(value)
},
)
Expand All @@ -236,7 +245,7 @@ fun PlayerView(
@Composable
private fun ChapterInfo(
player: Player,
visibilityState: DelayedVisibilityState,
controlsVisible: Boolean,
modifier: Modifier = Modifier,
) {
val currentMediaMetadata by player.currentMediaMetadataAsState()
Expand All @@ -255,7 +264,7 @@ private fun ChapterInfo(
}

AnimatedVisibility(
visible = visibilityState.isVisible || showChapterInfo,
visible = controlsVisible || showChapterInfo,
modifier = modifier,
enter = expandVertically(),
exit = shrinkVertically(),
Expand Down
Loading

0 comments on commit ec7bcfb

Please sign in to comment.