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