1 /* 2 * Copyright (C) 2018 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.ui; 17 18 import android.app.Notification; 19 import android.app.PendingIntent; 20 import android.content.BroadcastReceiver; 21 import android.content.Context; 22 import android.content.Intent; 23 import android.content.IntentFilter; 24 import android.graphics.Bitmap; 25 import android.graphics.Color; 26 import android.os.Handler; 27 import android.os.Looper; 28 import android.os.Message; 29 import android.support.v4.media.session.MediaSessionCompat; 30 import androidx.annotation.DrawableRes; 31 import androidx.annotation.IntDef; 32 import androidx.annotation.Nullable; 33 import androidx.annotation.StringRes; 34 import androidx.core.app.NotificationCompat; 35 import androidx.core.app.NotificationManagerCompat; 36 import androidx.media.app.NotificationCompat.MediaStyle; 37 import com.google.android.exoplayer2.C; 38 import com.google.android.exoplayer2.ControlDispatcher; 39 import com.google.android.exoplayer2.DefaultControlDispatcher; 40 import com.google.android.exoplayer2.PlaybackPreparer; 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.NotificationUtil; 45 import com.google.android.exoplayer2.util.Util; 46 import java.lang.annotation.Documented; 47 import java.lang.annotation.Retention; 48 import java.lang.annotation.RetentionPolicy; 49 import java.util.ArrayList; 50 import java.util.Arrays; 51 import java.util.Collections; 52 import java.util.HashMap; 53 import java.util.List; 54 import java.util.Map; 55 56 /** 57 * Starts, updates and cancels a media style notification reflecting the player state. The actions 58 * displayed and the drawables used can both be customized, as described below. 59 * 60 * <p>The notification is cancelled when {@code null} is passed to {@link #setPlayer(Player)} or 61 * when the notification is dismissed by the user. 62 * 63 * <p>If the player is released it must be removed from the manager by calling {@code 64 * setPlayer(null)}. 65 * 66 * <h3>Action customization</h3> 67 * 68 * Playback actions can be displayed or omitted as follows: 69 * 70 * <ul> 71 * <li><b>{@code useNavigationActions}</b> - Sets whether the previous and next actions are 72 * displayed. 73 * <ul> 74 * <li>Corresponding setter: {@link #setUseNavigationActions(boolean)} 75 * <li>Default: {@code true} 76 * </ul> 77 * <li><b>{@code useNavigationActionsInCompactView}</b> - Sets whether the previous and next 78 * actions are displayed in compact view (including the lock screen notification). 79 * <ul> 80 * <li>Corresponding setter: {@link #setUseNavigationActionsInCompactView(boolean)} 81 * <li>Default: {@code false} 82 * </ul> 83 * <li><b>{@code usePlayPauseActions}</b> - Sets whether the play and pause actions are displayed. 84 * <ul> 85 * <li>Corresponding setter: {@link #setUsePlayPauseActions(boolean)} 86 * <li>Default: {@code true} 87 * </ul> 88 * <li><b>{@code useStopAction}</b> - Sets whether the stop action is displayed. 89 * <ul> 90 * <li>Corresponding setter: {@link #setUseStopAction(boolean)} 91 * <li>Default: {@code false} 92 * </ul> 93 * <li><b>{@code rewindIncrementMs}</b> - Sets the rewind increment. If set to zero the rewind 94 * action is not displayed. 95 * <ul> 96 * <li>Corresponding setter: {@link #setControlDispatcher(ControlDispatcher)} 97 * <li>Default: {@link DefaultControlDispatcher#DEFAULT_REWIND_MS} (5000) 98 * </ul> 99 * <li><b>{@code fastForwardIncrementMs}</b> - Sets the fast forward increment. If set to zero the 100 * fast forward action is not displayed. 101 * <ul> 102 * <li>Corresponding setter: {@link #setControlDispatcher(ControlDispatcher)} 103 * <li>Default: {@link DefaultControlDispatcher#DEFAULT_FAST_FORWARD_MS} (15000) 104 * </ul> 105 * </ul> 106 * 107 * <h3>Overriding drawables</h3> 108 * 109 * The drawables used by PlayerNotificationManager can be overridden by drawables with the same 110 * names defined in your application. The drawables that can be overridden are: 111 * 112 * <ul> 113 * <li><b>{@code exo_notification_small_icon}</b> - The icon passed by default to {@link 114 * NotificationCompat.Builder#setSmallIcon(int)}. A different icon can also be specified 115 * programmatically by calling {@link #setSmallIcon(int)}. 116 * <li><b>{@code exo_notification_play}</b> - The play icon. 117 * <li><b>{@code exo_notification_pause}</b> - The pause icon. 118 * <li><b>{@code exo_notification_rewind}</b> - The rewind icon. 119 * <li><b>{@code exo_notification_fastforward}</b> - The fast forward icon. 120 * <li><b>{@code exo_notification_previous}</b> - The previous icon. 121 * <li><b>{@code exo_notification_next}</b> - The next icon. 122 * <li><b>{@code exo_notification_stop}</b> - The stop icon. 123 * </ul> 124 * 125 * Unlike the drawables above, the large icon (i.e. the icon passed to {@link 126 * NotificationCompat.Builder#setLargeIcon(Bitmap)} cannot be overridden in this way. Instead, the 127 * large icon is obtained from the {@link MediaDescriptionAdapter} injected when creating the 128 * PlayerNotificationManager. 129 */ 130 public class PlayerNotificationManager { 131 132 /** An adapter to provide content assets of the media currently playing. */ 133 public interface MediaDescriptionAdapter { 134 135 /** 136 * Gets the content title for the current media item. 137 * 138 * <p>See {@link NotificationCompat.Builder#setContentTitle(CharSequence)}. 139 * 140 * @param player The {@link Player} for which a notification is being built. 141 */ getCurrentContentTitle(Player player)142 CharSequence getCurrentContentTitle(Player player); 143 144 /** 145 * Creates a content intent for the current media item. 146 * 147 * <p>See {@link NotificationCompat.Builder#setContentIntent(PendingIntent)}. 148 * 149 * @param player The {@link Player} for which a notification is being built. 150 */ 151 @Nullable createCurrentContentIntent(Player player)152 PendingIntent createCurrentContentIntent(Player player); 153 154 /** 155 * Gets the content text for the current media item. 156 * 157 * <p>See {@link NotificationCompat.Builder#setContentText(CharSequence)}. 158 * 159 * @param player The {@link Player} for which a notification is being built. 160 */ 161 @Nullable getCurrentContentText(Player player)162 CharSequence getCurrentContentText(Player player); 163 164 /** 165 * Gets the content sub text for the current media item. 166 * 167 * <p>See {@link NotificationCompat.Builder#setSubText(CharSequence)}. 168 * 169 * @param player The {@link Player} for which a notification is being built. 170 */ 171 @Nullable getCurrentSubText(Player player)172 default CharSequence getCurrentSubText(Player player) { 173 return null; 174 } 175 176 /** 177 * Gets the large icon for the current media item. 178 * 179 * <p>When a bitmap needs to be loaded asynchronously, a placeholder bitmap (or null) should be 180 * returned. The actual bitmap should be passed to the {@link BitmapCallback} once it has been 181 * loaded. Because the adapter may be called multiple times for the same media item, bitmaps 182 * should be cached by the app and returned synchronously when possible. 183 * 184 * <p>See {@link NotificationCompat.Builder#setLargeIcon(Bitmap)}. 185 * 186 * @param player The {@link Player} for which a notification is being built. 187 * @param callback A {@link BitmapCallback} to provide a {@link Bitmap} asynchronously. 188 */ 189 @Nullable getCurrentLargeIcon(Player player, BitmapCallback callback)190 Bitmap getCurrentLargeIcon(Player player, BitmapCallback callback); 191 } 192 193 /** Defines and handles custom actions. */ 194 public interface CustomActionReceiver { 195 196 /** 197 * Gets the actions handled by this receiver. 198 * 199 * <p>If multiple {@link PlayerNotificationManager} instances are in use at the same time, the 200 * {@code instanceId} must be set as an intent extra with key {@link 201 * PlayerNotificationManager#EXTRA_INSTANCE_ID} to avoid sending the action to every custom 202 * action receiver. It's also necessary to ensure something is different about the actions. This 203 * may be any of the {@link Intent} attributes considered by {@link Intent#filterEquals}, or 204 * different request code integers when creating the {@link PendingIntent}s with {@link 205 * PendingIntent#getBroadcast}. The easiest approach is to use the {@code instanceId} as the 206 * request code. 207 * 208 * @param context The {@link Context}. 209 * @param instanceId The instance id of the {@link PlayerNotificationManager}. 210 * @return A map of custom actions. 211 */ createCustomActions(Context context, int instanceId)212 Map<String, NotificationCompat.Action> createCustomActions(Context context, int instanceId); 213 214 /** 215 * Gets the actions to be included in the notification given the current player state. 216 * 217 * @param player The {@link Player} for which a notification is being built. 218 * @return The actions to be included in the notification. 219 */ getCustomActions(Player player)220 List<String> getCustomActions(Player player); 221 222 /** 223 * Called when a custom action has been received. 224 * 225 * @param player The player. 226 * @param action The action from {@link Intent#getAction()}. 227 * @param intent The received {@link Intent}. 228 */ onCustomAction(Player player, String action, Intent intent)229 void onCustomAction(Player player, String action, Intent intent); 230 } 231 232 /** A listener for changes to the notification. */ 233 public interface NotificationListener { 234 235 /** 236 * Called after the notification has been started. 237 * 238 * @param notificationId The id with which the notification has been posted. 239 * @param notification The {@link Notification}. 240 * @deprecated Use {@link #onNotificationPosted(int, Notification, boolean)} instead. 241 */ 242 @Deprecated onNotificationStarted(int notificationId, Notification notification)243 default void onNotificationStarted(int notificationId, Notification notification) {} 244 245 /** 246 * Called after the notification has been cancelled. 247 * 248 * @param notificationId The id of the notification which has been cancelled. 249 * @deprecated Use {@link #onNotificationCancelled(int, boolean)}. 250 */ 251 @Deprecated onNotificationCancelled(int notificationId)252 default void onNotificationCancelled(int notificationId) {} 253 254 /** 255 * Called after the notification has been cancelled. 256 * 257 * @param notificationId The id of the notification which has been cancelled. 258 * @param dismissedByUser {@code true} if the notification is cancelled because the user 259 * dismissed the notification. 260 */ onNotificationCancelled(int notificationId, boolean dismissedByUser)261 default void onNotificationCancelled(int notificationId, boolean dismissedByUser) {} 262 263 /** 264 * Called each time after the notification has been posted. 265 * 266 * <p>For a service, the {@code ongoing} flag can be used as an indicator as to whether it 267 * should be in the foreground. 268 * 269 * @param notificationId The id of the notification which has been posted. 270 * @param notification The {@link Notification}. 271 * @param ongoing Whether the notification is ongoing. 272 */ onNotificationPosted( int notificationId, Notification notification, boolean ongoing)273 default void onNotificationPosted( 274 int notificationId, Notification notification, boolean ongoing) {} 275 } 276 277 /** Receives a {@link Bitmap}. */ 278 public final class BitmapCallback { 279 private final int notificationTag; 280 281 /** Create the receiver. */ BitmapCallback(int notificationTag)282 private BitmapCallback(int notificationTag) { 283 this.notificationTag = notificationTag; 284 } 285 286 /** 287 * Called when {@link Bitmap} is available. 288 * 289 * @param bitmap The bitmap to use as the large icon of the notification. 290 */ onBitmap(final Bitmap bitmap)291 public void onBitmap(final Bitmap bitmap) { 292 if (bitmap != null) { 293 postUpdateNotificationBitmap(bitmap, notificationTag); 294 } 295 } 296 } 297 298 /** The action which starts playback. */ 299 public static final String ACTION_PLAY = "com.google.android.exoplayer.play"; 300 /** The action which pauses playback. */ 301 public static final String ACTION_PAUSE = "com.google.android.exoplayer.pause"; 302 /** The action which skips to the previous window. */ 303 public static final String ACTION_PREVIOUS = "com.google.android.exoplayer.prev"; 304 /** The action which skips to the next window. */ 305 public static final String ACTION_NEXT = "com.google.android.exoplayer.next"; 306 /** The action which fast forwards. */ 307 public static final String ACTION_FAST_FORWARD = "com.google.android.exoplayer.ffwd"; 308 /** The action which rewinds. */ 309 public static final String ACTION_REWIND = "com.google.android.exoplayer.rewind"; 310 /** The action which stops playback. */ 311 public static final String ACTION_STOP = "com.google.android.exoplayer.stop"; 312 /** The extra key of the instance id of the player notification manager. */ 313 public static final String EXTRA_INSTANCE_ID = "INSTANCE_ID"; 314 /** 315 * The action which is executed when the notification is dismissed. It cancels the notification 316 * and calls {@link NotificationListener#onNotificationCancelled(int, boolean)}. 317 */ 318 private static final String ACTION_DISMISS = "com.google.android.exoplayer.dismiss"; 319 320 // Internal messages. 321 322 private static final int MSG_START_OR_UPDATE_NOTIFICATION = 0; 323 private static final int MSG_UPDATE_NOTIFICATION_BITMAP = 1; 324 325 /** 326 * Visibility of notification on the lock screen. One of {@link 327 * NotificationCompat#VISIBILITY_PRIVATE}, {@link NotificationCompat#VISIBILITY_PUBLIC} or {@link 328 * NotificationCompat#VISIBILITY_SECRET}. 329 */ 330 @Documented 331 @Retention(RetentionPolicy.SOURCE) 332 @IntDef({ 333 NotificationCompat.VISIBILITY_PRIVATE, 334 NotificationCompat.VISIBILITY_PUBLIC, 335 NotificationCompat.VISIBILITY_SECRET 336 }) 337 public @interface Visibility {} 338 339 /** 340 * Priority of the notification (required for API 25 and lower). One of {@link 341 * NotificationCompat#PRIORITY_DEFAULT}, {@link NotificationCompat#PRIORITY_MAX}, {@link 342 * NotificationCompat#PRIORITY_HIGH}, {@link NotificationCompat#PRIORITY_LOW }or {@link 343 * NotificationCompat#PRIORITY_MIN}. 344 */ 345 @Documented 346 @Retention(RetentionPolicy.SOURCE) 347 @IntDef({ 348 NotificationCompat.PRIORITY_DEFAULT, 349 NotificationCompat.PRIORITY_MAX, 350 NotificationCompat.PRIORITY_HIGH, 351 NotificationCompat.PRIORITY_LOW, 352 NotificationCompat.PRIORITY_MIN 353 }) 354 public @interface Priority {} 355 356 private static int instanceIdCounter; 357 358 private final Context context; 359 private final String channelId; 360 private final int notificationId; 361 private final MediaDescriptionAdapter mediaDescriptionAdapter; 362 @Nullable private final CustomActionReceiver customActionReceiver; 363 private final Handler mainHandler; 364 private final NotificationManagerCompat notificationManager; 365 private final IntentFilter intentFilter; 366 private final Player.EventListener playerListener; 367 private final NotificationBroadcastReceiver notificationBroadcastReceiver; 368 private final Map<String, NotificationCompat.Action> playbackActions; 369 private final Map<String, NotificationCompat.Action> customActions; 370 private final PendingIntent dismissPendingIntent; 371 private final int instanceId; 372 private final Timeline.Window window; 373 374 @Nullable private NotificationCompat.Builder builder; 375 @Nullable private List<NotificationCompat.Action> builderActions; 376 @Nullable private Player player; 377 @Nullable private PlaybackPreparer playbackPreparer; 378 private ControlDispatcher controlDispatcher; 379 private boolean isNotificationStarted; 380 private int currentNotificationTag; 381 @Nullable private NotificationListener notificationListener; 382 @Nullable private MediaSessionCompat.Token mediaSessionToken; 383 private boolean useNavigationActions; 384 private boolean useNavigationActionsInCompactView; 385 private boolean usePlayPauseActions; 386 private boolean useStopAction; 387 private int badgeIconType; 388 private boolean colorized; 389 private int defaults; 390 private int color; 391 @DrawableRes private int smallIconResourceId; 392 private int visibility; 393 @Priority private int priority; 394 private boolean useChronometer; 395 396 /** 397 * @deprecated Use {@link #createWithNotificationChannel(Context, String, int, int, int, 398 * MediaDescriptionAdapter)}. 399 */ 400 @Deprecated createWithNotificationChannel( Context context, String channelId, @StringRes int channelName, int notificationId, MediaDescriptionAdapter mediaDescriptionAdapter)401 public static PlayerNotificationManager createWithNotificationChannel( 402 Context context, 403 String channelId, 404 @StringRes int channelName, 405 int notificationId, 406 MediaDescriptionAdapter mediaDescriptionAdapter) { 407 return createWithNotificationChannel( 408 context, 409 channelId, 410 channelName, 411 /* channelDescription= */ 0, 412 notificationId, 413 mediaDescriptionAdapter); 414 } 415 416 /** 417 * Creates a notification manager and a low-priority notification channel with the specified 418 * {@code channelId} and {@code channelName}. 419 * 420 * <p>If the player notification manager is intended to be used within a foreground service, 421 * {@link #createWithNotificationChannel(Context, String, int, int, MediaDescriptionAdapter, 422 * NotificationListener)} should be used to which a {@link NotificationListener} can be passed. 423 * This way you'll receive the notification to put the service into the foreground by calling 424 * {@link android.app.Service#startForeground(int, Notification)}. 425 * 426 * @param context The {@link Context}. 427 * @param channelId The id of the notification channel. 428 * @param channelName A string resource identifier for the user visible name of the notification 429 * channel. The recommended maximum length is 40 characters. The string may be truncated if 430 * it's too long. 431 * @param channelDescription A string resource identifier for the user visible description of the 432 * notification channel, or 0 if no description is provided. The recommended maximum length is 433 * 300 characters. The value may be truncated if it is too long. 434 * @param notificationId The id of the notification. 435 * @param mediaDescriptionAdapter The {@link MediaDescriptionAdapter}. 436 */ createWithNotificationChannel( Context context, String channelId, @StringRes int channelName, @StringRes int channelDescription, int notificationId, MediaDescriptionAdapter mediaDescriptionAdapter)437 public static PlayerNotificationManager createWithNotificationChannel( 438 Context context, 439 String channelId, 440 @StringRes int channelName, 441 @StringRes int channelDescription, 442 int notificationId, 443 MediaDescriptionAdapter mediaDescriptionAdapter) { 444 NotificationUtil.createNotificationChannel( 445 context, channelId, channelName, channelDescription, NotificationUtil.IMPORTANCE_LOW); 446 return new PlayerNotificationManager( 447 context, channelId, notificationId, mediaDescriptionAdapter); 448 } 449 450 /** 451 * @deprecated Use {@link #createWithNotificationChannel(Context, String, int, int, int, 452 * MediaDescriptionAdapter, NotificationListener)}. 453 */ 454 @Deprecated createWithNotificationChannel( Context context, String channelId, @StringRes int channelName, int notificationId, MediaDescriptionAdapter mediaDescriptionAdapter, @Nullable NotificationListener notificationListener)455 public static PlayerNotificationManager createWithNotificationChannel( 456 Context context, 457 String channelId, 458 @StringRes int channelName, 459 int notificationId, 460 MediaDescriptionAdapter mediaDescriptionAdapter, 461 @Nullable NotificationListener notificationListener) { 462 return createWithNotificationChannel( 463 context, 464 channelId, 465 channelName, 466 /* channelDescription= */ 0, 467 notificationId, 468 mediaDescriptionAdapter, 469 notificationListener); 470 } 471 472 /** 473 * Creates a notification manager and a low-priority notification channel with the specified 474 * {@code channelId} and {@code channelName}. The {@link NotificationListener} passed as the last 475 * parameter will be notified when the notification is created and cancelled. 476 * 477 * @param context The {@link Context}. 478 * @param channelId The id of the notification channel. 479 * @param channelName A string resource identifier for the user visible name of the channel. The 480 * recommended maximum length is 40 characters. The string may be truncated if it's too long. 481 * @param channelDescription A string resource identifier for the user visible description of the 482 * channel, or 0 if no description is provided. 483 * @param notificationId The id of the notification. 484 * @param mediaDescriptionAdapter The {@link MediaDescriptionAdapter}. 485 * @param notificationListener The {@link NotificationListener}. 486 */ createWithNotificationChannel( Context context, String channelId, @StringRes int channelName, @StringRes int channelDescription, int notificationId, MediaDescriptionAdapter mediaDescriptionAdapter, @Nullable NotificationListener notificationListener)487 public static PlayerNotificationManager createWithNotificationChannel( 488 Context context, 489 String channelId, 490 @StringRes int channelName, 491 @StringRes int channelDescription, 492 int notificationId, 493 MediaDescriptionAdapter mediaDescriptionAdapter, 494 @Nullable NotificationListener notificationListener) { 495 NotificationUtil.createNotificationChannel( 496 context, channelId, channelName, channelDescription, NotificationUtil.IMPORTANCE_LOW); 497 return new PlayerNotificationManager( 498 context, channelId, notificationId, mediaDescriptionAdapter, notificationListener); 499 } 500 501 /** 502 * Creates a notification manager using the specified notification {@code channelId}. The caller 503 * is responsible for creating the notification channel. 504 * 505 * <p>When used within a service, consider using {@link #PlayerNotificationManager(Context, 506 * String, int, MediaDescriptionAdapter, NotificationListener)} to which a {@link 507 * NotificationListener} can be passed. 508 * 509 * @param context The {@link Context}. 510 * @param channelId The id of the notification channel. 511 * @param notificationId The id of the notification. 512 * @param mediaDescriptionAdapter The {@link MediaDescriptionAdapter}. 513 */ PlayerNotificationManager( Context context, String channelId, int notificationId, MediaDescriptionAdapter mediaDescriptionAdapter)514 public PlayerNotificationManager( 515 Context context, 516 String channelId, 517 int notificationId, 518 MediaDescriptionAdapter mediaDescriptionAdapter) { 519 this( 520 context, 521 channelId, 522 notificationId, 523 mediaDescriptionAdapter, 524 /* notificationListener= */ null, 525 /* customActionReceiver */ null); 526 } 527 528 /** 529 * Creates a notification manager using the specified notification {@code channelId} and {@link 530 * NotificationListener}. The caller is responsible for creating the notification channel. 531 * 532 * @param context The {@link Context}. 533 * @param channelId The id of the notification channel. 534 * @param notificationId The id of the notification. 535 * @param mediaDescriptionAdapter The {@link MediaDescriptionAdapter}. 536 * @param notificationListener The {@link NotificationListener}. 537 */ PlayerNotificationManager( Context context, String channelId, int notificationId, MediaDescriptionAdapter mediaDescriptionAdapter, @Nullable NotificationListener notificationListener)538 public PlayerNotificationManager( 539 Context context, 540 String channelId, 541 int notificationId, 542 MediaDescriptionAdapter mediaDescriptionAdapter, 543 @Nullable NotificationListener notificationListener) { 544 this( 545 context, 546 channelId, 547 notificationId, 548 mediaDescriptionAdapter, 549 notificationListener, 550 /* customActionReceiver*/ null); 551 } 552 553 /** 554 * Creates a notification manager using the specified notification {@code channelId} and {@link 555 * CustomActionReceiver}. The caller is responsible for creating the notification channel. 556 * 557 * <p>When used within a service, consider using {@link #PlayerNotificationManager(Context, 558 * String, int, MediaDescriptionAdapter, NotificationListener, CustomActionReceiver)} to which a 559 * {@link NotificationListener} can be passed. 560 * 561 * @param context The {@link Context}. 562 * @param channelId The id of the notification channel. 563 * @param notificationId The id of the notification. 564 * @param mediaDescriptionAdapter The {@link MediaDescriptionAdapter}. 565 * @param customActionReceiver The {@link CustomActionReceiver}. 566 */ PlayerNotificationManager( Context context, String channelId, int notificationId, MediaDescriptionAdapter mediaDescriptionAdapter, @Nullable CustomActionReceiver customActionReceiver)567 public PlayerNotificationManager( 568 Context context, 569 String channelId, 570 int notificationId, 571 MediaDescriptionAdapter mediaDescriptionAdapter, 572 @Nullable CustomActionReceiver customActionReceiver) { 573 this( 574 context, 575 channelId, 576 notificationId, 577 mediaDescriptionAdapter, 578 /* notificationListener */ null, 579 customActionReceiver); 580 } 581 582 /** 583 * Creates a notification manager using the specified notification {@code channelId}, {@link 584 * NotificationListener} and {@link CustomActionReceiver}. The caller is responsible for creating 585 * the notification channel. 586 * 587 * @param context The {@link Context}. 588 * @param channelId The id of the notification channel. 589 * @param notificationId The id of the notification. 590 * @param mediaDescriptionAdapter The {@link MediaDescriptionAdapter}. 591 * @param notificationListener The {@link NotificationListener}. 592 * @param customActionReceiver The {@link CustomActionReceiver}. 593 */ PlayerNotificationManager( Context context, String channelId, int notificationId, MediaDescriptionAdapter mediaDescriptionAdapter, @Nullable NotificationListener notificationListener, @Nullable CustomActionReceiver customActionReceiver)594 public PlayerNotificationManager( 595 Context context, 596 String channelId, 597 int notificationId, 598 MediaDescriptionAdapter mediaDescriptionAdapter, 599 @Nullable NotificationListener notificationListener, 600 @Nullable CustomActionReceiver customActionReceiver) { 601 context = context.getApplicationContext(); 602 this.context = context; 603 this.channelId = channelId; 604 this.notificationId = notificationId; 605 this.mediaDescriptionAdapter = mediaDescriptionAdapter; 606 this.notificationListener = notificationListener; 607 this.customActionReceiver = customActionReceiver; 608 controlDispatcher = new DefaultControlDispatcher(); 609 window = new Timeline.Window(); 610 instanceId = instanceIdCounter++; 611 //noinspection Convert2MethodRef 612 mainHandler = 613 Util.createHandler( 614 Looper.getMainLooper(), msg -> PlayerNotificationManager.this.handleMessage(msg)); 615 notificationManager = NotificationManagerCompat.from(context); 616 playerListener = new PlayerListener(); 617 notificationBroadcastReceiver = new NotificationBroadcastReceiver(); 618 intentFilter = new IntentFilter(); 619 useNavigationActions = true; 620 usePlayPauseActions = true; 621 colorized = true; 622 useChronometer = true; 623 color = Color.TRANSPARENT; 624 smallIconResourceId = R.drawable.exo_notification_small_icon; 625 defaults = 0; 626 priority = NotificationCompat.PRIORITY_LOW; 627 badgeIconType = NotificationCompat.BADGE_ICON_SMALL; 628 visibility = NotificationCompat.VISIBILITY_PUBLIC; 629 630 // initialize actions 631 playbackActions = createPlaybackActions(context, instanceId); 632 for (String action : playbackActions.keySet()) { 633 intentFilter.addAction(action); 634 } 635 customActions = 636 customActionReceiver != null 637 ? customActionReceiver.createCustomActions(context, instanceId) 638 : Collections.emptyMap(); 639 for (String action : customActions.keySet()) { 640 intentFilter.addAction(action); 641 } 642 dismissPendingIntent = createBroadcastIntent(ACTION_DISMISS, context, instanceId); 643 intentFilter.addAction(ACTION_DISMISS); 644 } 645 646 /** 647 * Sets the {@link Player}. 648 * 649 * <p>Setting the player starts a notification immediately unless the player is in {@link 650 * Player#STATE_IDLE}, in which case the notification is started as soon as the player transitions 651 * away from being idle. 652 * 653 * <p>If the player is released it must be removed from the manager by calling {@code 654 * setPlayer(null)}. This will cancel the notification. 655 * 656 * @param player The {@link Player} to use, or {@code null} to remove the current player. Only 657 * players which are accessed on the main thread are supported ({@code 658 * player.getApplicationLooper() == Looper.getMainLooper()}). 659 */ setPlayer(@ullable Player player)660 public final void setPlayer(@Nullable Player player) { 661 Assertions.checkState(Looper.myLooper() == Looper.getMainLooper()); 662 Assertions.checkArgument( 663 player == null || player.getApplicationLooper() == Looper.getMainLooper()); 664 if (this.player == player) { 665 return; 666 } 667 if (this.player != null) { 668 this.player.removeListener(playerListener); 669 if (player == null) { 670 stopNotification(/* dismissedByUser= */ false); 671 } 672 } 673 this.player = player; 674 if (player != null) { 675 player.addListener(playerListener); 676 postStartOrUpdateNotification(); 677 } 678 } 679 680 /** 681 * Sets the {@link PlaybackPreparer}. 682 * 683 * @param playbackPreparer The {@link PlaybackPreparer}. 684 */ setPlaybackPreparer(@ullable PlaybackPreparer playbackPreparer)685 public void setPlaybackPreparer(@Nullable PlaybackPreparer playbackPreparer) { 686 this.playbackPreparer = playbackPreparer; 687 } 688 689 /** 690 * Sets the {@link ControlDispatcher}. 691 * 692 * @param controlDispatcher The {@link ControlDispatcher}. 693 */ setControlDispatcher(ControlDispatcher controlDispatcher)694 public final void setControlDispatcher(ControlDispatcher controlDispatcher) { 695 if (this.controlDispatcher != controlDispatcher) { 696 this.controlDispatcher = controlDispatcher; 697 invalidate(); 698 } 699 } 700 701 /** 702 * Sets the {@link NotificationListener}. 703 * 704 * <p>Please note that you should call this method before you call {@link #setPlayer(Player)} or 705 * you may not get the {@link NotificationListener#onNotificationStarted(int, Notification)} 706 * called on your listener. 707 * 708 * @param notificationListener The {@link NotificationListener}. 709 * @deprecated Pass the notification listener to the constructor instead. 710 */ 711 @Deprecated setNotificationListener(NotificationListener notificationListener)712 public final void setNotificationListener(NotificationListener notificationListener) { 713 this.notificationListener = notificationListener; 714 } 715 716 /** 717 * @deprecated Use {@link #setControlDispatcher(ControlDispatcher)} with {@link 718 * DefaultControlDispatcher#DefaultControlDispatcher(long, long)}. 719 */ 720 @SuppressWarnings("deprecation") 721 @Deprecated setFastForwardIncrementMs(long fastForwardMs)722 public final void setFastForwardIncrementMs(long fastForwardMs) { 723 if (controlDispatcher instanceof DefaultControlDispatcher) { 724 ((DefaultControlDispatcher) controlDispatcher).setFastForwardIncrementMs(fastForwardMs); 725 invalidate(); 726 } 727 } 728 729 /** 730 * @deprecated Use {@link #setControlDispatcher(ControlDispatcher)} with {@link 731 * DefaultControlDispatcher#DefaultControlDispatcher(long, long)}. 732 */ 733 @SuppressWarnings("deprecation") 734 @Deprecated setRewindIncrementMs(long rewindMs)735 public final void setRewindIncrementMs(long rewindMs) { 736 if (controlDispatcher instanceof DefaultControlDispatcher) { 737 ((DefaultControlDispatcher) controlDispatcher).setRewindIncrementMs(rewindMs); 738 invalidate(); 739 } 740 } 741 742 /** 743 * Sets whether the navigation actions should be used. 744 * 745 * @param useNavigationActions Whether to use navigation actions or not. 746 */ setUseNavigationActions(boolean useNavigationActions)747 public final void setUseNavigationActions(boolean useNavigationActions) { 748 if (this.useNavigationActions != useNavigationActions) { 749 this.useNavigationActions = useNavigationActions; 750 invalidate(); 751 } 752 } 753 754 /** 755 * Sets whether navigation actions should be displayed in compact view. 756 * 757 * <p>If {@link #useNavigationActions} is set to {@code false} navigation actions are displayed 758 * neither in compact nor in full view mode of the notification. 759 * 760 * @param useNavigationActionsInCompactView Whether the navigation actions should be displayed in 761 * compact view. 762 */ setUseNavigationActionsInCompactView( boolean useNavigationActionsInCompactView)763 public final void setUseNavigationActionsInCompactView( 764 boolean useNavigationActionsInCompactView) { 765 if (this.useNavigationActionsInCompactView != useNavigationActionsInCompactView) { 766 this.useNavigationActionsInCompactView = useNavigationActionsInCompactView; 767 invalidate(); 768 } 769 } 770 771 /** 772 * Sets whether the play and pause actions should be used. 773 * 774 * @param usePlayPauseActions Whether to use play and pause actions. 775 */ setUsePlayPauseActions(boolean usePlayPauseActions)776 public final void setUsePlayPauseActions(boolean usePlayPauseActions) { 777 if (this.usePlayPauseActions != usePlayPauseActions) { 778 this.usePlayPauseActions = usePlayPauseActions; 779 invalidate(); 780 } 781 } 782 783 /** 784 * Sets whether the stop action should be used. 785 * 786 * @param useStopAction Whether to use the stop action. 787 */ setUseStopAction(boolean useStopAction)788 public final void setUseStopAction(boolean useStopAction) { 789 if (this.useStopAction == useStopAction) { 790 return; 791 } 792 this.useStopAction = useStopAction; 793 invalidate(); 794 } 795 796 /** 797 * Sets the {@link MediaSessionCompat.Token}. 798 * 799 * @param token The {@link MediaSessionCompat.Token}. 800 */ setMediaSessionToken(MediaSessionCompat.Token token)801 public final void setMediaSessionToken(MediaSessionCompat.Token token) { 802 if (!Util.areEqual(this.mediaSessionToken, token)) { 803 mediaSessionToken = token; 804 invalidate(); 805 } 806 } 807 808 /** 809 * Sets the badge icon type of the notification. 810 * 811 * <p>See {@link NotificationCompat.Builder#setBadgeIconType(int)}. 812 * 813 * @param badgeIconType The badge icon type. 814 */ setBadgeIconType(@otificationCompat.BadgeIconType int badgeIconType)815 public final void setBadgeIconType(@NotificationCompat.BadgeIconType int badgeIconType) { 816 if (this.badgeIconType == badgeIconType) { 817 return; 818 } 819 switch (badgeIconType) { 820 case NotificationCompat.BADGE_ICON_NONE: 821 case NotificationCompat.BADGE_ICON_SMALL: 822 case NotificationCompat.BADGE_ICON_LARGE: 823 this.badgeIconType = badgeIconType; 824 break; 825 default: 826 throw new IllegalArgumentException(); 827 } 828 invalidate(); 829 } 830 831 /** 832 * Sets whether the notification should be colorized. When set, the color set with {@link 833 * #setColor(int)} will be used as the background color for the notification. 834 * 835 * <p>See {@link NotificationCompat.Builder#setColorized(boolean)}. 836 * 837 * @param colorized Whether to colorize the notification. 838 */ setColorized(boolean colorized)839 public final void setColorized(boolean colorized) { 840 if (this.colorized != colorized) { 841 this.colorized = colorized; 842 invalidate(); 843 } 844 } 845 846 /** 847 * Sets the defaults. 848 * 849 * <p>See {@link NotificationCompat.Builder#setDefaults(int)}. 850 * 851 * @param defaults The default notification options. 852 */ setDefaults(int defaults)853 public final void setDefaults(int defaults) { 854 if (this.defaults != defaults) { 855 this.defaults = defaults; 856 invalidate(); 857 } 858 } 859 860 /** 861 * Sets the accent color of the notification. 862 * 863 * <p>See {@link NotificationCompat.Builder#setColor(int)}. 864 * 865 * @param color The color, in ARGB integer form like the constants in {@link Color}. 866 */ setColor(int color)867 public final void setColor(int color) { 868 if (this.color != color) { 869 this.color = color; 870 invalidate(); 871 } 872 } 873 874 /** 875 * Sets the priority of the notification required for API 25 and lower. 876 * 877 * <p>See {@link NotificationCompat.Builder#setPriority(int)}. 878 * 879 * @param priority The priority which can be one of {@link NotificationCompat#PRIORITY_DEFAULT}, 880 * {@link NotificationCompat#PRIORITY_MAX}, {@link NotificationCompat#PRIORITY_HIGH}, {@link 881 * NotificationCompat#PRIORITY_LOW} or {@link NotificationCompat#PRIORITY_MIN}. If not set 882 * {@link NotificationCompat#PRIORITY_LOW} is used by default. 883 */ setPriority(@riority int priority)884 public final void setPriority(@Priority int priority) { 885 if (this.priority == priority) { 886 return; 887 } 888 switch (priority) { 889 case NotificationCompat.PRIORITY_DEFAULT: 890 case NotificationCompat.PRIORITY_MAX: 891 case NotificationCompat.PRIORITY_HIGH: 892 case NotificationCompat.PRIORITY_LOW: 893 case NotificationCompat.PRIORITY_MIN: 894 this.priority = priority; 895 break; 896 default: 897 throw new IllegalArgumentException(); 898 } 899 invalidate(); 900 } 901 902 /** 903 * Sets the small icon of the notification which is also shown in the system status bar. 904 * 905 * <p>See {@link NotificationCompat.Builder#setSmallIcon(int)}. 906 * 907 * @param smallIconResourceId The resource id of the small icon. 908 */ setSmallIcon(@rawableRes int smallIconResourceId)909 public final void setSmallIcon(@DrawableRes int smallIconResourceId) { 910 if (this.smallIconResourceId != smallIconResourceId) { 911 this.smallIconResourceId = smallIconResourceId; 912 invalidate(); 913 } 914 } 915 916 /** 917 * Sets whether the elapsed time of the media playback should be displayed. 918 * 919 * <p>Note that this setting only works if all of the following are true: 920 * 921 * <ul> 922 * <li>The media is {@link Player#isPlaying() actively playing}. 923 * <li>The media is not {@link Player#isCurrentWindowDynamic() dynamically changing its 924 * duration} (like for example a live stream). 925 * <li>The media is not {@link Player#isPlayingAd() interrupted by an ad}. 926 * <li>The media is played at {@link Player#getPlaybackParameters() regular speed}. 927 * <li>The device is running at least API 21 (Lollipop). 928 * </ul> 929 * 930 * <p>See {@link NotificationCompat.Builder#setUsesChronometer(boolean)}. 931 * 932 * @param useChronometer Whether to use chronometer. 933 */ setUseChronometer(boolean useChronometer)934 public final void setUseChronometer(boolean useChronometer) { 935 if (this.useChronometer != useChronometer) { 936 this.useChronometer = useChronometer; 937 invalidate(); 938 } 939 } 940 941 /** 942 * Sets the visibility of the notification which determines whether and how the notification is 943 * shown when the device is in lock screen mode. 944 * 945 * <p>See {@link NotificationCompat.Builder#setVisibility(int)}. 946 * 947 * @param visibility The visibility which must be one of {@link 948 * NotificationCompat#VISIBILITY_PUBLIC}, {@link NotificationCompat#VISIBILITY_PRIVATE} or 949 * {@link NotificationCompat#VISIBILITY_SECRET}. 950 */ setVisibility(@isibility int visibility)951 public final void setVisibility(@Visibility int visibility) { 952 if (this.visibility == visibility) { 953 return; 954 } 955 switch (visibility) { 956 case NotificationCompat.VISIBILITY_PRIVATE: 957 case NotificationCompat.VISIBILITY_PUBLIC: 958 case NotificationCompat.VISIBILITY_SECRET: 959 this.visibility = visibility; 960 break; 961 default: 962 throw new IllegalStateException(); 963 } 964 invalidate(); 965 } 966 967 /** Forces an update of the notification if already started. */ invalidate()968 public void invalidate() { 969 if (isNotificationStarted) { 970 postStartOrUpdateNotification(); 971 } 972 } 973 startOrUpdateNotification(Player player, @Nullable Bitmap bitmap)974 private void startOrUpdateNotification(Player player, @Nullable Bitmap bitmap) { 975 boolean ongoing = getOngoing(player); 976 builder = createNotification(player, builder, ongoing, bitmap); 977 if (builder == null) { 978 stopNotification(/* dismissedByUser= */ false); 979 return; 980 } 981 Notification notification = builder.build(); 982 notificationManager.notify(notificationId, notification); 983 if (!isNotificationStarted) { 984 isNotificationStarted = true; 985 context.registerReceiver(notificationBroadcastReceiver, intentFilter); 986 if (notificationListener != null) { 987 notificationListener.onNotificationStarted(notificationId, notification); 988 } 989 } 990 @Nullable NotificationListener listener = notificationListener; 991 if (listener != null) { 992 listener.onNotificationPosted(notificationId, notification, ongoing); 993 } 994 } 995 stopNotification(boolean dismissedByUser)996 private void stopNotification(boolean dismissedByUser) { 997 if (isNotificationStarted) { 998 isNotificationStarted = false; 999 mainHandler.removeMessages(MSG_START_OR_UPDATE_NOTIFICATION); 1000 notificationManager.cancel(notificationId); 1001 context.unregisterReceiver(notificationBroadcastReceiver); 1002 if (notificationListener != null) { 1003 notificationListener.onNotificationCancelled(notificationId, dismissedByUser); 1004 notificationListener.onNotificationCancelled(notificationId); 1005 } 1006 } 1007 } 1008 1009 /** 1010 * Creates the notification given the current player state. 1011 * 1012 * @param player The player for which state to build a notification. 1013 * @param builder The builder used to build the last notification, or {@code null}. Re-using the 1014 * builder when possible can prevent notification flicker when {@code Util#SDK_INT} < 21. 1015 * @param ongoing Whether the notification should be ongoing. 1016 * @param largeIcon The large icon to be used. 1017 * @return The {@link NotificationCompat.Builder} on which to call {@link 1018 * NotificationCompat.Builder#build()} to obtain the notification, or {@code null} if no 1019 * notification should be displayed. 1020 */ 1021 @Nullable createNotification( Player player, @Nullable NotificationCompat.Builder builder, boolean ongoing, @Nullable Bitmap largeIcon)1022 protected NotificationCompat.Builder createNotification( 1023 Player player, 1024 @Nullable NotificationCompat.Builder builder, 1025 boolean ongoing, 1026 @Nullable Bitmap largeIcon) { 1027 if (player.getPlaybackState() == Player.STATE_IDLE 1028 && (player.getCurrentTimeline().isEmpty() || playbackPreparer == null)) { 1029 builderActions = null; 1030 return null; 1031 } 1032 1033 List<String> actionNames = getActions(player); 1034 List<NotificationCompat.Action> actions = new ArrayList<>(actionNames.size()); 1035 for (int i = 0; i < actionNames.size(); i++) { 1036 String actionName = actionNames.get(i); 1037 @Nullable 1038 NotificationCompat.Action action = 1039 playbackActions.containsKey(actionName) 1040 ? playbackActions.get(actionName) 1041 : customActions.get(actionName); 1042 if (action != null) { 1043 actions.add(action); 1044 } 1045 } 1046 1047 if (builder == null || !actions.equals(builderActions)) { 1048 builder = new NotificationCompat.Builder(context, channelId); 1049 builderActions = actions; 1050 for (int i = 0; i < actions.size(); i++) { 1051 builder.addAction(actions.get(i)); 1052 } 1053 } 1054 1055 MediaStyle mediaStyle = new MediaStyle(); 1056 if (mediaSessionToken != null) { 1057 mediaStyle.setMediaSession(mediaSessionToken); 1058 } 1059 mediaStyle.setShowActionsInCompactView(getActionIndicesForCompactView(actionNames, player)); 1060 // Configure dismiss action prior to API 21 ('x' button). 1061 mediaStyle.setShowCancelButton(!ongoing); 1062 mediaStyle.setCancelButtonIntent(dismissPendingIntent); 1063 builder.setStyle(mediaStyle); 1064 1065 // Set intent which is sent if the user selects 'clear all' 1066 builder.setDeleteIntent(dismissPendingIntent); 1067 1068 // Set notification properties from getters. 1069 builder 1070 .setBadgeIconType(badgeIconType) 1071 .setOngoing(ongoing) 1072 .setColor(color) 1073 .setColorized(colorized) 1074 .setSmallIcon(smallIconResourceId) 1075 .setVisibility(visibility) 1076 .setPriority(priority) 1077 .setDefaults(defaults); 1078 1079 // Changing "showWhen" causes notification flicker if SDK_INT < 21. 1080 if (Util.SDK_INT >= 21 1081 && useChronometer 1082 && player.isPlaying() 1083 && !player.isPlayingAd() 1084 && !player.isCurrentWindowDynamic() 1085 && player.getPlaybackParameters().speed == 1f) { 1086 builder 1087 .setWhen(System.currentTimeMillis() - player.getContentPosition()) 1088 .setShowWhen(true) 1089 .setUsesChronometer(true); 1090 } else { 1091 builder.setShowWhen(false).setUsesChronometer(false); 1092 } 1093 1094 // Set media specific notification properties from MediaDescriptionAdapter. 1095 builder.setContentTitle(mediaDescriptionAdapter.getCurrentContentTitle(player)); 1096 builder.setContentText(mediaDescriptionAdapter.getCurrentContentText(player)); 1097 builder.setSubText(mediaDescriptionAdapter.getCurrentSubText(player)); 1098 if (largeIcon == null) { 1099 largeIcon = 1100 mediaDescriptionAdapter.getCurrentLargeIcon( 1101 player, new BitmapCallback(++currentNotificationTag)); 1102 } 1103 setLargeIcon(builder, largeIcon); 1104 builder.setContentIntent(mediaDescriptionAdapter.createCurrentContentIntent(player)); 1105 1106 return builder; 1107 } 1108 1109 /** 1110 * Gets the names and order of the actions to be included in the notification at the current 1111 * player state. 1112 * 1113 * <p>The playback and custom actions are combined and placed in the following order if not 1114 * omitted: 1115 * 1116 * <pre> 1117 * +------------------------------------------------------------------------+ 1118 * | prev | << | play/pause | >> | next | custom actions | stop | 1119 * +------------------------------------------------------------------------+ 1120 * </pre> 1121 * 1122 * <p>This method can be safely overridden. However, the names must be of the playback actions 1123 * {@link #ACTION_PAUSE}, {@link #ACTION_PLAY}, {@link #ACTION_FAST_FORWARD}, {@link 1124 * #ACTION_REWIND}, {@link #ACTION_NEXT} or {@link #ACTION_PREVIOUS}, or a key contained in the 1125 * map returned by {@link CustomActionReceiver#createCustomActions(Context, int)}. Otherwise the 1126 * action name is ignored. 1127 */ getActions(Player player)1128 protected List<String> getActions(Player player) { 1129 boolean enablePrevious = false; 1130 boolean enableRewind = false; 1131 boolean enableFastForward = false; 1132 boolean enableNext = false; 1133 Timeline timeline = player.getCurrentTimeline(); 1134 if (!timeline.isEmpty() && !player.isPlayingAd()) { 1135 timeline.getWindow(player.getCurrentWindowIndex(), window); 1136 enablePrevious = window.isSeekable || !window.isDynamic || player.hasPrevious(); 1137 enableRewind = controlDispatcher.isRewindEnabled(); 1138 enableFastForward = controlDispatcher.isFastForwardEnabled(); 1139 enableNext = window.isDynamic || player.hasNext(); 1140 } 1141 1142 List<String> stringActions = new ArrayList<>(); 1143 if (useNavigationActions && enablePrevious) { 1144 stringActions.add(ACTION_PREVIOUS); 1145 } 1146 if (enableRewind) { 1147 stringActions.add(ACTION_REWIND); 1148 } 1149 if (usePlayPauseActions) { 1150 if (shouldShowPauseButton(player)) { 1151 stringActions.add(ACTION_PAUSE); 1152 } else { 1153 stringActions.add(ACTION_PLAY); 1154 } 1155 } 1156 if (enableFastForward) { 1157 stringActions.add(ACTION_FAST_FORWARD); 1158 } 1159 if (useNavigationActions && enableNext) { 1160 stringActions.add(ACTION_NEXT); 1161 } 1162 if (customActionReceiver != null) { 1163 stringActions.addAll(customActionReceiver.getCustomActions(player)); 1164 } 1165 if (useStopAction) { 1166 stringActions.add(ACTION_STOP); 1167 } 1168 return stringActions; 1169 } 1170 1171 /** 1172 * Gets an array with the indices of the buttons to be shown in compact mode. 1173 * 1174 * <p>This method can be overridden. The indices must refer to the list of actions passed as the 1175 * first parameter. 1176 * 1177 * @param actionNames The names of the actions included in the notification. 1178 * @param player The player for which a notification is being built. 1179 */ 1180 @SuppressWarnings("unused") getActionIndicesForCompactView(List<String> actionNames, Player player)1181 protected int[] getActionIndicesForCompactView(List<String> actionNames, Player player) { 1182 int pauseActionIndex = actionNames.indexOf(ACTION_PAUSE); 1183 int playActionIndex = actionNames.indexOf(ACTION_PLAY); 1184 int skipPreviousActionIndex = 1185 useNavigationActionsInCompactView ? actionNames.indexOf(ACTION_PREVIOUS) : -1; 1186 int skipNextActionIndex = 1187 useNavigationActionsInCompactView ? actionNames.indexOf(ACTION_NEXT) : -1; 1188 1189 int[] actionIndices = new int[3]; 1190 int actionCounter = 0; 1191 if (skipPreviousActionIndex != -1) { 1192 actionIndices[actionCounter++] = skipPreviousActionIndex; 1193 } 1194 boolean shouldShowPauseButton = shouldShowPauseButton(player); 1195 if (pauseActionIndex != -1 && shouldShowPauseButton) { 1196 actionIndices[actionCounter++] = pauseActionIndex; 1197 } else if (playActionIndex != -1 && !shouldShowPauseButton) { 1198 actionIndices[actionCounter++] = playActionIndex; 1199 } 1200 if (skipNextActionIndex != -1) { 1201 actionIndices[actionCounter++] = skipNextActionIndex; 1202 } 1203 return Arrays.copyOf(actionIndices, actionCounter); 1204 } 1205 1206 /** Returns whether the generated notification should be ongoing. */ getOngoing(Player player)1207 protected boolean getOngoing(Player player) { 1208 int playbackState = player.getPlaybackState(); 1209 return (playbackState == Player.STATE_BUFFERING || playbackState == Player.STATE_READY) 1210 && player.getPlayWhenReady(); 1211 } 1212 shouldShowPauseButton(Player player)1213 private boolean shouldShowPauseButton(Player player) { 1214 return player.getPlaybackState() != Player.STATE_ENDED 1215 && player.getPlaybackState() != Player.STATE_IDLE 1216 && player.getPlayWhenReady(); 1217 } 1218 postStartOrUpdateNotification()1219 private void postStartOrUpdateNotification() { 1220 if (!mainHandler.hasMessages(MSG_START_OR_UPDATE_NOTIFICATION)) { 1221 mainHandler.sendEmptyMessage(MSG_START_OR_UPDATE_NOTIFICATION); 1222 } 1223 } 1224 postUpdateNotificationBitmap(Bitmap bitmap, int notificationTag)1225 private void postUpdateNotificationBitmap(Bitmap bitmap, int notificationTag) { 1226 mainHandler 1227 .obtainMessage( 1228 MSG_UPDATE_NOTIFICATION_BITMAP, notificationTag, C.INDEX_UNSET /* ignored */, bitmap) 1229 .sendToTarget(); 1230 } 1231 handleMessage(Message msg)1232 private boolean handleMessage(Message msg) { 1233 switch (msg.what) { 1234 case MSG_START_OR_UPDATE_NOTIFICATION: 1235 if (player != null) { 1236 startOrUpdateNotification(player, /* bitmap= */ null); 1237 } 1238 break; 1239 case MSG_UPDATE_NOTIFICATION_BITMAP: 1240 if (player != null && isNotificationStarted && currentNotificationTag == msg.arg1) { 1241 startOrUpdateNotification(player, (Bitmap) msg.obj); 1242 } 1243 break; 1244 default: 1245 return false; 1246 } 1247 return true; 1248 } 1249 createPlaybackActions( Context context, int instanceId)1250 private static Map<String, NotificationCompat.Action> createPlaybackActions( 1251 Context context, int instanceId) { 1252 Map<String, NotificationCompat.Action> actions = new HashMap<>(); 1253 actions.put( 1254 ACTION_PLAY, 1255 new NotificationCompat.Action( 1256 R.drawable.exo_notification_play, 1257 context.getString(R.string.exo_controls_play_description), 1258 createBroadcastIntent(ACTION_PLAY, context, instanceId))); 1259 actions.put( 1260 ACTION_PAUSE, 1261 new NotificationCompat.Action( 1262 R.drawable.exo_notification_pause, 1263 context.getString(R.string.exo_controls_pause_description), 1264 createBroadcastIntent(ACTION_PAUSE, context, instanceId))); 1265 actions.put( 1266 ACTION_STOP, 1267 new NotificationCompat.Action( 1268 R.drawable.exo_notification_stop, 1269 context.getString(R.string.exo_controls_stop_description), 1270 createBroadcastIntent(ACTION_STOP, context, instanceId))); 1271 actions.put( 1272 ACTION_REWIND, 1273 new NotificationCompat.Action( 1274 R.drawable.exo_notification_rewind, 1275 context.getString(R.string.exo_controls_rewind_description), 1276 createBroadcastIntent(ACTION_REWIND, context, instanceId))); 1277 actions.put( 1278 ACTION_FAST_FORWARD, 1279 new NotificationCompat.Action( 1280 R.drawable.exo_notification_fastforward, 1281 context.getString(R.string.exo_controls_fastforward_description), 1282 createBroadcastIntent(ACTION_FAST_FORWARD, context, instanceId))); 1283 actions.put( 1284 ACTION_PREVIOUS, 1285 new NotificationCompat.Action( 1286 R.drawable.exo_notification_previous, 1287 context.getString(R.string.exo_controls_previous_description), 1288 createBroadcastIntent(ACTION_PREVIOUS, context, instanceId))); 1289 actions.put( 1290 ACTION_NEXT, 1291 new NotificationCompat.Action( 1292 R.drawable.exo_notification_next, 1293 context.getString(R.string.exo_controls_next_description), 1294 createBroadcastIntent(ACTION_NEXT, context, instanceId))); 1295 return actions; 1296 } 1297 createBroadcastIntent( String action, Context context, int instanceId)1298 private static PendingIntent createBroadcastIntent( 1299 String action, Context context, int instanceId) { 1300 Intent intent = new Intent(action).setPackage(context.getPackageName()); 1301 intent.putExtra(EXTRA_INSTANCE_ID, instanceId); 1302 return PendingIntent.getBroadcast( 1303 context, instanceId, intent, PendingIntent.FLAG_UPDATE_CURRENT); 1304 } 1305 1306 @SuppressWarnings("nullness:argument.type.incompatible") setLargeIcon(NotificationCompat.Builder builder, @Nullable Bitmap largeIcon)1307 private static void setLargeIcon(NotificationCompat.Builder builder, @Nullable Bitmap largeIcon) { 1308 builder.setLargeIcon(largeIcon); 1309 } 1310 1311 private class PlayerListener implements Player.EventListener { 1312 1313 @Override onPlaybackStateChanged(@layer.State int playbackState)1314 public void onPlaybackStateChanged(@Player.State int playbackState) { 1315 postStartOrUpdateNotification(); 1316 } 1317 1318 @Override onPlayWhenReadyChanged( boolean playWhenReady, @Player.PlayWhenReadyChangeReason int reason)1319 public void onPlayWhenReadyChanged( 1320 boolean playWhenReady, @Player.PlayWhenReadyChangeReason int reason) { 1321 postStartOrUpdateNotification(); 1322 } 1323 1324 @Override onIsPlayingChanged(boolean isPlaying)1325 public void onIsPlayingChanged(boolean isPlaying) { 1326 postStartOrUpdateNotification(); 1327 } 1328 1329 @Override onTimelineChanged(Timeline timeline, int reason)1330 public void onTimelineChanged(Timeline timeline, int reason) { 1331 postStartOrUpdateNotification(); 1332 } 1333 1334 @Override onPlaybackSpeedChanged(float playbackSpeed)1335 public void onPlaybackSpeedChanged(float playbackSpeed) { 1336 postStartOrUpdateNotification(); 1337 } 1338 1339 @Override onPositionDiscontinuity(int reason)1340 public void onPositionDiscontinuity(int reason) { 1341 postStartOrUpdateNotification(); 1342 } 1343 1344 @Override onRepeatModeChanged(@layer.RepeatMode int repeatMode)1345 public void onRepeatModeChanged(@Player.RepeatMode int repeatMode) { 1346 postStartOrUpdateNotification(); 1347 } 1348 1349 @Override onShuffleModeEnabledChanged(boolean shuffleModeEnabled)1350 public void onShuffleModeEnabledChanged(boolean shuffleModeEnabled) { 1351 postStartOrUpdateNotification(); 1352 } 1353 } 1354 1355 private class NotificationBroadcastReceiver extends BroadcastReceiver { 1356 1357 @Override onReceive(Context context, Intent intent)1358 public void onReceive(Context context, Intent intent) { 1359 Player player = PlayerNotificationManager.this.player; 1360 if (player == null 1361 || !isNotificationStarted 1362 || intent.getIntExtra(EXTRA_INSTANCE_ID, instanceId) != instanceId) { 1363 return; 1364 } 1365 String action = intent.getAction(); 1366 if (ACTION_PLAY.equals(action)) { 1367 if (player.getPlaybackState() == Player.STATE_IDLE) { 1368 if (playbackPreparer != null) { 1369 playbackPreparer.preparePlayback(); 1370 } 1371 } else if (player.getPlaybackState() == Player.STATE_ENDED) { 1372 controlDispatcher.dispatchSeekTo(player, player.getCurrentWindowIndex(), C.TIME_UNSET); 1373 } 1374 controlDispatcher.dispatchSetPlayWhenReady(player, /* playWhenReady= */ true); 1375 } else if (ACTION_PAUSE.equals(action)) { 1376 controlDispatcher.dispatchSetPlayWhenReady(player, /* playWhenReady= */ false); 1377 } else if (ACTION_PREVIOUS.equals(action)) { 1378 controlDispatcher.dispatchPrevious(player); 1379 } else if (ACTION_REWIND.equals(action)) { 1380 controlDispatcher.dispatchRewind(player); 1381 } else if (ACTION_FAST_FORWARD.equals(action)) { 1382 controlDispatcher.dispatchFastForward(player); 1383 } else if (ACTION_NEXT.equals(action)) { 1384 controlDispatcher.dispatchNext(player); 1385 } else if (ACTION_STOP.equals(action)) { 1386 controlDispatcher.dispatchStop(player, /* reset= */ true); 1387 } else if (ACTION_DISMISS.equals(action)) { 1388 stopNotification(/* dismissedByUser= */ true); 1389 } else if (action != null 1390 && customActionReceiver != null 1391 && customActions.containsKey(action)) { 1392 customActionReceiver.onCustomAction(player, action, intent); 1393 } 1394 } 1395 } 1396 } 1397