• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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} &lt; 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 | &lt;&lt; | play/pause | &gt;&gt; | 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