PlayerControlView.java

/*
 * 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_CURRENT_MEDIA_ITEM;
import static androidx.media3.common.Player.COMMAND_GET_TIMELINE;
import static androidx.media3.common.Player.COMMAND_GET_TRACKS;
import static androidx.media3.common.Player.COMMAND_PLAY_PAUSE;
import static androidx.media3.common.Player.COMMAND_PREPARE;
import static androidx.media3.common.Player.COMMAND_SEEK_BACK;
import static androidx.media3.common.Player.COMMAND_SEEK_FORWARD;
import static androidx.media3.common.Player.COMMAND_SEEK_IN_CURRENT_MEDIA_ITEM;
import static androidx.media3.common.Player.COMMAND_SEEK_TO_DEFAULT_POSITION;
import static androidx.media3.common.Player.COMMAND_SEEK_TO_MEDIA_ITEM;
import static androidx.media3.common.Player.COMMAND_SEEK_TO_NEXT;
import static androidx.media3.common.Player.COMMAND_SEEK_TO_PREVIOUS;
import static androidx.media3.common.Player.COMMAND_SET_REPEAT_MODE;
import static androidx.media3.common.Player.COMMAND_SET_SHUFFLE_MODE;
import static androidx.media3.common.Player.COMMAND_SET_SPEED_AND_PITCH;
import static androidx.media3.common.Player.COMMAND_SET_TRACK_SELECTION_PARAMETERS;
import static androidx.media3.common.Player.EVENT_AVAILABLE_COMMANDS_CHANGED;
import static androidx.media3.common.Player.EVENT_IS_PLAYING_CHANGED;
import static androidx.media3.common.Player.EVENT_PLAYBACK_PARAMETERS_CHANGED;
import static androidx.media3.common.Player.EVENT_PLAYBACK_STATE_CHANGED;
import static androidx.media3.common.Player.EVENT_PLAY_WHEN_READY_CHANGED;
import static androidx.media3.common.Player.EVENT_POSITION_DISCONTINUITY;
import static androidx.media3.common.Player.EVENT_REPEAT_MODE_CHANGED;
import static androidx.media3.common.Player.EVENT_SEEK_BACK_INCREMENT_CHANGED;
import static androidx.media3.common.Player.EVENT_SEEK_FORWARD_INCREMENT_CHANGED;
import static androidx.media3.common.Player.EVENT_SHUFFLE_MODE_ENABLED_CHANGED;
import static androidx.media3.common.Player.EVENT_TIMELINE_CHANGED;
import static androidx.media3.common.Player.EVENT_TRACKS_CHANGED;
import static androidx.media3.common.util.Assertions.checkNotNull;
import static androidx.media3.common.util.Util.castNonNull;
import static androidx.media3.common.util.Util.getDrawable;
import static androidx.media3.common.util.Util.msToUs;

import android.annotation.SuppressLint;
import android.content.Context;
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.graphics.Color;
import android.graphics.Typeface;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.os.Looper;
import android.util.AttributeSet;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.PopupWindow;
import android.widget.TextView;
import androidx.annotation.DrawableRes;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import androidx.core.content.res.ResourcesCompat;
import androidx.media3.common.C;
import androidx.media3.common.Format;
import androidx.media3.common.MediaLibraryInfo;
import androidx.media3.common.Player;
import androidx.media3.common.Player.Events;
import androidx.media3.common.Player.State;
import androidx.media3.common.Timeline;
import androidx.media3.common.TrackGroup;
import androidx.media3.common.TrackSelectionOverride;
import androidx.media3.common.TrackSelectionParameters;
import androidx.media3.common.Tracks;
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.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.google.common.collect.ImmutableList;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Formatter;
import java.util.List;
import java.util.Locale;
import java.util.concurrent.CopyOnWriteArrayList;

/**
 * A view for controlling {@link Player} instances.
 *
 * <p>A {@code PlayerControlView} can be customized by setting attributes (or calling corresponding
 * methods), or overriding drawables.
 *
 * <h2>Attributes</h2>
 *
 * The following attributes can be set on a {@code PlayerControlView} when used in a layout XML
 * file:
 *
 * <ul>
 *   <li><b>{@code show_timeout}</b> - The time between the last user interaction and the controls
 *       being automatically hidden, in milliseconds. Use zero if the controls should not
 *       automatically timeout.
 *       <ul>
 *         <li>Corresponding method: {@link #setShowTimeoutMs(int)}
 *         <li>Default: {@link #DEFAULT_SHOW_TIMEOUT_MS}
 *       </ul>
 *   <li><b>{@code show_rewind_button}</b> - Whether the rewind button is shown.
 *       <ul>
 *         <li>Corresponding method: {@link #setShowRewindButton(boolean)}
 *         <li>Default: true
 *       </ul>
 *   <li><b>{@code show_fastforward_button}</b> - Whether the fast forward button is shown.
 *       <ul>
 *         <li>Corresponding method: {@link #setShowFastForwardButton(boolean)}
 *         <li>Default: true
 *       </ul>
 *   <li><b>{@code show_previous_button}</b> - Whether the previous button is shown.
 *       <ul>
 *         <li>Corresponding method: {@link #setShowPreviousButton(boolean)}
 *         <li>Default: true
 *       </ul>
 *   <li><b>{@code show_next_button}</b> - Whether the next button is shown.
 *       <ul>
 *         <li>Corresponding method: {@link #setShowNextButton(boolean)}
 *         <li>Default: true
 *       </ul>
 *   <li><b>{@code repeat_toggle_modes}</b> - A flagged enumeration value specifying which repeat
 *       mode toggle options are enabled. Valid values are: {@code none}, {@code one}, {@code all},
 *       or {@code one|all}.
 *       <ul>
 *         <li>Corresponding method: {@link #setRepeatToggleModes(int)}
 *         <li>Default: {@link #DEFAULT_REPEAT_TOGGLE_MODES}
 *       </ul>
 *   <li><b>{@code show_shuffle_button}</b> - Whether the shuffle button is shown.
 *       <ul>
 *         <li>Corresponding method: {@link #setShowShuffleButton(boolean)}
 *         <li>Default: false
 *       </ul>
 *   <li><b>{@code show_subtitle_button}</b> - Whether the subtitle button is shown.
 *       <ul>
 *         <li>Corresponding method: {@link #setShowSubtitleButton(boolean)}
 *         <li>Default: false
 *       </ul>
 *   <li><b>{@code animation_enabled}</b> - Whether an animation is used to show and hide the
 *       playback controls.
 *       <ul>
 *         <li>Corresponding method: {@link #setAnimationEnabled(boolean)}
 *         <li>Default: true
 *       </ul>
 *   <li><b>{@code time_bar_min_update_interval}</b> - Specifies the minimum interval between time
 *       bar position updates.
 *       <ul>
 *         <li>Corresponding method: {@link #setTimeBarMinUpdateInterval(int)}
 *         <li>Default: {@link #DEFAULT_TIME_BAR_MIN_UPDATE_INTERVAL_MS}
 *       </ul>
 *   <li>All attributes that can be set on {@link DefaultTimeBar} can also be set on a {@code
 *       PlayerControlView}, and will be propagated to the inflated {@link DefaultTimeBar}.
 * </ul>
 *
 * <h2>Overriding drawables</h2>
 *
 * The drawables used by {@code PlayerControlView} can be overridden by drawables with the same
 * names defined in your application. The drawables that can be overridden are:
 *
 * <ul>
 *   <li><b>{@code exo_styled_controls_play}</b> - The play icon.
 *   <li><b>{@code exo_styled_controls_pause}</b> - The pause icon.
 *   <li><b>{@code exo_styled_controls_rewind}</b> - The background of rewind icon.
 *   <li><b>{@code exo_styled_controls_fastforward}</b> - The background of fast forward icon.
 *   <li><b>{@code exo_styled_controls_previous}</b> - The previous icon.
 *   <li><b>{@code exo_styled_controls_next}</b> - The next icon.
 *   <li><b>{@code exo_styled_controls_repeat_off}</b> - The repeat icon for {@link
 *       Player#REPEAT_MODE_OFF}.
 *   <li><b>{@code exo_styled_controls_repeat_one}</b> - The repeat icon for {@link
 *       Player#REPEAT_MODE_ONE}.
 *   <li><b>{@code exo_styled_controls_repeat_all}</b> - The repeat icon for {@link
 *       Player#REPEAT_MODE_ALL}.
 *   <li><b>{@code exo_styled_controls_shuffle_off}</b> - The shuffle icon when shuffling is
 *       disabled.
 *   <li><b>{@code exo_styled_controls_shuffle_on}</b> - The shuffle icon when shuffling is enabled.
 *   <li><b>{@code exo_styled_controls_vr}</b> - The VR icon.
 * </ul>
 */
@UnstableApi
public class PlayerControlView extends FrameLayout {

  static {
    MediaLibraryInfo.registerModule("media3.ui");
  }

  /**
   * @deprecated Register a {@link PlayerView.ControllerVisibilityListener} via {@link
   *     PlayerView#setControllerVisibilityListener(PlayerView.ControllerVisibilityListener)}
   *     instead. Using {@link PlayerControlView} as a standalone class without {@link PlayerView}
   *     is deprecated.
   */
  @Deprecated
  public interface VisibilityListener {

    /**
     * Called when the visibility changes.
     *
     * @param visibility The new visibility. Either {@link View#VISIBLE} or {@link View#GONE}.
     */
    void onVisibilityChange(int visibility);
  }

  /** Listener to be notified when progress has been updated. */
  public interface ProgressUpdateListener {

    /**
     * Called when progress needs to be updated.
     *
     * @param position The current position.
     * @param bufferedPosition The current buffered position.
     */
    void onProgressUpdate(long position, long bufferedPosition);
  }

  /**
   * @deprecated Register a {@link PlayerView.FullscreenButtonClickListener} via {@link
   *     PlayerView#setFullscreenButtonClickListener(PlayerView.FullscreenButtonClickListener)}
   *     instead. Using {@link PlayerControlView} as a standalone class without {@link PlayerView}
   *     is deprecated.
   */
  @Deprecated
  public interface OnFullScreenModeChangedListener {
    /**
     * Called to indicate a fullscreen mode change.
     *
     * @param isFullScreen {@code true} if the video rendering surface should be fullscreen {@code
     *     false} otherwise.
     */
    void onFullScreenModeChanged(boolean isFullScreen);
  }

  /** The default show timeout, in milliseconds. */
  public static final int DEFAULT_SHOW_TIMEOUT_MS = 5_000;
  /** The default repeat toggle modes. */
  public static final @RepeatModeUtil.RepeatToggleModes int DEFAULT_REPEAT_TOGGLE_MODES =
      RepeatModeUtil.REPEAT_TOGGLE_MODE_NONE;
  /** The default minimum interval between time bar position updates. */
  public static final int DEFAULT_TIME_BAR_MIN_UPDATE_INTERVAL_MS = 200;
  /** The maximum number of windows that can be shown in a multi-window time bar. */
  public static final int MAX_WINDOWS_FOR_MULTI_WINDOW_TIME_BAR = 100;
  /** The maximum interval between time bar position updates. */
  private static final int MAX_UPDATE_INTERVAL_MS = 1_000;
  // LINT.IfChange(playback_speeds)
  private static final float[] PLAYBACK_SPEEDS =
      new float[] {0.25f, 0.5f, 0.75f, 1f, 1.25f, 1.5f, 2f};

  private static final int SETTINGS_PLAYBACK_SPEED_POSITION = 0;
  private static final int SETTINGS_AUDIO_TRACK_SELECTION_POSITION = 1;

  private final PlayerControlViewLayoutManager controlViewLayoutManager;
  private final Resources resources;
  private final ComponentListener componentListener;

  @SuppressWarnings("deprecation") // Using the deprecated type for now.
  private final CopyOnWriteArrayList<VisibilityListener> visibilityListeners;

  private final RecyclerView settingsView;
  private final SettingsAdapter settingsAdapter;
  private final PlaybackSpeedAdapter playbackSpeedAdapter;
  private final TextTrackSelectionAdapter textTrackSelectionAdapter;
  private final AudioTrackSelectionAdapter audioTrackSelectionAdapter;
  // TODO(insun): Add setTrackNameProvider to use customized track name provider.
  private final TrackNameProvider trackNameProvider;
  private final PopupWindow settingsWindow;
  private final int settingsWindowMargin;

  @Nullable private final View previousButton;
  @Nullable private final View nextButton;
  @Nullable private final View playPauseButton;
  @Nullable private final View fastForwardButton;
  @Nullable private final View rewindButton;
  @Nullable private final TextView fastForwardButtonTextView;
  @Nullable private final TextView rewindButtonTextView;
  @Nullable private final ImageView repeatToggleButton;
  @Nullable private final ImageView shuffleButton;
  @Nullable private final View vrButton;
  @Nullable private final ImageView subtitleButton;
  @Nullable private final ImageView fullScreenButton;
  @Nullable private final ImageView minimalFullScreenButton;
  @Nullable private final View settingsButton;
  @Nullable private final View playbackSpeedButton;
  @Nullable private final View audioTrackButton;
  @Nullable private final TextView durationView;
  @Nullable private final TextView positionView;
  @Nullable private final TimeBar timeBar;
  private final StringBuilder formatBuilder;
  private final Formatter formatter;
  private final Timeline.Period period;
  private final Timeline.Window window;
  private final Runnable updateProgressAction;

  private final Drawable repeatOffButtonDrawable;
  private final Drawable repeatOneButtonDrawable;
  private final Drawable repeatAllButtonDrawable;
  private final String repeatOffButtonContentDescription;
  private final String repeatOneButtonContentDescription;
  private final String repeatAllButtonContentDescription;
  private final Drawable shuffleOnButtonDrawable;
  private final Drawable shuffleOffButtonDrawable;
  private final float buttonAlphaEnabled;
  private final float buttonAlphaDisabled;
  private final String shuffleOnContentDescription;
  private final String shuffleOffContentDescription;
  private final Drawable subtitleOnButtonDrawable;
  private final Drawable subtitleOffButtonDrawable;
  private final String subtitleOnContentDescription;
  private final String subtitleOffContentDescription;
  private final Drawable fullScreenExitDrawable;
  private final Drawable fullScreenEnterDrawable;
  private final String fullScreenExitContentDescription;
  private final String fullScreenEnterContentDescription;

  @Nullable private Player player;
  @Nullable private ProgressUpdateListener progressUpdateListener;

  @Nullable private OnFullScreenModeChangedListener onFullScreenModeChangedListener;
  private boolean isFullScreen;
  private boolean isAttachedToWindow;
  private boolean showMultiWindowTimeBar;
  private boolean multiWindowTimeBar;
  private boolean scrubbing;
  private int showTimeoutMs;
  private int timeBarMinUpdateIntervalMs;
  private @RepeatModeUtil.RepeatToggleModes int repeatToggleModes;
  private long[] adGroupTimesMs;
  private boolean[] playedAdGroups;
  private long[] extraAdGroupTimesMs;
  private boolean[] extraPlayedAdGroups;
  private long currentWindowOffset;

  private boolean needToHideBars;

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

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

  public PlayerControlView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
    this(context, attrs, defStyleAttr, attrs);
  }

  @SuppressWarnings({
    "nullness:argument",
    "nullness:assignment",
    "nullness:method.invocation",
    "nullness:methodref.receiver.bound"
  })
  public PlayerControlView(
      Context context,
      @Nullable AttributeSet attrs,
      int defStyleAttr,
      @Nullable AttributeSet playbackAttrs) {
    super(context, attrs, defStyleAttr);
    int controllerLayoutId = R.layout.exo_player_control_view;
    showTimeoutMs = DEFAULT_SHOW_TIMEOUT_MS;
    repeatToggleModes = DEFAULT_REPEAT_TOGGLE_MODES;
    timeBarMinUpdateIntervalMs = DEFAULT_TIME_BAR_MIN_UPDATE_INTERVAL_MS;
    boolean showRewindButton = true;
    boolean showFastForwardButton = true;
    boolean showPreviousButton = true;
    boolean showNextButton = true;
    boolean showShuffleButton = false;
    boolean showSubtitleButton = false;
    boolean animationEnabled = true;
    boolean showVrButton = false;

    if (playbackAttrs != null) {
      TypedArray a =
          context
              .getTheme()
              .obtainStyledAttributes(
                  playbackAttrs, R.styleable.PlayerControlView, defStyleAttr, /* defStyleRes= */ 0);
      try {
        controllerLayoutId =
            a.getResourceId(R.styleable.PlayerControlView_controller_layout_id, controllerLayoutId);
        showTimeoutMs = a.getInt(R.styleable.PlayerControlView_show_timeout, showTimeoutMs);
        repeatToggleModes = getRepeatToggleModes(a, repeatToggleModes);
        showRewindButton =
            a.getBoolean(R.styleable.PlayerControlView_show_rewind_button, showRewindButton);
        showFastForwardButton =
            a.getBoolean(
                R.styleable.PlayerControlView_show_fastforward_button, showFastForwardButton);
        showPreviousButton =
            a.getBoolean(R.styleable.PlayerControlView_show_previous_button, showPreviousButton);
        showNextButton =
            a.getBoolean(R.styleable.PlayerControlView_show_next_button, showNextButton);
        showShuffleButton =
            a.getBoolean(R.styleable.PlayerControlView_show_shuffle_button, showShuffleButton);
        showSubtitleButton =
            a.getBoolean(R.styleable.PlayerControlView_show_subtitle_button, showSubtitleButton);
        showVrButton = a.getBoolean(R.styleable.PlayerControlView_show_vr_button, showVrButton);
        setTimeBarMinUpdateInterval(
            a.getInt(
                R.styleable.PlayerControlView_time_bar_min_update_interval,
                timeBarMinUpdateIntervalMs));
        animationEnabled =
            a.getBoolean(R.styleable.PlayerControlView_animation_enabled, animationEnabled);
      } finally {
        a.recycle();
      }
    }

    LayoutInflater.from(context).inflate(controllerLayoutId, /* root= */ this);
    setDescendantFocusability(FOCUS_AFTER_DESCENDANTS);

    componentListener = new ComponentListener();
    visibilityListeners = new CopyOnWriteArrayList<>();
    period = new Timeline.Period();
    window = new Timeline.Window();
    formatBuilder = new StringBuilder();
    formatter = new Formatter(formatBuilder, Locale.getDefault());
    adGroupTimesMs = new long[0];
    playedAdGroups = new boolean[0];
    extraAdGroupTimesMs = new long[0];
    extraPlayedAdGroups = new boolean[0];
    updateProgressAction = this::updateProgress;

    durationView = findViewById(R.id.exo_duration);
    positionView = findViewById(R.id.exo_position);

    subtitleButton = findViewById(R.id.exo_subtitle);
    if (subtitleButton != null) {
      subtitleButton.setOnClickListener(componentListener);
    }

    fullScreenButton = findViewById(R.id.exo_fullscreen);
    initializeFullScreenButton(fullScreenButton, this::onFullScreenButtonClicked);
    minimalFullScreenButton = findViewById(R.id.exo_minimal_fullscreen);
    initializeFullScreenButton(minimalFullScreenButton, this::onFullScreenButtonClicked);

    settingsButton = findViewById(R.id.exo_settings);
    if (settingsButton != null) {
      settingsButton.setOnClickListener(componentListener);
    }

    playbackSpeedButton = findViewById(R.id.exo_playback_speed);
    if (playbackSpeedButton != null) {
      playbackSpeedButton.setOnClickListener(componentListener);
    }

    audioTrackButton = findViewById(R.id.exo_audio_track);
    if (audioTrackButton != null) {
      audioTrackButton.setOnClickListener(componentListener);
    }

    TimeBar customTimeBar = findViewById(R.id.exo_progress);
    View timeBarPlaceholder = findViewById(R.id.exo_progress_placeholder);
    if (customTimeBar != null) {
      timeBar = customTimeBar;
    } else if (timeBarPlaceholder != null) {
      // Propagate playbackAttrs as timebarAttrs so that DefaultTimeBar's custom attributes are
      // transferred, but standard attributes (e.g. background) are not.
      DefaultTimeBar defaultTimeBar =
          new DefaultTimeBar(context, null, 0, playbackAttrs, R.style.ExoStyledControls_TimeBar);
      defaultTimeBar.setId(R.id.exo_progress);
      defaultTimeBar.setLayoutParams(timeBarPlaceholder.getLayoutParams());
      ViewGroup parent = ((ViewGroup) timeBarPlaceholder.getParent());
      int timeBarIndex = parent.indexOfChild(timeBarPlaceholder);
      parent.removeView(timeBarPlaceholder);
      parent.addView(defaultTimeBar, timeBarIndex);
      timeBar = defaultTimeBar;
    } else {
      timeBar = null;
    }
    if (timeBar != null) {
      timeBar.addListener(componentListener);
    }

    playPauseButton = findViewById(R.id.exo_play_pause);
    if (playPauseButton != null) {
      playPauseButton.setOnClickListener(componentListener);
    }
    previousButton = findViewById(R.id.exo_prev);
    if (previousButton != null) {
      previousButton.setOnClickListener(componentListener);
    }
    nextButton = findViewById(R.id.exo_next);
    if (nextButton != null) {
      nextButton.setOnClickListener(componentListener);
    }
    Typeface typeface = ResourcesCompat.getFont(context, R.font.roboto_medium_numbers);
    View rewButton = findViewById(R.id.exo_rew);
    rewindButtonTextView = rewButton == null ? findViewById(R.id.exo_rew_with_amount) : null;
    if (rewindButtonTextView != null) {
      rewindButtonTextView.setTypeface(typeface);
    }
    rewindButton = rewButton == null ? rewindButtonTextView : rewButton;
    if (rewindButton != null) {
      rewindButton.setOnClickListener(componentListener);
    }
    View ffwdButton = findViewById(R.id.exo_ffwd);
    fastForwardButtonTextView = ffwdButton == null ? findViewById(R.id.exo_ffwd_with_amount) : null;
    if (fastForwardButtonTextView != null) {
      fastForwardButtonTextView.setTypeface(typeface);
    }
    fastForwardButton = ffwdButton == null ? fastForwardButtonTextView : ffwdButton;
    if (fastForwardButton != null) {
      fastForwardButton.setOnClickListener(componentListener);
    }
    repeatToggleButton = findViewById(R.id.exo_repeat_toggle);
    if (repeatToggleButton != null) {
      repeatToggleButton.setOnClickListener(componentListener);
    }
    shuffleButton = findViewById(R.id.exo_shuffle);
    if (shuffleButton != null) {
      shuffleButton.setOnClickListener(componentListener);
    }

    resources = context.getResources();
    buttonAlphaEnabled =
        (float) resources.getInteger(R.integer.exo_media_button_opacity_percentage_enabled) / 100;
    buttonAlphaDisabled =
        (float) resources.getInteger(R.integer.exo_media_button_opacity_percentage_disabled) / 100;

    vrButton = findViewById(R.id.exo_vr);
    if (vrButton != null) {
      updateButton(/* enabled= */ false, vrButton);
    }

    controlViewLayoutManager = new PlayerControlViewLayoutManager(this);
    controlViewLayoutManager.setAnimationEnabled(animationEnabled);

    String[] settingTexts = new String[2];
    Drawable[] settingIcons = new Drawable[2];
    settingTexts[SETTINGS_PLAYBACK_SPEED_POSITION] =
        resources.getString(R.string.exo_controls_playback_speed);
    settingIcons[SETTINGS_PLAYBACK_SPEED_POSITION] =
        getDrawable(context, resources, R.drawable.exo_styled_controls_speed);
    settingTexts[SETTINGS_AUDIO_TRACK_SELECTION_POSITION] =
        resources.getString(R.string.exo_track_selection_title_audio);
    settingIcons[SETTINGS_AUDIO_TRACK_SELECTION_POSITION] =
        getDrawable(context, resources, R.drawable.exo_styled_controls_audiotrack);
    settingsAdapter = new SettingsAdapter(settingTexts, settingIcons);
    settingsWindowMargin = resources.getDimensionPixelSize(R.dimen.exo_settings_offset);
    settingsView =
        (RecyclerView)
            LayoutInflater.from(context)
                .inflate(R.layout.exo_styled_settings_list, /* root= */ null);
    settingsView.setAdapter(settingsAdapter);
    settingsView.setLayoutManager(new LinearLayoutManager(getContext()));
    settingsWindow =
        new PopupWindow(settingsView, LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT, true);
    if (Util.SDK_INT < 23) {
      // Work around issue where tapping outside of the menu area or pressing the back button
      // doesn't dismiss the menu as expected. See: https://github.com/google/ExoPlayer/issues/8272.
      settingsWindow.setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT));
    }
    settingsWindow.setOnDismissListener(componentListener);
    needToHideBars = true;

    trackNameProvider = new DefaultTrackNameProvider(getResources());
    subtitleOnButtonDrawable =
        getDrawable(context, resources, R.drawable.exo_styled_controls_subtitle_on);
    subtitleOffButtonDrawable =
        getDrawable(context, resources, R.drawable.exo_styled_controls_subtitle_off);
    subtitleOnContentDescription =
        resources.getString(R.string.exo_controls_cc_enabled_description);
    subtitleOffContentDescription =
        resources.getString(R.string.exo_controls_cc_disabled_description);
    textTrackSelectionAdapter = new TextTrackSelectionAdapter();
    audioTrackSelectionAdapter = new AudioTrackSelectionAdapter();
    playbackSpeedAdapter =
        new PlaybackSpeedAdapter(
            resources.getStringArray(R.array.exo_controls_playback_speeds), PLAYBACK_SPEEDS);

    fullScreenExitDrawable =
        getDrawable(context, resources, R.drawable.exo_styled_controls_fullscreen_exit);
    fullScreenEnterDrawable =
        getDrawable(context, resources, R.drawable.exo_styled_controls_fullscreen_enter);
    repeatOffButtonDrawable =
        getDrawable(context, resources, R.drawable.exo_styled_controls_repeat_off);
    repeatOneButtonDrawable =
        getDrawable(context, resources, R.drawable.exo_styled_controls_repeat_one);
    repeatAllButtonDrawable =
        getDrawable(context, resources, R.drawable.exo_styled_controls_repeat_all);
    shuffleOnButtonDrawable =
        getDrawable(context, resources, R.drawable.exo_styled_controls_shuffle_on);
    shuffleOffButtonDrawable =
        getDrawable(context, resources, R.drawable.exo_styled_controls_shuffle_off);
    fullScreenExitContentDescription =
        resources.getString(R.string.exo_controls_fullscreen_exit_description);
    fullScreenEnterContentDescription =
        resources.getString(R.string.exo_controls_fullscreen_enter_description);
    repeatOffButtonContentDescription =
        resources.getString(R.string.exo_controls_repeat_off_description);
    repeatOneButtonContentDescription =
        resources.getString(R.string.exo_controls_repeat_one_description);
    repeatAllButtonContentDescription =
        resources.getString(R.string.exo_controls_repeat_all_description);
    shuffleOnContentDescription = resources.getString(R.string.exo_controls_shuffle_on_description);
    shuffleOffContentDescription =
        resources.getString(R.string.exo_controls_shuffle_off_description);

    // TODO(insun) : Make showing bottomBar configurable. (ex. show_bottom_bar attribute).
    ViewGroup bottomBar = findViewById(R.id.exo_bottom_bar);
    controlViewLayoutManager.setShowButton(bottomBar, true);
    controlViewLayoutManager.setShowButton(fastForwardButton, showFastForwardButton);
    controlViewLayoutManager.setShowButton(rewindButton, showRewindButton);
    controlViewLayoutManager.setShowButton(previousButton, showPreviousButton);
    controlViewLayoutManager.setShowButton(nextButton, showNextButton);
    controlViewLayoutManager.setShowButton(shuffleButton, showShuffleButton);
    controlViewLayoutManager.setShowButton(subtitleButton, showSubtitleButton);
    controlViewLayoutManager.setShowButton(vrButton, showVrButton);
    controlViewLayoutManager.setShowButton(
        repeatToggleButton, repeatToggleModes != RepeatModeUtil.REPEAT_TOGGLE_MODE_NONE);
    addOnLayoutChangeListener(this::onLayoutChange);
  }

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

  /**
   * Sets the {@link Player} to control.
   *
   * @param player The {@link Player} to control, 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;
    }
    if (this.player != null) {
      this.player.removeListener(componentListener);
    }
    this.player = player;
    if (player != null) {
      player.addListener(componentListener);
    }
    updateAll();
  }

  /**
   * Sets whether the time bar should show all windows, as opposed to just the current one. If the
   * timeline has a period with unknown duration or more than {@link
   * #MAX_WINDOWS_FOR_MULTI_WINDOW_TIME_BAR} windows the time bar will fall back to showing a single
   * window.
   *
   * @param showMultiWindowTimeBar Whether the time bar should show all windows.
   */
  public void setShowMultiWindowTimeBar(boolean showMultiWindowTimeBar) {
    this.showMultiWindowTimeBar = showMultiWindowTimeBar;
    updateTimeline();
  }

  /**
   * 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. Must be the same length as {@code
   *     extraAdGroupTimesMs}, or {@code null} if {@code extraAdGroupTimesMs} is {@code null}.
   */
  public void setExtraAdGroupMarkers(
      @Nullable long[] extraAdGroupTimesMs, @Nullable boolean[] extraPlayedAdGroups) {
    if (extraAdGroupTimesMs == null) {
      this.extraAdGroupTimesMs = new long[0];
      this.extraPlayedAdGroups = new boolean[0];
    } else {
      extraPlayedAdGroups = checkNotNull(extraPlayedAdGroups);
      Assertions.checkArgument(extraAdGroupTimesMs.length == extraPlayedAdGroups.length);
      this.extraAdGroupTimesMs = extraAdGroupTimesMs;
      this.extraPlayedAdGroups = extraPlayedAdGroups;
    }
    updateTimeline();
  }

  /**
   * @deprecated Register a {@link PlayerView.ControllerVisibilityListener} via {@link
   *     PlayerView#setControllerVisibilityListener(PlayerView.ControllerVisibilityListener)}
   *     instead. Using {@link PlayerControlView} as a standalone class without {@link PlayerView}
   *     is deprecated.
   */
  @SuppressWarnings("deprecation")
  @Deprecated
  public void addVisibilityListener(VisibilityListener listener) {
    checkNotNull(listener);
    visibilityListeners.add(listener);
  }

  /**
   * @deprecated Register a {@link PlayerView.ControllerVisibilityListener} via {@link
   *     PlayerView#setControllerVisibilityListener(PlayerView.ControllerVisibilityListener)}
   *     instead. Using {@link PlayerControlView} as a standalone class without {@link PlayerView}
   *     is deprecated.
   */
  @SuppressWarnings("deprecation")
  @Deprecated
  public void removeVisibilityListener(VisibilityListener listener) {
    visibilityListeners.remove(listener);
  }

  /**
   * Sets the {@link ProgressUpdateListener}.
   *
   * @param listener The listener to be notified about when progress is updated.
   */
  public void setProgressUpdateListener(@Nullable ProgressUpdateListener listener) {
    this.progressUpdateListener = listener;
  }

  /**
   * Sets whether the rewind button is shown.
   *
   * @param showRewindButton Whether the rewind button is shown.
   */
  public void setShowRewindButton(boolean showRewindButton) {
    controlViewLayoutManager.setShowButton(rewindButton, showRewindButton);
    updateNavigation();
  }

  /**
   * Sets whether the fast forward button is shown.
   *
   * @param showFastForwardButton Whether the fast forward button is shown.
   */
  public void setShowFastForwardButton(boolean showFastForwardButton) {
    controlViewLayoutManager.setShowButton(fastForwardButton, showFastForwardButton);
    updateNavigation();
  }

  /**
   * Sets whether the previous button is shown.
   *
   * @param showPreviousButton Whether the previous button is shown.
   */
  public void setShowPreviousButton(boolean showPreviousButton) {
    controlViewLayoutManager.setShowButton(previousButton, showPreviousButton);
    updateNavigation();
  }

  /**
   * Sets whether the next button is shown.
   *
   * @param showNextButton Whether the next button is shown.
   */
  public void setShowNextButton(boolean showNextButton) {
    controlViewLayoutManager.setShowButton(nextButton, showNextButton);
    updateNavigation();
  }

  /**
   * Returns the playback controls timeout. The playback controls are automatically hidden after
   * this duration of time has elapsed without user input.
   *
   * @return The duration in milliseconds. A non-positive value indicates that the controls will
   *     remain visible indefinitely.
   */
  public int getShowTimeoutMs() {
    return showTimeoutMs;
  }

  /**
   * Sets the playback controls timeout. The playback controls are automatically hidden after this
   * duration of time has elapsed without user input.
   *
   * @param showTimeoutMs The duration in milliseconds. A non-positive value will cause the controls
   *     to remain visible indefinitely.
   */
  public void setShowTimeoutMs(int showTimeoutMs) {
    this.showTimeoutMs = showTimeoutMs;
    if (isFullyVisible()) {
      controlViewLayoutManager.resetHideCallbacks();
    }
  }

  /**
   * Returns which repeat toggle modes are enabled.
   *
   * @return The currently enabled {@link RepeatModeUtil.RepeatToggleModes}.
   */
  public @RepeatModeUtil.RepeatToggleModes int getRepeatToggleModes() {
    return repeatToggleModes;
  }

  /**
   * Sets which repeat toggle modes are enabled.
   *
   * @param repeatToggleModes A set of {@link RepeatModeUtil.RepeatToggleModes}.
   */
  public void setRepeatToggleModes(@RepeatModeUtil.RepeatToggleModes int repeatToggleModes) {
    this.repeatToggleModes = repeatToggleModes;
    if (player != null && player.isCommandAvailable(COMMAND_SET_REPEAT_MODE)) {
      @Player.RepeatMode int currentMode = player.getRepeatMode();
      if (repeatToggleModes == RepeatModeUtil.REPEAT_TOGGLE_MODE_NONE
          && currentMode != Player.REPEAT_MODE_OFF) {
        player.setRepeatMode(Player.REPEAT_MODE_OFF);
      } else if (repeatToggleModes == RepeatModeUtil.REPEAT_TOGGLE_MODE_ONE
          && currentMode == Player.REPEAT_MODE_ALL) {
        player.setRepeatMode(Player.REPEAT_MODE_ONE);
      } else if (repeatToggleModes == RepeatModeUtil.REPEAT_TOGGLE_MODE_ALL
          && currentMode == Player.REPEAT_MODE_ONE) {
        player.setRepeatMode(Player.REPEAT_MODE_ALL);
      }
    }
    controlViewLayoutManager.setShowButton(
        repeatToggleButton, repeatToggleModes != RepeatModeUtil.REPEAT_TOGGLE_MODE_NONE);
    updateRepeatModeButton();
  }

  /** Returns whether the shuffle button is shown. */
  public boolean getShowShuffleButton() {
    return controlViewLayoutManager.getShowButton(shuffleButton);
  }

  /**
   * Sets whether the shuffle button is shown.
   *
   * @param showShuffleButton Whether the shuffle button is shown.
   */
  public void setShowShuffleButton(boolean showShuffleButton) {
    controlViewLayoutManager.setShowButton(shuffleButton, showShuffleButton);
    updateShuffleButton();
  }

  /** Returns whether the subtitle button is shown. */
  public boolean getShowSubtitleButton() {
    return controlViewLayoutManager.getShowButton(subtitleButton);
  }

  /**
   * Sets whether the subtitle button is shown.
   *
   * @param showSubtitleButton Whether the subtitle button is shown.
   */
  public void setShowSubtitleButton(boolean showSubtitleButton) {
    controlViewLayoutManager.setShowButton(subtitleButton, showSubtitleButton);
  }

  /** Returns whether the VR button is shown. */
  public boolean getShowVrButton() {
    return controlViewLayoutManager.getShowButton(vrButton);
  }

  /**
   * Sets whether the VR button is shown.
   *
   * @param showVrButton Whether the VR button is shown.
   */
  public void setShowVrButton(boolean showVrButton) {
    controlViewLayoutManager.setShowButton(vrButton, showVrButton);
  }

  /**
   * Sets listener for the VR button.
   *
   * @param onClickListener Listener for the VR button, or null to clear the listener.
   */
  public void setVrButtonListener(@Nullable OnClickListener onClickListener) {
    if (vrButton != null) {
      vrButton.setOnClickListener(onClickListener);
      updateButton(onClickListener != null, vrButton);
    }
  }

  /**
   * Sets whether an animation is used to show and hide the playback controls.
   *
   * @param animationEnabled Whether an animation is applied to show and hide playback controls.
   */
  public void setAnimationEnabled(boolean animationEnabled) {
    controlViewLayoutManager.setAnimationEnabled(animationEnabled);
  }

  /** Returns whether an animation is used to show and hide the playback controls. */
  public boolean isAnimationEnabled() {
    return controlViewLayoutManager.isAnimationEnabled();
  }

  /**
   * Sets the minimum interval between time bar position updates.
   *
   * <p>Note that smaller intervals, e.g. 33ms, will result in a smooth movement but will use more
   * CPU resources while the time bar is visible, whereas larger intervals, e.g. 200ms, will result
   * in a step-wise update with less CPU usage.
   *
   * @param minUpdateIntervalMs The minimum interval between time bar position updates, in
   *     milliseconds.
   */
  public void setTimeBarMinUpdateInterval(int minUpdateIntervalMs) {
    // Do not accept values below 16ms (60fps) and larger than the maximum update interval.
    timeBarMinUpdateIntervalMs =
        Util.constrainValue(minUpdateIntervalMs, 16, MAX_UPDATE_INTERVAL_MS);
  }

  /**
   * @deprecated Register a {@link PlayerView.FullscreenButtonClickListener} via {@link
   *     PlayerView#setFullscreenButtonClickListener(PlayerView.FullscreenButtonClickListener)}
   *     instead. Using {@link PlayerControlView} as a standalone class without {@link PlayerView}
   *     is deprecated.
   */
  @SuppressWarnings("deprecation")
  @Deprecated
  public void setOnFullScreenModeChangedListener(
      @Nullable OnFullScreenModeChangedListener listener) {
    onFullScreenModeChangedListener = listener;
    updateFullScreenButtonVisibility(fullScreenButton, listener != null);
    updateFullScreenButtonVisibility(minimalFullScreenButton, listener != null);
  }

  /**
   * Shows the playback controls. If {@link #getShowTimeoutMs()} is positive then the controls will
   * be automatically hidden after this duration of time has elapsed without user input.
   */
  public void show() {
    controlViewLayoutManager.show();
  }

  /** Hides the controller. */
  public void hide() {
    controlViewLayoutManager.hide();
  }

  /** Hides the controller without any animation. */
  public void hideImmediately() {
    controlViewLayoutManager.hideImmediately();
  }

  /** Returns whether the controller is fully visible, which means all UI controls are visible. */
  public boolean isFullyVisible() {
    return controlViewLayoutManager.isFullyVisible();
  }

  /** Returns whether the controller is currently visible. */
  public boolean isVisible() {
    return getVisibility() == VISIBLE;
  }

  @SuppressWarnings("deprecation") // Calling the deprecated listener for now.
  /* package */ void notifyOnVisibilityChange() {
    for (VisibilityListener visibilityListener : visibilityListeners) {
      visibilityListener.onVisibilityChange(getVisibility());
    }
  }

  /* package */ void updateAll() {
    updatePlayPauseButton();
    updateNavigation();
    updateRepeatModeButton();
    updateShuffleButton();
    updateTrackLists();
    updatePlaybackSpeedList();
    updateTimeline();
  }

  private void updatePlayPauseButton() {
    if (!isVisible() || !isAttachedToWindow) {
      return;
    }
    if (playPauseButton != null) {
      boolean shouldShowPauseButton = shouldShowPauseButton();
      @DrawableRes
      int drawableRes =
          shouldShowPauseButton
              ? R.drawable.exo_styled_controls_pause
              : R.drawable.exo_styled_controls_play;
      @StringRes
      int stringRes =
          shouldShowPauseButton
              ? R.string.exo_controls_pause_description
              : R.string.exo_controls_play_description;
      ((ImageView) playPauseButton)
          .setImageDrawable(getDrawable(getContext(), resources, drawableRes));
      playPauseButton.setContentDescription(resources.getString(stringRes));

      boolean enablePlayPause = shouldEnablePlayPauseButton();
      updateButton(enablePlayPause, playPauseButton);
    }
  }

  private void updateNavigation() {
    if (!isVisible() || !isAttachedToWindow) {
      return;
    }

    @Nullable Player player = this.player;
    boolean enableSeeking = false;
    boolean enablePrevious = false;
    boolean enableRewind = false;
    boolean enableFastForward = false;
    boolean enableNext = false;
    if (player != null) {
      enableSeeking =
          (showMultiWindowTimeBar && canShowMultiWindowTimeBar(player, window))
              ? player.isCommandAvailable(COMMAND_SEEK_TO_MEDIA_ITEM)
              : player.isCommandAvailable(COMMAND_SEEK_IN_CURRENT_MEDIA_ITEM);
      enablePrevious = player.isCommandAvailable(COMMAND_SEEK_TO_PREVIOUS);
      enableRewind = player.isCommandAvailable(COMMAND_SEEK_BACK);
      enableFastForward = player.isCommandAvailable(COMMAND_SEEK_FORWARD);
      enableNext = player.isCommandAvailable(COMMAND_SEEK_TO_NEXT);
    }

    if (enableRewind) {
      updateRewindButton();
    }
    if (enableFastForward) {
      updateFastForwardButton();
    }

    updateButton(enablePrevious, previousButton);
    updateButton(enableRewind, rewindButton);
    updateButton(enableFastForward, fastForwardButton);
    updateButton(enableNext, nextButton);
    if (timeBar != null) {
      timeBar.setEnabled(enableSeeking);
    }
  }

  private void updateRewindButton() {
    long rewindMs =
        player != null ? player.getSeekBackIncrement() : C.DEFAULT_SEEK_BACK_INCREMENT_MS;
    int rewindSec = (int) (rewindMs / 1_000);
    if (rewindButtonTextView != null) {
      rewindButtonTextView.setText(String.valueOf(rewindSec));
    }
    if (rewindButton != null) {
      rewindButton.setContentDescription(
          resources.getQuantityString(
              R.plurals.exo_controls_rewind_by_amount_description, rewindSec, rewindSec));
    }
  }

  private void updateFastForwardButton() {
    long fastForwardMs =
        player != null ? player.getSeekForwardIncrement() : C.DEFAULT_SEEK_FORWARD_INCREMENT_MS;
    int fastForwardSec = (int) (fastForwardMs / 1_000);
    if (fastForwardButtonTextView != null) {
      fastForwardButtonTextView.setText(String.valueOf(fastForwardSec));
    }
    if (fastForwardButton != null) {
      fastForwardButton.setContentDescription(
          resources.getQuantityString(
              R.plurals.exo_controls_fastforward_by_amount_description,
              fastForwardSec,
              fastForwardSec));
    }
  }

  private void updateRepeatModeButton() {
    if (!isVisible() || !isAttachedToWindow || repeatToggleButton == null) {
      return;
    }

    if (repeatToggleModes == RepeatModeUtil.REPEAT_TOGGLE_MODE_NONE) {
      updateButton(/* enabled= */ false, repeatToggleButton);
      return;
    }

    @Nullable Player player = this.player;
    if (player == null || !player.isCommandAvailable(COMMAND_SET_REPEAT_MODE)) {
      updateButton(/* enabled= */ false, repeatToggleButton);
      repeatToggleButton.setImageDrawable(repeatOffButtonDrawable);
      repeatToggleButton.setContentDescription(repeatOffButtonContentDescription);
      return;
    }

    updateButton(/* enabled= */ true, repeatToggleButton);
    switch (player.getRepeatMode()) {
      case Player.REPEAT_MODE_OFF:
        repeatToggleButton.setImageDrawable(repeatOffButtonDrawable);
        repeatToggleButton.setContentDescription(repeatOffButtonContentDescription);
        break;
      case Player.REPEAT_MODE_ONE:
        repeatToggleButton.setImageDrawable(repeatOneButtonDrawable);
        repeatToggleButton.setContentDescription(repeatOneButtonContentDescription);
        break;
      case Player.REPEAT_MODE_ALL:
        repeatToggleButton.setImageDrawable(repeatAllButtonDrawable);
        repeatToggleButton.setContentDescription(repeatAllButtonContentDescription);
        break;
      default:
        // Never happens.
    }
  }

  private void updateShuffleButton() {
    if (!isVisible() || !isAttachedToWindow || shuffleButton == null) {
      return;
    }

    @Nullable Player player = this.player;
    if (!controlViewLayoutManager.getShowButton(shuffleButton)) {
      updateButton(/* enabled= */ false, shuffleButton);
    } else if (player == null || !player.isCommandAvailable(COMMAND_SET_SHUFFLE_MODE)) {
      updateButton(/* enabled= */ false, shuffleButton);
      shuffleButton.setImageDrawable(shuffleOffButtonDrawable);
      shuffleButton.setContentDescription(shuffleOffContentDescription);
    } else {
      updateButton(/* enabled= */ true, shuffleButton);
      shuffleButton.setImageDrawable(
          player.getShuffleModeEnabled() ? shuffleOnButtonDrawable : shuffleOffButtonDrawable);
      shuffleButton.setContentDescription(
          player.getShuffleModeEnabled()
              ? shuffleOnContentDescription
              : shuffleOffContentDescription);
    }
  }

  private void updateTrackLists() {
    initTrackSelectionAdapter();
    updateButton(textTrackSelectionAdapter.getItemCount() > 0, subtitleButton);
    updateSettingsButton();
  }

  private void initTrackSelectionAdapter() {
    textTrackSelectionAdapter.clear();
    audioTrackSelectionAdapter.clear();
    if (player == null
        || !player.isCommandAvailable(COMMAND_GET_TRACKS)
        || !player.isCommandAvailable(COMMAND_SET_TRACK_SELECTION_PARAMETERS)) {
      return;
    }
    Tracks tracks = player.getCurrentTracks();
    audioTrackSelectionAdapter.init(gatherSupportedTrackInfosOfType(tracks, C.TRACK_TYPE_AUDIO));
    if (controlViewLayoutManager.getShowButton(subtitleButton)) {
      textTrackSelectionAdapter.init(gatherSupportedTrackInfosOfType(tracks, C.TRACK_TYPE_TEXT));
    } else {
      textTrackSelectionAdapter.init(ImmutableList.of());
    }
  }

  private ImmutableList<TrackInformation> gatherSupportedTrackInfosOfType(
      Tracks tracks, @C.TrackType int trackType) {
    ImmutableList.Builder<TrackInformation> trackInfos = new ImmutableList.Builder<>();
    List<Tracks.Group> trackGroups = tracks.getGroups();
    for (int trackGroupIndex = 0; trackGroupIndex < trackGroups.size(); trackGroupIndex++) {
      Tracks.Group trackGroup = trackGroups.get(trackGroupIndex);
      if (trackGroup.getType() != trackType) {
        continue;
      }
      for (int trackIndex = 0; trackIndex < trackGroup.length; trackIndex++) {
        if (!trackGroup.isTrackSupported(trackIndex)) {
          continue;
        }
        Format trackFormat = trackGroup.getTrackFormat(trackIndex);
        if ((trackFormat.selectionFlags & C.SELECTION_FLAG_FORCED) != 0) {
          continue;
        }
        String trackName = trackNameProvider.getTrackName(trackFormat);
        trackInfos.add(new TrackInformation(tracks, trackGroupIndex, trackIndex, trackName));
      }
    }
    return trackInfos.build();
  }

  private void updateTimeline() {
    @Nullable Player player = this.player;
    if (player == null) {
      return;
    }
    multiWindowTimeBar = showMultiWindowTimeBar && canShowMultiWindowTimeBar(player, window);
    currentWindowOffset = 0;
    long durationUs = 0;
    int adGroupCount = 0;
    Timeline timeline =
        player.isCommandAvailable(COMMAND_GET_TIMELINE)
            ? player.getCurrentTimeline()
            : Timeline.EMPTY;
    if (!timeline.isEmpty()) {
      int currentWindowIndex = player.getCurrentMediaItemIndex();
      int firstWindowIndex = multiWindowTimeBar ? 0 : currentWindowIndex;
      int lastWindowIndex = multiWindowTimeBar ? timeline.getWindowCount() - 1 : currentWindowIndex;
      for (int i = firstWindowIndex; i <= lastWindowIndex; i++) {
        if (i == currentWindowIndex) {
          currentWindowOffset = Util.usToMs(durationUs);
        }
        timeline.getWindow(i, window);
        if (window.durationUs == C.TIME_UNSET) {
          Assertions.checkState(!multiWindowTimeBar);
          break;
        }
        for (int j = window.firstPeriodIndex; j <= window.lastPeriodIndex; j++) {
          timeline.getPeriod(j, period);
          int removedGroups = period.getRemovedAdGroupCount();
          int totalGroups = period.getAdGroupCount();
          for (int adGroupIndex = removedGroups; adGroupIndex < totalGroups; adGroupIndex++) {
            long adGroupTimeInPeriodUs = period.getAdGroupTimeUs(adGroupIndex);
            if (adGroupTimeInPeriodUs == C.TIME_END_OF_SOURCE) {
              if (period.durationUs == C.TIME_UNSET) {
                // Don't show ad markers for postrolls in periods with unknown duration.
                continue;
              }
              adGroupTimeInPeriodUs = period.durationUs;
            }
            long adGroupTimeInWindowUs = adGroupTimeInPeriodUs + period.getPositionInWindowUs();
            if (adGroupTimeInWindowUs >= 0) {
              if (adGroupCount == adGroupTimesMs.length) {
                int newLength = adGroupTimesMs.length == 0 ? 1 : adGroupTimesMs.length * 2;
                adGroupTimesMs = Arrays.copyOf(adGroupTimesMs, newLength);
                playedAdGroups = Arrays.copyOf(playedAdGroups, newLength);
              }
              adGroupTimesMs[adGroupCount] = Util.usToMs(durationUs + adGroupTimeInWindowUs);
              playedAdGroups[adGroupCount] = period.hasPlayedAdGroup(adGroupIndex);
              adGroupCount++;
            }
          }
        }
        durationUs += window.durationUs;
      }
    } else if (player.isCommandAvailable(COMMAND_GET_CURRENT_MEDIA_ITEM)) {
      long playerDurationMs = player.getContentDuration();
      if (playerDurationMs != C.TIME_UNSET) {
        durationUs = msToUs(playerDurationMs);
      }
    }
    long durationMs = Util.usToMs(durationUs);
    if (durationView != null) {
      durationView.setText(Util.getStringForTime(formatBuilder, formatter, durationMs));
    }
    if (timeBar != null) {
      timeBar.setDuration(durationMs);
      int extraAdGroupCount = extraAdGroupTimesMs.length;
      int totalAdGroupCount = adGroupCount + extraAdGroupCount;
      if (totalAdGroupCount > adGroupTimesMs.length) {
        adGroupTimesMs = Arrays.copyOf(adGroupTimesMs, totalAdGroupCount);
        playedAdGroups = Arrays.copyOf(playedAdGroups, totalAdGroupCount);
      }
      System.arraycopy(extraAdGroupTimesMs, 0, adGroupTimesMs, adGroupCount, extraAdGroupCount);
      System.arraycopy(extraPlayedAdGroups, 0, playedAdGroups, adGroupCount, extraAdGroupCount);
      timeBar.setAdGroupTimesMs(adGroupTimesMs, playedAdGroups, totalAdGroupCount);
    }
    updateProgress();
  }

  private void updateProgress() {
    if (!isVisible() || !isAttachedToWindow) {
      return;
    }
    @Nullable Player player = this.player;
    long position = 0;
    long bufferedPosition = 0;
    if (player != null && player.isCommandAvailable(COMMAND_GET_CURRENT_MEDIA_ITEM)) {
      position = currentWindowOffset + player.getContentPosition();
      bufferedPosition = currentWindowOffset + player.getContentBufferedPosition();
    }
    if (positionView != null && !scrubbing) {
      positionView.setText(Util.getStringForTime(formatBuilder, formatter, position));
    }
    if (timeBar != null) {
      timeBar.setPosition(position);
      timeBar.setBufferedPosition(bufferedPosition);
    }
    if (progressUpdateListener != null) {
      progressUpdateListener.onProgressUpdate(position, bufferedPosition);
    }

    // Cancel any pending updates and schedule a new one if necessary.
    removeCallbacks(updateProgressAction);
    int playbackState = player == null ? Player.STATE_IDLE : player.getPlaybackState();
    if (player != null && player.isPlaying()) {
      long mediaTimeDelayMs =
          timeBar != null ? timeBar.getPreferredUpdateDelay() : MAX_UPDATE_INTERVAL_MS;

      // Limit delay to the start of the next full second to ensure position display is smooth.
      long mediaTimeUntilNextFullSecondMs = 1000 - position % 1000;
      mediaTimeDelayMs = Math.min(mediaTimeDelayMs, mediaTimeUntilNextFullSecondMs);

      // Calculate the delay until the next update in real time, taking playback speed into account.
      float playbackSpeed = player.getPlaybackParameters().speed;
      long delayMs =
          playbackSpeed > 0 ? (long) (mediaTimeDelayMs / playbackSpeed) : MAX_UPDATE_INTERVAL_MS;

      // Constrain the delay to avoid too frequent / infrequent updates.
      delayMs = Util.constrainValue(delayMs, timeBarMinUpdateIntervalMs, MAX_UPDATE_INTERVAL_MS);
      postDelayed(updateProgressAction, delayMs);
    } else if (playbackState != Player.STATE_ENDED && playbackState != Player.STATE_IDLE) {
      postDelayed(updateProgressAction, MAX_UPDATE_INTERVAL_MS);
    }
  }

  private void updatePlaybackSpeedList() {
    if (player == null) {
      return;
    }
    playbackSpeedAdapter.updateSelectedIndex(player.getPlaybackParameters().speed);
    settingsAdapter.setSubTextAtPosition(
        SETTINGS_PLAYBACK_SPEED_POSITION, playbackSpeedAdapter.getSelectedText());
    updateSettingsButton();
  }

  private void updateSettingsButton() {
    updateButton(settingsAdapter.hasSettingsToShow(), settingsButton);
  }

  private void updateSettingsWindowSize() {
    settingsView.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);

    int maxWidth = getWidth() - settingsWindowMargin * 2;
    int itemWidth = settingsView.getMeasuredWidth();
    int width = Math.min(itemWidth, maxWidth);
    settingsWindow.setWidth(width);

    int maxHeight = getHeight() - settingsWindowMargin * 2;
    int totalHeight = settingsView.getMeasuredHeight();
    int height = Math.min(maxHeight, totalHeight);
    settingsWindow.setHeight(height);
  }

  private void displaySettingsWindow(RecyclerView.Adapter<?> adapter, View anchorView) {
    settingsView.setAdapter(adapter);

    updateSettingsWindowSize();

    needToHideBars = false;
    settingsWindow.dismiss();
    needToHideBars = true;

    int xoff = getWidth() - settingsWindow.getWidth() - settingsWindowMargin;
    int yoff = -settingsWindow.getHeight() - settingsWindowMargin;

    settingsWindow.showAsDropDown(anchorView, xoff, yoff);
  }

  private void setPlaybackSpeed(float speed) {
    if (player == null || !player.isCommandAvailable(COMMAND_SET_SPEED_AND_PITCH)) {
      return;
    }
    player.setPlaybackParameters(player.getPlaybackParameters().withSpeed(speed));
  }

  /* package */ void requestPlayPauseFocus() {
    if (playPauseButton != null) {
      playPauseButton.requestFocus();
    }
  }

  private void updateButton(boolean enabled, @Nullable View view) {
    if (view == null) {
      return;
    }
    view.setEnabled(enabled);
    view.setAlpha(enabled ? buttonAlphaEnabled : buttonAlphaDisabled);
  }

  private void seekToTimeBarPosition(Player player, long positionMs) {
    if (multiWindowTimeBar) {
      if (player.isCommandAvailable(COMMAND_GET_TIMELINE)
          && player.isCommandAvailable(COMMAND_SEEK_TO_MEDIA_ITEM)) {
        Timeline timeline = player.getCurrentTimeline();
        int windowCount = timeline.getWindowCount();
        int windowIndex = 0;
        while (true) {
          long windowDurationMs = timeline.getWindow(windowIndex, window).getDurationMs();
          if (positionMs < windowDurationMs) {
            break;
          } else if (windowIndex == windowCount - 1) {
            // Seeking past the end of the last window should seek to the end of the timeline.
            positionMs = windowDurationMs;
            break;
          }
          positionMs -= windowDurationMs;
          windowIndex++;
        }
        player.seekTo(windowIndex, positionMs);
      }
    } else if (player.isCommandAvailable(COMMAND_SEEK_IN_CURRENT_MEDIA_ITEM)) {
      player.seekTo(positionMs);
    }
    updateProgress();
  }

  private void onFullScreenButtonClicked(View v) {
    if (onFullScreenModeChangedListener == null) {
      return;
    }

    isFullScreen = !isFullScreen;
    updateFullScreenButtonForState(fullScreenButton, isFullScreen);
    updateFullScreenButtonForState(minimalFullScreenButton, isFullScreen);
    if (onFullScreenModeChangedListener != null) {
      onFullScreenModeChangedListener.onFullScreenModeChanged(isFullScreen);
    }
  }

  private void updateFullScreenButtonForState(
      @Nullable ImageView fullScreenButton, boolean isFullScreen) {
    if (fullScreenButton == null) {
      return;
    }
    if (isFullScreen) {
      fullScreenButton.setImageDrawable(fullScreenExitDrawable);
      fullScreenButton.setContentDescription(fullScreenExitContentDescription);
    } else {
      fullScreenButton.setImageDrawable(fullScreenEnterDrawable);
      fullScreenButton.setContentDescription(fullScreenEnterContentDescription);
    }
  }

  private void onSettingViewClicked(int position) {
    if (position == SETTINGS_PLAYBACK_SPEED_POSITION) {
      displaySettingsWindow(playbackSpeedAdapter, checkNotNull(settingsButton));
    } else if (position == SETTINGS_AUDIO_TRACK_SELECTION_POSITION) {
      displaySettingsWindow(audioTrackSelectionAdapter, checkNotNull(settingsButton));
    } else {
      settingsWindow.dismiss();
    }
  }

  @Override
  public void onAttachedToWindow() {
    super.onAttachedToWindow();
    controlViewLayoutManager.onAttachedToWindow();
    isAttachedToWindow = true;
    if (isFullyVisible()) {
      controlViewLayoutManager.resetHideCallbacks();
    }
    updateAll();
  }

  @Override
  public void onDetachedFromWindow() {
    super.onDetachedFromWindow();
    controlViewLayoutManager.onDetachedFromWindow();
    isAttachedToWindow = false;
    removeCallbacks(updateProgressAction);
    controlViewLayoutManager.removeHideCallbacks();
  }

  @Override
  public boolean dispatchKeyEvent(KeyEvent event) {
    return dispatchMediaKeyEvent(event) || super.dispatchKeyEvent(event);
  }

  /**
   * Called to process media key events. Any {@link KeyEvent} can be passed but only media key
   * events will be handled.
   *
   * @param event A key event.
   * @return Whether the key event was handled.
   */
  public boolean dispatchMediaKeyEvent(KeyEvent event) {
    int keyCode = event.getKeyCode();
    @Nullable Player player = this.player;
    if (player == null || !isHandledMediaKey(keyCode)) {
      return false;
    }
    if (event.getAction() == KeyEvent.ACTION_DOWN) {
      if (keyCode == KeyEvent.KEYCODE_MEDIA_FAST_FORWARD) {
        if (player.getPlaybackState() != Player.STATE_ENDED
            && player.isCommandAvailable(COMMAND_SEEK_FORWARD)) {
          player.seekForward();
        }
      } else if (keyCode == KeyEvent.KEYCODE_MEDIA_REWIND
          && player.isCommandAvailable(COMMAND_SEEK_BACK)) {
        player.seekBack();
      } else if (event.getRepeatCount() == 0) {
        switch (keyCode) {
          case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE:
          case KeyEvent.KEYCODE_HEADSETHOOK:
            dispatchPlayPause(player);
            break;
          case KeyEvent.KEYCODE_MEDIA_PLAY:
            dispatchPlay(player);
            break;
          case KeyEvent.KEYCODE_MEDIA_PAUSE:
            dispatchPause(player);
            break;
          case KeyEvent.KEYCODE_MEDIA_NEXT:
            if (player.isCommandAvailable(COMMAND_SEEK_TO_NEXT)) {
              player.seekToNext();
            }
            break;
          case KeyEvent.KEYCODE_MEDIA_PREVIOUS:
            if (player.isCommandAvailable(COMMAND_SEEK_TO_PREVIOUS)) {
              player.seekToPrevious();
            }
            break;
          default:
            break;
        }
      }
    }
    return true;
  }

  @Override
  protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
    super.onLayout(changed, left, top, right, bottom);
    controlViewLayoutManager.onLayout(changed, left, top, right, bottom);
  }

  private void onLayoutChange(
      View v,
      int left,
      int top,
      int right,
      int bottom,
      int oldLeft,
      int oldTop,
      int oldRight,
      int oldBottom) {
    int width = right - left;
    int height = bottom - top;
    int oldWidth = oldRight - oldLeft;
    int oldHeight = oldBottom - oldTop;

    if ((width != oldWidth || height != oldHeight) && settingsWindow.isShowing()) {
      updateSettingsWindowSize();
      int xOffset = getWidth() - settingsWindow.getWidth() - settingsWindowMargin;
      int yOffset = -settingsWindow.getHeight() - settingsWindowMargin;
      settingsWindow.update(v, xOffset, yOffset, -1, -1);
    }
  }

  private boolean shouldEnablePlayPauseButton() {
    return player != null
        && player.isCommandAvailable(COMMAND_PLAY_PAUSE)
        && (!player.isCommandAvailable(COMMAND_GET_TIMELINE)
            || !player.getCurrentTimeline().isEmpty());
  }

  private boolean shouldShowPauseButton() {
    return player != null
        && player.getPlaybackState() != Player.STATE_ENDED
        && player.getPlaybackState() != Player.STATE_IDLE
        && player.getPlayWhenReady();
  }

  private void dispatchPlayPause(Player player) {
    @State int state = player.getPlaybackState();
    if (state == Player.STATE_IDLE || state == Player.STATE_ENDED || !player.getPlayWhenReady()) {
      dispatchPlay(player);
    } else {
      dispatchPause(player);
    }
  }

  private void dispatchPlay(Player player) {
    @State int state = player.getPlaybackState();
    if (state == Player.STATE_IDLE && player.isCommandAvailable(COMMAND_PREPARE)) {
      player.prepare();
    } else if (state == Player.STATE_ENDED
        && player.isCommandAvailable(COMMAND_SEEK_TO_DEFAULT_POSITION)) {
      player.seekToDefaultPosition();
    }
    if (player.isCommandAvailable(COMMAND_PLAY_PAUSE)) {
      player.play();
    }
  }

  private void dispatchPause(Player player) {
    if (player.isCommandAvailable(COMMAND_PLAY_PAUSE)) {
      player.pause();
    }
  }

  @SuppressLint("InlinedApi")
  private static boolean isHandledMediaKey(int keyCode) {
    return keyCode == KeyEvent.KEYCODE_MEDIA_FAST_FORWARD
        || keyCode == KeyEvent.KEYCODE_MEDIA_REWIND
        || keyCode == KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE
        || keyCode == KeyEvent.KEYCODE_HEADSETHOOK
        || keyCode == KeyEvent.KEYCODE_MEDIA_PLAY
        || keyCode == KeyEvent.KEYCODE_MEDIA_PAUSE
        || keyCode == KeyEvent.KEYCODE_MEDIA_NEXT
        || keyCode == KeyEvent.KEYCODE_MEDIA_PREVIOUS;
  }

  /**
   * Returns whether the specified {@code player} can be shown on a multi-window time bar.
   *
   * @param player The {@link Player} to check.
   * @param window A scratch {@link Timeline.Window} instance.
   * @return Whether the specified timeline can be shown on a multi-window time bar.
   */
  private static boolean canShowMultiWindowTimeBar(Player player, Timeline.Window window) {
    if (!player.isCommandAvailable(COMMAND_GET_TIMELINE)) {
      return false;
    }
    Timeline timeline = player.getCurrentTimeline();
    int windowCount = timeline.getWindowCount();
    if (windowCount <= 1 || windowCount > MAX_WINDOWS_FOR_MULTI_WINDOW_TIME_BAR) {
      return false;
    }
    for (int i = 0; i < windowCount; i++) {
      if (timeline.getWindow(i, window).durationUs == C.TIME_UNSET) {
        return false;
      }
    }
    return true;
  }

  private static void initializeFullScreenButton(View fullScreenButton, OnClickListener listener) {
    if (fullScreenButton == null) {
      return;
    }
    fullScreenButton.setVisibility(GONE);
    fullScreenButton.setOnClickListener(listener);
  }

  private static void updateFullScreenButtonVisibility(
      @Nullable View fullScreenButton, boolean visible) {
    if (fullScreenButton == null) {
      return;
    }
    if (visible) {
      fullScreenButton.setVisibility(VISIBLE);
    } else {
      fullScreenButton.setVisibility(GONE);
    }
  }

  @SuppressWarnings("ResourceType")
  private static @RepeatModeUtil.RepeatToggleModes int getRepeatToggleModes(
      TypedArray a, @RepeatModeUtil.RepeatToggleModes int defaultValue) {
    return a.getInt(R.styleable.PlayerControlView_repeat_toggle_modes, defaultValue);
  }

  private final class ComponentListener
      implements Player.Listener,
          TimeBar.OnScrubListener,
          OnClickListener,
          PopupWindow.OnDismissListener {

    @Override
    public void onEvents(Player player, Events events) {
      if (events.containsAny(
          EVENT_PLAYBACK_STATE_CHANGED,
          EVENT_PLAY_WHEN_READY_CHANGED,
          EVENT_AVAILABLE_COMMANDS_CHANGED)) {
        updatePlayPauseButton();
      }
      if (events.containsAny(
          EVENT_PLAYBACK_STATE_CHANGED,
          EVENT_PLAY_WHEN_READY_CHANGED,
          EVENT_IS_PLAYING_CHANGED,
          EVENT_AVAILABLE_COMMANDS_CHANGED)) {
        updateProgress();
      }
      if (events.containsAny(EVENT_REPEAT_MODE_CHANGED, EVENT_AVAILABLE_COMMANDS_CHANGED)) {
        updateRepeatModeButton();
      }
      if (events.containsAny(
          EVENT_SHUFFLE_MODE_ENABLED_CHANGED, EVENT_AVAILABLE_COMMANDS_CHANGED)) {
        updateShuffleButton();
      }
      if (events.containsAny(
          EVENT_REPEAT_MODE_CHANGED,
          EVENT_SHUFFLE_MODE_ENABLED_CHANGED,
          EVENT_POSITION_DISCONTINUITY,
          EVENT_TIMELINE_CHANGED,
          EVENT_SEEK_BACK_INCREMENT_CHANGED,
          EVENT_SEEK_FORWARD_INCREMENT_CHANGED,
          EVENT_AVAILABLE_COMMANDS_CHANGED)) {
        updateNavigation();
      }
      if (events.containsAny(
          EVENT_POSITION_DISCONTINUITY, EVENT_TIMELINE_CHANGED, EVENT_AVAILABLE_COMMANDS_CHANGED)) {
        updateTimeline();
      }
      if (events.containsAny(EVENT_PLAYBACK_PARAMETERS_CHANGED, EVENT_AVAILABLE_COMMANDS_CHANGED)) {
        updatePlaybackSpeedList();
      }
      if (events.containsAny(EVENT_TRACKS_CHANGED, EVENT_AVAILABLE_COMMANDS_CHANGED)) {
        updateTrackLists();
      }
    }

    @Override
    public void onScrubStart(TimeBar timeBar, long position) {
      scrubbing = true;
      if (positionView != null) {
        positionView.setText(Util.getStringForTime(formatBuilder, formatter, position));
      }
      controlViewLayoutManager.removeHideCallbacks();
    }

    @Override
    public void onScrubMove(TimeBar timeBar, long position) {
      if (positionView != null) {
        positionView.setText(Util.getStringForTime(formatBuilder, formatter, position));
      }
    }

    @Override
    public void onScrubStop(TimeBar timeBar, long position, boolean canceled) {
      scrubbing = false;
      if (!canceled && player != null) {
        seekToTimeBarPosition(player, position);
      }
      controlViewLayoutManager.resetHideCallbacks();
    }

    @Override
    public void onDismiss() {
      if (needToHideBars) {
        controlViewLayoutManager.resetHideCallbacks();
      }
    }

    @Override
    public void onClick(View view) {
      @Nullable Player player = PlayerControlView.this.player;
      if (player == null) {
        return;
      }
      controlViewLayoutManager.resetHideCallbacks();
      if (nextButton == view) {
        if (player.isCommandAvailable(COMMAND_SEEK_TO_NEXT)) {
          player.seekToNext();
        }
      } else if (previousButton == view) {
        if (player.isCommandAvailable(COMMAND_SEEK_TO_PREVIOUS)) {
          player.seekToPrevious();
        }
      } else if (fastForwardButton == view) {
        if (player.getPlaybackState() != Player.STATE_ENDED
            && player.isCommandAvailable(COMMAND_SEEK_FORWARD)) {
          player.seekForward();
        }
      } else if (rewindButton == view) {
        if (player.isCommandAvailable(COMMAND_SEEK_BACK)) {
          player.seekBack();
        }
      } else if (playPauseButton == view) {
        dispatchPlayPause(player);
      } else if (repeatToggleButton == view) {
        if (player.isCommandAvailable(COMMAND_SET_REPEAT_MODE)) {
          player.setRepeatMode(
              RepeatModeUtil.getNextRepeatMode(player.getRepeatMode(), repeatToggleModes));
        }
      } else if (shuffleButton == view) {
        if (player.isCommandAvailable(COMMAND_SET_SHUFFLE_MODE)) {
          player.setShuffleModeEnabled(!player.getShuffleModeEnabled());
        }
      } else if (settingsButton == view) {
        controlViewLayoutManager.removeHideCallbacks();
        displaySettingsWindow(settingsAdapter, settingsButton);
      } else if (playbackSpeedButton == view) {
        controlViewLayoutManager.removeHideCallbacks();
        displaySettingsWindow(playbackSpeedAdapter, playbackSpeedButton);
      } else if (audioTrackButton == view) {
        controlViewLayoutManager.removeHideCallbacks();
        displaySettingsWindow(audioTrackSelectionAdapter, audioTrackButton);
      } else if (subtitleButton == view) {
        controlViewLayoutManager.removeHideCallbacks();
        displaySettingsWindow(textTrackSelectionAdapter, subtitleButton);
      }
    }
  }

  private class SettingsAdapter extends RecyclerView.Adapter<SettingViewHolder> {

    private final String[] mainTexts;
    private final String[] subTexts;
    private final Drawable[] iconIds;

    public SettingsAdapter(String[] mainTexts, Drawable[] iconIds) {
      this.mainTexts = mainTexts;
      this.subTexts = new String[mainTexts.length];
      this.iconIds = iconIds;
    }

    @Override
    public SettingViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
      View v =
          LayoutInflater.from(getContext())
              .inflate(R.layout.exo_styled_settings_list_item, parent, /* attachToRoot= */ false);
      return new SettingViewHolder(v);
    }

    @Override
    public void onBindViewHolder(SettingViewHolder holder, int position) {
      if (shouldShowSetting(position)) {
        holder.itemView.setLayoutParams(
            new RecyclerView.LayoutParams(
                ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT));
      } else {
        holder.itemView.setLayoutParams(new RecyclerView.LayoutParams(0, 0));
      }

      holder.mainTextView.setText(mainTexts[position]);

      if (subTexts[position] == null) {
        holder.subTextView.setVisibility(GONE);
      } else {
        holder.subTextView.setText(subTexts[position]);
      }

      if (iconIds[position] == null) {
        holder.iconView.setVisibility(GONE);
      } else {
        holder.iconView.setImageDrawable(iconIds[position]);
      }
    }

    @Override
    public long getItemId(int position) {
      return position;
    }

    @Override
    public int getItemCount() {
      return mainTexts.length;
    }

    public void setSubTextAtPosition(int position, String subText) {
      this.subTexts[position] = subText;
    }

    public boolean hasSettingsToShow() {
      return shouldShowSetting(SETTINGS_AUDIO_TRACK_SELECTION_POSITION)
          || shouldShowSetting(SETTINGS_PLAYBACK_SPEED_POSITION);
    }

    private boolean shouldShowSetting(int position) {
      if (player == null) {
        return false;
      }
      switch (position) {
        case SETTINGS_AUDIO_TRACK_SELECTION_POSITION:
          return player.isCommandAvailable(COMMAND_GET_TRACKS)
              && player.isCommandAvailable(COMMAND_SET_TRACK_SELECTION_PARAMETERS);
        case SETTINGS_PLAYBACK_SPEED_POSITION:
          return player.isCommandAvailable(COMMAND_SET_SPEED_AND_PITCH);
        default:
          return true;
      }
    }
  }

  private final class SettingViewHolder extends RecyclerView.ViewHolder {

    private final TextView mainTextView;
    private final TextView subTextView;
    private final ImageView iconView;

    public SettingViewHolder(View itemView) {
      super(itemView);
      if (Util.SDK_INT < 26) {
        // Workaround for https://github.com/google/ExoPlayer/issues/9061.
        itemView.setFocusable(true);
      }
      mainTextView = itemView.findViewById(R.id.exo_main_text);
      subTextView = itemView.findViewById(R.id.exo_sub_text);
      iconView = itemView.findViewById(R.id.exo_icon);
      itemView.setOnClickListener(v -> onSettingViewClicked(getAdapterPosition()));
    }
  }

  private final class PlaybackSpeedAdapter extends RecyclerView.Adapter<SubSettingViewHolder> {

    private final String[] playbackSpeedTexts;
    private final float[] playbackSpeeds;
    private int selectedIndex;

    public PlaybackSpeedAdapter(String[] playbackSpeedTexts, float[] playbackSpeeds) {
      this.playbackSpeedTexts = playbackSpeedTexts;
      this.playbackSpeeds = playbackSpeeds;
    }

    public void updateSelectedIndex(float playbackSpeed) {
      int closestMatchIndex = 0;
      float closestMatchDifference = Float.MAX_VALUE;
      for (int i = 0; i < playbackSpeeds.length; i++) {
        float difference = Math.abs(playbackSpeed - playbackSpeeds[i]);
        if (difference < closestMatchDifference) {
          closestMatchIndex = i;
          closestMatchDifference = difference;
        }
      }
      selectedIndex = closestMatchIndex;
    }

    public String getSelectedText() {
      return playbackSpeedTexts[selectedIndex];
    }

    @Override
    public SubSettingViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
      View v =
          LayoutInflater.from(getContext())
              .inflate(
                  R.layout.exo_styled_sub_settings_list_item, parent, /* attachToRoot= */ false);
      return new SubSettingViewHolder(v);
    }

    @Override
    public void onBindViewHolder(SubSettingViewHolder holder, int position) {
      if (position < playbackSpeedTexts.length) {
        holder.textView.setText(playbackSpeedTexts[position]);
      }
      if (position == selectedIndex) {
        holder.itemView.setSelected(true);
        holder.checkView.setVisibility(VISIBLE);
      } else {
        holder.itemView.setSelected(false);
        holder.checkView.setVisibility(INVISIBLE);
      }
      holder.itemView.setOnClickListener(
          v -> {
            if (position != selectedIndex) {
              setPlaybackSpeed(playbackSpeeds[position]);
            }
            settingsWindow.dismiss();
          });
    }

    @Override
    public int getItemCount() {
      return playbackSpeedTexts.length;
    }
  }

  private static final class TrackInformation {

    public final Tracks.Group trackGroup;
    public final int trackIndex;
    public final String trackName;

    public TrackInformation(Tracks tracks, int trackGroupIndex, int trackIndex, String trackName) {
      this.trackGroup = tracks.getGroups().get(trackGroupIndex);
      this.trackIndex = trackIndex;
      this.trackName = trackName;
    }

    public boolean isSelected() {
      return trackGroup.isTrackSelected(trackIndex);
    }
  }

  private final class TextTrackSelectionAdapter extends TrackSelectionAdapter {
    @Override
    public void init(List<TrackInformation> trackInformations) {
      boolean subtitleIsOn = false;
      for (int i = 0; i < trackInformations.size(); i++) {
        if (trackInformations.get(i).isSelected()) {
          subtitleIsOn = true;
          break;
        }
      }

      if (subtitleButton != null) {
        subtitleButton.setImageDrawable(
            subtitleIsOn ? subtitleOnButtonDrawable : subtitleOffButtonDrawable);
        subtitleButton.setContentDescription(
            subtitleIsOn ? subtitleOnContentDescription : subtitleOffContentDescription);
      }
      this.tracks = trackInformations;
    }

    @Override
    public void onBindViewHolderAtZeroPosition(SubSettingViewHolder holder) {
      // CC options include "Off" at the first position, which disables text rendering.
      holder.textView.setText(R.string.exo_track_selection_none);
      boolean isTrackSelectionOff = true;
      for (int i = 0; i < tracks.size(); i++) {
        if (tracks.get(i).isSelected()) {
          isTrackSelectionOff = false;
          break;
        }
      }
      holder.checkView.setVisibility(isTrackSelectionOff ? VISIBLE : INVISIBLE);
      holder.itemView.setOnClickListener(
          v -> {
            if (player != null
                && player.isCommandAvailable(COMMAND_SET_TRACK_SELECTION_PARAMETERS)) {
              TrackSelectionParameters trackSelectionParameters =
                  player.getTrackSelectionParameters();
              player.setTrackSelectionParameters(
                  trackSelectionParameters
                      .buildUpon()
                      .clearOverridesOfType(C.TRACK_TYPE_TEXT)
                      .setIgnoredTextSelectionFlags(~C.SELECTION_FLAG_FORCED)
                      .build());
              settingsWindow.dismiss();
            }
          });
    }

    @Override
    public void onBindViewHolder(SubSettingViewHolder holder, int position) {
      super.onBindViewHolder(holder, position);
      if (position > 0) {
        TrackInformation track = tracks.get(position - 1);
        holder.checkView.setVisibility(track.isSelected() ? VISIBLE : INVISIBLE);
      }
    }

    @Override
    public void onTrackSelection(String subtext) {
      // No-op
    }
  }

  private final class AudioTrackSelectionAdapter extends TrackSelectionAdapter {

    @Override
    public void onBindViewHolderAtZeroPosition(SubSettingViewHolder holder) {
      // Audio track selection option includes "Auto" at the top.
      holder.textView.setText(R.string.exo_track_selection_auto);
      // hasSelectionOverride is true means there is an explicit track selection, not "Auto".
      TrackSelectionParameters parameters = checkNotNull(player).getTrackSelectionParameters();
      boolean hasSelectionOverride = hasSelectionOverride(parameters);
      holder.checkView.setVisibility(hasSelectionOverride ? INVISIBLE : VISIBLE);
      holder.itemView.setOnClickListener(
          v -> {
            if (player == null
                || !player.isCommandAvailable(COMMAND_SET_TRACK_SELECTION_PARAMETERS)) {
              return;
            }
            TrackSelectionParameters trackSelectionParameters =
                player.getTrackSelectionParameters();
            castNonNull(player)
                .setTrackSelectionParameters(
                    trackSelectionParameters
                        .buildUpon()
                        .clearOverridesOfType(C.TRACK_TYPE_AUDIO)
                        .setTrackTypeDisabled(C.TRACK_TYPE_AUDIO, /* disabled= */ false)
                        .build());
            settingsAdapter.setSubTextAtPosition(
                SETTINGS_AUDIO_TRACK_SELECTION_POSITION,
                getResources().getString(R.string.exo_track_selection_auto));
            settingsWindow.dismiss();
          });
    }

    private boolean hasSelectionOverride(TrackSelectionParameters trackSelectionParameters) {
      for (int i = 0; i < tracks.size(); i++) {
        TrackGroup trackGroup = tracks.get(i).trackGroup.getMediaTrackGroup();
        if (trackSelectionParameters.overrides.containsKey(trackGroup)) {
          return true;
        }
      }
      return false;
    }

    @Override
    public void onTrackSelection(String subtext) {
      settingsAdapter.setSubTextAtPosition(SETTINGS_AUDIO_TRACK_SELECTION_POSITION, subtext);
    }

    @Override
    public void init(List<TrackInformation> trackInformations) {
      this.tracks = trackInformations;
      // Update subtext in settings menu with current audio track selection.
      TrackSelectionParameters params = checkNotNull(player).getTrackSelectionParameters();
      if (trackInformations.isEmpty()) {
        settingsAdapter.setSubTextAtPosition(
            SETTINGS_AUDIO_TRACK_SELECTION_POSITION,
            getResources().getString(R.string.exo_track_selection_none));
        // TODO(insun) : Make the audio item in main settings (settingsAdapater)
        //  to be non-clickable.
      } else if (!hasSelectionOverride(params)) {
        settingsAdapter.setSubTextAtPosition(
            SETTINGS_AUDIO_TRACK_SELECTION_POSITION,
            getResources().getString(R.string.exo_track_selection_auto));
      } else {
        for (int i = 0; i < trackInformations.size(); i++) {
          TrackInformation track = trackInformations.get(i);
          if (track.isSelected()) {
            settingsAdapter.setSubTextAtPosition(
                SETTINGS_AUDIO_TRACK_SELECTION_POSITION, track.trackName);
            break;
          }
        }
      }
    }
  }

  private abstract class TrackSelectionAdapter extends RecyclerView.Adapter<SubSettingViewHolder> {

    protected List<TrackInformation> tracks;

    protected TrackSelectionAdapter() {
      this.tracks = new ArrayList<>();
    }

    public abstract void init(List<TrackInformation> trackInformations);

    @Override
    public SubSettingViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
      View v =
          LayoutInflater.from(getContext())
              .inflate(
                  R.layout.exo_styled_sub_settings_list_item, parent, /* attachToRoot= */ false);
      return new SubSettingViewHolder(v);
    }

    protected abstract void onBindViewHolderAtZeroPosition(SubSettingViewHolder holder);

    protected abstract void onTrackSelection(String subtext);

    @Override
    public void onBindViewHolder(SubSettingViewHolder holder, int position) {
      @Nullable Player player = PlayerControlView.this.player;
      if (player == null) {
        return;
      }
      if (position == 0) {
        onBindViewHolderAtZeroPosition(holder);
      } else {
        TrackInformation track = tracks.get(position - 1);
        TrackGroup mediaTrackGroup = track.trackGroup.getMediaTrackGroup();
        TrackSelectionParameters params = player.getTrackSelectionParameters();
        boolean explicitlySelected =
            params.overrides.get(mediaTrackGroup) != null && track.isSelected();
        holder.textView.setText(track.trackName);
        holder.checkView.setVisibility(explicitlySelected ? VISIBLE : INVISIBLE);
        holder.itemView.setOnClickListener(
            v -> {
              if (!player.isCommandAvailable(COMMAND_SET_TRACK_SELECTION_PARAMETERS)) {
                return;
              }
              TrackSelectionParameters trackSelectionParameters =
                  player.getTrackSelectionParameters();
              player.setTrackSelectionParameters(
                  trackSelectionParameters
                      .buildUpon()
                      .setOverrideForType(
                          new TrackSelectionOverride(
                              mediaTrackGroup, ImmutableList.of(track.trackIndex)))
                      .setTrackTypeDisabled(track.trackGroup.getType(), /* disabled= */ false)
                      .build());
              onTrackSelection(track.trackName);
              settingsWindow.dismiss();
            });
      }
    }

    @Override
    public int getItemCount() {
      return tracks.isEmpty() ? 0 : tracks.size() + 1;
    }

    protected void clear() {
      tracks = Collections.emptyList();
    }
  }

  private static class SubSettingViewHolder extends RecyclerView.ViewHolder {

    public final TextView textView;
    public final View checkView;

    public SubSettingViewHolder(View itemView) {
      super(itemView);
      if (Util.SDK_INT < 26) {
        // Workaround for https://github.com/google/ExoPlayer/issues/9061.
        itemView.setFocusable(true);
      }
      textView = itemView.findViewById(R.id.exo_text);
      checkView = itemView.findViewById(R.id.exo_check);
    }
  }
}