Loading app/src/main/java/org/lineageos/twelve/services/PlaybackService.kt +100 −6 Original line number Diff line number Diff line Loading @@ -7,6 +7,7 @@ package org.lineageos.twelve.services import android.app.PendingIntent import android.content.Intent import android.content.res.Resources import android.media.audiofx.AudioEffect import android.os.Bundle import android.os.IBinder Loading @@ -25,6 +26,7 @@ import androidx.media3.common.util.UnstableApi import androidx.media3.common.util.Util import androidx.media3.exoplayer.DefaultLoadControl import androidx.media3.exoplayer.ExoPlayer import androidx.media3.session.CommandButton import androidx.media3.session.DefaultMediaNotificationProvider import androidx.media3.session.LibraryResult import androidx.media3.session.MediaController Loading @@ -42,21 +44,24 @@ import org.lineageos.twelve.R import org.lineageos.twelve.TwelveApplication import org.lineageos.twelve.ext.enableFloatOutput import org.lineageos.twelve.ext.enableOffload import org.lineageos.twelve.ext.next import org.lineageos.twelve.ext.setOffloadEnabled import org.lineageos.twelve.ext.skipSilence import org.lineageos.twelve.ext.stopPlaybackOnTaskRemoved import org.lineageos.twelve.ext.typedRepeatMode import org.lineageos.twelve.models.RepeatMode import org.lineageos.twelve.ui.widgets.NowPlayingAppWidgetProvider @OptIn(UnstableApi::class) class PlaybackService : MediaLibraryService(), LifecycleOwner { enum class CustomCommand(val value: String, extras: Bundle) { enum class CustomCommand { /** * Toggles audio offload mode. * * Arguments: * - [ARG_VALUE] ([Boolean]): Whether to enable or disable offload */ TOGGLE_OFFLOAD("toggle_offload", Bundle.EMPTY), TOGGLE_OFFLOAD, /** * Toggles skip silence. Loading @@ -64,7 +69,7 @@ class PlaybackService : MediaLibraryService(), LifecycleOwner { * Arguments: * - [ARG_VALUE] ([Boolean]): Whether to enable or disable skip silence */ TOGGLE_SKIP_SILENCE("toggle_skip_silence", Bundle.EMPTY), TOGGLE_SKIP_SILENCE, /** * Get the audio session ID. Loading @@ -72,9 +77,68 @@ class PlaybackService : MediaLibraryService(), LifecycleOwner { * Response: * - [RSP_VALUE] ([Int]): The audio session ID */ GET_AUDIO_SESSION_ID("get_audio_session_id", Bundle.EMPTY); GET_AUDIO_SESSION_ID, val sessionCommand = SessionCommand(value, extras) /** * Toggle shuffle mode. * * Arguments: * - [ARG_VALUE] ([Boolean]): Whether to enable or disable shuffle mode */ TOGGLE_SHUFFLE { override fun buildCommandButton( player: ExoPlayer, resources: Resources, ) = player.shuffleModeEnabled.let { shuffleModeEnabled -> val (icon, titleStringResId) = when (shuffleModeEnabled) { true -> CommandButton.ICON_SHUFFLE_ON to R.string.shuffle_on false -> CommandButton.ICON_SHUFFLE_OFF to R.string.shuffle_off } CommandButton.Builder(icon) .setDisplayName(resources.getString(titleStringResId)) .setSessionCommand( SessionCommand( name, bundleOf(ARG_VALUE to !shuffleModeEnabled), ) ) .build() } }, /** * Toggle repeat mode. * * Arguments: * - [ARG_VALUE] ([String]): The repeat mode */ TOGGLE_REPEAT { override fun buildCommandButton( player: ExoPlayer, resources: Resources, ) = player.typedRepeatMode.let { repeatMode -> val (icon, titleStringResId) = when (repeatMode) { RepeatMode.NONE -> CommandButton.ICON_REPEAT_OFF to R.string.repeat_off RepeatMode.ALL -> CommandButton.ICON_REPEAT_ALL to R.string.repeat_all RepeatMode.ONE -> CommandButton.ICON_REPEAT_ONE to R.string.repeat_one } CommandButton.Builder(icon) .setDisplayName(resources.getString(titleStringResId)) .setSessionCommand( SessionCommand( name, bundleOf(ARG_VALUE to repeatMode.next().name), ) ) .build() } }; val sessionCommand = SessionCommand(name, Bundle.EMPTY) open fun buildCommandButton(player: ExoPlayer, resources: Resources): CommandButton? = null companion object { const val ARG_VALUE = "value" Loading @@ -82,7 +146,7 @@ class PlaybackService : MediaLibraryService(), LifecycleOwner { fun fromCustomAction( customAction: String ) = entries.firstOrNull { it.value == customAction } ) = entries.firstOrNull { it.name == customAction } suspend fun MediaController.sendCustomCommand( customCommand: CustomCommand, Loading Loading @@ -304,6 +368,22 @@ class PlaybackService : MediaLibraryService(), LifecycleOwner { ) } CustomCommand.TOGGLE_SHUFFLE -> { args.getBoolean(CustomCommand.ARG_VALUE).let { player.shuffleModeEnabled = it } SessionResult(SessionResult.RESULT_SUCCESS) } CustomCommand.TOGGLE_REPEAT -> { args.getString(CustomCommand.ARG_VALUE)?.let { player.typedRepeatMode = RepeatMode.valueOf(it) } SessionResult(SessionResult.RESULT_SUCCESS) } null -> SessionResult(SessionError.ERROR_NOT_SUPPORTED) } } Loading Loading @@ -349,6 +429,7 @@ class PlaybackService : MediaLibraryService(), LifecycleOwner { ) .setBitmapLoader(CoilBitmapLoader(this, lifecycleScope)) .setSessionActivity(getSingleTopActivity()) .setCustomLayout(getCustomLayout()) .build() setMediaNotificationProvider( Loading Loading @@ -391,6 +472,15 @@ class PlaybackService : MediaLibraryService(), LifecycleOwner { NowPlayingAppWidgetProvider.update(this@PlaybackService) } } // Update the shuffle and repeat buttons if (events.containsAny( Player.EVENT_REPEAT_MODE_CHANGED, Player.EVENT_SHUFFLE_MODE_ENABLED_CHANGED ) ) { mediaLibrarySession.setCustomLayout(getCustomLayout()) } } } } Loading Loading @@ -458,4 +548,8 @@ class PlaybackService : MediaLibraryService(), LifecycleOwner { }, PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT, ) private fun getCustomLayout() = CustomCommand.entries.mapNotNull { it.buildCommandButton(player, resources) } } app/src/main/res/values/strings.xml +7 −0 Original line number Diff line number Diff line Loading @@ -269,4 +269,11 @@ <string name="provider">Provider</string> <string name="provider_format" translatable="false">%1$s (%2$s)</string> <string name="create_playlist_error_empty_name">No name specified</string> <!-- Player actions --> <string name="shuffle_on">Shuffle on</string> <string name="shuffle_off">Shuffle off</string> <string name="repeat_all">Repeat all</string> <string name="repeat_one">Repeat one</string> <string name="repeat_off">Repeat off</string> </resources> Loading
app/src/main/java/org/lineageos/twelve/services/PlaybackService.kt +100 −6 Original line number Diff line number Diff line Loading @@ -7,6 +7,7 @@ package org.lineageos.twelve.services import android.app.PendingIntent import android.content.Intent import android.content.res.Resources import android.media.audiofx.AudioEffect import android.os.Bundle import android.os.IBinder Loading @@ -25,6 +26,7 @@ import androidx.media3.common.util.UnstableApi import androidx.media3.common.util.Util import androidx.media3.exoplayer.DefaultLoadControl import androidx.media3.exoplayer.ExoPlayer import androidx.media3.session.CommandButton import androidx.media3.session.DefaultMediaNotificationProvider import androidx.media3.session.LibraryResult import androidx.media3.session.MediaController Loading @@ -42,21 +44,24 @@ import org.lineageos.twelve.R import org.lineageos.twelve.TwelveApplication import org.lineageos.twelve.ext.enableFloatOutput import org.lineageos.twelve.ext.enableOffload import org.lineageos.twelve.ext.next import org.lineageos.twelve.ext.setOffloadEnabled import org.lineageos.twelve.ext.skipSilence import org.lineageos.twelve.ext.stopPlaybackOnTaskRemoved import org.lineageos.twelve.ext.typedRepeatMode import org.lineageos.twelve.models.RepeatMode import org.lineageos.twelve.ui.widgets.NowPlayingAppWidgetProvider @OptIn(UnstableApi::class) class PlaybackService : MediaLibraryService(), LifecycleOwner { enum class CustomCommand(val value: String, extras: Bundle) { enum class CustomCommand { /** * Toggles audio offload mode. * * Arguments: * - [ARG_VALUE] ([Boolean]): Whether to enable or disable offload */ TOGGLE_OFFLOAD("toggle_offload", Bundle.EMPTY), TOGGLE_OFFLOAD, /** * Toggles skip silence. Loading @@ -64,7 +69,7 @@ class PlaybackService : MediaLibraryService(), LifecycleOwner { * Arguments: * - [ARG_VALUE] ([Boolean]): Whether to enable or disable skip silence */ TOGGLE_SKIP_SILENCE("toggle_skip_silence", Bundle.EMPTY), TOGGLE_SKIP_SILENCE, /** * Get the audio session ID. Loading @@ -72,9 +77,68 @@ class PlaybackService : MediaLibraryService(), LifecycleOwner { * Response: * - [RSP_VALUE] ([Int]): The audio session ID */ GET_AUDIO_SESSION_ID("get_audio_session_id", Bundle.EMPTY); GET_AUDIO_SESSION_ID, val sessionCommand = SessionCommand(value, extras) /** * Toggle shuffle mode. * * Arguments: * - [ARG_VALUE] ([Boolean]): Whether to enable or disable shuffle mode */ TOGGLE_SHUFFLE { override fun buildCommandButton( player: ExoPlayer, resources: Resources, ) = player.shuffleModeEnabled.let { shuffleModeEnabled -> val (icon, titleStringResId) = when (shuffleModeEnabled) { true -> CommandButton.ICON_SHUFFLE_ON to R.string.shuffle_on false -> CommandButton.ICON_SHUFFLE_OFF to R.string.shuffle_off } CommandButton.Builder(icon) .setDisplayName(resources.getString(titleStringResId)) .setSessionCommand( SessionCommand( name, bundleOf(ARG_VALUE to !shuffleModeEnabled), ) ) .build() } }, /** * Toggle repeat mode. * * Arguments: * - [ARG_VALUE] ([String]): The repeat mode */ TOGGLE_REPEAT { override fun buildCommandButton( player: ExoPlayer, resources: Resources, ) = player.typedRepeatMode.let { repeatMode -> val (icon, titleStringResId) = when (repeatMode) { RepeatMode.NONE -> CommandButton.ICON_REPEAT_OFF to R.string.repeat_off RepeatMode.ALL -> CommandButton.ICON_REPEAT_ALL to R.string.repeat_all RepeatMode.ONE -> CommandButton.ICON_REPEAT_ONE to R.string.repeat_one } CommandButton.Builder(icon) .setDisplayName(resources.getString(titleStringResId)) .setSessionCommand( SessionCommand( name, bundleOf(ARG_VALUE to repeatMode.next().name), ) ) .build() } }; val sessionCommand = SessionCommand(name, Bundle.EMPTY) open fun buildCommandButton(player: ExoPlayer, resources: Resources): CommandButton? = null companion object { const val ARG_VALUE = "value" Loading @@ -82,7 +146,7 @@ class PlaybackService : MediaLibraryService(), LifecycleOwner { fun fromCustomAction( customAction: String ) = entries.firstOrNull { it.value == customAction } ) = entries.firstOrNull { it.name == customAction } suspend fun MediaController.sendCustomCommand( customCommand: CustomCommand, Loading Loading @@ -304,6 +368,22 @@ class PlaybackService : MediaLibraryService(), LifecycleOwner { ) } CustomCommand.TOGGLE_SHUFFLE -> { args.getBoolean(CustomCommand.ARG_VALUE).let { player.shuffleModeEnabled = it } SessionResult(SessionResult.RESULT_SUCCESS) } CustomCommand.TOGGLE_REPEAT -> { args.getString(CustomCommand.ARG_VALUE)?.let { player.typedRepeatMode = RepeatMode.valueOf(it) } SessionResult(SessionResult.RESULT_SUCCESS) } null -> SessionResult(SessionError.ERROR_NOT_SUPPORTED) } } Loading Loading @@ -349,6 +429,7 @@ class PlaybackService : MediaLibraryService(), LifecycleOwner { ) .setBitmapLoader(CoilBitmapLoader(this, lifecycleScope)) .setSessionActivity(getSingleTopActivity()) .setCustomLayout(getCustomLayout()) .build() setMediaNotificationProvider( Loading Loading @@ -391,6 +472,15 @@ class PlaybackService : MediaLibraryService(), LifecycleOwner { NowPlayingAppWidgetProvider.update(this@PlaybackService) } } // Update the shuffle and repeat buttons if (events.containsAny( Player.EVENT_REPEAT_MODE_CHANGED, Player.EVENT_SHUFFLE_MODE_ENABLED_CHANGED ) ) { mediaLibrarySession.setCustomLayout(getCustomLayout()) } } } } Loading Loading @@ -458,4 +548,8 @@ class PlaybackService : MediaLibraryService(), LifecycleOwner { }, PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT, ) private fun getCustomLayout() = CustomCommand.entries.mapNotNull { it.buildCommandButton(player, resources) } }
app/src/main/res/values/strings.xml +7 −0 Original line number Diff line number Diff line Loading @@ -269,4 +269,11 @@ <string name="provider">Provider</string> <string name="provider_format" translatable="false">%1$s (%2$s)</string> <string name="create_playlist_error_empty_name">No name specified</string> <!-- Player actions --> <string name="shuffle_on">Shuffle on</string> <string name="shuffle_off">Shuffle off</string> <string name="repeat_all">Repeat all</string> <string name="repeat_one">Repeat one</string> <string name="repeat_off">Repeat off</string> </resources>