mirror of
https://github.com/androidx/media.git
synced 2025-04-30 06:46:50 +08:00
Add PreviousButtonState
and NextButtonState
to ui-compose
* Provide a helper Composable for remembering the state instance and launching the listening coroutine that observes the changes in the Player * Add an example to demo-compose of using Previous- and Next- ButtonStates inside a Previous- and NextButton Composable. * Reformat the MainActivity usage of `Previous`, `PlayPause`, `Next` buttons to form Minimal Player Controls PiperOrigin-RevId: 691943147
This commit is contained in:
parent
676a3872a5
commit
1b302e879a
@ -57,8 +57,10 @@
|
|||||||
* IMA extension:
|
* IMA extension:
|
||||||
* UI:
|
* UI:
|
||||||
* Add `PlayerSurface` Composable to `media3-ui-compose` module.
|
* Add `PlayerSurface` Composable to `media3-ui-compose` module.
|
||||||
* Add `PlayPauseButtonState` class and `rememberPlayPauseButtonState`
|
* Add `PlayPauseButtonState`, `NextButtonState`, `PreviousButtonState`
|
||||||
Composable to `media3-ui-compose` module.
|
classes and the corresponding `rememberPlayPauseButtonState`,
|
||||||
|
`rememberNextButtonState`, `rememberPreviousButtonState` Composables to
|
||||||
|
`media3-ui-compose` module.
|
||||||
* Downloads:
|
* Downloads:
|
||||||
* OkHttp Extension:
|
* OkHttp Extension:
|
||||||
* Cronet Extension:
|
* Cronet Extension:
|
||||||
@ -76,8 +78,9 @@
|
|||||||
* Cast Extension:
|
* Cast Extension:
|
||||||
* Test Utilities:
|
* Test Utilities:
|
||||||
* Demo app:
|
* Demo app:
|
||||||
* Add `PlayPauseButton` Composable UI element to `demo-compose` utilizing
|
* Add `PlayPauseButton`, `NextButton`, `PreviousButton` and
|
||||||
`PlayPauseButtonState`.
|
`MinimalControls` Composable UI elements to `demo-compose` utilizing
|
||||||
|
`PlayPauseButtonState`, `NextButtonState`, and `PreviousButtonState`.
|
||||||
* Remove deprecated symbols:
|
* Remove deprecated symbols:
|
||||||
* Remove deprecated `AudioMixer.create()` method. Use
|
* Remove deprecated `AudioMixer.create()` method. Use
|
||||||
`DefaultAudioMixer.Factory().create()` instead.
|
`DefaultAudioMixer.Factory().create()` instead.
|
||||||
|
@ -19,18 +19,22 @@ import android.os.Bundle
|
|||||||
import androidx.activity.ComponentActivity
|
import androidx.activity.ComponentActivity
|
||||||
import androidx.activity.compose.setContent
|
import androidx.activity.compose.setContent
|
||||||
import androidx.activity.enableEdgeToEdge
|
import androidx.activity.enableEdgeToEdge
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.navigationBarsPadding
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import androidx.media3.common.MediaItem
|
import androidx.media3.common.MediaItem
|
||||||
import androidx.media3.common.Player
|
import androidx.media3.common.Player
|
||||||
import androidx.media3.demo.compose.buttons.PlayPauseButton
|
import androidx.media3.demo.compose.buttons.MinimalControls
|
||||||
import androidx.media3.demo.compose.data.videos
|
import androidx.media3.demo.compose.data.videos
|
||||||
import androidx.media3.exoplayer.ExoPlayer
|
import androidx.media3.exoplayer.ExoPlayer
|
||||||
import androidx.media3.ui.compose.PlayerSurface
|
import androidx.media3.ui.compose.PlayerSurface
|
||||||
@ -45,21 +49,35 @@ class MainActivity : ComponentActivity() {
|
|||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val exoPlayer = remember {
|
val exoPlayer = remember {
|
||||||
ExoPlayer.Builder(context).build().apply {
|
ExoPlayer.Builder(context).build().apply {
|
||||||
setMediaItem(MediaItem.fromUri(videos[0]))
|
setMediaItems(videos.map { MediaItem.fromUri(it) })
|
||||||
prepare()
|
prepare()
|
||||||
playWhenReady = true
|
|
||||||
repeatMode = Player.REPEAT_MODE_ONE
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
MediaPlayerScreen(player = exoPlayer, modifier = Modifier.fillMaxSize())
|
MediaPlayerScreen(
|
||||||
|
player = exoPlayer,
|
||||||
|
modifier = Modifier.fillMaxSize().navigationBarsPadding(),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun MediaPlayerScreen(player: Player, modifier: Modifier = Modifier) {
|
private fun MediaPlayerScreen(player: Player, modifier: Modifier = Modifier) {
|
||||||
|
var showControls by remember { mutableStateOf(true) }
|
||||||
Box(modifier) {
|
Box(modifier) {
|
||||||
PlayerSurface(player = player, surfaceType = SURFACE_TYPE_SURFACE_VIEW)
|
PlayerSurface(
|
||||||
PlayPauseButton(player, Modifier.align(Alignment.Center).size(100.dp))
|
player = player,
|
||||||
|
surfaceType = SURFACE_TYPE_SURFACE_VIEW,
|
||||||
|
modifier =
|
||||||
|
modifier.clickable(
|
||||||
|
interactionSource = remember { MutableInteractionSource() },
|
||||||
|
indication = null, // to prevent the ripple from the tap
|
||||||
|
) {
|
||||||
|
showControls = !showControls
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if (showControls) {
|
||||||
|
MinimalControls(player, Modifier.align(Alignment.Center))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,51 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2024 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
|
||||||
|
*
|
||||||
|
* https://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.media3.demo.compose.buttons
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.media3.common.Player
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Minimal playback controls for a [Player].
|
||||||
|
*
|
||||||
|
* Includes buttons for seeking to a previous/next items or playing/pausing the playback.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
internal fun MinimalControls(player: Player, modifier: Modifier = Modifier) {
|
||||||
|
val graySemiTransparentBackground = Color.Gray.copy(alpha = 0.4f)
|
||||||
|
val modifierForIconButton =
|
||||||
|
modifier.size(80.dp).background(graySemiTransparentBackground, CircleShape)
|
||||||
|
Row(
|
||||||
|
modifier = modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.SpaceEvenly,
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
PreviousButton(player, modifierForIconButton)
|
||||||
|
PlayPauseButton(player, modifierForIconButton)
|
||||||
|
NextButton(player, modifierForIconButton)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,40 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2024 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
|
||||||
|
*
|
||||||
|
* https://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.media3.demo.compose.buttons
|
||||||
|
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.SkipNext
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.media3.common.Player
|
||||||
|
import androidx.media3.demo.compose.R
|
||||||
|
import androidx.media3.ui.compose.state.rememberNextButtonState
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
internal fun NextButton(player: Player, modifier: Modifier = Modifier) {
|
||||||
|
val state = rememberNextButtonState(player)
|
||||||
|
IconButton(onClick = state::onClick, modifier = modifier, enabled = state.isEnabled) {
|
||||||
|
Icon(
|
||||||
|
Icons.Default.SkipNext,
|
||||||
|
contentDescription = stringResource(R.string.next_button),
|
||||||
|
modifier = modifier,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
@ -17,8 +17,8 @@
|
|||||||
package androidx.media3.demo.compose.buttons
|
package androidx.media3.demo.compose.buttons
|
||||||
|
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.PauseCircle
|
import androidx.compose.material.icons.filled.Pause
|
||||||
import androidx.compose.material.icons.filled.PlayCircle
|
import androidx.compose.material.icons.filled.PlayArrow
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
@ -31,7 +31,7 @@ import androidx.media3.ui.compose.state.rememberPlayPauseButtonState
|
|||||||
@Composable
|
@Composable
|
||||||
internal fun PlayPauseButton(player: Player, modifier: Modifier = Modifier) {
|
internal fun PlayPauseButton(player: Player, modifier: Modifier = Modifier) {
|
||||||
val state = rememberPlayPauseButtonState(player)
|
val state = rememberPlayPauseButtonState(player)
|
||||||
val icon = if (state.showPlay) Icons.Default.PlayCircle else Icons.Default.PauseCircle
|
val icon = if (state.showPlay) Icons.Default.PlayArrow else Icons.Default.Pause
|
||||||
val contentDescription =
|
val contentDescription =
|
||||||
if (state.showPlay) stringResource(R.string.playpause_button_play)
|
if (state.showPlay) stringResource(R.string.playpause_button_play)
|
||||||
else stringResource(R.string.playpause_button_pause)
|
else stringResource(R.string.playpause_button_pause)
|
||||||
|
@ -0,0 +1,40 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2024 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
|
||||||
|
*
|
||||||
|
* https://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.media3.demo.compose.buttons
|
||||||
|
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.SkipPrevious
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.media3.common.Player
|
||||||
|
import androidx.media3.demo.compose.R
|
||||||
|
import androidx.media3.ui.compose.state.rememberPreviousButtonState
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
internal fun PreviousButton(player: Player, modifier: Modifier = Modifier) {
|
||||||
|
val state = rememberPreviousButtonState(player)
|
||||||
|
IconButton(onClick = state::onClick, modifier = modifier, enabled = state.isEnabled) {
|
||||||
|
Icon(
|
||||||
|
Icons.Default.SkipPrevious,
|
||||||
|
contentDescription = stringResource(R.string.previous_button),
|
||||||
|
modifier = modifier,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
@ -18,6 +18,5 @@ package androidx.media3.demo.compose.data
|
|||||||
val videos =
|
val videos =
|
||||||
listOf(
|
listOf(
|
||||||
"https://html5demos.com/assets/dizzy.mp4",
|
"https://html5demos.com/assets/dizzy.mp4",
|
||||||
"https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_4x3/gear0/fileSequence0.aac",
|
|
||||||
"https://storage.googleapis.com/exoplayer-test-media-1/gen-3/screens/dash-vod-single-segment/video-vp9-360.webm",
|
"https://storage.googleapis.com/exoplayer-test-media-1/gen-3/screens/dash-vod-single-segment/video-vp9-360.webm",
|
||||||
)
|
)
|
||||||
|
@ -17,4 +17,6 @@
|
|||||||
<string name="app_name">Media3 Compose Demo</string>
|
<string name="app_name">Media3 Compose Demo</string>
|
||||||
<string name="playpause_button_play">Play</string>
|
<string name="playpause_button_play">Play</string>
|
||||||
<string name="playpause_button_pause">Pause</string>
|
<string name="playpause_button_pause">Pause</string>
|
||||||
|
<string name="next_button">Next</string>
|
||||||
|
<string name="previous_button">Previous</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
@ -0,0 +1,65 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2024 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
|
||||||
|
*
|
||||||
|
* https://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.media3.ui.compose.state
|
||||||
|
|
||||||
|
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.media3.common.Player
|
||||||
|
import androidx.media3.common.listen
|
||||||
|
import androidx.media3.common.util.UnstableApi
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remembers the value of [NextButtonState] created based on the passed [Player] and launch a
|
||||||
|
* coroutine to listen to [Player]'s changes. If the [Player] instance changes between compositions,
|
||||||
|
* produce and remember a new value.
|
||||||
|
*/
|
||||||
|
@UnstableApi
|
||||||
|
@Composable
|
||||||
|
fun rememberNextButtonState(player: Player): NextButtonState {
|
||||||
|
val nextButtonState = remember(player) { NextButtonState(player) }
|
||||||
|
LaunchedEffect(player) { nextButtonState.observe() }
|
||||||
|
return nextButtonState
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* State that holds all interactions to correctly deal with a UI component representing a
|
||||||
|
* seek-to-next button.
|
||||||
|
*
|
||||||
|
* This button has no internal state to maintain, it can only be enabled or disabled.
|
||||||
|
*
|
||||||
|
* @property[isEnabled] determined by `isCommandAvailable(Player.COMMAND_SEEK_TO_NEXT)`
|
||||||
|
*/
|
||||||
|
@UnstableApi
|
||||||
|
class NextButtonState(private val player: Player) {
|
||||||
|
var isEnabled by mutableStateOf(player.isCommandAvailable(Player.COMMAND_SEEK_TO_NEXT))
|
||||||
|
private set
|
||||||
|
|
||||||
|
fun onClick() {
|
||||||
|
player.seekToNext()
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun observe(): Nothing =
|
||||||
|
player.listen { events ->
|
||||||
|
if (events.contains(Player.EVENT_AVAILABLE_COMMANDS_CHANGED)) {
|
||||||
|
isEnabled = isCommandAvailable(Player.COMMAND_SEEK_TO_NEXT)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,65 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2024 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
|
||||||
|
*
|
||||||
|
* https://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.media3.ui.compose.state
|
||||||
|
|
||||||
|
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.media3.common.Player
|
||||||
|
import androidx.media3.common.listen
|
||||||
|
import androidx.media3.common.util.UnstableApi
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remembers the value of [PreviousButtonState] created based on the passed [Player] and launch a
|
||||||
|
* coroutine to listen to [Player]'s changes. If the [Player] instance changes between compositions,
|
||||||
|
* produce and remember a new value.
|
||||||
|
*/
|
||||||
|
@UnstableApi
|
||||||
|
@Composable
|
||||||
|
fun rememberPreviousButtonState(player: Player): PreviousButtonState {
|
||||||
|
val previousButtonState = remember(player) { PreviousButtonState(player) }
|
||||||
|
LaunchedEffect(player) { previousButtonState.observe() }
|
||||||
|
return previousButtonState
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* State that holds all interactions to correctly deal with a UI component representing a
|
||||||
|
* seek-to-previous button.
|
||||||
|
*
|
||||||
|
* This button has no internal state to maintain, it can only be enabled or disabled.
|
||||||
|
*
|
||||||
|
* @property[isEnabled] determined by `isCommandAvailable(Player.COMMAND_SEEK_TO_PREVIOUS)`
|
||||||
|
*/
|
||||||
|
@UnstableApi
|
||||||
|
class PreviousButtonState(private val player: Player) {
|
||||||
|
var isEnabled by mutableStateOf(player.isCommandAvailable(Player.COMMAND_SEEK_TO_PREVIOUS))
|
||||||
|
private set
|
||||||
|
|
||||||
|
fun onClick() {
|
||||||
|
player.seekToPrevious()
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun observe(): Nothing =
|
||||||
|
player.listen { events ->
|
||||||
|
if (events.contains(Player.EVENT_AVAILABLE_COMMANDS_CHANGED)) {
|
||||||
|
isEnabled = isCommandAvailable(Player.COMMAND_SEEK_TO_PREVIOUS)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,99 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2024 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
|
||||||
|
*
|
||||||
|
* https://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.media3.ui.compose.state
|
||||||
|
|
||||||
|
import androidx.compose.ui.test.junit4.createComposeRule
|
||||||
|
import androidx.media3.common.Player
|
||||||
|
import androidx.media3.ui.compose.utils.TestPlayer
|
||||||
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
|
import com.google.common.truth.Truth.assertThat
|
||||||
|
import org.junit.Rule
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
|
||||||
|
/** Unit test for [NextButtonState]. */
|
||||||
|
@RunWith(AndroidJUnit4::class)
|
||||||
|
class NextButtonStateTest {
|
||||||
|
|
||||||
|
@get:Rule val composeTestRule = createComposeRule()
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun addSeekNextCommandToPlayer_buttonStateTogglesFromDisabledToEnabled() {
|
||||||
|
val player = TestPlayer()
|
||||||
|
player.playbackState = Player.STATE_READY
|
||||||
|
player.playWhenReady = true
|
||||||
|
player.removeCommands(Player.COMMAND_SEEK_TO_NEXT)
|
||||||
|
|
||||||
|
lateinit var state: NextButtonState
|
||||||
|
composeTestRule.setContent { state = rememberNextButtonState(player = player) }
|
||||||
|
|
||||||
|
assertThat(state.isEnabled).isFalse()
|
||||||
|
|
||||||
|
player.addCommands(Player.COMMAND_SEEK_TO_NEXT)
|
||||||
|
composeTestRule.waitForIdle()
|
||||||
|
|
||||||
|
assertThat(state.isEnabled).isTrue()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun removeSeekNextCommandToPlayer_buttonStateTogglesFromEnabledToDisabled() {
|
||||||
|
val player = TestPlayer()
|
||||||
|
player.playbackState = Player.STATE_READY
|
||||||
|
player.playWhenReady = true
|
||||||
|
|
||||||
|
lateinit var state: NextButtonState
|
||||||
|
composeTestRule.setContent { state = rememberNextButtonState(player = player) }
|
||||||
|
|
||||||
|
assertThat(state.isEnabled).isTrue()
|
||||||
|
|
||||||
|
player.removeCommands(Player.COMMAND_SEEK_TO_NEXT)
|
||||||
|
composeTestRule.waitForIdle()
|
||||||
|
|
||||||
|
assertThat(state.isEnabled).isFalse()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun clickNextOnPenultimateMediaItem_buttonStateTogglesFromEnabledToDisabled() {
|
||||||
|
val player = TestPlayer()
|
||||||
|
player.playbackState = Player.STATE_READY
|
||||||
|
player.playWhenReady = true
|
||||||
|
|
||||||
|
lateinit var state: NextButtonState
|
||||||
|
composeTestRule.setContent { state = rememberNextButtonState(player = player) }
|
||||||
|
|
||||||
|
assertThat(state.isEnabled).isTrue()
|
||||||
|
|
||||||
|
player.seekToNext()
|
||||||
|
composeTestRule.waitForIdle()
|
||||||
|
|
||||||
|
assertThat(state.isEnabled).isFalse()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun playerInReadyState_buttonClicked_nextItemPlaying() {
|
||||||
|
val player = TestPlayer()
|
||||||
|
player.playbackState = Player.STATE_READY
|
||||||
|
player.playWhenReady = true
|
||||||
|
val state = NextButtonState(player)
|
||||||
|
|
||||||
|
assertThat(player.currentMediaItemIndex).isEqualTo(0)
|
||||||
|
|
||||||
|
state.onClick()
|
||||||
|
|
||||||
|
assertThat(player.currentMediaItemIndex).isEqualTo(1)
|
||||||
|
}
|
||||||
|
}
|
@ -16,15 +16,11 @@
|
|||||||
|
|
||||||
package androidx.media3.ui.compose.state
|
package androidx.media3.ui.compose.state
|
||||||
|
|
||||||
import android.os.Looper
|
|
||||||
import androidx.compose.ui.test.junit4.createComposeRule
|
import androidx.compose.ui.test.junit4.createComposeRule
|
||||||
import androidx.media3.common.Player
|
import androidx.media3.common.Player
|
||||||
import androidx.media3.common.SimpleBasePlayer
|
import androidx.media3.ui.compose.utils.TestPlayer
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
import com.google.common.collect.ImmutableList
|
|
||||||
import com.google.common.truth.Truth.assertThat
|
import com.google.common.truth.Truth.assertThat
|
||||||
import com.google.common.util.concurrent.Futures
|
|
||||||
import com.google.common.util.concurrent.ListenableFuture
|
|
||||||
import org.junit.Rule
|
import org.junit.Rule
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import org.junit.runner.RunWith
|
import org.junit.runner.RunWith
|
||||||
@ -133,74 +129,3 @@ class PlayPauseButtonStateTest {
|
|||||||
assertThat(player.playbackState).isEqualTo(Player.STATE_BUFFERING)
|
assertThat(player.playbackState).isEqualTo(Player.STATE_BUFFERING)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private class TestPlayer : SimpleBasePlayer(Looper.myLooper()!!) {
|
|
||||||
private var state =
|
|
||||||
State.Builder()
|
|
||||||
.setAvailableCommands(Player.Commands.Builder().addAllCommands().build())
|
|
||||||
.setPlaylist(ImmutableList.of(MediaItemData.Builder(/* uid= */ Any()).build()))
|
|
||||||
.build()
|
|
||||||
|
|
||||||
override fun getState(): State {
|
|
||||||
return state
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun handleSetPlayWhenReady(playWhenReady: Boolean): ListenableFuture<*> {
|
|
||||||
state =
|
|
||||||
state
|
|
||||||
.buildUpon()
|
|
||||||
.setPlayWhenReady(playWhenReady, PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST)
|
|
||||||
.build()
|
|
||||||
return Futures.immediateVoidFuture()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun handlePrepare(): ListenableFuture<*> {
|
|
||||||
state =
|
|
||||||
state
|
|
||||||
.buildUpon()
|
|
||||||
.setPlayerError(null)
|
|
||||||
.setPlaybackState(if (state.timeline.isEmpty) STATE_ENDED else STATE_BUFFERING)
|
|
||||||
.build()
|
|
||||||
return Futures.immediateVoidFuture()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun handleSeek(
|
|
||||||
mediaItemIndex: Int,
|
|
||||||
positionMs: Long,
|
|
||||||
seekCommand: @Player.Command Int,
|
|
||||||
): ListenableFuture<*> {
|
|
||||||
state =
|
|
||||||
state.buildUpon().setPlaybackState(STATE_BUFFERING).setContentPositionMs(positionMs).build()
|
|
||||||
return Futures.immediateVoidFuture()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setPlaybackState(playbackState: @Player.State Int) {
|
|
||||||
state = state.buildUpon().setPlaybackState(playbackState).build()
|
|
||||||
invalidateState()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setPosition(positionMs: Long) {
|
|
||||||
state = state.buildUpon().setContentPositionMs(positionMs).build()
|
|
||||||
invalidateState()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun removeCommands(vararg commands: @Player.Command Int) {
|
|
||||||
state =
|
|
||||||
state
|
|
||||||
.buildUpon()
|
|
||||||
.setAvailableCommands(
|
|
||||||
Player.Commands.Builder().addAllCommands().removeAll(*commands).build()
|
|
||||||
)
|
|
||||||
.build()
|
|
||||||
invalidateState()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun addCommands(vararg commands: @Player.Command Int) {
|
|
||||||
state =
|
|
||||||
state
|
|
||||||
.buildUpon()
|
|
||||||
.setAvailableCommands(Player.Commands.Builder().addAll(*commands).build())
|
|
||||||
.build()
|
|
||||||
invalidateState()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -0,0 +1,82 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2024 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
|
||||||
|
*
|
||||||
|
* https://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.media3.ui.compose.state
|
||||||
|
|
||||||
|
import androidx.compose.ui.test.junit4.createComposeRule
|
||||||
|
import androidx.media3.common.Player
|
||||||
|
import androidx.media3.ui.compose.utils.TestPlayer
|
||||||
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
|
import com.google.common.truth.Truth.assertThat
|
||||||
|
import org.junit.Rule
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
|
||||||
|
/** Unit test for [PreviousButtonState]. */
|
||||||
|
@RunWith(AndroidJUnit4::class)
|
||||||
|
class PreviousButtonStateTest {
|
||||||
|
|
||||||
|
@get:Rule val composeTestRule = createComposeRule()
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun addSeekPrevCommandToPlayer_buttonStateTogglesFromDisabledToEnabled() {
|
||||||
|
val player = TestPlayer()
|
||||||
|
player.playbackState = Player.STATE_READY
|
||||||
|
player.playWhenReady = true
|
||||||
|
player.removeCommands(Player.COMMAND_SEEK_TO_PREVIOUS)
|
||||||
|
|
||||||
|
lateinit var state: PreviousButtonState
|
||||||
|
composeTestRule.setContent { state = rememberPreviousButtonState(player = player) }
|
||||||
|
|
||||||
|
assertThat(state.isEnabled).isFalse()
|
||||||
|
|
||||||
|
composeTestRule.runOnUiThread { player.addCommands(Player.COMMAND_SEEK_TO_PREVIOUS) }
|
||||||
|
composeTestRule.waitForIdle()
|
||||||
|
|
||||||
|
assertThat(state.isEnabled).isTrue()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun removeSeekPrevCommandToPlayer_buttonStateTogglesFromEnabledToDisabled() {
|
||||||
|
val player = TestPlayer()
|
||||||
|
player.playbackState = Player.STATE_READY
|
||||||
|
player.playWhenReady = true
|
||||||
|
|
||||||
|
lateinit var state: PreviousButtonState
|
||||||
|
composeTestRule.setContent { state = rememberPreviousButtonState(player = player) }
|
||||||
|
|
||||||
|
assertThat(state.isEnabled).isTrue()
|
||||||
|
|
||||||
|
composeTestRule.runOnUiThread { player.removeCommands(Player.COMMAND_SEEK_TO_PREVIOUS) }
|
||||||
|
composeTestRule.waitForIdle()
|
||||||
|
|
||||||
|
assertThat(state.isEnabled).isFalse()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun playerInReadyState_prevButtonClicked_sameItemPlayingFromBeginning() {
|
||||||
|
val player = TestPlayer()
|
||||||
|
player.playbackState = Player.STATE_READY
|
||||||
|
player.playWhenReady = true
|
||||||
|
val state = PreviousButtonState(player)
|
||||||
|
|
||||||
|
assertThat(player.currentMediaItemIndex).isEqualTo(0)
|
||||||
|
|
||||||
|
state.onClick()
|
||||||
|
|
||||||
|
assertThat(player.currentMediaItemIndex).isEqualTo(0)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,113 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2024 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
|
||||||
|
*
|
||||||
|
* https://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.media3.ui.compose.utils
|
||||||
|
|
||||||
|
import android.os.Looper
|
||||||
|
import androidx.media3.common.Player
|
||||||
|
import androidx.media3.common.SimpleBasePlayer
|
||||||
|
import com.google.common.collect.ImmutableList
|
||||||
|
import com.google.common.util.concurrent.Futures
|
||||||
|
import com.google.common.util.concurrent.ListenableFuture
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A fake [Player] that uses [SimpleBasePlayer]'s minimal number of default methods implementations
|
||||||
|
* to build upon to simulate realistic playback scenarios for testing.
|
||||||
|
*/
|
||||||
|
internal class TestPlayer : SimpleBasePlayer(Looper.myLooper()!!) {
|
||||||
|
private var state =
|
||||||
|
State.Builder()
|
||||||
|
.setAvailableCommands(Player.Commands.Builder().addAllCommands().build())
|
||||||
|
.setPlaylist(
|
||||||
|
ImmutableList.of(
|
||||||
|
MediaItemData.Builder(/* uid= */ "First").build(),
|
||||||
|
MediaItemData.Builder(/* uid= */ "Second").build(),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.setPlayWhenReady(true, PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
override fun getState(): State {
|
||||||
|
return state
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun handleSetPlayWhenReady(playWhenReady: Boolean): ListenableFuture<*> {
|
||||||
|
state =
|
||||||
|
state
|
||||||
|
.buildUpon()
|
||||||
|
.setPlayWhenReady(playWhenReady, PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST)
|
||||||
|
.build()
|
||||||
|
return Futures.immediateVoidFuture()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun handlePrepare(): ListenableFuture<*> {
|
||||||
|
state =
|
||||||
|
state
|
||||||
|
.buildUpon()
|
||||||
|
.setPlayerError(null)
|
||||||
|
.setPlaybackState(if (state.timeline.isEmpty) STATE_ENDED else STATE_BUFFERING)
|
||||||
|
.build()
|
||||||
|
return Futures.immediateVoidFuture()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun handleSeek(
|
||||||
|
mediaItemIndex: Int,
|
||||||
|
positionMs: Long,
|
||||||
|
seekCommand: @Player.Command Int,
|
||||||
|
): ListenableFuture<*> {
|
||||||
|
state =
|
||||||
|
state
|
||||||
|
.buildUpon()
|
||||||
|
.setPlaybackState(STATE_BUFFERING)
|
||||||
|
.setCurrentMediaItemIndex(mediaItemIndex)
|
||||||
|
.setContentPositionMs(positionMs)
|
||||||
|
.build()
|
||||||
|
if (mediaItemIndex == state.playlist.size - 1) {
|
||||||
|
removeCommands(Player.COMMAND_SEEK_TO_NEXT)
|
||||||
|
}
|
||||||
|
return Futures.immediateVoidFuture()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setPlaybackState(playbackState: @Player.State Int) {
|
||||||
|
state = state.buildUpon().setPlaybackState(playbackState).build()
|
||||||
|
invalidateState()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setPosition(positionMs: Long) {
|
||||||
|
state = state.buildUpon().setContentPositionMs(positionMs).build()
|
||||||
|
invalidateState()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun removeCommands(vararg commands: @Player.Command Int) {
|
||||||
|
state =
|
||||||
|
state
|
||||||
|
.buildUpon()
|
||||||
|
.setAvailableCommands(
|
||||||
|
Player.Commands.Builder().addAllCommands().removeAll(*commands).build()
|
||||||
|
)
|
||||||
|
.build()
|
||||||
|
invalidateState()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun addCommands(vararg commands: @Player.Command Int) {
|
||||||
|
state =
|
||||||
|
state
|
||||||
|
.buildUpon()
|
||||||
|
.setAvailableCommands(Player.Commands.Builder().addAll(*commands).build())
|
||||||
|
.build()
|
||||||
|
invalidateState()
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user