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:
|
||||
* UI:
|
||||
* Add `PlayerSurface` Composable to `media3-ui-compose` module.
|
||||
* Add `PlayPauseButtonState` class and `rememberPlayPauseButtonState`
|
||||
Composable to `media3-ui-compose` module.
|
||||
* Add `PlayPauseButtonState`, `NextButtonState`, `PreviousButtonState`
|
||||
classes and the corresponding `rememberPlayPauseButtonState`,
|
||||
`rememberNextButtonState`, `rememberPreviousButtonState` Composables to
|
||||
`media3-ui-compose` module.
|
||||
* Downloads:
|
||||
* OkHttp Extension:
|
||||
* Cronet Extension:
|
||||
@ -76,8 +78,9 @@
|
||||
* Cast Extension:
|
||||
* Test Utilities:
|
||||
* Demo app:
|
||||
* Add `PlayPauseButton` Composable UI element to `demo-compose` utilizing
|
||||
`PlayPauseButtonState`.
|
||||
* Add `PlayPauseButton`, `NextButton`, `PreviousButton` and
|
||||
`MinimalControls` Composable UI elements to `demo-compose` utilizing
|
||||
`PlayPauseButtonState`, `NextButtonState`, and `PreviousButtonState`.
|
||||
* Remove deprecated symbols:
|
||||
* Remove deprecated `AudioMixer.create()` method. Use
|
||||
`DefaultAudioMixer.Factory().create()` instead.
|
||||
|
@ -19,18 +19,22 @@ import android.os.Bundle
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
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.fillMaxSize
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.navigationBarsPadding
|
||||
import androidx.compose.runtime.Composable
|
||||
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.platform.LocalContext
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.media3.common.MediaItem
|
||||
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.exoplayer.ExoPlayer
|
||||
import androidx.media3.ui.compose.PlayerSurface
|
||||
@ -45,21 +49,35 @@ class MainActivity : ComponentActivity() {
|
||||
val context = LocalContext.current
|
||||
val exoPlayer = remember {
|
||||
ExoPlayer.Builder(context).build().apply {
|
||||
setMediaItem(MediaItem.fromUri(videos[0]))
|
||||
setMediaItems(videos.map { MediaItem.fromUri(it) })
|
||||
prepare()
|
||||
playWhenReady = true
|
||||
repeatMode = Player.REPEAT_MODE_ONE
|
||||
}
|
||||
}
|
||||
MediaPlayerScreen(player = exoPlayer, modifier = Modifier.fillMaxSize())
|
||||
MediaPlayerScreen(
|
||||
player = exoPlayer,
|
||||
modifier = Modifier.fillMaxSize().navigationBarsPadding(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MediaPlayerScreen(player: Player, modifier: Modifier = Modifier) {
|
||||
var showControls by remember { mutableStateOf(true) }
|
||||
Box(modifier) {
|
||||
PlayerSurface(player = player, surfaceType = SURFACE_TYPE_SURFACE_VIEW)
|
||||
PlayPauseButton(player, Modifier.align(Alignment.Center).size(100.dp))
|
||||
PlayerSurface(
|
||||
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
|
||||
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.PauseCircle
|
||||
import androidx.compose.material.icons.filled.PlayCircle
|
||||
import androidx.compose.material.icons.filled.Pause
|
||||
import androidx.compose.material.icons.filled.PlayArrow
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.runtime.Composable
|
||||
@ -31,7 +31,7 @@ import androidx.media3.ui.compose.state.rememberPlayPauseButtonState
|
||||
@Composable
|
||||
internal fun PlayPauseButton(player: Player, modifier: Modifier = Modifier) {
|
||||
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 =
|
||||
if (state.showPlay) stringResource(R.string.playpause_button_play)
|
||||
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 =
|
||||
listOf(
|
||||
"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",
|
||||
)
|
||||
|
@ -17,4 +17,6 @@
|
||||
<string name="app_name">Media3 Compose Demo</string>
|
||||
<string name="playpause_button_play">Play</string>
|
||||
<string name="playpause_button_pause">Pause</string>
|
||||
<string name="next_button">Next</string>
|
||||
<string name="previous_button">Previous</string>
|
||||
</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
|
||||
|
||||
import android.os.Looper
|
||||
import androidx.compose.ui.test.junit4.createComposeRule
|
||||
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 com.google.common.collect.ImmutableList
|
||||
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.Test
|
||||
import org.junit.runner.RunWith
|
||||
@ -133,74 +129,3 @@ class PlayPauseButtonStateTest {
|
||||
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