1 /*
2  * Copyright (C) 2016 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.annotation.SuppressLint;
19 import android.content.Context;
20 import android.content.res.Resources;
21 import android.content.res.TypedArray;
22 import android.graphics.Bitmap;
23 import android.graphics.BitmapFactory;
24 import android.graphics.Matrix;
25 import android.graphics.RectF;
26 import android.graphics.drawable.BitmapDrawable;
27 import android.graphics.drawable.Drawable;
28 import android.os.Looper;
29 import android.util.AttributeSet;
30 import android.view.KeyEvent;
31 import android.view.LayoutInflater;
32 import android.view.MotionEvent;
33 import android.view.SurfaceView;
34 import android.view.TextureView;
35 import android.view.View;
36 import android.view.ViewGroup;
37 import android.widget.FrameLayout;
38 import android.widget.ImageView;
39 import android.widget.TextView;
40 import androidx.annotation.IntDef;
41 import androidx.annotation.Nullable;
42 import androidx.annotation.RequiresApi;
43 import androidx.core.content.ContextCompat;
44 import com.google.android.exoplayer2.C;
45 import com.google.android.exoplayer2.ControlDispatcher;
46 import com.google.android.exoplayer2.DefaultControlDispatcher;
47 import com.google.android.exoplayer2.ExoPlaybackException;
48 import com.google.android.exoplayer2.PlaybackPreparer;
49 import com.google.android.exoplayer2.Player;
50 import com.google.android.exoplayer2.Player.DiscontinuityReason;
51 import com.google.android.exoplayer2.metadata.Metadata;
52 import com.google.android.exoplayer2.metadata.flac.PictureFrame;
53 import com.google.android.exoplayer2.metadata.id3.ApicFrame;
54 import com.google.android.exoplayer2.source.TrackGroupArray;
55 import com.google.android.exoplayer2.source.ads.AdsLoader;
56 import com.google.android.exoplayer2.text.Cue;
57 import com.google.android.exoplayer2.text.TextOutput;
58 import com.google.android.exoplayer2.trackselection.TrackSelection;
59 import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
60 import com.google.android.exoplayer2.ui.AspectRatioFrameLayout.ResizeMode;
61 import com.google.android.exoplayer2.ui.spherical.SingleTapListener;
62 import com.google.android.exoplayer2.ui.spherical.SphericalGLSurfaceView;
63 import com.google.android.exoplayer2.util.Assertions;
64 import com.google.android.exoplayer2.util.ErrorMessageProvider;
65 import com.google.android.exoplayer2.util.RepeatModeUtil;
66 import com.google.android.exoplayer2.util.Util;
67 import com.google.android.exoplayer2.video.VideoDecoderGLSurfaceView;
68 import com.google.android.exoplayer2.video.VideoListener;
69 import java.lang.annotation.Documented;
70 import java.lang.annotation.Retention;
71 import java.lang.annotation.RetentionPolicy;
72 import java.util.ArrayList;
73 import java.util.List;
74 import org.checkerframework.checker.nullness.qual.EnsuresNonNullIf;
75 import org.checkerframework.checker.nullness.qual.RequiresNonNull;
76 
77 /**
78  * A high level view for {@link Player} media playbacks. It displays video, subtitles and album art
79  * during playback, and displays playback controls using a {@link PlayerControlView}.
80  *
81  * <p>A PlayerView can be customized by setting attributes (or calling corresponding methods),
82  * overriding drawables, overriding the view's layout file, or by specifying a custom view layout
83  * file.
84  *
85  * <h3>Attributes</h3>
86  *
87  * The following attributes can be set on a PlayerView when used in a layout XML file:
88  *
89  * <ul>
90  *   <li><b>{@code use_artwork}</b> - Whether artwork is used if available in audio streams.
91  *       <ul>
92  *         <li>Corresponding method: {@link #setUseArtwork(boolean)}
93  *         <li>Default: {@code true}
94  *       </ul>
95  *   <li><b>{@code default_artwork}</b> - Default artwork to use if no artwork available in audio
96  *       streams.
97  *       <ul>
98  *         <li>Corresponding method: {@link #setDefaultArtwork(Drawable)}
99  *         <li>Default: {@code null}
100  *       </ul>
101  *   <li><b>{@code use_controller}</b> - Whether the playback controls can be shown.
102  *       <ul>
103  *         <li>Corresponding method: {@link #setUseController(boolean)}
104  *         <li>Default: {@code true}
105  *       </ul>
106  *   <li><b>{@code hide_on_touch}</b> - Whether the playback controls are hidden by touch events.
107  *       <ul>
108  *         <li>Corresponding method: {@link #setControllerHideOnTouch(boolean)}
109  *         <li>Default: {@code true}
110  *       </ul>
111  *   <li><b>{@code auto_show}</b> - Whether the playback controls are automatically shown when
112  *       playback starts, pauses, ends, or fails. If set to false, the playback controls can be
113  *       manually operated with {@link #showController()} and {@link #hideController()}.
114  *       <ul>
115  *         <li>Corresponding method: {@link #setControllerAutoShow(boolean)}
116  *         <li>Default: {@code true}
117  *       </ul>
118  *   <li><b>{@code hide_during_ads}</b> - Whether the playback controls are hidden during ads.
119  *       Controls are always shown during ads if they are enabled and the player is paused.
120  *       <ul>
121  *         <li>Corresponding method: {@link #setControllerHideDuringAds(boolean)}
122  *         <li>Default: {@code true}
123  *       </ul>
124  *   <li><b>{@code show_buffering}</b> - Whether the buffering spinner is displayed when the player
125  *       is buffering. Valid values are {@code never}, {@code when_playing} and {@code always}.
126  *       <ul>
127  *         <li>Corresponding method: {@link #setShowBuffering(int)}
128  *         <li>Default: {@code never}
129  *       </ul>
130  *   <li><b>{@code resize_mode}</b> - Controls how video and album art is resized within the view.
131  *       Valid values are {@code fit}, {@code fixed_width}, {@code fixed_height} and {@code fill}.
132  *       <ul>
133  *         <li>Corresponding method: {@link #setResizeMode(int)}
134  *         <li>Default: {@code fit}
135  *       </ul>
136  *   <li><b>{@code surface_type}</b> - The type of surface view used for video playbacks. Valid
137  *       values are {@code surface_view}, {@code texture_view}, {@code spherical_gl_surface_view},
138  *       {@code video_decoder_gl_surface_view} and {@code none}. Using {@code none} is recommended
139  *       for audio only applications, since creating the surface can be expensive. Using {@code
140  *       surface_view} is recommended for video applications. Note, TextureView can only be used in
141  *       a hardware accelerated window. When rendered in software, TextureView will draw nothing.
142  *       <ul>
143  *         <li>Corresponding method: None
144  *         <li>Default: {@code surface_view}
145  *       </ul>
146  *   <li><b>{@code use_sensor_rotation}</b> - Whether to use the orientation sensor for rotation
147  *       during spherical playbacks (if available).
148  *       <ul>
149  *         <li>Corresponding method: {@link #setUseSensorRotation(boolean)}
150  *         <li>Default: {@code true}
151  *       </ul>
152  *   <li><b>{@code shutter_background_color}</b> - The background color of the {@code exo_shutter}
153  *       view.
154  *       <ul>
155  *         <li>Corresponding method: {@link #setShutterBackgroundColor(int)}
156  *         <li>Default: {@code unset}
157  *       </ul>
158  *   <li><b>{@code keep_content_on_player_reset}</b> - Whether the currently displayed video frame
159  *       or media artwork is kept visible when the player is reset.
160  *       <ul>
161  *         <li>Corresponding method: {@link #setKeepContentOnPlayerReset(boolean)}
162  *         <li>Default: {@code false}
163  *       </ul>
164  *   <li><b>{@code player_layout_id}</b> - Specifies the id of the layout to be inflated. See below
165  *       for more details.
166  *       <ul>
167  *         <li>Corresponding method: None
168  *         <li>Default: {@code R.layout.exo_player_view}
169  *       </ul>
170  *   <li><b>{@code controller_layout_id}</b> - Specifies the id of the layout resource to be
171  *       inflated by the child {@link PlayerControlView}. See below for more details.
172  *       <ul>
173  *         <li>Corresponding method: None
174  *         <li>Default: {@code R.layout.exo_player_control_view}
175  *       </ul>
176  *   <li>All attributes that can be set on {@link PlayerControlView} and {@link DefaultTimeBar} can
177  *       also be set on a PlayerView, and will be propagated to the inflated {@link
178  *       PlayerControlView} unless the layout is overridden to specify a custom {@code
179  *       exo_controller} (see below).
180  * </ul>
181  *
182  * <h3>Overriding drawables</h3>
183  *
184  * The drawables used by {@link PlayerControlView} (with its default layout file) can be overridden
185  * by drawables with the same names defined in your application. See the {@link PlayerControlView}
186  * documentation for a list of drawables that can be overridden.
187  *
188  * <h3>Overriding the layout file</h3>
189  *
190  * To customize the layout of PlayerView throughout your app, or just for certain configurations,
191  * you can define {@code exo_player_view.xml} layout files in your application {@code res/layout*}
192  * directories. These layouts will override the one provided by the ExoPlayer library, and will be
193  * inflated for use by PlayerView. The view identifies and binds its children by looking for the
194  * following ids:
195  *
196  * <ul>
197  *   <li><b>{@code exo_content_frame}</b> - A frame whose aspect ratio is resized based on the video
198  *       or album art of the media being played, and the configured {@code resize_mode}. The video
199  *       surface view is inflated into this frame as its first child.
200  *       <ul>
201  *         <li>Type: {@link AspectRatioFrameLayout}
202  *       </ul>
203  *   <li><b>{@code exo_shutter}</b> - A view that's made visible when video should be hidden. This
204  *       view is typically an opaque view that covers the video surface, thereby obscuring it when
205  *       visible. Obscuring the surface in this way also helps to prevent flicker at the start of
206  *       playback when {@code surface_type="surface_view"}.
207  *       <ul>
208  *         <li>Type: {@link View}
209  *       </ul>
210  *   <li><b>{@code exo_buffering}</b> - A view that's made visible when the player is buffering.
211  *       This view typically displays a buffering spinner or animation.
212  *       <ul>
213  *         <li>Type: {@link View}
214  *       </ul>
215  *   <li><b>{@code exo_subtitles}</b> - Displays subtitles.
216  *       <ul>
217  *         <li>Type: {@link SubtitleView}
218  *       </ul>
219  *   <li><b>{@code exo_artwork}</b> - Displays album art.
220  *       <ul>
221  *         <li>Type: {@link ImageView}
222  *       </ul>
223  *   <li><b>{@code exo_error_message}</b> - Displays an error message to the user if playback fails.
224  *       <ul>
225  *         <li>Type: {@link TextView}
226  *       </ul>
227  *   <li><b>{@code exo_controller_placeholder}</b> - A placeholder that's replaced with the inflated
228  *       {@link PlayerControlView}. Ignored if an {@code exo_controller} view exists.
229  *       <ul>
230  *         <li>Type: {@link View}
231  *       </ul>
232  *   <li><b>{@code exo_controller}</b> - An already inflated {@link PlayerControlView}. Allows use
233  *       of a custom extension of {@link PlayerControlView}. {@link PlayerControlView} and {@link
234  *       DefaultTimeBar} attributes set on the PlayerView will not be automatically propagated
235  *       through to this instance. If a view exists with this id, any {@code
236  *       exo_controller_placeholder} view will be ignored.
237  *       <ul>
238  *         <li>Type: {@link PlayerControlView}
239  *       </ul>
240  *   <li><b>{@code exo_ad_overlay}</b> - A {@link FrameLayout} positioned on top of the player which
241  *       is used to show ad UI (if applicable).
242  *       <ul>
243  *         <li>Type: {@link FrameLayout}
244  *       </ul>
245  *   <li><b>{@code exo_overlay}</b> - A {@link FrameLayout} positioned on top of the player which
246  *       the app can access via {@link #getOverlayFrameLayout()}, provided for convenience.
247  *       <ul>
248  *         <li>Type: {@link FrameLayout}
249  *       </ul>
250  * </ul>
251  *
252  * <p>All child views are optional and so can be omitted if not required, however where defined they
253  * must be of the expected type.
254  *
255  * <h3>Specifying a custom layout file</h3>
256  *
257  * Defining your own {@code exo_player_view.xml} is useful to customize the layout of PlayerView
258  * throughout your application. It's also possible to customize the layout for a single instance in
259  * a layout file. This is achieved by setting the {@code player_layout_id} attribute on a
260  * PlayerView. This will cause the specified layout to be inflated instead of {@code
261  * exo_player_view.xml} for only the instance on which the attribute is set.
262  */
263 public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider {
264 
265   // LINT.IfChange
266   /**
267    * Determines when the buffering view is shown. One of {@link #SHOW_BUFFERING_NEVER}, {@link
268    * #SHOW_BUFFERING_WHEN_PLAYING} or {@link #SHOW_BUFFERING_ALWAYS}.
269    */
270   @Documented
271   @Retention(RetentionPolicy.SOURCE)
272   @IntDef({SHOW_BUFFERING_NEVER, SHOW_BUFFERING_WHEN_PLAYING, SHOW_BUFFERING_ALWAYS})
273   public @interface ShowBuffering {}
274   /** The buffering view is never shown. */
275   public static final int SHOW_BUFFERING_NEVER = 0;
276   /**
277    * The buffering view is shown when the player is in the {@link Player#STATE_BUFFERING buffering}
278    * state and {@link Player#getPlayWhenReady() playWhenReady} is {@code true}.
279    */
280   public static final int SHOW_BUFFERING_WHEN_PLAYING = 1;
281   /**
282    * The buffering view is always shown when the player is in the {@link Player#STATE_BUFFERING
283    * buffering} state.
284    */
285   public static final int SHOW_BUFFERING_ALWAYS = 2;
286   // LINT.ThenChange(../../../../../../res/values/attrs.xml)
287 
288   // LINT.IfChange
289   private static final int SURFACE_TYPE_NONE = 0;
290   private static final int SURFACE_TYPE_SURFACE_VIEW = 1;
291   private static final int SURFACE_TYPE_TEXTURE_VIEW = 2;
292   private static final int SURFACE_TYPE_SPHERICAL_GL_SURFACE_VIEW = 3;
293   private static final int SURFACE_TYPE_VIDEO_DECODER_GL_SURFACE_VIEW = 4;
294   // LINT.ThenChange(../../../../../../res/values/attrs.xml)
295 
296   private final ComponentListener componentListener;
297   @Nullable private final AspectRatioFrameLayout contentFrame;
298   @Nullable private final View shutterView;
299   @Nullable private final View surfaceView;
300   @Nullable private final ImageView artworkView;
301   @Nullable private final SubtitleView subtitleView;
302   @Nullable private final View bufferingView;
303   @Nullable private final TextView errorMessageView;
304   @Nullable private final PlayerControlView controller;
305   @Nullable private final FrameLayout adOverlayFrameLayout;
306   @Nullable private final FrameLayout overlayFrameLayout;
307 
308   @Nullable private Player player;
309   private boolean useController;
310   @Nullable private PlayerControlView.VisibilityListener controllerVisibilityListener;
311   private boolean useArtwork;
312   @Nullable private Drawable defaultArtwork;
313   private @ShowBuffering int showBuffering;
314   private boolean keepContentOnPlayerReset;
315   private boolean useSensorRotation;
316   @Nullable private ErrorMessageProvider<? super ExoPlaybackException> errorMessageProvider;
317   @Nullable private CharSequence customErrorMessage;
318   private int controllerShowTimeoutMs;
319   private boolean controllerAutoShow;
320   private boolean controllerHideDuringAds;
321   private boolean controllerHideOnTouch;
322   private int textureViewRotation;
323   private boolean isTouching;
324   private static final int PICTURE_TYPE_FRONT_COVER = 3;
325   private static final int PICTURE_TYPE_NOT_SET = -1;
326 
PlayerView(Context context)327   public PlayerView(Context context) {
328     this(context, /* attrs= */ null);
329   }
330 
PlayerView(Context context, @Nullable AttributeSet attrs)331   public PlayerView(Context context, @Nullable AttributeSet attrs) {
332     this(context, attrs, /* defStyleAttr= */ 0);
333   }
334 
335   @SuppressWarnings({"nullness:argument.type.incompatible", "nullness:method.invocation.invalid"})
PlayerView(Context context, @Nullable AttributeSet attrs, int defStyleAttr)336   public PlayerView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
337     super(context, attrs, defStyleAttr);
338 
339     componentListener = new ComponentListener();
340 
341     if (isInEditMode()) {
342       contentFrame = null;
343       shutterView = null;
344       surfaceView = null;
345       artworkView = null;
346       subtitleView = null;
347       bufferingView = null;
348       errorMessageView = null;
349       controller = null;
350       adOverlayFrameLayout = null;
351       overlayFrameLayout = null;
352       ImageView logo = new ImageView(context);
353       if (Util.SDK_INT >= 23) {
354         configureEditModeLogoV23(getResources(), logo);
355       } else {
356         configureEditModeLogo(getResources(), logo);
357       }
358       addView(logo);
359       return;
360     }
361 
362     boolean shutterColorSet = false;
363     int shutterColor = 0;
364     int playerLayoutId = R.layout.exo_player_view;
365     boolean useArtwork = true;
366     int defaultArtworkId = 0;
367     boolean useController = true;
368     int surfaceType = SURFACE_TYPE_SURFACE_VIEW;
369     int resizeMode = AspectRatioFrameLayout.RESIZE_MODE_FIT;
370     int controllerShowTimeoutMs = PlayerControlView.DEFAULT_SHOW_TIMEOUT_MS;
371     boolean controllerHideOnTouch = true;
372     boolean controllerAutoShow = true;
373     boolean controllerHideDuringAds = true;
374     int showBuffering = SHOW_BUFFERING_NEVER;
375     useSensorRotation = true;
376     if (attrs != null) {
377       TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.PlayerView, 0, 0);
378       try {
379         shutterColorSet = a.hasValue(R.styleable.PlayerView_shutter_background_color);
380         shutterColor = a.getColor(R.styleable.PlayerView_shutter_background_color, shutterColor);
381         playerLayoutId = a.getResourceId(R.styleable.PlayerView_player_layout_id, playerLayoutId);
382         useArtwork = a.getBoolean(R.styleable.PlayerView_use_artwork, useArtwork);
383         defaultArtworkId =
384             a.getResourceId(R.styleable.PlayerView_default_artwork, defaultArtworkId);
385         useController = a.getBoolean(R.styleable.PlayerView_use_controller, useController);
386         surfaceType = a.getInt(R.styleable.PlayerView_surface_type, surfaceType);
387         resizeMode = a.getInt(R.styleable.PlayerView_resize_mode, resizeMode);
388         controllerShowTimeoutMs =
389             a.getInt(R.styleable.PlayerView_show_timeout, controllerShowTimeoutMs);
390         controllerHideOnTouch =
391             a.getBoolean(R.styleable.PlayerView_hide_on_touch, controllerHideOnTouch);
392         controllerAutoShow = a.getBoolean(R.styleable.PlayerView_auto_show, controllerAutoShow);
393         showBuffering = a.getInteger(R.styleable.PlayerView_show_buffering, showBuffering);
394         keepContentOnPlayerReset =
395             a.getBoolean(
396                 R.styleable.PlayerView_keep_content_on_player_reset, keepContentOnPlayerReset);
397         controllerHideDuringAds =
398             a.getBoolean(R.styleable.PlayerView_hide_during_ads, controllerHideDuringAds);
399         useSensorRotation =
400             a.getBoolean(R.styleable.PlayerView_use_sensor_rotation, useSensorRotation);
401       } finally {
402         a.recycle();
403       }
404     }
405 
406     LayoutInflater.from(context).inflate(playerLayoutId, this);
407     setDescendantFocusability(FOCUS_AFTER_DESCENDANTS);
408 
409     // Content frame.
410     contentFrame = findViewById(R.id.exo_content_frame);
411     if (contentFrame != null) {
412       setResizeModeRaw(contentFrame, resizeMode);
413     }
414 
415     // Shutter view.
416     shutterView = findViewById(R.id.exo_shutter);
417     if (shutterView != null && shutterColorSet) {
418       shutterView.setBackgroundColor(shutterColor);
419     }
420 
421     // Create a surface view and insert it into the content frame, if there is one.
422     if (contentFrame != null && surfaceType != SURFACE_TYPE_NONE) {
423       ViewGroup.LayoutParams params =
424           new ViewGroup.LayoutParams(
425               ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
426       switch (surfaceType) {
427         case SURFACE_TYPE_TEXTURE_VIEW:
428           surfaceView = new TextureView(context);
429           break;
430         case SURFACE_TYPE_SPHERICAL_GL_SURFACE_VIEW:
431           SphericalGLSurfaceView sphericalGLSurfaceView = new SphericalGLSurfaceView(context);
432           sphericalGLSurfaceView.setSingleTapListener(componentListener);
433           sphericalGLSurfaceView.setUseSensorRotation(useSensorRotation);
434           surfaceView = sphericalGLSurfaceView;
435           break;
436         case SURFACE_TYPE_VIDEO_DECODER_GL_SURFACE_VIEW:
437           surfaceView = new VideoDecoderGLSurfaceView(context);
438           break;
439         default:
440           surfaceView = new SurfaceView(context);
441           break;
442       }
443       surfaceView.setLayoutParams(params);
444       contentFrame.addView(surfaceView, 0);
445     } else {
446       surfaceView = null;
447     }
448 
449     // Ad overlay frame layout.
450     adOverlayFrameLayout = findViewById(R.id.exo_ad_overlay);
451 
452     // Overlay frame layout.
453     overlayFrameLayout = findViewById(R.id.exo_overlay);
454 
455     // Artwork view.
456     artworkView = findViewById(R.id.exo_artwork);
457     this.useArtwork = useArtwork && artworkView != null;
458     if (defaultArtworkId != 0) {
459       defaultArtwork = ContextCompat.getDrawable(getContext(), defaultArtworkId);
460     }
461 
462     // Subtitle view.
463     subtitleView = findViewById(R.id.exo_subtitles);
464     if (subtitleView != null) {
465       subtitleView.setUserDefaultStyle();
466       subtitleView.setUserDefaultTextSize();
467     }
468 
469     // Buffering view.
470     bufferingView = findViewById(R.id.exo_buffering);
471     if (bufferingView != null) {
472       bufferingView.setVisibility(View.GONE);
473     }
474     this.showBuffering = showBuffering;
475 
476     // Error message view.
477     errorMessageView = findViewById(R.id.exo_error_message);
478     if (errorMessageView != null) {
479       errorMessageView.setVisibility(View.GONE);
480     }
481 
482     // Playback control view.
483     PlayerControlView customController = findViewById(R.id.exo_controller);
484     View controllerPlaceholder = findViewById(R.id.exo_controller_placeholder);
485     if (customController != null) {
486       this.controller = customController;
487     } else if (controllerPlaceholder != null) {
488       // Propagate attrs as playbackAttrs so that PlayerControlView's custom attributes are
489       // transferred, but standard attributes (e.g. background) are not.
490       this.controller = new PlayerControlView(context, null, 0, attrs);
491       controller.setId(R.id.exo_controller);
492       controller.setLayoutParams(controllerPlaceholder.getLayoutParams());
493       ViewGroup parent = ((ViewGroup) controllerPlaceholder.getParent());
494       int controllerIndex = parent.indexOfChild(controllerPlaceholder);
495       parent.removeView(controllerPlaceholder);
496       parent.addView(controller, controllerIndex);
497     } else {
498       this.controller = null;
499     }
500     this.controllerShowTimeoutMs = controller != null ? controllerShowTimeoutMs : 0;
501     this.controllerHideOnTouch = controllerHideOnTouch;
502     this.controllerAutoShow = controllerAutoShow;
503     this.controllerHideDuringAds = controllerHideDuringAds;
504     this.useController = useController && controller != null;
505     hideController();
506     updateContentDescription();
507     if (controller != null) {
508       controller.addVisibilityListener(/* listener= */ componentListener);
509     }
510   }
511 
512   /**
513    * Switches the view targeted by a given {@link Player}.
514    *
515    * @param player The player whose target view is being switched.
516    * @param oldPlayerView The old view to detach from the player.
517    * @param newPlayerView The new view to attach to the player.
518    */
switchTargetView( Player player, @Nullable PlayerView oldPlayerView, @Nullable PlayerView newPlayerView)519   public static void switchTargetView(
520       Player player, @Nullable PlayerView oldPlayerView, @Nullable PlayerView newPlayerView) {
521     if (oldPlayerView == newPlayerView) {
522       return;
523     }
524     // We attach the new view before detaching the old one because this ordering allows the player
525     // to swap directly from one surface to another, without transitioning through a state where no
526     // surface is attached. This is significantly more efficient and achieves a more seamless
527     // transition when using platform provided video decoders.
528     if (newPlayerView != null) {
529       newPlayerView.setPlayer(player);
530     }
531     if (oldPlayerView != null) {
532       oldPlayerView.setPlayer(null);
533     }
534   }
535 
536   /** Returns the player currently set on this view, or null if no player is set. */
537   @Nullable
getPlayer()538   public Player getPlayer() {
539     return player;
540   }
541 
542   /**
543    * Set the {@link Player} to use.
544    *
545    * <p>To transition a {@link Player} from targeting one view to another, it's recommended to use
546    * {@link #switchTargetView(Player, PlayerView, PlayerView)} rather than this method. If you do
547    * wish to use this method directly, be sure to attach the player to the new view <em>before</em>
548    * calling {@code setPlayer(null)} to detach it from the old one. This ordering is significantly
549    * more efficient and may allow for more seamless transitions.
550    *
551    * @param player The {@link Player} to use, or {@code null} to detach the current player. Only
552    *     players which are accessed on the main thread are supported ({@code
553    *     player.getApplicationLooper() == Looper.getMainLooper()}).
554    */
setPlayer(@ullable Player player)555   public void setPlayer(@Nullable Player player) {
556     Assertions.checkState(Looper.myLooper() == Looper.getMainLooper());
557     Assertions.checkArgument(
558         player == null || player.getApplicationLooper() == Looper.getMainLooper());
559     if (this.player == player) {
560       return;
561     }
562     @Nullable Player oldPlayer = this.player;
563     if (oldPlayer != null) {
564       oldPlayer.removeListener(componentListener);
565       @Nullable Player.VideoComponent oldVideoComponent = oldPlayer.getVideoComponent();
566       if (oldVideoComponent != null) {
567         oldVideoComponent.removeVideoListener(componentListener);
568         if (surfaceView instanceof TextureView) {
569           oldVideoComponent.clearVideoTextureView((TextureView) surfaceView);
570         } else if (surfaceView instanceof SphericalGLSurfaceView) {
571           ((SphericalGLSurfaceView) surfaceView).setVideoComponent(null);
572         } else if (surfaceView instanceof VideoDecoderGLSurfaceView) {
573           oldVideoComponent.setVideoDecoderOutputBufferRenderer(null);
574         } else if (surfaceView instanceof SurfaceView) {
575           oldVideoComponent.clearVideoSurfaceView((SurfaceView) surfaceView);
576         }
577       }
578       @Nullable Player.TextComponent oldTextComponent = oldPlayer.getTextComponent();
579       if (oldTextComponent != null) {
580         oldTextComponent.removeTextOutput(componentListener);
581       }
582     }
583     this.player = player;
584     if (useController()) {
585       controller.setPlayer(player);
586     }
587     if (subtitleView != null) {
588       subtitleView.setCues(null);
589     }
590     updateBuffering();
591     updateErrorMessage();
592     updateForCurrentTrackSelections(/* isNewPlayer= */ true);
593     if (player != null) {
594       @Nullable Player.VideoComponent newVideoComponent = player.getVideoComponent();
595       if (newVideoComponent != null) {
596         if (surfaceView instanceof TextureView) {
597           newVideoComponent.setVideoTextureView((TextureView) surfaceView);
598         } else if (surfaceView instanceof SphericalGLSurfaceView) {
599           ((SphericalGLSurfaceView) surfaceView).setVideoComponent(newVideoComponent);
600         } else if (surfaceView instanceof VideoDecoderGLSurfaceView) {
601           newVideoComponent.setVideoDecoderOutputBufferRenderer(
602               ((VideoDecoderGLSurfaceView) surfaceView).getVideoDecoderOutputBufferRenderer());
603         } else if (surfaceView instanceof SurfaceView) {
604           newVideoComponent.setVideoSurfaceView((SurfaceView) surfaceView);
605         }
606         newVideoComponent.addVideoListener(componentListener);
607       }
608       @Nullable Player.TextComponent newTextComponent = player.getTextComponent();
609       if (newTextComponent != null) {
610         newTextComponent.addTextOutput(componentListener);
611       }
612       player.addListener(componentListener);
613       maybeShowController(false);
614     } else {
615       hideController();
616     }
617   }
618 
619   @Override
setVisibility(int visibility)620   public void setVisibility(int visibility) {
621     super.setVisibility(visibility);
622     if (surfaceView instanceof SurfaceView) {
623       // Work around https://github.com/google/ExoPlayer/issues/3160.
624       surfaceView.setVisibility(visibility);
625     }
626   }
627 
628   /**
629    * Sets the {@link ResizeMode}.
630    *
631    * @param resizeMode The {@link ResizeMode}.
632    */
setResizeMode(@esizeMode int resizeMode)633   public void setResizeMode(@ResizeMode int resizeMode) {
634     Assertions.checkStateNotNull(contentFrame);
635     contentFrame.setResizeMode(resizeMode);
636   }
637 
638   /** Returns the {@link ResizeMode}. */
getResizeMode()639   public @ResizeMode int getResizeMode() {
640     Assertions.checkStateNotNull(contentFrame);
641     return contentFrame.getResizeMode();
642   }
643 
644   /** Returns whether artwork is displayed if present in the media. */
getUseArtwork()645   public boolean getUseArtwork() {
646     return useArtwork;
647   }
648 
649   /**
650    * Sets whether artwork is displayed if present in the media.
651    *
652    * @param useArtwork Whether artwork is displayed.
653    */
setUseArtwork(boolean useArtwork)654   public void setUseArtwork(boolean useArtwork) {
655     Assertions.checkState(!useArtwork || artworkView != null);
656     if (this.useArtwork != useArtwork) {
657       this.useArtwork = useArtwork;
658       updateForCurrentTrackSelections(/* isNewPlayer= */ false);
659     }
660   }
661 
662   /** Returns the default artwork to display. */
663   @Nullable
getDefaultArtwork()664   public Drawable getDefaultArtwork() {
665     return defaultArtwork;
666   }
667 
668   /**
669    * Sets the default artwork to display if {@code useArtwork} is {@code true} and no artwork is
670    * present in the media.
671    *
672    * @param defaultArtwork the default artwork to display.
673    * @deprecated use (@link {@link #setDefaultArtwork(Drawable)} instead.
674    */
675   @Deprecated
setDefaultArtwork(@ullable Bitmap defaultArtwork)676   public void setDefaultArtwork(@Nullable Bitmap defaultArtwork) {
677     setDefaultArtwork(
678         defaultArtwork == null ? null : new BitmapDrawable(getResources(), defaultArtwork));
679   }
680 
681   /**
682    * Sets the default artwork to display if {@code useArtwork} is {@code true} and no artwork is
683    * present in the media.
684    *
685    * @param defaultArtwork the default artwork to display
686    */
setDefaultArtwork(@ullable Drawable defaultArtwork)687   public void setDefaultArtwork(@Nullable Drawable defaultArtwork) {
688     if (this.defaultArtwork != defaultArtwork) {
689       this.defaultArtwork = defaultArtwork;
690       updateForCurrentTrackSelections(/* isNewPlayer= */ false);
691     }
692   }
693 
694   /** Returns whether the playback controls can be shown. */
getUseController()695   public boolean getUseController() {
696     return useController;
697   }
698 
699   /**
700    * Sets whether the playback controls can be shown. If set to {@code false} the playback controls
701    * are never visible and are disconnected from the player.
702    *
703    * @param useController Whether the playback controls can be shown.
704    */
setUseController(boolean useController)705   public void setUseController(boolean useController) {
706     Assertions.checkState(!useController || controller != null);
707     if (this.useController == useController) {
708       return;
709     }
710     this.useController = useController;
711     if (useController()) {
712       controller.setPlayer(player);
713     } else if (controller != null) {
714       controller.hide();
715       controller.setPlayer(/* player= */ null);
716     }
717     updateContentDescription();
718   }
719 
720   /**
721    * Sets the background color of the {@code exo_shutter} view.
722    *
723    * @param color The background color.
724    */
setShutterBackgroundColor(int color)725   public void setShutterBackgroundColor(int color) {
726     if (shutterView != null) {
727       shutterView.setBackgroundColor(color);
728     }
729   }
730 
731   /**
732    * Sets whether the currently displayed video frame or media artwork is kept visible when the
733    * player is reset. A player reset is defined to mean the player being re-prepared with different
734    * media, the player transitioning to unprepared media, {@link Player#stop(boolean)} being called
735    * with {@code reset=true}, or the player being replaced or cleared by calling {@link
736    * #setPlayer(Player)}.
737    *
738    * <p>If enabled, the currently displayed video frame or media artwork will be kept visible until
739    * the player set on the view has been successfully prepared with new media and loaded enough of
740    * it to have determined the available tracks. Hence enabling this option allows transitioning
741    * from playing one piece of media to another, or from using one player instance to another,
742    * without clearing the view's content.
743    *
744    * <p>If disabled, the currently displayed video frame or media artwork will be hidden as soon as
745    * the player is reset. Note that the video frame is hidden by making {@code exo_shutter} visible.
746    * Hence the video frame will not be hidden if using a custom layout that omits this view.
747    *
748    * @param keepContentOnPlayerReset Whether the currently displayed video frame or media artwork is
749    *     kept visible when the player is reset.
750    */
setKeepContentOnPlayerReset(boolean keepContentOnPlayerReset)751   public void setKeepContentOnPlayerReset(boolean keepContentOnPlayerReset) {
752     if (this.keepContentOnPlayerReset != keepContentOnPlayerReset) {
753       this.keepContentOnPlayerReset = keepContentOnPlayerReset;
754       updateForCurrentTrackSelections(/* isNewPlayer= */ false);
755     }
756   }
757 
758   /**
759    * Sets whether to use the orientation sensor for rotation during spherical playbacks (if
760    * available)
761    *
762    * @param useSensorRotation Whether to use the orientation sensor for rotation during spherical
763    *     playbacks.
764    */
setUseSensorRotation(boolean useSensorRotation)765   public void setUseSensorRotation(boolean useSensorRotation) {
766     if (this.useSensorRotation != useSensorRotation) {
767       this.useSensorRotation = useSensorRotation;
768       if (surfaceView instanceof SphericalGLSurfaceView) {
769         ((SphericalGLSurfaceView) surfaceView).setUseSensorRotation(useSensorRotation);
770       }
771     }
772   }
773 
774   /**
775    * Sets whether a buffering spinner is displayed when the player is in the buffering state. The
776    * buffering spinner is not displayed by default.
777    *
778    * @deprecated Use {@link #setShowBuffering(int)}
779    * @param showBuffering Whether the buffering icon is displayed
780    */
781   @Deprecated
setShowBuffering(boolean showBuffering)782   public void setShowBuffering(boolean showBuffering) {
783     setShowBuffering(showBuffering ? SHOW_BUFFERING_WHEN_PLAYING : SHOW_BUFFERING_NEVER);
784   }
785 
786   /**
787    * Sets whether a buffering spinner is displayed when the player is in the buffering state. The
788    * buffering spinner is not displayed by default.
789    *
790    * @param showBuffering The mode that defines when the buffering spinner is displayed. One of
791    *     {@link #SHOW_BUFFERING_NEVER}, {@link #SHOW_BUFFERING_WHEN_PLAYING} and {@link
792    *     #SHOW_BUFFERING_ALWAYS}.
793    */
setShowBuffering(@howBuffering int showBuffering)794   public void setShowBuffering(@ShowBuffering int showBuffering) {
795     if (this.showBuffering != showBuffering) {
796       this.showBuffering = showBuffering;
797       updateBuffering();
798     }
799   }
800 
801   /**
802    * Sets the optional {@link ErrorMessageProvider}.
803    *
804    * @param errorMessageProvider The error message provider.
805    */
setErrorMessageProvider( @ullable ErrorMessageProvider<? super ExoPlaybackException> errorMessageProvider)806   public void setErrorMessageProvider(
807       @Nullable ErrorMessageProvider<? super ExoPlaybackException> errorMessageProvider) {
808     if (this.errorMessageProvider != errorMessageProvider) {
809       this.errorMessageProvider = errorMessageProvider;
810       updateErrorMessage();
811     }
812   }
813 
814   /**
815    * Sets a custom error message to be displayed by the view. The error message will be displayed
816    * permanently, unless it is cleared by passing {@code null} to this method.
817    *
818    * @param message The message to display, or {@code null} to clear a previously set message.
819    */
setCustomErrorMessage(@ullable CharSequence message)820   public void setCustomErrorMessage(@Nullable CharSequence message) {
821     Assertions.checkState(errorMessageView != null);
822     customErrorMessage = message;
823     updateErrorMessage();
824   }
825 
826   @Override
dispatchKeyEvent(KeyEvent event)827   public boolean dispatchKeyEvent(KeyEvent event) {
828     if (player != null && player.isPlayingAd()) {
829       return super.dispatchKeyEvent(event);
830     }
831 
832     boolean isDpadKey = isDpadKey(event.getKeyCode());
833     boolean handled = false;
834     if (isDpadKey && useController() && !controller.isVisible()) {
835       // Handle the key event by showing the controller.
836       maybeShowController(true);
837       handled = true;
838     } else if (dispatchMediaKeyEvent(event) || super.dispatchKeyEvent(event)) {
839       // The key event was handled as a media key or by the super class. We should also show the
840       // controller, or extend its show timeout if already visible.
841       maybeShowController(true);
842       handled = true;
843     } else if (isDpadKey && useController()) {
844       // The key event wasn't handled, but we should extend the controller's show timeout.
845       maybeShowController(true);
846     }
847     return handled;
848   }
849 
850   /**
851    * Called to process media key events. Any {@link KeyEvent} can be passed but only media key
852    * events will be handled. Does nothing if playback controls are disabled.
853    *
854    * @param event A key event.
855    * @return Whether the key event was handled.
856    */
dispatchMediaKeyEvent(KeyEvent event)857   public boolean dispatchMediaKeyEvent(KeyEvent event) {
858     return useController() && controller.dispatchMediaKeyEvent(event);
859   }
860 
861   /** Returns whether the controller is currently visible. */
isControllerVisible()862   public boolean isControllerVisible() {
863     return controller != null && controller.isVisible();
864   }
865 
866   /**
867    * Shows the playback controls. Does nothing if playback controls are disabled.
868    *
869    * <p>The playback controls are automatically hidden during playback after {{@link
870    * #getControllerShowTimeoutMs()}}. They are shown indefinitely when playback has not started yet,
871    * is paused, has ended or failed.
872    */
showController()873   public void showController() {
874     showController(shouldShowControllerIndefinitely());
875   }
876 
877   /** Hides the playback controls. Does nothing if playback controls are disabled. */
hideController()878   public void hideController() {
879     if (controller != null) {
880       controller.hide();
881     }
882   }
883 
884   /**
885    * Returns the playback controls timeout. The playback controls are automatically hidden after
886    * this duration of time has elapsed without user input and with playback or buffering in
887    * progress.
888    *
889    * @return The timeout in milliseconds. A non-positive value will cause the controller to remain
890    *     visible indefinitely.
891    */
getControllerShowTimeoutMs()892   public int getControllerShowTimeoutMs() {
893     return controllerShowTimeoutMs;
894   }
895 
896   /**
897    * Sets the playback controls timeout. The playback controls are automatically hidden after this
898    * duration of time has elapsed without user input and with playback or buffering in progress.
899    *
900    * @param controllerShowTimeoutMs The timeout in milliseconds. A non-positive value will cause the
901    *     controller to remain visible indefinitely.
902    */
setControllerShowTimeoutMs(int controllerShowTimeoutMs)903   public void setControllerShowTimeoutMs(int controllerShowTimeoutMs) {
904     Assertions.checkStateNotNull(controller);
905     this.controllerShowTimeoutMs = controllerShowTimeoutMs;
906     if (controller.isVisible()) {
907       // Update the controller's timeout if necessary.
908       showController();
909     }
910   }
911 
912   /** Returns whether the playback controls are hidden by touch events. */
getControllerHideOnTouch()913   public boolean getControllerHideOnTouch() {
914     return controllerHideOnTouch;
915   }
916 
917   /**
918    * Sets whether the playback controls are hidden by touch events.
919    *
920    * @param controllerHideOnTouch Whether the playback controls are hidden by touch events.
921    */
setControllerHideOnTouch(boolean controllerHideOnTouch)922   public void setControllerHideOnTouch(boolean controllerHideOnTouch) {
923     Assertions.checkStateNotNull(controller);
924     this.controllerHideOnTouch = controllerHideOnTouch;
925     updateContentDescription();
926   }
927 
928   /**
929    * Returns whether the playback controls are automatically shown when playback starts, pauses,
930    * ends, or fails. If set to false, the playback controls can be manually operated with {@link
931    * #showController()} and {@link #hideController()}.
932    */
getControllerAutoShow()933   public boolean getControllerAutoShow() {
934     return controllerAutoShow;
935   }
936 
937   /**
938    * Sets whether the playback controls are automatically shown when playback starts, pauses, ends,
939    * or fails. If set to false, the playback controls can be manually operated with {@link
940    * #showController()} and {@link #hideController()}.
941    *
942    * @param controllerAutoShow Whether the playback controls are allowed to show automatically.
943    */
setControllerAutoShow(boolean controllerAutoShow)944   public void setControllerAutoShow(boolean controllerAutoShow) {
945     this.controllerAutoShow = controllerAutoShow;
946   }
947 
948   /**
949    * Sets whether the playback controls are hidden when ads are playing. Controls are always shown
950    * during ads if they are enabled and the player is paused.
951    *
952    * @param controllerHideDuringAds Whether the playback controls are hidden when ads are playing.
953    */
setControllerHideDuringAds(boolean controllerHideDuringAds)954   public void setControllerHideDuringAds(boolean controllerHideDuringAds) {
955     this.controllerHideDuringAds = controllerHideDuringAds;
956   }
957 
958   /**
959    * Set the {@link PlayerControlView.VisibilityListener}.
960    *
961    * @param listener The listener to be notified about visibility changes, or null to remove the
962    *     current listener.
963    */
setControllerVisibilityListener( @ullable PlayerControlView.VisibilityListener listener)964   public void setControllerVisibilityListener(
965       @Nullable PlayerControlView.VisibilityListener listener) {
966     Assertions.checkStateNotNull(controller);
967     if (this.controllerVisibilityListener == listener) {
968       return;
969     }
970     if (this.controllerVisibilityListener != null) {
971       controller.removeVisibilityListener(this.controllerVisibilityListener);
972     }
973     this.controllerVisibilityListener = listener;
974     if (listener != null) {
975       controller.addVisibilityListener(listener);
976     }
977   }
978 
979   /**
980    * Sets the {@link PlaybackPreparer}.
981    *
982    * @param playbackPreparer The {@link PlaybackPreparer}, or null to remove the current playback
983    *     preparer.
984    */
setPlaybackPreparer(@ullable PlaybackPreparer playbackPreparer)985   public void setPlaybackPreparer(@Nullable PlaybackPreparer playbackPreparer) {
986     Assertions.checkStateNotNull(controller);
987     controller.setPlaybackPreparer(playbackPreparer);
988   }
989 
990   /**
991    * Sets the {@link ControlDispatcher}.
992    *
993    * @param controlDispatcher The {@link ControlDispatcher}.
994    */
setControlDispatcher(ControlDispatcher controlDispatcher)995   public void setControlDispatcher(ControlDispatcher controlDispatcher) {
996     Assertions.checkStateNotNull(controller);
997     controller.setControlDispatcher(controlDispatcher);
998   }
999 
1000   /**
1001    * @deprecated Use {@link #setControlDispatcher(ControlDispatcher)} with {@link
1002    *     DefaultControlDispatcher#DefaultControlDispatcher(long, long)}.
1003    */
1004   @SuppressWarnings("deprecation")
1005   @Deprecated
setRewindIncrementMs(int rewindMs)1006   public void setRewindIncrementMs(int rewindMs) {
1007     Assertions.checkStateNotNull(controller);
1008     controller.setRewindIncrementMs(rewindMs);
1009   }
1010 
1011   /**
1012    * @deprecated Use {@link #setControlDispatcher(ControlDispatcher)} with {@link
1013    *     DefaultControlDispatcher#DefaultControlDispatcher(long, long)}.
1014    */
1015   @SuppressWarnings("deprecation")
1016   @Deprecated
setFastForwardIncrementMs(int fastForwardMs)1017   public void setFastForwardIncrementMs(int fastForwardMs) {
1018     Assertions.checkStateNotNull(controller);
1019     controller.setFastForwardIncrementMs(fastForwardMs);
1020   }
1021 
1022   /**
1023    * Sets which repeat toggle modes are enabled.
1024    *
1025    * @param repeatToggleModes A set of {@link RepeatModeUtil.RepeatToggleModes}.
1026    */
setRepeatToggleModes(@epeatModeUtil.RepeatToggleModes int repeatToggleModes)1027   public void setRepeatToggleModes(@RepeatModeUtil.RepeatToggleModes int repeatToggleModes) {
1028     Assertions.checkStateNotNull(controller);
1029     controller.setRepeatToggleModes(repeatToggleModes);
1030   }
1031 
1032   /**
1033    * Sets whether the shuffle button is shown.
1034    *
1035    * @param showShuffleButton Whether the shuffle button is shown.
1036    */
setShowShuffleButton(boolean showShuffleButton)1037   public void setShowShuffleButton(boolean showShuffleButton) {
1038     Assertions.checkStateNotNull(controller);
1039     controller.setShowShuffleButton(showShuffleButton);
1040   }
1041 
1042   /**
1043    * Sets whether the time bar should show all windows, as opposed to just the current one.
1044    *
1045    * @param showMultiWindowTimeBar Whether to show all windows.
1046    */
setShowMultiWindowTimeBar(boolean showMultiWindowTimeBar)1047   public void setShowMultiWindowTimeBar(boolean showMultiWindowTimeBar) {
1048     Assertions.checkStateNotNull(controller);
1049     controller.setShowMultiWindowTimeBar(showMultiWindowTimeBar);
1050   }
1051 
1052   /**
1053    * Sets the millisecond positions of extra ad markers relative to the start of the window (or
1054    * timeline, if in multi-window mode) and whether each extra ad has been played or not. The
1055    * markers are shown in addition to any ad markers for ads in the player's timeline.
1056    *
1057    * @param extraAdGroupTimesMs The millisecond timestamps of the extra ad markers to show, or
1058    *     {@code null} to show no extra ad markers.
1059    * @param extraPlayedAdGroups Whether each ad has been played, or {@code null} to show no extra ad
1060    *     markers.
1061    */
setExtraAdGroupMarkers( @ullable long[] extraAdGroupTimesMs, @Nullable boolean[] extraPlayedAdGroups)1062   public void setExtraAdGroupMarkers(
1063       @Nullable long[] extraAdGroupTimesMs, @Nullable boolean[] extraPlayedAdGroups) {
1064     Assertions.checkStateNotNull(controller);
1065     controller.setExtraAdGroupMarkers(extraAdGroupTimesMs, extraPlayedAdGroups);
1066   }
1067 
1068   /**
1069    * Set the {@link AspectRatioFrameLayout.AspectRatioListener}.
1070    *
1071    * @param listener The listener to be notified about aspect ratios changes of the video content or
1072    *     the content frame.
1073    */
setAspectRatioListener( @ullable AspectRatioFrameLayout.AspectRatioListener listener)1074   public void setAspectRatioListener(
1075       @Nullable AspectRatioFrameLayout.AspectRatioListener listener) {
1076     Assertions.checkStateNotNull(contentFrame);
1077     contentFrame.setAspectRatioListener(listener);
1078   }
1079 
1080   /**
1081    * Gets the view onto which video is rendered. This is a:
1082    *
1083    * <ul>
1084    *   <li>{@link SurfaceView} by default, or if the {@code surface_type} attribute is set to {@code
1085    *       surface_view}.
1086    *   <li>{@link TextureView} if {@code surface_type} is {@code texture_view}.
1087    *   <li>{@link SphericalGLSurfaceView} if {@code surface_type} is {@code
1088    *       spherical_gl_surface_view}.
1089    *   <li>{@link VideoDecoderGLSurfaceView} if {@code surface_type} is {@code
1090    *       video_decoder_gl_surface_view}.
1091    *   <li>{@code null} if {@code surface_type} is {@code none}.
1092    * </ul>
1093    *
1094    * @return The {@link SurfaceView}, {@link TextureView}, {@link SphericalGLSurfaceView}, {@link
1095    *     VideoDecoderGLSurfaceView} or {@code null}.
1096    */
1097   @Nullable
getVideoSurfaceView()1098   public View getVideoSurfaceView() {
1099     return surfaceView;
1100   }
1101 
1102   /**
1103    * Gets the overlay {@link FrameLayout}, which can be populated with UI elements to show on top of
1104    * the player.
1105    *
1106    * @return The overlay {@link FrameLayout}, or {@code null} if the layout has been customized and
1107    *     the overlay is not present.
1108    */
1109   @Nullable
getOverlayFrameLayout()1110   public FrameLayout getOverlayFrameLayout() {
1111     return overlayFrameLayout;
1112   }
1113 
1114   /**
1115    * Gets the {@link SubtitleView}.
1116    *
1117    * @return The {@link SubtitleView}, or {@code null} if the layout has been customized and the
1118    *     subtitle view is not present.
1119    */
1120   @Nullable
getSubtitleView()1121   public SubtitleView getSubtitleView() {
1122     return subtitleView;
1123   }
1124 
1125   @Override
onTouchEvent(MotionEvent event)1126   public boolean onTouchEvent(MotionEvent event) {
1127     if (!useController() || player == null) {
1128       return false;
1129     }
1130     switch (event.getAction()) {
1131       case MotionEvent.ACTION_DOWN:
1132         isTouching = true;
1133         return true;
1134       case MotionEvent.ACTION_UP:
1135         if (isTouching) {
1136           isTouching = false;
1137           performClick();
1138           return true;
1139         }
1140         return false;
1141       default:
1142         return false;
1143     }
1144   }
1145 
1146   @Override
performClick()1147   public boolean performClick() {
1148     super.performClick();
1149     return toggleControllerVisibility();
1150   }
1151 
1152   @Override
onTrackballEvent(MotionEvent ev)1153   public boolean onTrackballEvent(MotionEvent ev) {
1154     if (!useController() || player == null) {
1155       return false;
1156     }
1157     maybeShowController(true);
1158     return true;
1159   }
1160 
1161   /**
1162    * Should be called when the player is visible to the user and if {@code surface_type} is {@code
1163    * spherical_gl_surface_view}. It is the counterpart to {@link #onPause()}.
1164    *
1165    * <p>This method should typically be called in {@code Activity.onStart()}, or {@code
1166    * Activity.onResume()} for API versions &lt;= 23.
1167    */
onResume()1168   public void onResume() {
1169     if (surfaceView instanceof SphericalGLSurfaceView) {
1170       ((SphericalGLSurfaceView) surfaceView).onResume();
1171     }
1172   }
1173 
1174   /**
1175    * Should be called when the player is no longer visible to the user and if {@code surface_type}
1176    * is {@code spherical_gl_surface_view}. It is the counterpart to {@link #onResume()}.
1177    *
1178    * <p>This method should typically be called in {@code Activity.onStop()}, or {@code
1179    * Activity.onPause()} for API versions &lt;= 23.
1180    */
onPause()1181   public void onPause() {
1182     if (surfaceView instanceof SphericalGLSurfaceView) {
1183       ((SphericalGLSurfaceView) surfaceView).onPause();
1184     }
1185   }
1186 
1187   /**
1188    * Called when there's a change in the aspect ratio of the content being displayed. The default
1189    * implementation sets the aspect ratio of the content frame to that of the content, unless the
1190    * content view is a {@link SphericalGLSurfaceView} in which case the frame's aspect ratio is
1191    * cleared.
1192    *
1193    * @param contentAspectRatio The aspect ratio of the content.
1194    * @param contentFrame The content frame, or {@code null}.
1195    * @param contentView The view that holds the content being displayed, or {@code null}.
1196    */
onContentAspectRatioChanged( float contentAspectRatio, @Nullable AspectRatioFrameLayout contentFrame, @Nullable View contentView)1197   protected void onContentAspectRatioChanged(
1198       float contentAspectRatio,
1199       @Nullable AspectRatioFrameLayout contentFrame,
1200       @Nullable View contentView) {
1201     if (contentFrame != null) {
1202       contentFrame.setAspectRatio(
1203           contentView instanceof SphericalGLSurfaceView ? 0 : contentAspectRatio);
1204     }
1205   }
1206 
1207   // AdsLoader.AdViewProvider implementation.
1208 
1209   @Override
getAdViewGroup()1210   public ViewGroup getAdViewGroup() {
1211     return Assertions.checkStateNotNull(
1212         adOverlayFrameLayout, "exo_ad_overlay must be present for ad playback");
1213   }
1214 
1215   @Override
getAdOverlayViews()1216   public View[] getAdOverlayViews() {
1217     ArrayList<View> overlayViews = new ArrayList<>();
1218     if (overlayFrameLayout != null) {
1219       overlayViews.add(overlayFrameLayout);
1220     }
1221     if (controller != null) {
1222       overlayViews.add(controller);
1223     }
1224     return overlayViews.toArray(new View[0]);
1225   }
1226 
1227   // Internal methods.
1228 
1229   @EnsuresNonNullIf(expression = "controller", result = true)
useController()1230   private boolean useController() {
1231     if (useController) {
1232       Assertions.checkStateNotNull(controller);
1233       return true;
1234     }
1235     return false;
1236   }
1237 
1238   @EnsuresNonNullIf(expression = "artworkView", result = true)
useArtwork()1239   private boolean useArtwork() {
1240     if (useArtwork) {
1241       Assertions.checkStateNotNull(artworkView);
1242       return true;
1243     }
1244     return false;
1245   }
1246 
toggleControllerVisibility()1247   private boolean toggleControllerVisibility() {
1248     if (!useController() || player == null) {
1249       return false;
1250     }
1251     if (!controller.isVisible()) {
1252       maybeShowController(true);
1253     } else if (controllerHideOnTouch) {
1254       controller.hide();
1255     }
1256     return true;
1257   }
1258 
1259   /** Shows the playback controls, but only if forced or shown indefinitely. */
maybeShowController(boolean isForced)1260   private void maybeShowController(boolean isForced) {
1261     if (isPlayingAd() && controllerHideDuringAds) {
1262       return;
1263     }
1264     if (useController()) {
1265       boolean wasShowingIndefinitely = controller.isVisible() && controller.getShowTimeoutMs() <= 0;
1266       boolean shouldShowIndefinitely = shouldShowControllerIndefinitely();
1267       if (isForced || wasShowingIndefinitely || shouldShowIndefinitely) {
1268         showController(shouldShowIndefinitely);
1269       }
1270     }
1271   }
1272 
shouldShowControllerIndefinitely()1273   private boolean shouldShowControllerIndefinitely() {
1274     if (player == null) {
1275       return true;
1276     }
1277     int playbackState = player.getPlaybackState();
1278     return controllerAutoShow
1279         && (playbackState == Player.STATE_IDLE
1280             || playbackState == Player.STATE_ENDED
1281             || !player.getPlayWhenReady());
1282   }
1283 
showController(boolean showIndefinitely)1284   private void showController(boolean showIndefinitely) {
1285     if (!useController()) {
1286       return;
1287     }
1288     controller.setShowTimeoutMs(showIndefinitely ? 0 : controllerShowTimeoutMs);
1289     controller.show();
1290   }
1291 
isPlayingAd()1292   private boolean isPlayingAd() {
1293     return player != null && player.isPlayingAd() && player.getPlayWhenReady();
1294   }
1295 
updateForCurrentTrackSelections(boolean isNewPlayer)1296   private void updateForCurrentTrackSelections(boolean isNewPlayer) {
1297     @Nullable Player player = this.player;
1298     if (player == null || player.getCurrentTrackGroups().isEmpty()) {
1299       if (!keepContentOnPlayerReset) {
1300         hideArtwork();
1301         closeShutter();
1302       }
1303       return;
1304     }
1305 
1306     if (isNewPlayer && !keepContentOnPlayerReset) {
1307       // Hide any video from the previous player.
1308       closeShutter();
1309     }
1310 
1311     TrackSelectionArray selections = player.getCurrentTrackSelections();
1312     for (int i = 0; i < selections.length; i++) {
1313       if (player.getRendererType(i) == C.TRACK_TYPE_VIDEO && selections.get(i) != null) {
1314         // Video enabled so artwork must be hidden. If the shutter is closed, it will be opened in
1315         // onRenderedFirstFrame().
1316         hideArtwork();
1317         return;
1318       }
1319     }
1320 
1321     // Video disabled so the shutter must be closed.
1322     closeShutter();
1323     // Display artwork if enabled and available, else hide it.
1324     if (useArtwork()) {
1325       for (int i = 0; i < selections.length; i++) {
1326         @Nullable TrackSelection selection = selections.get(i);
1327         if (selection != null) {
1328           for (int j = 0; j < selection.length(); j++) {
1329             @Nullable Metadata metadata = selection.getFormat(j).metadata;
1330             if (metadata != null && setArtworkFromMetadata(metadata)) {
1331               return;
1332             }
1333           }
1334         }
1335       }
1336       if (setDrawableArtwork(defaultArtwork)) {
1337         return;
1338       }
1339     }
1340     // Artwork disabled or unavailable.
1341     hideArtwork();
1342   }
1343 
1344   @RequiresNonNull("artworkView")
setArtworkFromMetadata(Metadata metadata)1345   private boolean setArtworkFromMetadata(Metadata metadata) {
1346     boolean isArtworkSet = false;
1347     int currentPictureType = PICTURE_TYPE_NOT_SET;
1348     for (int i = 0; i < metadata.length(); i++) {
1349       Metadata.Entry metadataEntry = metadata.get(i);
1350       int pictureType;
1351       byte[] bitmapData;
1352       if (metadataEntry instanceof ApicFrame) {
1353         bitmapData = ((ApicFrame) metadataEntry).pictureData;
1354         pictureType = ((ApicFrame) metadataEntry).pictureType;
1355       } else if (metadataEntry instanceof PictureFrame) {
1356         bitmapData = ((PictureFrame) metadataEntry).pictureData;
1357         pictureType = ((PictureFrame) metadataEntry).pictureType;
1358       } else {
1359         continue;
1360       }
1361       // Prefer the first front cover picture. If there aren't any, prefer the first picture.
1362       if (currentPictureType == PICTURE_TYPE_NOT_SET || pictureType == PICTURE_TYPE_FRONT_COVER) {
1363         Bitmap bitmap = BitmapFactory.decodeByteArray(bitmapData, 0, bitmapData.length);
1364         isArtworkSet = setDrawableArtwork(new BitmapDrawable(getResources(), bitmap));
1365         currentPictureType = pictureType;
1366         if (currentPictureType == PICTURE_TYPE_FRONT_COVER) {
1367           break;
1368         }
1369       }
1370     }
1371     return isArtworkSet;
1372   }
1373 
1374   @RequiresNonNull("artworkView")
setDrawableArtwork(@ullable Drawable drawable)1375   private boolean setDrawableArtwork(@Nullable Drawable drawable) {
1376     if (drawable != null) {
1377       int drawableWidth = drawable.getIntrinsicWidth();
1378       int drawableHeight = drawable.getIntrinsicHeight();
1379       if (drawableWidth > 0 && drawableHeight > 0) {
1380         float artworkAspectRatio = (float) drawableWidth / drawableHeight;
1381         onContentAspectRatioChanged(artworkAspectRatio, contentFrame, artworkView);
1382         artworkView.setImageDrawable(drawable);
1383         artworkView.setVisibility(VISIBLE);
1384         return true;
1385       }
1386     }
1387     return false;
1388   }
1389 
hideArtwork()1390   private void hideArtwork() {
1391     if (artworkView != null) {
1392       artworkView.setImageResource(android.R.color.transparent); // Clears any bitmap reference.
1393       artworkView.setVisibility(INVISIBLE);
1394     }
1395   }
1396 
closeShutter()1397   private void closeShutter() {
1398     if (shutterView != null) {
1399       shutterView.setVisibility(View.VISIBLE);
1400     }
1401   }
1402 
updateBuffering()1403   private void updateBuffering() {
1404     if (bufferingView != null) {
1405       boolean showBufferingSpinner =
1406           player != null
1407               && player.getPlaybackState() == Player.STATE_BUFFERING
1408               && (showBuffering == SHOW_BUFFERING_ALWAYS
1409                   || (showBuffering == SHOW_BUFFERING_WHEN_PLAYING && player.getPlayWhenReady()));
1410       bufferingView.setVisibility(showBufferingSpinner ? View.VISIBLE : View.GONE);
1411     }
1412   }
1413 
updateErrorMessage()1414   private void updateErrorMessage() {
1415     if (errorMessageView != null) {
1416       if (customErrorMessage != null) {
1417         errorMessageView.setText(customErrorMessage);
1418         errorMessageView.setVisibility(View.VISIBLE);
1419         return;
1420       }
1421       @Nullable ExoPlaybackException error = player != null ? player.getPlayerError() : null;
1422       if (error != null && errorMessageProvider != null) {
1423         CharSequence errorMessage = errorMessageProvider.getErrorMessage(error).second;
1424         errorMessageView.setText(errorMessage);
1425         errorMessageView.setVisibility(View.VISIBLE);
1426       } else {
1427         errorMessageView.setVisibility(View.GONE);
1428       }
1429     }
1430   }
1431 
updateContentDescription()1432   private void updateContentDescription() {
1433     if (controller == null || !useController) {
1434       setContentDescription(/* contentDescription= */ null);
1435     } else if (controller.getVisibility() == View.VISIBLE) {
1436       setContentDescription(
1437           /* contentDescription= */ controllerHideOnTouch
1438               ? getResources().getString(R.string.exo_controls_hide)
1439               : null);
1440     } else {
1441       setContentDescription(
1442           /* contentDescription= */ getResources().getString(R.string.exo_controls_show));
1443     }
1444   }
1445 
updateControllerVisibility()1446   private void updateControllerVisibility() {
1447     if (isPlayingAd() && controllerHideDuringAds) {
1448       hideController();
1449     } else {
1450       maybeShowController(false);
1451     }
1452   }
1453 
1454   @RequiresApi(23)
configureEditModeLogoV23(Resources resources, ImageView logo)1455   private static void configureEditModeLogoV23(Resources resources, ImageView logo) {
1456     logo.setImageDrawable(resources.getDrawable(R.drawable.exo_edit_mode_logo, null));
1457     logo.setBackgroundColor(resources.getColor(R.color.exo_edit_mode_background_color, null));
1458   }
1459 
configureEditModeLogo(Resources resources, ImageView logo)1460   private static void configureEditModeLogo(Resources resources, ImageView logo) {
1461     logo.setImageDrawable(resources.getDrawable(R.drawable.exo_edit_mode_logo));
1462     logo.setBackgroundColor(resources.getColor(R.color.exo_edit_mode_background_color));
1463   }
1464 
1465   @SuppressWarnings("ResourceType")
setResizeModeRaw(AspectRatioFrameLayout aspectRatioFrame, int resizeMode)1466   private static void setResizeModeRaw(AspectRatioFrameLayout aspectRatioFrame, int resizeMode) {
1467     aspectRatioFrame.setResizeMode(resizeMode);
1468   }
1469 
1470   /** Applies a texture rotation to a {@link TextureView}. */
applyTextureViewRotation(TextureView textureView, int textureViewRotation)1471   private static void applyTextureViewRotation(TextureView textureView, int textureViewRotation) {
1472     Matrix transformMatrix = new Matrix();
1473     float textureViewWidth = textureView.getWidth();
1474     float textureViewHeight = textureView.getHeight();
1475     if (textureViewWidth != 0 && textureViewHeight != 0 && textureViewRotation != 0) {
1476       float pivotX = textureViewWidth / 2;
1477       float pivotY = textureViewHeight / 2;
1478       transformMatrix.postRotate(textureViewRotation, pivotX, pivotY);
1479 
1480       // After rotation, scale the rotated texture to fit the TextureView size.
1481       RectF originalTextureRect = new RectF(0, 0, textureViewWidth, textureViewHeight);
1482       RectF rotatedTextureRect = new RectF();
1483       transformMatrix.mapRect(rotatedTextureRect, originalTextureRect);
1484       transformMatrix.postScale(
1485           textureViewWidth / rotatedTextureRect.width(),
1486           textureViewHeight / rotatedTextureRect.height(),
1487           pivotX,
1488           pivotY);
1489     }
1490     textureView.setTransform(transformMatrix);
1491   }
1492 
1493   @SuppressLint("InlinedApi")
isDpadKey(int keyCode)1494   private boolean isDpadKey(int keyCode) {
1495     return keyCode == KeyEvent.KEYCODE_DPAD_UP
1496         || keyCode == KeyEvent.KEYCODE_DPAD_UP_RIGHT
1497         || keyCode == KeyEvent.KEYCODE_DPAD_RIGHT
1498         || keyCode == KeyEvent.KEYCODE_DPAD_DOWN_RIGHT
1499         || keyCode == KeyEvent.KEYCODE_DPAD_DOWN
1500         || keyCode == KeyEvent.KEYCODE_DPAD_DOWN_LEFT
1501         || keyCode == KeyEvent.KEYCODE_DPAD_LEFT
1502         || keyCode == KeyEvent.KEYCODE_DPAD_UP_LEFT
1503         || keyCode == KeyEvent.KEYCODE_DPAD_CENTER;
1504   }
1505 
1506   private final class ComponentListener
1507       implements Player.EventListener,
1508           TextOutput,
1509           VideoListener,
1510           OnLayoutChangeListener,
1511           SingleTapListener,
1512           PlayerControlView.VisibilityListener {
1513 
1514     // TextOutput implementation
1515 
1516     @Override
onCues(List<Cue> cues)1517     public void onCues(List<Cue> cues) {
1518       if (subtitleView != null) {
1519         subtitleView.onCues(cues);
1520       }
1521     }
1522 
1523     // VideoListener implementation
1524 
1525     @Override
onVideoSizeChanged( int width, int height, int unappliedRotationDegrees, float pixelWidthHeightRatio)1526     public void onVideoSizeChanged(
1527         int width, int height, int unappliedRotationDegrees, float pixelWidthHeightRatio) {
1528       float videoAspectRatio =
1529           (height == 0 || width == 0) ? 1 : (width * pixelWidthHeightRatio) / height;
1530 
1531       if (surfaceView instanceof TextureView) {
1532         // Try to apply rotation transformation when our surface is a TextureView.
1533         if (unappliedRotationDegrees == 90 || unappliedRotationDegrees == 270) {
1534           // We will apply a rotation 90/270 degree to the output texture of the TextureView.
1535           // In this case, the output video's width and height will be swapped.
1536           videoAspectRatio = 1 / videoAspectRatio;
1537         }
1538         if (textureViewRotation != 0) {
1539           surfaceView.removeOnLayoutChangeListener(this);
1540         }
1541         textureViewRotation = unappliedRotationDegrees;
1542         if (textureViewRotation != 0) {
1543           // The texture view's dimensions might be changed after layout step.
1544           // So add an OnLayoutChangeListener to apply rotation after layout step.
1545           surfaceView.addOnLayoutChangeListener(this);
1546         }
1547         applyTextureViewRotation((TextureView) surfaceView, textureViewRotation);
1548       }
1549 
1550       onContentAspectRatioChanged(videoAspectRatio, contentFrame, surfaceView);
1551     }
1552 
1553     @Override
onRenderedFirstFrame()1554     public void onRenderedFirstFrame() {
1555       if (shutterView != null) {
1556         shutterView.setVisibility(INVISIBLE);
1557       }
1558     }
1559 
1560     @Override
onTracksChanged(TrackGroupArray tracks, TrackSelectionArray selections)1561     public void onTracksChanged(TrackGroupArray tracks, TrackSelectionArray selections) {
1562       updateForCurrentTrackSelections(/* isNewPlayer= */ false);
1563     }
1564 
1565     // Player.EventListener implementation
1566 
1567     @Override
onPlaybackStateChanged(@layer.State int playbackState)1568     public void onPlaybackStateChanged(@Player.State int playbackState) {
1569       updateBuffering();
1570       updateErrorMessage();
1571       updateControllerVisibility();
1572     }
1573 
1574     @Override
onPlayWhenReadyChanged( boolean playWhenReady, @Player.PlayWhenReadyChangeReason int reason)1575     public void onPlayWhenReadyChanged(
1576         boolean playWhenReady, @Player.PlayWhenReadyChangeReason int reason) {
1577       updateBuffering();
1578       updateControllerVisibility();
1579     }
1580 
1581     @Override
onPositionDiscontinuity(@iscontinuityReason int reason)1582     public void onPositionDiscontinuity(@DiscontinuityReason int reason) {
1583       if (isPlayingAd() && controllerHideDuringAds) {
1584         hideController();
1585       }
1586     }
1587 
1588     // OnLayoutChangeListener implementation
1589 
1590     @Override
onLayoutChange( View view, int left, int top, int right, int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom)1591     public void onLayoutChange(
1592         View view,
1593         int left,
1594         int top,
1595         int right,
1596         int bottom,
1597         int oldLeft,
1598         int oldTop,
1599         int oldRight,
1600         int oldBottom) {
1601       applyTextureViewRotation((TextureView) view, textureViewRotation);
1602     }
1603 
1604     // SingleTapListener implementation
1605 
1606     @Override
onSingleTapUp(MotionEvent e)1607     public boolean onSingleTapUp(MotionEvent e) {
1608       return toggleControllerVisibility();
1609     }
1610 
1611     // PlayerControlView.VisibilityListener implementation
1612 
1613     @Override
onVisibilityChange(int visibility)1614     public void onVisibilityChange(int visibility) {
1615       updateContentDescription();
1616     }
1617   }
1618 }
1619