MediaControlView.java

/*
 * Copyright 2018 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.media2.widget;

import static androidx.media2.MediaController.ControllerResult.RESULT_CODE_NOT_SUPPORTED;
import static androidx.media2.MediaController.ControllerResult.RESULT_CODE_SUCCESS;

import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.animation.ValueAnimator;
import android.content.Context;
import android.content.DialogInterface;
import android.content.pm.ActivityInfo;
import android.content.res.Resources;
import android.graphics.Point;
import android.graphics.drawable.ScaleDrawable;
import android.net.Uri;
import android.os.Bundle;
import android.util.AttributeSet;
import android.util.DisplayMetrics;
import android.util.Log;
import android.view.Gravity;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.view.accessibility.AccessibilityManager;
import android.view.animation.LinearInterpolator;
import android.widget.AdapterView;
import android.widget.BaseAdapter;
import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.ListView;
import android.widget.PopupWindow;
import android.widget.ProgressBar;
import android.widget.RelativeLayout;
import android.widget.SeekBar;
import android.widget.SeekBar.OnSeekBarChangeListener;
import android.widget.TextView;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.annotation.VisibleForTesting;
import androidx.appcompat.app.AlertDialog;
import androidx.core.content.ContextCompat;
import androidx.media2.MediaController;
import androidx.media2.MediaItem;
import androidx.media2.MediaMetadata;
import androidx.media2.MediaPlayer;
import androidx.media2.MediaSession;
import androidx.media2.SessionCommand;
import androidx.media2.SessionCommandGroup;
import androidx.media2.SessionPlayer;
import androidx.media2.SessionToken;
import androidx.media2.UriMediaItem;
import androidx.mediarouter.app.MediaRouteButton;
import androidx.mediarouter.media.MediaRouteSelector;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Formatter;
import java.util.List;
import java.util.Locale;
import java.util.concurrent.Executor;

/**
 * A View that contains the controls for {@link MediaPlayer}.
 * It provides a wide range of buttons that serve the following functions: play/pause,
 * rewind/fast-forward, skip to next/previous, select subtitle track, enter/exit full screen mode,
 * adjust video quality, select audio track, and adjust playback speed.
 * <p>
 * The easiest way to use a MediaControlView is by creating a {@link VideoView}, which will
 * internally create a MediaControlView instance and handle all the commands from buttons inside
 * MediaControlView. For more information, refer to {@link VideoView}.
 *
 * It is also possible to create a MediaControlView programmatically and add it to a custom video
 * view. In this case, the app will need to create a {@link MediaSession} instance and set
 * {@link SessionToken its token} inside MediaControlView by calling
 * {@link #setSessionToken(SessionToken)}. Then MediaControlView will create a
 * {@link MediaController} and could send commands to the connected {@link MediaSession session}.
 * By default, the buttons inside MediaControlView will not visible unless the corresponding
 * {@link SessionCommand} is marked as allowed. For more details, refer to {@link MediaSession}.
 * <p>
 * Currently, MediaControlView animates off-screen in two steps:
 *   1) Title and bottom bars slide up and down respectively and the transport controls fade out,
 *      leaving only the progress bar at the bottom of the view.
 *   2) Progress bar slides down off-screen.
 * <p>
 * In addition, the following customizations are supported:
 * 1) Set focus to the play/pause button by calling {@link #requestPlayButtonFocus()}.
 * 2) Set full screen behavior by calling {@link #setOnFullScreenListener(OnFullScreenListener)}
 *
 */
@RequiresApi(19) // TODO correct minSdk API use incompatibilities and remove before release.
public class MediaControlView extends BaseLayout {
    private static final String TAG = "MediaControlView";
    static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);

    static final String KEY_VIDEO_TRACK_COUNT = "VideoTrackCount";
    static final String KEY_AUDIO_TRACK_COUNT = "AudioTrackCount";
    static final String KEY_SUBTITLE_TRACK_COUNT = "SubtitleTrackCount";
    static final String KEY_SUBTITLE_TRACK_LANGUAGE_LIST = "SubtitleTrackLanguageList";
    static final String KEY_SELECTED_AUDIO_INDEX = "SelectedAudioIndex";
    static final String KEY_SELECTED_SUBTITLE_INDEX = "SelectedSubtitleIndex";
    static final String EVENT_UPDATE_TRACK_STATUS = "UpdateTrackStatus";
    static final String KEY_STATE_IS_ADVERTISEMENT = "MediaTypeAdvertisement";
    static final String EVENT_UPDATE_MEDIA_TYPE_STATUS = "UpdateMediaTypeStatus";
    static final String EVENT_UPDATE_SUBTITLE_SELECTED = "UpdateSubtitleSelected";
    static final String EVENT_UPDATE_SUBTITLE_DESELECTED = "UpdateSubtitleDeselected";

    // String for sending command to show subtitle to MediaSession.
    static final String COMMAND_SHOW_SUBTITLE = "showSubtitle";
    // String for sending command to hide subtitle to MediaSession.
    static final String COMMAND_HIDE_SUBTITLE = "hideSubtitle";
    // String for sending command to select audio track to MediaSession.
    static final String COMMAND_SELECT_AUDIO_TRACK = "SelectTrack";

    private static final int SETTINGS_MODE_AUDIO_TRACK = 0;
    private static final int SETTINGS_MODE_PLAYBACK_SPEED = 1;
    private static final int SETTINGS_MODE_SUBTITLE_TRACK = 2;
    private static final int SETTINGS_MODE_VIDEO_QUALITY = 3;
    private static final int SETTINGS_MODE_MAIN = 4;
    private static final int PLAYBACK_SPEED_1x_INDEX = 3;

    private static final int MEDIA_TYPE_DEFAULT = 0;
    private static final int MEDIA_TYPE_MUSIC = 1;
    private static final int MEDIA_TYPE_ADVERTISEMENT = 2;

    private static final int BOTTOM_BAR_RIGHT_VIEW_MAX_ICON_NUM_DEFAULT = 3;
    private static final int BOTTOM_BAR_RIGHT_VIEW_MAX_ICON_NUM_MUSIC = 2;

    private static final int SIZE_TYPE_EMBEDDED = 0;
    private static final int SIZE_TYPE_FULL = 1;
    private static final int SIZE_TYPE_MINIMAL = 2;

    // Int for defining the UX state where all the views (TitleBar, ProgressBar, BottomBar) are
    // all visible.
    private static final int UX_STATE_ALL_VISIBLE = 0;
    // Int for defining the UX state where only the ProgressBar view is visible.
    private static final int UX_STATE_ONLY_PROGRESS_VISIBLE = 1;
    // Int for defining the UX state where none of the views are visible.
    private static final int UX_STATE_NONE_VISIBLE = 2;
    // Int for defining the UX state where the views are being animated (shown or hidden).
    private static final int UX_STATE_ANIMATING = 3;

    private static final long DEFAULT_SHOW_CONTROLLER_INTERVAL_MS = 2000;
    private static final long DEFAULT_PROGRESS_UPDATE_TIME_MS = 1000;
    private static final long REWIND_TIME_MS = 10000;
    private static final long FORWARD_TIME_MS = 30000;
    private static final long AD_SKIP_WAIT_TIME_MS = 5000;
    private static final long HIDE_TIME_MS = 250;
    private static final long SHOW_TIME_MS = 250;
    private static final int MAX_PROGRESS = 1000;
    private static final int MAX_SCALE_LEVEL = 10000;
    private static final int RESOURCE_NON_EXISTENT = -1;
    private static final int SEEK_POSITION_NOT_SET = -1;
    private static final String RESOURCE_EMPTY = "";

    Resources mResources;
    Controller mController;
    OnFullScreenListener mOnFullScreenListener;
    private AccessibilityManager mAccessibilityManager;
    private WindowManager mWindowManager;
    private int mPrevWidth;
    private int mPrevOrientation;
    private int mOriginalLeftBarWidth;
    private int mMaxTimeViewWidth;
    private int mEmbeddedSettingsItemWidth;
    private int mFullSettingsItemWidth;
    private int mSettingsItemHeight;
    private int mSettingsWindowMargin;
    private int mIconSize;
    int mVideoTrackCount;
    int mAudioTrackCount;
    int mSubtitleTrackCount;
    int mSettingsMode;
    int mSelectedSubtitleTrackIndex;
    int mSelectedAudioTrackIndex;
    int mSelectedVideoQualityIndex;
    int mSelectedSpeedIndex;
    int mMediaType;
    // TODO: Add lock for accessing mSizeType and mUxState (b/111862062)
    int mSizeType;
    int mUxState;
    long mDuration;
    long mPlaybackActions;
    long mShowControllerIntervalMs;
    // TODO: Add lock for accessing mCurrentSeekPosition and mNextSeekPosition (b/111862062)
    long mCurrentSeekPosition;
    long mNextSeekPosition;
    boolean mDragging;
    boolean mIsFullScreen;
    boolean mOverflowIsShowing;
    boolean mIsStopped;
    boolean mSeekAvailable;
    boolean mIsAdvertisement;
    boolean mNeedToHideBars;
    boolean mWasPlaying;

    // Relating to Title Bar View
    private ViewGroup mRoot;
    private View mTitleBar;
    private TextView mTitleView;
    private View mAdExternalLink;
    private ImageButton mBackButton;
    private MediaRouteButton mRouteButton;
    private MediaRouteSelector mRouteSelector;

    // Relating to Center View
    private ViewGroup mCenterView;
    View mTransportControls;
    ImageButton mPlayPauseButton;
    ImageButton mFfwdButton;
    ImageButton mRewButton;
    // TODO: Disable Next/Previous buttons when the current item does not have a next/previous
    // item in the playlist. (b/119159436)
    private ImageButton mNextButton;
    private ImageButton mPrevButton;

    // Relating to Minimal Size Fullscreen View
    private LinearLayout mMinimalSizeFullScreenView;

    // Relating to Progress Bar View
    View mProgressBar;
    ProgressBar mProgress;
    private View mProgressBuffer;

    // Relating to Bottom Bar View
    private ViewGroup mBottomBar;

    // Relating to Bottom Bar Left View
    private ViewGroup mBottomBarLeftView;
    private ViewGroup mTimeView;
    private TextView mEndTime;
    TextView mCurrentTime;
    private TextView mAdSkipView;
    private StringBuilder mFormatBuilder;
    private Formatter mFormatter;

    // Relating to Bottom Bar Right View
    private ViewGroup mBottomBarRightView;
    ViewGroup mBasicControls;
    ViewGroup mExtraControls;
    ViewGroup mCustomButtons;
    ImageButton mSubtitleButton;
    ImageButton mFullScreenButton;
    ImageButton mOverflowShowButton;
    ImageButton mOverflowHideButton;
    private ImageButton mVideoQualityButton;
    private ImageButton mSettingsButton;
    private TextView mAdRemainingView;

    // Relating to Settings List View
    private ListView mSettingsListView;
    private PopupWindow mSettingsWindow;
    SettingsAdapter mSettingsAdapter;
    SubSettingsAdapter mSubSettingsAdapter;
    private List<String> mSettingsMainTextsList;
    List<String> mSettingsSubTextsList;
    private List<Integer> mSettingsIconIdsList;
    List<String> mSubtitleDescriptionsList;
    List<String> mAudioTrackList;
    List<String> mVideoQualityList;
    List<String> mPlaybackSpeedTextList;
    List<Integer> mPlaybackSpeedMultBy100List;
    int mCustomPlaybackSpeedIndex;

    AnimatorSet mHideMainBarsAnimator;
    AnimatorSet mHideProgressBarAnimator;
    AnimatorSet mHideAllBarsAnimator;
    AnimatorSet mShowMainBarsAnimator;
    AnimatorSet mShowAllBarsAnimator;
    ValueAnimator mOverflowShowAnimator;
    ValueAnimator mOverflowHideAnimator;

    public MediaControlView(@NonNull Context context) {
        this(context, null);
    }

    public MediaControlView(@NonNull Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public MediaControlView(@NonNull Context context, @Nullable AttributeSet attrs,
            int defStyleAttr) {
        super(context, attrs, defStyleAttr);

        mResources = context.getResources();
        mController = new Controller();
        // Inflate MediaControlView from XML
        mRoot = makeControllerView();
        addView(mRoot);
        mShowControllerIntervalMs = DEFAULT_SHOW_CONTROLLER_INTERVAL_MS;
        mAccessibilityManager = (AccessibilityManager) context.getSystemService(
                Context.ACCESSIBILITY_SERVICE);
    }

    /**
     * Sets MediaSession token to control corresponding MediaSession. It makes it possible to
     * send and receive data between MediaControlView and VideoView.
     */
    public void setSessionToken(@NonNull SessionToken token) {
        mController.setSessionToken(token);
        if (mController.hasMetadata()) {
            updateMetadata();
        }
    }

    /**
     * Registers a callback to be invoked when the fullscreen mode should be changed.
     * This needs to be implemented in order to display the fullscreen button.
     * @param l The callback that will be run
     */
    public void setOnFullScreenListener(@NonNull OnFullScreenListener l) {
        mOnFullScreenListener = l;
        mFullScreenButton.setVisibility(View.VISIBLE);
    }

    /**
     *  Requests focus for the play/pause button.
     */
    public void requestPlayButtonFocus() {
        if (mPlayPauseButton != null) {
            mPlayPauseButton.requestFocus();
        }
    }

    /**
     * Interface definition of a callback to be invoked to inform the fullscreen mode is changed.
     * Application should handle the fullscreen mode accordingly.
     */
    public interface OnFullScreenListener {
        /**
         * Called to indicate a fullscreen mode change.
         */
        void onFullScreen(@NonNull View view, boolean fullScreen);
    }

    @Override
    public CharSequence getAccessibilityClassName() {
        return MediaControlView.class.getName();
    }

    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        if (ev.getAction() == MotionEvent.ACTION_UP) {
            if (mMediaType != MEDIA_TYPE_MUSIC || mSizeType != SIZE_TYPE_FULL) {
                toggleMediaControlViewVisibility();
            }
        }
        return true;
    }

    @Override
    public boolean onTrackballEvent(MotionEvent ev) {
        if (ev.getAction() == MotionEvent.ACTION_UP) {
            if (mMediaType != MEDIA_TYPE_MUSIC || mSizeType != SIZE_TYPE_FULL) {
                toggleMediaControlViewVisibility();
            }
        }
        return true;
    }

    @Override
    public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        // Update layout when this view's width changes in order to avoid any UI overlap between
        // transport controls.
        if (mPrevWidth != getMeasuredWidth()) {
            // The following view may not have been initialized yet.
            if (mTimeView.getWidth() == 0) {
                return;
            }

            // Update layout if necessary
            int currWidth = getMeasuredWidth();
            int currHeight = getMeasuredHeight();
            Point screenSize = new Point();
            mWindowManager.getDefaultDisplay().getSize(screenSize);
            if (mMediaType == MEDIA_TYPE_DEFAULT) {
                updateLayout(BOTTOM_BAR_RIGHT_VIEW_MAX_ICON_NUM_DEFAULT, currWidth,
                        currHeight, screenSize.x, screenSize.y);
            } else if (mMediaType == MEDIA_TYPE_MUSIC) {
                updateLayout(BOTTOM_BAR_RIGHT_VIEW_MAX_ICON_NUM_MUSIC, currWidth,
                        currHeight, screenSize.x, screenSize.y);
            }
            mPrevWidth = currWidth;

            // By default, show all bars and hide settings window and overflow view when view size
            // is changed.
            showAllBars();
            hideSettingsAndOverflow();
        }

        // By default, show all bars and hide settings window and overflow view when view
        // orientation is changed.
        int currOrientation = retrieveOrientation();
        if (currOrientation != mPrevOrientation) {
            showAllBars();
            hideSettingsAndOverflow();
            mPrevOrientation = currOrientation;
        }
    }

    @Override
    public void setEnabled(boolean enabled) {
        super.setEnabled(enabled);

        if (mPlayPauseButton != null) {
            mPlayPauseButton.setEnabled(enabled);
        }
        if (mFfwdButton != null) {
            mFfwdButton.setEnabled(enabled);
        }
        if (mRewButton != null) {
            mRewButton.setEnabled(enabled);
        }
        if (mNextButton != null) {
            mNextButton.setEnabled(enabled);
        }
        if (mPrevButton != null) {
            mPrevButton.setEnabled(enabled);
        }
        if (mProgress != null) {
            mProgress.setEnabled(enabled);
        }
        if (mSubtitleButton != null) {
            mSubtitleButton.setEnabled(enabled);
        }
        if (mFullScreenButton != null) {
            mFullScreenButton.setEnabled(enabled);
        }
        if (mOverflowShowButton != null) {
            mOverflowShowButton.setEnabled(enabled);
        }
        if (mOverflowHideButton != null) {
            mOverflowHideButton.setEnabled(enabled);
        }
        if (mVideoQualityButton != null) {
            mVideoQualityButton.setEnabled(enabled);
        }
        if (mSettingsButton != null) {
            mSettingsButton.setEnabled(enabled);
        }
        if (mBackButton != null) {
            mBackButton.setEnabled(enabled);
        }
        if (mRouteButton != null) {
            mRouteButton.setEnabled(enabled);
        }
        disableUnsupportedButtons();
    }

    @Override
    public void onVisibilityAggregated(boolean isVisible) {
        super.onVisibilityAggregated(isVisible);

        if (isVisible) {
            disableUnsupportedButtons();
            removeCallbacks(mUpdateProgress);
            post(mUpdateProgress);
        } else {
            removeCallbacks(mUpdateProgress);
        }
    }

    void setRouteSelector(MediaRouteSelector selector) {
        mRouteSelector = selector;
        if (mRouteSelector != null && !mRouteSelector.isEmpty()) {
            mRouteButton.setRouteSelector(selector);
            mRouteButton.setVisibility(View.VISIBLE);
        } else {
            mRouteButton.setRouteSelector(MediaRouteSelector.EMPTY);
            mRouteButton.setVisibility(View.GONE);
        }
    }

    void setShowControllerInterval(long interval) {
        mShowControllerIntervalMs = interval;
    }

    ///////////////////////////////////////////////////
    // Protected or private methods
    ///////////////////////////////////////////////////

    /**
     * Create the view that holds the widgets that control playback.
     * Derived classes can override this to create their own.
     *
     * @return The controller view.
     */
    private ViewGroup makeControllerView() {
        ViewGroup root = (ViewGroup) inflateLayout(getContext(), R.layout.media_controller);
        initControllerView(root);
        return root;
    }

    static View inflateLayout(Context context, int resId) {
        LayoutInflater inflater = (LayoutInflater) context
                .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
        return inflater.inflate(resId, null);
    }

    @SuppressWarnings("deprecation")
    private void initControllerView(ViewGroup v) {
        mWindowManager = (WindowManager) getContext().getApplicationContext()
                .getSystemService(Context.WINDOW_SERVICE);
        mIconSize = mResources.getDimensionPixelSize(R.dimen.mcv2_icon_size);

        // Relating to Title Bar View
        mTitleBar = v.findViewById(R.id.title_bar);
        mTitleView = v.findViewById(R.id.title_text);
        mAdExternalLink = v.findViewById(R.id.ad_external_link);
        mBackButton = v.findViewById(R.id.back);
        if (mBackButton != null) {
            mBackButton.setOnClickListener(mBackListener);
            mBackButton.setVisibility(View.GONE);
        }
        mRouteButton = v.findViewById(R.id.cast);

        // Relating to Center View
        mCenterView = v.findViewById(R.id.center_view);
        mTransportControls = inflateTransportControls(R.layout.embedded_transport_controls);
        mCenterView.addView(mTransportControls);

        // Relating to Minimal Size FullScreen View. This view is visible only when the current
        // size type is Minimal and it is a view that stretches from left to right end
        // and helps locate the fullscreen button at the right end of the screen.
        mMinimalSizeFullScreenView = (LinearLayout) v.findViewById(R.id.minimal_fullscreen_view);
        LinearLayout.LayoutParams params =
                (LinearLayout.LayoutParams) mMinimalSizeFullScreenView.getLayoutParams();
        int iconSize = mResources.getDimensionPixelSize(R.dimen.mcv2_icon_size);
        params.setMargins(0, iconSize * (-1), 0, 0);
        mMinimalSizeFullScreenView.setLayoutParams(params);
        mMinimalSizeFullScreenView.setVisibility(View.GONE);

        // Relating to Progress Bar View
        mProgressBar = v.findViewById(R.id.progress_bar);
        mProgress = v.findViewById(R.id.progress);
        if (mProgress != null) {
            if (mProgress instanceof SeekBar) {
                SeekBar seeker = (SeekBar) mProgress;
                seeker.setOnSeekBarChangeListener(mSeekListener);
                seeker.setProgressDrawable(mResources.getDrawable(R.drawable.custom_progress));
                seeker.setThumb(mResources.getDrawable(R.drawable.custom_progress_thumb));
                seeker.setThumbOffset(0);
            }
            mProgress.setMax(MAX_PROGRESS);
        }
        mProgressBuffer = v.findViewById(R.id.progress_buffer);
        mCurrentSeekPosition = SEEK_POSITION_NOT_SET;
        mNextSeekPosition = SEEK_POSITION_NOT_SET;

        // Relating to Bottom Bar View
        mBottomBar = v.findViewById(R.id.bottom_bar);

        // Relating to Bottom Bar Left View
        mBottomBarLeftView = v.findViewById(R.id.bottom_bar_left);
        mTimeView = v.findViewById(R.id.time);
        mEndTime = v.findViewById(R.id.time_end);
        mCurrentTime = v.findViewById(R.id.time_current);
        mAdSkipView = v.findViewById(R.id.ad_skip_time);
        mFormatBuilder = new StringBuilder();
        mFormatter = new Formatter(mFormatBuilder, Locale.getDefault());

        // Relating to Bottom Bar Right View
        mBasicControls = v.findViewById(R.id.basic_controls);
        mExtraControls = v.findViewById(R.id.extra_controls);
        mCustomButtons = v.findViewById(R.id.custom_buttons);
        mSubtitleButton = v.findViewById(R.id.subtitle);
        if (mSubtitleButton != null) {
            mSubtitleButton.setOnClickListener(mSubtitleListener);
        }
        mFullScreenButton = v.findViewById(R.id.fullscreen);
        if (mFullScreenButton != null) {
            mFullScreenButton.setOnClickListener(mFullScreenListener);
        }
        mOverflowShowButton = v.findViewById(R.id.overflow_show);
        if (mOverflowShowButton != null) {
            mOverflowShowButton.setOnClickListener(mOverflowShowListener);
        }
        mOverflowHideButton = v.findViewById(R.id.overflow_hide);
        if (mOverflowHideButton != null) {
            mOverflowHideButton.setOnClickListener(mOverflowHideListener);
        }
        mSettingsButton = v.findViewById(R.id.settings);
        if (mSettingsButton != null) {
            mSettingsButton.setOnClickListener(mSettingsButtonListener);
        }
        mVideoQualityButton = v.findViewById(R.id.video_quality);
        if (mVideoQualityButton != null) {
            mVideoQualityButton.setOnClickListener(mVideoQualityListener);
        }
        mAdRemainingView = v.findViewById(R.id.ad_remaining);

        // Relating to Settings List View
        initializeSettingsLists();
        mSettingsListView = (ListView) inflateLayout(getContext(), R.layout.settings_list);
        mSettingsAdapter = new SettingsAdapter(mSettingsMainTextsList, mSettingsSubTextsList,
                mSettingsIconIdsList);
        mSubSettingsAdapter = new SubSettingsAdapter(null, 0);
        mSettingsListView.setAdapter(mSettingsAdapter);
        mSettingsListView.setChoiceMode(ListView.CHOICE_MODE_SINGLE);
        mSettingsListView.setOnItemClickListener(mSettingsItemClickListener);

        mEmbeddedSettingsItemWidth = mResources.getDimensionPixelSize(
                R.dimen.mcv2_embedded_settings_width);
        mFullSettingsItemWidth = mResources.getDimensionPixelSize(R.dimen.mcv2_full_settings_width);
        mSettingsItemHeight = mResources.getDimensionPixelSize(
                R.dimen.mcv2_settings_height);
        mSettingsWindowMargin = (-1) * mResources.getDimensionPixelSize(
                R.dimen.mcv2_settings_offset);
        mSettingsWindow = new PopupWindow(mSettingsListView, mEmbeddedSettingsItemWidth,
                LayoutParams.WRAP_CONTENT, true);
        mSettingsWindow.setOnDismissListener(mSettingsDismissListener);

        int titleBarTranslateY =
                (-1) * mResources.getDimensionPixelSize(R.dimen.mcv2_title_bar_height);
        int bottomBarHeight = mResources.getDimensionPixelSize(R.dimen.mcv2_bottom_bar_height);
        int progressThumbHeight = mResources.getDimensionPixelSize(
                R.dimen.mcv2_custom_progress_thumb_size);
        int progressBarHeight = mResources.getDimensionPixelSize(
                R.dimen.mcv2_custom_progress_max_size);
        int bottomBarTranslateY = bottomBarHeight + progressThumbHeight / 2 - progressBarHeight / 2;

        ValueAnimator fadeOutAnimator = ValueAnimator.ofFloat(1.0f, 0.0f);
        fadeOutAnimator.setInterpolator(new LinearInterpolator());
        fadeOutAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                float alpha = (float) animation.getAnimatedValue();
                SeekBar seekBar = (SeekBar) mProgress;
                if (mSizeType != SIZE_TYPE_MINIMAL) {
                    ScaleDrawable thumb = (ScaleDrawable) seekBar.getThumb();
                    if (thumb != null) {
                        thumb.setLevel((int) (MAX_SCALE_LEVEL * alpha));
                    }
                }

                mTransportControls.setAlpha(alpha);
                if (alpha == 0.0f) {
                    mTransportControls.setVisibility(View.GONE);
                } else if (alpha == 1.0f) {
                    setEnabled(false);
                }
                if (mSizeType == SIZE_TYPE_MINIMAL) {
                    mFullScreenButton.setAlpha(alpha);
                    mProgressBar.setAlpha(alpha);
                    if (alpha == 0.0f) {
                        if (mOnFullScreenListener != null) {
                            mFullScreenButton.setVisibility(View.GONE);
                        }
                        mProgressBar.setVisibility(View.GONE);
                    }
                }
            }
        });

        ValueAnimator fadeInAnimator = ValueAnimator.ofFloat(0.0f, 1.0f);
        fadeInAnimator.setInterpolator(new LinearInterpolator());
        fadeInAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                float alpha = (float) animation.getAnimatedValue();
                SeekBar seekBar = (SeekBar) mProgress;
                if (mSizeType != SIZE_TYPE_MINIMAL) {
                    ScaleDrawable thumb = (ScaleDrawable) seekBar.getThumb();
                    if (thumb != null) {
                        thumb.setLevel((int) (MAX_SCALE_LEVEL * alpha));
                    }
                }

                mTransportControls.setAlpha(alpha);
                if (alpha == 0.0f) {
                    mTransportControls.setVisibility(View.VISIBLE);
                } else if (alpha == 1.0f) {
                    setEnabled(true);
                }
                if (mSizeType == SIZE_TYPE_MINIMAL) {
                    mFullScreenButton.setAlpha(alpha);
                    mProgressBar.setAlpha(alpha);
                    if (alpha == 0.0f) {
                        if (mOnFullScreenListener != null) {
                            mFullScreenButton.setVisibility(View.VISIBLE);
                        }
                        mProgressBar.setVisibility(View.VISIBLE);
                    }
                }
            }
        });

        mHideMainBarsAnimator = new AnimatorSet();
        mHideMainBarsAnimator
                .play(ObjectAnimator.ofFloat(mTitleBar, "translationY",
                        0, titleBarTranslateY))
                .with(ObjectAnimator.ofFloat(mBottomBar, "translationY",
                        0, bottomBarTranslateY))
                .with(ObjectAnimator.ofFloat(mProgressBar, "translationY",
                        0, bottomBarTranslateY))
                .with(fadeOutAnimator);
        mHideMainBarsAnimator.setDuration(HIDE_TIME_MS);
        mHideMainBarsAnimator.getChildAnimations().get(0).addListener(
                new AnimatorListenerAdapter() {
                    @Override
                    public void onAnimationStart(Animator animation) {
                        setEnabled(false);
                        mUxState = UX_STATE_ANIMATING;
                    }

                    @Override
                    public void onAnimationEnd(Animator animation) {
                        setEnabled(true);
                        mUxState = UX_STATE_ONLY_PROGRESS_VISIBLE;
                    }
                });

        mHideProgressBarAnimator = new AnimatorSet();
        mHideProgressBarAnimator
                .play(ObjectAnimator.ofFloat(mBottomBar, "translationY",
                        bottomBarTranslateY, bottomBarTranslateY + progressBarHeight))
                .with(ObjectAnimator.ofFloat(mProgressBar, "translationY",
                        bottomBarTranslateY, bottomBarTranslateY + progressBarHeight));
        mHideProgressBarAnimator.setDuration(HIDE_TIME_MS);
        mHideProgressBarAnimator.getChildAnimations().get(0).addListener(
                new AnimatorListenerAdapter() {
                    @Override
                    public void onAnimationStart(Animator animation) {
                        setEnabled(false);
                        mUxState = UX_STATE_ANIMATING;
                    }

                    @Override
                    public void onAnimationEnd(Animator animation) {
                        setEnabled(true);
                        mUxState = UX_STATE_NONE_VISIBLE;
                    }
                });

        mHideAllBarsAnimator = new AnimatorSet();
        mHideAllBarsAnimator
                .play(ObjectAnimator.ofFloat(mTitleBar, "translationY",
                        0, titleBarTranslateY))
                .with(ObjectAnimator.ofFloat(mBottomBar, "translationY",
                        0, bottomBarTranslateY + progressBarHeight))
                .with(ObjectAnimator.ofFloat(mProgressBar, "translationY",
                        0, bottomBarTranslateY + progressBarHeight))
                .with(fadeOutAnimator);
        mHideAllBarsAnimator.setDuration(HIDE_TIME_MS);
        mHideAllBarsAnimator.getChildAnimations().get(0).addListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationStart(Animator animation) {
                setEnabled(false);
                mUxState = UX_STATE_ANIMATING;
            }

            @Override
            public void onAnimationEnd(Animator animation) {
                setEnabled(true);
                mUxState = UX_STATE_NONE_VISIBLE;
            }
        });

        mShowMainBarsAnimator = new AnimatorSet();
        mShowMainBarsAnimator
                .play(ObjectAnimator.ofFloat(mTitleBar, "translationY",
                        titleBarTranslateY, 0))
                .with(ObjectAnimator.ofFloat(mBottomBar, "translationY",
                        bottomBarTranslateY, 0))
                .with(ObjectAnimator.ofFloat(mProgressBar, "translationY",
                        bottomBarTranslateY, 0))
                .with(fadeInAnimator);
        mShowMainBarsAnimator.setDuration(SHOW_TIME_MS);
        mShowMainBarsAnimator.getChildAnimations().get(0).addListener(
                new AnimatorListenerAdapter() {
                    @Override
                    public void onAnimationStart(Animator animation) {
                        setEnabled(false);
                        mUxState = UX_STATE_ANIMATING;
                    }

                    @Override
                    public void onAnimationEnd(Animator animation) {
                        setEnabled(true);
                        mUxState = UX_STATE_ALL_VISIBLE;
                    }
                });

        mShowAllBarsAnimator = new AnimatorSet();
        mShowAllBarsAnimator
                .play(ObjectAnimator.ofFloat(mTitleBar, "translationY",
                        titleBarTranslateY, 0))
                .with(ObjectAnimator.ofFloat(mBottomBar, "translationY",
                        bottomBarTranslateY + progressBarHeight, 0))
                .with(ObjectAnimator.ofFloat(mProgressBar, "translationY",
                        bottomBarTranslateY + progressBarHeight, 0))
                .with(fadeInAnimator);
        mShowAllBarsAnimator.setDuration(SHOW_TIME_MS);
        mShowAllBarsAnimator.getChildAnimations().get(0).addListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationStart(Animator animation) {
                setEnabled(false);
                mUxState = UX_STATE_ANIMATING;
            }

            @Override
            public void onAnimationEnd(Animator animation) {
                setEnabled(true);
                mUxState = UX_STATE_ALL_VISIBLE;
            }
        });

        mOverflowShowAnimator = ValueAnimator.ofFloat(0.0f, 1.0f);
        mOverflowShowAnimator.setDuration(SHOW_TIME_MS);
        mOverflowShowAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                animateOverflow(animation);
            }
        });
        mOverflowShowAnimator.addListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationStart(Animator animation) {
                mExtraControls.setVisibility(View.VISIBLE);
                mOverflowShowButton.setVisibility(View.GONE);
                mOverflowHideButton.setVisibility(View.VISIBLE);
            }

            @Override
            public void onAnimationEnd(Animator animation) {
                mBasicControls.setVisibility(View.GONE);

                if (mSizeType == SIZE_TYPE_FULL && mMediaType == MEDIA_TYPE_DEFAULT) {
                    mFfwdButton.setVisibility(View.GONE);
                }
            }
        });

        mOverflowHideAnimator = ValueAnimator.ofFloat(1.0f, 0.0f);
        mOverflowHideAnimator.setDuration(SHOW_TIME_MS);
        mOverflowHideAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                animateOverflow(animation);
            }
        });
        mOverflowHideAnimator.addListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationStart(Animator animation) {
                mBasicControls.setVisibility(View.VISIBLE);
                mOverflowShowButton.setVisibility(View.VISIBLE);
                mOverflowHideButton.setVisibility(View.GONE);

                if (mSizeType == SIZE_TYPE_FULL && mMediaType == MEDIA_TYPE_DEFAULT) {
                    mFfwdButton.setVisibility(View.VISIBLE);
                }
            }

            @Override
            public void onAnimationEnd(Animator animation) {
                mExtraControls.setVisibility(View.GONE);
            }
        });
    }

    /**
     * Disable pause or seek buttons if the stream cannot be paused or seeked.
     * This requires the control interface to be a MediaPlayerControlExt
     * TODO: b/110905302
     */
    private void disableUnsupportedButtons() {
        try {
            if (mPlayPauseButton != null && !mController.canPause()) {
                mPlayPauseButton.setEnabled(false);
            }
            if (mRewButton != null && !mController.canSeekBackward()) {
                mRewButton.setEnabled(false);
            }
            if (mFfwdButton != null && !mController.canSeekForward()) {
                mFfwdButton.setEnabled(false);
            }
            if (mProgress != null && !mController.canSeekBackward()
                    && !mController.canSeekForward()) {
                mProgress.setEnabled(false);
            }
        } catch (IncompatibleClassChangeError ex) {
            // We were given an old version of the interface, that doesn't have
            // the canPause/canSeekXYZ methods. This is OK, it just means we
            // assume the media can be paused and seeked, and so we don't disable
            // the buttons.
        }
    }

    final Runnable mUpdateProgress = new Runnable() {
        @Override
        public void run() {
            boolean isShowing = getVisibility() == View.VISIBLE;
            if (!mDragging && isShowing && mController.isPlaying()) {
                long pos = setProgress();
                postDelayed(mUpdateProgress,
                        DEFAULT_PROGRESS_UPDATE_TIME_MS - (pos % DEFAULT_PROGRESS_UPDATE_TIME_MS));
            }
        }
    };

    String stringForTime(long timeMs) {
        long totalSeconds = timeMs / 1000;

        long seconds = totalSeconds % 60;
        long minutes = (totalSeconds / 60) % 60;
        long hours = totalSeconds / 3600;

        mFormatBuilder.setLength(0);
        if (hours > 0) {
            return mFormatter.format("%d:%02d:%02d", hours, minutes, seconds).toString();
        } else {
            return mFormatter.format("%02d:%02d", minutes, seconds).toString();
        }
    }

    long setProgress() {
        int positionOnProgressBar = 0;
        long currentPosition = mController.getCurrentPosition();
        if (currentPosition > mDuration) {
            currentPosition = mDuration;
        }
        if (mDuration > 0) {
            positionOnProgressBar = (int) (MAX_PROGRESS * currentPosition / mDuration);
        }
        if (mProgress != null && currentPosition != mDuration) {
            mProgress.setProgress(positionOnProgressBar);
            // If the media is a local file, there is no need to set a buffer, so set secondary
            // progress to maximum.
            if (mController.getBufferPercentage() < 0) {
                mProgress.setSecondaryProgress(MAX_PROGRESS);
            } else {
                mProgress.setSecondaryProgress((int) mController.getBufferPercentage() * 10);
            }
        }

        if (mEndTime != null) {
            mEndTime.setText(stringForTime(mDuration));
        }
        if (mCurrentTime != null) {
            mCurrentTime.setText(stringForTime(currentPosition));
        }

        if (mIsAdvertisement) {
            // Update the remaining number of seconds until the first 5 seconds of the
            // advertisement.
            if (mAdSkipView != null) {
                if (currentPosition <= AD_SKIP_WAIT_TIME_MS) {
                    if (mAdSkipView.getVisibility() == View.GONE) {
                        mAdSkipView.setVisibility(View.VISIBLE);
                    }
                    String skipTimeText = mResources.getString(
                            R.string.MediaControlView_ad_skip_wait_time,
                            ((AD_SKIP_WAIT_TIME_MS - currentPosition) / 1000 + 1));
                    mAdSkipView.setText(skipTimeText);
                } else {
                    if (mAdSkipView.getVisibility() == View.VISIBLE) {
                        mAdSkipView.setVisibility(View.GONE);
                        mNextButton.setEnabled(true);
                        mNextButton.clearColorFilter();
                    }
                }
            }
            // Update the remaining number of seconds of the advertisement.
            if (mAdRemainingView != null) {
                long remainingTime =
                        (mDuration - currentPosition < 0) ? 0 : (mDuration - currentPosition);
                String remainingTimeText = mResources.getString(
                        R.string.MediaControlView_ad_remaining_time,
                        stringForTime(remainingTime));
                mAdRemainingView.setText(remainingTimeText);
            }
        }
        return currentPosition;
    }

    void togglePausePlayState() {
        if (mController.isPlaying()) {
            mController.pause();
            mPlayPauseButton.setImageDrawable(
                    mResources.getDrawable(R.drawable.ic_play_circle_filled));
            mPlayPauseButton.setContentDescription(
                    mResources.getString(R.string.mcv2_play_button_desc));
        } else {
            if (mIsStopped) {
                mController.seekTo(0);
            }
            mController.play();
            mPlayPauseButton.setImageDrawable(
                    mResources.getDrawable(R.drawable.ic_pause_circle_filled));
            mPlayPauseButton.setContentDescription(
                    mResources.getString(R.string.mcv2_pause_button_desc));
        }
    }

    private void toggleMediaControlViewVisibility() {
        if (shouldNotHideBars() || mShowControllerIntervalMs == 0
                || mUxState == UX_STATE_ANIMATING) {
            return;
        }
        removeCallbacks(mHideMainBars);
        removeCallbacks(mHideProgressBar);

        switch (mUxState) {
            case UX_STATE_NONE_VISIBLE:
                post(mShowAllBars);
                break;
            case UX_STATE_ONLY_PROGRESS_VISIBLE:
                post(mShowMainBars);
                break;
            case UX_STATE_ALL_VISIBLE:
                post(mHideAllBars);
                break;
        }
    }

    private final Runnable mShowAllBars = new Runnable() {
        @Override
        public void run() {
            mShowAllBarsAnimator.start();
            if (mController.isPlaying()) {
                postDelayed(mHideMainBars, mShowControllerIntervalMs);
            }
        }
    };

    private final Runnable mShowMainBars = new Runnable() {
        @Override
        public void run() {
            mShowMainBarsAnimator.start();
            postDelayed(mHideMainBars, mShowControllerIntervalMs);
        }
    };

    private final Runnable mHideAllBars = new Runnable() {
        @Override
        public void run() {
            if (shouldNotHideBars()) {
                return;
            }
            mHideAllBarsAnimator.start();
        }
    };

    Runnable mHideMainBars = new Runnable() {
        @Override
        public void run() {
            if (!mController.isPlaying() || shouldNotHideBars()) {
                return;
            }
            mHideMainBarsAnimator.start();
            postDelayed(mHideProgressBar, mShowControllerIntervalMs);
        }
    };

    final Runnable mHideProgressBar = new Runnable() {
        @Override
        public void run() {
            if (!mController.isPlaying() || shouldNotHideBars()) {
                return;
            }
            mHideProgressBarAnimator.start();
        }
    };

    // There are two scenarios that can trigger the seekbar listener to trigger:
    //
    // The first is the user using the touchpad to adjust the position of the
    // seekbar's thumb. In this case onStartTrackingTouch is called followed by
    // a number of onProgressChanged notifications, concluded by onStopTrackingTouch.
    // We're setting the field "mDragging" to true for the duration of the dragging
    // session to avoid jumps in the position in case of ongoing playback.
    //
    // The second scenario involves the user operating the scroll ball, in this
    // case there WON'T BE onStartTrackingTouch/onStopTrackingTouch notifications,
    // we will simply apply the updated position without suspending regular updates.
    private final OnSeekBarChangeListener mSeekListener = new OnSeekBarChangeListener() {
        @Override
        public void onStartTrackingTouch(SeekBar bar) {
            if (!mSeekAvailable) {
                return;
            }

            mDragging = true;

            // By removing these pending progress messages we make sure
            // that a) we won't update the progress while the user adjusts
            // the seekbar and b) once the user is done dragging the thumb
            // we will post one of these messages to the queue again and
            // this ensures that there will be exactly one message queued up.
            removeCallbacks(mUpdateProgress);
            removeCallbacks(mHideMainBars);
            removeCallbacks(mHideProgressBar);

            // Check if playback is currently stopped. In this case, update the pause button to
            // show the play image instead of the replay image.
            if (mIsStopped) {
                updateForStoppedState(false);
            }

            if (isCurrentMediaItemFromNetwork() && mController.isPlaying()) {
                mWasPlaying = true;
                mController.pause();
            }
        }

        @Override
        public void onProgressChanged(SeekBar bar, int progress, boolean fromUser) {
            if (!mSeekAvailable) {
                return;
            }
            if (!fromUser) {
                // We're not interested in programmatically generated changes to
                // the progress bar's position.
                return;
            }
            // Check if progress bar is being dragged since this method may be called after
            // onStopTrackingTouch() is called.
            if (mDragging && mDuration > 0) {
                long newPosition = ((mDuration * progress) / MAX_PROGRESS);
                // Do not seek if the current media item has a http scheme URL to improve seek
                // performance.
                boolean shouldSeekNow = !isCurrentMediaItemFromNetwork();
                seekTo(newPosition, shouldSeekNow);
            }
        }

        @Override
        public void onStopTrackingTouch(SeekBar bar) {
            if (!mSeekAvailable) {
                return;
            }
            mDragging = false;

            long latestSeekPosition = getLatestSeekPosition();
            // Reset existing seek positions since we only need to seek to the latest position.
            if (isCurrentMediaItemFromNetwork()) {
                mCurrentSeekPosition = SEEK_POSITION_NOT_SET;
                mNextSeekPosition = SEEK_POSITION_NOT_SET;
            }
            seekTo(latestSeekPosition, true);

            if (mWasPlaying) {
                mWasPlaying = false;
                mController.play();
            }
        }
    };

    private final OnClickListener mPlayPauseListener = new OnClickListener() {
        @Override
        public void onClick(View v) {
            resetHideCallbacks();
            togglePausePlayState();
        }
    };

    private final OnClickListener mRewListener = new OnClickListener() {
        @Override
        public void onClick(View v) {
            resetHideCallbacks();
            removeCallbacks(mUpdateProgress);

            long latestSeekPosition = getLatestSeekPosition();
            if (mIsStopped && mDuration != 0) {
                // If the media is currently stopped, rewinding will start the media from the
                // beginning. Instead, seek to 10 seconds before the end of the media.
                seekTo(mDuration - REWIND_TIME_MS, true);
                updateForStoppedState(false);
            } else {
                seekTo(Math.max(latestSeekPosition - REWIND_TIME_MS, 0), true);
            }
        }
    };

    private final OnClickListener mFfwdListener = new OnClickListener() {
        @Override
        public void onClick(View v) {
            resetHideCallbacks();
            removeCallbacks(mUpdateProgress);

            long latestSeekPosition = getLatestSeekPosition();
            seekTo(Math.min(latestSeekPosition + FORWARD_TIME_MS, mDuration), true);
            if (latestSeekPosition + FORWARD_TIME_MS >= mDuration) {
                // If the media is currently paused, fast-forwarding beyond the duration value will
                // not return a callback that updates the play/pause and ffwd buttons. Thus,
                // update the buttons manually here.
                updateForStoppedState(true);
            }
        }
    };

    private final OnClickListener mNextListener = new OnClickListener() {
        @Override
        public void onClick(View v) {
            resetHideCallbacks();
            mController.skipToNextItem();
        }
    };

    private final OnClickListener mPrevListener = new OnClickListener() {
        @Override
        public void onClick(View v) {
            resetHideCallbacks();
            mController.skipToPreviousItem();
        }
    };

    private final OnClickListener mBackListener = new OnClickListener() {
        @Override
        public void onClick(View v) {
            View parent = (View) getParent();
            if (parent != null) {
                parent.onKeyDown(KeyEvent.KEYCODE_BACK,
                        new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_BACK));
            }
        }
    };

    private final OnClickListener mSubtitleListener = new OnClickListener() {
        @Override
        public void onClick(View v) {
            removeCallbacks(mHideMainBars);
            removeCallbacks(mHideProgressBar);

            mSettingsMode = SETTINGS_MODE_SUBTITLE_TRACK;
            mSubSettingsAdapter.setTexts(mSubtitleDescriptionsList);
            mSubSettingsAdapter.setCheckPosition(mSelectedSubtitleTrackIndex);
            displaySettingsWindow(mSubSettingsAdapter);
        }
    };

    private final OnClickListener mVideoQualityListener = new OnClickListener() {
        @Override
        public void onClick(View v) {
            removeCallbacks(mHideMainBars);
            removeCallbacks(mHideProgressBar);

            mSettingsMode = SETTINGS_MODE_VIDEO_QUALITY;
            mSubSettingsAdapter.setTexts(mVideoQualityList);
            mSubSettingsAdapter.setCheckPosition(mSelectedVideoQualityIndex);
            displaySettingsWindow(mSubSettingsAdapter);
        }
    };

    private final OnClickListener mFullScreenListener = new OnClickListener() {
        @Override
        public void onClick(View v) {
            resetHideCallbacks();

            if (mOnFullScreenListener == null) {
                return;
            }

            final boolean isEnteringFullScreen = !mIsFullScreen;
            if (isEnteringFullScreen) {
                mFullScreenButton.setImageDrawable(
                        mResources.getDrawable(R.drawable.ic_fullscreen_exit));
            } else {
                mFullScreenButton.setImageDrawable(
                        mResources.getDrawable(R.drawable.ic_fullscreen));
            }
            mIsFullScreen = isEnteringFullScreen;
            mOnFullScreenListener.onFullScreen(MediaControlView.this,
                    mIsFullScreen);
        }
    };

    private final OnClickListener mOverflowShowListener = new OnClickListener() {
        @Override
        public void onClick(View v) {
            resetHideCallbacks();

            mOverflowIsShowing = true;
            mOverflowShowAnimator.start();
        }
    };

    private final OnClickListener mOverflowHideListener = new OnClickListener() {
        @Override
        public void onClick(View v) {
            resetHideCallbacks();

            mOverflowIsShowing = false;
            mOverflowHideAnimator.start();
        }
    };

    private final OnClickListener mSettingsButtonListener = new OnClickListener() {
        @Override
        public void onClick(View v) {
            removeCallbacks(mHideMainBars);
            removeCallbacks(mHideProgressBar);

            mSettingsMode = SETTINGS_MODE_MAIN;
            mSettingsAdapter.setSubTexts(mSettingsSubTextsList);
            displaySettingsWindow(mSettingsAdapter);
        }
    };

    private final AdapterView.OnItemClickListener mSettingsItemClickListener =
            new AdapterView.OnItemClickListener() {
        @Override
        public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
            switch (mSettingsMode) {
                case SETTINGS_MODE_MAIN:
                    if (position == SETTINGS_MODE_AUDIO_TRACK) {
                        mSubSettingsAdapter.setTexts(mAudioTrackList);
                        mSubSettingsAdapter.setCheckPosition(mSelectedAudioTrackIndex);
                        mSettingsMode = SETTINGS_MODE_AUDIO_TRACK;
                    } else if (position == SETTINGS_MODE_PLAYBACK_SPEED) {
                        mSubSettingsAdapter.setTexts(mPlaybackSpeedTextList);
                        mSubSettingsAdapter.setCheckPosition(mSelectedSpeedIndex);
                        mSettingsMode = SETTINGS_MODE_PLAYBACK_SPEED;
                    }
                    displaySettingsWindow(mSubSettingsAdapter);
                    break;
                case SETTINGS_MODE_AUDIO_TRACK:
                    if (position != mSelectedAudioTrackIndex) {
                        mSelectedAudioTrackIndex = position;
                        if (mAudioTrackCount > 0) {
                            mController.selectAudioTrack(position);
                        }
                        mSettingsSubTextsList.set(SETTINGS_MODE_AUDIO_TRACK,
                                mSubSettingsAdapter.getMainText(position));
                    }
                    dismissSettingsWindow();
                    break;
                case SETTINGS_MODE_PLAYBACK_SPEED:
                    if (position != mSelectedSpeedIndex) {
                        float speed = mPlaybackSpeedMultBy100List.get(position) / 100.0f;
                        mController.setSpeed(speed);
                    }
                    dismissSettingsWindow();
                    break;
                case SETTINGS_MODE_SUBTITLE_TRACK:
                    if (position != mSelectedSubtitleTrackIndex) {
                        if (position > 0) {
                            mController.showSubtitle(position - 1);
                            mSubtitleButton.setImageDrawable(
                                    mResources.getDrawable(R.drawable.ic_subtitle_on));
                            mSubtitleButton.setContentDescription(
                                    mResources.getString(R.string.mcv2_cc_is_on));
                        } else {
                            mController.hideSubtitle();
                            mSubtitleButton.setImageDrawable(
                                    mResources.getDrawable(R.drawable.ic_subtitle_off));
                            mSubtitleButton.setContentDescription(
                                    mResources.getString(R.string.mcv2_cc_is_off));
                        }
                    }
                    dismissSettingsWindow();
                    break;
                case SETTINGS_MODE_VIDEO_QUALITY:
                    mSelectedVideoQualityIndex = position;
                    dismissSettingsWindow();
                    break;
            }
        }
    };

    private PopupWindow.OnDismissListener mSettingsDismissListener =
            new PopupWindow.OnDismissListener() {
                @Override
                public void onDismiss() {
                    if (mNeedToHideBars) {
                        postDelayed(mHideMainBars, mShowControllerIntervalMs);
                    }
                }
            };

    void updateMetadata() {
        if (!mController.hasMetadata()) {
            return;
        }

        long duration = mController.getDurationMs();
        if (duration != 0) {
            mDuration = duration;
            mTimeView.setVisibility(View.VISIBLE);
            setProgress();
        }

        if (mMediaType != MEDIA_TYPE_MUSIC) {
            CharSequence title = mController.getTitle();
            if (title != null) {
                mTitleView.setText(title.toString());
            }
        } else {
            CharSequence title = mController.getTitle();
            if (title == null) {
                title = mResources.getString(R.string.mcv2_music_title_unknown_text);
            }
            CharSequence artist = mController.getArtistText();
            if (artist == null) {
                artist = mResources.getString(R.string.mcv2_music_artist_unknown_text);
            }
            // Update title for Embedded size type
            mTitleView.setText(title.toString() + " - " + artist.toString());

            // Remove unnecessary buttons
            mVideoQualityButton.setVisibility(View.GONE);
            if (mFfwdButton != null) {
                mFfwdButton.setVisibility(View.GONE);
            }
            if (mRewButton != null) {
                mRewButton.setVisibility(View.GONE);
            }

            Point screenSize = new Point();
            mWindowManager.getDefaultDisplay().getSize(screenSize);
            updateLayout(BOTTOM_BAR_RIGHT_VIEW_MAX_ICON_NUM_MUSIC, getMeasuredWidth(),
                    getMeasuredHeight(), screenSize.x, screenSize.y);
        }
    }

    void updateLayoutForAd() {
        if (mIsAdvertisement) {
            mRewButton.setVisibility(View.GONE);
            mFfwdButton.setVisibility(View.GONE);
            mPrevButton.setVisibility(View.GONE);
            mTimeView.setVisibility(View.GONE);

            mAdSkipView.setVisibility(View.VISIBLE);
            mAdRemainingView.setVisibility(View.VISIBLE);
            mAdExternalLink.setVisibility(View.VISIBLE);

            mProgress.setEnabled(false);
            mNextButton.setEnabled(false);
            mNextButton.setColorFilter(R.color.gray);
        } else {
            mRewButton.setVisibility(View.VISIBLE);
            mFfwdButton.setVisibility(View.VISIBLE);
            mPrevButton.setVisibility(View.VISIBLE);
            mTimeView.setVisibility(View.VISIBLE);

            mAdSkipView.setVisibility(View.GONE);
            mAdRemainingView.setVisibility(View.GONE);
            mAdExternalLink.setVisibility(View.GONE);

            mProgress.setEnabled(true);
            mNextButton.setEnabled(true);
            mNextButton.clearColorFilter();
            disableUnsupportedButtons();
        }
    }

    private void updateLayout(int maxIconNum, int currWidth, int currHeight, int screenWidth,
            int screenHeight) {
        if (mMaxTimeViewWidth == 0) {
            // Save the width of the initial time view since it represents the maximum width that
            // this class supports (00:00:00 · 00:00:00).
            mMaxTimeViewWidth = mTimeView.getWidth();
        }
        int bottomBarRightWidthMax = mIconSize * maxIconNum;
        int fullWidth = mTransportControls.getWidth() + mMaxTimeViewWidth + bottomBarRightWidthMax;
        int screenMaxLength = Math.max(screenWidth, screenHeight);
        int embeddedWidth = mMaxTimeViewWidth + bottomBarRightWidthMax;

        // If Media type is default, the size of MCV2 is full only when the current width is equal
        // to the max length of the screen (only landscape mode). If Media type is music, however,
        // the size of MCV2 is full when the current width is equal to the current screen width
        // (both landscape and portrait modes).
        boolean isFullSize = (mMediaType == MEDIA_TYPE_DEFAULT) ? currWidth == screenMaxLength
                : currWidth == screenWidth;
        if (isFullSize) {
            if (mSizeType != SIZE_TYPE_FULL) {
                updateLayoutForSizeChange(SIZE_TYPE_FULL);
                if (mMediaType == MEDIA_TYPE_MUSIC) {
                    mTitleView.setVisibility(View.GONE);
                } else {
                    mUxState = UX_STATE_NONE_VISIBLE;
                    toggleMediaControlViewVisibility();
                }
            }
        } else if (embeddedWidth <= currWidth) {
            if (mSizeType != SIZE_TYPE_EMBEDDED) {
                updateLayoutForSizeChange(SIZE_TYPE_EMBEDDED);
                if (mMediaType == MEDIA_TYPE_MUSIC) {
                    mTitleView.setVisibility(View.VISIBLE);
                }
            }
        } else {
            if (mSizeType != SIZE_TYPE_MINIMAL) {
                updateLayoutForSizeChange(SIZE_TYPE_MINIMAL);
                if (mMediaType == MEDIA_TYPE_MUSIC) {
                    mTitleView.setVisibility(View.GONE);
                }
            }
        }
    }

    @SuppressWarnings("deprecation")
    private void updateLayoutForSizeChange(int sizeType) {
        mSizeType = sizeType;
        RelativeLayout.LayoutParams timeViewParams =
                (RelativeLayout.LayoutParams) mTimeView.getLayoutParams();
        SeekBar seeker = (SeekBar) mProgress;
        switch (mSizeType) {
            case SIZE_TYPE_EMBEDDED:
                // Relating to Title Bar
                mTitleBar.setVisibility(View.VISIBLE);
                mBackButton.setVisibility(View.GONE);
                mTitleView.setPadding(
                        mResources.getDimensionPixelSize(R.dimen.mcv2_embedded_icon_padding),
                        mTitleView.getPaddingTop(),
                        mTitleView.getPaddingRight(),
                        mTitleView.getPaddingBottom());

                // Relating to Full Screen Button
                if (mOnFullScreenListener != null) {
                    mMinimalSizeFullScreenView.setVisibility(View.GONE);
                    mFullScreenButton = mBasicControls.findViewById(R.id.fullscreen);
                    mFullScreenButton.setOnClickListener(mFullScreenListener);
                }

                // Relating to Center View
                mCenterView.removeAllViews();
                mBottomBarLeftView.removeView(mTransportControls);
                mBottomBarLeftView.setVisibility(View.GONE);
                mTransportControls = inflateTransportControls(R.layout.embedded_transport_controls);
                mCenterView.addView(mTransportControls);

                // Relating to Progress Bar
                seeker.setThumb(mResources.getDrawable(R.drawable.custom_progress_thumb));
                seeker.setThumbOffset(0);
                seeker.invalidate();
                mProgressBuffer.setVisibility(View.VISIBLE);

                // Relating to Bottom Bar
                mBottomBar.setVisibility(View.VISIBLE);
                if (timeViewParams.getRules()[RelativeLayout.LEFT_OF] != 0) {
                    timeViewParams.removeRule(RelativeLayout.LEFT_OF);
                    timeViewParams.addRule(RelativeLayout.RIGHT_OF, R.id.bottom_bar_left);
                }
                break;
            case SIZE_TYPE_FULL:
                // Relating to Title Bar
                mTitleBar.setVisibility(View.VISIBLE);
                mBackButton.setVisibility(View.VISIBLE);
                mTitleView.setPadding(
                        0,
                        mTitleView.getPaddingTop(),
                        mTitleView.getPaddingRight(),
                        mTitleView.getPaddingBottom());

                // Relating to Full Screen Button
                if (mOnFullScreenListener != null) {
                    mMinimalSizeFullScreenView.setVisibility(View.GONE);
                    mFullScreenButton = mBasicControls.findViewById(R.id.fullscreen);
                    mFullScreenButton.setOnClickListener(mFullScreenListener);
                }

                // Relating to Center View
                mCenterView.removeAllViews();
                mBottomBarLeftView.removeView(mTransportControls);
                mTransportControls = inflateTransportControls(R.layout.full_transport_controls);
                mBottomBarLeftView.addView(mTransportControls, 0);
                mBottomBarLeftView.setVisibility(View.VISIBLE);

                // Relating to Progress Bar
                seeker.setThumb(mResources.getDrawable(R.drawable.custom_progress_thumb));
                seeker.setThumbOffset(0);
                seeker.invalidate();
                mProgressBuffer.setVisibility(View.VISIBLE);

                // Relating to Bottom Bar
                mBottomBar.setVisibility(View.VISIBLE);
                if (timeViewParams.getRules()[RelativeLayout.RIGHT_OF] != 0) {
                    timeViewParams.removeRule(RelativeLayout.RIGHT_OF);
                    timeViewParams.addRule(RelativeLayout.LEFT_OF, R.id.basic_controls);
                }
                break;
            case SIZE_TYPE_MINIMAL:
                // Relating to Title Bar
                mTitleBar.setVisibility(View.GONE);
                mBackButton.setVisibility(View.GONE);

                // Relating to Full Screen Button
                if (mOnFullScreenListener != null) {
                    mMinimalSizeFullScreenView.setVisibility(View.VISIBLE);
                    mFullScreenButton = mMinimalSizeFullScreenView.findViewById(
                            R.id.minimal_fullscreen);
                    mFullScreenButton.setOnClickListener(mFullScreenListener);
                }

                // Relating to Center View
                mCenterView.removeAllViews();
                mBottomBarLeftView.removeView(mTransportControls);
                mTransportControls = inflateTransportControls(R.layout.minimal_transport_controls);
                mCenterView.addView(mTransportControls);

                // Relating to Progress Bar
                seeker.setThumb(null);
                mProgressBuffer.setVisibility(View.GONE);

                // Relating to Bottom Bar
                mBottomBar.setVisibility(View.GONE);
                break;
        }
        mTimeView.setLayoutParams(timeViewParams);

        // Update play/pause and ffwd buttons based on whether the media is currently stopped or
        // not.
        updateForStoppedState(mIsStopped);

        if (mIsFullScreen) {
            mFullScreenButton.setImageDrawable(
                    mResources.getDrawable(R.drawable.ic_fullscreen_exit));
        } else {
            mFullScreenButton.setImageDrawable(
                    mResources.getDrawable(R.drawable.ic_fullscreen));
        }
    }

    private View inflateTransportControls(int layoutId) {
        View v = inflateLayout(getContext(), layoutId);
        mPlayPauseButton = v.findViewById(R.id.pause);
        if (mPlayPauseButton != null) {
            mPlayPauseButton.requestFocus();
            mPlayPauseButton.setOnClickListener(mPlayPauseListener);
        }
        mFfwdButton = v.findViewById(R.id.ffwd);
        if (mFfwdButton != null) {
            if (mMediaType == MEDIA_TYPE_MUSIC) {
                mFfwdButton.setVisibility(View.GONE);
            } else {
                mFfwdButton.setOnClickListener(mFfwdListener);
            }
        }
        mRewButton = v.findViewById(R.id.rew);
        if (mRewButton != null) {
            if (mMediaType == MEDIA_TYPE_MUSIC) {
                mRewButton.setVisibility(View.GONE);
            } else {
                mRewButton.setOnClickListener(mRewListener);
            }
        }
        mNextButton = v.findViewById(R.id.next);
        if (mNextButton != null) {
            if (mController.canSkipToNext()) {
                mNextButton.setOnClickListener(mNextListener);
            } else {
                mNextButton.setVisibility(View.GONE);
            }
        }
        mPrevButton = v.findViewById(R.id.prev);
        if (mPrevButton != null) {
            if (mController.canSkipToPrevious()) {
                mPrevButton.setOnClickListener(mPrevListener);
            } else {
                mPrevButton.setVisibility(View.GONE);
            }
        }
        return v;
    }

    private void initializeSettingsLists() {
        mSettingsMainTextsList = new ArrayList<String>();
        mSettingsMainTextsList.add(
                mResources.getString(R.string.MediaControlView_audio_track_text));
        mSettingsMainTextsList.add(
                mResources.getString(R.string.MediaControlView_playback_speed_text));

        mSettingsSubTextsList = new ArrayList<String>();
        mSettingsSubTextsList.add(
                mResources.getString(R.string.MediaControlView_audio_track_none_text));
        String normalSpeed = mResources.getString(R.string.MediaControlView_playback_speed_normal);
        mSettingsSubTextsList.add(normalSpeed);
        mSettingsSubTextsList.add(RESOURCE_EMPTY);

        mSettingsIconIdsList = new ArrayList<Integer>();
        mSettingsIconIdsList.add(R.drawable.ic_audiotrack);
        mSettingsIconIdsList.add(R.drawable.ic_play_circle_filled);

        mAudioTrackList = new ArrayList<String>();
        mAudioTrackList.add(
                mResources.getString(R.string.MediaControlView_audio_track_none_text));

        mVideoQualityList = new ArrayList<String>();
        mVideoQualityList.add(
                mResources.getString(R.string.MediaControlView_video_quality_auto_text));

        mPlaybackSpeedTextList = new ArrayList<String>(Arrays.asList(
                mResources.getStringArray(R.array.MediaControlView_playback_speeds)));
        // Select the normal speed (1x) as the default value.
        mPlaybackSpeedTextList.add(PLAYBACK_SPEED_1x_INDEX, normalSpeed);
        mSelectedSpeedIndex = PLAYBACK_SPEED_1x_INDEX;

        mPlaybackSpeedMultBy100List = new ArrayList<Integer>();
        int[] speeds = mResources.getIntArray(R.array.speed_multiplied_by_100);
        for (int i = 0; i < speeds.length; i++) {
            mPlaybackSpeedMultBy100List.add(speeds[i]);
        }
        mCustomPlaybackSpeedIndex = -1;
    }

    /**
     * @return true iff the current media item is from network.
     */
    @VisibleForTesting
    boolean isCurrentMediaItemFromNetwork() {
        MediaItem currentMediaItem = mController.getCurrentMediaItem();

        if (!(currentMediaItem instanceof UriMediaItem)) {
            return false;
        }

        Uri uri = ((UriMediaItem) currentMediaItem).getUri();
        return UriUtil.isFromNetwork(uri);
    }

    void displaySettingsWindow(BaseAdapter adapter) {
        // Set Adapter
        mSettingsListView.setAdapter(adapter);

        // Set width of window
        int itemWidth = (mSizeType == SIZE_TYPE_EMBEDDED)
                ? mEmbeddedSettingsItemWidth : mFullSettingsItemWidth;
        mSettingsWindow.setWidth(itemWidth);

        // Calculate height of window
        int maxHeight = getMeasuredHeight() + mSettingsWindowMargin * 2;
        int totalHeight = adapter.getCount() * mSettingsItemHeight;
        int height = (totalHeight < maxHeight) ? totalHeight : maxHeight;
        mSettingsWindow.setHeight(height);

        // Show window
        mNeedToHideBars = false;
        mSettingsWindow.dismiss();
        mSettingsWindow.showAsDropDown(this, mSettingsWindowMargin,
                mSettingsWindowMargin - height, Gravity.BOTTOM | Gravity.RIGHT);
        mNeedToHideBars = true;
    }

    void dismissSettingsWindow() {
        mNeedToHideBars = true;
        mSettingsWindow.dismiss();
    }

    void animateOverflow(ValueAnimator animation) {
        RelativeLayout.LayoutParams extraControlsParams =
                (RelativeLayout.LayoutParams) mExtraControls.getLayoutParams();
        int iconWidth = mResources.getDimensionPixelSize(R.dimen.mcv2_icon_size);
        // Currently, mExtraControls view is set to the right end of the bottom bar
        // view. This animates the view by setting the initial margin value to the
        // negative value of its width ((-2) * iconWidth) and the final margin value
        // to the positive value of the overflow button width (iconWidth).
        int extraControlMargin = (-2 * iconWidth)
                + (int) (3 * iconWidth * (float) animation.getAnimatedValue());
        extraControlsParams.setMargins(0, 0, extraControlMargin, 0);
        mExtraControls.setLayoutParams(extraControlsParams);

        mTimeView.setAlpha(1 - (float) animation.getAnimatedValue());
        mBasicControls.setAlpha(1 - (float) animation.getAnimatedValue());

        if (mSizeType == SIZE_TYPE_FULL && mMediaType == MEDIA_TYPE_DEFAULT) {
            int transportControlMargin =
                    (-1) * (int) (iconWidth * (float) animation.getAnimatedValue());
            LinearLayout.LayoutParams transportControlsParams =
                    (LinearLayout.LayoutParams) mTransportControls.getLayoutParams();
            transportControlsParams.setMargins(transportControlMargin, 0, 0, 0);
            mTransportControls.setLayoutParams(transportControlsParams);

            mFfwdButton.setAlpha(1 - (float) animation.getAnimatedValue());
        }
    }

    void resetHideCallbacks() {
        removeCallbacks(mHideMainBars);
        removeCallbacks(mHideProgressBar);
        postDelayed(mHideMainBars, mShowControllerIntervalMs);
    }

    void updateAllowedCommands(SessionCommandGroup commands) {
        if (DEBUG) {
            Log.d(TAG, "updateAllowedCommands(): commands: " + commands);
        }

        if (mController.getAllowedCommands() == commands) {
            return;
        }
        mController.setAllowedCommands(commands);

        if (commands.hasCommand(SessionCommand.COMMAND_CODE_PLAYER_PAUSE)) {
            mPlayPauseButton.setVisibility(View.VISIBLE);
            mPlayPauseButton.setEnabled(true);
        } else {
            mPlayPauseButton.setVisibility(View.GONE);
        }
        if (commands.hasCommand(SessionCommand.COMMAND_CODE_SESSION_REWIND)
                && mMediaType != MEDIA_TYPE_MUSIC) {
            if (mRewButton != null) {
                mRewButton.setVisibility(View.VISIBLE);
                mRewButton.setEnabled(true);
            }
        } else {
            if (mRewButton != null) {
                mRewButton.setVisibility(View.GONE);
            }
        }
        if (commands.hasCommand(SessionCommand.COMMAND_CODE_SESSION_FAST_FORWARD)
                && mMediaType != MEDIA_TYPE_MUSIC) {
            if (mFfwdButton != null) {
                mFfwdButton.setVisibility(View.VISIBLE);
                mFfwdButton.setEnabled(true);
            }
        } else {
            if (mFfwdButton != null) {
                mFfwdButton.setVisibility(View.GONE);
            }
        }
        if (commands.hasCommand(
                SessionCommand.COMMAND_CODE_PLAYER_SKIP_TO_PREVIOUS_PLAYLIST_ITEM)) {
            if (mPrevButton != null) {
                mPrevButton.setVisibility(VISIBLE);
                mPrevButton.setEnabled(true);
            }
        } else {
            if (mPrevButton != null) {
                mPrevButton.setVisibility(View.GONE);
            }
        }
        if (commands.hasCommand(
                SessionCommand.COMMAND_CODE_PLAYER_SKIP_TO_NEXT_PLAYLIST_ITEM)) {
            if (mNextButton != null) {
                mNextButton.setVisibility(VISIBLE);
                mNextButton.setEnabled(true);
            }
        } else {
            if (mNextButton != null) {
                mNextButton.setVisibility(View.GONE);
            }
        }
        if (commands.hasCommand(SessionCommand.COMMAND_CODE_PLAYER_SEEK_TO)) {
            mSeekAvailable = true;
            mProgress.setEnabled(true);
        }
        if (commands.hasCommand(new SessionCommand(COMMAND_SHOW_SUBTITLE, null))
                && commands.hasCommand(new SessionCommand(COMMAND_HIDE_SUBTITLE, null))) {
            mSubtitleButton.setVisibility(View.VISIBLE);
        } else {
            mSubtitleButton.setVisibility(View.GONE);
        }
    }

    private int retrieveOrientation() {
        DisplayMetrics dm = Resources.getSystem().getDisplayMetrics();
        int width = dm.widthPixels;
        int height = dm.heightPixels;

        return (height > width)
                ? ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
                : ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE;
    }

    boolean shouldNotHideBars() {
        return (mMediaType == MEDIA_TYPE_MUSIC && mSizeType == SIZE_TYPE_FULL)
                || mAccessibilityManager.isTouchExplorationEnabled()
                || mController.getPlaybackState() == SessionPlayer.PLAYER_STATE_ERROR
                || mController.getPlaybackState() == SessionPlayer.PLAYER_STATE_IDLE;
    }

    void seekTo(long newPosition, boolean shouldSeekNow) {
        int positionOnProgressBar = (mDuration <= 0)
                ? 0 : (int) (MAX_PROGRESS * newPosition / mDuration);
        mProgress.setProgress(positionOnProgressBar);
        mCurrentTime.setText(stringForTime(newPosition));

        if (mCurrentSeekPosition == SEEK_POSITION_NOT_SET) {
            // If current seek position is not set, update its value and seek now if necessary.
            mCurrentSeekPosition = newPosition;

            if (shouldSeekNow) {
                mController.seekTo(mCurrentSeekPosition);
            }
        } else {
            // If current seek position is already set, update the next seek position.
            mNextSeekPosition = newPosition;
        }
    }

    private void showAllBars() {
        if (mUxState != UX_STATE_ALL_VISIBLE) {
            removeCallbacks(mHideMainBars);
            removeCallbacks(mHideProgressBar);
            // b/112570875
            post(mShowMainBars);
        } else {
            resetHideCallbacks();
        }
    }

    private void hideSettingsAndOverflow() {
        mSettingsWindow.dismiss();
        if (mOverflowIsShowing) {
            mOverflowHideAnimator.start();
        }
    }

    long getLatestSeekPosition() {
        if (mNextSeekPosition != SEEK_POSITION_NOT_SET) {
            return mNextSeekPosition;
        } else if (mCurrentSeekPosition != SEEK_POSITION_NOT_SET) {
            return mCurrentSeekPosition;
        }
        return mController.getCurrentPosition();
    }

    void removeCustomSpeedFromList() {
        mPlaybackSpeedMultBy100List.remove(mCustomPlaybackSpeedIndex);
        mPlaybackSpeedTextList.remove(mCustomPlaybackSpeedIndex);
        mCustomPlaybackSpeedIndex = -1;
    }

    void updateSelectedSpeed(int selectedSpeedIndex, String selectedSpeedText) {
        mSelectedSpeedIndex = selectedSpeedIndex;
        mSettingsSubTextsList.set(SETTINGS_MODE_PLAYBACK_SPEED, selectedSpeedText);
        mSubSettingsAdapter.setTexts(mPlaybackSpeedTextList);
        mSubSettingsAdapter.setCheckPosition(mSelectedSpeedIndex);
    }

    void updateForStoppedState(boolean isStopped) {
        if (isStopped) {
            mIsStopped = true;
            if (mPlayPauseButton != null) {
                mPlayPauseButton.setImageDrawable(
                        mResources.getDrawable(R.drawable.ic_replay_circle_filled));
                mPlayPauseButton.setContentDescription(
                        mResources.getString(R.string.mcv2_replay_button_desc));
            }
            if (mFfwdButton != null) {
                mFfwdButton.setAlpha(0.5f);
                mFfwdButton.setEnabled(false);
            }
        } else {
            mIsStopped = false;
            if (mPlayPauseButton != null) {
                if (mController.isPlaying()) {
                    mPlayPauseButton.setImageDrawable(
                            mResources.getDrawable(R.drawable.ic_pause_circle_filled));
                    mPlayPauseButton.setContentDescription(
                            mResources.getString(R.string.mcv2_pause_button_desc));
                } else {
                    mPlayPauseButton.setImageDrawable(
                            mResources.getDrawable(R.drawable.ic_play_circle_filled));
                    mPlayPauseButton.setContentDescription(
                            mResources.getString(R.string.mcv2_play_button_desc));
                }
            }
            if (mFfwdButton != null) {
                mFfwdButton.setAlpha(1.0f);
                mFfwdButton.setEnabled(true);
            }
        }
    }

    private class SettingsAdapter extends BaseAdapter {
        private List<Integer> mIconIds;
        private List<String> mMainTexts;
        private List<String> mSubTexts;

        SettingsAdapter(List<String> mainTexts, @Nullable List<String> subTexts,
                @Nullable List<Integer> iconIds) {
            mMainTexts = mainTexts;
            mSubTexts = subTexts;
            mIconIds = iconIds;
        }

        public void updateSubTexts(List<String> subTexts) {
            mSubTexts = subTexts;
            notifyDataSetChanged();
        }

        public String getMainText(int position) {
            if (mMainTexts != null) {
                if (position < mMainTexts.size()) {
                    return mMainTexts.get(position);
                }
            }
            return RESOURCE_EMPTY;
        }

        @Override
        public int getCount() {
            return (mMainTexts == null) ? 0 : mMainTexts.size();
        }

        @Override
        public long getItemId(int position) {
            // Auto-generated method stub--does not have any purpose here
            return 0;
        }

        @Override
        public Object getItem(int position) {
            // Auto-generated method stub--does not have any purpose here
            return null;
        }

        @Override
        public View getView(int position, View convertView, ViewGroup container) {
            View row;
            if (mSizeType == SIZE_TYPE_FULL) {
                row = inflateLayout(getContext(), R.layout.full_settings_list_item);
            } else {
                row = inflateLayout(getContext(), R.layout.embedded_settings_list_item);
            }
            TextView mainTextView = (TextView) row.findViewById(R.id.main_text);
            TextView subTextView = (TextView) row.findViewById(R.id.sub_text);
            ImageView iconView = (ImageView) row.findViewById(R.id.icon);

            // Set main text
            mainTextView.setText(mMainTexts.get(position));

            // Remove sub text and center the main text if sub texts do not exist at all or the sub
            // text at this particular position is empty.
            if (mSubTexts == null || RESOURCE_EMPTY.equals(mSubTexts.get(position))) {
                subTextView.setVisibility(View.GONE);
            } else {
                // Otherwise, set sub text.
                subTextView.setText(mSubTexts.get(position));
            }

            // Remove main icon and set visibility to gone if icons are set to null or the icon at
            // this particular position is set to RESOURCE_NON_EXISTENT.
            if (mIconIds == null || mIconIds.get(position) == RESOURCE_NON_EXISTENT) {
                iconView.setVisibility(View.GONE);
            } else {
                // Otherwise, set main icon.
                iconView.setImageDrawable(mResources.getDrawable(mIconIds.get(position)));
            }
            return row;
        }

        public void setSubTexts(List<String> subTexts) {
            mSubTexts = subTexts;
        }
    }

    private class SubSettingsAdapter extends BaseAdapter {
        private List<String> mTexts;
        private int mCheckPosition;

        SubSettingsAdapter(List<String> texts, int checkPosition) {
            mTexts = texts;
            mCheckPosition = checkPosition;
        }

        public String getMainText(int position) {
            if (mTexts != null) {
                if (position < mTexts.size()) {
                    return mTexts.get(position);
                }
            }
            return RESOURCE_EMPTY;
        }

        @Override
        public int getCount() {
            return (mTexts == null) ? 0 : mTexts.size();
        }

        @Override
        public long getItemId(int position) {
            // Auto-generated method stub--does not have any purpose here
            return 0;
        }

        @Override
        public Object getItem(int position) {
            // Auto-generated method stub--does not have any purpose here
            return null;
        }

        @Override
        public View getView(int position, View convertView, ViewGroup container) {
            View row;
            if (mSizeType == SIZE_TYPE_FULL) {
                row = inflateLayout(getContext(), R.layout.full_sub_settings_list_item);
            } else {
                row = inflateLayout(getContext(), R.layout.embedded_sub_settings_list_item);
            }
            TextView textView = (TextView) row.findViewById(R.id.text);
            ImageView checkView = (ImageView) row.findViewById(R.id.check);

            textView.setText(mTexts.get(position));
            if (position != mCheckPosition) {
                checkView.setVisibility(View.INVISIBLE);
            }
            return row;
        }

        public void setTexts(List<String> texts) {
            mTexts = texts;
        }

        public void setCheckPosition(int checkPosition) {
            mCheckPosition = checkPosition;
        }
    }

    class Controller {
        private MediaController mController;
        int mPlaybackState = SessionPlayer.PLAYER_STATE_IDLE;
        int mPrevState = SessionPlayer.PLAYER_STATE_IDLE;
        MediaMetadata mMediaMetadata;
        private Executor mCallbackExecutor;
        SessionCommandGroup mAllowedCommands;

        Controller() {
            mCallbackExecutor = ContextCompat.getMainExecutor(getContext());
        }

        void setSessionToken(SessionToken token) {
            if (mController != null) {
                mController.close();
            }
            mController = new MediaController(getContext(), token, mCallbackExecutor,
                    new MediaControllerCallback());
            mPlaybackState = mController.getPlayerState();
            MediaItem currentItem = mController.getCurrentMediaItem();
            mMediaMetadata = currentItem != null ? currentItem.getMetadata() : null;
        }

        boolean hasMetadata() {
            return mMediaMetadata != null;
        }

        boolean isPlaying() {
            return mPlaybackState == SessionPlayer.PLAYER_STATE_PLAYING;
        }

        long getCurrentPosition() {
            if (mController != null) {
                long currentPosition = mController.getCurrentPosition();
                return (currentPosition < 0) ? 0 : currentPosition;
            }
            return 0;
        }

        long getBufferPercentage() {
            if (mController != null && mDuration != 0) {
                long bufferedPos = mController.getBufferedPosition();
                return (bufferedPos < 0) ? -1 : (bufferedPos * 100 / mDuration);
            }
            return 0;
        }

        int getPlaybackState() {
            if (mController != null) {
                return mController.getPlayerState();
            }
            return SessionPlayer.PLAYER_STATE_IDLE;
        }

        boolean canPause() {
            return mAllowedCommands != null && mAllowedCommands.hasCommand(
                    SessionCommand.COMMAND_CODE_PLAYER_PAUSE);
        }

        boolean canSeekBackward() {
            return mAllowedCommands != null && mAllowedCommands.hasCommand(
                    SessionCommand.COMMAND_CODE_SESSION_REWIND);
        }

        boolean canSeekForward() {
            return mAllowedCommands != null && mAllowedCommands.hasCommand(
                    SessionCommand.COMMAND_CODE_SESSION_FAST_FORWARD);
        }

        boolean canSkipToNext() {
            return mAllowedCommands != null && mAllowedCommands.hasCommand(
                    SessionCommand.COMMAND_CODE_PLAYER_SKIP_TO_NEXT_PLAYLIST_ITEM);
        }

        boolean canSkipToPrevious() {
            return mAllowedCommands != null && mAllowedCommands.hasCommand(
                    SessionCommand.COMMAND_CODE_PLAYER_SKIP_TO_PREVIOUS_PLAYLIST_ITEM);
        }

        void pause() {
            if (mController != null) {
                mController.pause();
            }
        }

        void play() {
            if (mController != null) {
                mController.play();
            }
        }

        void seekTo(long posMs) {
            if (mController != null) {
                mController.seekTo(posMs);
            }
        }

        void skipToNextItem() {
            if (mController != null) {
                mController.skipToNextPlaylistItem();
            }
        }

        void skipToPreviousItem() {
            if (mController != null) {
                mController.skipToPreviousPlaylistItem();
            }
        }

        void setSpeed(float speed) {
            if (mController != null) {
                mController.setPlaybackSpeed(speed);
            }
        }

        void selectAudioTrack(int trackIndex) {
            if (mController != null) {
                Bundle extra = new Bundle();
                extra.putInt(KEY_SELECTED_AUDIO_INDEX, trackIndex);
                mController.sendCustomCommand(
                        new SessionCommand(COMMAND_SELECT_AUDIO_TRACK, null),
                        extra);
            }
        }

        void showSubtitle(int trackIndex) {
            if (mController != null) {
                Bundle extra = new Bundle();
                extra.putInt(KEY_SELECTED_SUBTITLE_INDEX, trackIndex);
                mController.sendCustomCommand(
                        new SessionCommand(COMMAND_SHOW_SUBTITLE, null), extra);
            }
        }

        void hideSubtitle() {
            if (mController != null) {
                mController.sendCustomCommand(
                        new SessionCommand(COMMAND_HIDE_SUBTITLE, null), null);
            }
        }

        long getDurationMs() {
            // TODO Remove this if-block after b/109639439 is fixed.
            if (mMediaMetadata != null) {
                if (mMediaMetadata.containsKey(MediaMetadata.METADATA_KEY_DURATION)) {
                    return mMediaMetadata.getLong(MediaMetadata.METADATA_KEY_DURATION);
                }
            }
            if (mController != null) {
                return mController.getDuration();
            }
            return 0;
        }

        CharSequence getTitle() {
            if (mMediaMetadata != null) {
                if (mMediaMetadata.containsKey(MediaMetadata.METADATA_KEY_TITLE)) {
                    return mMediaMetadata.getText(MediaMetadata.METADATA_KEY_TITLE);
                }
            }
            return null;
        }

        CharSequence getArtistText() {
            if (mMediaMetadata != null) {
                if (mMediaMetadata.containsKey(MediaMetadata.METADATA_KEY_ARTIST)) {
                    return mMediaMetadata.getText(MediaMetadata.METADATA_KEY_ARTIST);
                }
            }
            return null;
        }

        MediaItem getCurrentMediaItem() {
            if (mController != null) {
                return mController.getCurrentMediaItem();
            }
            return null;
        }

        void setAllowedCommands(SessionCommandGroup commands) {
            mAllowedCommands = commands;
        }

        SessionCommandGroup getAllowedCommands() {
            return mAllowedCommands;
        }

        class MediaControllerCallback extends MediaController.ControllerCallback {
            @Override
            public void onPlayerStateChanged(@NonNull MediaController controller,
                    @SessionPlayer.PlayerState int state) {
                if (DEBUG) {
                    Log.d(TAG, "onPlayerStateChanged(state: " + state + ")");
                }
                mPlaybackState = state;

                // Update pause button depending on playback state for the following two reasons:
                //   1) Need to handle case where app customizes playback state behavior when app
                //      activity is resumed.
                //   2) Need to handle case where the media file reaches end of duration.
                if (mPlaybackState != mPrevState) {
                    switch (mPlaybackState) {
                        case SessionPlayer.PLAYER_STATE_PLAYING:
                            removeCallbacks(mUpdateProgress);
                            post(mUpdateProgress);
                            resetHideCallbacks();
                            updateForStoppedState(false);
                            break;
                        case SessionPlayer.PLAYER_STATE_PAUSED:
                            mPlayPauseButton.setImageDrawable(
                                    mResources.getDrawable(R.drawable.ic_play_circle_filled));
                            mPlayPauseButton.setContentDescription(
                                    mResources.getString(R.string.mcv2_play_button_desc));
                            removeCallbacks(mUpdateProgress);
                            break;
                        case SessionPlayer.PLAYER_STATE_ERROR:
                            MediaControlView.this.setEnabled(false);
                            mPlayPauseButton.setImageDrawable(
                                    mResources.getDrawable(R.drawable.ic_play_circle_filled));
                            mPlayPauseButton.setContentDescription(
                                    mResources.getString(R.string.mcv2_play_button_desc));
                            removeCallbacks(mUpdateProgress);
                            if (getWindowToken() != null) {
                                new AlertDialog.Builder(getContext())
                                        .setMessage(R.string.mcv2_playback_error_text)
                                        .setPositiveButton(R.string.mcv2_error_dialog_button,
                                                new DialogInterface.OnClickListener() {
                                                    @Override
                                                    public void onClick(
                                                            DialogInterface dialogInterface,
                                                            int i) {
                                                        dialogInterface.dismiss();
                                                    }
                                                })
                                        .setCancelable(true)
                                        .show();
                            }
                    }
                    mPrevState = mPlaybackState;
                }
            }

            @Override
            public void onSeekCompleted(MediaController controller, long position) {
                if (DEBUG) {
                    Log.d(TAG, "onSeekCompleted(): " + position);
                }
                // Update progress bar and time text.
                int positionOnProgressBar = (mDuration <= 0)
                        ? 0 : (int) (MAX_PROGRESS * position / mDuration);
                mProgress.setProgress(positionOnProgressBar);
                mCurrentTime.setText(stringForTime(position));

                if (mNextSeekPosition != SEEK_POSITION_NOT_SET) {
                    mCurrentSeekPosition = mNextSeekPosition;

                    // If the next seek position is set, seek to that position.
                    MediaControlView.this.mController.seekTo(mNextSeekPosition);
                    mNextSeekPosition = SEEK_POSITION_NOT_SET;
                } else {
                    mCurrentSeekPosition = SEEK_POSITION_NOT_SET;

                    // If the next seek position is not set, start to update progress.
                    removeCallbacks(mUpdateProgress);
                    removeCallbacks(mHideMainBars);
                    post(mUpdateProgress);
                    postDelayed(mHideMainBars, mShowControllerIntervalMs);
                }
            }

            @Override
            public void onCurrentMediaItemChanged(@NonNull MediaController controller,
                    @Nullable MediaItem mediaItem) {
                if (DEBUG) {
                    Log.d(TAG, "onCurrentMediaItemChanged(): " + mediaItem);
                }
                if (mediaItem != null) {
                    mMediaMetadata = mediaItem.getMetadata();
                    updateMetadata();
                }
            }

            @Override
            public void onPlaybackCompleted(MediaController controller) {
                if (DEBUG) {
                    Log.d(TAG, "onPlaybackCompleted()");
                }
                updateForStoppedState(true);
                // The progress bar and current time text may not have been updated.
                mProgress.setProgress(MAX_PROGRESS);
                mCurrentTime.setText(stringForTime(mDuration));
            }

            @Override
            public void onConnected(@NonNull MediaController controller,
                    @NonNull SessionCommandGroup allowedCommands) {
                if (DEBUG) {
                    Log.d(TAG, "onConnected(): " + allowedCommands);
                }
                updateAllowedCommands(allowedCommands);

                MediaItem mediaItem = controller.getCurrentMediaItem();
                if (mediaItem != null) {
                    mMediaMetadata = mediaItem.getMetadata();
                    updateMetadata();
                }
            }

            @Override
            public void onAllowedCommandsChanged(@NonNull MediaController controller,
                    @NonNull SessionCommandGroup commands) {
                updateAllowedCommands(commands);
            }

            @Override
            public void onPlaylistChanged(@NonNull MediaController controller,
                    @NonNull List<MediaItem> list,
                    @Nullable MediaMetadata metadata) {
                if (DEBUG) {
                    Log.d(TAG, "onPlaylistChanged(): list: " + list);
                }
            }

            @Override
            public void onPlaybackSpeedChanged(@NonNull MediaController controller, float speed) {
                int customSpeedMultBy100 = Math.round(speed * 100);
                // An application may set a custom playback speed that is not included in the
                // default playback speed list. The code below handles adding/removing the custom
                // playback speed to the default list.
                if (mCustomPlaybackSpeedIndex != -1) {
                    // Remove existing custom playback speed
                    removeCustomSpeedFromList();
                }

                if (mPlaybackSpeedMultBy100List.contains(customSpeedMultBy100)) {
                    for (int i = 0; i < mPlaybackSpeedMultBy100List.size(); i++) {
                        if (customSpeedMultBy100 == mPlaybackSpeedMultBy100List.get(i)) {
                            updateSelectedSpeed(i, mPlaybackSpeedTextList.get(i));
                            break;
                        }
                    }
                } else {
                    String customSpeedText = mResources.getString(
                            R.string.MediaControlView_custom_playback_speed_text,
                            customSpeedMultBy100 / 100.0f);

                    for (int i = 0; i < mPlaybackSpeedMultBy100List.size(); i++) {
                        if (customSpeedMultBy100 < mPlaybackSpeedMultBy100List.get(i)) {
                            mPlaybackSpeedMultBy100List.add(i, customSpeedMultBy100);
                            mPlaybackSpeedTextList.add(i, customSpeedText);
                            updateSelectedSpeed(i, customSpeedText);
                            break;
                        }
                        // Add to end of list if the custom speed value is greater than all the
                        // value in the default speed list.
                        if (i == mPlaybackSpeedMultBy100List.size() - 1
                                && customSpeedMultBy100 > mPlaybackSpeedMultBy100List.get(i)) {
                            mPlaybackSpeedMultBy100List.add(customSpeedMultBy100);
                            mPlaybackSpeedTextList.add(customSpeedText);
                            updateSelectedSpeed(i + 1, customSpeedText);
                        }
                    }
                    mCustomPlaybackSpeedIndex = mSelectedSpeedIndex;
                }
            }

            @Override
            public MediaController.ControllerResult onCustomCommand(
                    @NonNull MediaController controller, @NonNull SessionCommand command,
                    @Nullable Bundle args) {
                if (DEBUG) {
                    Log.d(TAG, "onCustomCommand(): command: " + command);
                }
                switch (command.getCustomCommand()) {
                    case EVENT_UPDATE_TRACK_STATUS:
                        mVideoTrackCount = (args != null) ? args.getInt(KEY_VIDEO_TRACK_COUNT) : 0;
                        // If there is one or more audio tracks, and this information has not been
                        // reflected into the Settings window yet, automatically check the first
                        // track.
                        // Otherwise, the Audio Track selection will be defaulted to "None".
                        mAudioTrackCount = (args != null) ? args.getInt(KEY_AUDIO_TRACK_COUNT) : 0;
                        mAudioTrackList = new ArrayList<String>();
                        if (mAudioTrackCount > 0) {
                            for (int i = 0; i < mAudioTrackCount; i++) {
                                String track = mResources.getString(
                                        R.string.MediaControlView_audio_track_number_text, i + 1);
                                mAudioTrackList.add(track);
                            }
                            // Change sub text inside the Settings window.
                            mSettingsSubTextsList.set(SETTINGS_MODE_AUDIO_TRACK,
                                    mAudioTrackList.get(0));
                        } else {
                            mAudioTrackList.add(mResources.getString(
                                    R.string.MediaControlView_audio_track_none_text));
                        }
                        if (mVideoTrackCount == 0 && mAudioTrackCount > 0) {
                            mMediaType = MEDIA_TYPE_MUSIC;
                        }
                        mSubtitleTrackCount = (args != null)
                                ? args.getInt(KEY_SUBTITLE_TRACK_COUNT) : 0;
                        List<String> subtitleTracksLanguageList = (args != null)
                                ? args.getStringArrayList(KEY_SUBTITLE_TRACK_LANGUAGE_LIST) : null;
                        mSubtitleDescriptionsList = new ArrayList<String>();
                        if (mSubtitleTrackCount > 0) {
                            mSubtitleButton.setAlpha(1.0f);
                            mSubtitleButton.setEnabled(true);
                            mSubtitleDescriptionsList.add(mResources.getString(
                                    R.string.MediaControlView_subtitle_off_text));
                            for (int i = 0; i < mSubtitleTrackCount; i++) {
                                String lang = subtitleTracksLanguageList.get(i);
                                String track;
                                if (lang.equals("")) {
                                    track = mResources.getString(
                                            R.string.MediaControlView_subtitle_track_number_text,
                                            i + 1);
                                } else {
                                    track = mResources.getString(
                                            R.string
                                            .MediaControlView_subtitle_track_number_and_lang_text,
                                            i + 1, lang);
                                }
                                mSubtitleDescriptionsList.add(track);
                            }
                        } else {
                            if (mMediaType == MEDIA_TYPE_MUSIC) {
                                mSubtitleButton.setVisibility(View.GONE);
                            } else {
                                mSubtitleButton.setAlpha(0.5f);
                                mSubtitleButton.setEnabled(false);
                            }
                        }
                        break;
                    case EVENT_UPDATE_MEDIA_TYPE_STATUS:
                        boolean isAd = (args != null)
                                && args.getBoolean(KEY_STATE_IS_ADVERTISEMENT);
                        if (isAd != mIsAdvertisement) {
                            mIsAdvertisement = isAd;
                            updateLayoutForAd();
                        }
                        break;
                    case EVENT_UPDATE_SUBTITLE_SELECTED:
                        int selectedTrackIndex = args != null
                                ? args.getInt(KEY_SELECTED_SUBTITLE_INDEX, -1)
                                : -1;
                        if (selectedTrackIndex < 0 || selectedTrackIndex >= mSubtitleTrackCount) {
                            Log.w(TAG, "Selected subtitle track index (" + selectedTrackIndex
                                    + ") is out of range.");
                            break;
                        }
                        mSelectedSubtitleTrackIndex = selectedTrackIndex + 1;
                        if (mSettingsMode == SETTINGS_MODE_SUBTITLE_TRACK) {
                            mSubSettingsAdapter.setCheckPosition(mSelectedSubtitleTrackIndex);
                        }
                        break;
                    case EVENT_UPDATE_SUBTITLE_DESELECTED:
                        mSelectedSubtitleTrackIndex = 0;
                        if (mSettingsMode == SETTINGS_MODE_SUBTITLE_TRACK) {
                            mSubSettingsAdapter.setCheckPosition(mSelectedSubtitleTrackIndex);
                        }
                        break;
                    default:
                        return new MediaController.ControllerResult(
                                RESULT_CODE_NOT_SUPPORTED, null);
                }
                return new MediaController.ControllerResult(RESULT_CODE_SUCCESS, null);
            }
        }
    }
}