diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 00000000..f2147e70
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,6 @@
+root = true
+
+[*.{kt,kts}]
+#Custom configuration
+ktlint_disabled_rules = no-wildcard-imports
+insert_final_newline = true
diff --git a/androidenhancedvideoplayer/src/androidTest/java/com/profusion/androidenhancedvideoplayer/test/EnhancedVideoPlayerTest.kt b/androidenhancedvideoplayer/src/androidTest/java/com/profusion/androidenhancedvideoplayer/test/EnhancedVideoPlayerTest.kt
index de3d7491..5ad0e1a3 100644
--- a/androidenhancedvideoplayer/src/androidTest/java/com/profusion/androidenhancedvideoplayer/test/EnhancedVideoPlayerTest.kt
+++ b/androidenhancedvideoplayer/src/androidTest/java/com/profusion/androidenhancedvideoplayer/test/EnhancedVideoPlayerTest.kt
@@ -1,9 +1,12 @@
package com.profusion.androidenhancedvideoplayer.test
import androidx.compose.ui.test.assertIsDisplayed
+import androidx.compose.ui.test.doubleClick
import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onAllNodesWithTag
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.performClick
+import androidx.compose.ui.test.performTouchInput
import com.profusion.androidenhancedvideoplayer.components.EnhancedVideoPlayer
import org.junit.Rule
import org.junit.Test
@@ -29,4 +32,38 @@ class EnhancedVideoPlayerTest {
composeTestRule.onNodeWithTag("PlayerControlsParent", useUnmergedTree = true)
.assertIsDisplayed()
}
+
+ @Test
+ fun enhancedVideoPlayer_WhenDoubleClickHappenOnTheFirstHalfOfScreenVideoShouldShowRewindIcon() {
+ composeTestRule.setContent {
+ EnhancedVideoPlayer(
+ resourceId = R.raw.login_screen_background
+ )
+ }
+
+ composeTestRule.onAllNodesWithTag("SeekClickableArea", useUnmergedTree = true)[0]
+ .performTouchInput {
+ doubleClick()
+ }
+
+ composeTestRule.onNodeWithTag("RewindIcon", useUnmergedTree = true)
+ .assertIsDisplayed()
+ }
+
+ @Test
+ fun enhancedVideoPlayer_WhenDoubleClickHappenOnTheLastHalfOfScreenVideoShouldShowRewindIcon() {
+ composeTestRule.setContent {
+ EnhancedVideoPlayer(
+ resourceId = R.raw.login_screen_background
+ )
+ }
+
+ composeTestRule.onAllNodesWithTag("SeekClickableArea", useUnmergedTree = true)[1]
+ .performTouchInput {
+ doubleClick()
+ }
+
+ composeTestRule.onNodeWithTag("ForwardIcon", useUnmergedTree = true)
+ .assertIsDisplayed()
+ }
}
diff --git a/androidenhancedvideoplayer/src/main/java/com/profusion/androidenhancedvideoplayer/components/EnhancedVideoPlayer.kt b/androidenhancedvideoplayer/src/main/java/com/profusion/androidenhancedvideoplayer/components/EnhancedVideoPlayer.kt
index fd0b1cbe..8249d529 100644
--- a/androidenhancedvideoplayer/src/main/java/com/profusion/androidenhancedvideoplayer/components/EnhancedVideoPlayer.kt
+++ b/androidenhancedvideoplayer/src/main/java/com/profusion/androidenhancedvideoplayer/components/EnhancedVideoPlayer.kt
@@ -2,10 +2,14 @@ package com.profusion.androidenhancedvideoplayer.components
import android.content.res.Configuration
import android.net.Uri
+import androidx.compose.animation.*
+import androidx.compose.animation.core.*
+import androidx.compose.foundation.*
import androidx.compose.foundation.background
-import androidx.compose.foundation.clickable
-import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.foundation.layout.*
import androidx.compose.foundation.layout.Box
+import androidx.compose.material.*
+import androidx.compose.runtime.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.getValue
@@ -14,10 +18,13 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.*
+import androidx.compose.ui.graphics.*
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.unit.*
import androidx.compose.ui.viewinterop.AndroidView
import androidx.media3.common.MediaItem
import androidx.media3.common.Player
@@ -27,6 +34,7 @@ import androidx.media3.ui.AspectRatioFrameLayout
import androidx.media3.ui.PlayerView
import com.profusion.androidenhancedvideoplayer.components.playerOverlay.ControlsCustomization
import com.profusion.androidenhancedvideoplayer.components.playerOverlay.PlayerControls
+import com.profusion.androidenhancedvideoplayer.components.playerOverlay.SeekHandler
import com.profusion.androidenhancedvideoplayer.components.playerOverlay.SettingsControlsCustomization
import com.profusion.androidenhancedvideoplayer.utils.TimeoutEffect
import com.profusion.androidenhancedvideoplayer.utils.fillMaxSizeOnLandscape
@@ -38,6 +46,8 @@ import com.profusion.androidenhancedvideoplayer.utils.setStatusBarVisibility
private const val MAIN_PACKAGE_PATH_PREFIX = "android.resource://"
private const val CURRENT_TIME_TICK_IN_MS = 50L
+private const val DEFAULT_SEEK_TIME_MS = 10 * 1000L // 10 seconds
+
@androidx.annotation.OptIn(UnstableApi::class)
@Composable
fun EnhancedVideoPlayer(
@@ -49,7 +59,8 @@ fun EnhancedVideoPlayer(
soundOff: Boolean = true,
currentTimeTickInMs: Long = CURRENT_TIME_TICK_IN_MS,
controlsCustomization: ControlsCustomization = ControlsCustomization(),
- settingsControlsCustomization: SettingsControlsCustomization = SettingsControlsCustomization()
+ settingsControlsCustomization: SettingsControlsCustomization = SettingsControlsCustomization(),
+ transformSeekIncrementRatio: (tapCount: Int) -> Long = { it -> it * DEFAULT_SEEK_TIME_MS }
) {
val context = LocalContext.current
val mainPackagePath = "$MAIN_PACKAGE_PATH_PREFIX${context.packageName}/"
@@ -65,7 +76,8 @@ fun EnhancedVideoPlayer(
soundOff = soundOff,
currentTimeTickInMs = currentTimeTickInMs,
controlsCustomization = controlsCustomization,
- settingsControlsCustomization = settingsControlsCustomization
+ settingsControlsCustomization = settingsControlsCustomization,
+ transformSeekIncrementRatio = { transformSeekIncrementRatio(it) }
)
}
@@ -80,6 +92,7 @@ fun EnhancedVideoPlayer(
soundOff: Boolean = true,
currentTimeTickInMs: Long = CURRENT_TIME_TICK_IN_MS,
controlsCustomization: ControlsCustomization = ControlsCustomization(),
+ transformSeekIncrementRatio: (tapCount: Int) -> Long = { it -> it * DEFAULT_SEEK_TIME_MS },
settingsControlsCustomization: SettingsControlsCustomization = SettingsControlsCustomization()
) {
val context = LocalContext.current
@@ -96,7 +109,6 @@ fun EnhancedVideoPlayer(
prepare()
}
}
-
var isPlaying by remember { mutableStateOf(exoPlayer.isPlaying) }
var hasEnded by remember { mutableStateOf(exoPlayer.playbackState == ExoPlayer.STATE_ENDED) }
var isControlsVisible by remember { mutableStateOf(false) }
@@ -114,6 +126,10 @@ fun EnhancedVideoPlayer(
context.setNavigationBarVisibility(shouldShowSystemUi)
}
+ fun setControlsVisibility(visible: Boolean) {
+ isControlsVisible = visible
+ }
+
DisposableEffect(context) {
val listener = object : Player.Listener {
override fun onEvents(player: Player, events: Player.Events) {
@@ -142,11 +158,6 @@ fun EnhancedVideoPlayer(
Box(
modifier = Modifier
- .clickable(
- indication = null,
- interactionSource = remember { MutableInteractionSource() },
- onClick = { isControlsVisible = !isControlsVisible }
- )
.background(Color.Black)
.fillMaxSizeOnLandscape(orientation)
.testTag("VideoPlayerParent"),
@@ -167,6 +178,18 @@ fun EnhancedVideoPlayer(
}
}
)
+ Box(modifier = Modifier.matchParentSize()) {
+ SeekHandler(
+ disableSeekForward = hasEnded,
+ isControlsVisible = isControlsVisible,
+ exoPlayer = exoPlayer,
+ controlsCustomization = controlsCustomization,
+ toggleControlsVisibility = { isControlsVisible = !isControlsVisible },
+ setControlsVisibility = ::setControlsVisibility,
+ transformSeekIncrementRatio = transformSeekIncrementRatio
+ )
+ }
+
PlayerControls(
title = title,
isVisible = isControlsVisible,
diff --git a/androidenhancedvideoplayer/src/main/java/com/profusion/androidenhancedvideoplayer/components/playerOverlay/PlayerControls.kt b/androidenhancedvideoplayer/src/main/java/com/profusion/androidenhancedvideoplayer/components/playerOverlay/PlayerControls.kt
index 446cdce8..56ae75d8 100644
--- a/androidenhancedvideoplayer/src/main/java/com/profusion/androidenhancedvideoplayer/components/playerOverlay/PlayerControls.kt
+++ b/androidenhancedvideoplayer/src/main/java/com/profusion/androidenhancedvideoplayer/components/playerOverlay/PlayerControls.kt
@@ -13,11 +13,14 @@ class ControlsCustomization(
val nextIconContent: @Composable () -> Unit = { NextIcon() },
val fullScreenIconContent: @Composable () -> Unit = { FullScreenIcon() },
val exitFullScreenIconContent: @Composable () -> Unit = { ExitFullScreenIcon() },
- val settingsIconContent: @Composable () -> Unit = { SettingsIcon() }
+ val settingsIconContent: @Composable () -> Unit = { SettingsIcon() },
+ val forwardIconContent: @Composable (modifier: Modifier) -> Unit = { ForwardIcon(it) },
+ val rewindIconContent: @Composable (modifier: Modifier) -> Unit = { RewindIcon(it) }
)
@Composable
fun PlayerControls(
+ modifier: Modifier = Modifier,
title: String? = null,
isVisible: Boolean,
isPlaying: Boolean,
@@ -33,8 +36,7 @@ fun PlayerControls(
onSpeedSelected: (Float) -> Unit,
onSeekBarValueChange: (Long) -> Unit,
customization: ControlsCustomization,
- settingsControlsCustomization: SettingsControlsCustomization,
- modifier: Modifier = Modifier
+ settingsControlsCustomization: SettingsControlsCustomization
) {
PlayerControlsScaffold(
modifier = modifier.testTag("PlayerControlsParent"),
diff --git a/androidenhancedvideoplayer/src/main/java/com/profusion/androidenhancedvideoplayer/components/playerOverlay/PlayerControlsScaffold.kt b/androidenhancedvideoplayer/src/main/java/com/profusion/androidenhancedvideoplayer/components/playerOverlay/PlayerControlsScaffold.kt
index 7b56cb77..9473c98f 100644
--- a/androidenhancedvideoplayer/src/main/java/com/profusion/androidenhancedvideoplayer/components/playerOverlay/PlayerControlsScaffold.kt
+++ b/androidenhancedvideoplayer/src/main/java/com/profusion/androidenhancedvideoplayer/components/playerOverlay/PlayerControlsScaffold.kt
@@ -10,7 +10,7 @@ import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
-import androidx.compose.ui.graphics.Color
+import com.profusion.androidenhancedvideoplayer.styling.Colors
@Composable
fun PlayerControlsScaffold(
@@ -25,7 +25,7 @@ fun PlayerControlsScaffold(
enter = fadeIn(),
exit = fadeOut(),
modifier = modifier
- .background(Color.Black.copy(alpha = 0.6f))
+ .background(Colors.controlsShadow)
) {
Column(
modifier = Modifier.fillMaxSize(),
diff --git a/androidenhancedvideoplayer/src/main/java/com/profusion/androidenhancedvideoplayer/components/playerOverlay/PlayerIcons.kt b/androidenhancedvideoplayer/src/main/java/com/profusion/androidenhancedvideoplayer/components/playerOverlay/PlayerIcons.kt
index 3077bab4..d729ce49 100644
--- a/androidenhancedvideoplayer/src/main/java/com/profusion/androidenhancedvideoplayer/components/playerOverlay/PlayerIcons.kt
+++ b/androidenhancedvideoplayer/src/main/java/com/profusion/androidenhancedvideoplayer/components/playerOverlay/PlayerIcons.kt
@@ -107,3 +107,23 @@ fun CheckIcon(modifier: Modifier = Modifier) {
modifier = modifier
)
}
+
+@Composable
+fun ForwardIcon(modifier: Modifier = Modifier) {
+ Icon(
+ painter = painterResource(id = R.drawable.ic_forward),
+ tint = Color.White,
+ contentDescription = stringResource(R.string.controls_forward_description),
+ modifier = modifier.testTag("ForwardIcon")
+ )
+}
+
+@Composable
+fun RewindIcon(modifier: Modifier = Modifier) {
+ Icon(
+ painter = painterResource(id = R.drawable.ic_rewind),
+ tint = Color.White,
+ contentDescription = stringResource(R.string.controls_rewind_description),
+ modifier = modifier.testTag("RewindIcon")
+ )
+}
diff --git a/androidenhancedvideoplayer/src/main/java/com/profusion/androidenhancedvideoplayer/components/playerOverlay/SeekClickableArea.kt b/androidenhancedvideoplayer/src/main/java/com/profusion/androidenhancedvideoplayer/components/playerOverlay/SeekClickableArea.kt
new file mode 100644
index 00000000..e6cadc1b
--- /dev/null
+++ b/androidenhancedvideoplayer/src/main/java/com/profusion/androidenhancedvideoplayer/components/playerOverlay/SeekClickableArea.kt
@@ -0,0 +1,81 @@
+package com.profusion.androidenhancedvideoplayer.components.playerOverlay
+
+import androidx.compose.foundation.gestures.detectTapGestures
+import androidx.compose.foundation.indication
+import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxHeight
+import androidx.compose.foundation.layout.height
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.scale
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.input.pointer.pointerInput
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.res.stringResource
+import com.profusion.androidenhancedvideoplayer.R
+import com.profusion.androidenhancedvideoplayer.styling.Dimensions
+
+private const val TEXT_MAX_LINES = 2
+
+@Composable
+fun SeekClickableArea(
+ modifier: Modifier = Modifier,
+ scaleAnimation: Float,
+ tapCount: Int,
+ disableSeekClick: Boolean = false,
+ onSeekSingleTap: () -> Unit,
+ onSeekDoubleTap: () -> Unit,
+ checkIfCanToggleIsControlsVisible: () -> Unit,
+ getSeekTime: () -> Int,
+ seekIcon: @Composable (modifier: Modifier) -> Unit
+) {
+ val isTapCountGreaterThanZero = tapCount > 0
+ Box(
+ modifier = Modifier
+ .fillMaxHeight()
+ .indication(
+ indication = null,
+ interactionSource = remember { MutableInteractionSource() }
+ )
+ .pointerInput(tapCount, disableSeekClick) {
+ detectTapGestures(
+ onTap = if (isTapCountGreaterThanZero) {
+ if (disableSeekClick) {
+ null
+ } else { { onSeekSingleTap() } }
+ } else {
+ { checkIfCanToggleIsControlsVisible() }
+ },
+ onDoubleTap = if (isTapCountGreaterThanZero) { null } else {
+ if (disableSeekClick) {
+ null
+ } else {
+ { onSeekDoubleTap() }
+ }
+ }
+ )
+ }
+ .testTag("SeekClickableArea")
+ .then(modifier),
+ contentAlignment = Alignment.Center
+ ) {
+ if (isTapCountGreaterThanZero) {
+ val timeLabel = getSeekTime()
+ Column(horizontalAlignment = Alignment.CenterHorizontally) {
+ seekIcon(modifier = Modifier.scale(scaleAnimation))
+ Spacer(modifier = Modifier.height(Dimensions.large))
+ Text(
+ text = "$timeLabel ${stringResource(id = R.string.controls_time_unit)}",
+ maxLines = TEXT_MAX_LINES,
+ color = Color.White
+ )
+ }
+ }
+ }
+}
diff --git a/androidenhancedvideoplayer/src/main/java/com/profusion/androidenhancedvideoplayer/components/playerOverlay/SeekHandler.kt b/androidenhancedvideoplayer/src/main/java/com/profusion/androidenhancedvideoplayer/components/playerOverlay/SeekHandler.kt
new file mode 100644
index 00000000..fb91c24e
--- /dev/null
+++ b/androidenhancedvideoplayer/src/main/java/com/profusion/androidenhancedvideoplayer/components/playerOverlay/SeekHandler.kt
@@ -0,0 +1,157 @@
+package com.profusion.androidenhancedvideoplayer.components.playerOverlay
+
+import androidx.compose.animation.core.FastOutSlowInEasing
+import androidx.compose.animation.core.RepeatMode
+import androidx.compose.animation.core.animateFloat
+import androidx.compose.animation.core.infiniteRepeatable
+import androidx.compose.animation.core.rememberInfiniteTransition
+import androidx.compose.animation.core.tween
+import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.derivedStateOf
+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.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.media3.exoplayer.ExoPlayer
+import com.profusion.androidenhancedvideoplayer.styling.Colors
+import com.profusion.androidenhancedvideoplayer.utils.JobsHolder
+import com.profusion.androidenhancedvideoplayer.utils.executeAfterTimeout
+import kotlin.math.max
+import kotlin.math.min
+
+private const val ICON_ANIMATION_DURATION_MS = 650
+private const val ICON_INITIAL_SCALE = 0.8f
+private const val ICON_TARGET_SCALE = 1.1f
+private const val JOB_TIMEOUT = 650L
+private const val TRANSITION_LABEL = "scaleSeekIcon"
+
+@Composable
+fun SeekHandler(
+ disableSeekForward: Boolean,
+ isControlsVisible: Boolean,
+ exoPlayer: ExoPlayer,
+ toggleControlsVisibility: () -> Unit,
+ setControlsVisibility: (value: Boolean) -> Unit,
+ transformSeekIncrementRatio: (tapCount: Int) -> Long,
+ controlsCustomization: ControlsCustomization
+) {
+ val jobs = JobsHolder
+ val scope = rememberCoroutineScope()
+ var forwardTapCount by remember { mutableStateOf(0) }
+ var rewindTapCount by remember { mutableStateOf(0) }
+ val isRewinding by remember { derivedStateOf { rewindTapCount > 0 } }
+ val isForwarding by remember { derivedStateOf { forwardTapCount > 0 } }
+
+ val transition = rememberInfiniteTransition(TRANSITION_LABEL)
+
+ val scale = transition.animateFloat(
+ initialValue = ICON_INITIAL_SCALE,
+ targetValue = ICON_TARGET_SCALE,
+ animationSpec = infiniteRepeatable(
+ animation = tween(
+ durationMillis = ICON_ANIMATION_DURATION_MS,
+ easing = FastOutSlowInEasing
+ ),
+ repeatMode = RepeatMode.Restart
+ )
+ )
+
+ LaunchedEffect(forwardTapCount) {
+ if (forwardTapCount > 0) {
+ val incrementTime = transformSeekIncrementRatio(forwardTapCount) -
+ transformSeekIncrementRatio(
+ forwardTapCount - 1
+ )
+ val timeToSeek = exoPlayer.currentPosition + incrementTime
+ exoPlayer.seekTo(min(exoPlayer.duration, timeToSeek))
+ }
+ }
+
+ LaunchedEffect(rewindTapCount) {
+ if (rewindTapCount > 0) {
+ val incrementTime = transformSeekIncrementRatio(rewindTapCount) -
+ transformSeekIncrementRatio(
+ rewindTapCount - 1
+ )
+ val timeToSeek = exoPlayer.currentPosition - incrementTime
+ exoPlayer.seekTo(max(0, timeToSeek))
+ }
+ }
+
+ fun checkIfCanToggleIsControlsVisible() {
+ if (!isRewinding && !isForwarding) {
+ toggleControlsVisibility()
+ }
+ }
+
+ fun onForwardSingleTap() {
+ jobs.seekJob = executeAfterTimeout(scope, jobs.seekJob, JOB_TIMEOUT) {
+ forwardTapCount = 0
+ }
+ forwardTapCount++
+ }
+
+ fun onForwardDoubleTap() {
+ if (isRewinding) return
+ setControlsVisibility(false)
+ onForwardSingleTap()
+ }
+
+ fun onRewindSingleTap() {
+ jobs.seekJob = executeAfterTimeout(scope, jobs.seekJob, JOB_TIMEOUT) {
+ rewindTapCount = 0
+ }
+ rewindTapCount++
+ }
+
+ fun onRewindDoubleTap() {
+ if (isForwarding) return
+ setControlsVisibility(false)
+ onRewindSingleTap()
+ }
+
+ Row(
+ modifier = Modifier
+ .fillMaxSize()
+ .background(
+ if (isRewinding || isForwarding) {
+ Colors.controlsShadow
+ } else {
+ Color.Transparent
+ }
+ )
+ .clickable { setControlsVisibility(!isControlsVisible) }
+ ) {
+ SeekClickableArea(
+ modifier = Modifier.weight(1f),
+ tapCount = rewindTapCount,
+ scaleAnimation = scale.value,
+ onSeekDoubleTap = ::onRewindDoubleTap,
+ onSeekSingleTap = ::onRewindSingleTap,
+ checkIfCanToggleIsControlsVisible = ::checkIfCanToggleIsControlsVisible,
+ getSeekTime = { (transformSeekIncrementRatio(rewindTapCount) / 1000).toInt() },
+ seekIcon = { controlsCustomization.rewindIconContent(it) }
+ )
+ Spacer(modifier = Modifier.weight(0.2f))
+ SeekClickableArea(
+ modifier = Modifier.weight(1f),
+ tapCount = forwardTapCount,
+ scaleAnimation = scale.value,
+ disableSeekClick = disableSeekForward,
+ onSeekDoubleTap = ::onForwardDoubleTap,
+ onSeekSingleTap = ::onForwardSingleTap,
+ checkIfCanToggleIsControlsVisible = ::checkIfCanToggleIsControlsVisible,
+ getSeekTime = { (transformSeekIncrementRatio(forwardTapCount) / 1000).toInt() },
+ seekIcon = { controlsCustomization.forwardIconContent(it) }
+ )
+ }
+}
diff --git a/androidenhancedvideoplayer/src/main/java/com/profusion/androidenhancedvideoplayer/styling/Colors.kt b/androidenhancedvideoplayer/src/main/java/com/profusion/androidenhancedvideoplayer/styling/Colors.kt
new file mode 100644
index 00000000..1aaefe6e
--- /dev/null
+++ b/androidenhancedvideoplayer/src/main/java/com/profusion/androidenhancedvideoplayer/styling/Colors.kt
@@ -0,0 +1,7 @@
+package com.profusion.androidenhancedvideoplayer.styling
+
+import androidx.compose.ui.graphics.Color
+
+object Colors {
+ val controlsShadow = Color.Black.copy(alpha = 0.5f)
+}
diff --git a/androidenhancedvideoplayer/src/main/java/com/profusion/androidenhancedvideoplayer/utils/ExecuteAfterTimeout.kt b/androidenhancedvideoplayer/src/main/java/com/profusion/androidenhancedvideoplayer/utils/ExecuteAfterTimeout.kt
new file mode 100644
index 00000000..b86a0bb5
--- /dev/null
+++ b/androidenhancedvideoplayer/src/main/java/com/profusion/androidenhancedvideoplayer/utils/ExecuteAfterTimeout.kt
@@ -0,0 +1,27 @@
+package com.profusion.androidenhancedvideoplayer.utils
+
+import android.util.Log
+import kotlinx.coroutines.CancellationException
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.launch
+
+private const val TAG = "EXECUTE_AFTER_TIMEOUT"
+
+fun executeAfterTimeout(
+ scope: CoroutineScope,
+ job: Job?,
+ timeInMillis: Long,
+ onJobComplete: () -> Unit
+): Job {
+ job?.cancel()
+ return scope.launch() {
+ try {
+ delay(timeInMillis)
+ onJobComplete()
+ } catch (e: CancellationException) {
+ Log.i(TAG, e.stackTraceToString())
+ }
+ }
+}
diff --git a/androidenhancedvideoplayer/src/main/java/com/profusion/androidenhancedvideoplayer/utils/JobsHolder.kt b/androidenhancedvideoplayer/src/main/java/com/profusion/androidenhancedvideoplayer/utils/JobsHolder.kt
new file mode 100644
index 00000000..4a39bd73
--- /dev/null
+++ b/androidenhancedvideoplayer/src/main/java/com/profusion/androidenhancedvideoplayer/utils/JobsHolder.kt
@@ -0,0 +1,7 @@
+package com.profusion.androidenhancedvideoplayer.utils
+
+import kotlinx.coroutines.Job
+
+object JobsHolder {
+ var seekJob: Job? = null
+}
diff --git a/androidenhancedvideoplayer/src/main/res/drawable/ic_forward.xml b/androidenhancedvideoplayer/src/main/res/drawable/ic_forward.xml
new file mode 100644
index 00000000..42a715f8
--- /dev/null
+++ b/androidenhancedvideoplayer/src/main/res/drawable/ic_forward.xml
@@ -0,0 +1,5 @@
+
+
+
diff --git a/androidenhancedvideoplayer/src/main/res/drawable/ic_rewind.xml b/androidenhancedvideoplayer/src/main/res/drawable/ic_rewind.xml
new file mode 100644
index 00000000..66250677
--- /dev/null
+++ b/androidenhancedvideoplayer/src/main/res/drawable/ic_rewind.xml
@@ -0,0 +1,5 @@
+
+
+
diff --git a/androidenhancedvideoplayer/src/main/res/values-pt-rBR/controls_strings.xml b/androidenhancedvideoplayer/src/main/res/values-pt-rBR/controls_strings.xml
index 9bcaeb4a..38ce4ee8 100644
--- a/androidenhancedvideoplayer/src/main/res/values-pt-rBR/controls_strings.xml
+++ b/androidenhancedvideoplayer/src/main/res/values-pt-rBR/controls_strings.xml
@@ -1,10 +1,13 @@
Sair da Tela Cheia
+ Avançar
Tela Cheia
Próximo
Pausar
Reproduzir
Anterior
Repetir
+ Retroceder
Configurações
+ Segundos
diff --git a/androidenhancedvideoplayer/src/main/res/values/controls_strings.xml b/androidenhancedvideoplayer/src/main/res/values/controls_strings.xml
index 6d510b07..0b71711f 100644
--- a/androidenhancedvideoplayer/src/main/res/values/controls_strings.xml
+++ b/androidenhancedvideoplayer/src/main/res/values/controls_strings.xml
@@ -1,10 +1,13 @@
Exit Fullscreen
+ Forward
Fullscreen
Next
Pause
Play
Previous
Replay
+ Rewind
Settings
+ Seconds