public class

PlayerView

extends FrameLayout

implements AdViewProvider

 java.lang.Object

↳FrameLayout

↳androidx.media3.ui.PlayerView

Gradle dependencies

compile group: 'androidx.media3', name: 'media3-ui', version: '1.0.0-alpha03'

  • groupId: androidx.media3
  • artifactId: media3-ui
  • version: 1.0.0-alpha03

Artifact androidx.media3:media3-ui:1.0.0-alpha03 it located at Google repository (https://maven.google.com/)

Overview

A high level view for Player media playbacks. It displays video, subtitles and album art during playback, and displays playback controls using a PlayerControlView.

A PlayerView can be customized by setting attributes (or calling corresponding methods), or overriding drawables.

Attributes

The following attributes can be set on a PlayerView when used in a layout XML file:
  • use_artwork - Whether artwork is used if available in audio streams.
  • default_artwork - Default artwork to use if no artwork available in audio streams.
  • use_controller - Whether the playback controls can be shown.
  • hide_on_touch - Whether the playback controls are hidden by touch events.
  • auto_show - Whether the playback controls are automatically shown when playback starts, pauses, ends, or fails. If set to false, the playback controls can be manually operated with PlayerView.showController() and PlayerView.hideController().
  • hide_during_ads - Whether the playback controls are hidden during ads. Controls are always shown during ads if they are enabled and the player is paused.
  • show_buffering - Whether the buffering spinner is displayed when the player is buffering. Valid values are never, when_playing and always.
  • resize_mode - Controls how video and album art is resized within the view. Valid values are fit, fixed_width, fixed_height, fill and zoom.
  • surface_type - The type of surface view used for video playbacks. Valid values are surface_view, texture_view, spherical_gl_surface_view, video_decoder_gl_surface_view and none. Using none is recommended for audio only applications, since creating the surface can be expensive. Using surface_view is recommended for video applications. Note, TextureView can only be used in a hardware accelerated window. When rendered in software, TextureView will draw nothing.
    • Corresponding method: None
    • Default: surface_view
  • shutter_background_color - The background color of the exo_shutter view.
  • keep_content_on_player_reset - Whether the currently displayed video frame or media artwork is kept visible when the player is reset.
  • All attributes that can be set on PlayerControlView and DefaultTimeBar can also be set on a PlayerView, and will be propagated to the inflated PlayerControlView unless the layout is overridden to specify a custom exo_controller.

Overriding drawables

The drawables used by PlayerControlView (with its default layout file) can be overridden by drawables with the same names defined in your application. See the PlayerControlView documentation for a list of drawables that can be overridden.

Summary

Fields
public static final intSHOW_BUFFERING_ALWAYS

The buffering view is always shown when the player is in the buffering state.

public static final intSHOW_BUFFERING_NEVER

The buffering view is never shown.

public static final intSHOW_BUFFERING_WHEN_PLAYING

The buffering view is shown when the player is in the buffering state and playWhenReady is true.

Constructors
publicPlayerView(Context context)

publicPlayerView(Context context, AttributeSet attrs)

publicPlayerView(Context context, AttributeSet attrs, int defStyleAttr)

Methods
public booleandispatchKeyEvent(KeyEvent event)

public booleandispatchMediaKeyEvent(KeyEvent event)

Called to process media key events.

public java.util.List<AdOverlayInfo>getAdOverlayInfos()

public ViewGroupgetAdViewGroup()

public booleangetControllerAutoShow()

Returns whether the playback controls are automatically shown when playback starts, pauses, ends, or fails.

public booleangetControllerHideOnTouch()

Returns whether the playback controls are hidden by touch events.

public intgetControllerShowTimeoutMs()

Returns the playback controls timeout.

public DrawablegetDefaultArtwork()

Returns the default artwork to display.

public FrameLayoutgetOverlayFrameLayout()

Gets the overlay FrameLayout, which can be populated with UI elements to show on top of the player.

public PlayergetPlayer()

Returns the player currently set on this view, or null if no player is set.

public intgetResizeMode()

Returns the AspectRatioFrameLayout.ResizeMode.

public SubtitleViewgetSubtitleView()

Gets the SubtitleView.

public booleangetUseArtwork()

Returns whether artwork is displayed if present in the media.

public booleangetUseController()

Returns whether the playback controls can be shown.

public ViewgetVideoSurfaceView()

Gets the view onto which video is rendered.

public voidhideController()

Hides the playback controls.

public booleanisControllerFullyVisible()

Returns whether the controller is currently fully visible.

protected voidonContentAspectRatioChanged(AspectRatioFrameLayout contentFrame, float aspectRatio)

Called when there's a change in the desired aspect ratio of the content frame.

public voidonPause()

Should be called when the player is no longer visible to the user, if the surface_type extends GLSurfaceView.

public voidonResume()

Should be called when the player is visible to the user, if the surface_type extends GLSurfaceView.

public booleanonTouchEvent(MotionEvent event)

public booleanonTrackballEvent(MotionEvent ev)

public booleanperformClick()

public voidsetAspectRatioListener(AspectRatioFrameLayout.AspectRatioListener listener)

Sets the AspectRatioFrameLayout.AspectRatioListener.

public voidsetControllerAutoShow(boolean controllerAutoShow)

Sets whether the playback controls are automatically shown when playback starts, pauses, ends, or fails.

public voidsetControllerHideDuringAds(boolean controllerHideDuringAds)

Sets whether the playback controls are hidden when ads are playing.

public voidsetControllerHideOnTouch(boolean controllerHideOnTouch)

Sets whether the playback controls are hidden by touch events.

public voidsetControllerOnFullScreenModeChangedListener(PlayerControlView.OnFullScreenModeChangedListener listener)

Sets the PlayerControlView.OnFullScreenModeChangedListener.

public voidsetControllerShowTimeoutMs(int controllerShowTimeoutMs)

Sets the playback controls timeout.

public voidsetControllerVisibilityListener(PlayerControlView.VisibilityListener listener)

Sets the PlayerControlView.VisibilityListener.

public voidsetCustomErrorMessage(java.lang.CharSequence message)

Sets a custom error message to be displayed by the view.

public voidsetDefaultArtwork(Drawable defaultArtwork)

Sets the default artwork to display if useArtwork is true and no artwork is present in the media.

public voidsetErrorMessageProvider(ErrorMessageProvider<java.lang.Throwable> errorMessageProvider)

Sets the optional ErrorMessageProvider.

public voidsetExtraAdGroupMarkers(long[] extraAdGroupTimesMs[], boolean[] extraPlayedAdGroups[])

Sets the millisecond positions of extra ad markers relative to the start of the window (or timeline, if in multi-window mode) and whether each extra ad has been played or not.

public voidsetKeepContentOnPlayerReset(boolean keepContentOnPlayerReset)

Sets whether the currently displayed video frame or media artwork is kept visible when the player is reset.

public voidsetPlayer(Player player)

Sets the Player to use.

public voidsetRepeatToggleModes(int repeatToggleModes)

Sets which repeat toggle modes are enabled.

public voidsetResizeMode(int resizeMode)

Sets the AspectRatioFrameLayout.ResizeMode.

public voidsetShowBuffering(int showBuffering)

Sets whether a buffering spinner is displayed when the player is in the buffering state.

public voidsetShowFastForwardButton(boolean showFastForwardButton)

Sets whether the fast forward button is shown.

public voidsetShowMultiWindowTimeBar(boolean showMultiWindowTimeBar)

Sets whether the time bar should show all windows, as opposed to just the current one.

public voidsetShowNextButton(boolean showNextButton)

Sets whether the next button is shown.

public voidsetShowPreviousButton(boolean showPreviousButton)

Sets whether the previous button is shown.

public voidsetShowRewindButton(boolean showRewindButton)

Sets whether the rewind button is shown.

public voidsetShowShuffleButton(boolean showShuffleButton)

Sets whether the shuffle button is shown.

public voidsetShowSubtitleButton(boolean showSubtitleButton)

Sets whether the subtitle button is shown.

public voidsetShowVrButton(boolean showVrButton)

Sets whether the vr button is shown.

public voidsetShutterBackgroundColor(int color)

Sets the background color of the exo_shutter view.

public voidsetUseArtwork(boolean useArtwork)

Sets whether artwork is displayed if present in the media.

public voidsetUseController(boolean useController)

Sets whether the playback controls can be shown.

public voidsetVisibility(int visibility)

public voidshowController()

Shows the playback controls.

public static voidswitchTargetView(Player player, PlayerView oldPlayerView, PlayerView newPlayerView)

Switches the view targeted by a given Player.

from java.lang.Objectclone, equals, finalize, getClass, hashCode, notify, notifyAll, toString, wait, wait, wait

Fields

public static final int SHOW_BUFFERING_NEVER

The buffering view is never shown.

public static final int SHOW_BUFFERING_WHEN_PLAYING

The buffering view is shown when the player is in the buffering state and playWhenReady is true.

public static final int SHOW_BUFFERING_ALWAYS

The buffering view is always shown when the player is in the buffering state.

Constructors

public PlayerView(Context context)

public PlayerView(Context context, AttributeSet attrs)

public PlayerView(Context context, AttributeSet attrs, int defStyleAttr)

Methods

public static void switchTargetView(Player player, PlayerView oldPlayerView, PlayerView newPlayerView)

Switches the view targeted by a given Player.

Parameters:

player: The player whose target view is being switched.
oldPlayerView: The old view to detach from the player.
newPlayerView: The new view to attach to the player.

public Player getPlayer()

Returns the player currently set on this view, or null if no player is set.

public void setPlayer(Player player)

Sets the Player to use.

To transition a Player from targeting one view to another, it's recommended to use PlayerView.switchTargetView(Player, PlayerView, PlayerView) rather than this method. If you do wish to use this method directly, be sure to attach the player to the new view before calling setPlayer(null) to detach it from the old one. This ordering is significantly more efficient and may allow for more seamless transitions.

Parameters:

player: The Player to use, or null to detach the current player. Only players which are accessed on the main thread are supported (player.getApplicationLooper() == Looper.getMainLooper()).

public void setVisibility(int visibility)

public void setResizeMode(int resizeMode)

Sets the AspectRatioFrameLayout.ResizeMode.

Parameters:

resizeMode: The AspectRatioFrameLayout.ResizeMode.

public int getResizeMode()

Returns the AspectRatioFrameLayout.ResizeMode.

public boolean getUseArtwork()

Returns whether artwork is displayed if present in the media.

public void setUseArtwork(boolean useArtwork)

Sets whether artwork is displayed if present in the media.

Parameters:

useArtwork: Whether artwork is displayed.

public Drawable getDefaultArtwork()

Returns the default artwork to display.

public void setDefaultArtwork(Drawable defaultArtwork)

Sets the default artwork to display if useArtwork is true and no artwork is present in the media.

Parameters:

defaultArtwork: the default artwork to display

public boolean getUseController()

Returns whether the playback controls can be shown.

public void setUseController(boolean useController)

Sets whether the playback controls can be shown. If set to false the playback controls are never visible and are disconnected from the player.

Parameters:

useController: Whether the playback controls can be shown.

public void setShutterBackgroundColor(int color)

Sets the background color of the exo_shutter view.

Parameters:

color: The background color.

public void setKeepContentOnPlayerReset(boolean keepContentOnPlayerReset)

Sets whether the currently displayed video frame or media artwork is kept visible when the player is reset. A player reset is defined to mean the player being re-prepared with different media, the player transitioning to unprepared media or an empty list of media items, or the player being replaced or cleared by calling PlayerView.setPlayer(Player).

If enabled, the currently displayed video frame or media artwork will be kept visible until the player set on the view has been successfully prepared with new media and loaded enough of it to have determined the available tracks. Hence enabling this option allows transitioning from playing one piece of media to another, or from using one player instance to another, without clearing the view's content.

If disabled, the currently displayed video frame or media artwork will be hidden as soon as the player is reset. Note that the video frame is hidden by making exo_shutter visible. Hence the video frame will not be hidden if using a custom layout that omits this view.

Parameters:

keepContentOnPlayerReset: Whether the currently displayed video frame or media artwork is kept visible when the player is reset.

public void setShowBuffering(int showBuffering)

Sets whether a buffering spinner is displayed when the player is in the buffering state. The buffering spinner is not displayed by default.

Parameters:

showBuffering: The mode that defines when the buffering spinner is displayed. One of PlayerView.SHOW_BUFFERING_NEVER, PlayerView.SHOW_BUFFERING_WHEN_PLAYING and PlayerView.SHOW_BUFFERING_ALWAYS.

public void setErrorMessageProvider(ErrorMessageProvider<java.lang.Throwable> errorMessageProvider)

Sets the optional ErrorMessageProvider.

Parameters:

errorMessageProvider: The error message provider.

public void setCustomErrorMessage(java.lang.CharSequence message)

Sets a custom error message to be displayed by the view. The error message will be displayed permanently, unless it is cleared by passing null to this method.

Parameters:

message: The message to display, or null to clear a previously set message.

public boolean dispatchKeyEvent(KeyEvent event)

public boolean dispatchMediaKeyEvent(KeyEvent event)

Called to process media key events. Any can be passed but only media key events will be handled. Does nothing if playback controls are disabled.

Parameters:

event: A key event.

Returns:

Whether the key event was handled.

public boolean isControllerFullyVisible()

Returns whether the controller is currently fully visible.

public void showController()

Shows the playback controls. Does nothing if playback controls are disabled.

The playback controls are automatically hidden during playback after {PlayerView.getControllerShowTimeoutMs()}. They are shown indefinitely when playback has not started yet, is paused, has ended or failed.

public void hideController()

Hides the playback controls. Does nothing if playback controls are disabled.

public int getControllerShowTimeoutMs()

Returns the playback controls timeout. The playback controls are automatically hidden after this duration of time has elapsed without user input and with playback or buffering in progress.

Returns:

The timeout in milliseconds. A non-positive value will cause the controller to remain visible indefinitely.

public void setControllerShowTimeoutMs(int controllerShowTimeoutMs)

Sets the playback controls timeout. The playback controls are automatically hidden after this duration of time has elapsed without user input and with playback or buffering in progress.

Parameters:

controllerShowTimeoutMs: The timeout in milliseconds. A non-positive value will cause the controller to remain visible indefinitely.

public boolean getControllerHideOnTouch()

Returns whether the playback controls are hidden by touch events.

public void setControllerHideOnTouch(boolean controllerHideOnTouch)

Sets whether the playback controls are hidden by touch events.

Parameters:

controllerHideOnTouch: Whether the playback controls are hidden by touch events.

public boolean getControllerAutoShow()

Returns whether the playback controls are automatically shown when playback starts, pauses, ends, or fails. If set to false, the playback controls can be manually operated with PlayerView.showController() and PlayerView.hideController().

public void setControllerAutoShow(boolean controllerAutoShow)

Sets whether the playback controls are automatically shown when playback starts, pauses, ends, or fails. If set to false, the playback controls can be manually operated with PlayerView.showController() and PlayerView.hideController().

Parameters:

controllerAutoShow: Whether the playback controls are allowed to show automatically.

public void setControllerHideDuringAds(boolean controllerHideDuringAds)

Sets whether the playback controls are hidden when ads are playing. Controls are always shown during ads if they are enabled and the player is paused.

Parameters:

controllerHideDuringAds: Whether the playback controls are hidden when ads are playing.

public void setControllerVisibilityListener(PlayerControlView.VisibilityListener listener)

Sets the PlayerControlView.VisibilityListener.

Parameters:

listener: The listener to be notified about visibility changes, or null to remove the current listener.

public void setControllerOnFullScreenModeChangedListener(PlayerControlView.OnFullScreenModeChangedListener listener)

Sets the PlayerControlView.OnFullScreenModeChangedListener.

Parameters:

listener: The listener to be notified when the fullscreen button is clicked, or null to remove the current listener and hide the fullscreen button.

public void setShowRewindButton(boolean showRewindButton)

Sets whether the rewind button is shown.

Parameters:

showRewindButton: Whether the rewind button is shown.

public void setShowFastForwardButton(boolean showFastForwardButton)

Sets whether the fast forward button is shown.

Parameters:

showFastForwardButton: Whether the fast forward button is shown.

public void setShowPreviousButton(boolean showPreviousButton)

Sets whether the previous button is shown.

Parameters:

showPreviousButton: Whether the previous button is shown.

public void setShowNextButton(boolean showNextButton)

Sets whether the next button is shown.

Parameters:

showNextButton: Whether the next button is shown.

public void setRepeatToggleModes(int repeatToggleModes)

Sets which repeat toggle modes are enabled.

Parameters:

repeatToggleModes: A set of .

public void setShowShuffleButton(boolean showShuffleButton)

Sets whether the shuffle button is shown.

Parameters:

showShuffleButton: Whether the shuffle button is shown.

public void setShowSubtitleButton(boolean showSubtitleButton)

Sets whether the subtitle button is shown.

Parameters:

showSubtitleButton: Whether the subtitle button is shown.

public void setShowVrButton(boolean showVrButton)

Sets whether the vr button is shown.

Parameters:

showVrButton: Whether the vr button is shown.

public void setShowMultiWindowTimeBar(boolean showMultiWindowTimeBar)

Sets whether the time bar should show all windows, as opposed to just the current one.

Parameters:

showMultiWindowTimeBar: Whether to show all windows.

public void setExtraAdGroupMarkers(long[] extraAdGroupTimesMs[], boolean[] extraPlayedAdGroups[])

Sets the millisecond positions of extra ad markers relative to the start of the window (or timeline, if in multi-window mode) and whether each extra ad has been played or not. The markers are shown in addition to any ad markers for ads in the player's timeline.

Parameters:

extraAdGroupTimesMs: The millisecond timestamps of the extra ad markers to show, or null to show no extra ad markers.
extraPlayedAdGroups: Whether each ad has been played, or null to show no extra ad markers.

public void setAspectRatioListener(AspectRatioFrameLayout.AspectRatioListener listener)

Sets the AspectRatioFrameLayout.AspectRatioListener.

Parameters:

listener: The listener to be notified about aspect ratios changes of the video content or the content frame.

public View getVideoSurfaceView()

Gets the view onto which video is rendered. This is a:

  • by default, or if the surface_type attribute is set to surface_view.
  • TextureView if surface_type is texture_view.
  • SphericalGLSurfaceView if surface_type is spherical_gl_surface_view.
  • VideoDecoderGLSurfaceView if surface_type is video_decoder_gl_surface_view.
  • null if surface_type is none.

Returns:

The , TextureView, SphericalGLSurfaceView, VideoDecoderGLSurfaceView or null.

public FrameLayout getOverlayFrameLayout()

Gets the overlay FrameLayout, which can be populated with UI elements to show on top of the player.

Returns:

The overlay FrameLayout, or null if the layout has been customized and the overlay is not present.

public SubtitleView getSubtitleView()

Gets the SubtitleView.

Returns:

The SubtitleView, or null if the layout has been customized and the subtitle view is not present.

public boolean onTouchEvent(MotionEvent event)

public boolean performClick()

public boolean onTrackballEvent(MotionEvent ev)

public void onResume()

Should be called when the player is visible to the user, if the surface_type extends GLSurfaceView. It is the counterpart to PlayerView.onPause().

This method should typically be called in Activity.onStart(), or Activity.onResume() for API versions <= 23.

public void onPause()

Should be called when the player is no longer visible to the user, if the surface_type extends GLSurfaceView. It is the counterpart to PlayerView.onResume().

This method should typically be called in Activity.onStop(), or Activity.onPause() for API versions <= 23.

protected void onContentAspectRatioChanged(AspectRatioFrameLayout contentFrame, float aspectRatio)

Called when there's a change in the desired aspect ratio of the content frame. The default implementation sets the aspect ratio of the content frame to the specified value.

Parameters:

contentFrame: The content frame, or null.
aspectRatio: The aspect ratio to apply.

public ViewGroup getAdViewGroup()

public java.util.List<AdOverlayInfo> getAdOverlayInfos()

Source

/*
 * Copyright 2019 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package androidx.media3.ui;

import static androidx.media3.common.Player.COMMAND_GET_TEXT;
import static androidx.media3.common.Player.COMMAND_SET_VIDEO_SURFACE;
import static androidx.media3.common.util.Assertions.checkNotNull;
import static java.lang.annotation.ElementType.TYPE_USE;

import android.annotation.SuppressLint;
import android.content.Context;
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Matrix;
import android.graphics.RectF;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.opengl.GLSurfaceView;
import android.os.Looper;
import android.util.AttributeSet;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.SurfaceView;
import android.view.TextureView;
import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.annotation.IntDef;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.core.content.ContextCompat;
import androidx.media3.common.AdOverlayInfo;
import androidx.media3.common.AdViewProvider;
import androidx.media3.common.C;
import androidx.media3.common.ErrorMessageProvider;
import androidx.media3.common.MediaMetadata;
import androidx.media3.common.PlaybackException;
import androidx.media3.common.Player;
import androidx.media3.common.Player.DiscontinuityReason;
import androidx.media3.common.Timeline;
import androidx.media3.common.Timeline.Period;
import androidx.media3.common.TracksInfo;
import androidx.media3.common.VideoSize;
import androidx.media3.common.text.Cue;
import androidx.media3.common.util.Assertions;
import androidx.media3.common.util.RepeatModeUtil;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.common.util.Util;
import androidx.media3.ui.AspectRatioFrameLayout.ResizeMode;
import com.google.common.collect.ImmutableList;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.ArrayList;
import java.util.List;
import org.checkerframework.checker.nullness.qual.EnsuresNonNullIf;
import org.checkerframework.checker.nullness.qual.RequiresNonNull;

/**
 * A high level view for {@link Player} media playbacks. It displays video, subtitles and album art
 * during playback, and displays playback controls using a {@link PlayerControlView}.
 *
 * <p>A PlayerView can be customized by setting attributes (or calling corresponding methods), or
 * overriding drawables.
 *
 * <h2>Attributes</h2>
 *
 * The following attributes can be set on a PlayerView when used in a layout XML file:
 *
 * <ul>
 *   <li><b>{@code use_artwork}</b> - Whether artwork is used if available in audio streams.
 *       <ul>
 *         <li>Corresponding method: {@link #setUseArtwork(boolean)}
 *         <li>Default: {@code true}
 *       </ul>
 *   <li><b>{@code default_artwork}</b> - Default artwork to use if no artwork available in audio
 *       streams.
 *       <ul>
 *         <li>Corresponding method: {@link #setDefaultArtwork(Drawable)}
 *         <li>Default: {@code null}
 *       </ul>
 *   <li><b>{@code use_controller}</b> - Whether the playback controls can be shown.
 *       <ul>
 *         <li>Corresponding method: {@link #setUseController(boolean)}
 *         <li>Default: {@code true}
 *       </ul>
 *   <li><b>{@code hide_on_touch}</b> - Whether the playback controls are hidden by touch events.
 *       <ul>
 *         <li>Corresponding method: {@link #setControllerHideOnTouch(boolean)}
 *         <li>Default: {@code true}
 *       </ul>
 *   <li><b>{@code auto_show}</b> - Whether the playback controls are automatically shown when
 *       playback starts, pauses, ends, or fails. If set to false, the playback controls can be
 *       manually operated with {@link #showController()} and {@link #hideController()}.
 *       <ul>
 *         <li>Corresponding method: {@link #setControllerAutoShow(boolean)}
 *         <li>Default: {@code true}
 *       </ul>
 *   <li><b>{@code hide_during_ads}</b> - Whether the playback controls are hidden during ads.
 *       Controls are always shown during ads if they are enabled and the player is paused.
 *       <ul>
 *         <li>Corresponding method: {@link #setControllerHideDuringAds(boolean)}
 *         <li>Default: {@code true}
 *       </ul>
 *   <li><b>{@code show_buffering}</b> - Whether the buffering spinner is displayed when the player
 *       is buffering. Valid values are {@code never}, {@code when_playing} and {@code always}.
 *       <ul>
 *         <li>Corresponding method: {@link #setShowBuffering(int)}
 *         <li>Default: {@code never}
 *       </ul>
 *   <li><b>{@code resize_mode}</b> - Controls how video and album art is resized within the view.
 *       Valid values are {@code fit}, {@code fixed_width}, {@code fixed_height}, {@code fill} and
 *       {@code zoom}.
 *       <ul>
 *         <li>Corresponding method: {@link #setResizeMode(int)}
 *         <li>Default: {@code fit}
 *       </ul>
 *   <li><b>{@code surface_type}</b> - The type of surface view used for video playbacks. Valid
 *       values are {@code surface_view}, {@code texture_view}, {@code spherical_gl_surface_view},
 *       {@code video_decoder_gl_surface_view} and {@code none}. Using {@code none} is recommended
 *       for audio only applications, since creating the surface can be expensive. Using {@code
 *       surface_view} is recommended for video applications. Note, TextureView can only be used in
 *       a hardware accelerated window. When rendered in software, TextureView will draw nothing.
 *       <ul>
 *         <li>Corresponding method: None
 *         <li>Default: {@code surface_view}
 *       </ul>
 *   <li><b>{@code shutter_background_color}</b> - The background color of the {@code exo_shutter}
 *       view.
 *       <ul>
 *         <li>Corresponding method: {@link #setShutterBackgroundColor(int)}
 *         <li>Default: {@code unset}
 *       </ul>
 *   <li><b>{@code keep_content_on_player_reset}</b> - Whether the currently displayed video frame
 *       or media artwork is kept visible when the player is reset.
 *       <ul>
 *         <li>Corresponding method: {@link #setKeepContentOnPlayerReset(boolean)}
 *         <li>Default: {@code false}
 *       </ul>
 *   <li>All attributes that can be set on {@link PlayerControlView} and {@link DefaultTimeBar} can
 *       also be set on a PlayerView, and will be propagated to the inflated {@link
 *       PlayerControlView} unless the layout is overridden to specify a custom {@code
 *       exo_controller}.
 * </ul>
 *
 * <h2>Overriding drawables</h2>
 *
 * The drawables used by {@link PlayerControlView} (with its default layout file) can be overridden
 * by drawables with the same names defined in your application. See the {@link PlayerControlView}
 * documentation for a list of drawables that can be overridden.
 */
@UnstableApi
public class PlayerView extends FrameLayout implements AdViewProvider {

  /**
   * Determines when the buffering view is shown. One of {@link #SHOW_BUFFERING_NEVER}, {@link
   * #SHOW_BUFFERING_WHEN_PLAYING} or {@link #SHOW_BUFFERING_ALWAYS}.
   */
  @Documented
  @Retention(RetentionPolicy.SOURCE)
  @Target(TYPE_USE)
  @IntDef({SHOW_BUFFERING_NEVER, SHOW_BUFFERING_WHEN_PLAYING, SHOW_BUFFERING_ALWAYS})
  public @interface ShowBuffering {}
  /** The buffering view is never shown. */
  public static final int SHOW_BUFFERING_NEVER = 0;
  /**
   * The buffering view is shown when the player is in the {@link Player#STATE_BUFFERING buffering}
   * state and {@link Player#getPlayWhenReady() playWhenReady} is {@code true}.
   */
  public static final int SHOW_BUFFERING_WHEN_PLAYING = 1;
  /**
   * The buffering view is always shown when the player is in the {@link Player#STATE_BUFFERING
   * buffering} state.
   */
  public static final int SHOW_BUFFERING_ALWAYS = 2;

  private static final int SURFACE_TYPE_NONE = 0;
  private static final int SURFACE_TYPE_SURFACE_VIEW = 1;
  private static final int SURFACE_TYPE_TEXTURE_VIEW = 2;
  private static final int SURFACE_TYPE_SPHERICAL_GL_SURFACE_VIEW = 3;
  private static final int SURFACE_TYPE_VIDEO_DECODER_GL_SURFACE_VIEW = 4;

  private final ComponentListener componentListener;
  @Nullable private final AspectRatioFrameLayout contentFrame;
  @Nullable private final View shutterView;
  @Nullable private final View surfaceView;
  private final boolean surfaceViewIgnoresVideoAspectRatio;
  @Nullable private final ImageView artworkView;
  @Nullable private final SubtitleView subtitleView;
  @Nullable private final View bufferingView;
  @Nullable private final TextView errorMessageView;
  @Nullable private final PlayerControlView controller;
  @Nullable private final FrameLayout adOverlayFrameLayout;
  @Nullable private final FrameLayout overlayFrameLayout;

  @Nullable private Player player;
  private boolean useController;
  @Nullable private PlayerControlView.VisibilityListener controllerVisibilityListener;
  private boolean useArtwork;
  @Nullable private Drawable defaultArtwork;
  private @ShowBuffering int showBuffering;
  private boolean keepContentOnPlayerReset;
  @Nullable private ErrorMessageProvider<? super PlaybackException> errorMessageProvider;
  @Nullable private CharSequence customErrorMessage;
  private int controllerShowTimeoutMs;
  private boolean controllerAutoShow;
  private boolean controllerHideDuringAds;
  private boolean controllerHideOnTouch;
  private int textureViewRotation;
  private boolean isTouching;
  private static final int PICTURE_TYPE_FRONT_COVER = 3;
  private static final int PICTURE_TYPE_NOT_SET = -1;

  public PlayerView(Context context) {
    this(context, /* attrs= */ null);
  }

  public PlayerView(Context context, @Nullable AttributeSet attrs) {
    this(context, attrs, /* defStyleAttr= */ 0);
  }

  @SuppressWarnings({"nullness:argument", "nullness:method.invocation"})
  public PlayerView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);

    componentListener = new ComponentListener();

    if (isInEditMode()) {
      contentFrame = null;
      shutterView = null;
      surfaceView = null;
      surfaceViewIgnoresVideoAspectRatio = false;
      artworkView = null;
      subtitleView = null;
      bufferingView = null;
      errorMessageView = null;
      controller = null;
      adOverlayFrameLayout = null;
      overlayFrameLayout = null;
      ImageView logo = new ImageView(context);
      if (Util.SDK_INT >= 23) {
        configureEditModeLogoV23(getResources(), logo);
      } else {
        configureEditModeLogo(getResources(), logo);
      }
      addView(logo);
      return;
    }

    boolean shutterColorSet = false;
    int shutterColor = 0;
    int playerLayoutId = R.layout.exo_player_view;
    boolean useArtwork = true;
    int defaultArtworkId = 0;
    boolean useController = true;
    int surfaceType = SURFACE_TYPE_SURFACE_VIEW;
    int resizeMode = AspectRatioFrameLayout.RESIZE_MODE_FIT;
    int controllerShowTimeoutMs = PlayerControlView.DEFAULT_SHOW_TIMEOUT_MS;
    boolean controllerHideOnTouch = true;
    boolean controllerAutoShow = true;
    boolean controllerHideDuringAds = true;
    int showBuffering = SHOW_BUFFERING_NEVER;
    if (attrs != null) {
      TypedArray a =
          context
              .getTheme()
              .obtainStyledAttributes(
                  attrs, R.styleable.PlayerView, defStyleAttr, /* defStyleRes= */ 0);
      try {
        shutterColorSet = a.hasValue(R.styleable.PlayerView_shutter_background_color);
        shutterColor = a.getColor(R.styleable.PlayerView_shutter_background_color, shutterColor);
        playerLayoutId = a.getResourceId(R.styleable.PlayerView_player_layout_id, playerLayoutId);
        useArtwork = a.getBoolean(R.styleable.PlayerView_use_artwork, useArtwork);
        defaultArtworkId =
            a.getResourceId(R.styleable.PlayerView_default_artwork, defaultArtworkId);
        useController = a.getBoolean(R.styleable.PlayerView_use_controller, useController);
        surfaceType = a.getInt(R.styleable.PlayerView_surface_type, surfaceType);
        resizeMode = a.getInt(R.styleable.PlayerView_resize_mode, resizeMode);
        controllerShowTimeoutMs =
            a.getInt(R.styleable.PlayerView_show_timeout, controllerShowTimeoutMs);
        controllerHideOnTouch =
            a.getBoolean(R.styleable.PlayerView_hide_on_touch, controllerHideOnTouch);
        controllerAutoShow = a.getBoolean(R.styleable.PlayerView_auto_show, controllerAutoShow);
        showBuffering = a.getInteger(R.styleable.PlayerView_show_buffering, showBuffering);
        keepContentOnPlayerReset =
            a.getBoolean(
                R.styleable.PlayerView_keep_content_on_player_reset, keepContentOnPlayerReset);
        controllerHideDuringAds =
            a.getBoolean(R.styleable.PlayerView_hide_during_ads, controllerHideDuringAds);
      } finally {
        a.recycle();
      }
    }

    LayoutInflater.from(context).inflate(playerLayoutId, this);
    setDescendantFocusability(FOCUS_AFTER_DESCENDANTS);

    // Content frame.
    contentFrame = findViewById(R.id.exo_content_frame);
    if (contentFrame != null) {
      setResizeModeRaw(contentFrame, resizeMode);
    }

    // Shutter view.
    shutterView = findViewById(R.id.exo_shutter);
    if (shutterView != null && shutterColorSet) {
      shutterView.setBackgroundColor(shutterColor);
    }

    // Create a surface view and insert it into the content frame, if there is one.
    boolean surfaceViewIgnoresVideoAspectRatio = false;
    if (contentFrame != null && surfaceType != SURFACE_TYPE_NONE) {
      ViewGroup.LayoutParams params =
          new ViewGroup.LayoutParams(
              ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
      switch (surfaceType) {
        case SURFACE_TYPE_TEXTURE_VIEW:
          surfaceView = new TextureView(context);
          break;
        case SURFACE_TYPE_SPHERICAL_GL_SURFACE_VIEW:
          try {
            Class<?> clazz =
                Class.forName("androidx.media3.exoplayer.video.spherical.SphericalGLSurfaceView");
            surfaceView = (View) clazz.getConstructor(Context.class).newInstance(context);
          } catch (Exception e) {
            throw new IllegalStateException(
                "spherical_gl_surface_view requires an ExoPlayer dependency", e);
          }
          surfaceViewIgnoresVideoAspectRatio = true;
          break;
        case SURFACE_TYPE_VIDEO_DECODER_GL_SURFACE_VIEW:
          try {
            Class<?> clazz =
                Class.forName("androidx.media3.exoplayer.video.VideoDecoderGLSurfaceView");
            surfaceView = (View) clazz.getConstructor(Context.class).newInstance(context);
          } catch (Exception e) {
            throw new IllegalStateException(
                "video_decoder_gl_surface_view requires an ExoPlayer dependency", e);
          }
          break;
        default:
          surfaceView = new SurfaceView(context);
          break;
      }
      surfaceView.setLayoutParams(params);
      // We don't want surfaceView to be clickable separately to the PlayerView itself, but we
      // do want to register as an OnClickListener so that surfaceView implementations can propagate
      // click events up to the PlayerView by calling their own performClick method.
      surfaceView.setOnClickListener(componentListener);
      surfaceView.setClickable(false);
      contentFrame.addView(surfaceView, 0);
    } else {
      surfaceView = null;
    }
    this.surfaceViewIgnoresVideoAspectRatio = surfaceViewIgnoresVideoAspectRatio;

    // Ad overlay frame layout.
    adOverlayFrameLayout = findViewById(R.id.exo_ad_overlay);

    // Overlay frame layout.
    overlayFrameLayout = findViewById(R.id.exo_overlay);

    // Artwork view.
    artworkView = findViewById(R.id.exo_artwork);
    this.useArtwork = useArtwork && artworkView != null;
    if (defaultArtworkId != 0) {
      defaultArtwork = ContextCompat.getDrawable(getContext(), defaultArtworkId);
    }

    // Subtitle view.
    subtitleView = findViewById(R.id.exo_subtitles);
    if (subtitleView != null) {
      subtitleView.setUserDefaultStyle();
      subtitleView.setUserDefaultTextSize();
    }

    // Buffering view.
    bufferingView = findViewById(R.id.exo_buffering);
    if (bufferingView != null) {
      bufferingView.setVisibility(View.GONE);
    }
    this.showBuffering = showBuffering;

    // Error message view.
    errorMessageView = findViewById(R.id.exo_error_message);
    if (errorMessageView != null) {
      errorMessageView.setVisibility(View.GONE);
    }

    // Playback control view.
    PlayerControlView customController = findViewById(R.id.exo_controller);
    View controllerPlaceholder = findViewById(R.id.exo_controller_placeholder);
    if (customController != null) {
      this.controller = customController;
    } else if (controllerPlaceholder != null) {
      // Propagate attrs as playbackAttrs so that PlayerControlView's custom attributes are
      // transferred, but standard attributes (e.g. background) are not.
      this.controller = new PlayerControlView(context, null, 0, attrs);
      controller.setId(R.id.exo_controller);
      controller.setLayoutParams(controllerPlaceholder.getLayoutParams());
      ViewGroup parent = ((ViewGroup) controllerPlaceholder.getParent());
      int controllerIndex = parent.indexOfChild(controllerPlaceholder);
      parent.removeView(controllerPlaceholder);
      parent.addView(controller, controllerIndex);
    } else {
      this.controller = null;
    }
    this.controllerShowTimeoutMs = controller != null ? controllerShowTimeoutMs : 0;
    this.controllerHideOnTouch = controllerHideOnTouch;
    this.controllerAutoShow = controllerAutoShow;
    this.controllerHideDuringAds = controllerHideDuringAds;
    this.useController = useController && controller != null;
    if (controller != null) {
      controller.hideImmediately();
      controller.addVisibilityListener(/* listener= */ componentListener);
    }
    updateContentDescription();
  }

  /**
   * Switches the view targeted by a given {@link Player}.
   *
   * @param player The player whose target view is being switched.
   * @param oldPlayerView The old view to detach from the player.
   * @param newPlayerView The new view to attach to the player.
   */
  public static void switchTargetView(
      Player player, @Nullable PlayerView oldPlayerView, @Nullable PlayerView newPlayerView) {
    if (oldPlayerView == newPlayerView) {
      return;
    }
    // We attach the new view before detaching the old one because this ordering allows the player
    // to swap directly from one surface to another, without transitioning through a state where no
    // surface is attached. This is significantly more efficient and achieves a more seamless
    // transition when using platform provided video decoders.
    if (newPlayerView != null) {
      newPlayerView.setPlayer(player);
    }
    if (oldPlayerView != null) {
      oldPlayerView.setPlayer(null);
    }
  }

  /** Returns the player currently set on this view, or null if no player is set. */
  @Nullable
  public Player getPlayer() {
    return player;
  }

  /**
   * Sets the {@link Player} to use.
   *
   * <p>To transition a {@link Player} from targeting one view to another, it's recommended to use
   * {@link #switchTargetView(Player, PlayerView, PlayerView)} rather than this method. If you do
   * wish to use this method directly, be sure to attach the player to the new view <em>before</em>
   * calling {@code setPlayer(null)} to detach it from the old one. This ordering is significantly
   * more efficient and may allow for more seamless transitions.
   *
   * @param player The {@link Player} to use, or {@code null} to detach the current player. Only
   *     players which are accessed on the main thread are supported ({@code
   *     player.getApplicationLooper() == Looper.getMainLooper()}).
   */
  public void setPlayer(@Nullable Player player) {
    Assertions.checkState(Looper.myLooper() == Looper.getMainLooper());
    Assertions.checkArgument(
        player == null || player.getApplicationLooper() == Looper.getMainLooper());
    if (this.player == player) {
      return;
    }
    @Nullable Player oldPlayer = this.player;
    if (oldPlayer != null) {
      oldPlayer.removeListener(componentListener);
      if (surfaceView instanceof TextureView) {
        oldPlayer.clearVideoTextureView((TextureView) surfaceView);
      } else if (surfaceView instanceof SurfaceView) {
        oldPlayer.clearVideoSurfaceView((SurfaceView) surfaceView);
      }
    }
    if (subtitleView != null) {
      subtitleView.setCues(null);
    }
    this.player = player;
    if (useController()) {
      controller.setPlayer(player);
    }
    updateBuffering();
    updateErrorMessage();
    updateForCurrentTrackSelections(/* isNewPlayer= */ true);
    if (player != null) {
      if (player.isCommandAvailable(COMMAND_SET_VIDEO_SURFACE)) {
        if (surfaceView instanceof TextureView) {
          player.setVideoTextureView((TextureView) surfaceView);
        } else if (surfaceView instanceof SurfaceView) {
          player.setVideoSurfaceView((SurfaceView) surfaceView);
        }
        updateAspectRatio();
      }
      if (subtitleView != null && player.isCommandAvailable(COMMAND_GET_TEXT)) {
        subtitleView.setCues(player.getCurrentCues());
      }
      player.addListener(componentListener);
      maybeShowController(false);
    } else {
      hideController();
    }
  }

  @Override
  public void setVisibility(int visibility) {
    super.setVisibility(visibility);
    if (surfaceView instanceof SurfaceView) {
      // Work around https://github.com/google/ExoPlayer/issues/3160.
      surfaceView.setVisibility(visibility);
    }
  }

  /**
   * Sets the {@link ResizeMode}.
   *
   * @param resizeMode The {@link ResizeMode}.
   */
  public void setResizeMode(@ResizeMode int resizeMode) {
    Assertions.checkStateNotNull(contentFrame);
    contentFrame.setResizeMode(resizeMode);
  }

  /** Returns the {@link ResizeMode}. */
  public @ResizeMode int getResizeMode() {
    Assertions.checkStateNotNull(contentFrame);
    return contentFrame.getResizeMode();
  }

  /** Returns whether artwork is displayed if present in the media. */
  public boolean getUseArtwork() {
    return useArtwork;
  }

  /**
   * Sets whether artwork is displayed if present in the media.
   *
   * @param useArtwork Whether artwork is displayed.
   */
  public void setUseArtwork(boolean useArtwork) {
    Assertions.checkState(!useArtwork || artworkView != null);
    if (this.useArtwork != useArtwork) {
      this.useArtwork = useArtwork;
      updateForCurrentTrackSelections(/* isNewPlayer= */ false);
    }
  }

  /** Returns the default artwork to display. */
  @Nullable
  public Drawable getDefaultArtwork() {
    return defaultArtwork;
  }

  /**
   * Sets the default artwork to display if {@code useArtwork} is {@code true} and no artwork is
   * present in the media.
   *
   * @param defaultArtwork the default artwork to display
   */
  public void setDefaultArtwork(@Nullable Drawable defaultArtwork) {
    if (this.defaultArtwork != defaultArtwork) {
      this.defaultArtwork = defaultArtwork;
      updateForCurrentTrackSelections(/* isNewPlayer= */ false);
    }
  }

  /** Returns whether the playback controls can be shown. */
  public boolean getUseController() {
    return useController;
  }

  /**
   * Sets whether the playback controls can be shown. If set to {@code false} the playback controls
   * are never visible and are disconnected from the player.
   *
   * @param useController Whether the playback controls can be shown.
   */
  public void setUseController(boolean useController) {
    Assertions.checkState(!useController || controller != null);
    if (this.useController == useController) {
      return;
    }
    this.useController = useController;
    if (useController()) {
      controller.setPlayer(player);
    } else if (controller != null) {
      controller.hide();
      controller.setPlayer(/* player= */ null);
    }
    updateContentDescription();
  }

  /**
   * Sets the background color of the {@code exo_shutter} view.
   *
   * @param color The background color.
   */
  public void setShutterBackgroundColor(int color) {
    if (shutterView != null) {
      shutterView.setBackgroundColor(color);
    }
  }

  /**
   * Sets whether the currently displayed video frame or media artwork is kept visible when the
   * player is reset. A player reset is defined to mean the player being re-prepared with different
   * media, the player transitioning to unprepared media or an empty list of media items, or the
   * player being replaced or cleared by calling {@link #setPlayer(Player)}.
   *
   * <p>If enabled, the currently displayed video frame or media artwork will be kept visible until
   * the player set on the view has been successfully prepared with new media and loaded enough of
   * it to have determined the available tracks. Hence enabling this option allows transitioning
   * from playing one piece of media to another, or from using one player instance to another,
   * without clearing the view's content.
   *
   * <p>If disabled, the currently displayed video frame or media artwork will be hidden as soon as
   * the player is reset. Note that the video frame is hidden by making {@code exo_shutter} visible.
   * Hence the video frame will not be hidden if using a custom layout that omits this view.
   *
   * @param keepContentOnPlayerReset Whether the currently displayed video frame or media artwork is
   *     kept visible when the player is reset.
   */
  public void setKeepContentOnPlayerReset(boolean keepContentOnPlayerReset) {
    if (this.keepContentOnPlayerReset != keepContentOnPlayerReset) {
      this.keepContentOnPlayerReset = keepContentOnPlayerReset;
      updateForCurrentTrackSelections(/* isNewPlayer= */ false);
    }
  }

  /**
   * Sets whether a buffering spinner is displayed when the player is in the buffering state. The
   * buffering spinner is not displayed by default.
   *
   * @param showBuffering The mode that defines when the buffering spinner is displayed. One of
   *     {@link #SHOW_BUFFERING_NEVER}, {@link #SHOW_BUFFERING_WHEN_PLAYING} and {@link
   *     #SHOW_BUFFERING_ALWAYS}.
   */
  public void setShowBuffering(@ShowBuffering int showBuffering) {
    if (this.showBuffering != showBuffering) {
      this.showBuffering = showBuffering;
      updateBuffering();
    }
  }

  /**
   * Sets the optional {@link ErrorMessageProvider}.
   *
   * @param errorMessageProvider The error message provider.
   */
  public void setErrorMessageProvider(
      @Nullable ErrorMessageProvider<? super PlaybackException> errorMessageProvider) {
    if (this.errorMessageProvider != errorMessageProvider) {
      this.errorMessageProvider = errorMessageProvider;
      updateErrorMessage();
    }
  }

  /**
   * Sets a custom error message to be displayed by the view. The error message will be displayed
   * permanently, unless it is cleared by passing {@code null} to this method.
   *
   * @param message The message to display, or {@code null} to clear a previously set message.
   */
  public void setCustomErrorMessage(@Nullable CharSequence message) {
    Assertions.checkState(errorMessageView != null);
    customErrorMessage = message;
    updateErrorMessage();
  }

  @Override
  public boolean dispatchKeyEvent(KeyEvent event) {
    if (player != null && player.isPlayingAd()) {
      return super.dispatchKeyEvent(event);
    }

    boolean isDpadKey = isDpadKey(event.getKeyCode());
    boolean handled = false;
    if (isDpadKey && useController() && !controller.isFullyVisible()) {
      // Handle the key event by showing the controller.
      maybeShowController(true);
      handled = true;
    } else if (dispatchMediaKeyEvent(event) || super.dispatchKeyEvent(event)) {
      // The key event was handled as a media key or by the super class. We should also show the
      // controller, or extend its show timeout if already visible.
      maybeShowController(true);
      handled = true;
    } else if (isDpadKey && useController()) {
      // The key event wasn't handled, but we should extend the controller's show timeout.
      maybeShowController(true);
    }
    return handled;
  }

  /**
   * Called to process media key events. Any {@link KeyEvent} can be passed but only media key
   * events will be handled. Does nothing if playback controls are disabled.
   *
   * @param event A key event.
   * @return Whether the key event was handled.
   */
  public boolean dispatchMediaKeyEvent(KeyEvent event) {
    return useController() && controller.dispatchMediaKeyEvent(event);
  }

  /** Returns whether the controller is currently fully visible. */
  public boolean isControllerFullyVisible() {
    return controller != null && controller.isFullyVisible();
  }

  /**
   * Shows the playback controls. Does nothing if playback controls are disabled.
   *
   * <p>The playback controls are automatically hidden during playback after {{@link
   * #getControllerShowTimeoutMs()}}. They are shown indefinitely when playback has not started yet,
   * is paused, has ended or failed.
   */
  public void showController() {
    showController(shouldShowControllerIndefinitely());
  }

  /** Hides the playback controls. Does nothing if playback controls are disabled. */
  public void hideController() {
    if (controller != null) {
      controller.hide();
    }
  }

  /**
   * Returns the playback controls timeout. The playback controls are automatically hidden after
   * this duration of time has elapsed without user input and with playback or buffering in
   * progress.
   *
   * @return The timeout in milliseconds. A non-positive value will cause the controller to remain
   *     visible indefinitely.
   */
  public int getControllerShowTimeoutMs() {
    return controllerShowTimeoutMs;
  }

  /**
   * Sets the playback controls timeout. The playback controls are automatically hidden after this
   * duration of time has elapsed without user input and with playback or buffering in progress.
   *
   * @param controllerShowTimeoutMs The timeout in milliseconds. A non-positive value will cause the
   *     controller to remain visible indefinitely.
   */
  public void setControllerShowTimeoutMs(int controllerShowTimeoutMs) {
    Assertions.checkStateNotNull(controller);
    this.controllerShowTimeoutMs = controllerShowTimeoutMs;
    if (controller.isFullyVisible()) {
      // Update the controller's timeout if necessary.
      showController();
    }
  }

  /** Returns whether the playback controls are hidden by touch events. */
  public boolean getControllerHideOnTouch() {
    return controllerHideOnTouch;
  }

  /**
   * Sets whether the playback controls are hidden by touch events.
   *
   * @param controllerHideOnTouch Whether the playback controls are hidden by touch events.
   */
  public void setControllerHideOnTouch(boolean controllerHideOnTouch) {
    Assertions.checkStateNotNull(controller);
    this.controllerHideOnTouch = controllerHideOnTouch;
    updateContentDescription();
  }

  /**
   * Returns whether the playback controls are automatically shown when playback starts, pauses,
   * ends, or fails. If set to false, the playback controls can be manually operated with {@link
   * #showController()} and {@link #hideController()}.
   */
  public boolean getControllerAutoShow() {
    return controllerAutoShow;
  }

  /**
   * Sets whether the playback controls are automatically shown when playback starts, pauses, ends,
   * or fails. If set to false, the playback controls can be manually operated with {@link
   * #showController()} and {@link #hideController()}.
   *
   * @param controllerAutoShow Whether the playback controls are allowed to show automatically.
   */
  public void setControllerAutoShow(boolean controllerAutoShow) {
    this.controllerAutoShow = controllerAutoShow;
  }

  /**
   * Sets whether the playback controls are hidden when ads are playing. Controls are always shown
   * during ads if they are enabled and the player is paused.
   *
   * @param controllerHideDuringAds Whether the playback controls are hidden when ads are playing.
   */
  public void setControllerHideDuringAds(boolean controllerHideDuringAds) {
    this.controllerHideDuringAds = controllerHideDuringAds;
  }

  /**
   * Sets the {@link PlayerControlView.VisibilityListener}.
   *
   * @param listener The listener to be notified about visibility changes, or null to remove the
   *     current listener.
   */
  public void setControllerVisibilityListener(
      @Nullable PlayerControlView.VisibilityListener listener) {
    Assertions.checkStateNotNull(controller);
    if (this.controllerVisibilityListener == listener) {
      return;
    }
    if (this.controllerVisibilityListener != null) {
      controller.removeVisibilityListener(this.controllerVisibilityListener);
    }
    this.controllerVisibilityListener = listener;
    if (listener != null) {
      controller.addVisibilityListener(listener);
    }
  }

  /**
   * Sets the {@link PlayerControlView.OnFullScreenModeChangedListener}.
   *
   * @param listener The listener to be notified when the fullscreen button is clicked, or null to
   *     remove the current listener and hide the fullscreen button.
   */
  public void setControllerOnFullScreenModeChangedListener(
      @Nullable PlayerControlView.OnFullScreenModeChangedListener listener) {
    Assertions.checkStateNotNull(controller);
    controller.setOnFullScreenModeChangedListener(listener);
  }

  /**
   * Sets whether the rewind button is shown.
   *
   * @param showRewindButton Whether the rewind button is shown.
   */
  public void setShowRewindButton(boolean showRewindButton) {
    Assertions.checkStateNotNull(controller);
    controller.setShowRewindButton(showRewindButton);
  }

  /**
   * Sets whether the fast forward button is shown.
   *
   * @param showFastForwardButton Whether the fast forward button is shown.
   */
  public void setShowFastForwardButton(boolean showFastForwardButton) {
    Assertions.checkStateNotNull(controller);
    controller.setShowFastForwardButton(showFastForwardButton);
  }

  /**
   * Sets whether the previous button is shown.
   *
   * @param showPreviousButton Whether the previous button is shown.
   */
  public void setShowPreviousButton(boolean showPreviousButton) {
    Assertions.checkStateNotNull(controller);
    controller.setShowPreviousButton(showPreviousButton);
  }

  /**
   * Sets whether the next button is shown.
   *
   * @param showNextButton Whether the next button is shown.
   */
  public void setShowNextButton(boolean showNextButton) {
    Assertions.checkStateNotNull(controller);
    controller.setShowNextButton(showNextButton);
  }

  /**
   * Sets which repeat toggle modes are enabled.
   *
   * @param repeatToggleModes A set of {@link RepeatModeUtil.RepeatToggleModes}.
   */
  public void setRepeatToggleModes(@RepeatModeUtil.RepeatToggleModes int repeatToggleModes) {
    Assertions.checkStateNotNull(controller);
    controller.setRepeatToggleModes(repeatToggleModes);
  }

  /**
   * Sets whether the shuffle button is shown.
   *
   * @param showShuffleButton Whether the shuffle button is shown.
   */
  public void setShowShuffleButton(boolean showShuffleButton) {
    Assertions.checkStateNotNull(controller);
    controller.setShowShuffleButton(showShuffleButton);
  }

  /**
   * Sets whether the subtitle button is shown.
   *
   * @param showSubtitleButton Whether the subtitle button is shown.
   */
  public void setShowSubtitleButton(boolean showSubtitleButton) {
    Assertions.checkStateNotNull(controller);
    controller.setShowSubtitleButton(showSubtitleButton);
  }

  /**
   * Sets whether the vr button is shown.
   *
   * @param showVrButton Whether the vr button is shown.
   */
  public void setShowVrButton(boolean showVrButton) {
    Assertions.checkStateNotNull(controller);
    controller.setShowVrButton(showVrButton);
  }

  /**
   * Sets whether the time bar should show all windows, as opposed to just the current one.
   *
   * @param showMultiWindowTimeBar Whether to show all windows.
   */
  public void setShowMultiWindowTimeBar(boolean showMultiWindowTimeBar) {
    Assertions.checkStateNotNull(controller);
    controller.setShowMultiWindowTimeBar(showMultiWindowTimeBar);
  }

  /**
   * Sets the millisecond positions of extra ad markers relative to the start of the window (or
   * timeline, if in multi-window mode) and whether each extra ad has been played or not. The
   * markers are shown in addition to any ad markers for ads in the player's timeline.
   *
   * @param extraAdGroupTimesMs The millisecond timestamps of the extra ad markers to show, or
   *     {@code null} to show no extra ad markers.
   * @param extraPlayedAdGroups Whether each ad has been played, or {@code null} to show no extra ad
   *     markers.
   */
  public void setExtraAdGroupMarkers(
      @Nullable long[] extraAdGroupTimesMs, @Nullable boolean[] extraPlayedAdGroups) {
    Assertions.checkStateNotNull(controller);
    controller.setExtraAdGroupMarkers(extraAdGroupTimesMs, extraPlayedAdGroups);
  }

  /**
   * Sets the {@link AspectRatioFrameLayout.AspectRatioListener}.
   *
   * @param listener The listener to be notified about aspect ratios changes of the video content or
   *     the content frame.
   */
  public void setAspectRatioListener(
      @Nullable AspectRatioFrameLayout.AspectRatioListener listener) {
    Assertions.checkStateNotNull(contentFrame);
    contentFrame.setAspectRatioListener(listener);
  }

  /**
   * Gets the view onto which video is rendered. This is a:
   *
   * <ul>
   *   <li>{@link SurfaceView} by default, or if the {@code surface_type} attribute is set to {@code
   *       surface_view}.
   *   <li>{@link TextureView} if {@code surface_type} is {@code texture_view}.
   *   <li>{@code SphericalGLSurfaceView} if {@code surface_type} is {@code
   *       spherical_gl_surface_view}.
   *   <li>{@code VideoDecoderGLSurfaceView} if {@code surface_type} is {@code
   *       video_decoder_gl_surface_view}.
   *   <li>{@code null} if {@code surface_type} is {@code none}.
   * </ul>
   *
   * @return The {@link SurfaceView}, {@link TextureView}, {@code SphericalGLSurfaceView}, {@code
   *     VideoDecoderGLSurfaceView} or {@code null}.
   */
  @Nullable
  public View getVideoSurfaceView() {
    return surfaceView;
  }

  /**
   * Gets the overlay {@link FrameLayout}, which can be populated with UI elements to show on top of
   * the player.
   *
   * @return The overlay {@link FrameLayout}, or {@code null} if the layout has been customized and
   *     the overlay is not present.
   */
  @Nullable
  public FrameLayout getOverlayFrameLayout() {
    return overlayFrameLayout;
  }

  /**
   * Gets the {@link SubtitleView}.
   *
   * @return The {@link SubtitleView}, or {@code null} if the layout has been customized and the
   *     subtitle view is not present.
   */
  @Nullable
  public SubtitleView getSubtitleView() {
    return subtitleView;
  }

  @Override
  public boolean onTouchEvent(MotionEvent event) {
    if (!useController() || player == null) {
      return false;
    }
    switch (event.getAction()) {
      case MotionEvent.ACTION_DOWN:
        isTouching = true;
        return true;
      case MotionEvent.ACTION_UP:
        if (isTouching) {
          isTouching = false;
          return performClick();
        }
        return false;
      default:
        return false;
    }
  }

  @Override
  public boolean performClick() {
    super.performClick();
    return toggleControllerVisibility();
  }

  @Override
  public boolean onTrackballEvent(MotionEvent ev) {
    if (!useController() || player == null) {
      return false;
    }
    maybeShowController(true);
    return true;
  }

  /**
   * Should be called when the player is visible to the user, if the {@code surface_type} extends
   * {@link GLSurfaceView}. It is the counterpart to {@link #onPause()}.
   *
   * <p>This method should typically be called in {@code Activity.onStart()}, or {@code
   * Activity.onResume()} for API versions &lt;= 23.
   */
  public void onResume() {
    if (surfaceView instanceof GLSurfaceView) {
      ((GLSurfaceView) surfaceView).onResume();
    }
  }

  /**
   * Should be called when the player is no longer visible to the user, if the {@code surface_type}
   * extends {@link GLSurfaceView}. It is the counterpart to {@link #onResume()}.
   *
   * <p>This method should typically be called in {@code Activity.onStop()}, or {@code
   * Activity.onPause()} for API versions &lt;= 23.
   */
  public void onPause() {
    if (surfaceView instanceof GLSurfaceView) {
      ((GLSurfaceView) surfaceView).onPause();
    }
  }

  /**
   * Called when there's a change in the desired aspect ratio of the content frame. The default
   * implementation sets the aspect ratio of the content frame to the specified value.
   *
   * @param contentFrame The content frame, or {@code null}.
   * @param aspectRatio The aspect ratio to apply.
   */
  protected void onContentAspectRatioChanged(
      @Nullable AspectRatioFrameLayout contentFrame, float aspectRatio) {
    if (contentFrame != null) {
      contentFrame.setAspectRatio(aspectRatio);
    }
  }

  // AdsLoader.AdViewProvider implementation.

  @Override
  public ViewGroup getAdViewGroup() {
    return Assertions.checkStateNotNull(
        adOverlayFrameLayout, "exo_ad_overlay must be present for ad playback");
  }

  @Override
  public List<AdOverlayInfo> getAdOverlayInfos() {
    List<AdOverlayInfo> overlayViews = new ArrayList<>();
    if (overlayFrameLayout != null) {
      overlayViews.add(
          new AdOverlayInfo(
              overlayFrameLayout,
              AdOverlayInfo.PURPOSE_NOT_VISIBLE,
              /* detailedReason= */ "Transparent overlay does not impact viewability"));
    }
    if (controller != null) {
      overlayViews.add(new AdOverlayInfo(controller, AdOverlayInfo.PURPOSE_CONTROLS));
    }
    return ImmutableList.copyOf(overlayViews);
  }

  // Internal methods.

  @EnsuresNonNullIf(expression = "controller", result = true)
  private boolean useController() {
    if (useController) {
      Assertions.checkStateNotNull(controller);
      return true;
    }
    return false;
  }

  @EnsuresNonNullIf(expression = "artworkView", result = true)
  private boolean useArtwork() {
    if (useArtwork) {
      Assertions.checkStateNotNull(artworkView);
      return true;
    }
    return false;
  }

  private boolean toggleControllerVisibility() {
    if (!useController() || player == null) {
      return false;
    }
    if (!controller.isFullyVisible()) {
      maybeShowController(true);
      return true;
    } else if (controllerHideOnTouch) {
      controller.hide();
      return true;
    }
    return false;
  }

  /** Shows the playback controls, but only if forced or shown indefinitely. */
  private void maybeShowController(boolean isForced) {
    if (isPlayingAd() && controllerHideDuringAds) {
      return;
    }
    if (useController()) {
      boolean wasShowingIndefinitely =
          controller.isFullyVisible() && controller.getShowTimeoutMs() <= 0;
      boolean shouldShowIndefinitely = shouldShowControllerIndefinitely();
      if (isForced || wasShowingIndefinitely || shouldShowIndefinitely) {
        showController(shouldShowIndefinitely);
      }
    }
  }

  private boolean shouldShowControllerIndefinitely() {
    if (player == null) {
      return true;
    }
    int playbackState = player.getPlaybackState();
    return controllerAutoShow
        && !player.getCurrentTimeline().isEmpty()
        && (playbackState == Player.STATE_IDLE
            || playbackState == Player.STATE_ENDED
            || !checkNotNull(player).getPlayWhenReady());
  }

  private void showController(boolean showIndefinitely) {
    if (!useController()) {
      return;
    }
    controller.setShowTimeoutMs(showIndefinitely ? 0 : controllerShowTimeoutMs);
    controller.show();
  }

  private boolean isPlayingAd() {
    return player != null && player.isPlayingAd() && player.getPlayWhenReady();
  }

  private void updateForCurrentTrackSelections(boolean isNewPlayer) {
    @Nullable Player player = this.player;
    if (player == null || player.getCurrentTracksInfo().getTrackGroupInfos().isEmpty()) {
      if (!keepContentOnPlayerReset) {
        hideArtwork();
        closeShutter();
      }
      return;
    }

    if (isNewPlayer && !keepContentOnPlayerReset) {
      // Hide any video from the previous player.
      closeShutter();
    }

    if (player.getCurrentTracksInfo().isTypeSelected(C.TRACK_TYPE_VIDEO)) {
      // Video enabled, so artwork must be hidden. If the shutter is closed, it will be opened
      // in onRenderedFirstFrame().
      hideArtwork();
      return;
    }

    // Video disabled so the shutter must be closed.
    closeShutter();
    // Display artwork if enabled and available, else hide it.
    if (useArtwork()) {
      if (setArtworkFromMediaMetadata(player.getMediaMetadata())) {
        return;
      }
      if (setDrawableArtwork(defaultArtwork)) {
        return;
      }
    }
    // Artwork disabled or unavailable.
    hideArtwork();
  }

  @RequiresNonNull("artworkView")
  private boolean setArtworkFromMediaMetadata(MediaMetadata mediaMetadata) {
    if (mediaMetadata.artworkData == null) {
      return false;
    }
    Bitmap bitmap =
        BitmapFactory.decodeByteArray(
            mediaMetadata.artworkData, /* offset= */ 0, mediaMetadata.artworkData.length);
    return setDrawableArtwork(new BitmapDrawable(getResources(), bitmap));
  }

  @RequiresNonNull("artworkView")
  private boolean setDrawableArtwork(@Nullable Drawable drawable) {
    if (drawable != null) {
      int drawableWidth = drawable.getIntrinsicWidth();
      int drawableHeight = drawable.getIntrinsicHeight();
      if (drawableWidth > 0 && drawableHeight > 0) {
        float artworkAspectRatio = (float) drawableWidth / drawableHeight;
        onContentAspectRatioChanged(contentFrame, artworkAspectRatio);
        artworkView.setImageDrawable(drawable);
        artworkView.setVisibility(VISIBLE);
        return true;
      }
    }
    return false;
  }

  private void hideArtwork() {
    if (artworkView != null) {
      artworkView.setImageResource(android.R.color.transparent); // Clears any bitmap reference.
      artworkView.setVisibility(INVISIBLE);
    }
  }

  private void closeShutter() {
    if (shutterView != null) {
      shutterView.setVisibility(View.VISIBLE);
    }
  }

  private void updateBuffering() {
    if (bufferingView != null) {
      boolean showBufferingSpinner =
          player != null
              && player.getPlaybackState() == Player.STATE_BUFFERING
              && (showBuffering == SHOW_BUFFERING_ALWAYS
                  || (showBuffering == SHOW_BUFFERING_WHEN_PLAYING && player.getPlayWhenReady()));
      bufferingView.setVisibility(showBufferingSpinner ? View.VISIBLE : View.GONE);
    }
  }

  private void updateErrorMessage() {
    if (errorMessageView != null) {
      if (customErrorMessage != null) {
        errorMessageView.setText(customErrorMessage);
        errorMessageView.setVisibility(View.VISIBLE);
        return;
      }
      @Nullable PlaybackException error = player != null ? player.getPlayerError() : null;
      if (error != null && errorMessageProvider != null) {
        CharSequence errorMessage = errorMessageProvider.getErrorMessage(error).second;
        errorMessageView.setText(errorMessage);
        errorMessageView.setVisibility(View.VISIBLE);
      } else {
        errorMessageView.setVisibility(View.GONE);
      }
    }
  }

  private void updateContentDescription() {
    if (controller == null || !useController) {
      setContentDescription(/* contentDescription= */ null);
    } else if (controller.isFullyVisible()) {
      setContentDescription(
          /* contentDescription= */ controllerHideOnTouch
              ? getResources().getString(R.string.exo_controls_hide)
              : null);
    } else {
      setContentDescription(
          /* contentDescription= */ getResources().getString(R.string.exo_controls_show));
    }
  }

  private void updateControllerVisibility() {
    if (isPlayingAd() && controllerHideDuringAds) {
      hideController();
    } else {
      maybeShowController(false);
    }
  }

  private void updateAspectRatio() {
    VideoSize videoSize = player != null ? player.getVideoSize() : VideoSize.UNKNOWN;
    int width = videoSize.width;
    int height = videoSize.height;
    int unappliedRotationDegrees = videoSize.unappliedRotationDegrees;
    float videoAspectRatio =
        (height == 0 || width == 0) ? 0 : (width * videoSize.pixelWidthHeightRatio) / height;

    if (surfaceView instanceof TextureView) {
      // Try to apply rotation transformation when our surface is a TextureView.
      if (videoAspectRatio > 0
          && (unappliedRotationDegrees == 90 || unappliedRotationDegrees == 270)) {
        // We will apply a rotation 90/270 degree to the output texture of the TextureView.
        // In this case, the output video's width and height will be swapped.
        videoAspectRatio = 1 / videoAspectRatio;
      }
      if (textureViewRotation != 0) {
        surfaceView.removeOnLayoutChangeListener(componentListener);
      }
      textureViewRotation = unappliedRotationDegrees;
      if (textureViewRotation != 0) {
        // The texture view's dimensions might be changed after layout step.
        // So add an OnLayoutChangeListener to apply rotation after layout step.
        surfaceView.addOnLayoutChangeListener(componentListener);
      }
      applyTextureViewRotation((TextureView) surfaceView, textureViewRotation);
    }

    onContentAspectRatioChanged(
        contentFrame, surfaceViewIgnoresVideoAspectRatio ? 0 : videoAspectRatio);
  }

  @RequiresApi(23)
  private static void configureEditModeLogoV23(Resources resources, ImageView logo) {
    logo.setImageDrawable(resources.getDrawable(R.drawable.exo_edit_mode_logo, null));
    logo.setBackgroundColor(resources.getColor(R.color.exo_edit_mode_background_color, null));
  }

  private static void configureEditModeLogo(Resources resources, ImageView logo) {
    logo.setImageDrawable(resources.getDrawable(R.drawable.exo_edit_mode_logo));
    logo.setBackgroundColor(resources.getColor(R.color.exo_edit_mode_background_color));
  }

  @SuppressWarnings("ResourceType")
  private static void setResizeModeRaw(AspectRatioFrameLayout aspectRatioFrame, int resizeMode) {
    aspectRatioFrame.setResizeMode(resizeMode);
  }

  /** Applies a texture rotation to a {@link TextureView}. */
  private static void applyTextureViewRotation(TextureView textureView, int textureViewRotation) {
    Matrix transformMatrix = new Matrix();
    float textureViewWidth = textureView.getWidth();
    float textureViewHeight = textureView.getHeight();
    if (textureViewWidth != 0 && textureViewHeight != 0 && textureViewRotation != 0) {
      float pivotX = textureViewWidth / 2;
      float pivotY = textureViewHeight / 2;
      transformMatrix.postRotate(textureViewRotation, pivotX, pivotY);

      // After rotation, scale the rotated texture to fit the TextureView size.
      RectF originalTextureRect = new RectF(0, 0, textureViewWidth, textureViewHeight);
      RectF rotatedTextureRect = new RectF();
      transformMatrix.mapRect(rotatedTextureRect, originalTextureRect);
      transformMatrix.postScale(
          textureViewWidth / rotatedTextureRect.width(),
          textureViewHeight / rotatedTextureRect.height(),
          pivotX,
          pivotY);
    }
    textureView.setTransform(transformMatrix);
  }

  @SuppressLint("InlinedApi")
  private boolean isDpadKey(int keyCode) {
    return keyCode == KeyEvent.KEYCODE_DPAD_UP
        || keyCode == KeyEvent.KEYCODE_DPAD_UP_RIGHT
        || keyCode == KeyEvent.KEYCODE_DPAD_RIGHT
        || keyCode == KeyEvent.KEYCODE_DPAD_DOWN_RIGHT
        || keyCode == KeyEvent.KEYCODE_DPAD_DOWN
        || keyCode == KeyEvent.KEYCODE_DPAD_DOWN_LEFT
        || keyCode == KeyEvent.KEYCODE_DPAD_LEFT
        || keyCode == KeyEvent.KEYCODE_DPAD_UP_LEFT
        || keyCode == KeyEvent.KEYCODE_DPAD_CENTER;
  }

  private final class ComponentListener
      implements Player.Listener,
          OnLayoutChangeListener,
          OnClickListener,
          PlayerControlView.VisibilityListener {

    private final Period period;
    private @Nullable Object lastPeriodUidWithTracks;

    public ComponentListener() {
      period = new Period();
    }

    // Player.Listener implementation

    @Override
    public void onCues(List<Cue> cues) {
      if (subtitleView != null) {
        subtitleView.setCues(cues);
      }
    }

    @Override
    public void onVideoSizeChanged(VideoSize videoSize) {
      updateAspectRatio();
    }

    @Override
    public void onRenderedFirstFrame() {
      if (shutterView != null) {
        shutterView.setVisibility(INVISIBLE);
      }
    }

    @Override
    public void onTracksInfoChanged(TracksInfo tracksInfo) {
      // Suppress the update if transitioning to an unprepared period within the same window. This
      // is necessary to avoid closing the shutter when such a transition occurs. See:
      // https://github.com/google/ExoPlayer/issues/5507.
      Player player = checkNotNull(PlayerView.this.player);
      Timeline timeline = player.getCurrentTimeline();
      if (timeline.isEmpty()) {
        lastPeriodUidWithTracks = null;
      } else if (!player.getCurrentTracksInfo().getTrackGroupInfos().isEmpty()) {
        lastPeriodUidWithTracks =
            timeline.getPeriod(player.getCurrentPeriodIndex(), period, /* setIds= */ true).uid;
      } else if (lastPeriodUidWithTracks != null) {
        int lastPeriodIndexWithTracks = timeline.getIndexOfPeriod(lastPeriodUidWithTracks);
        if (lastPeriodIndexWithTracks != C.INDEX_UNSET) {
          int lastWindowIndexWithTracks =
              timeline.getPeriod(lastPeriodIndexWithTracks, period).windowIndex;
          if (player.getCurrentMediaItemIndex() == lastWindowIndexWithTracks) {
            // We're in the same media item. Suppress the update.
            return;
          }
        }
        lastPeriodUidWithTracks = null;
      }

      updateForCurrentTrackSelections(/* isNewPlayer= */ false);
    }

    @Override
    public void onPlaybackStateChanged(@Player.State int playbackState) {
      updateBuffering();
      updateErrorMessage();
      updateControllerVisibility();
    }

    @Override
    public void onPlayWhenReadyChanged(
        boolean playWhenReady, @Player.PlayWhenReadyChangeReason int reason) {
      updateBuffering();
      updateControllerVisibility();
    }

    @Override
    public void onPositionDiscontinuity(
        Player.PositionInfo oldPosition,
        Player.PositionInfo newPosition,
        @DiscontinuityReason int reason) {
      if (isPlayingAd() && controllerHideDuringAds) {
        hideController();
      }
    }

    // OnLayoutChangeListener implementation

    @Override
    public void onLayoutChange(
        View view,
        int left,
        int top,
        int right,
        int bottom,
        int oldLeft,
        int oldTop,
        int oldRight,
        int oldBottom) {
      applyTextureViewRotation((TextureView) view, textureViewRotation);
    }

    // OnClickListener implementation

    @Override
    public void onClick(View view) {
      toggleControllerVisibility();
    }

    // PlayerControlView.VisibilityListener implementation

    @Override
    public void onVisibilityChange(int visibility) {
      updateContentDescription();
    }
  }
}