1 /*
2  * Copyright (C) 2017 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.google.android.exoplayer2.ext.mediasession;
17 
18 import android.content.Intent;
19 import android.graphics.Bitmap;
20 import android.net.Uri;
21 import android.os.Bundle;
22 import android.os.Handler;
23 import android.os.Looper;
24 import android.os.ResultReceiver;
25 import android.os.SystemClock;
26 import android.support.v4.media.MediaDescriptionCompat;
27 import android.support.v4.media.MediaMetadataCompat;
28 import android.support.v4.media.RatingCompat;
29 import android.support.v4.media.session.MediaControllerCompat;
30 import android.support.v4.media.session.MediaSessionCompat;
31 import android.support.v4.media.session.PlaybackStateCompat;
32 import android.util.Pair;
33 import android.view.KeyEvent;
34 import androidx.annotation.LongDef;
35 import androidx.annotation.Nullable;
36 import com.google.android.exoplayer2.C;
37 import com.google.android.exoplayer2.ControlDispatcher;
38 import com.google.android.exoplayer2.DefaultControlDispatcher;
39 import com.google.android.exoplayer2.ExoPlaybackException;
40 import com.google.android.exoplayer2.ExoPlayerLibraryInfo;
41 import com.google.android.exoplayer2.Player;
42 import com.google.android.exoplayer2.Timeline;
43 import com.google.android.exoplayer2.util.Assertions;
44 import com.google.android.exoplayer2.util.ErrorMessageProvider;
45 import com.google.android.exoplayer2.util.RepeatModeUtil;
46 import com.google.android.exoplayer2.util.Util;
47 import java.lang.annotation.Retention;
48 import java.lang.annotation.RetentionPolicy;
49 import java.util.ArrayList;
50 import java.util.Collections;
51 import java.util.HashMap;
52 import java.util.List;
53 import java.util.Map;
54 import org.checkerframework.checker.nullness.qual.EnsuresNonNullIf;
55 
56 /**
57  * Connects a {@link MediaSessionCompat} to a {@link Player}.
58  *
59  * <p>This connector does <em>not</em> call {@link MediaSessionCompat#setActive(boolean)}, and so
60  * application code is responsible for making the session active when desired. A session must be
61  * active for transport controls to be displayed (e.g. on the lock screen) and for it to receive
62  * media button events.
63  *
64  * <p>The connector listens for actions sent by the media session's controller and implements these
65  * actions by calling appropriate player methods. The playback state of the media session is
66  * automatically synced with the player. The connector can also be optionally extended by providing
67  * various collaborators:
68  *
69  * <ul>
70  *   <li>Actions to initiate media playback ({@code PlaybackStateCompat#ACTION_PREPARE_*} and {@code
71  *       PlaybackStateCompat#ACTION_PLAY_*}) can be handled by a {@link PlaybackPreparer} passed to
72  *       {@link #setPlaybackPreparer(PlaybackPreparer)}.
73  *   <li>Custom actions can be handled by passing one or more {@link CustomActionProvider}s to
74  *       {@link #setCustomActionProviders(CustomActionProvider...)}.
75  *   <li>To enable a media queue and navigation within it, you can set a {@link QueueNavigator} by
76  *       calling {@link #setQueueNavigator(QueueNavigator)}. Use of {@link TimelineQueueNavigator}
77  *       is recommended for most use cases.
78  *   <li>To enable editing of the media queue, you can set a {@link QueueEditor} by calling {@link
79  *       #setQueueEditor(QueueEditor)}.
80  *   <li>A {@link MediaButtonEventHandler} can be set by calling {@link
81  *       #setMediaButtonEventHandler(MediaButtonEventHandler)}. By default media button events are
82  *       handled by {@link MediaSessionCompat.Callback#onMediaButtonEvent(Intent)}.
83  *   <li>An {@link ErrorMessageProvider} for providing human readable error messages and
84  *       corresponding error codes can be set by calling {@link
85  *       #setErrorMessageProvider(ErrorMessageProvider)}.
86  *   <li>A {@link MediaMetadataProvider} can be set by calling {@link
87  *       #setMediaMetadataProvider(MediaMetadataProvider)}. By default the {@link
88  *       DefaultMediaMetadataProvider} is used.
89  * </ul>
90  */
91 public final class MediaSessionConnector {
92 
93   static {
94     ExoPlayerLibraryInfo.registerModule("goog.exo.mediasession");
95   }
96 
97   /** Playback actions supported by the connector. */
98   @LongDef(
99       flag = true,
100       value = {
101         PlaybackStateCompat.ACTION_PLAY_PAUSE,
102         PlaybackStateCompat.ACTION_PLAY,
103         PlaybackStateCompat.ACTION_PAUSE,
104         PlaybackStateCompat.ACTION_SEEK_TO,
105         PlaybackStateCompat.ACTION_FAST_FORWARD,
106         PlaybackStateCompat.ACTION_REWIND,
107         PlaybackStateCompat.ACTION_STOP,
108         PlaybackStateCompat.ACTION_SET_REPEAT_MODE,
109         PlaybackStateCompat.ACTION_SET_SHUFFLE_MODE
110       })
111   @Retention(RetentionPolicy.SOURCE)
112   public @interface PlaybackActions {}
113 
114   @PlaybackActions
115   public static final long ALL_PLAYBACK_ACTIONS =
116       PlaybackStateCompat.ACTION_PLAY_PAUSE
117           | PlaybackStateCompat.ACTION_PLAY
118           | PlaybackStateCompat.ACTION_PAUSE
119           | PlaybackStateCompat.ACTION_SEEK_TO
120           | PlaybackStateCompat.ACTION_FAST_FORWARD
121           | PlaybackStateCompat.ACTION_REWIND
122           | PlaybackStateCompat.ACTION_STOP
123           | PlaybackStateCompat.ACTION_SET_REPEAT_MODE
124           | PlaybackStateCompat.ACTION_SET_SHUFFLE_MODE;
125 
126   /** The default playback actions. */
127   @PlaybackActions public static final long DEFAULT_PLAYBACK_ACTIONS = ALL_PLAYBACK_ACTIONS;
128 
129   /**
130    * The name of the {@link PlaybackStateCompat} float extra with the value of {@link
131    * Player#getPlaybackSpeed()}.
132    */
133   public static final String EXTRAS_SPEED = "EXO_SPEED";
134 
135   private static final long BASE_PLAYBACK_ACTIONS =
136       PlaybackStateCompat.ACTION_PLAY_PAUSE
137           | PlaybackStateCompat.ACTION_PLAY
138           | PlaybackStateCompat.ACTION_PAUSE
139           | PlaybackStateCompat.ACTION_STOP
140           | PlaybackStateCompat.ACTION_SET_SHUFFLE_MODE
141           | PlaybackStateCompat.ACTION_SET_REPEAT_MODE;
142   private static final int BASE_MEDIA_SESSION_FLAGS =
143       MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS
144           | MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS;
145   private static final int EDITOR_MEDIA_SESSION_FLAGS =
146       BASE_MEDIA_SESSION_FLAGS | MediaSessionCompat.FLAG_HANDLES_QUEUE_COMMANDS;
147 
148   private static final MediaMetadataCompat METADATA_EMPTY =
149       new MediaMetadataCompat.Builder().build();
150 
151   /** Receiver of media commands sent by a media controller. */
152   public interface CommandReceiver {
153     /**
154      * See {@link MediaSessionCompat.Callback#onCommand(String, Bundle, ResultReceiver)}. The
155      * receiver may handle the command, but is not required to do so. Changes to the player should
156      * be made via the {@link ControlDispatcher}.
157      *
158      * @param player The player connected to the media session.
159      * @param controlDispatcher A {@link ControlDispatcher} that should be used for dispatching
160      *     changes to the player.
161      * @param command The command name.
162      * @param extras Optional parameters for the command, may be null.
163      * @param cb A result receiver to which a result may be sent by the command, may be null.
164      * @return Whether the receiver handled the command.
165      */
onCommand( Player player, ControlDispatcher controlDispatcher, String command, @Nullable Bundle extras, @Nullable ResultReceiver cb)166     boolean onCommand(
167         Player player,
168         ControlDispatcher controlDispatcher,
169         String command,
170         @Nullable Bundle extras,
171         @Nullable ResultReceiver cb);
172   }
173 
174   /** Interface to which playback preparation and play actions are delegated. */
175   public interface PlaybackPreparer extends CommandReceiver {
176 
177     long ACTIONS =
178         PlaybackStateCompat.ACTION_PREPARE
179             | PlaybackStateCompat.ACTION_PREPARE_FROM_MEDIA_ID
180             | PlaybackStateCompat.ACTION_PREPARE_FROM_SEARCH
181             | PlaybackStateCompat.ACTION_PREPARE_FROM_URI
182             | PlaybackStateCompat.ACTION_PLAY_FROM_MEDIA_ID
183             | PlaybackStateCompat.ACTION_PLAY_FROM_SEARCH
184             | PlaybackStateCompat.ACTION_PLAY_FROM_URI;
185 
186     /**
187      * Returns the actions which are supported by the preparer. The supported actions must be a
188      * bitmask combined out of {@link PlaybackStateCompat#ACTION_PREPARE}, {@link
189      * PlaybackStateCompat#ACTION_PREPARE_FROM_MEDIA_ID}, {@link
190      * PlaybackStateCompat#ACTION_PREPARE_FROM_SEARCH}, {@link
191      * PlaybackStateCompat#ACTION_PREPARE_FROM_URI}, {@link
192      * PlaybackStateCompat#ACTION_PLAY_FROM_MEDIA_ID}, {@link
193      * PlaybackStateCompat#ACTION_PLAY_FROM_SEARCH} and {@link
194      * PlaybackStateCompat#ACTION_PLAY_FROM_URI}.
195      *
196      * @return The bitmask of the supported media actions.
197      */
getSupportedPrepareActions()198     long getSupportedPrepareActions();
199     /**
200      * See {@link MediaSessionCompat.Callback#onPrepare()}.
201      *
202      * @param playWhenReady Whether playback should be started after preparation.
203      */
onPrepare(boolean playWhenReady)204     void onPrepare(boolean playWhenReady);
205     /**
206      * See {@link MediaSessionCompat.Callback#onPrepareFromMediaId(String, Bundle)}.
207      *
208      * @param mediaId The media id of the media item to be prepared.
209      * @param playWhenReady Whether playback should be started after preparation.
210      * @param extras A {@link Bundle} of extras passed by the media controller, may be null.
211      */
onPrepareFromMediaId(String mediaId, boolean playWhenReady, @Nullable Bundle extras)212     void onPrepareFromMediaId(String mediaId, boolean playWhenReady, @Nullable Bundle extras);
213     /**
214      * See {@link MediaSessionCompat.Callback#onPrepareFromSearch(String, Bundle)}.
215      *
216      * @param query The search query.
217      * @param playWhenReady Whether playback should be started after preparation.
218      * @param extras A {@link Bundle} of extras passed by the media controller, may be null.
219      */
onPrepareFromSearch(String query, boolean playWhenReady, @Nullable Bundle extras)220     void onPrepareFromSearch(String query, boolean playWhenReady, @Nullable Bundle extras);
221     /**
222      * See {@link MediaSessionCompat.Callback#onPrepareFromUri(Uri, Bundle)}.
223      *
224      * @param uri The {@link Uri} of the media item to be prepared.
225      * @param playWhenReady Whether playback should be started after preparation.
226      * @param extras A {@link Bundle} of extras passed by the media controller, may be null.
227      */
onPrepareFromUri(Uri uri, boolean playWhenReady, @Nullable Bundle extras)228     void onPrepareFromUri(Uri uri, boolean playWhenReady, @Nullable Bundle extras);
229   }
230 
231   /**
232    * Handles queue navigation actions, and updates the media session queue by calling {@code
233    * MediaSessionCompat.setQueue()}.
234    */
235   public interface QueueNavigator extends CommandReceiver {
236 
237     long ACTIONS =
238         PlaybackStateCompat.ACTION_SKIP_TO_QUEUE_ITEM
239             | PlaybackStateCompat.ACTION_SKIP_TO_NEXT
240             | PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS;
241 
242     /**
243      * Returns the actions which are supported by the navigator. The supported actions must be a
244      * bitmask combined out of {@link PlaybackStateCompat#ACTION_SKIP_TO_QUEUE_ITEM}, {@link
245      * PlaybackStateCompat#ACTION_SKIP_TO_NEXT}, {@link
246      * PlaybackStateCompat#ACTION_SKIP_TO_PREVIOUS}.
247      *
248      * @param player The player connected to the media session.
249      * @return The bitmask of the supported media actions.
250      */
getSupportedQueueNavigatorActions(Player player)251     long getSupportedQueueNavigatorActions(Player player);
252     /**
253      * Called when the timeline of the player has changed.
254      *
255      * @param player The player connected to the media session.
256      */
onTimelineChanged(Player player)257     void onTimelineChanged(Player player);
258     /**
259      * Called when the current window index changed.
260      *
261      * @param player The player connected to the media session.
262      */
onCurrentWindowIndexChanged(Player player)263     void onCurrentWindowIndexChanged(Player player);
264     /**
265      * Gets the id of the currently active queue item, or {@link
266      * MediaSessionCompat.QueueItem#UNKNOWN_ID} if the active item is unknown.
267      *
268      * <p>To let the connector publish metadata for the active queue item, the queue item with the
269      * returned id must be available in the list of items returned by {@link
270      * MediaControllerCompat#getQueue()}.
271      *
272      * @param player The player connected to the media session.
273      * @return The id of the active queue item.
274      */
getActiveQueueItemId(@ullable Player player)275     long getActiveQueueItemId(@Nullable Player player);
276     /**
277      * See {@link MediaSessionCompat.Callback#onSkipToPrevious()}.
278      *
279      * @param player The player connected to the media session.
280      * @param controlDispatcher A {@link ControlDispatcher} that should be used for dispatching
281      *     changes to the player.
282      */
onSkipToPrevious(Player player, ControlDispatcher controlDispatcher)283     void onSkipToPrevious(Player player, ControlDispatcher controlDispatcher);
284     /**
285      * See {@link MediaSessionCompat.Callback#onSkipToQueueItem(long)}.
286      *
287      * @param player The player connected to the media session.
288      * @param controlDispatcher A {@link ControlDispatcher} that should be used for dispatching
289      *     changes to the player.
290      */
onSkipToQueueItem(Player player, ControlDispatcher controlDispatcher, long id)291     void onSkipToQueueItem(Player player, ControlDispatcher controlDispatcher, long id);
292     /**
293      * See {@link MediaSessionCompat.Callback#onSkipToNext()}.
294      *
295      * @param player The player connected to the media session.
296      * @param controlDispatcher A {@link ControlDispatcher} that should be used for dispatching
297      *     changes to the player.
298      */
onSkipToNext(Player player, ControlDispatcher controlDispatcher)299     void onSkipToNext(Player player, ControlDispatcher controlDispatcher);
300   }
301 
302   /** Handles media session queue edits. */
303   public interface QueueEditor extends CommandReceiver {
304 
305     /**
306      * See {@link MediaSessionCompat.Callback#onAddQueueItem(MediaDescriptionCompat description)}.
307      */
onAddQueueItem(Player player, MediaDescriptionCompat description)308     void onAddQueueItem(Player player, MediaDescriptionCompat description);
309     /**
310      * See {@link MediaSessionCompat.Callback#onAddQueueItem(MediaDescriptionCompat description, int
311      * index)}.
312      */
onAddQueueItem(Player player, MediaDescriptionCompat description, int index)313     void onAddQueueItem(Player player, MediaDescriptionCompat description, int index);
314     /**
315      * See {@link MediaSessionCompat.Callback#onRemoveQueueItem(MediaDescriptionCompat
316      * description)}.
317      */
onRemoveQueueItem(Player player, MediaDescriptionCompat description)318     void onRemoveQueueItem(Player player, MediaDescriptionCompat description);
319   }
320 
321   /** Callback receiving a user rating for the active media item. */
322   public interface RatingCallback extends CommandReceiver {
323 
324     /** See {@link MediaSessionCompat.Callback#onSetRating(RatingCompat)}. */
onSetRating(Player player, RatingCompat rating)325     void onSetRating(Player player, RatingCompat rating);
326 
327     /** See {@link MediaSessionCompat.Callback#onSetRating(RatingCompat, Bundle)}. */
onSetRating(Player player, RatingCompat rating, @Nullable Bundle extras)328     void onSetRating(Player player, RatingCompat rating, @Nullable Bundle extras);
329   }
330 
331   /** Handles requests for enabling or disabling captions. */
332   public interface CaptionCallback extends CommandReceiver {
333 
334     /** See {@link MediaSessionCompat.Callback#onSetCaptioningEnabled(boolean)}. */
onSetCaptioningEnabled(Player player, boolean enabled)335     void onSetCaptioningEnabled(Player player, boolean enabled);
336 
337     /**
338      * Returns whether the media currently being played has captions.
339      *
340      * <p>This method is called each time the media session playback state needs to be updated and
341      * published upon a player state change.
342      */
hasCaptions(Player player)343     boolean hasCaptions(Player player);
344   }
345 
346   /** Handles a media button event. */
347   public interface MediaButtonEventHandler {
348     /**
349      * See {@link MediaSessionCompat.Callback#onMediaButtonEvent(Intent)}.
350      *
351      * @param player The {@link Player}.
352      * @param controlDispatcher A {@link ControlDispatcher} that should be used for dispatching
353      *     changes to the player.
354      * @param mediaButtonEvent The {@link Intent}.
355      * @return True if the event was handled, false otherwise.
356      */
onMediaButtonEvent( Player player, ControlDispatcher controlDispatcher, Intent mediaButtonEvent)357     boolean onMediaButtonEvent(
358         Player player, ControlDispatcher controlDispatcher, Intent mediaButtonEvent);
359   }
360 
361   /**
362    * Provides a {@link PlaybackStateCompat.CustomAction} to be published and handles the action when
363    * sent by a media controller.
364    */
365   public interface CustomActionProvider {
366     /**
367      * Called when a custom action provided by this provider is sent to the media session.
368      *
369      * @param player The player connected to the media session.
370      * @param controlDispatcher A {@link ControlDispatcher} that should be used for dispatching
371      *     changes to the player.
372      * @param action The name of the action which was sent by a media controller.
373      * @param extras Optional extras sent by a media controller, may be null.
374      */
onCustomAction( Player player, ControlDispatcher controlDispatcher, String action, @Nullable Bundle extras)375     void onCustomAction(
376         Player player, ControlDispatcher controlDispatcher, String action, @Nullable Bundle extras);
377 
378     /**
379      * Returns a {@link PlaybackStateCompat.CustomAction} which will be published to the media
380      * session by the connector or {@code null} if this action should not be published at the given
381      * player state.
382      *
383      * @param player The player connected to the media session.
384      * @return The custom action to be included in the session playback state or {@code null}.
385      */
386     @Nullable
getCustomAction(Player player)387     PlaybackStateCompat.CustomAction getCustomAction(Player player);
388   }
389 
390   /** Provides a {@link MediaMetadataCompat} for a given player state. */
391   public interface MediaMetadataProvider {
392     /**
393      * Gets the {@link MediaMetadataCompat} to be published to the session.
394      *
395      * <p>An app may need to load metadata resources like artwork bitmaps asynchronously. In such a
396      * case the app should return a {@link MediaMetadataCompat} object that does not contain these
397      * resources as a placeholder. The app should start an asynchronous operation to download the
398      * bitmap and put it into a cache. Finally, the app should call {@link
399      * #invalidateMediaSessionMetadata()}. This causes this callback to be called again and the app
400      * can now return a {@link MediaMetadataCompat} object with all the resources included.
401      *
402      * @param player The player connected to the media session.
403      * @return The {@link MediaMetadataCompat} to be published to the session.
404      */
getMetadata(Player player)405     MediaMetadataCompat getMetadata(Player player);
406   }
407 
408   /** The wrapped {@link MediaSessionCompat}. */
409   public final MediaSessionCompat mediaSession;
410 
411   private final Looper looper;
412   private final ComponentListener componentListener;
413   private final ArrayList<CommandReceiver> commandReceivers;
414   private final ArrayList<CommandReceiver> customCommandReceivers;
415 
416   private ControlDispatcher controlDispatcher;
417   private CustomActionProvider[] customActionProviders;
418   private Map<String, CustomActionProvider> customActionMap;
419   @Nullable private MediaMetadataProvider mediaMetadataProvider;
420   @Nullable private Player player;
421   @Nullable private ErrorMessageProvider<? super ExoPlaybackException> errorMessageProvider;
422   @Nullable private Pair<Integer, CharSequence> customError;
423   @Nullable private Bundle customErrorExtras;
424   @Nullable private PlaybackPreparer playbackPreparer;
425   @Nullable private QueueNavigator queueNavigator;
426   @Nullable private QueueEditor queueEditor;
427   @Nullable private RatingCallback ratingCallback;
428   @Nullable private CaptionCallback captionCallback;
429   @Nullable private MediaButtonEventHandler mediaButtonEventHandler;
430 
431   private long enabledPlaybackActions;
432 
433   /**
434    * Creates an instance.
435    *
436    * @param mediaSession The {@link MediaSessionCompat} to connect to.
437    */
MediaSessionConnector(MediaSessionCompat mediaSession)438   public MediaSessionConnector(MediaSessionCompat mediaSession) {
439     this.mediaSession = mediaSession;
440     looper = Util.getLooper();
441     componentListener = new ComponentListener();
442     commandReceivers = new ArrayList<>();
443     customCommandReceivers = new ArrayList<>();
444     controlDispatcher = new DefaultControlDispatcher();
445     customActionProviders = new CustomActionProvider[0];
446     customActionMap = Collections.emptyMap();
447     mediaMetadataProvider =
448         new DefaultMediaMetadataProvider(
449             mediaSession.getController(), /* metadataExtrasPrefix= */ null);
450     enabledPlaybackActions = DEFAULT_PLAYBACK_ACTIONS;
451     mediaSession.setFlags(BASE_MEDIA_SESSION_FLAGS);
452     mediaSession.setCallback(componentListener, new Handler(looper));
453   }
454 
455   /**
456    * Sets the player to be connected to the media session. Must be called on the same thread that is
457    * used to access the player.
458    *
459    * @param player The player to be connected to the {@code MediaSession}, or {@code null} to
460    *     disconnect the current player.
461    */
setPlayer(@ullable Player player)462   public void setPlayer(@Nullable Player player) {
463     Assertions.checkArgument(player == null || player.getApplicationLooper() == looper);
464     if (this.player != null) {
465       this.player.removeListener(componentListener);
466     }
467     this.player = player;
468     if (player != null) {
469       player.addListener(componentListener);
470     }
471     invalidateMediaSessionPlaybackState();
472     invalidateMediaSessionMetadata();
473   }
474 
475   /**
476    * Sets the {@link PlaybackPreparer}.
477    *
478    * @param playbackPreparer The {@link PlaybackPreparer}.
479    */
setPlaybackPreparer(@ullable PlaybackPreparer playbackPreparer)480   public void setPlaybackPreparer(@Nullable PlaybackPreparer playbackPreparer) {
481     if (this.playbackPreparer != playbackPreparer) {
482       unregisterCommandReceiver(this.playbackPreparer);
483       this.playbackPreparer = playbackPreparer;
484       registerCommandReceiver(playbackPreparer);
485       invalidateMediaSessionPlaybackState();
486     }
487   }
488 
489   /**
490    * Sets the {@link ControlDispatcher}.
491    *
492    * @param controlDispatcher The {@link ControlDispatcher}.
493    */
setControlDispatcher(ControlDispatcher controlDispatcher)494   public void setControlDispatcher(ControlDispatcher controlDispatcher) {
495     if (this.controlDispatcher != controlDispatcher) {
496       this.controlDispatcher = controlDispatcher;
497       invalidateMediaSessionPlaybackState();
498     }
499   }
500 
501   /**
502    * Sets the {@link MediaButtonEventHandler}. Pass {@code null} if the media button event should be
503    * handled by {@link MediaSessionCompat.Callback#onMediaButtonEvent(Intent)}.
504    *
505    * <p>Please note that prior to API 21 MediaButton events are not delivered to the {@link
506    * MediaSessionCompat}. Instead they are delivered as key events (see <a
507    * href="https://developer.android.com/guide/topics/media-apps/mediabuttons">'Responding to media
508    * buttons'</a>). In an {@link android.app.Activity Activity}, media button events arrive at the
509    * {@link android.app.Activity#dispatchKeyEvent(KeyEvent)} method.
510    *
511    * <p>If you are running the player in a foreground service (prior to API 21), you can create an
512    * intent filter and handle the {@code android.intent.action.MEDIA_BUTTON} action yourself. See <a
513    * href="https://developer.android.com/reference/androidx/media/session/MediaButtonReceiver#service-handling-action_media_button">
514    * Service handling ACTION_MEDIA_BUTTON</a> for more information.
515    *
516    * @param mediaButtonEventHandler The {@link MediaButtonEventHandler}, or null to let the event be
517    *     handled by {@link MediaSessionCompat.Callback#onMediaButtonEvent(Intent)}.
518    */
setMediaButtonEventHandler( @ullable MediaButtonEventHandler mediaButtonEventHandler)519   public void setMediaButtonEventHandler(
520       @Nullable MediaButtonEventHandler mediaButtonEventHandler) {
521     this.mediaButtonEventHandler = mediaButtonEventHandler;
522   }
523 
524   /**
525    * Sets the enabled playback actions.
526    *
527    * @param enabledPlaybackActions The enabled playback actions.
528    */
setEnabledPlaybackActions(@laybackActions long enabledPlaybackActions)529   public void setEnabledPlaybackActions(@PlaybackActions long enabledPlaybackActions) {
530     enabledPlaybackActions &= ALL_PLAYBACK_ACTIONS;
531     if (this.enabledPlaybackActions != enabledPlaybackActions) {
532       this.enabledPlaybackActions = enabledPlaybackActions;
533       invalidateMediaSessionPlaybackState();
534     }
535   }
536 
537   /**
538    * @deprecated Use {@link #setControlDispatcher(ControlDispatcher)} with {@link
539    *     DefaultControlDispatcher#DefaultControlDispatcher(long, long)} instead.
540    */
541   @SuppressWarnings("deprecation")
542   @Deprecated
setRewindIncrementMs(int rewindMs)543   public void setRewindIncrementMs(int rewindMs) {
544     if (controlDispatcher instanceof DefaultControlDispatcher) {
545       ((DefaultControlDispatcher) controlDispatcher).setRewindIncrementMs(rewindMs);
546       invalidateMediaSessionPlaybackState();
547     }
548   }
549 
550   /**
551    * @deprecated Use {@link #setControlDispatcher(ControlDispatcher)} with {@link
552    *     DefaultControlDispatcher#DefaultControlDispatcher(long, long)} instead.
553    */
554   @SuppressWarnings("deprecation")
555   @Deprecated
setFastForwardIncrementMs(int fastForwardMs)556   public void setFastForwardIncrementMs(int fastForwardMs) {
557     if (controlDispatcher instanceof DefaultControlDispatcher) {
558       ((DefaultControlDispatcher) controlDispatcher).setFastForwardIncrementMs(fastForwardMs);
559       invalidateMediaSessionPlaybackState();
560     }
561   }
562 
563   /**
564    * Sets the optional {@link ErrorMessageProvider}.
565    *
566    * @param errorMessageProvider The error message provider.
567    */
setErrorMessageProvider( @ullable ErrorMessageProvider<? super ExoPlaybackException> errorMessageProvider)568   public void setErrorMessageProvider(
569       @Nullable ErrorMessageProvider<? super ExoPlaybackException> errorMessageProvider) {
570     if (this.errorMessageProvider != errorMessageProvider) {
571       this.errorMessageProvider = errorMessageProvider;
572       invalidateMediaSessionPlaybackState();
573     }
574   }
575 
576   /**
577    * Sets the {@link QueueNavigator} to handle queue navigation actions {@code ACTION_SKIP_TO_NEXT},
578    * {@code ACTION_SKIP_TO_PREVIOUS} and {@code ACTION_SKIP_TO_QUEUE_ITEM}.
579    *
580    * @param queueNavigator The queue navigator.
581    */
setQueueNavigator(@ullable QueueNavigator queueNavigator)582   public void setQueueNavigator(@Nullable QueueNavigator queueNavigator) {
583     if (this.queueNavigator != queueNavigator) {
584       unregisterCommandReceiver(this.queueNavigator);
585       this.queueNavigator = queueNavigator;
586       registerCommandReceiver(queueNavigator);
587     }
588   }
589 
590   /**
591    * Sets the {@link QueueEditor} to handle queue edits sent by the media controller.
592    *
593    * @param queueEditor The queue editor.
594    */
setQueueEditor(@ullable QueueEditor queueEditor)595   public void setQueueEditor(@Nullable QueueEditor queueEditor) {
596     if (this.queueEditor != queueEditor) {
597       unregisterCommandReceiver(this.queueEditor);
598       this.queueEditor = queueEditor;
599       registerCommandReceiver(queueEditor);
600       mediaSession.setFlags(
601           queueEditor == null ? BASE_MEDIA_SESSION_FLAGS : EDITOR_MEDIA_SESSION_FLAGS);
602     }
603   }
604 
605   /**
606    * Sets the {@link RatingCallback} to handle user ratings.
607    *
608    * @param ratingCallback The rating callback.
609    */
setRatingCallback(@ullable RatingCallback ratingCallback)610   public void setRatingCallback(@Nullable RatingCallback ratingCallback) {
611     if (this.ratingCallback != ratingCallback) {
612       unregisterCommandReceiver(this.ratingCallback);
613       this.ratingCallback = ratingCallback;
614       registerCommandReceiver(this.ratingCallback);
615     }
616   }
617 
618   /**
619    * Sets the {@link CaptionCallback} to handle requests to enable or disable captions.
620    *
621    * @param captionCallback The caption callback.
622    */
setCaptionCallback(@ullable CaptionCallback captionCallback)623   public void setCaptionCallback(@Nullable CaptionCallback captionCallback) {
624     if (this.captionCallback != captionCallback) {
625       unregisterCommandReceiver(this.captionCallback);
626       this.captionCallback = captionCallback;
627       registerCommandReceiver(this.captionCallback);
628     }
629   }
630 
631   /**
632    * Sets a custom error on the session.
633    *
634    * <p>This sets the error code via {@link PlaybackStateCompat.Builder#setErrorMessage(int,
635    * CharSequence)}. By default, the error code will be set to {@link
636    * PlaybackStateCompat#ERROR_CODE_APP_ERROR}.
637    *
638    * @param message The error string to report or {@code null} to clear the error.
639    */
setCustomErrorMessage(@ullable CharSequence message)640   public void setCustomErrorMessage(@Nullable CharSequence message) {
641     int code = (message == null) ? 0 : PlaybackStateCompat.ERROR_CODE_APP_ERROR;
642     setCustomErrorMessage(message, code);
643   }
644 
645   /**
646    * Sets a custom error on the session.
647    *
648    * @param message The error string to report or {@code null} to clear the error.
649    * @param code The error code to report. Ignored when {@code message} is {@code null}.
650    */
setCustomErrorMessage(@ullable CharSequence message, int code)651   public void setCustomErrorMessage(@Nullable CharSequence message, int code) {
652     setCustomErrorMessage(message, code, /* extras= */ null);
653   }
654 
655   /**
656    * Sets a custom error on the session.
657    *
658    * @param message The error string to report or {@code null} to clear the error.
659    * @param code The error code to report. Ignored when {@code message} is {@code null}.
660    * @param extras Extras to include in reported {@link PlaybackStateCompat}.
661    */
setCustomErrorMessage( @ullable CharSequence message, int code, @Nullable Bundle extras)662   public void setCustomErrorMessage(
663       @Nullable CharSequence message, int code, @Nullable Bundle extras) {
664     customError = (message == null) ? null : new Pair<>(code, message);
665     customErrorExtras = (message == null) ? null : extras;
666     invalidateMediaSessionPlaybackState();
667   }
668 
669   /**
670    * Sets custom action providers. The order of the {@link CustomActionProvider}s determines the
671    * order in which the actions are published.
672    *
673    * @param customActionProviders The custom action providers, or null to remove all existing custom
674    *     action providers.
675    */
setCustomActionProviders(@ullable CustomActionProvider... customActionProviders)676   public void setCustomActionProviders(@Nullable CustomActionProvider... customActionProviders) {
677     this.customActionProviders =
678         customActionProviders == null ? new CustomActionProvider[0] : customActionProviders;
679     invalidateMediaSessionPlaybackState();
680   }
681 
682   /**
683    * Sets a provider of metadata to be published to the media session. Pass {@code null} if no
684    * metadata should be published.
685    *
686    * @param mediaMetadataProvider The provider of metadata to publish, or {@code null} if no
687    *     metadata should be published.
688    */
setMediaMetadataProvider(@ullable MediaMetadataProvider mediaMetadataProvider)689   public void setMediaMetadataProvider(@Nullable MediaMetadataProvider mediaMetadataProvider) {
690     if (this.mediaMetadataProvider != mediaMetadataProvider) {
691       this.mediaMetadataProvider = mediaMetadataProvider;
692       invalidateMediaSessionMetadata();
693     }
694   }
695 
696   /**
697    * Updates the metadata of the media session.
698    *
699    * <p>Apps normally only need to call this method when the backing data for a given media item has
700    * changed and the metadata should be updated immediately.
701    *
702    * <p>The {@link MediaMetadataCompat} which is published to the session is obtained by calling
703    * {@link MediaMetadataProvider#getMetadata(Player)}.
704    */
invalidateMediaSessionMetadata()705   public final void invalidateMediaSessionMetadata() {
706     MediaMetadataCompat metadata =
707         mediaMetadataProvider != null && player != null
708             ? mediaMetadataProvider.getMetadata(player)
709             : METADATA_EMPTY;
710     mediaSession.setMetadata(metadata);
711   }
712 
713   /**
714    * Updates the playback state of the media session.
715    *
716    * <p>Apps normally only need to call this method when the custom actions provided by a {@link
717    * CustomActionProvider} changed and the playback state needs to be updated immediately.
718    */
invalidateMediaSessionPlaybackState()719   public final void invalidateMediaSessionPlaybackState() {
720     PlaybackStateCompat.Builder builder = new PlaybackStateCompat.Builder();
721     @Nullable Player player = this.player;
722     if (player == null) {
723       builder
724           .setActions(buildPrepareActions())
725           .setState(
726               PlaybackStateCompat.STATE_NONE,
727               /* position= */ 0,
728               /* playbackSpeed= */ 0,
729               /* updateTime= */ SystemClock.elapsedRealtime());
730 
731       mediaSession.setRepeatMode(PlaybackStateCompat.REPEAT_MODE_NONE);
732       mediaSession.setShuffleMode(PlaybackStateCompat.SHUFFLE_MODE_NONE);
733       mediaSession.setPlaybackState(builder.build());
734       return;
735     }
736 
737     Map<String, CustomActionProvider> currentActions = new HashMap<>();
738     for (CustomActionProvider customActionProvider : customActionProviders) {
739       @Nullable
740       PlaybackStateCompat.CustomAction customAction = customActionProvider.getCustomAction(player);
741       if (customAction != null) {
742         currentActions.put(customAction.getAction(), customActionProvider);
743         builder.addCustomAction(customAction);
744       }
745     }
746     customActionMap = Collections.unmodifiableMap(currentActions);
747 
748     Bundle extras = new Bundle();
749     @Nullable ExoPlaybackException playbackError = player.getPlayerError();
750     boolean reportError = playbackError != null || customError != null;
751     int sessionPlaybackState =
752         reportError
753             ? PlaybackStateCompat.STATE_ERROR
754             : getMediaSessionPlaybackState(player.getPlaybackState(), player.getPlayWhenReady());
755     if (customError != null) {
756       builder.setErrorMessage(customError.first, customError.second);
757       if (customErrorExtras != null) {
758         extras.putAll(customErrorExtras);
759       }
760     } else if (playbackError != null && errorMessageProvider != null) {
761       Pair<Integer, String> message = errorMessageProvider.getErrorMessage(playbackError);
762       builder.setErrorMessage(message.first, message.second);
763     }
764     long activeQueueItemId =
765         queueNavigator != null
766             ? queueNavigator.getActiveQueueItemId(player)
767             : MediaSessionCompat.QueueItem.UNKNOWN_ID;
768     float playbackSpeed = player.getPlaybackSpeed();
769     extras.putFloat(EXTRAS_SPEED, playbackSpeed);
770     float sessionPlaybackSpeed = player.isPlaying() ? playbackSpeed : 0f;
771     builder
772         .setActions(buildPrepareActions() | buildPlaybackActions(player))
773         .setActiveQueueItemId(activeQueueItemId)
774         .setBufferedPosition(player.getBufferedPosition())
775         .setState(
776             sessionPlaybackState,
777             player.getCurrentPosition(),
778             sessionPlaybackSpeed,
779             /* updateTime= */ SystemClock.elapsedRealtime())
780         .setExtras(extras);
781 
782     @Player.RepeatMode int repeatMode = player.getRepeatMode();
783     mediaSession.setRepeatMode(
784         repeatMode == Player.REPEAT_MODE_ONE
785             ? PlaybackStateCompat.REPEAT_MODE_ONE
786             : repeatMode == Player.REPEAT_MODE_ALL
787                 ? PlaybackStateCompat.REPEAT_MODE_ALL
788                 : PlaybackStateCompat.REPEAT_MODE_NONE);
789     mediaSession.setShuffleMode(
790         player.getShuffleModeEnabled()
791             ? PlaybackStateCompat.SHUFFLE_MODE_ALL
792             : PlaybackStateCompat.SHUFFLE_MODE_NONE);
793     mediaSession.setPlaybackState(builder.build());
794   }
795 
796   /**
797    * Updates the queue of the media session by calling {@link
798    * QueueNavigator#onTimelineChanged(Player)}.
799    *
800    * <p>Apps normally only need to call this method when the backing data for a given queue item has
801    * changed and the queue should be updated immediately.
802    */
invalidateMediaSessionQueue()803   public final void invalidateMediaSessionQueue() {
804     if (queueNavigator != null && player != null) {
805       queueNavigator.onTimelineChanged(player);
806     }
807   }
808 
809   /**
810    * Registers a custom command receiver for responding to commands delivered via {@link
811    * MediaSessionCompat.Callback#onCommand(String, Bundle, ResultReceiver)}.
812    *
813    * <p>Commands are only dispatched to this receiver when a player is connected.
814    *
815    * @param commandReceiver The command receiver to register.
816    */
registerCustomCommandReceiver(@ullable CommandReceiver commandReceiver)817   public void registerCustomCommandReceiver(@Nullable CommandReceiver commandReceiver) {
818     if (commandReceiver != null && !customCommandReceivers.contains(commandReceiver)) {
819       customCommandReceivers.add(commandReceiver);
820     }
821   }
822 
823   /**
824    * Unregisters a previously registered custom command receiver.
825    *
826    * @param commandReceiver The command receiver to unregister.
827    */
unregisterCustomCommandReceiver(@ullable CommandReceiver commandReceiver)828   public void unregisterCustomCommandReceiver(@Nullable CommandReceiver commandReceiver) {
829     if (commandReceiver != null) {
830       customCommandReceivers.remove(commandReceiver);
831     }
832   }
833 
registerCommandReceiver(@ullable CommandReceiver commandReceiver)834   private void registerCommandReceiver(@Nullable CommandReceiver commandReceiver) {
835     if (commandReceiver != null && !commandReceivers.contains(commandReceiver)) {
836       commandReceivers.add(commandReceiver);
837     }
838   }
839 
unregisterCommandReceiver(@ullable CommandReceiver commandReceiver)840   private void unregisterCommandReceiver(@Nullable CommandReceiver commandReceiver) {
841     if (commandReceiver != null) {
842       commandReceivers.remove(commandReceiver);
843     }
844   }
845 
buildPrepareActions()846   private long buildPrepareActions() {
847     return playbackPreparer == null
848         ? 0
849         : (PlaybackPreparer.ACTIONS & playbackPreparer.getSupportedPrepareActions());
850   }
851 
buildPlaybackActions(Player player)852   private long buildPlaybackActions(Player player) {
853     boolean enableSeeking = false;
854     boolean enableRewind = false;
855     boolean enableFastForward = false;
856     boolean enableSetRating = false;
857     boolean enableSetCaptioningEnabled = false;
858     Timeline timeline = player.getCurrentTimeline();
859     if (!timeline.isEmpty() && !player.isPlayingAd()) {
860       enableSeeking = player.isCurrentWindowSeekable();
861       enableRewind = enableSeeking && controlDispatcher.isRewindEnabled();
862       enableFastForward = enableSeeking && controlDispatcher.isFastForwardEnabled();
863       enableSetRating = ratingCallback != null;
864       enableSetCaptioningEnabled = captionCallback != null && captionCallback.hasCaptions(player);
865     }
866 
867     long playbackActions = BASE_PLAYBACK_ACTIONS;
868     if (enableSeeking) {
869       playbackActions |= PlaybackStateCompat.ACTION_SEEK_TO;
870     }
871     if (enableFastForward) {
872       playbackActions |= PlaybackStateCompat.ACTION_FAST_FORWARD;
873     }
874     if (enableRewind) {
875       playbackActions |= PlaybackStateCompat.ACTION_REWIND;
876     }
877     playbackActions &= enabledPlaybackActions;
878 
879     long actions = playbackActions;
880     if (queueNavigator != null) {
881       actions |=
882           (QueueNavigator.ACTIONS & queueNavigator.getSupportedQueueNavigatorActions(player));
883     }
884     if (enableSetRating) {
885       actions |= PlaybackStateCompat.ACTION_SET_RATING;
886     }
887     if (enableSetCaptioningEnabled) {
888       actions |= PlaybackStateCompat.ACTION_SET_CAPTIONING_ENABLED;
889     }
890     return actions;
891   }
892 
893   @EnsuresNonNullIf(result = true, expression = "player")
canDispatchPlaybackAction(long action)894   private boolean canDispatchPlaybackAction(long action) {
895     return player != null && (enabledPlaybackActions & action) != 0;
896   }
897 
898   @EnsuresNonNullIf(result = true, expression = "playbackPreparer")
canDispatchToPlaybackPreparer(long action)899   private boolean canDispatchToPlaybackPreparer(long action) {
900     return playbackPreparer != null
901         && (playbackPreparer.getSupportedPrepareActions() & action) != 0;
902   }
903 
904   @EnsuresNonNullIf(
905       result = true,
906       expression = {"player", "queueNavigator"})
canDispatchToQueueNavigator(long action)907   private boolean canDispatchToQueueNavigator(long action) {
908     return player != null
909         && queueNavigator != null
910         && (queueNavigator.getSupportedQueueNavigatorActions(player) & action) != 0;
911   }
912 
913   @EnsuresNonNullIf(
914       result = true,
915       expression = {"player", "ratingCallback"})
canDispatchSetRating()916   private boolean canDispatchSetRating() {
917     return player != null && ratingCallback != null;
918   }
919 
920   @EnsuresNonNullIf(
921       result = true,
922       expression = {"player", "captionCallback"})
canDispatchSetCaptioningEnabled()923   private boolean canDispatchSetCaptioningEnabled() {
924     return player != null && captionCallback != null;
925   }
926 
927   @EnsuresNonNullIf(
928       result = true,
929       expression = {"player", "queueEditor"})
canDispatchQueueEdit()930   private boolean canDispatchQueueEdit() {
931     return player != null && queueEditor != null;
932   }
933 
934   @EnsuresNonNullIf(
935       result = true,
936       expression = {"player", "mediaButtonEventHandler"})
canDispatchMediaButtonEvent()937   private boolean canDispatchMediaButtonEvent() {
938     return player != null && mediaButtonEventHandler != null;
939   }
940 
seekTo(Player player, int windowIndex, long positionMs)941   private void seekTo(Player player, int windowIndex, long positionMs) {
942     controlDispatcher.dispatchSeekTo(player, windowIndex, positionMs);
943   }
944 
getMediaSessionPlaybackState( @layer.State int exoPlayerPlaybackState, boolean playWhenReady)945   private static int getMediaSessionPlaybackState(
946       @Player.State int exoPlayerPlaybackState, boolean playWhenReady) {
947     switch (exoPlayerPlaybackState) {
948       case Player.STATE_BUFFERING:
949         return PlaybackStateCompat.STATE_BUFFERING;
950       case Player.STATE_READY:
951         return playWhenReady ? PlaybackStateCompat.STATE_PLAYING : PlaybackStateCompat.STATE_PAUSED;
952       case Player.STATE_ENDED:
953         return PlaybackStateCompat.STATE_STOPPED;
954       case Player.STATE_IDLE:
955       default:
956         return PlaybackStateCompat.STATE_NONE;
957     }
958   }
959 
960   /**
961    * Provides a default {@link MediaMetadataCompat} with properties and extras taken from the {@link
962    * MediaDescriptionCompat} of the {@link MediaSessionCompat.QueueItem} of the active queue item.
963    */
964   public static final class DefaultMediaMetadataProvider implements MediaMetadataProvider {
965 
966     private final MediaControllerCompat mediaController;
967     private final String metadataExtrasPrefix;
968 
969     /**
970      * Creates a new instance.
971      *
972      * @param mediaController The {@link MediaControllerCompat}.
973      * @param metadataExtrasPrefix A string to prefix extra keys which are propagated from the
974      *     active queue item to the session metadata.
975      */
DefaultMediaMetadataProvider( MediaControllerCompat mediaController, @Nullable String metadataExtrasPrefix)976     public DefaultMediaMetadataProvider(
977         MediaControllerCompat mediaController, @Nullable String metadataExtrasPrefix) {
978       this.mediaController = mediaController;
979       this.metadataExtrasPrefix = metadataExtrasPrefix != null ? metadataExtrasPrefix : "";
980     }
981 
982     @Override
getMetadata(Player player)983     public MediaMetadataCompat getMetadata(Player player) {
984       if (player.getCurrentTimeline().isEmpty()) {
985         return METADATA_EMPTY;
986       }
987       MediaMetadataCompat.Builder builder = new MediaMetadataCompat.Builder();
988       if (player.isPlayingAd()) {
989         builder.putLong(MediaMetadataCompat.METADATA_KEY_ADVERTISEMENT, 1);
990       }
991       builder.putLong(
992           MediaMetadataCompat.METADATA_KEY_DURATION,
993           player.isCurrentWindowDynamic() || player.getDuration() == C.TIME_UNSET
994               ? -1
995               : player.getDuration());
996       long activeQueueItemId = mediaController.getPlaybackState().getActiveQueueItemId();
997       if (activeQueueItemId != MediaSessionCompat.QueueItem.UNKNOWN_ID) {
998         List<MediaSessionCompat.QueueItem> queue = mediaController.getQueue();
999         for (int i = 0; queue != null && i < queue.size(); i++) {
1000           MediaSessionCompat.QueueItem queueItem = queue.get(i);
1001           if (queueItem.getQueueId() == activeQueueItemId) {
1002             MediaDescriptionCompat description = queueItem.getDescription();
1003             @Nullable Bundle extras = description.getExtras();
1004             if (extras != null) {
1005               for (String key : extras.keySet()) {
1006                 @Nullable Object value = extras.get(key);
1007                 if (value instanceof String) {
1008                   builder.putString(metadataExtrasPrefix + key, (String) value);
1009                 } else if (value instanceof CharSequence) {
1010                   builder.putText(metadataExtrasPrefix + key, (CharSequence) value);
1011                 } else if (value instanceof Long) {
1012                   builder.putLong(metadataExtrasPrefix + key, (Long) value);
1013                 } else if (value instanceof Integer) {
1014                   builder.putLong(metadataExtrasPrefix + key, (Integer) value);
1015                 } else if (value instanceof Bitmap) {
1016                   builder.putBitmap(metadataExtrasPrefix + key, (Bitmap) value);
1017                 } else if (value instanceof RatingCompat) {
1018                   builder.putRating(metadataExtrasPrefix + key, (RatingCompat) value);
1019                 }
1020               }
1021             }
1022             @Nullable CharSequence title = description.getTitle();
1023             if (title != null) {
1024               String titleString = String.valueOf(title);
1025               builder.putString(MediaMetadataCompat.METADATA_KEY_TITLE, titleString);
1026               builder.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_TITLE, titleString);
1027             }
1028             @Nullable CharSequence subtitle = description.getSubtitle();
1029             if (subtitle != null) {
1030               builder.putString(
1031                   MediaMetadataCompat.METADATA_KEY_DISPLAY_SUBTITLE, String.valueOf(subtitle));
1032             }
1033             @Nullable CharSequence displayDescription = description.getDescription();
1034             if (displayDescription != null) {
1035               builder.putString(
1036                   MediaMetadataCompat.METADATA_KEY_DISPLAY_DESCRIPTION,
1037                   String.valueOf(displayDescription));
1038             }
1039             @Nullable Bitmap iconBitmap = description.getIconBitmap();
1040             if (iconBitmap != null) {
1041               builder.putBitmap(MediaMetadataCompat.METADATA_KEY_DISPLAY_ICON, iconBitmap);
1042             }
1043             @Nullable Uri iconUri = description.getIconUri();
1044             if (iconUri != null) {
1045               builder.putString(
1046                   MediaMetadataCompat.METADATA_KEY_DISPLAY_ICON_URI, String.valueOf(iconUri));
1047             }
1048             @Nullable String mediaId = description.getMediaId();
1049             if (mediaId != null) {
1050               builder.putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, mediaId);
1051             }
1052             @Nullable Uri mediaUri = description.getMediaUri();
1053             if (mediaUri != null) {
1054               builder.putString(
1055                   MediaMetadataCompat.METADATA_KEY_MEDIA_URI, String.valueOf(mediaUri));
1056             }
1057             break;
1058           }
1059         }
1060       }
1061       return builder.build();
1062     }
1063   }
1064 
1065   private class ComponentListener extends MediaSessionCompat.Callback
1066       implements Player.EventListener {
1067 
1068     private int currentWindowIndex;
1069     private int currentWindowCount;
1070 
1071     // Player.EventListener implementation.
1072 
1073     @Override
onTimelineChanged(Timeline timeline, @Player.TimelineChangeReason int reason)1074     public void onTimelineChanged(Timeline timeline, @Player.TimelineChangeReason int reason) {
1075       Player player = Assertions.checkNotNull(MediaSessionConnector.this.player);
1076       int windowCount = player.getCurrentTimeline().getWindowCount();
1077       int windowIndex = player.getCurrentWindowIndex();
1078       if (queueNavigator != null) {
1079         queueNavigator.onTimelineChanged(player);
1080         invalidateMediaSessionPlaybackState();
1081       } else if (currentWindowCount != windowCount || currentWindowIndex != windowIndex) {
1082         // active queue item and queue navigation actions may need to be updated
1083         invalidateMediaSessionPlaybackState();
1084       }
1085       currentWindowCount = windowCount;
1086       currentWindowIndex = windowIndex;
1087       invalidateMediaSessionMetadata();
1088     }
1089 
1090     @Override
onPlaybackStateChanged(@layer.State int playbackState)1091     public void onPlaybackStateChanged(@Player.State int playbackState) {
1092       invalidateMediaSessionPlaybackState();
1093     }
1094 
1095     @Override
onPlayWhenReadyChanged( boolean playWhenReady, @Player.PlayWhenReadyChangeReason int reason)1096     public void onPlayWhenReadyChanged(
1097         boolean playWhenReady, @Player.PlayWhenReadyChangeReason int reason) {
1098       invalidateMediaSessionPlaybackState();
1099     }
1100 
1101     @Override
onIsPlayingChanged(boolean isPlaying)1102     public void onIsPlayingChanged(boolean isPlaying) {
1103       invalidateMediaSessionPlaybackState();
1104     }
1105 
1106     @Override
onRepeatModeChanged(@layer.RepeatMode int repeatMode)1107     public void onRepeatModeChanged(@Player.RepeatMode int repeatMode) {
1108       invalidateMediaSessionPlaybackState();
1109     }
1110 
1111     @Override
onShuffleModeEnabledChanged(boolean shuffleModeEnabled)1112     public void onShuffleModeEnabledChanged(boolean shuffleModeEnabled) {
1113       invalidateMediaSessionPlaybackState();
1114       invalidateMediaSessionQueue();
1115     }
1116 
1117     @Override
onPositionDiscontinuity(@layer.DiscontinuityReason int reason)1118     public void onPositionDiscontinuity(@Player.DiscontinuityReason int reason) {
1119       Player player = Assertions.checkNotNull(MediaSessionConnector.this.player);
1120       if (currentWindowIndex != player.getCurrentWindowIndex()) {
1121         if (queueNavigator != null) {
1122           queueNavigator.onCurrentWindowIndexChanged(player);
1123         }
1124         currentWindowIndex = player.getCurrentWindowIndex();
1125         // Update playback state after queueNavigator.onCurrentWindowIndexChanged has been called
1126         // and before updating metadata.
1127         invalidateMediaSessionPlaybackState();
1128         invalidateMediaSessionMetadata();
1129         return;
1130       }
1131       invalidateMediaSessionPlaybackState();
1132     }
1133 
1134     @Override
onPlaybackSpeedChanged(float playbackSpeed)1135     public void onPlaybackSpeedChanged(float playbackSpeed) {
1136       invalidateMediaSessionPlaybackState();
1137     }
1138 
1139     // MediaSessionCompat.Callback implementation.
1140 
1141     @Override
onPlay()1142     public void onPlay() {
1143       if (canDispatchPlaybackAction(PlaybackStateCompat.ACTION_PLAY)) {
1144         if (player.getPlaybackState() == Player.STATE_IDLE) {
1145           if (playbackPreparer != null) {
1146             playbackPreparer.onPrepare(/* playWhenReady= */ true);
1147           }
1148         } else if (player.getPlaybackState() == Player.STATE_ENDED) {
1149           seekTo(player, player.getCurrentWindowIndex(), C.TIME_UNSET);
1150         }
1151         controlDispatcher.dispatchSetPlayWhenReady(
1152             Assertions.checkNotNull(player), /* playWhenReady= */ true);
1153       }
1154     }
1155 
1156     @Override
onPause()1157     public void onPause() {
1158       if (canDispatchPlaybackAction(PlaybackStateCompat.ACTION_PAUSE)) {
1159         controlDispatcher.dispatchSetPlayWhenReady(player, /* playWhenReady= */ false);
1160       }
1161     }
1162 
1163     @Override
onSeekTo(long positionMs)1164     public void onSeekTo(long positionMs) {
1165       if (canDispatchPlaybackAction(PlaybackStateCompat.ACTION_SEEK_TO)) {
1166         seekTo(player, player.getCurrentWindowIndex(), positionMs);
1167       }
1168     }
1169 
1170     @Override
onFastForward()1171     public void onFastForward() {
1172       if (canDispatchPlaybackAction(PlaybackStateCompat.ACTION_FAST_FORWARD)) {
1173         controlDispatcher.dispatchFastForward(player);
1174       }
1175     }
1176 
1177     @Override
onRewind()1178     public void onRewind() {
1179       if (canDispatchPlaybackAction(PlaybackStateCompat.ACTION_REWIND)) {
1180         controlDispatcher.dispatchRewind(player);
1181       }
1182     }
1183 
1184     @Override
onStop()1185     public void onStop() {
1186       if (canDispatchPlaybackAction(PlaybackStateCompat.ACTION_STOP)) {
1187         controlDispatcher.dispatchStop(player, /* reset= */ true);
1188       }
1189     }
1190 
1191     @Override
onSetShuffleMode(@laybackStateCompat.ShuffleMode int shuffleMode)1192     public void onSetShuffleMode(@PlaybackStateCompat.ShuffleMode int shuffleMode) {
1193       if (canDispatchPlaybackAction(PlaybackStateCompat.ACTION_SET_SHUFFLE_MODE)) {
1194         boolean shuffleModeEnabled;
1195         switch (shuffleMode) {
1196           case PlaybackStateCompat.SHUFFLE_MODE_ALL:
1197           case PlaybackStateCompat.SHUFFLE_MODE_GROUP:
1198             shuffleModeEnabled = true;
1199             break;
1200           case PlaybackStateCompat.SHUFFLE_MODE_NONE:
1201           case PlaybackStateCompat.SHUFFLE_MODE_INVALID:
1202           default:
1203             shuffleModeEnabled = false;
1204             break;
1205         }
1206         controlDispatcher.dispatchSetShuffleModeEnabled(player, shuffleModeEnabled);
1207       }
1208     }
1209 
1210     @Override
onSetRepeatMode(@laybackStateCompat.RepeatMode int mediaSessionRepeatMode)1211     public void onSetRepeatMode(@PlaybackStateCompat.RepeatMode int mediaSessionRepeatMode) {
1212       if (canDispatchPlaybackAction(PlaybackStateCompat.ACTION_SET_REPEAT_MODE)) {
1213         @RepeatModeUtil.RepeatToggleModes int repeatMode;
1214         switch (mediaSessionRepeatMode) {
1215           case PlaybackStateCompat.REPEAT_MODE_ALL:
1216           case PlaybackStateCompat.REPEAT_MODE_GROUP:
1217             repeatMode = Player.REPEAT_MODE_ALL;
1218             break;
1219           case PlaybackStateCompat.REPEAT_MODE_ONE:
1220             repeatMode = Player.REPEAT_MODE_ONE;
1221             break;
1222           case PlaybackStateCompat.REPEAT_MODE_NONE:
1223           case PlaybackStateCompat.REPEAT_MODE_INVALID:
1224           default:
1225             repeatMode = Player.REPEAT_MODE_OFF;
1226             break;
1227         }
1228         controlDispatcher.dispatchSetRepeatMode(player, repeatMode);
1229       }
1230     }
1231 
1232     @Override
onSkipToNext()1233     public void onSkipToNext() {
1234       if (canDispatchToQueueNavigator(PlaybackStateCompat.ACTION_SKIP_TO_NEXT)) {
1235         queueNavigator.onSkipToNext(player, controlDispatcher);
1236       }
1237     }
1238 
1239     @Override
onSkipToPrevious()1240     public void onSkipToPrevious() {
1241       if (canDispatchToQueueNavigator(PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS)) {
1242         queueNavigator.onSkipToPrevious(player, controlDispatcher);
1243       }
1244     }
1245 
1246     @Override
onSkipToQueueItem(long id)1247     public void onSkipToQueueItem(long id) {
1248       if (canDispatchToQueueNavigator(PlaybackStateCompat.ACTION_SKIP_TO_QUEUE_ITEM)) {
1249         queueNavigator.onSkipToQueueItem(player, controlDispatcher, id);
1250       }
1251     }
1252 
1253     @Override
onCustomAction(String action, @Nullable Bundle extras)1254     public void onCustomAction(String action, @Nullable Bundle extras) {
1255       if (player != null && customActionMap.containsKey(action)) {
1256         customActionMap.get(action).onCustomAction(player, controlDispatcher, action, extras);
1257         invalidateMediaSessionPlaybackState();
1258       }
1259     }
1260 
1261     @Override
onCommand(String command, @Nullable Bundle extras, @Nullable ResultReceiver cb)1262     public void onCommand(String command, @Nullable Bundle extras, @Nullable ResultReceiver cb) {
1263       if (player != null) {
1264         for (int i = 0; i < commandReceivers.size(); i++) {
1265           if (commandReceivers.get(i).onCommand(player, controlDispatcher, command, extras, cb)) {
1266             return;
1267           }
1268         }
1269         for (int i = 0; i < customCommandReceivers.size(); i++) {
1270           if (customCommandReceivers
1271               .get(i)
1272               .onCommand(player, controlDispatcher, command, extras, cb)) {
1273             return;
1274           }
1275         }
1276       }
1277     }
1278 
1279     @Override
onPrepare()1280     public void onPrepare() {
1281       if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PREPARE)) {
1282         playbackPreparer.onPrepare(/* playWhenReady= */ false);
1283       }
1284     }
1285 
1286     @Override
onPrepareFromMediaId(String mediaId, @Nullable Bundle extras)1287     public void onPrepareFromMediaId(String mediaId, @Nullable Bundle extras) {
1288       if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PREPARE_FROM_MEDIA_ID)) {
1289         playbackPreparer.onPrepareFromMediaId(mediaId, /* playWhenReady= */ false, extras);
1290       }
1291     }
1292 
1293     @Override
onPrepareFromSearch(String query, @Nullable Bundle extras)1294     public void onPrepareFromSearch(String query, @Nullable Bundle extras) {
1295       if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PREPARE_FROM_SEARCH)) {
1296         playbackPreparer.onPrepareFromSearch(query, /* playWhenReady= */ false, extras);
1297       }
1298     }
1299 
1300     @Override
onPrepareFromUri(Uri uri, @Nullable Bundle extras)1301     public void onPrepareFromUri(Uri uri, @Nullable Bundle extras) {
1302       if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PREPARE_FROM_URI)) {
1303         playbackPreparer.onPrepareFromUri(uri, /* playWhenReady= */ false, extras);
1304       }
1305     }
1306 
1307     @Override
onPlayFromMediaId(String mediaId, @Nullable Bundle extras)1308     public void onPlayFromMediaId(String mediaId, @Nullable Bundle extras) {
1309       if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PLAY_FROM_MEDIA_ID)) {
1310         playbackPreparer.onPrepareFromMediaId(mediaId, /* playWhenReady= */ true, extras);
1311       }
1312     }
1313 
1314     @Override
onPlayFromSearch(String query, @Nullable Bundle extras)1315     public void onPlayFromSearch(String query, @Nullable Bundle extras) {
1316       if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PLAY_FROM_SEARCH)) {
1317         playbackPreparer.onPrepareFromSearch(query, /* playWhenReady= */ true, extras);
1318       }
1319     }
1320 
1321     @Override
onPlayFromUri(Uri uri, @Nullable Bundle extras)1322     public void onPlayFromUri(Uri uri, @Nullable Bundle extras) {
1323       if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PLAY_FROM_URI)) {
1324         playbackPreparer.onPrepareFromUri(uri, /* playWhenReady= */ true, extras);
1325       }
1326     }
1327 
1328     @Override
onSetRating(RatingCompat rating)1329     public void onSetRating(RatingCompat rating) {
1330       if (canDispatchSetRating()) {
1331         ratingCallback.onSetRating(player, rating);
1332       }
1333     }
1334 
1335     @Override
onSetRating(RatingCompat rating, @Nullable Bundle extras)1336     public void onSetRating(RatingCompat rating, @Nullable Bundle extras) {
1337       if (canDispatchSetRating()) {
1338         ratingCallback.onSetRating(player, rating, extras);
1339       }
1340     }
1341 
1342     @Override
onAddQueueItem(MediaDescriptionCompat description)1343     public void onAddQueueItem(MediaDescriptionCompat description) {
1344       if (canDispatchQueueEdit()) {
1345         queueEditor.onAddQueueItem(player, description);
1346       }
1347     }
1348 
1349     @Override
onAddQueueItem(MediaDescriptionCompat description, int index)1350     public void onAddQueueItem(MediaDescriptionCompat description, int index) {
1351       if (canDispatchQueueEdit()) {
1352         queueEditor.onAddQueueItem(player, description, index);
1353       }
1354     }
1355 
1356     @Override
onRemoveQueueItem(MediaDescriptionCompat description)1357     public void onRemoveQueueItem(MediaDescriptionCompat description) {
1358       if (canDispatchQueueEdit()) {
1359         queueEditor.onRemoveQueueItem(player, description);
1360       }
1361     }
1362 
1363     @Override
onSetCaptioningEnabled(boolean enabled)1364     public void onSetCaptioningEnabled(boolean enabled) {
1365       if (canDispatchSetCaptioningEnabled()) {
1366         captionCallback.onSetCaptioningEnabled(player, enabled);
1367       }
1368     }
1369 
1370     @Override
onMediaButtonEvent(Intent mediaButtonEvent)1371     public boolean onMediaButtonEvent(Intent mediaButtonEvent) {
1372       boolean isHandled =
1373           canDispatchMediaButtonEvent()
1374               && mediaButtonEventHandler.onMediaButtonEvent(
1375                   player, controlDispatcher, mediaButtonEvent);
1376       return isHandled || super.onMediaButtonEvent(mediaButtonEvent);
1377     }
1378   }
1379 }
1380