Skip to content

How to properly order media controls on Notification and Android Auto? #3361

Description

@k-arabadzhiev

Hello,
I'm getting different order on different devices when using the SLOTs api:

I have this enum:

enum class PlayerSessionCommand(
    val commandButton: CommandButton,
) {
    REWIND(
        commandButton = CommandButton.Builder(ICON_SKIP_BACK_10)
            .setPlayerCommand(Player.COMMAND_SEEK_BACK)
            .setDisplayName("Rewind")
            .setSlots(CommandButton.SLOT_BACK)
            .setExtras(Bundle().apply {
                if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU)
                    putInt(DefaultMediaNotificationProvider.COMMAND_KEY_COMPACT_VIEW_INDEX, 0)
            })
            .build(),
    ),
    PLAY(
        commandButton = CommandButton.Builder(ICON_PLAY)
            .setPlayerCommand(Player.COMMAND_PLAY_PAUSE)
            .setDisplayName("Play")
            .setExtras(Bundle().apply {
                if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU)
                    putInt(DefaultMediaNotificationProvider.COMMAND_KEY_COMPACT_VIEW_INDEX, 1)
            })
            .build()
    ),
    PAUSE(
        commandButton = CommandButton.Builder(ICON_PAUSE)
            .setPlayerCommand(Player.COMMAND_PLAY_PAUSE)
            .setDisplayName("Pause")
            .setExtras(Bundle().apply {
                if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU)
                    putInt(DefaultMediaNotificationProvider.COMMAND_KEY_COMPACT_VIEW_INDEX, 1)
            })
            .build()
    ),
    FORWARD(
        commandButton = CommandButton.Builder(ICON_SKIP_FORWARD_10)
            .setPlayerCommand(Player.COMMAND_SEEK_FORWARD)
            .setDisplayName("Forward")
            .setSlots(CommandButton.SLOT_FORWARD)
            .setExtras(Bundle().apply {
                if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU)
                    putInt(DefaultMediaNotificationProvider.COMMAND_KEY_COMPACT_VIEW_INDEX, 2)
            })
            .build(),
    ),
    PREVIOUS(
        commandButton = CommandButton.Builder(ICON_PREVIOUS)
            .setPlayerCommand(Player.COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM)
            .setDisplayName("Previous")
            .setSlots(
                CommandButton.SLOT_OVERFLOW
            )
            .build()
    ),
    NEXT(
        commandButton = CommandButton.Builder(ICON_NEXT)
            .setPlayerCommand(Player.COMMAND_SEEK_TO_NEXT_MEDIA_ITEM)
            .setDisplayName("Next")
            .setSlots(
                CommandButton.SLOT_OVERFLOW
            )
            .build(),
    )
}

and I'm using it like this:

   private val playerSessionCommandButtons = listOf(
        PlayerSessionCommand.PREVIOUS,
        PlayerSessionCommand.REWIND,
        PlayerSessionCommand.FORWARD,
        PlayerSessionCommand.NEXT
    ).map { it.commandButton }

    fun update(
        session: MediaSession,
        mediaItem: MediaItem?
    ) {
        if (mediaItem == null) return

        val layout = buildLayout(mediaItem)
        updateSessionCommands(session, mediaItem, layout)
    }

    private fun updateSessionCommands(
        session: MediaSession,
        mediaItem: MediaItem,
        layout: List<CommandButton>
    ) {
        val playerCommands = if (mediaItem.isLive) {
            DEFAULT_PLAYER_COMMANDS.buildUpon()
                .removeAll(
                    COMMAND_SEEK_TO_PREVIOUS,
                    COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM,
                    COMMAND_SEEK_TO_NEXT,
                    COMMAND_SEEK_TO_NEXT_MEDIA_ITEM,
                    COMMAND_GET_TIMELINE // removes from Queue from Auto
                )
                .build()
        } else {
            val hasPreviousItem = session.player.hasPreviousMediaItem()
            val hasNextMediaItem = session.player.hasNextMediaItem()
            DEFAULT_PLAYER_COMMANDS.buildUpon().apply {
                if (!hasPreviousItem)
                    removeAll(COMMAND_SEEK_TO_PREVIOUS, COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM)

                if (!hasNextMediaItem)
                    removeAll(COMMAND_SEEK_TO_NEXT, COMMAND_SEEK_TO_NEXT_MEDIA_ITEM)
            }.build()
        }

        session.setMediaButtonPreferences(layout)
        session.connectedControllers.forEach {
            session.setAvailableCommands(
                it, DEFAULT_SESSION_COMMANDS, playerCommands
            )
        }
    }

    private fun buildLayout(mediaItem: MediaItem): List<CommandButton> {
        return when {
            mediaItem.isLive -> {
                // empty list can be returned as well, but this makes the intent clearer
                listOf(
                    PlayerSessionCommand.PLAY,
                    PlayerSessionCommand.PAUSE
                ).map { it.commandButton }
            }
            else -> playerSessionCommandButtons
        }
    }

I've tested on Android auto, Samsung (Api 36.1) and Vivo (Api 36.0):

first media item Nth media item last media item
samsung Image Image Image
vivo Image Image Image
android auto Image Image Image

The vivo and AA seem to be ordering the same way, while samsung it's slightly different. Is it possible to make the ordering for all for the first item the same as what vivo/aa are doing, while for the last item what samsung is doing?

While on the topic of slots, I've noticed there are these 2 constants:

  /**
   * A slot in a playback control UI for secondary backward-directed playback actions, most commonly
   * used for previous or rewind actions.
   */
  @UnstableApi public static final int SLOT_BACK_SECONDARY = 4;

  /**
   * A slot in a playback control UI for secondary forward-directed playback actions, most commonly
   * used for next or fast-forward actions.
   */
  @UnstableApi public static final int SLOT_FORWARD_SECONDARY = 5;

But I just couldn't get them to work as per the comment, that's why I'm using SLOT_OVERFLOW in my enum builder.

EDIT: android auto version on both devices: 17.4.663004-release

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions