blob: 913c1d86c072d276af3d9b83ce50a9123a8aba89 [file] [log] [blame]
/*
* 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.content.Intent;
import android.graphics.Bitmap;
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.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 android.view.KeyEvent;
import androidx.annotation.LongDef;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ControlDispatcher;
import com.google.android.exoplayer2.DefaultControlDispatcher;
import com.google.android.exoplayer2.ExoPlaybackException;
import com.google.android.exoplayer2.ExoPlayerLibraryInfo;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.ErrorMessageProvider;
import com.google.android.exoplayer2.util.RepeatModeUtil;
import com.google.android.exoplayer2.util.Util;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.checkerframework.checker.nullness.qual.EnsuresNonNullIf;
/**
* Connects a {@link MediaSessionCompat} to a {@link Player}.
*
* <p>This connector does <em>not</em> call {@link MediaSessionCompat#setActive(boolean)}, and so
* application code is responsible for making the session active when desired. A session must be
* active for transport controls to be displayed (e.g. on the lock screen) and for it to receive
* media button events.
*
* <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 to
* {@link #setPlaybackPreparer(PlaybackPreparer)}.
* <li>Custom actions can be handled by passing one or more {@link CustomActionProvider}s to
* {@link #setCustomActionProviders(CustomActionProvider...)}.
* <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>To enable editing of the media queue, you can set a {@link QueueEditor} by calling {@link
* #setQueueEditor(QueueEditor)}.
* <li>A {@link MediaButtonEventHandler} can be set by calling {@link
* #setMediaButtonEventHandler(MediaButtonEventHandler)}. By default media button events are
* handled by {@link MediaSessionCompat.Callback#onMediaButtonEvent(Intent)}.
* <li>An {@link ErrorMessageProvider} for providing human readable error messages and
* corresponding error codes can be set by calling {@link
* #setErrorMessageProvider(ErrorMessageProvider)}.
* <li>A {@link MediaMetadataProvider} can be set by calling {@link
* #setMediaMetadataProvider(MediaMetadataProvider)}. By default the {@link
* DefaultMediaMetadataProvider} is used.
* </ul>
*/
public final class MediaSessionConnector {
static {
ExoPlayerLibraryInfo.registerModule("goog.exo.mediasession");
}
/** Playback actions supported by the connector. */
@LongDef(
flag = true,
value = {
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
})
@Retention(RetentionPolicy.SOURCE)
public @interface PlaybackActions {}
@PlaybackActions
public static final long ALL_PLAYBACK_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;
/** The default playback actions. */
@PlaybackActions public static final long DEFAULT_PLAYBACK_ACTIONS = ALL_PLAYBACK_ACTIONS;
/**
* The name of the {@link PlaybackStateCompat} float extra with the value of {@link
* Player#getPlaybackSpeed()}.
*/
public static final String EXTRAS_SPEED = "EXO_SPEED";
private static final long BASE_PLAYBACK_ACTIONS =
PlaybackStateCompat.ACTION_PLAY_PAUSE
| PlaybackStateCompat.ACTION_PLAY
| PlaybackStateCompat.ACTION_PAUSE
| PlaybackStateCompat.ACTION_STOP
| PlaybackStateCompat.ACTION_SET_SHUFFLE_MODE
| PlaybackStateCompat.ACTION_SET_REPEAT_MODE;
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;
private static final MediaMetadataCompat METADATA_EMPTY =
new MediaMetadataCompat.Builder().build();
/** Receiver of media commands sent by a media controller. */
public interface CommandReceiver {
/**
* See {@link MediaSessionCompat.Callback#onCommand(String, Bundle, ResultReceiver)}. The
* receiver may handle the command, but is not required to do so. Changes to the player should
* be made via the {@link ControlDispatcher}.
*
* @param player The player connected to the media session.
* @param controlDispatcher A {@link ControlDispatcher} that should be used for dispatching
* changes to the player.
* @param command The command name.
* @param extras Optional parameters for the command, may be null.
* @param cb A result receiver to which a result may be sent by the command, may be null.
* @return Whether the receiver handled the command.
*/
boolean onCommand(
Player player,
ControlDispatcher controlDispatcher,
String command,
@Nullable Bundle extras,
@Nullable ResultReceiver cb);
}
/** Interface to which playback preparation and play 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()}.
*
* @param playWhenReady Whether playback should be started after preparation.
*/
void onPrepare(boolean playWhenReady);
/**
* See {@link MediaSessionCompat.Callback#onPrepareFromMediaId(String, Bundle)}.
*
* @param mediaId The media id of the media item to be prepared.
* @param playWhenReady Whether playback should be started after preparation.
* @param extras A {@link Bundle} of extras passed by the media controller.
*/
void onPrepareFromMediaId(String mediaId, boolean playWhenReady, Bundle extras);
/**
* See {@link MediaSessionCompat.Callback#onPrepareFromSearch(String, Bundle)}.
*
* @param query The search query.
* @param playWhenReady Whether playback should be started after preparation.
* @param extras A {@link Bundle} of extras passed by the media controller.
*/
void onPrepareFromSearch(String query, boolean playWhenReady, Bundle extras);
/**
* See {@link MediaSessionCompat.Callback#onPrepareFromUri(Uri, Bundle)}.
*
* @param uri The {@link Uri} of the media item to be prepared.
* @param playWhenReady Whether playback should be started after preparation.
* @param extras A {@link Bundle} of extras passed by the media controller.
*/
void onPrepareFromUri(Uri uri, boolean playWhenReady, Bundle extras);
}
/**
* 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 player connected to the media session.
* @return The bitmask of the supported media actions.
*/
long getSupportedQueueNavigatorActions(Player player);
/**
* Called when the timeline of the player has changed.
*
* @param player The player connected to the media session.
*/
void onTimelineChanged(Player player);
/**
* Called when the current window index changed.
*
* @param player The player connected to the media session.
*/
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()}.
*
* @param player The player connected to the media session.
* @param controlDispatcher A {@link ControlDispatcher} that should be used for dispatching
* changes to the player.
*/
void onSkipToPrevious(Player player, ControlDispatcher controlDispatcher);
/**
* See {@link MediaSessionCompat.Callback#onSkipToQueueItem(long)}.
*
* @param player The player connected to the media session.
* @param controlDispatcher A {@link ControlDispatcher} that should be used for dispatching
* changes to the player.
*/
void onSkipToQueueItem(Player player, ControlDispatcher controlDispatcher, long id);
/**
* See {@link MediaSessionCompat.Callback#onSkipToNext()}.
*
* @param player The player connected to the media session.
* @param controlDispatcher A {@link ControlDispatcher} that should be used for dispatching
* changes to the player.
*/
void onSkipToNext(Player player, ControlDispatcher controlDispatcher);
}
/** Handles media session queue edits. */
public interface QueueEditor extends CommandReceiver {
/**
* 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);
}
/** Callback receiving a user rating for the active media item. */
public interface RatingCallback extends CommandReceiver {
/** See {@link MediaSessionCompat.Callback#onSetRating(RatingCompat)}. */
void onSetRating(Player player, RatingCompat rating);
/** See {@link MediaSessionCompat.Callback#onSetRating(RatingCompat, Bundle)}. */
void onSetRating(Player player, RatingCompat rating, Bundle extras);
}
/** Handles requests for enabling or disabling captions. */
public interface CaptionCallback extends CommandReceiver {
/** See {@link MediaSessionCompat.Callback#onSetCaptioningEnabled(boolean)}. */
void onSetCaptioningEnabled(Player player, boolean enabled);
/**
* Returns whether the media currently being played has captions.
*
* <p>This method is called each time the media session playback state needs to be updated and
* published upon a player state change.
*/
boolean hasCaptions(Player player);
}
/** Handles a media button event. */
public interface MediaButtonEventHandler {
/**
* See {@link MediaSessionCompat.Callback#onMediaButtonEvent(Intent)}.
*
* @param player The {@link Player}.
* @param controlDispatcher A {@link ControlDispatcher} that should be used for dispatching
* changes to the player.
* @param mediaButtonEvent The {@link Intent}.
* @return True if the event was handled, false otherwise.
*/
boolean onMediaButtonEvent(
Player player, ControlDispatcher controlDispatcher, Intent mediaButtonEvent);
}
/**
* 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 player The player connected to the media session.
* @param controlDispatcher A {@link ControlDispatcher} that should be used for dispatching
* changes to the player.
* @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(
Player player, ControlDispatcher controlDispatcher, String action, @Nullable 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.
*
* @param player The player connected to the media session.
* @return The custom action to be included in the session playback state or {@code null}.
*/
@Nullable
PlaybackStateCompat.CustomAction getCustomAction(Player player);
}
/** Provides a {@link MediaMetadataCompat} for a given player state. */
public interface MediaMetadataProvider {
/**
* Gets the {@link MediaMetadataCompat} to be published to the session.
*
* <p>An app may need to load metadata resources like artwork bitmaps asynchronously. In such a
* case the app should return a {@link MediaMetadataCompat} object that does not contain these
* resources as a placeholder. The app should start an asynchronous operation to download the
* bitmap and put it into a cache. Finally, the app should call {@link
* #invalidateMediaSessionMetadata()}. This causes this callback to be called again and the app
* can now return a {@link MediaMetadataCompat} object with all the resources included.
*
* @param player The player connected to the media session.
* @return The {@link MediaMetadataCompat} to be published to the session.
*/
MediaMetadataCompat getMetadata(Player player);
}
/** The wrapped {@link MediaSessionCompat}. */
public final MediaSessionCompat mediaSession;
private final Looper looper;
private final ComponentListener componentListener;
private final ArrayList<CommandReceiver> commandReceivers;
private final ArrayList<CommandReceiver> customCommandReceivers;
private ControlDispatcher controlDispatcher;
private CustomActionProvider[] customActionProviders;
private Map<String, CustomActionProvider> customActionMap;
@Nullable private MediaMetadataProvider mediaMetadataProvider;
@Nullable private Player player;
@Nullable private ErrorMessageProvider<? super ExoPlaybackException> errorMessageProvider;
@Nullable private Pair<Integer, CharSequence> customError;
@Nullable private Bundle customErrorExtras;
@Nullable private PlaybackPreparer playbackPreparer;
@Nullable private QueueNavigator queueNavigator;
@Nullable private QueueEditor queueEditor;
@Nullable private RatingCallback ratingCallback;
@Nullable private CaptionCallback captionCallback;
@Nullable private MediaButtonEventHandler mediaButtonEventHandler;
private long enabledPlaybackActions;
/**
* Creates an instance.
*
* @param mediaSession The {@link MediaSessionCompat} to connect to.
*/
public MediaSessionConnector(MediaSessionCompat mediaSession) {
this.mediaSession = mediaSession;
looper = Util.getLooper();
componentListener = new ComponentListener();
commandReceivers = new ArrayList<>();
customCommandReceivers = new ArrayList<>();
controlDispatcher = new DefaultControlDispatcher();
customActionProviders = new CustomActionProvider[0];
customActionMap = Collections.emptyMap();
mediaMetadataProvider =
new DefaultMediaMetadataProvider(
mediaSession.getController(), /* metadataExtrasPrefix= */ null);
enabledPlaybackActions = DEFAULT_PLAYBACK_ACTIONS;
mediaSession.setFlags(BASE_MEDIA_SESSION_FLAGS);
mediaSession.setCallback(componentListener, new Handler(looper));
}
/**
* Sets the player to be connected to the media session. Must be called on the same thread that is
* used to access the player.
*
* @param player The player to be connected to the {@code MediaSession}, or {@code null} to
* disconnect the current player.
*/
public void setPlayer(@Nullable Player player) {
Assertions.checkArgument(player == null || player.getApplicationLooper() == looper);
if (this.player != null) {
this.player.removeListener(componentListener);
}
this.player = player;
if (player != null) {
player.addListener(componentListener);
}
invalidateMediaSessionPlaybackState();
invalidateMediaSessionMetadata();
}
/**
* Sets the {@link PlaybackPreparer}.
*
* @param playbackPreparer The {@link PlaybackPreparer}.
*/
public void setPlaybackPreparer(@Nullable PlaybackPreparer playbackPreparer) {
if (this.playbackPreparer != playbackPreparer) {
unregisterCommandReceiver(this.playbackPreparer);
this.playbackPreparer = playbackPreparer;
registerCommandReceiver(playbackPreparer);
invalidateMediaSessionPlaybackState();
}
}
/**
* Sets the {@link ControlDispatcher}.
*
* @param controlDispatcher The {@link ControlDispatcher}.
*/
public void setControlDispatcher(ControlDispatcher controlDispatcher) {
if (this.controlDispatcher != controlDispatcher) {
this.controlDispatcher = controlDispatcher;
invalidateMediaSessionPlaybackState();
}
}
/**
* Sets the {@link MediaButtonEventHandler}. Pass {@code null} if the media button event should be
* handled by {@link MediaSessionCompat.Callback#onMediaButtonEvent(Intent)}.
*
* <p>Please note that prior to API 21 MediaButton events are not delivered to the {@link
* MediaSessionCompat}. Instead they are delivered as key events (see <a
* href="https://developer.android.com/guide/topics/media-apps/mediabuttons">'Responding to media
* buttons'</a>). In an {@link android.app.Activity Activity}, media button events arrive at the
* {@link android.app.Activity#dispatchKeyEvent(KeyEvent)} method.
*
* <p>If you are running the player in a foreground service (prior to API 21), you can create an
* intent filter and handle the {@code android.intent.action.MEDIA_BUTTON} action yourself. See <a
* href="https://developer.android.com/reference/androidx/media/session/MediaButtonReceiver#service-handling-action_media_button">
* Service handling ACTION_MEDIA_BUTTON</a> for more information.
*
* @param mediaButtonEventHandler The {@link MediaButtonEventHandler}, or null to let the event be
* handled by {@link MediaSessionCompat.Callback#onMediaButtonEvent(Intent)}.
*/
public void setMediaButtonEventHandler(
@Nullable MediaButtonEventHandler mediaButtonEventHandler) {
this.mediaButtonEventHandler = mediaButtonEventHandler;
}
/**
* Sets the enabled playback actions.
*
* @param enabledPlaybackActions The enabled playback actions.
*/
public void setEnabledPlaybackActions(@PlaybackActions long enabledPlaybackActions) {
enabledPlaybackActions &= ALL_PLAYBACK_ACTIONS;
if (this.enabledPlaybackActions != enabledPlaybackActions) {
this.enabledPlaybackActions = enabledPlaybackActions;
invalidateMediaSessionPlaybackState();
}
}
/**
* @deprecated Use {@link #setControlDispatcher(ControlDispatcher)} with {@link
* DefaultControlDispatcher#DefaultControlDispatcher(long, long)} instead.
*/
@SuppressWarnings("deprecation")
@Deprecated
public void setRewindIncrementMs(int rewindMs) {
if (controlDispatcher instanceof DefaultControlDispatcher) {
((DefaultControlDispatcher) controlDispatcher).setRewindIncrementMs(rewindMs);
invalidateMediaSessionPlaybackState();
}
}
/**
* @deprecated Use {@link #setControlDispatcher(ControlDispatcher)} with {@link
* DefaultControlDispatcher#DefaultControlDispatcher(long, long)} instead.
*/
@SuppressWarnings("deprecation")
@Deprecated
public void setFastForwardIncrementMs(int fastForwardMs) {
if (controlDispatcher instanceof DefaultControlDispatcher) {
((DefaultControlDispatcher) controlDispatcher).setFastForwardIncrementMs(fastForwardMs);
invalidateMediaSessionPlaybackState();
}
}
/**
* Sets the optional {@link ErrorMessageProvider}.
*
* @param errorMessageProvider The error message provider.
*/
public void setErrorMessageProvider(
@Nullable ErrorMessageProvider<? super ExoPlaybackException> errorMessageProvider) {
if (this.errorMessageProvider != errorMessageProvider) {
this.errorMessageProvider = errorMessageProvider;
invalidateMediaSessionPlaybackState();
}
}
/**
* 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(@Nullable QueueNavigator queueNavigator) {
if (this.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(@Nullable QueueEditor queueEditor) {
if (this.queueEditor != queueEditor) {
unregisterCommandReceiver(this.queueEditor);
this.queueEditor = queueEditor;
registerCommandReceiver(queueEditor);
mediaSession.setFlags(
queueEditor == null ? BASE_MEDIA_SESSION_FLAGS : EDITOR_MEDIA_SESSION_FLAGS);
}
}
/**
* Sets the {@link RatingCallback} to handle user ratings.
*
* @param ratingCallback The rating callback.
*/
public void setRatingCallback(@Nullable RatingCallback ratingCallback) {
if (this.ratingCallback != ratingCallback) {
unregisterCommandReceiver(this.ratingCallback);
this.ratingCallback = ratingCallback;
registerCommandReceiver(this.ratingCallback);
}
}
/**
* Sets the {@link CaptionCallback} to handle requests to enable or disable captions.
*
* @param captionCallback The caption callback.
*/
public void setCaptionCallback(@Nullable CaptionCallback captionCallback) {
if (this.captionCallback != captionCallback) {
unregisterCommandReceiver(this.captionCallback);
this.captionCallback = captionCallback;
registerCommandReceiver(this.captionCallback);
}
}
/**
* Sets a custom error on the session.
*
* <p>This sets the error code via {@link PlaybackStateCompat.Builder#setErrorMessage(int,
* CharSequence)}. By default, the error code will be set to {@link
* PlaybackStateCompat#ERROR_CODE_APP_ERROR}.
*
* @param message The error string to report or {@code null} to clear the error.
*/
public void setCustomErrorMessage(@Nullable CharSequence message) {
int code = (message == null) ? 0 : PlaybackStateCompat.ERROR_CODE_APP_ERROR;
setCustomErrorMessage(message, code);
}
/**
* Sets a custom error on the session.
*
* @param message The error string to report or {@code null} to clear the error.
* @param code The error code to report. Ignored when {@code message} is {@code null}.
*/
public void setCustomErrorMessage(@Nullable CharSequence message, int code) {
setCustomErrorMessage(message, code, /* extras= */ null);
}
/**
* Sets a custom error on the session.
*
* @param message The error string to report or {@code null} to clear the error.
* @param code The error code to report. Ignored when {@code message} is {@code null}.
* @param extras Extras to include in reported {@link PlaybackStateCompat}.
*/
public void setCustomErrorMessage(
@Nullable CharSequence message, int code, @Nullable Bundle extras) {
customError = (message == null) ? null : new Pair<>(code, message);
customErrorExtras = (message == null) ? null : extras;
invalidateMediaSessionPlaybackState();
}
/**
* Sets custom action providers. The order of the {@link CustomActionProvider}s determines the
* order in which the actions are published.
*
* @param customActionProviders The custom action providers, or null to remove all existing custom
* action providers.
*/
public void setCustomActionProviders(@Nullable CustomActionProvider... customActionProviders) {
this.customActionProviders =
customActionProviders == null ? new CustomActionProvider[0] : customActionProviders;
invalidateMediaSessionPlaybackState();
}
/**
* Sets a provider of metadata to be published to the media session. Pass {@code null} if no
* metadata should be published.
*
* @param mediaMetadataProvider The provider of metadata to publish, or {@code null} if no
* metadata should be published.
*/
public void setMediaMetadataProvider(@Nullable MediaMetadataProvider mediaMetadataProvider) {
if (this.mediaMetadataProvider != mediaMetadataProvider) {
this.mediaMetadataProvider = mediaMetadataProvider;
invalidateMediaSessionMetadata();
}
}
/**
* Updates the metadata of the media session.
*
* <p>Apps normally only need to call this method when the backing data for a given media item has
* changed and the metadata should be updated immediately.
*
* <p>The {@link MediaMetadataCompat} which is published to the session is obtained by calling
* {@link MediaMetadataProvider#getMetadata(Player)}.
*/
public final void invalidateMediaSessionMetadata() {
MediaMetadataCompat metadata =
mediaMetadataProvider != null && player != null
? mediaMetadataProvider.getMetadata(player)
: METADATA_EMPTY;
mediaSession.setMetadata(metadata);
}
/**
* Updates the playback state of the media session.
*
* <p>Apps normally only need to call this method when the custom actions provided by a {@link
* CustomActionProvider} changed and the playback state needs to be updated immediately.
*/
public final void invalidateMediaSessionPlaybackState() {
PlaybackStateCompat.Builder builder = new PlaybackStateCompat.Builder();
@Nullable Player player = this.player;
if (player == null) {
builder
.setActions(buildPrepareActions())
.setState(
PlaybackStateCompat.STATE_NONE,
/* position= */ 0,
/* playbackSpeed= */ 0,
/* updateTime= */ SystemClock.elapsedRealtime());
mediaSession.setRepeatMode(PlaybackStateCompat.REPEAT_MODE_NONE);
mediaSession.setShuffleMode(PlaybackStateCompat.SHUFFLE_MODE_NONE);
mediaSession.setPlaybackState(builder.build());
return;
}
Map<String, CustomActionProvider> currentActions = new HashMap<>();
for (CustomActionProvider customActionProvider : customActionProviders) {
@Nullable
PlaybackStateCompat.CustomAction customAction = customActionProvider.getCustomAction(player);
if (customAction != null) {
currentActions.put(customAction.getAction(), customActionProvider);
builder.addCustomAction(customAction);
}
}
customActionMap = Collections.unmodifiableMap(currentActions);
Bundle extras = new Bundle();
@Nullable ExoPlaybackException playbackError = player.getPlayerError();
boolean reportError = playbackError != null || customError != null;
int sessionPlaybackState =
reportError
? PlaybackStateCompat.STATE_ERROR
: getMediaSessionPlaybackState(player.getPlaybackState(), player.getPlayWhenReady());
if (customError != null) {
builder.setErrorMessage(customError.first, customError.second);
if (customErrorExtras != null) {
extras.putAll(customErrorExtras);
}
} else if (playbackError != null && errorMessageProvider != null) {
Pair<Integer, String> message = errorMessageProvider.getErrorMessage(playbackError);
builder.setErrorMessage(message.first, message.second);
}
long activeQueueItemId =
queueNavigator != null
? queueNavigator.getActiveQueueItemId(player)
: MediaSessionCompat.QueueItem.UNKNOWN_ID;
float playbackSpeed = player.getPlaybackSpeed();
extras.putFloat(EXTRAS_SPEED, playbackSpeed);
float sessionPlaybackSpeed = player.isPlaying() ? playbackSpeed : 0f;
builder
.setActions(buildPrepareActions() | buildPlaybackActions(player))
.setActiveQueueItemId(activeQueueItemId)
.setBufferedPosition(player.getBufferedPosition())
.setState(
sessionPlaybackState,
player.getCurrentPosition(),
sessionPlaybackSpeed,
/* updateTime= */ SystemClock.elapsedRealtime())
.setExtras(extras);
@Player.RepeatMode int repeatMode = player.getRepeatMode();
mediaSession.setRepeatMode(
repeatMode == Player.REPEAT_MODE_ONE
? PlaybackStateCompat.REPEAT_MODE_ONE
: repeatMode == Player.REPEAT_MODE_ALL
? PlaybackStateCompat.REPEAT_MODE_ALL
: PlaybackStateCompat.REPEAT_MODE_NONE);
mediaSession.setShuffleMode(
player.getShuffleModeEnabled()
? PlaybackStateCompat.SHUFFLE_MODE_ALL
: PlaybackStateCompat.SHUFFLE_MODE_NONE);
mediaSession.setPlaybackState(builder.build());
}
/**
* Updates the queue of the media session by calling {@link
* QueueNavigator#onTimelineChanged(Player)}.
*
* <p>Apps normally only need to call this method when the backing data for a given queue item has
* changed and the queue should be updated immediately.
*/
public final void invalidateMediaSessionQueue() {
if (queueNavigator != null && player != null) {
queueNavigator.onTimelineChanged(player);
}
}
/**
* Registers a custom command receiver for responding to commands delivered via {@link
* MediaSessionCompat.Callback#onCommand(String, Bundle, ResultReceiver)}.
*
* <p>Commands are only dispatched to this receiver when a player is connected.
*
* @param commandReceiver The command receiver to register.
*/
public void registerCustomCommandReceiver(@Nullable CommandReceiver commandReceiver) {
if (commandReceiver != null && !customCommandReceivers.contains(commandReceiver)) {
customCommandReceivers.add(commandReceiver);
}
}
/**
* Unregisters a previously registered custom command receiver.
*
* @param commandReceiver The command receiver to unregister.
*/
public void unregisterCustomCommandReceiver(@Nullable CommandReceiver commandReceiver) {
if (commandReceiver != null) {
customCommandReceivers.remove(commandReceiver);
}
}
private void registerCommandReceiver(@Nullable CommandReceiver commandReceiver) {
if (commandReceiver != null && !commandReceivers.contains(commandReceiver)) {
commandReceivers.add(commandReceiver);
}
}
private void unregisterCommandReceiver(@Nullable CommandReceiver commandReceiver) {
if (commandReceiver != null) {
commandReceivers.remove(commandReceiver);
}
}
private long buildPrepareActions() {
return playbackPreparer == null
? 0
: (PlaybackPreparer.ACTIONS & playbackPreparer.getSupportedPrepareActions());
}
private long buildPlaybackActions(Player player) {
boolean enableSeeking = false;
boolean enableRewind = false;
boolean enableFastForward = false;
boolean enableSetRating = false;
boolean enableSetCaptioningEnabled = false;
Timeline timeline = player.getCurrentTimeline();
if (!timeline.isEmpty() && !player.isPlayingAd()) {
enableSeeking = player.isCurrentWindowSeekable();
enableRewind = enableSeeking && controlDispatcher.isRewindEnabled();
enableFastForward = enableSeeking && controlDispatcher.isFastForwardEnabled();
enableSetRating = ratingCallback != null;
enableSetCaptioningEnabled = captionCallback != null && captionCallback.hasCaptions(player);
}
long playbackActions = BASE_PLAYBACK_ACTIONS;
if (enableSeeking) {
playbackActions |= PlaybackStateCompat.ACTION_SEEK_TO;
}
if (enableFastForward) {
playbackActions |= PlaybackStateCompat.ACTION_FAST_FORWARD;
}
if (enableRewind) {
playbackActions |= PlaybackStateCompat.ACTION_REWIND;
}
playbackActions &= enabledPlaybackActions;
long actions = playbackActions;
if (queueNavigator != null) {
actions |=
(QueueNavigator.ACTIONS & queueNavigator.getSupportedQueueNavigatorActions(player));
}
if (enableSetRating) {
actions |= PlaybackStateCompat.ACTION_SET_RATING;
}
if (enableSetCaptioningEnabled) {
actions |= PlaybackStateCompat.ACTION_SET_CAPTIONING_ENABLED;
}
return actions;
}
@EnsuresNonNullIf(result = true, expression = "player")
private boolean canDispatchPlaybackAction(long action) {
return player != null && (enabledPlaybackActions & action) != 0;
}
@EnsuresNonNullIf(result = true, expression = "playbackPreparer")
private boolean canDispatchToPlaybackPreparer(long action) {
return playbackPreparer != null
&& (playbackPreparer.getSupportedPrepareActions() & action) != 0;
}
@EnsuresNonNullIf(
result = true,
expression = {"player", "queueNavigator"})
private boolean canDispatchToQueueNavigator(long action) {
return player != null
&& queueNavigator != null
&& (queueNavigator.getSupportedQueueNavigatorActions(player) & action) != 0;
}
@EnsuresNonNullIf(
result = true,
expression = {"player", "ratingCallback"})
private boolean canDispatchSetRating() {
return player != null && ratingCallback != null;
}
@EnsuresNonNullIf(
result = true,
expression = {"player", "captionCallback"})
private boolean canDispatchSetCaptioningEnabled() {
return player != null && captionCallback != null;
}
@EnsuresNonNullIf(
result = true,
expression = {"player", "queueEditor"})
private boolean canDispatchQueueEdit() {
return player != null && queueEditor != null;
}
@EnsuresNonNullIf(
result = true,
expression = {"player", "mediaButtonEventHandler"})
private boolean canDispatchMediaButtonEvent() {
return player != null && mediaButtonEventHandler != null;
}
private void seekTo(Player player, int windowIndex, long positionMs) {
controlDispatcher.dispatchSeekTo(player, windowIndex, positionMs);
}
private static int getMediaSessionPlaybackState(
@Player.State 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_STOPPED;
case Player.STATE_IDLE:
default:
return PlaybackStateCompat.STATE_NONE;
}
}
/**
* Provides a default {@link MediaMetadataCompat} with properties and extras taken from the {@link
* MediaDescriptionCompat} of the {@link MediaSessionCompat.QueueItem} of the active queue item.
*/
public static final class DefaultMediaMetadataProvider implements MediaMetadataProvider {
private final MediaControllerCompat mediaController;
private final String metadataExtrasPrefix;
/**
* Creates a new instance.
*
* @param mediaController The {@link MediaControllerCompat}.
* @param metadataExtrasPrefix A string to prefix extra keys which are propagated from the
* active queue item to the session metadata.
*/
public DefaultMediaMetadataProvider(
MediaControllerCompat mediaController, @Nullable String metadataExtrasPrefix) {
this.mediaController = mediaController;
this.metadataExtrasPrefix = metadataExtrasPrefix != null ? metadataExtrasPrefix : "";
}
@Override
public MediaMetadataCompat getMetadata(Player player) {
if (player.getCurrentTimeline().isEmpty()) {
return METADATA_EMPTY;
}
MediaMetadataCompat.Builder builder = new MediaMetadataCompat.Builder();
if (player.isPlayingAd()) {
builder.putLong(MediaMetadataCompat.METADATA_KEY_ADVERTISEMENT, 1);
}
builder.putLong(
MediaMetadataCompat.METADATA_KEY_DURATION,
player.isCurrentWindowDynamic() || player.getDuration() == C.TIME_UNSET
? -1
: player.getDuration());
long activeQueueItemId = mediaController.getPlaybackState().getActiveQueueItemId();
if (activeQueueItemId != MediaSessionCompat.QueueItem.UNKNOWN_ID) {
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();
@Nullable Bundle extras = description.getExtras();
if (extras != null) {
for (String key : extras.keySet()) {
@Nullable Object value = extras.get(key);
if (value instanceof String) {
builder.putString(metadataExtrasPrefix + key, (String) value);
} else if (value instanceof CharSequence) {
builder.putText(metadataExtrasPrefix + key, (CharSequence) value);
} else if (value instanceof Long) {
builder.putLong(metadataExtrasPrefix + key, (Long) value);
} else if (value instanceof Integer) {
builder.putLong(metadataExtrasPrefix + key, (Integer) value);
} else if (value instanceof Bitmap) {
builder.putBitmap(metadataExtrasPrefix + key, (Bitmap) value);
} else if (value instanceof RatingCompat) {
builder.putRating(metadataExtrasPrefix + key, (RatingCompat) value);
}
}
}
@Nullable CharSequence title = description.getTitle();
if (title != null) {
String titleString = String.valueOf(title);
builder.putString(MediaMetadataCompat.METADATA_KEY_TITLE, titleString);
builder.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_TITLE, titleString);
}
@Nullable CharSequence subtitle = description.getSubtitle();
if (subtitle != null) {
builder.putString(
MediaMetadataCompat.METADATA_KEY_DISPLAY_SUBTITLE, String.valueOf(subtitle));
}
@Nullable CharSequence displayDescription = description.getDescription();
if (displayDescription != null) {
builder.putString(
MediaMetadataCompat.METADATA_KEY_DISPLAY_DESCRIPTION,
String.valueOf(displayDescription));
}
@Nullable Bitmap iconBitmap = description.getIconBitmap();
if (iconBitmap != null) {
builder.putBitmap(MediaMetadataCompat.METADATA_KEY_DISPLAY_ICON, iconBitmap);
}
@Nullable Uri iconUri = description.getIconUri();
if (iconUri != null) {
builder.putString(
MediaMetadataCompat.METADATA_KEY_DISPLAY_ICON_URI, String.valueOf(iconUri));
}
@Nullable String mediaId = description.getMediaId();
if (mediaId != null) {
builder.putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, mediaId);
}
@Nullable Uri mediaUri = description.getMediaUri();
if (mediaUri != null) {
builder.putString(
MediaMetadataCompat.METADATA_KEY_MEDIA_URI, String.valueOf(mediaUri));
}
break;
}
}
}
return builder.build();
}
}
private class ComponentListener extends MediaSessionCompat.Callback
implements Player.EventListener {
private int currentWindowIndex;
private int currentWindowCount;
// Player.EventListener implementation.
@Override
public void onTimelineChanged(Timeline timeline, @Player.TimelineChangeReason int reason) {
Player player = Assertions.checkNotNull(MediaSessionConnector.this.player);
int windowCount = player.getCurrentTimeline().getWindowCount();
int windowIndex = player.getCurrentWindowIndex();
if (queueNavigator != null) {
queueNavigator.onTimelineChanged(player);
invalidateMediaSessionPlaybackState();
} else if (currentWindowCount != windowCount || currentWindowIndex != windowIndex) {
// active queue item and queue navigation actions may need to be updated
invalidateMediaSessionPlaybackState();
}
currentWindowCount = windowCount;
currentWindowIndex = windowIndex;
invalidateMediaSessionMetadata();
}
@Override
public void onPlaybackStateChanged(@Player.State int playbackState) {
invalidateMediaSessionPlaybackState();
}
@Override
public void onPlayWhenReadyChanged(
boolean playWhenReady, @Player.PlayWhenReadyChangeReason int reason) {
invalidateMediaSessionPlaybackState();
}
@Override
public void onIsPlayingChanged(boolean isPlaying) {
invalidateMediaSessionPlaybackState();
}
@Override
public void onRepeatModeChanged(@Player.RepeatMode int repeatMode) {
invalidateMediaSessionPlaybackState();
}
@Override
public void onShuffleModeEnabledChanged(boolean shuffleModeEnabled) {
invalidateMediaSessionPlaybackState();
invalidateMediaSessionQueue();
}
@Override
public void onPositionDiscontinuity(@Player.DiscontinuityReason int reason) {
Player player = Assertions.checkNotNull(MediaSessionConnector.this.player);
if (currentWindowIndex != player.getCurrentWindowIndex()) {
if (queueNavigator != null) {
queueNavigator.onCurrentWindowIndexChanged(player);
}
currentWindowIndex = player.getCurrentWindowIndex();
// Update playback state after queueNavigator.onCurrentWindowIndexChanged has been called
// and before updating metadata.
invalidateMediaSessionPlaybackState();
invalidateMediaSessionMetadata();
return;
}
invalidateMediaSessionPlaybackState();
}
@Override
public void onPlaybackSpeedChanged(float playbackSpeed) {
invalidateMediaSessionPlaybackState();
}
// MediaSessionCompat.Callback implementation.
@Override
public void onPlay() {
if (canDispatchPlaybackAction(PlaybackStateCompat.ACTION_PLAY)) {
if (player.getPlaybackState() == Player.STATE_IDLE) {
if (playbackPreparer != null) {
playbackPreparer.onPrepare(/* playWhenReady= */ true);
}
} else if (player.getPlaybackState() == Player.STATE_ENDED) {
seekTo(player, player.getCurrentWindowIndex(), C.TIME_UNSET);
}
controlDispatcher.dispatchSetPlayWhenReady(
Assertions.checkNotNull(player), /* playWhenReady= */ true);
}
}
@Override
public void onPause() {
if (canDispatchPlaybackAction(PlaybackStateCompat.ACTION_PAUSE)) {
controlDispatcher.dispatchSetPlayWhenReady(player, /* playWhenReady= */ false);
}
}
@Override
public void onSeekTo(long positionMs) {
if (canDispatchPlaybackAction(PlaybackStateCompat.ACTION_SEEK_TO)) {
seekTo(player, player.getCurrentWindowIndex(), positionMs);
}
}
@Override
public void onFastForward() {
if (canDispatchPlaybackAction(PlaybackStateCompat.ACTION_FAST_FORWARD)) {
controlDispatcher.dispatchFastForward(player);
}
}
@Override
public void onRewind() {
if (canDispatchPlaybackAction(PlaybackStateCompat.ACTION_REWIND)) {
controlDispatcher.dispatchRewind(player);
}
}
@Override
public void onStop() {
if (canDispatchPlaybackAction(PlaybackStateCompat.ACTION_STOP)) {
controlDispatcher.dispatchStop(player, /* reset= */ true);
}
}
@Override
public void onSetShuffleMode(@PlaybackStateCompat.ShuffleMode int shuffleMode) {
if (canDispatchPlaybackAction(PlaybackStateCompat.ACTION_SET_SHUFFLE_MODE)) {
boolean shuffleModeEnabled;
switch (shuffleMode) {
case PlaybackStateCompat.SHUFFLE_MODE_ALL:
case PlaybackStateCompat.SHUFFLE_MODE_GROUP:
shuffleModeEnabled = true;
break;
case PlaybackStateCompat.SHUFFLE_MODE_NONE:
case PlaybackStateCompat.SHUFFLE_MODE_INVALID:
default:
shuffleModeEnabled = false;
break;
}
controlDispatcher.dispatchSetShuffleModeEnabled(player, shuffleModeEnabled);
}
}
@Override
public void onSetRepeatMode(@PlaybackStateCompat.RepeatMode int mediaSessionRepeatMode) {
if (canDispatchPlaybackAction(PlaybackStateCompat.ACTION_SET_REPEAT_MODE)) {
@RepeatModeUtil.RepeatToggleModes int repeatMode;
switch (mediaSessionRepeatMode) {
case PlaybackStateCompat.REPEAT_MODE_ALL:
case PlaybackStateCompat.REPEAT_MODE_GROUP:
repeatMode = Player.REPEAT_MODE_ALL;
break;
case PlaybackStateCompat.REPEAT_MODE_ONE:
repeatMode = Player.REPEAT_MODE_ONE;
break;
case PlaybackStateCompat.REPEAT_MODE_NONE:
case PlaybackStateCompat.REPEAT_MODE_INVALID:
default:
repeatMode = Player.REPEAT_MODE_OFF;
break;
}
controlDispatcher.dispatchSetRepeatMode(player, repeatMode);
}
}
@Override
public void onSkipToNext() {
if (canDispatchToQueueNavigator(PlaybackStateCompat.ACTION_SKIP_TO_NEXT)) {
queueNavigator.onSkipToNext(player, controlDispatcher);
}
}
@Override
public void onSkipToPrevious() {
if (canDispatchToQueueNavigator(PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS)) {
queueNavigator.onSkipToPrevious(player, controlDispatcher);
}
}
@Override
public void onSkipToQueueItem(long id) {
if (canDispatchToQueueNavigator(PlaybackStateCompat.ACTION_SKIP_TO_QUEUE_ITEM)) {
queueNavigator.onSkipToQueueItem(player, controlDispatcher, id);
}
}
@Override
public void onCustomAction(String action, @Nullable Bundle extras) {
if (player != null && customActionMap.containsKey(action)) {
customActionMap.get(action).onCustomAction(player, controlDispatcher, action, extras);
invalidateMediaSessionPlaybackState();
}
}
@Override
public void onCommand(String command, @Nullable Bundle extras, @Nullable ResultReceiver cb) {
if (player != null) {
for (int i = 0; i < commandReceivers.size(); i++) {
if (commandReceivers.get(i).onCommand(player, controlDispatcher, command, extras, cb)) {
return;
}
}
for (int i = 0; i < customCommandReceivers.size(); i++) {
if (customCommandReceivers
.get(i)
.onCommand(player, controlDispatcher, command, extras, cb)) {
return;
}
}
}
}
@Override
public void onPrepare() {
if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PREPARE)) {
playbackPreparer.onPrepare(/* playWhenReady= */ false);
}
}
@Override
public void onPrepareFromMediaId(String mediaId, Bundle extras) {
if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PREPARE_FROM_MEDIA_ID)) {
playbackPreparer.onPrepareFromMediaId(mediaId, /* playWhenReady= */ false, extras);
}
}
@Override
public void onPrepareFromSearch(String query, Bundle extras) {
if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PREPARE_FROM_SEARCH)) {
playbackPreparer.onPrepareFromSearch(query, /* playWhenReady= */ false, extras);
}
}
@Override
public void onPrepareFromUri(Uri uri, Bundle extras) {
if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PREPARE_FROM_URI)) {
playbackPreparer.onPrepareFromUri(uri, /* playWhenReady= */ false, extras);
}
}
@Override
public void onPlayFromMediaId(String mediaId, Bundle extras) {
if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PLAY_FROM_MEDIA_ID)) {
playbackPreparer.onPrepareFromMediaId(mediaId, /* playWhenReady= */ true, extras);
}
}
@Override
public void onPlayFromSearch(String query, Bundle extras) {
if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PLAY_FROM_SEARCH)) {
playbackPreparer.onPrepareFromSearch(query, /* playWhenReady= */ true, extras);
}
}
@Override
public void onPlayFromUri(Uri uri, Bundle extras) {
if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PLAY_FROM_URI)) {
playbackPreparer.onPrepareFromUri(uri, /* playWhenReady= */ true, extras);
}
}
@Override
public void onSetRating(RatingCompat rating) {
if (canDispatchSetRating()) {
ratingCallback.onSetRating(player, rating);
}
}
@Override
public void onSetRating(RatingCompat rating, Bundle extras) {
if (canDispatchSetRating()) {
ratingCallback.onSetRating(player, rating, extras);
}
}
@Override
public void onAddQueueItem(MediaDescriptionCompat description) {
if (canDispatchQueueEdit()) {
queueEditor.onAddQueueItem(player, description);
}
}
@Override
public void onAddQueueItem(MediaDescriptionCompat description, int index) {
if (canDispatchQueueEdit()) {
queueEditor.onAddQueueItem(player, description, index);
}
}
@Override
public void onRemoveQueueItem(MediaDescriptionCompat description) {
if (canDispatchQueueEdit()) {
queueEditor.onRemoveQueueItem(player, description);
}
}
@Override
public void onSetCaptioningEnabled(boolean enabled) {
if (canDispatchSetCaptioningEnabled()) {
captionCallback.onSetCaptioningEnabled(player, enabled);
}
}
@Override
public boolean onMediaButtonEvent(Intent mediaButtonEvent) {
boolean isHandled =
canDispatchMediaButtonEvent()
&& mediaButtonEventHandler.onMediaButtonEvent(
player, controlDispatcher, mediaButtonEvent);
return isHandled || super.onMediaButtonEvent(mediaButtonEvent);
}
}
}