mirror of
https://github.com/androidx/media.git
synced 2025-04-30 06:46:50 +08:00
Add PlayPauseButtonState
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 PlayPauseButtonState inside a PlayPauseButton Composable. The smart State object has been deemed a preferred solution over collecting Flows due to queuing/timing/buffering limitations. Instead, it uses the new `Player.listen` suspending extension function to catch the relevant events. PiperOrigin-RevId: 691879975
This commit is contained in:
parent
f991e1f023
commit
676a3872a5
@ -57,6 +57,8 @@
|
|||||||
* 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`
|
||||||
|
Composable to `media3-ui-compose` module.
|
||||||
* Downloads:
|
* Downloads:
|
||||||
* OkHttp Extension:
|
* OkHttp Extension:
|
||||||
* Cronet Extension:
|
* Cronet Extension:
|
||||||
@ -74,6 +76,8 @@
|
|||||||
* Cast Extension:
|
* Cast Extension:
|
||||||
* Test Utilities:
|
* Test Utilities:
|
||||||
* Demo app:
|
* Demo app:
|
||||||
|
* Add `PlayPauseButton` Composable UI element to `demo-compose` utilizing
|
||||||
|
`PlayPauseButtonState`.
|
||||||
* 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.
|
||||||
|
@ -52,7 +52,6 @@ android {
|
|||||||
disable 'GoogleAppIndexingWarning','MissingTranslation'
|
disable 'GoogleAppIndexingWarning','MissingTranslation'
|
||||||
}
|
}
|
||||||
buildFeatures {
|
buildFeatures {
|
||||||
viewBinding true
|
|
||||||
compose true
|
compose true
|
||||||
}
|
}
|
||||||
composeOptions {
|
composeOptions {
|
||||||
@ -71,18 +70,16 @@ dependencies {
|
|||||||
implementation composeBom
|
implementation composeBom
|
||||||
|
|
||||||
implementation 'androidx.activity:activity-compose:1.9.0'
|
implementation 'androidx.activity:activity-compose:1.9.0'
|
||||||
implementation 'androidx.compose.foundation:foundation-android:1.6.7'
|
implementation 'androidx.compose.foundation:foundation'
|
||||||
implementation 'androidx.compose.material3:material3-android:1.2.1'
|
implementation 'androidx.compose.material3:material3'
|
||||||
|
implementation 'androidx.compose.material:material-icons-extended'
|
||||||
|
implementation 'androidx.compose.ui:ui-tooling-preview'
|
||||||
implementation 'com.google.android.material:material:' + androidxMaterialVersion
|
implementation 'com.google.android.material:material:' + androidxMaterialVersion
|
||||||
|
|
||||||
implementation project(modulePrefix + 'lib-exoplayer')
|
implementation project(modulePrefix + 'lib-exoplayer')
|
||||||
implementation project(modulePrefix + 'lib-ui-compose')
|
implementation project(modulePrefix + 'lib-ui-compose')
|
||||||
|
debugImplementation 'androidx.compose.ui:ui-tooling'
|
||||||
|
|
||||||
// For detecting and debugging leaks only. LeakCanary is not needed for demo app to work.
|
// For detecting and debugging leaks only. LeakCanary is not needed for demo app to work.
|
||||||
debugImplementation 'com.squareup.leakcanary:leakcanary-android:' + leakCanaryVersion
|
debugImplementation 'com.squareup.leakcanary:leakcanary-android:' + leakCanaryVersion
|
||||||
|
|
||||||
testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:' + kotlinxCoroutinesVersion
|
|
||||||
testImplementation 'org.robolectric:robolectric:' + robolectricVersion
|
|
||||||
testImplementation project(modulePrefix + 'test-utils')
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
20
demos/compose/lint.xml
Normal file
20
demos/compose/lint.xml
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- 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.
|
||||||
|
-->
|
||||||
|
<lint>
|
||||||
|
<issue id="UnsafeOptInUsageError">
|
||||||
|
<option name="opt-in" value="androidx.media3.common.util.UnstableApi" />
|
||||||
|
</issue>
|
||||||
|
</lint>
|
@ -19,15 +19,18 @@ 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.layout.Arrangement
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.material3.Surface
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
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.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
|
||||||
@ -39,11 +42,6 @@ class MainActivity : ComponentActivity() {
|
|||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
enableEdgeToEdge()
|
enableEdgeToEdge()
|
||||||
setContent {
|
setContent {
|
||||||
Surface {
|
|
||||||
Column(
|
|
||||||
verticalArrangement = Arrangement.Center,
|
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
|
||||||
) {
|
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val exoPlayer = remember {
|
val exoPlayer = remember {
|
||||||
ExoPlayer.Builder(context).build().apply {
|
ExoPlayer.Builder(context).build().apply {
|
||||||
@ -53,13 +51,15 @@ class MainActivity : ComponentActivity() {
|
|||||||
repeatMode = Player.REPEAT_MODE_ONE
|
repeatMode = Player.REPEAT_MODE_ONE
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
PlayerSurface(
|
MediaPlayerScreen(player = exoPlayer, modifier = Modifier.fillMaxSize())
|
||||||
player = exoPlayer,
|
}
|
||||||
surfaceType = SURFACE_TYPE_SURFACE_VIEW,
|
}
|
||||||
modifier = Modifier.align(Alignment.CenterHorizontally),
|
|
||||||
)
|
@Composable
|
||||||
}
|
private fun MediaPlayerScreen(player: Player, modifier: Modifier = Modifier) {
|
||||||
}
|
Box(modifier) {
|
||||||
|
PlayerSurface(player = player, surfaceType = SURFACE_TYPE_SURFACE_VIEW)
|
||||||
|
PlayPauseButton(player, Modifier.align(Alignment.Center).size(100.dp))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,41 @@
|
|||||||
|
/*
|
||||||
|
* 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.PauseCircle
|
||||||
|
import androidx.compose.material.icons.filled.PlayCircle
|
||||||
|
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.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 contentDescription =
|
||||||
|
if (state.showPlay) stringResource(R.string.playpause_button_play)
|
||||||
|
else stringResource(R.string.playpause_button_pause)
|
||||||
|
IconButton(onClick = state::onClick, modifier = modifier, enabled = state.isEnabled) {
|
||||||
|
Icon(icon, contentDescription = contentDescription, modifier = modifier)
|
||||||
|
}
|
||||||
|
}
|
@ -15,12 +15,6 @@
|
|||||||
-->
|
-->
|
||||||
<resources>
|
<resources>
|
||||||
<string name="app_name">Media3 Compose Demo</string>
|
<string name="app_name">Media3 Compose Demo</string>
|
||||||
<string name="current_playlist_name">Current playlist</string>
|
<string name="playpause_button_play">Play</string>
|
||||||
<string name="open_player_content_description">Click to view your play list</string>
|
<string name="playpause_button_pause">Pause</string>
|
||||||
<string name="added_media_item_format">Added %1$s to playlist</string>
|
|
||||||
<string name="shuffle">Shuffle</string>
|
|
||||||
<string name="play_button">Play</string>
|
|
||||||
<string name="waiting_for_metadata">Waiting for playlist to load…</string>
|
|
||||||
<string name="notification_permission_denied">
|
|
||||||
"Without notification access the app can't warn about failed background operations"</string>
|
|
||||||
</resources>
|
</resources>
|
||||||
|
@ -55,6 +55,11 @@ dependencies {
|
|||||||
|
|
||||||
implementation 'androidx.compose.foundation:foundation'
|
implementation 'androidx.compose.foundation:foundation'
|
||||||
implementation 'androidx.core:core:' + androidxCoreVersion
|
implementation 'androidx.core:core:' + androidxCoreVersion
|
||||||
|
|
||||||
|
testImplementation 'androidx.compose.ui:ui-test'
|
||||||
|
testImplementation 'androidx.compose.ui:ui-test-junit4'
|
||||||
|
testImplementation project(modulePrefix + 'test-utils')
|
||||||
|
testImplementation 'org.robolectric:robolectric:' + robolectricVersion
|
||||||
}
|
}
|
||||||
|
|
||||||
ext {
|
ext {
|
||||||
|
@ -0,0 +1,100 @@
|
|||||||
|
/*
|
||||||
|
* 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.Timeline
|
||||||
|
import androidx.media3.common.listen
|
||||||
|
import androidx.media3.common.util.UnstableApi
|
||||||
|
import androidx.media3.common.util.Util.handlePauseButtonAction
|
||||||
|
import androidx.media3.common.util.Util.handlePlayButtonAction
|
||||||
|
import androidx.media3.common.util.Util.handlePlayPauseButtonAction
|
||||||
|
import androidx.media3.common.util.Util.shouldEnablePlayPauseButton
|
||||||
|
import androidx.media3.common.util.Util.shouldShowPlayButton
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remembers the value of [PlayPauseButtonState] 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 rememberPlayPauseButtonState(player: Player): PlayPauseButtonState {
|
||||||
|
val playPauseButtonState = remember(player) { PlayPauseButtonState(player) }
|
||||||
|
LaunchedEffect(player) { playPauseButtonState.observe() }
|
||||||
|
return playPauseButtonState
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* State that converts the necessary information from the [Player] to correctly deal with a UI
|
||||||
|
* component representing a PlayPause button.
|
||||||
|
*
|
||||||
|
* @property[isEnabled] determined by `isCommandAvailable(Player.COMMAND_PLAY_PAUSE)` and having
|
||||||
|
* something in the [Timeline] to play
|
||||||
|
* @property[showPlay] determined by [shouldShowPlayButton]
|
||||||
|
*/
|
||||||
|
@UnstableApi
|
||||||
|
class PlayPauseButtonState(private val player: Player) {
|
||||||
|
var isEnabled by mutableStateOf(shouldEnablePlayPauseButton(player))
|
||||||
|
private set
|
||||||
|
|
||||||
|
var showPlay by mutableStateOf(shouldShowPlayButton(player))
|
||||||
|
private set
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles the interaction with the PlayPause button according to the current state of the
|
||||||
|
* [Player].
|
||||||
|
*
|
||||||
|
* The [Player] update that follows can take a form of [Player.play], [Player.pause],
|
||||||
|
* [Player.prepare] or [Player.seekToDefaultPosition].
|
||||||
|
*
|
||||||
|
* @see [handlePlayButtonAction]
|
||||||
|
* @see [handlePauseButtonAction]
|
||||||
|
* @see [shouldShowPlayButton]
|
||||||
|
*/
|
||||||
|
fun onClick() {
|
||||||
|
handlePlayPauseButtonAction(player)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subscribes to updates from [Player.Events] and listens to
|
||||||
|
* * [Player.EVENT_PLAYBACK_STATE_CHANGED] and [Player.EVENT_PLAY_WHEN_READY_CHANGED] in order to
|
||||||
|
* determine whether a play or a pause button should be presented on a UI element for playback
|
||||||
|
* control.
|
||||||
|
* * [Player.EVENT_AVAILABLE_COMMANDS_CHANGED] in order to determine whether the button should be
|
||||||
|
* enabled, i.e. respond to user input.
|
||||||
|
*/
|
||||||
|
suspend fun observe(): Nothing =
|
||||||
|
player.listen { events ->
|
||||||
|
if (
|
||||||
|
events.containsAny(
|
||||||
|
Player.EVENT_PLAYBACK_STATE_CHANGED,
|
||||||
|
Player.EVENT_PLAY_WHEN_READY_CHANGED,
|
||||||
|
Player.EVENT_AVAILABLE_COMMANDS_CHANGED,
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
showPlay = shouldShowPlayButton(this)
|
||||||
|
isEnabled = shouldEnablePlayPauseButton(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -14,6 +14,22 @@
|
|||||||
limitations under the License.
|
limitations under the License.
|
||||||
-->
|
-->
|
||||||
|
|
||||||
<manifest package="androidx.media3.ui.compose.test">
|
<manifest package="androidx.media3.ui.compose.test"
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<uses-sdk/>
|
<uses-sdk/>
|
||||||
|
|
||||||
|
<application>
|
||||||
|
<!--
|
||||||
|
Setting a base ComponentActivity as the test app's main activity.
|
||||||
|
The androidx.compose.ui.test.junit4.createComposeRule() defaults to this
|
||||||
|
activity, so in order for the runner to resolve it, it must be defined here.
|
||||||
|
-->
|
||||||
|
<activity android:name="androidx.activity.ComponentActivity"
|
||||||
|
android:exported="true">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
|
</intent-filter>
|
||||||
|
</activity>
|
||||||
|
</application>
|
||||||
</manifest>
|
</manifest>
|
||||||
|
@ -0,0 +1,206 @@
|
|||||||
|
/*
|
||||||
|
* 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 android.os.Looper
|
||||||
|
import androidx.compose.ui.test.junit4.createComposeRule
|
||||||
|
import androidx.media3.common.Player
|
||||||
|
import androidx.media3.common.SimpleBasePlayer
|
||||||
|
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
|
||||||
|
|
||||||
|
/** Unit test for [PlayPauseButtonState]. */
|
||||||
|
@RunWith(AndroidJUnit4::class)
|
||||||
|
class PlayPauseButtonStateTest {
|
||||||
|
|
||||||
|
@get:Rule val composeTestRule = createComposeRule()
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun playerIsBuffering_pausePlayer_playIconShowing() {
|
||||||
|
val player = TestPlayer()
|
||||||
|
player.playbackState = Player.STATE_BUFFERING
|
||||||
|
player.play()
|
||||||
|
|
||||||
|
lateinit var state: PlayPauseButtonState
|
||||||
|
composeTestRule.setContent { state = rememberPlayPauseButtonState(player = player) }
|
||||||
|
|
||||||
|
assertThat(state.showPlay).isFalse()
|
||||||
|
|
||||||
|
player.pause()
|
||||||
|
composeTestRule.waitForIdle()
|
||||||
|
|
||||||
|
assertThat(state.showPlay).isTrue()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun playerIsIdling_preparePlayer_pauseIconShowing() {
|
||||||
|
val player = TestPlayer()
|
||||||
|
player.playbackState = Player.STATE_IDLE
|
||||||
|
player.play()
|
||||||
|
|
||||||
|
lateinit var state: PlayPauseButtonState
|
||||||
|
composeTestRule.setContent { state = rememberPlayPauseButtonState(player = player) }
|
||||||
|
|
||||||
|
assertThat(state.showPlay).isTrue()
|
||||||
|
|
||||||
|
player.prepare()
|
||||||
|
composeTestRule.waitForIdle()
|
||||||
|
|
||||||
|
assertThat(state.showPlay).isFalse()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun addPlayPauseCommandToPlayer_buttonStateTogglesFromDisabledToEnabled() {
|
||||||
|
val player = TestPlayer()
|
||||||
|
player.playbackState = Player.STATE_READY
|
||||||
|
player.play()
|
||||||
|
player.removeCommands(Player.COMMAND_PLAY_PAUSE)
|
||||||
|
|
||||||
|
lateinit var state: PlayPauseButtonState
|
||||||
|
composeTestRule.setContent { state = rememberPlayPauseButtonState(player = player) }
|
||||||
|
|
||||||
|
assertThat(state.isEnabled).isFalse()
|
||||||
|
|
||||||
|
player.addCommands(Player.COMMAND_PLAY_PAUSE)
|
||||||
|
composeTestRule.waitForIdle()
|
||||||
|
|
||||||
|
assertThat(state.isEnabled).isTrue()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun playerInReadyState_buttonClicked_playerPaused() {
|
||||||
|
val player = TestPlayer()
|
||||||
|
player.playbackState = Player.STATE_READY
|
||||||
|
player.play()
|
||||||
|
|
||||||
|
val state = PlayPauseButtonState(player)
|
||||||
|
|
||||||
|
assertThat(state.showPlay).isFalse()
|
||||||
|
assertThat(player.playWhenReady).isTrue()
|
||||||
|
|
||||||
|
state.onClick() // Player pauses
|
||||||
|
|
||||||
|
assertThat(player.playWhenReady).isFalse()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun playerInEndedState_buttonClicked_playerBuffersAndPlays() {
|
||||||
|
val player = TestPlayer()
|
||||||
|
player.playbackState = Player.STATE_ENDED
|
||||||
|
player.setPosition(456)
|
||||||
|
val state = PlayPauseButtonState(player)
|
||||||
|
|
||||||
|
assertThat(state.showPlay).isTrue()
|
||||||
|
|
||||||
|
state.onClick() // Player seeks to default position and plays
|
||||||
|
|
||||||
|
assertThat(player.contentPosition).isEqualTo(0)
|
||||||
|
assertThat(player.playWhenReady).isTrue()
|
||||||
|
assertThat(player.playbackState).isEqualTo(Player.STATE_BUFFERING)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun playerInIdleState_buttonClicked_playerBuffersAndPlays() {
|
||||||
|
val player = TestPlayer()
|
||||||
|
player.playbackState = Player.STATE_IDLE
|
||||||
|
val state = PlayPauseButtonState(player)
|
||||||
|
|
||||||
|
assertThat(state.showPlay).isTrue() // Player not prepared, Play icon
|
||||||
|
|
||||||
|
state.onClick() // Player prepares and goes into buffering
|
||||||
|
|
||||||
|
assertThat(player.playWhenReady).isTrue()
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user