com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector.java Source code

Java tutorial

Introduction

Here is the source code for com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector.java

Source

/*
 * Copyright (C) 2017 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
 *
 *      http://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 com.google.android.exoplayer2.ext.mediasession;

import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.os.ResultReceiver;
import android.os.SystemClock;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.media.MediaDescriptionCompat;
import android.support.v4.media.MediaMetadataCompat;
import android.support.v4.media.RatingCompat;
import android.support.v4.media.session.MediaControllerCompat;
import android.support.v4.media.session.MediaSessionCompat;
import android.support.v4.media.session.PlaybackStateCompat;
import android.util.Pair;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ExoPlaybackException;
import com.google.android.exoplayer2.ExoPlayerLibraryInfo;
import com.google.android.exoplayer2.PlaybackParameters;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.util.ErrorMessageProvider;
import com.google.android.exoplayer2.util.RepeatModeUtil;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * Connects a {@link MediaSessionCompat} to a {@link Player}.
 * <p>
 * The connector listens for actions sent by the media session's controller and implements these
 * actions by calling appropriate player methods. The playback state of the media session is
 * automatically synced with the player. The connector can also be optionally extended by providing
 * various collaborators:
 * <ul>
 *   <li>Actions to initiate media playback ({@code PlaybackStateCompat#ACTION_PREPARE_*} and
 *   {@code PlaybackStateCompat#ACTION_PLAY_*}) can be handled by a {@link PlaybackPreparer} passed
 *   when calling {@link #setPlayer(Player, PlaybackPreparer, CustomActionProvider...)}. Custom
 *   actions can be handled by passing one or more {@link CustomActionProvider}s in a similar way.
 *   </li>
 *   <li>To enable a media queue and navigation within it, you can set a {@link QueueNavigator} by
 *   calling {@link #setQueueNavigator(QueueNavigator)}. Use of {@link TimelineQueueNavigator} is
 *   recommended for most use cases.</li>
 *   <li>To enable editing of the media queue, you can set a {@link QueueEditor} by calling
 *   {@link #setQueueEditor(QueueEditor)}.</li>
 *   <li>An {@link ErrorMessageProvider} for providing human readable error messages and
 *   corresponding error codes can be set by calling
 *   {@link #setErrorMessageProvider(ErrorMessageProvider)}.</li>
 * </ul>
 */
public final class MediaSessionConnector {

    static {
        ExoPlayerLibraryInfo.registerModule("goog.exo.mediasession");
    }

    /**
     * The default repeat toggle modes which is the bitmask of
     * {@link RepeatModeUtil#REPEAT_TOGGLE_MODE_ONE} and
     * {@link RepeatModeUtil#REPEAT_TOGGLE_MODE_ALL}.
     */
    public static final @RepeatModeUtil.RepeatToggleModes int DEFAULT_REPEAT_TOGGLE_MODES = RepeatModeUtil.REPEAT_TOGGLE_MODE_ONE
            | RepeatModeUtil.REPEAT_TOGGLE_MODE_ALL;
    public static final String EXTRAS_PITCH = "EXO_PITCH";
    private static final int BASE_MEDIA_SESSION_FLAGS = MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS
            | MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS;
    private static final int EDITOR_MEDIA_SESSION_FLAGS = BASE_MEDIA_SESSION_FLAGS
            | MediaSessionCompat.FLAG_HANDLES_QUEUE_COMMANDS;

    /**
     * Receiver of media commands sent by a media controller.
     */
    public interface CommandReceiver {
        /**
         * Returns the commands the receiver handles, or {@code null} if no commands need to be handled.
         */
        String[] getCommands();

        /**
         * See {@link MediaSessionCompat.Callback#onCommand(String, Bundle, ResultReceiver)}.
         */
        void onCommand(Player player, String command, Bundle extras, ResultReceiver cb);
    }

    /**
     * Interface to which playback preparation actions are delegated.
     */
    public interface PlaybackPreparer extends CommandReceiver {

        long ACTIONS = PlaybackStateCompat.ACTION_PREPARE | PlaybackStateCompat.ACTION_PREPARE_FROM_MEDIA_ID
                | PlaybackStateCompat.ACTION_PREPARE_FROM_SEARCH | PlaybackStateCompat.ACTION_PREPARE_FROM_URI
                | PlaybackStateCompat.ACTION_PLAY_FROM_MEDIA_ID | PlaybackStateCompat.ACTION_PLAY_FROM_SEARCH
                | PlaybackStateCompat.ACTION_PLAY_FROM_URI;

        /**
         * Returns the actions which are supported by the preparer. The supported actions must be a
         * bitmask combined out of {@link PlaybackStateCompat#ACTION_PREPARE},
         * {@link PlaybackStateCompat#ACTION_PREPARE_FROM_MEDIA_ID},
         * {@link PlaybackStateCompat#ACTION_PREPARE_FROM_SEARCH},
         * {@link PlaybackStateCompat#ACTION_PREPARE_FROM_URI},
         * {@link PlaybackStateCompat#ACTION_PLAY_FROM_MEDIA_ID},
         * {@link PlaybackStateCompat#ACTION_PLAY_FROM_SEARCH} and
         * {@link PlaybackStateCompat#ACTION_PLAY_FROM_URI}.
         *
         * @return The bitmask of the supported media actions.
         */
        long getSupportedPrepareActions();

        /**
         * See {@link MediaSessionCompat.Callback#onPrepare()}.
         */
        void onPrepare();

        /**
         * See {@link MediaSessionCompat.Callback#onPrepareFromMediaId(String, Bundle)}.
         */
        void onPrepareFromMediaId(String mediaId, Bundle extras);

        /**
         * See {@link MediaSessionCompat.Callback#onPrepareFromSearch(String, Bundle)}.
         */
        void onPrepareFromSearch(String query, Bundle extras);

        /**
         * See {@link MediaSessionCompat.Callback#onPrepareFromUri(Uri, Bundle)}.
         */
        void onPrepareFromUri(Uri uri, Bundle extras);
    }

    /**
     * Interface to which playback actions are delegated.
     */
    public interface PlaybackController extends CommandReceiver {

        long ACTIONS = PlaybackStateCompat.ACTION_PLAY_PAUSE | PlaybackStateCompat.ACTION_PLAY
                | PlaybackStateCompat.ACTION_PAUSE | PlaybackStateCompat.ACTION_SEEK_TO
                | PlaybackStateCompat.ACTION_FAST_FORWARD | PlaybackStateCompat.ACTION_REWIND
                | PlaybackStateCompat.ACTION_STOP | PlaybackStateCompat.ACTION_SET_REPEAT_MODE
                | PlaybackStateCompat.ACTION_SET_SHUFFLE_MODE;

        /**
         * Returns the actions which are supported by the controller. The supported actions must be a
         * bitmask combined out of {@link PlaybackStateCompat#ACTION_PLAY_PAUSE},
         * {@link PlaybackStateCompat#ACTION_PLAY}, {@link PlaybackStateCompat#ACTION_PAUSE},
         * {@link PlaybackStateCompat#ACTION_SEEK_TO}, {@link PlaybackStateCompat#ACTION_FAST_FORWARD},
         * {@link PlaybackStateCompat#ACTION_REWIND}, {@link PlaybackStateCompat#ACTION_STOP},
         * {@link PlaybackStateCompat#ACTION_SET_REPEAT_MODE} and
         * {@link PlaybackStateCompat#ACTION_SET_SHUFFLE_MODE}.
         *
         * @param player The player.
         * @return The bitmask of the supported media actions.
         */
        long getSupportedPlaybackActions(@Nullable Player player);

        /**
         * See {@link MediaSessionCompat.Callback#onPlay()}.
         */
        void onPlay(Player player);

        /**
         * See {@link MediaSessionCompat.Callback#onPause()}.
         */
        void onPause(Player player);

        /**
         * See {@link MediaSessionCompat.Callback#onSeekTo(long)}.
         */
        void onSeekTo(Player player, long position);

        /**
         * See {@link MediaSessionCompat.Callback#onFastForward()}.
         */
        void onFastForward(Player player);

        /**
         * See {@link MediaSessionCompat.Callback#onRewind()}.
         */
        void onRewind(Player player);

        /**
         * See {@link MediaSessionCompat.Callback#onStop()}.
         */
        void onStop(Player player);

        /**
         * See {@link MediaSessionCompat.Callback#onSetShuffleMode(int)}.
         */
        void onSetShuffleMode(Player player, int shuffleMode);

        /**
         * See {@link MediaSessionCompat.Callback#onSetRepeatMode(int)}.
         */
        void onSetRepeatMode(Player player, int repeatMode);
    }

    /**
     * Handles queue navigation actions, and updates the media session queue by calling
     * {@code MediaSessionCompat.setQueue()}.
     */
    public interface QueueNavigator extends CommandReceiver {

        long ACTIONS = PlaybackStateCompat.ACTION_SKIP_TO_QUEUE_ITEM | PlaybackStateCompat.ACTION_SKIP_TO_NEXT
                | PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS;

        /**
         * Returns the actions which are supported by the navigator. The supported actions must be a
         * bitmask combined out of {@link PlaybackStateCompat#ACTION_SKIP_TO_QUEUE_ITEM},
         * {@link PlaybackStateCompat#ACTION_SKIP_TO_NEXT},
         * {@link PlaybackStateCompat#ACTION_SKIP_TO_PREVIOUS}.
         *
         * @param player The {@link Player}.
         * @return The bitmask of the supported media actions.
         */
        long getSupportedQueueNavigatorActions(@Nullable Player player);

        /**
         * Called when the timeline of the player has changed.
         *
         * @param player The player of which the timeline has changed.
         */
        void onTimelineChanged(Player player);

        /**
         * Called when the current window index changed.
         *
         * @param player The player of which the current window index of the timeline has changed.
         */
        void onCurrentWindowIndexChanged(Player player);

        /**
         * Gets the id of the currently active queue item, or
         * {@link MediaSessionCompat.QueueItem#UNKNOWN_ID} if the active item is unknown.
         * <p>
         * To let the connector publish metadata for the active queue item, the queue item with the
         * returned id must be available in the list of items returned by
         * {@link MediaControllerCompat#getQueue()}.
         *
         * @param player The player connected to the media session.
         * @return The id of the active queue item.
         */
        long getActiveQueueItemId(@Nullable Player player);

        /**
         * See {@link MediaSessionCompat.Callback#onSkipToPrevious()}.
         */
        void onSkipToPrevious(Player player);

        /**
         * See {@link MediaSessionCompat.Callback#onSkipToQueueItem(long)}.
         */
        void onSkipToQueueItem(Player player, long id);

        /**
         * See {@link MediaSessionCompat.Callback#onSkipToNext()}.
         */
        void onSkipToNext(Player player);
    }

    /**
     * Handles media session queue edits.
     */
    public interface QueueEditor extends CommandReceiver {

        long ACTIONS = PlaybackStateCompat.ACTION_SET_RATING;

        /**
         * Returns {@link PlaybackStateCompat#ACTION_SET_RATING} or {@code 0}. The Media API does
         * not declare action constants for adding and removing queue items.
         *
         * @param player The {@link Player}.
         */
        long getSupportedQueueEditorActions(@Nullable Player player);

        /**
         * See {@link MediaSessionCompat.Callback#onAddQueueItem(MediaDescriptionCompat description)}.
         */
        void onAddQueueItem(Player player, MediaDescriptionCompat description);

        /**
         * See {@link MediaSessionCompat.Callback#onAddQueueItem(MediaDescriptionCompat description,
         * int index)}.
         */
        void onAddQueueItem(Player player, MediaDescriptionCompat description, int index);

        /**
         * See {@link MediaSessionCompat.Callback#onRemoveQueueItem(MediaDescriptionCompat
         * description)}.
         */
        void onRemoveQueueItem(Player player, MediaDescriptionCompat description);

        /**
         * See {@link MediaSessionCompat.Callback#onRemoveQueueItemAt(int index)}.
         */
        void onRemoveQueueItemAt(Player player, int index);

        /**
         * See {@link MediaSessionCompat.Callback#onSetRating(RatingCompat)}.
         */
        void onSetRating(Player player, RatingCompat rating);
    }

    /**
     * Provides a {@link PlaybackStateCompat.CustomAction} to be published and handles the action when
     * sent by a media controller.
     */
    public interface CustomActionProvider {
        /**
         * Called when a custom action provided by this provider is sent to the media session.
         *
         * @param action The name of the action which was sent by a media controller.
         * @param extras Optional extras sent by a media controller.
         */
        void onCustomAction(String action, Bundle extras);

        /**
         * Returns a {@link PlaybackStateCompat.CustomAction} which will be published to the
         * media session by the connector or {@code null} if this action should not be published at the
         * given player state.
         *
         * @return The custom action to be included in the session playback state or {@code null}.
         */
        PlaybackStateCompat.CustomAction getCustomAction();
    }

    /**
     * The wrapped {@link MediaSessionCompat}.
     */
    public final MediaSessionCompat mediaSession;

    private final MediaControllerCompat mediaController;
    private final Handler handler;
    private final boolean doMaintainMetadata;
    private final ExoPlayerEventListener exoPlayerEventListener;
    private final MediaSessionCallback mediaSessionCallback;
    private final PlaybackController playbackController;
    private final Map<String, CommandReceiver> commandMap;

    private Player player;
    private CustomActionProvider[] customActionProviders;
    private Map<String, CustomActionProvider> customActionMap;
    private ErrorMessageProvider<? super ExoPlaybackException> errorMessageProvider;
    private PlaybackPreparer playbackPreparer;
    private QueueNavigator queueNavigator;
    private QueueEditor queueEditor;
    private ExoPlaybackException playbackException;

    /**
     * Creates an instance. Must be called on the same thread that is used to construct the player
     * instances passed to {@link #setPlayer(Player, PlaybackPreparer, CustomActionProvider...)}.
     * <p>
     * Equivalent to {@code MediaSessionConnector(mediaSession, new DefaultPlaybackController())}.
     *
     * @param mediaSession The {@link MediaSessionCompat} to connect to.
     */
    public MediaSessionConnector(MediaSessionCompat mediaSession) {
        this(mediaSession, null);
    }

    /**
     * Creates an instance. Must be called on the same thread that is used to construct the player
     * instances passed to {@link #setPlayer(Player, PlaybackPreparer, CustomActionProvider...)}.
     * <p>
     * Equivalent to {@code MediaSessionConnector(mediaSession, playbackController, true)}.
     *
     * @param mediaSession The {@link MediaSessionCompat} to connect to.
     * @param playbackController A {@link PlaybackController} for handling playback actions.
     */
    public MediaSessionConnector(MediaSessionCompat mediaSession, PlaybackController playbackController) {
        this(mediaSession, playbackController, true);
    }

    /**
     * Creates an instance. Must be called on the same thread that is used to construct the player
     * instances passed to {@link #setPlayer(Player, PlaybackPreparer, CustomActionProvider...)}.
     *
     * @param mediaSession The {@link MediaSessionCompat} to connect to.
     * @param playbackController A {@link PlaybackController} for handling playback actions, or
     *     {@code null} if the connector should handle playback actions directly.
     * @param doMaintainMetadata Whether the connector should maintain the metadata of the session. If
     *     {@code false}, you need to maintain the metadata of the media session yourself (provide at
     *     least the duration to allow clients to show a progress bar).
     */
    public MediaSessionConnector(MediaSessionCompat mediaSession, PlaybackController playbackController,
            boolean doMaintainMetadata) {
        this.mediaSession = mediaSession;
        this.playbackController = playbackController != null ? playbackController : new DefaultPlaybackController();
        this.handler = new Handler(Looper.myLooper() != null ? Looper.myLooper() : Looper.getMainLooper());
        this.doMaintainMetadata = doMaintainMetadata;
        mediaSession.setFlags(BASE_MEDIA_SESSION_FLAGS);
        mediaController = mediaSession.getController();
        mediaSessionCallback = new MediaSessionCallback();
        exoPlayerEventListener = new ExoPlayerEventListener();
        customActionMap = Collections.emptyMap();
        commandMap = new HashMap<>();
        registerCommandReceiver(playbackController);
    }

    /**
     * Sets the player to be connected to the media session.
     * <p>
     * The order in which any {@link CustomActionProvider}s are passed determines the order of the
     * actions published with the playback state of the session.
     *
     * @param player The player to be connected to the {@code MediaSession}.
     * @param playbackPreparer An optional {@link PlaybackPreparer} for preparing the player.
     * @param customActionProviders An optional {@link CustomActionProvider}s to publish and handle
     *     custom actions.
     */
    public void setPlayer(Player player, PlaybackPreparer playbackPreparer,
            CustomActionProvider... customActionProviders) {
        if (this.player != null) {
            this.player.removeListener(exoPlayerEventListener);
            mediaSession.setCallback(null);
        }
        unregisterCommandReceiver(this.playbackPreparer);

        this.player = player;
        this.playbackPreparer = playbackPreparer;
        registerCommandReceiver(playbackPreparer);

        this.customActionProviders = (player != null && customActionProviders != null) ? customActionProviders
                : new CustomActionProvider[0];
        if (player != null) {
            mediaSession.setCallback(mediaSessionCallback, handler);
            player.addListener(exoPlayerEventListener);
        }
        updateMediaSessionPlaybackState();
        updateMediaSessionMetadata();
    }

    /**
     * Sets the {@link ErrorMessageProvider}.
     *
     * @param errorMessageProvider The error message provider.
     */
    public void setErrorMessageProvider(ErrorMessageProvider<? super ExoPlaybackException> errorMessageProvider) {
        this.errorMessageProvider = errorMessageProvider;
    }

    /**
     * Sets the {@link QueueNavigator} to handle queue navigation actions {@code ACTION_SKIP_TO_NEXT},
     * {@code ACTION_SKIP_TO_PREVIOUS} and {@code ACTION_SKIP_TO_QUEUE_ITEM}.
     *
     * @param queueNavigator The queue navigator.
     */
    public void setQueueNavigator(QueueNavigator queueNavigator) {
        unregisterCommandReceiver(this.queueNavigator);
        this.queueNavigator = queueNavigator;
        registerCommandReceiver(queueNavigator);
    }

    /**
     * Sets the {@link QueueEditor} to handle queue edits sent by the media controller.
     *
     * @param queueEditor The queue editor.
     */
    public void setQueueEditor(QueueEditor queueEditor) {
        unregisterCommandReceiver(this.queueEditor);
        this.queueEditor = queueEditor;
        registerCommandReceiver(queueEditor);
        mediaSession.setFlags(queueEditor == null ? BASE_MEDIA_SESSION_FLAGS : EDITOR_MEDIA_SESSION_FLAGS);
    }

    private void registerCommandReceiver(CommandReceiver commandReceiver) {
        if (commandReceiver != null && commandReceiver.getCommands() != null) {
            for (String command : commandReceiver.getCommands()) {
                commandMap.put(command, commandReceiver);
            }
        }
    }

    private void unregisterCommandReceiver(CommandReceiver commandReceiver) {
        if (commandReceiver != null && commandReceiver.getCommands() != null) {
            for (String command : commandReceiver.getCommands()) {
                commandMap.remove(command);
            }
        }
    }

    private void updateMediaSessionPlaybackState() {
        PlaybackStateCompat.Builder builder = new PlaybackStateCompat.Builder();
        if (player == null) {
            builder.setActions(buildPlaybackActions()).setState(PlaybackStateCompat.STATE_NONE, 0, 0, 0);
            mediaSession.setPlaybackState(builder.build());
            return;
        }

        Map<String, CustomActionProvider> currentActions = new HashMap<>();
        for (CustomActionProvider customActionProvider : customActionProviders) {
            PlaybackStateCompat.CustomAction customAction = customActionProvider.getCustomAction();
            if (customAction != null) {
                currentActions.put(customAction.getAction(), customActionProvider);
                builder.addCustomAction(customAction);
            }
        }
        customActionMap = Collections.unmodifiableMap(currentActions);

        int sessionPlaybackState = playbackException != null ? PlaybackStateCompat.STATE_ERROR
                : mapPlaybackState(player.getPlaybackState(), player.getPlayWhenReady());
        if (playbackException != null) {
            if (errorMessageProvider != null) {
                Pair<Integer, String> message = errorMessageProvider.getErrorMessage(playbackException);
                builder.setErrorMessage(message.first, message.second);
            }
            if (player.getPlaybackState() != Player.STATE_IDLE) {
                playbackException = null;
            }
        }
        long activeQueueItemId = queueNavigator != null ? queueNavigator.getActiveQueueItemId(player)
                : MediaSessionCompat.QueueItem.UNKNOWN_ID;
        Bundle extras = new Bundle();
        extras.putFloat(EXTRAS_PITCH, player.getPlaybackParameters().pitch);
        builder.setActions(buildPlaybackActions()).setActiveQueueItemId(activeQueueItemId)
                .setBufferedPosition(player.getBufferedPosition())
                .setState(sessionPlaybackState, player.getCurrentPosition(), player.getPlaybackParameters().speed,
                        SystemClock.elapsedRealtime())
                .setExtras(extras);
        mediaSession.setPlaybackState(builder.build());
    }

    private long buildPlaybackActions() {
        long actions = (PlaybackController.ACTIONS & playbackController.getSupportedPlaybackActions(player));
        if (playbackPreparer != null) {
            actions |= (PlaybackPreparer.ACTIONS & playbackPreparer.getSupportedPrepareActions());
        }
        if (queueNavigator != null) {
            actions |= (QueueNavigator.ACTIONS & queueNavigator.getSupportedQueueNavigatorActions(player));
        }
        if (queueEditor != null) {
            actions |= (QueueEditor.ACTIONS & queueEditor.getSupportedQueueEditorActions(player));
        }
        return actions;
    }

    private void updateMediaSessionMetadata() {
        if (doMaintainMetadata) {
            MediaMetadataCompat.Builder builder = new MediaMetadataCompat.Builder();
            if (player != null && player.isPlayingAd()) {
                builder.putLong(MediaMetadataCompat.METADATA_KEY_ADVERTISEMENT, 1);
            }
            builder.putLong(MediaMetadataCompat.METADATA_KEY_DURATION,
                    player == null ? 0 : player.getDuration() == C.TIME_UNSET ? -1 : player.getDuration());

            if (queueNavigator != null) {
                long activeQueueItemId = queueNavigator.getActiveQueueItemId(player);
                List<MediaSessionCompat.QueueItem> queue = mediaController.getQueue();
                for (int i = 0; queue != null && i < queue.size(); i++) {
                    MediaSessionCompat.QueueItem queueItem = queue.get(i);
                    if (queueItem.getQueueId() == activeQueueItemId) {
                        MediaDescriptionCompat description = queueItem.getDescription();
                        if (description.getTitle() != null) {
                            builder.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_TITLE,
                                    String.valueOf(description.getTitle()));
                        }
                        if (description.getSubtitle() != null) {
                            builder.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_SUBTITLE,
                                    String.valueOf(description.getSubtitle()));
                        }
                        if (description.getDescription() != null) {
                            builder.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_DESCRIPTION,
                                    String.valueOf(description.getDescription()));
                        }
                        if (description.getIconBitmap() != null) {
                            builder.putBitmap(MediaMetadataCompat.METADATA_KEY_DISPLAY_ICON,
                                    description.getIconBitmap());
                        }
                        if (description.getIconUri() != null) {
                            builder.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_ICON_URI,
                                    String.valueOf(description.getIconUri()));
                        }
                        if (description.getMediaId() != null) {
                            builder.putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID,
                                    String.valueOf(description.getMediaId()));
                        }
                        if (description.getMediaUri() != null) {
                            builder.putString(MediaMetadataCompat.METADATA_KEY_MEDIA_URI,
                                    String.valueOf(description.getMediaUri()));
                        }
                        break;
                    }
                }
            }
            mediaSession.setMetadata(builder.build());
        }
    }

    private int mapPlaybackState(int exoPlayerPlaybackState, boolean playWhenReady) {
        switch (exoPlayerPlaybackState) {
        case Player.STATE_BUFFERING:
            return PlaybackStateCompat.STATE_BUFFERING;
        case Player.STATE_READY:
            return playWhenReady ? PlaybackStateCompat.STATE_PLAYING : PlaybackStateCompat.STATE_PAUSED;
        case Player.STATE_ENDED:
            return PlaybackStateCompat.STATE_PAUSED;
        default:
            return PlaybackStateCompat.STATE_NONE;
        }
    }

    private boolean canDispatchToPlaybackPreparer(long action) {
        return playbackPreparer != null
                && (playbackPreparer.getSupportedPrepareActions() & PlaybackPreparer.ACTIONS & action) != 0;
    }

    private boolean canDispatchToPlaybackController(long action) {
        return (playbackController.getSupportedPlaybackActions(player) & PlaybackController.ACTIONS & action) != 0;
    }

    private boolean canDispatchToQueueNavigator(long action) {
        return queueNavigator != null && (queueNavigator.getSupportedQueueNavigatorActions(player)
                & QueueNavigator.ACTIONS & action) != 0;
    }

    private boolean canDispatchToQueueEditor(long action) {
        return queueEditor != null
                && (queueEditor.getSupportedQueueEditorActions(player) & QueueEditor.ACTIONS & action) != 0;
    }

    private class ExoPlayerEventListener extends Player.DefaultEventListener {

        private int currentWindowIndex;
        private int currentWindowCount;

        @Override
        public void onTimelineChanged(Timeline timeline, Object manifest) {
            int windowCount = player.getCurrentTimeline().getWindowCount();
            int windowIndex = player.getCurrentWindowIndex();
            if (queueNavigator != null) {
                queueNavigator.onTimelineChanged(player);
                updateMediaSessionPlaybackState();
            } else if (currentWindowCount != windowCount || currentWindowIndex != windowIndex) {
                // active queue item and queue navigation actions may need to be updated
                updateMediaSessionPlaybackState();
            }
            if (currentWindowCount != windowCount) {
                // active queue item and queue navigation actions may need to be updated
                updateMediaSessionPlaybackState();
            }
            currentWindowCount = windowCount;
            currentWindowIndex = player.getCurrentWindowIndex();
            updateMediaSessionMetadata();
        }

        @Override
        public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {
            updateMediaSessionPlaybackState();
        }

        @Override
        public void onRepeatModeChanged(@Player.RepeatMode int repeatMode) {
            mediaSession.setRepeatMode(repeatMode == Player.REPEAT_MODE_ONE ? PlaybackStateCompat.REPEAT_MODE_ONE
                    : repeatMode == Player.REPEAT_MODE_ALL ? PlaybackStateCompat.REPEAT_MODE_ALL
                            : PlaybackStateCompat.REPEAT_MODE_NONE);
            updateMediaSessionPlaybackState();
        }

        @Override
        public void onShuffleModeEnabledChanged(boolean shuffleModeEnabled) {
            mediaSession.setShuffleMode(shuffleModeEnabled ? PlaybackStateCompat.SHUFFLE_MODE_ALL
                    : PlaybackStateCompat.SHUFFLE_MODE_NONE);
            updateMediaSessionPlaybackState();
        }

        @Override
        public void onPlayerError(ExoPlaybackException error) {
            playbackException = error;
            updateMediaSessionPlaybackState();
        }

        @Override
        public void onPositionDiscontinuity(@Player.DiscontinuityReason int reason) {
            if (currentWindowIndex != player.getCurrentWindowIndex()) {
                if (queueNavigator != null) {
                    queueNavigator.onCurrentWindowIndexChanged(player);
                }
                currentWindowIndex = player.getCurrentWindowIndex();
                updateMediaSessionMetadata();
            }
            updateMediaSessionPlaybackState();
        }

        @Override
        public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) {
            updateMediaSessionPlaybackState();
        }

    }

    private class MediaSessionCallback extends MediaSessionCompat.Callback {

        @Override
        public void onPlay() {
            if (canDispatchToPlaybackController(PlaybackStateCompat.ACTION_PLAY)) {
                playbackController.onPlay(player);
            }
        }

        @Override
        public void onPause() {
            if (canDispatchToPlaybackController(PlaybackStateCompat.ACTION_PAUSE)) {
                playbackController.onPause(player);
            }
        }

        @Override
        public void onSeekTo(long position) {
            if (canDispatchToPlaybackController(PlaybackStateCompat.ACTION_SEEK_TO)) {
                playbackController.onSeekTo(player, position);
            }
        }

        @Override
        public void onFastForward() {
            if (canDispatchToPlaybackController(PlaybackStateCompat.ACTION_FAST_FORWARD)) {
                playbackController.onFastForward(player);
            }
        }

        @Override
        public void onRewind() {
            if (canDispatchToPlaybackController(PlaybackStateCompat.ACTION_REWIND)) {
                playbackController.onRewind(player);
            }
        }

        @Override
        public void onStop() {
            if (canDispatchToPlaybackController(PlaybackStateCompat.ACTION_STOP)) {
                playbackController.onStop(player);
            }
        }

        @Override
        public void onSetShuffleMode(int shuffleMode) {
            if (canDispatchToPlaybackController(PlaybackStateCompat.ACTION_SET_SHUFFLE_MODE)) {
                playbackController.onSetShuffleMode(player, shuffleMode);
            }
        }

        @Override
        public void onSetRepeatMode(int repeatMode) {
            if (canDispatchToPlaybackController(PlaybackStateCompat.ACTION_SET_REPEAT_MODE)) {
                playbackController.onSetRepeatMode(player, repeatMode);
            }
        }

        @Override
        public void onSkipToNext() {
            if (canDispatchToQueueNavigator(PlaybackStateCompat.ACTION_SKIP_TO_NEXT)) {
                queueNavigator.onSkipToNext(player);
            }
        }

        @Override
        public void onSkipToPrevious() {
            if (canDispatchToQueueNavigator(PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS)) {
                queueNavigator.onSkipToPrevious(player);
            }
        }

        @Override
        public void onSkipToQueueItem(long id) {
            if (canDispatchToQueueNavigator(PlaybackStateCompat.ACTION_SKIP_TO_QUEUE_ITEM)) {
                queueNavigator.onSkipToQueueItem(player, id);
            }
        }

        @Override
        public void onCustomAction(@NonNull String action, @Nullable Bundle extras) {
            Map<String, CustomActionProvider> actionMap = customActionMap;
            if (actionMap.containsKey(action)) {
                actionMap.get(action).onCustomAction(action, extras);
                updateMediaSessionPlaybackState();
            }
        }

        @Override
        public void onCommand(String command, Bundle extras, ResultReceiver cb) {
            CommandReceiver commandReceiver = commandMap.get(command);
            if (commandReceiver != null) {
                commandReceiver.onCommand(player, command, extras, cb);
            }
        }

        @Override
        public void onPrepare() {
            if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PREPARE)) {
                player.stop();
                player.setPlayWhenReady(false);
                playbackPreparer.onPrepare();
            }
        }

        @Override
        public void onPrepareFromMediaId(String mediaId, Bundle extras) {
            if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PREPARE_FROM_MEDIA_ID)) {
                player.stop();
                player.setPlayWhenReady(false);
                playbackPreparer.onPrepareFromMediaId(mediaId, extras);
            }
        }

        @Override
        public void onPrepareFromSearch(String query, Bundle extras) {
            if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PREPARE_FROM_SEARCH)) {
                player.stop();
                player.setPlayWhenReady(false);
                playbackPreparer.onPrepareFromSearch(query, extras);
            }
        }

        @Override
        public void onPrepareFromUri(Uri uri, Bundle extras) {
            if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PREPARE_FROM_URI)) {
                player.stop();
                player.setPlayWhenReady(false);
                playbackPreparer.onPrepareFromUri(uri, extras);
            }
        }

        @Override
        public void onPlayFromMediaId(String mediaId, Bundle extras) {
            if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PLAY_FROM_MEDIA_ID)) {
                player.stop();
                player.setPlayWhenReady(true);
                playbackPreparer.onPrepareFromMediaId(mediaId, extras);
            }
        }

        @Override
        public void onPlayFromSearch(String query, Bundle extras) {
            if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PLAY_FROM_SEARCH)) {
                player.stop();
                player.setPlayWhenReady(true);
                playbackPreparer.onPrepareFromSearch(query, extras);
            }
        }

        @Override
        public void onPlayFromUri(Uri uri, Bundle extras) {
            if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PLAY_FROM_URI)) {
                player.stop();
                player.setPlayWhenReady(true);
                playbackPreparer.onPrepareFromUri(uri, extras);
            }
        }

        @Override
        public void onAddQueueItem(MediaDescriptionCompat description) {
            if (queueEditor != null) {
                queueEditor.onAddQueueItem(player, description);
            }
        }

        @Override
        public void onAddQueueItem(MediaDescriptionCompat description, int index) {
            if (queueEditor != null) {
                queueEditor.onAddQueueItem(player, description, index);
            }
        }

        @Override
        public void onRemoveQueueItem(MediaDescriptionCompat description) {
            if (queueEditor != null) {
                queueEditor.onRemoveQueueItem(player, description);
            }
        }

        @Override
        public void onRemoveQueueItemAt(int index) {
            if (queueEditor != null) {
                queueEditor.onRemoveQueueItemAt(player, index);
            }
        }

        @Override
        public void onSetRating(RatingCompat rating) {
            if (canDispatchToQueueEditor(PlaybackStateCompat.ACTION_SET_RATING)) {
                queueEditor.onSetRating(player, rating);
            }
        }

    }

}