Skip to content

Commit

Permalink
feat/WIP: Started adding repeat and shuffle options
Browse files Browse the repository at this point in the history
Signed-off-by: Gabriel Fontán <[email protected]>
  • Loading branch information
BobbyESP committed May 5, 2024
1 parent 9154046 commit 6972f64
Show file tree
Hide file tree
Showing 15 changed files with 332 additions and 18 deletions.
6 changes: 6 additions & 0 deletions .idea/studiobot.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,10 @@ import androidx.media3.common.C
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
import com.bobbyesp.mediaplayer.service.notifications.MediaNotificationManager
import dagger.Module
Expand Down Expand Up @@ -60,9 +62,10 @@ object MediaPlayerModule {
@Singleton
fun provideMediaSession(
@ApplicationContext context: Context,
player: ExoPlayer
player: ExoPlayer,
mediaLibrarySessionCallback: MediaLibrarySessionCallback
): MediaSession =
MediaSession.Builder(context, player)
MediaLibrarySession.Builder(context, player, mediaLibrarySessionCallback)
.build()

@Provides
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package com.bobbyesp.mediaplayer.service

import android.content.Context
import android.os.Bundle
import androidx.annotation.OptIn
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.util.UnstableApi
import androidx.media3.session.MediaLibraryService
import androidx.media3.session.MediaSession
import androidx.media3.session.SessionCommand
import androidx.media3.session.SessionResult
import com.bobbyesp.mediaplayer.service.MediaServiceHandler.Companion.ACTION_TOGGLE_LIBRARY
import com.bobbyesp.mediaplayer.service.MediaServiceHandler.Companion.ACTION_TOGGLE_LIKE
import com.bobbyesp.mediaplayer.service.MediaServiceHandler.Companion.ACTION_TOGGLE_REPEAT_MODE
import com.bobbyesp.mediaplayer.service.MediaServiceHandler.Companion.ACTION_TOGGLE_SHUFFLE
import com.bobbyesp.mediaplayer.service.MediaServiceHandler.Companion.CommandToggleLibrary
import com.bobbyesp.mediaplayer.service.MediaServiceHandler.Companion.CommandToggleLike
import com.bobbyesp.mediaplayer.service.MediaServiceHandler.Companion.CommandToggleRepeatMode
import com.bobbyesp.mediaplayer.service.MediaServiceHandler.Companion.CommandToggleShuffle
import com.google.common.util.concurrent.Futures
import com.google.common.util.concurrent.ListenableFuture
import dagger.hilt.android.qualifiers.ApplicationContext
import javax.inject.Inject

class MediaLibrarySessionCallback @Inject constructor(
@ApplicationContext val context: Context
) : MediaLibraryService.MediaLibrarySession.Callback {

override fun onConnect(
session: MediaSession,
controller: MediaSession.ControllerInfo
): MediaSession.ConnectionResult {
val connectionResult = super.onConnect(session, controller)
return MediaSession.ConnectionResult.accept(
connectionResult.availableSessionCommands.buildUpon()
.add(CommandToggleLibrary)
.add(CommandToggleLike)
.add(CommandToggleShuffle)
.add(CommandToggleRepeatMode)
.build(),
connectionResult.availablePlayerCommands
)
}

@OptIn(UnstableApi::class)
override fun onCustomCommand(
session: MediaSession,
controller: MediaSession.ControllerInfo,
customCommand: SessionCommand,
args: Bundle,
): ListenableFuture<SessionResult> {
when (customCommand.customAction) {
ACTION_TOGGLE_LIKE -> TODO()
ACTION_TOGGLE_LIBRARY -> TODO()
ACTION_TOGGLE_SHUFFLE -> session.player.shuffleModeEnabled =
!session.player.shuffleModeEnabled

ACTION_TOGGLE_REPEAT_MODE -> session.player.repeatMode =
when (session.player.repeatMode) {
REPEAT_MODE_OFF -> REPEAT_MODE_ONE
REPEAT_MODE_ONE -> REPEAT_MODE_ALL
REPEAT_MODE_ALL -> REPEAT_MODE_OFF
else -> REPEAT_MODE_OFF
}
}
return Futures.immediateFuture(SessionResult(SessionResult.RESULT_SUCCESS))
}

}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.bobbyesp.mediaplayer.service

import android.os.Bundle
import androidx.media3.common.MediaItem
import androidx.media3.common.MediaMetadata
import androidx.media3.common.Player
Expand All @@ -11,7 +12,10 @@ import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.exoplayer.analytics.AnalyticsListener
import androidx.media3.exoplayer.analytics.PlaybackStats
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.queue.EmptyQueue
import com.bobbyesp.mediaplayer.service.queue.Queue
import kotlinx.coroutines.CoroutineScope
Expand Down Expand Up @@ -47,7 +51,19 @@ class MediaServiceHandler @Inject constructor(

private var job: Job? = null
private val scope = CoroutineScope(Dispatchers.Main)
private lateinit var mediaSessionInterface: MediaSessionLayoutHandler

fun setMediaSessionInterface(mediaSessionInterface: MediaSessionLayoutHandler) {
this.mediaSessionInterface = mediaSessionInterface
}

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

override fun onIsPlayingChanged(isPlaying: Boolean) {
mediaSessionInterface.updateNotificationLayout()
_mediaState.update {
MediaState.Playing(isPlaying)
}
Expand All @@ -57,11 +73,6 @@ class MediaServiceHandler @Inject constructor(
super.onIsPlayingChanged(isPlaying)
}

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

/**
* Stops the player, clears the media queue, and releases resources.
*/
Expand Down Expand Up @@ -221,13 +232,6 @@ class MediaServiceHandler @Inject constructor(
}
}

override fun onEvents(player: Player, events: Player.Events) {
super.onEvents(player, events)
if (events.containsAny(EVENT_TIMELINE_CHANGED, EVENT_POSITION_DISCONTINUITY)) {
currentMediaItem.value = player.currentMediaItem
}
}

fun getActualMediaItem(): MediaItem? {
return player.currentMediaItem
}
Expand All @@ -252,6 +256,12 @@ class MediaServiceHandler @Inject constructor(
return player.currentPosition / player.duration.toFloat()
}

override fun onEvents(player: Player, events: Player.Events) {
super.onEvents(player, events)
if (events.containsAny(EVENT_TIMELINE_CHANGED, EVENT_POSITION_DISCONTINUITY)) {
currentMediaItem.value = player.currentMediaItem
}
}

override fun onPlaybackStateChanged(playbackState: Int) {
when (playbackState) {
Expand Down Expand Up @@ -281,6 +291,44 @@ class MediaServiceHandler @Inject constructor(
}
}

/**
* This method is triggered when the shuffle mode is enabled or disabled in the player.
* It updates the notification layout and, if shuffle mode is enabled, it shuffles the media items in the player.
*
* @param shuffleModeEnabled A boolean indicating whether shuffle mode is enabled.
*/
override fun onShuffleModeEnabledChanged(shuffleModeEnabled: Boolean) {
// Update the notification layout
mediaSessionInterface.updateNotificationLayout()

// If shuffle mode is enabled
if (shuffleModeEnabled) {
// Always put current playing item at first
// Create an array of indices representing the media items in the player
val shuffledIndices = IntArray(player.mediaItemCount) { it }

// Shuffle the indices
shuffledIndices.shuffle()

// Swap the current media item index with the first index
shuffledIndices[shuffledIndices.indexOf(player.currentMediaItemIndex)] =
shuffledIndices[0]
shuffledIndices[0] = player.currentMediaItemIndex

// Set the shuffle order in the player using the shuffled indices
player.setShuffleOrder(
ShuffleOrder.DefaultShuffleOrder(
shuffledIndices,
System.currentTimeMillis()
)
)
}
}

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

private suspend fun startProgressUpdate() = job.run {
while (true) {
delay(250)
Expand All @@ -303,6 +351,17 @@ class MediaServiceHandler @Inject constructor(
) {
TODO("Not yet implemented")
}

companion object {
const val ACTION_TOGGLE_LIBRARY = "TOGGLE_LIBRARY"
const val ACTION_TOGGLE_LIKE = "TOGGLE_LIKE"
const val ACTION_TOGGLE_SHUFFLE = "TOGGLE_SHUFFLE"
const val ACTION_TOGGLE_REPEAT_MODE = "TOGGLE_REPEAT_MODE"
val CommandToggleLibrary = SessionCommand(ACTION_TOGGLE_LIBRARY, Bundle.EMPTY)
val CommandToggleLike = SessionCommand(ACTION_TOGGLE_LIKE, Bundle.EMPTY)
val CommandToggleShuffle = SessionCommand(ACTION_TOGGLE_SHUFFLE, Bundle.EMPTY)
val CommandToggleRepeatMode = SessionCommand(ACTION_TOGGLE_REPEAT_MODE, Bundle.EMPTY)
}
}

sealed class PlayerEvent {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,26 @@ package com.bobbyesp.mediaplayer.service

import android.content.Intent
import android.os.Binder
import android.util.Log
import androidx.media3.common.Player
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.util.UnstableApi
import androidx.media3.session.CommandButton
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 dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject

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

Expand All @@ -25,14 +34,48 @@ class MediaplayerService : MediaSessionService() {
@Inject
lateinit var mediaServiceHandler: MediaServiceHandler

override fun updateNotificationLayout() {
Log.i("MediaplayerService", "Updating notification layout")
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(
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()
)
)
}

@UnstableApi
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
connectionHandler.connect(mediaServiceHandler)
notificationManager.startNotificationService(
mediaSessionService = this,
mediaSession = mediaSession
)

mediaServiceHandler.setMediaSessionInterface(this)
return super.onStartCommand(intent, flags, startId)
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.bobbyesp.mediaplayer.service.notifications

interface MediaSessionLayoutHandler {
fun updateNotificationLayout()
}
12 changes: 12 additions & 0 deletions app/mediaplayer/src/main/res/drawable/repeat.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:height="24dp"
android:tint="#000000"
android:viewportHeight="24"
android:viewportWidth="24"
android:width="24dp">

<path
android:fillColor="@android:color/white"
android:pathData="M7,7h10v1.79c0,0.45 0.54,0.67 0.85,0.35l2.79,-2.79c0.2,-0.2 0.2,-0.51 0,-0.71l-2.79,-2.79c-0.31,-0.31 -0.85,-0.09 -0.85,0.36L17,5L6,5c-0.55,0 -1,0.45 -1,1v4c0,0.55 0.45,1 1,1s1,-0.45 1,-1L7,7zM17,17L7,17v-1.79c0,-0.45 -0.54,-0.67 -0.85,-0.35l-2.79,2.79c-0.2,0.2 -0.2,0.51 0,0.71l2.79,2.79c0.31,0.31 0.85,0.09 0.85,-0.36L7,19h11c0.55,0 1,-0.45 1,-1v-4c0,-0.55 -0.45,-1 -1,-1s-1,0.45 -1,1v3z" />

</vector>
12 changes: 12 additions & 0 deletions app/mediaplayer/src/main/res/drawable/repeat_on.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:height="24dp"
android:tint="#000000"
android:viewportHeight="24"
android:viewportWidth="24"
android:width="24dp">

<path
android:fillColor="@android:color/white"
android:pathData="M21,1H3C1.9,1 1,1.9 1,3v18c0,1.1 0.9,2 2,2h18c1.1,0 2,-0.9 2,-2V3C23,1.9 22.1,1 21,1zM19,18c0,0.55 -0.45,1 -1,1H7v1.79c0,0.45 -0.54,0.67 -0.85,0.36l-2.79,-2.79c-0.2,-0.2 -0.2,-0.51 0,-0.71l2.79,-2.79C6.46,14.54 7,14.76 7,15.21V17h10v-3c0,-0.55 0.45,-1 1,-1s1,0.45 1,1V18zM20.64,6.35l-2.79,2.79C17.54,9.46 17,9.24 17,8.79V7H7v3c0,0.55 -0.45,1 -1,1s-1,-0.45 -1,-1V6c0,-0.55 0.45,-1 1,-1h11V3.21c0,-0.45 0.54,-0.67 0.85,-0.36l2.79,2.79C20.84,5.84 20.84,6.15 20.64,6.35z" />

</vector>
12 changes: 12 additions & 0 deletions app/mediaplayer/src/main/res/drawable/repeat_one_on.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:height="24dp"
android:tint="#000000"
android:viewportHeight="24"
android:viewportWidth="24"
android:width="24dp">

<path
android:fillColor="@android:color/white"
android:pathData="M21,1H3C1.9,1 1,1.9 1,3v18c0,1.1 0.9,2 2,2h18c1.1,0 2,-0.9 2,-2V3C23,1.9 22.1,1 21,1zM19,18c0,0.55 -0.45,1 -1,1H7v1.79c0,0.45 -0.54,0.67 -0.85,0.36l-2.79,-2.79c-0.2,-0.2 -0.2,-0.51 0,-0.71l2.79,-2.79C6.46,14.54 7,14.76 7,15.21V17h10v-3c0,-0.55 0.45,-1 1,-1s1,0.45 1,1V18zM10.75,10.5c-0.41,0 -0.75,-0.34 -0.75,-0.75S10.34,9 10.75,9h1.5C12.66,9 13,9.34 13,9.75v4.5c0,0.41 -0.34,0.75 -0.75,0.75s-0.75,-0.34 -0.75,-0.75V10.5H10.75zM20.64,6.35l-2.79,2.79C17.54,9.46 17,9.24 17,8.79V7H7v3c0,0.55 -0.45,1 -1,1s-1,-0.45 -1,-1V6c0,-0.55 0.45,-1 1,-1h11V3.21c0,-0.45 0.54,-0.67 0.85,-0.36l2.79,2.79C20.84,5.84 20.84,6.15 20.64,6.35z" />

</vector>
12 changes: 12 additions & 0 deletions app/mediaplayer/src/main/res/drawable/shuffle.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:height="24dp"
android:tint="#000000"
android:viewportHeight="24"
android:viewportWidth="24"
android:width="24dp">

<path
android:fillColor="@android:color/white"
android:pathData="M10.59,9.17L6.12,4.7c-0.39,-0.39 -1.02,-0.39 -1.41,0 -0.39,0.39 -0.39,1.02 0,1.41l4.46,4.46 1.42,-1.4zM15.35,4.85l1.19,1.19L4.7,17.88c-0.39,0.39 -0.39,1.02 0,1.41 0.39,0.39 1.02,0.39 1.41,0L17.96,7.46l1.19,1.19c0.31,0.31 0.85,0.09 0.85,-0.36L20,4.5c0,-0.28 -0.22,-0.5 -0.5,-0.5h-3.79c-0.45,0 -0.67,0.54 -0.36,0.85zM14.83,13.41l-1.41,1.41 3.13,3.13 -1.2,1.2c-0.31,0.31 -0.09,0.85 0.36,0.85h3.79c0.28,0 0.5,-0.22 0.5,-0.5v-3.79c0,-0.45 -0.54,-0.67 -0.85,-0.35l-1.19,1.19 -3.13,-3.14z" />

</vector>
12 changes: 12 additions & 0 deletions app/mediaplayer/src/main/res/drawable/shuffle_on.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:height="24dp"
android:tint="#000000"
android:viewportHeight="24"
android:viewportWidth="24"
android:width="24dp">

<path
android:fillColor="@android:color/white"
android:pathData="M21,1H3C1.9,1 1,1.9 1,3v18c0,1.1 0.9,2 2,2h18c1.1,0 2,-0.9 2,-2V3C23,1.9 22.1,1 21,1zM4.3,4.7c0.39,-0.39 1.02,-0.39 1.41,0l4.47,4.47l-1.42,1.4L4.3,6.11C3.91,5.72 3.91,5.09 4.3,4.7zM19.59,19.5c0,0.28 -0.22,0.5 -0.5,0.5H15.3c-0.45,0 -0.67,-0.54 -0.36,-0.85l1.2,-1.2l-3.13,-3.13l1.41,-1.41l3.13,3.14l1.19,-1.19c0.31,-0.32 0.85,-0.1 0.85,0.35V19.5zM19.59,8.29c0,0.45 -0.54,0.67 -0.85,0.36l-1.19,-1.19L5.7,19.29c-0.39,0.39 -1.02,0.39 -1.41,0c-0.39,-0.39 -0.39,-1.02 0,-1.41L16.13,6.04l-1.19,-1.19C14.63,4.54 14.85,4 15.3,4h3.79c0.28,0 0.5,0.22 0.5,0.5V8.29z" />

</vector>
Loading

0 comments on commit 6972f64

Please sign in to comment.