Skip to content

Commit

Permalink
feat: Added shuffle and repeat to the player
Browse files Browse the repository at this point in the history
Signed-off-by: Gabriel Fontán <gabilessto@gmail.com>
  • Loading branch information
BobbyESP committed May 5, 2024
1 parent ad5a26d commit 62ed746
Show file tree
Hide file tree
Showing 13 changed files with 202 additions and 127 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import androidx.media3.common.util.UnstableApi
import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.exoplayer.trackselection.DefaultTrackSelector
import androidx.media3.session.MediaLibraryService.MediaLibrarySession
import androidx.media3.session.MediaSession
import com.bobbyesp.mediaplayer.service.ConnectionHandler
import com.bobbyesp.mediaplayer.service.MediaLibrarySessionCallback
import com.bobbyesp.mediaplayer.service.MediaServiceHandler
Expand Down Expand Up @@ -61,11 +60,11 @@ object MediaPlayerModule {

@Provides
@Singleton
fun provideMediaSession(
fun provideMediaLibrarySession(
@ApplicationContext context: Context,
player: ExoPlayer,
mediaLibrarySessionCallback: MediaLibrarySessionCallback,
): MediaSession =
): MediaLibrarySession =
MediaLibrarySession.Builder(context, player, mediaLibrarySessionCallback)
.setSessionActivity(
PendingIntent.getActivity(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@ package com.bobbyesp.mediaplayer.service

import android.os.Bundle
import androidx.media3.common.MediaItem
import androidx.media3.common.MediaMetadata
import androidx.media3.common.Player
import androidx.media3.common.Player.EVENT_POSITION_DISCONTINUITY
import androidx.media3.common.Player.EVENT_TIMELINE_CHANGED
import androidx.media3.common.Player.REPEAT_MODE_ALL
import androidx.media3.common.Player.REPEAT_MODE_OFF
import androidx.media3.common.Player.REPEAT_MODE_ONE
import androidx.media3.common.Player.STATE_IDLE
import androidx.media3.common.util.UnstableApi
import androidx.media3.exoplayer.ExoPlayer
Expand All @@ -15,7 +17,7 @@ import androidx.media3.exoplayer.analytics.PlaybackStatsListener
import androidx.media3.exoplayer.source.ShuffleOrder
import androidx.media3.session.SessionCommand
import com.bobbyesp.mediaplayer.ext.toMediaItem
import com.bobbyesp.mediaplayer.service.notifications.MediaSessionLayoutHandler
import com.bobbyesp.mediaplayer.service.notifications.customLayout.MediaSessionLayoutHandler
import com.bobbyesp.mediaplayer.service.queue.EmptyQueue
import com.bobbyesp.mediaplayer.service.queue.Queue
import kotlinx.coroutines.CoroutineScope
Expand Down Expand Up @@ -47,7 +49,9 @@ class MediaServiceHandler @Inject constructor(

val currentMediaItem = MutableStateFlow<MediaItem?>(null)

val isThePlayerPlaying: MutableStateFlow<Boolean> = MutableStateFlow(false)
val isPlaying: MutableStateFlow<Boolean> = MutableStateFlow(false)
val shuffleModeEnabled = MutableStateFlow(false)
val repeatMode = MutableStateFlow(REPEAT_MODE_OFF)

private var job: Job? = null
private val scope = CoroutineScope(Dispatchers.Main)
Expand All @@ -59,15 +63,16 @@ class MediaServiceHandler @Inject constructor(

init {
player.addListener(this)
repeatMode.update { player.repeatMode }
shuffleModeEnabled.update { player.shuffleModeEnabled }
job = Job()
}

override fun onIsPlayingChanged(isPlaying: Boolean) {
mediaSessionInterface.updateNotificationLayout()
_mediaState.update {
MediaState.Playing(isPlaying)
}
isThePlayerPlaying.update {
this.isPlaying.update {
isPlaying
}
super.onIsPlayingChanged(isPlaying)
Expand All @@ -91,15 +96,6 @@ class MediaServiceHandler @Inject constructor(
player.prepare()
}

/**
* Sets a list of media items as the current queue and prepares the player for playback.
* @param mediaItems The list of media items to be set.
*/
fun setMediaItems(mediaItems: List<MediaItem>) {
player.setMediaItems(mediaItems)
player.prepare()
}

fun playQueue(queue: Queue, playWhenReady: Boolean = true) {
currentQueue = queue
queueTitle = null
Expand Down Expand Up @@ -152,46 +148,6 @@ class MediaServiceHandler @Inject constructor(
}
}

/**
* Adds a media item to the end of the queue and prepares the player for playback.
* @param mediaItem The media item to be added.
*/
fun addMediaItem(mediaItem: MediaItem) {
player.addMediaItem(mediaItem)
player.prepare()
}

/**
* Removes a media item from the queue.
* @param index The media item to be removed.
*/
fun removeMediaItemAtIndex(index: Int) {
player.removeMediaItem(index)
}

/**
* Moves a media item within the queue.
* @param currentIndex The current index of the media item.
* @param newIndex The new index for the media item.
*/
fun moveMediaItem(currentIndex: Int, newIndex: Int) {
player.moveMediaItem(currentIndex, newIndex)
}

/**
* Skips to the next media item in the queue.
*/
fun skipToNext() {
player.seekToNext()
}

/**
* Skips to the previous media item in the queue.
*/
fun skipToPrevious() {
player.seekToPrevious()
}

/**
* Seeks to a specific position in the current media item.
* @param positionMs The position to seek to, in milliseconds.
Expand Down Expand Up @@ -229,31 +185,21 @@ class MediaServiceHandler @Inject constructor(
is PlayerEvent.UpdateProgress -> {
player.seekTo(playerEvent.updatedProgress)
}
}
}

fun getActualMediaItem(): MediaItem? {
return player.currentMediaItem
}

fun getActualMediaItemIndex(): Int {
return player.currentMediaItemIndex
}

fun getActualMediaItemMetadata(): MediaMetadata? {
return player.currentMediaItem?.mediaMetadata
}

fun getActualMediaItemDuration(): Long {
return player.duration
}

fun getActualMediaItemPosition(): Long {
return player.currentPosition
}
PlayerEvent.ToggleRepeat -> {
val repeatMode = when (player.repeatMode) {
REPEAT_MODE_OFF -> REPEAT_MODE_ONE
REPEAT_MODE_ONE -> REPEAT_MODE_ALL
REPEAT_MODE_ALL -> REPEAT_MODE_OFF
else -> REPEAT_MODE_OFF
}
player.repeatMode = repeatMode
}

fun getActualMediaItemPositionPercentage(): Float {
return player.currentPosition / player.duration.toFloat()
PlayerEvent.ToggleShuffle -> {
player.shuffleModeEnabled = !player.shuffleModeEnabled
}
}
}

override fun onEvents(player: Player, events: Player.Events) {
Expand Down Expand Up @@ -300,6 +246,7 @@ class MediaServiceHandler @Inject constructor(
override fun onShuffleModeEnabledChanged(shuffleModeEnabled: Boolean) {
// Update the notification layout
mediaSessionInterface.updateNotificationLayout()
this.shuffleModeEnabled.update { shuffleModeEnabled }

// If shuffle mode is enabled
if (shuffleModeEnabled) {
Expand Down Expand Up @@ -327,6 +274,7 @@ class MediaServiceHandler @Inject constructor(

override fun onRepeatModeChanged(repeatMode: Int) {
mediaSessionInterface.updateNotificationLayout()
this.repeatMode.update { repeatMode }
}

private suspend fun startProgressUpdate() = job.run {
Expand Down Expand Up @@ -369,6 +317,8 @@ sealed class PlayerEvent {
data object Stop : PlayerEvent()
data object Next : PlayerEvent()
data object Previous : PlayerEvent()
data object ToggleShuffle : PlayerEvent()
data object ToggleRepeat : PlayerEvent()
data class UpdateProgress(val updatedProgress: Long) : PlayerEvent()
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,21 +9,22 @@ import androidx.media3.common.Player.REPEAT_MODE_OFF
import androidx.media3.common.Player.REPEAT_MODE_ONE
import androidx.media3.common.util.UnstableApi
import androidx.media3.session.CommandButton
import androidx.media3.session.MediaLibraryService
import androidx.media3.session.MediaSession
import androidx.media3.session.MediaSessionService
import com.bobbyesp.mediaplayer.R
import com.bobbyesp.mediaplayer.service.MediaServiceHandler.Companion.CommandToggleRepeatMode
import com.bobbyesp.mediaplayer.service.MediaServiceHandler.Companion.CommandToggleShuffle
import com.bobbyesp.mediaplayer.service.notifications.MediaNotificationManager
import com.bobbyesp.mediaplayer.service.notifications.MediaSessionLayoutHandler
import com.bobbyesp.mediaplayer.service.notifications.customLayout.MediaSessionLayoutHandler
import com.google.common.collect.ImmutableList
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject

@UnstableApi
@AndroidEntryPoint
class MediaplayerService : MediaSessionService(), MediaSessionLayoutHandler {
class MediaplayerService : MediaLibraryService(), MediaSessionLayoutHandler {
@Inject
lateinit var mediaSession: MediaSession
lateinit var mediaSession: MediaLibrarySession

@Inject
lateinit var notificationManager: MediaNotificationManager
Expand All @@ -39,44 +40,38 @@ class MediaplayerService : MediaSessionService(), MediaSessionLayoutHandler {
Log.i("MediaplayerService", "Shuffle mode: ${mediaSession.player.shuffleModeEnabled}")
Log.i("MediaplayerService", "Repeat mode: ${mediaSession.player.repeatMode}")

mediaSession.setCustomLayout(
listOf(
CommandButton.Builder()
.setDisplayName(getString(if (mediaSession.player.shuffleModeEnabled) R.string.action_shuffle_off else R.string.action_shuffle_on))
.setIconResId(if (mediaSession.player.shuffleModeEnabled) R.drawable.shuffle_on else R.drawable.shuffle)
.setSessionCommand(CommandToggleShuffle)
.build(),
CommandButton.Builder()
.setDisplayName(
getString(
when (mediaSession.player.repeatMode) {
REPEAT_MODE_OFF -> R.string.repeat_mode_off
REPEAT_MODE_ONE -> R.string.repeat_mode_one
REPEAT_MODE_ALL -> R.string.repeat_mode_all
else -> throw IllegalStateException()
}
)
)
.setIconResId(
val commandButtons = ImmutableList.of<CommandButton>(
CommandButton.Builder()
.setDisplayName(getString(if (mediaSession.player.shuffleModeEnabled) R.string.action_shuffle_off else R.string.action_shuffle_on))
.setIconResId(if (mediaSession.player.shuffleModeEnabled) R.drawable.shuffle_on else R.drawable.shuffle)
.setSessionCommand(CommandToggleShuffle).build(),
CommandButton.Builder()
.setDisplayName(
getString(
when (mediaSession.player.repeatMode) {
REPEAT_MODE_OFF -> R.drawable.repeat
REPEAT_MODE_ONE -> R.drawable.repeat_one_on
REPEAT_MODE_ALL -> R.drawable.repeat_on
REPEAT_MODE_OFF -> R.string.repeat_mode_off
REPEAT_MODE_ONE -> R.string.repeat_mode_one
REPEAT_MODE_ALL -> R.string.repeat_mode_all
else -> throw IllegalStateException()
}
)
.setSessionCommand(CommandToggleRepeatMode)
.build()
)
).setIconResId(
when (mediaSession.player.repeatMode) {
REPEAT_MODE_OFF -> R.drawable.repeat
REPEAT_MODE_ONE -> R.drawable.repeat_one_on
REPEAT_MODE_ALL -> R.drawable.repeat_on
else -> throw IllegalStateException()
}
).setSessionCommand(CommandToggleRepeatMode).build()
)
mediaSession.setCustomLayout(commandButtons)
}

@UnstableApi
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
connectionHandler.connect(mediaServiceHandler)
notificationManager.startNotificationService(
mediaSessionService = this,
mediaSession = mediaSession
mediaSessionService = this, mediaSession = mediaSession
)
mediaServiceHandler.setMediaSessionInterface(this)
return super.onStartCommand(intent, flags, startId)
Expand All @@ -101,7 +96,7 @@ class MediaplayerService : MediaSessionService(), MediaSessionLayoutHandler {
stopSelf()
}

override fun onGetSession(controllerInfo: MediaSession.ControllerInfo): MediaSession =
override fun onGetSession(controllerInfo: MediaSession.ControllerInfo): MediaLibrarySession =
mediaSession

inner class MusicBinder : Binder() {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.bobbyesp.mediaplayer.service.notifications
package com.bobbyesp.mediaplayer.service.notifications.customLayout

interface MediaSessionLayoutHandler {
fun updateNotificationLayout()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package com.bobbyesp.metadator.presentation.components.others

import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Repeat
import androidx.compose.material.icons.rounded.RepeatOne
import androidx.compose.material3.Icon
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.res.stringResource
import androidx.media3.common.Player.REPEAT_MODE_ALL
import androidx.media3.common.Player.REPEAT_MODE_OFF
import androidx.media3.common.Player.REPEAT_MODE_ONE

@Composable
fun RepeatStateIcon(
modifier: Modifier = Modifier,
repeatMode: Int
) {
when (repeatMode) {
REPEAT_MODE_OFF -> {
Icon(
imageVector = Icons.Rounded.Repeat,
contentDescription = stringResource(id = com.bobbyesp.mediaplayer.R.string.repeat_mode_off),
modifier = modifier.alpha(0.5f)
)
}

REPEAT_MODE_ONE -> {
Icon(
imageVector = Icons.Rounded.RepeatOne,
contentDescription = stringResource(id = com.bobbyesp.mediaplayer.R.string.repeat_mode_one),
modifier = modifier
)
}

REPEAT_MODE_ALL -> {
Icon(
imageVector = Icons.Rounded.Repeat,
contentDescription = stringResource(id = com.bobbyesp.mediaplayer.R.string.repeat_mode_all),
modifier = modifier
)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package com.bobbyesp.metadator.presentation.components.others

import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Shuffle
import androidx.compose.material.icons.rounded.ShuffleOn
import androidx.compose.material3.Icon
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource

@Composable
fun ShuffleStateIcon(
modifier: Modifier = Modifier,
isShuffleEnabled: Boolean
) {
when (isShuffleEnabled) {
true -> {
Icon(
imageVector = Icons.Rounded.ShuffleOn,
contentDescription = stringResource(id = com.bobbyesp.mediaplayer.R.string.action_shuffle_on),
modifier = modifier
)
}

false -> {
Icon(
imageVector = Icons.Rounded.Shuffle,
contentDescription = stringResource(id = com.bobbyesp.mediaplayer.R.string.action_shuffle_off),
modifier = modifier
)
}
}
}
Loading

0 comments on commit 62ed746

Please sign in to comment.