androidx.media.widget.MediaControlView2.java Source code

Java tutorial

Introduction

Here is the source code for androidx.media.widget.MediaControlView2.java

Source

/*
 * 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.media.widget;

import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;

import android.content.Context;
import android.content.res.Resources;
import android.graphics.Point;
import android.os.Bundle;
import android.support.v4.media.MediaMetadataCompat;
import android.support.v4.media.session.MediaControllerCompat;
import android.support.v4.media.session.PlaybackStateCompat;
import android.util.AttributeSet;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowManager;
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.IntDef;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.annotation.RestrictTo;
import androidx.media.SessionToken2;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Formatter;
import java.util.List;
import java.util.Locale;

// import androidx.mediarouter.app.MediaRouteButton;
// import androidx.mediarouter.media.MediaRouter;
// import androidx.mediarouter.media.MediaRouteSelector;

/**
 * @hide
 * A View that contains the controls for MediaPlayer2.
 * It provides a wide range of UI including buttons such as "Play/Pause", "Rewind", "Fast Forward",
 * "Subtitle", "Full Screen", and it is also possible to add multiple custom buttons.
 *
 * <p>
 * <em> MediaControlView2 can be initialized in two different ways: </em>
 * 1) When VideoView2 is initialized, it automatically initializes a MediaControlView2 instance and
 * adds it to the view.
 * 2) Initialize MediaControlView2 programmatically and add it to a ViewGroup instance.
 *
 * In the first option, VideoView2 automatically connects MediaControlView2 to MediaController,
 * which is necessary to communicate with MediaSession2. In the second option, however, the
 * developer needs to manually retrieve a MediaController instance and set it to MediaControlView2
 * by calling setController(MediaController controller).
 *
 * <p>
 * There is no separate method that handles the show/hide behavior for MediaControlView2. Instead,
 * one can directly change the visibility of this view by calling View.setVisibility(int). The
 * values supported are View.VISIBLE and View.GONE.
 * In addition, the following customization is supported:
 * Set focus to the play/pause button by calling requestPlayButtonFocus().
 *
 * <p>
 * It is also possible to add custom buttons with custom icons and actions inside MediaControlView2.
 * Those buttons will be shown when the overflow button is clicked.
 * See VideoView2#setCustomActions for more details on how to add.
 */
@RequiresApi(21) // TODO correct minSdk API use incompatibilities and remove before release.
@RestrictTo(LIBRARY_GROUP)
public class MediaControlView2 extends BaseLayout {
    /**
     * @hide
     */
    @RestrictTo(LIBRARY_GROUP)
    @IntDef({ BUTTON_PLAY_PAUSE, BUTTON_FFWD, BUTTON_REW, BUTTON_NEXT, BUTTON_PREV, BUTTON_SUBTITLE,
            BUTTON_FULL_SCREEN, BUTTON_OVERFLOW, BUTTON_MUTE, BUTTON_ASPECT_RATIO, BUTTON_SETTINGS })
    @Retention(RetentionPolicy.SOURCE)
    public @interface Button {
    }

    /**
     * MediaControlView2 button value for playing and pausing media.
     * @hide
     */
    @RestrictTo(LIBRARY_GROUP)
    public static final int BUTTON_PLAY_PAUSE = 1;
    /**
     * MediaControlView2 button value for jumping 30 seconds forward.
     * @hide
     */
    @RestrictTo(LIBRARY_GROUP)
    public static final int BUTTON_FFWD = 2;
    /**
     * MediaControlView2 button value for jumping 10 seconds backward.
     * @hide
     */
    @RestrictTo(LIBRARY_GROUP)
    public static final int BUTTON_REW = 3;
    /**
     * MediaControlView2 button value for jumping to next media.
     * @hide
     */
    @RestrictTo(LIBRARY_GROUP)
    public static final int BUTTON_NEXT = 4;
    /**
     * MediaControlView2 button value for jumping to previous media.
     * @hide
     */
    @RestrictTo(LIBRARY_GROUP)
    public static final int BUTTON_PREV = 5;
    /**
     * MediaControlView2 button value for showing/hiding subtitle track.
     * @hide
     */
    @RestrictTo(LIBRARY_GROUP)
    public static final int BUTTON_SUBTITLE = 6;
    /**
     * MediaControlView2 button value for toggling full screen.
     * @hide
     */
    @RestrictTo(LIBRARY_GROUP)
    public static final int BUTTON_FULL_SCREEN = 7;
    /**
     * MediaControlView2 button value for showing/hiding overflow buttons.
     * @hide
     */
    @RestrictTo(LIBRARY_GROUP)
    public static final int BUTTON_OVERFLOW = 8;
    /**
     * MediaControlView2 button value for muting audio.
     * @hide
     */
    @RestrictTo(LIBRARY_GROUP)
    public static final int BUTTON_MUTE = 9;
    /**
     * MediaControlView2 button value for adjusting aspect ratio of view.
     * @hide
     */
    @RestrictTo(LIBRARY_GROUP)
    public static final int BUTTON_ASPECT_RATIO = 10;
    /**
     * MediaControlView2 button value for showing/hiding settings page.
     * @hide
     */
    @RestrictTo(LIBRARY_GROUP)
    public static final int BUTTON_SETTINGS = 11;

    private static final String TAG = "MediaControlView2";

    static final String ARGUMENT_KEY_FULLSCREEN = "fullScreen";

    // TODO: Make these constants public api to support custom video view.
    // TODO: Combine these constants into one regarding TrackInfo.
    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_PLAYBACK_SPEED = "PlaybackSpeed";
    static final String KEY_SELECTED_AUDIO_INDEX = "SelectedAudioIndex";
    static final String KEY_SELECTED_SUBTITLE_INDEX = "SelectedSubtitleIndex";
    static final String EVENT_UPDATE_TRACK_STATUS = "UpdateTrackStatus";

    // TODO: Remove this once integrating with MediaSession2 & MediaMetadata2
    static final String KEY_STATE_IS_ADVERTISEMENT = "MediaTypeAdvertisement";
    static final String EVENT_UPDATE_MEDIA_TYPE_STATUS = "UpdateMediaTypeStatus";

    // 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";
    // TODO: remove once the implementation is revised
    public static final String COMMAND_SET_FULLSCREEN = "setFullscreen";
    // String for sending command to select audio track to MediaSession.
    static final String COMMAND_SELECT_AUDIO_TRACK = "SelectTrack";
    // String for sending command to set playback speed to MediaSession.
    static final String COMMAND_SET_PLAYBACK_SPEED = "SetPlaybackSpeed";
    // String for sending command to mute audio to MediaSession.
    static final String COMMAND_MUTE = "Mute";
    // String for sending command to unmute audio to MediaSession.
    static final String COMMAND_UNMUTE = "Unmute";

    private static final int SETTINGS_MODE_AUDIO_TRACK = 0;
    private static final int SETTINGS_MODE_PLAYBACK_SPEED = 1;
    private static final int SETTINGS_MODE_HELP = 2;
    private static final int SETTINGS_MODE_SUBTITLE_TRACK = 3;
    private static final int SETTINGS_MODE_VIDEO_QUALITY = 4;
    private static final int SETTINGS_MODE_MAIN = 5;
    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 SIZE_TYPE_EMBEDDED = 0;
    private static final int SIZE_TYPE_FULL = 1;
    // TODO: add support for Minimal size type.
    private static final int SIZE_TYPE_MINIMAL = 2;

    private static final int MAX_PROGRESS = 1000;
    private static final int DEFAULT_PROGRESS_UPDATE_TIME_MS = 1000;
    private static final int REWIND_TIME_MS = 10000;
    private static final int FORWARD_TIME_MS = 30000;
    private static final int AD_SKIP_WAIT_TIME_MS = 5000;
    private static final int RESOURCE_NON_EXISTENT = -1;
    private static final String RESOURCE_EMPTY = "";

    private Resources mResources;
    private MediaControllerCompat mController;
    private MediaControllerCompat.TransportControls mControls;
    private PlaybackStateCompat mPlaybackState;
    private MediaMetadataCompat mMetadata;
    private int mDuration;
    private int mPrevState;
    private int mPrevWidth;
    private int mPrevHeight;
    private int mOriginalLeftBarWidth;
    private int mVideoTrackCount;
    private int mAudioTrackCount;
    private int mSubtitleTrackCount;
    private int mSettingsMode;
    private int mSelectedSubtitleTrackIndex;
    private int mSelectedAudioTrackIndex;
    private int mSelectedVideoQualityIndex;
    private int mSelectedSpeedIndex;
    private int mEmbeddedSettingsItemWidth;
    private int mFullSettingsItemWidth;
    private int mEmbeddedSettingsItemHeight;
    private int mFullSettingsItemHeight;
    private int mSettingsWindowMargin;
    private int mMediaType;
    private int mSizeType;
    private int mOrientation;
    private long mPlaybackActions;
    private boolean mDragging;
    private boolean mIsFullScreen;
    private boolean mOverflowExpanded;
    private boolean mIsStopped;
    private boolean mSubtitleIsEnabled;
    private boolean mSeekAvailable;
    private boolean mIsAdvertisement;
    private boolean mIsMute;
    private boolean mNeedUXUpdate;

    // Relating to Title Bar View
    private ViewGroup mRoot;
    private View mTitleBar;
    private TextView mTitleView;
    private View mAdExternalLink;
    private ImageButton mBackButton;
    // TODO (b/77158231) revive
    // private MediaRouteButton mRouteButton;
    // private MediaRouteSelector mRouteSelector;

    // Relating to Center View
    private ViewGroup mCenterView;
    private View mTransportControls;
    private ImageButton mPlayPauseButton;
    private ImageButton mFfwdButton;
    private ImageButton mRewButton;
    private ImageButton mNextButton;
    private ImageButton mPrevButton;

    // Relating to Minimal Extra View
    private LinearLayout mMinimalExtraView;

    // Relating to Progress Bar View
    private 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;
    private TextView mCurrentTime;
    private TextView mAdSkipView;
    private StringBuilder mFormatBuilder;
    private Formatter mFormatter;

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

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

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

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

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

    public MediaControlView2(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr,
            int defStyleRes) {
        //        super((instance, superProvider, privateProvider) ->
        //                ApiLoader.getProvider().createMediaControlView2(
        //                        (MediaControlView2) instance, superProvider, privateProvider,
        //                        attrs, defStyleAttr, defStyleRes),
        //                context, attrs, defStyleAttr, defStyleRes);
        //        mProvider.initialize(attrs, defStyleAttr, defStyleRes);
        super(context, attrs, defStyleAttr, defStyleRes);

        mResources = getContext().getResources();
        // Inflate MediaControlView2 from XML
        mRoot = makeControllerView();
        addView(mRoot);
    }

    /**
     * Sets MediaSession2 token to control corresponding MediaSession2.
     */
    public void setMediaSessionToken(SessionToken2 token) {
        //mProvider.setMediaSessionToken_impl(token);
    }

    /**
     * Registers a callback to be invoked when the fullscreen mode should be changed.
     * @param l The callback that will be run
     */
    public void setOnFullScreenListener(OnFullScreenListener l) {
        //mProvider.setOnFullScreenListener_impl(l);
    }

    /**
     * @hide TODO: remove once the implementation is revised
     */
    @RestrictTo(LIBRARY_GROUP)
    public void setController(MediaControllerCompat controller) {
        mController = controller;
        if (controller != null) {
            mControls = controller.getTransportControls();
            // Set mMetadata and mPlaybackState to existing MediaSession variables since they may
            // be called before the callback is called
            mPlaybackState = mController.getPlaybackState();
            mMetadata = mController.getMetadata();
            updateDuration();
            updateTitle();

            mController.registerCallback(new MediaControllerCallback());
        }
    }

    /**
     * Changes the visibility state of an individual button. Default value is View.Visible.
     *
     * @param button the {@code Button} assigned to individual buttons
     * <ul>
     * <li>{@link #BUTTON_PLAY_PAUSE}
     * <li>{@link #BUTTON_FFWD}
     * <li>{@link #BUTTON_REW}
     * <li>{@link #BUTTON_NEXT}
     * <li>{@link #BUTTON_PREV}
     * <li>{@link #BUTTON_SUBTITLE}
     * <li>{@link #BUTTON_FULL_SCREEN}
     * <li>{@link #BUTTON_MUTE}
     * <li>{@link #BUTTON_OVERFLOW}
     * <li>{@link #BUTTON_ASPECT_RATIO}
     * <li>{@link #BUTTON_SETTINGS}
     * </ul>
     * @param visibility One of {@link #VISIBLE}, {@link #INVISIBLE}, or {@link #GONE}.
     * @hide
     */
    @RestrictTo(LIBRARY_GROUP)
    public void setButtonVisibility(@Button int button, /*@Visibility*/ int visibility) {
        // TODO: add member variables for Fast-Forward/Prvious/Rewind buttons to save visibility in
        // order to prevent being overriden inside updateLayout().
        switch (button) {
        case MediaControlView2.BUTTON_PLAY_PAUSE:
            if (mPlayPauseButton != null && canPause()) {
                mPlayPauseButton.setVisibility(visibility);
            }
            break;
        case MediaControlView2.BUTTON_FFWD:
            if (mFfwdButton != null && canSeekForward()) {
                mFfwdButton.setVisibility(visibility);
            }
            break;
        case MediaControlView2.BUTTON_REW:
            if (mRewButton != null && canSeekBackward()) {
                mRewButton.setVisibility(visibility);
            }
            break;
        case MediaControlView2.BUTTON_NEXT:
            if (mNextButton != null) {
                mNextButton.setVisibility(visibility);
            }
            break;
        case MediaControlView2.BUTTON_PREV:
            if (mPrevButton != null) {
                mPrevButton.setVisibility(visibility);
            }
            break;
        case MediaControlView2.BUTTON_SUBTITLE:
            if (mSubtitleButton != null && mSubtitleTrackCount > 0) {
                mSubtitleButton.setVisibility(visibility);
            }
            break;
        case MediaControlView2.BUTTON_FULL_SCREEN:
            if (mFullScreenButton != null) {
                mFullScreenButton.setVisibility(visibility);
            }
            break;
        case MediaControlView2.BUTTON_OVERFLOW:
            if (mOverflowButtonRight != null) {
                mOverflowButtonRight.setVisibility(visibility);
            }
            break;
        case MediaControlView2.BUTTON_MUTE:
            if (mMuteButton != null) {
                mMuteButton.setVisibility(visibility);
            }
            break;
        case MediaControlView2.BUTTON_SETTINGS:
            if (mSettingsButton != null) {
                mSettingsButton.setVisibility(visibility);
            }
            break;
        default:
            break;
        }
    }

    /**
     *  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(View view, boolean fullScreen);
    }

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

    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        return false;
    }

    // TODO: Should this function be removed?
    @Override
    public boolean onTrackballEvent(MotionEvent ev) {
        return false;
    }

    @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() || mPrevHeight != getMeasuredHeight() || mNeedUXUpdate) {
            // Dismiss SettingsWindow if it is showing.
            mSettingsWindow.dismiss();

            // These views may not have been initialized yet.
            if (mTransportControls.getWidth() == 0 || mTimeView.getWidth() == 0) {
                return;
            }

            int currWidth = getMeasuredWidth();
            int currHeight = getMeasuredHeight();
            WindowManager manager = (WindowManager) getContext().getApplicationContext()
                    .getSystemService(Context.WINDOW_SERVICE);
            Point screenSize = new Point();
            manager.getDefaultDisplay().getSize(screenSize);
            int screenWidth = screenSize.x;
            int screenHeight = screenSize.y;
            int fullIconSize = mResources.getDimensionPixelSize(R.dimen.mcv2_full_icon_size);
            int embeddedIconSize = mResources.getDimensionPixelSize(R.dimen.mcv2_embedded_icon_size);
            int marginSize = mResources.getDimensionPixelSize(R.dimen.mcv2_icon_margin);

            // TODO: add support for Advertisement Mode.
            if (mMediaType == MEDIA_TYPE_DEFAULT) {
                // Max number of icons inside BottomBarRightView for Music mode is 4.
                int maxIconCount = 4;
                updateLayout(maxIconCount, fullIconSize, embeddedIconSize, marginSize, currWidth, currHeight,
                        screenWidth, screenHeight);

            } else if (mMediaType == MEDIA_TYPE_MUSIC) {
                if (mNeedUXUpdate) {
                    // One-time operation for Music media type
                    mBasicControls.removeView(mMuteButton);
                    mExtraControls.addView(mMuteButton, 0);
                    mVideoQualityButton.setVisibility(View.GONE);
                    if (mFfwdButton != null) {
                        mFfwdButton.setVisibility(View.GONE);
                    }
                    if (mRewButton != null) {
                        mRewButton.setVisibility(View.GONE);
                    }
                }
                mNeedUXUpdate = false;

                // Max number of icons inside BottomBarRightView for Music mode is 3.
                int maxIconCount = 3;
                updateLayout(maxIconCount, fullIconSize, embeddedIconSize, marginSize, currWidth, currHeight,
                        screenWidth, screenHeight);
            }
            mPrevWidth = currWidth;
            mPrevHeight = currHeight;
        }
        // TODO: move this to a different location.
        // Update title bar parameters in order to avoid overlap between title view and the right
        // side of the title bar.
        updateTitleBarLayout();
    }

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

        // TODO: Merge the below code with disableUnsupportedButtons().
        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);
        }
        disableUnsupportedButtons();
    }

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

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

    // TODO (b/77158231) revive once androidx.mediarouter.* packagaes are available.
    /*
    void setRouteSelector(MediaRouteSelector selector) {
    mRouteSelector = selector;
    if (mRouteSelector != null && !mRouteSelector.isEmpty()) {
        mRouteButton.setRouteSelector(selector, MediaRouter.CALLBACK_FLAG_PERFORM_ACTIVE_SCAN);
        mRouteButton.setVisibility(View.VISIBLE);
    } else {
        mRouteButton.setRouteSelector(MediaRouteSelector.EMPTY);
        mRouteButton.setVisibility(View.GONE);
    }
    }
    */

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

    private boolean isPlaying() {
        if (mPlaybackState != null) {
            return mPlaybackState.getState() == PlaybackStateCompat.STATE_PLAYING;
        }
        return false;
    }

    private int getCurrentPosition() {
        mPlaybackState = mController.getPlaybackState();
        if (mPlaybackState != null) {
            return (int) mPlaybackState.getPosition();
        }
        return 0;
    }

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

    private boolean canPause() {
        if (mPlaybackState != null) {
            return (mPlaybackState.getActions() & PlaybackStateCompat.ACTION_PAUSE) != 0;
        }
        return true;
    }

    private boolean canSeekBackward() {
        if (mPlaybackState != null) {
            return (mPlaybackState.getActions() & PlaybackStateCompat.ACTION_REWIND) != 0;
        }
        return true;
    }

    private boolean canSeekForward() {
        if (mPlaybackState != null) {
            return (mPlaybackState.getActions() & PlaybackStateCompat.ACTION_FAST_FORWARD) != 0;
        }
        return true;
    }

    /**
     * Create the view that holds the widgets that control playback.
     * Derived classes can override this to create their own.
     *
     * @return The controller view.
     */
    // TODO: This was "protected". Determine if it should be protected in MCV2.
    private ViewGroup makeControllerView() {
        ViewGroup root = (ViewGroup) inflateLayout(getContext(), R.layout.media_controller);
        initControllerView(root);
        return root;
    }

    // TODO(b/76444971) make sure this is compatible with ApiHelper's one in updatable.
    private 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) {
        // 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);
        }
        // TODO (b/77158231) revive
        // 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 Extra View
        mMinimalExtraView = (LinearLayout) v.findViewById(R.id.minimal_extra_view);
        LinearLayout.LayoutParams params = (LinearLayout.LayoutParams) mMinimalExtraView.getLayoutParams();
        int iconSize = mResources.getDimensionPixelSize(R.dimen.mcv2_embedded_icon_size);
        int marginSize = mResources.getDimensionPixelSize(R.dimen.mcv2_icon_margin);
        params.setMargins(0, (iconSize + marginSize * 2) * (-1), 0, 0);
        mMinimalExtraView.setLayoutParams(params);
        mMinimalExtraView.setVisibility(View.GONE);

        // Relating to Progress Bar View
        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));
            }
            mProgress.setMax(MAX_PROGRESS);
        }
        mProgressBuffer = v.findViewById(R.id.progress_buffer);

        // 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
        mBottomBarRightView = v.findViewById(R.id.bottom_bar_right);
        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);
            // TODO: Show Fullscreen button when only it is possible.
        }
        mOverflowButtonRight = v.findViewById(R.id.overflow_right);
        if (mOverflowButtonRight != null) {
            mOverflowButtonRight.setOnClickListener(mOverflowRightListener);
        }
        mOverflowButtonLeft = v.findViewById(R.id.overflow_left);
        if (mOverflowButtonLeft != null) {
            mOverflowButtonLeft.setOnClickListener(mOverflowLeftListener);
        }
        mMuteButton = v.findViewById(R.id.mute);
        if (mMuteButton != null) {
            mMuteButton.setOnClickListener(mMuteButtonListener);
        }
        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);
        mEmbeddedSettingsItemHeight = mResources.getDimensionPixelSize(R.dimen.mcv2_embedded_settings_height);
        mFullSettingsItemHeight = mResources.getDimensionPixelSize(R.dimen.mcv2_full_settings_height);
        mSettingsWindowMargin = (-1) * mResources.getDimensionPixelSize(R.dimen.mcv2_settings_offset);
        mSettingsWindow = new PopupWindow(mSettingsListView, mEmbeddedSettingsItemWidth, LayoutParams.WRAP_CONTENT,
                true);
    }

    /**
     * Disable pause or seek buttons if the stream cannot be paused or seeked.
     * This requires the control interface to be a MediaPlayerControlExt
     */
    private void disableUnsupportedButtons() {
        try {
            if (mPlayPauseButton != null && !canPause()) {
                mPlayPauseButton.setEnabled(false);
            }
            if (mRewButton != null && !canSeekBackward()) {
                mRewButton.setEnabled(false);
            }
            if (mFfwdButton != null && !canSeekForward()) {
                mFfwdButton.setEnabled(false);
            }
            // TODO What we really should do is add a canSeek to the MediaPlayerControl interface;
            // this scheme can break the case when applications want to allow seek through the
            // progress bar but disable forward/backward buttons.
            //
            // However, currently the flags SEEK_BACKWARD_AVAILABLE, SEEK_FORWARD_AVAILABLE,
            // and SEEK_AVAILABLE are all (un)set together; as such the aforementioned issue
            // shouldn't arise in existing applications.
            if (mProgress != null && !canSeekBackward() && !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.
        }
    }

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

    private String stringForTime(int timeMs) {
        int totalSeconds = timeMs / 1000;

        int seconds = totalSeconds % 60;
        int minutes = (totalSeconds / 60) % 60;
        int 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();
        }
    }

    private int setProgress() {
        if (mController == null || mDragging) {
            return 0;
        }
        int positionOnProgressBar = 0;
        int currentPosition = getCurrentPosition();
        if (mDuration > 0) {
            positionOnProgressBar = (int) (MAX_PROGRESS * (long) 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 (getBufferPercentage() < 0) {
                mProgress.setSecondaryProgress(MAX_PROGRESS);
            } else {
                mProgress.setSecondaryProgress(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.MediaControlView2_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) {
                int remainingTime = (mDuration - currentPosition < 0) ? 0 : (mDuration - currentPosition);
                String remainingTimeText = mResources.getString(R.string.MediaControlView2_ad_remaining_time,
                        stringForTime(remainingTime));
                mAdRemainingView.setText(remainingTimeText);
            }
        }
        return currentPosition;
    }

    private void togglePausePlayState() {
        if (isPlaying()) {
            mControls.pause();
            mPlayPauseButton.setImageDrawable(mResources.getDrawable(R.drawable.ic_play_circle_filled, null));
            mPlayPauseButton.setContentDescription(mResources.getString(R.string.mcv2_play_button_desc));
        } else {
            mControls.play();
            mPlayPauseButton.setImageDrawable(mResources.getDrawable(R.drawable.ic_pause_circle_filled, null));
            mPlayPauseButton.setContentDescription(mResources.getString(R.string.mcv2_pause_button_desc));
        }
    }

    // There are two scenarios that can trigger the seekbar listener to trigger:
    //
    // The first is the user using the touchpad to adjust the posititon 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);

            // 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) {
                mPlayPauseButton.setImageDrawable(mResources.getDrawable(R.drawable.ic_play_circle_filled, null));
                mPlayPauseButton.setContentDescription(mResources.getString(R.string.mcv2_play_button_desc));
                mIsStopped = false;
            }
        }

        @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;
            }
            if (mDuration > 0) {
                int position = (int) (((long) mDuration * progress) / MAX_PROGRESS);
                mControls.seekTo(position);

                if (mCurrentTime != null) {
                    mCurrentTime.setText(stringForTime(position));
                }
            }
        }

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

            setProgress();

            // Ensure that progress is properly updated in the future,
            // the call to show() does not guarantee this because it is a
            // no-op if we are already showing.
            post(mUpdateProgress);
        }
    };

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

    private final OnClickListener mRewListener = new OnClickListener() {
        @Override
        public void onClick(View v) {
            int pos = getCurrentPosition() - REWIND_TIME_MS;
            mControls.seekTo(pos);
            setProgress();
        }
    };

    private final OnClickListener mFfwdListener = new OnClickListener() {
        @Override
        public void onClick(View v) {
            int pos = getCurrentPosition() + FORWARD_TIME_MS;
            mControls.seekTo(pos);
            setProgress();
        }
    };

    private final OnClickListener mNextListener = new OnClickListener() {
        @Override
        public void onClick(View v) {
            mControls.skipToNext();
        }
    };

    private final OnClickListener mPrevListener = new OnClickListener() {
        @Override
        public void onClick(View v) {
            mControls.skipToPrevious();
        }
    };

    private final OnClickListener mBackListener = new OnClickListener() {
        @Override
        public void onClick(View v) {
            // TODO: implement
        }
    };

    private final OnClickListener mSubtitleListener = new OnClickListener() {
        @Override
        public void onClick(View v) {
            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) {
            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) {
            final boolean isEnteringFullScreen = !mIsFullScreen;
            // TODO: Re-arrange the button layouts according to the UX.
            if (isEnteringFullScreen) {
                mFullScreenButton.setImageDrawable(mResources.getDrawable(R.drawable.ic_fullscreen_exit, null));
            } else {
                mFullScreenButton.setImageDrawable(mResources.getDrawable(R.drawable.ic_fullscreen, null));
            }
            Bundle args = new Bundle();
            args.putBoolean(ARGUMENT_KEY_FULLSCREEN, isEnteringFullScreen);
            mController.sendCommand(COMMAND_SET_FULLSCREEN, args, null);

            mIsFullScreen = isEnteringFullScreen;
        }
    };

    private final OnClickListener mOverflowRightListener = new OnClickListener() {
        @Override
        public void onClick(View v) {
            mBasicControls.setVisibility(View.GONE);
            mExtraControls.setVisibility(View.VISIBLE);
        }
    };

    private final OnClickListener mOverflowLeftListener = new OnClickListener() {
        @Override
        public void onClick(View v) {
            mBasicControls.setVisibility(View.VISIBLE);
            mExtraControls.setVisibility(View.GONE);
        }
    };

    private final OnClickListener mMuteButtonListener = new OnClickListener() {
        @Override
        public void onClick(View v) {
            if (!mIsMute) {
                mMuteButton.setImageDrawable(mResources.getDrawable(R.drawable.ic_mute, null));
                mMuteButton.setContentDescription(mResources.getString(R.string.mcv2_muted_button_desc));
                mIsMute = true;
                mController.sendCommand(COMMAND_MUTE, null, null);
            } else {
                mMuteButton.setImageDrawable(mResources.getDrawable(R.drawable.ic_unmute, null));
                mMuteButton.setContentDescription(mResources.getString(R.string.mcv2_unmuted_button_desc));
                mIsMute = false;
                mController.sendCommand(COMMAND_UNMUTE, null, null);
            }
        }
    };

    private final OnClickListener mSettingsButtonListener = new OnClickListener() {
        @Override
        public void onClick(View v) {
            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;
                } else if (position == SETTINGS_MODE_HELP) {
                    // TODO: implement this.
                    mSettingsWindow.dismiss();
                    return;
                }
                displaySettingsWindow(mSubSettingsAdapter);
                break;
            case SETTINGS_MODE_AUDIO_TRACK:
                if (position != mSelectedAudioTrackIndex) {
                    mSelectedAudioTrackIndex = position;
                    if (mAudioTrackCount > 0) {
                        Bundle extra = new Bundle();
                        extra.putInt(KEY_SELECTED_AUDIO_INDEX, position);
                        mController.sendCommand(COMMAND_SELECT_AUDIO_TRACK, extra, null);
                    }
                    mSettingsSubTextsList.set(SETTINGS_MODE_AUDIO_TRACK, mSubSettingsAdapter.getMainText(position));
                }
                mSettingsWindow.dismiss();
                break;
            case SETTINGS_MODE_PLAYBACK_SPEED:
                if (position != mSelectedSpeedIndex) {
                    mSelectedSpeedIndex = position;
                    Bundle extra = new Bundle();
                    extra.putFloat(KEY_PLAYBACK_SPEED, mPlaybackSpeedList.get(position));
                    mController.sendCommand(COMMAND_SET_PLAYBACK_SPEED, extra, null);
                    mSettingsSubTextsList.set(SETTINGS_MODE_PLAYBACK_SPEED,
                            mSubSettingsAdapter.getMainText(position));
                }
                mSettingsWindow.dismiss();
                break;
            case SETTINGS_MODE_HELP:
                // TODO: implement this.
                break;
            case SETTINGS_MODE_SUBTITLE_TRACK:
                if (position != mSelectedSubtitleTrackIndex) {
                    mSelectedSubtitleTrackIndex = position;
                    if (position > 0) {
                        Bundle extra = new Bundle();
                        extra.putInt(KEY_SELECTED_SUBTITLE_INDEX, position - 1);
                        mController.sendCommand(COMMAND_SHOW_SUBTITLE, extra, null);
                        mSubtitleButton.setImageDrawable(mResources.getDrawable(R.drawable.ic_subtitle_on, null));
                        mSubtitleButton.setContentDescription(mResources.getString(R.string.mcv2_cc_is_on));
                        mSubtitleIsEnabled = true;
                    } else {
                        mController.sendCommand(COMMAND_HIDE_SUBTITLE, null, null);
                        mSubtitleButton.setImageDrawable(mResources.getDrawable(R.drawable.ic_subtitle_off, null));
                        mSubtitleButton.setContentDescription(mResources.getString(R.string.mcv2_cc_is_off));
                        mSubtitleIsEnabled = false;
                    }
                }
                mSettingsWindow.dismiss();
                break;
            case SETTINGS_MODE_VIDEO_QUALITY:
                // TODO: add support for video quality
                mSelectedVideoQualityIndex = position;
                mSettingsWindow.dismiss();
                break;
            }
        }
    };

    private void updateDuration() {
        if (mMetadata != null) {
            if (mMetadata.containsKey(MediaMetadataCompat.METADATA_KEY_DURATION)) {
                mDuration = (int) mMetadata.getLong(MediaMetadataCompat.METADATA_KEY_DURATION);
                // update progress bar
                setProgress();
            }
        }
    }

    private void updateTitle() {
        if (mMetadata != null) {
            if (mMetadata.containsKey(MediaMetadataCompat.METADATA_KEY_TITLE)) {
                mTitleView.setText(mMetadata.getString(MediaMetadataCompat.METADATA_KEY_TITLE));
            }
        }
    }

    // The title bar is made up of two separate LinearLayouts. If the sum of the two bars are
    // greater than the length of the title bar, reduce the size of the left bar (which makes the
    // TextView that contains the title of the media file shrink).
    private void updateTitleBarLayout() {
        if (mTitleBar != null) {
            int titleBarWidth = mTitleBar.getWidth();

            View leftBar = mTitleBar.findViewById(R.id.title_bar_left);
            View rightBar = mTitleBar.findViewById(R.id.title_bar_right);
            int leftBarWidth = leftBar.getWidth();
            int rightBarWidth = rightBar.getWidth();

            RelativeLayout.LayoutParams params = (RelativeLayout.LayoutParams) leftBar.getLayoutParams();
            if (leftBarWidth + rightBarWidth > titleBarWidth) {
                params.width = titleBarWidth - rightBarWidth;
                mOriginalLeftBarWidth = leftBarWidth;
            } else if (leftBarWidth + rightBarWidth < titleBarWidth && mOriginalLeftBarWidth != 0) {
                params.width = mOriginalLeftBarWidth;
                mOriginalLeftBarWidth = 0;
            }
            leftBar.setLayoutParams(params);
        }
    }

    private void updateAudioMetadata() {
        if (mMediaType != MEDIA_TYPE_MUSIC) {
            return;
        }

        if (mMetadata != null) {
            String titleText = "";
            String artistText = "";
            if (mMetadata.containsKey(MediaMetadataCompat.METADATA_KEY_TITLE)) {
                titleText = mMetadata.getString(MediaMetadataCompat.METADATA_KEY_TITLE);
            } else {
                titleText = mResources.getString(R.string.mcv2_music_title_unknown_text);
            }

            if (mMetadata.containsKey(MediaMetadataCompat.METADATA_KEY_ARTIST)) {
                artistText = mMetadata.getString(MediaMetadataCompat.METADATA_KEY_ARTIST);
            } else {
                artistText = mResources.getString(R.string.mcv2_music_artist_unknown_text);
            }

            // Update title for Embedded size type
            mTitleView.setText(titleText + " - " + artistText);

            // Set to true to update layout inside onMeasure()
            mNeedUXUpdate = true;
        }
    }

    private void updateLayout() {
        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 maxIconCount, int fullIconSize, int embeddedIconSize, int marginSize,
            int currWidth, int currHeight, int screenWidth, int screenHeight) {
        int fullBottomBarRightWidthMax = fullIconSize * maxIconCount + marginSize * (maxIconCount * 2);
        int embeddedBottomBarRightWidthMax = embeddedIconSize * maxIconCount + marginSize * (maxIconCount * 2);
        int fullWidth = mTransportControls.getWidth() + mTimeView.getWidth() + fullBottomBarRightWidthMax;
        int embeddedWidth = mTimeView.getWidth() + embeddedBottomBarRightWidthMax;
        int screenMaxLength = Math.max(screenWidth, screenHeight);

        if (fullWidth > screenMaxLength) {
            // TODO: screen may be smaller than the length needed for Full size.
        }

        boolean isFullSize = (mMediaType == MEDIA_TYPE_DEFAULT) ? (currWidth == screenMaxLength)
                : (currWidth == screenWidth && currHeight == screenHeight);

        if (isFullSize) {
            if (mSizeType != SIZE_TYPE_FULL) {
                updateLayoutForSizeChange(SIZE_TYPE_FULL);
                if (mMediaType == MEDIA_TYPE_MUSIC) {
                    mTitleView.setVisibility(View.GONE);
                }
            }
        } 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);

            // Relating to Full Screen Button
            mMinimalExtraView.setVisibility(View.GONE);
            mFullScreenButton = mBottomBarRightView.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));
            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);

            // Relating to Full Screen Button
            mMinimalExtraView.setVisibility(View.GONE);
            mFullScreenButton = mBottomBarRightView.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));
            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.bottom_bar_right);
            }
            break;
        case SIZE_TYPE_MINIMAL:
            // Relating to Title Bar
            mTitleBar.setVisibility(View.GONE);
            mBackButton.setVisibility(View.GONE);

            // Relating to Full Screen Button
            mMinimalExtraView.setVisibility(View.VISIBLE);
            mFullScreenButton = mMinimalExtraView.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);

        if (isPlaying()) {
            mPlayPauseButton.setImageDrawable(mResources.getDrawable(R.drawable.ic_pause_circle_filled, null));
            mPlayPauseButton.setContentDescription(mResources.getString(R.string.mcv2_pause_button_desc));
        } else {
            mPlayPauseButton.setImageDrawable(mResources.getDrawable(R.drawable.ic_play_circle_filled, null));
            mPlayPauseButton.setContentDescription(mResources.getString(R.string.mcv2_play_button_desc));
        }

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

    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) {
            mFfwdButton.setOnClickListener(mFfwdListener);
            if (mMediaType == MEDIA_TYPE_MUSIC) {
                mFfwdButton.setVisibility(View.GONE);
            }
        }
        mRewButton = v.findViewById(R.id.rew);
        if (mRewButton != null) {
            mRewButton.setOnClickListener(mRewListener);
            if (mMediaType == MEDIA_TYPE_MUSIC) {
                mRewButton.setVisibility(View.GONE);
            }
        }
        // TODO: Add support for Next and Previous buttons
        mNextButton = v.findViewById(R.id.next);
        if (mNextButton != null) {
            mNextButton.setOnClickListener(mNextListener);
            mNextButton.setVisibility(View.GONE);
        }
        mPrevButton = v.findViewById(R.id.prev);
        if (mPrevButton != null) {
            mPrevButton.setOnClickListener(mPrevListener);
            mPrevButton.setVisibility(View.GONE);
        }
        return v;
    }

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

        mSettingsSubTextsList = new ArrayList<String>();
        mSettingsSubTextsList.add(mResources.getString(R.string.MediaControlView2_audio_track_none_text));
        mSettingsSubTextsList
                .add(mResources.getStringArray(R.array.MediaControlView2_playback_speeds)[PLAYBACK_SPEED_1x_INDEX]);
        mSettingsSubTextsList.add(RESOURCE_EMPTY);

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

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

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

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

        mPlaybackSpeedList = new ArrayList<Float>();
        int[] speeds = mResources.getIntArray(R.array.speed_multiplied_by_100);
        for (int i = 0; i < speeds.length; i++) {
            float speed = (float) speeds[i] / 100.0f;
            mPlaybackSpeedList.add(speed);
        }
    }

    private 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 and show
        int itemHeight = (mSizeType == SIZE_TYPE_EMBEDDED) ? mEmbeddedSettingsItemHeight : mFullSettingsItemHeight;
        int totalHeight = adapter.getCount() * itemHeight;
        mSettingsWindow.dismiss();
        mSettingsWindow.showAsDropDown(this, mSettingsWindowMargin, mSettingsWindowMargin - totalHeight,
                Gravity.BOTTOM | Gravity.RIGHT);
    }

    @RequiresApi(26) // TODO correct minSdk API use incompatibilities and remove before release.
    private class MediaControllerCallback extends MediaControllerCompat.Callback {
        @Override
        public void onPlaybackStateChanged(PlaybackStateCompat 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.getState() != mPrevState) {
                switch (mPlaybackState.getState()) {
                case PlaybackStateCompat.STATE_PLAYING:
                    mPlayPauseButton
                            .setImageDrawable(mResources.getDrawable(R.drawable.ic_pause_circle_filled, null));
                    mPlayPauseButton.setContentDescription(mResources.getString(R.string.mcv2_pause_button_desc));
                    removeCallbacks(mUpdateProgress);
                    post(mUpdateProgress);
                    break;
                case PlaybackStateCompat.STATE_PAUSED:
                    mPlayPauseButton
                            .setImageDrawable(mResources.getDrawable(R.drawable.ic_play_circle_filled, null));
                    mPlayPauseButton.setContentDescription(mResources.getString(R.string.mcv2_play_button_desc));
                    break;
                case PlaybackStateCompat.STATE_STOPPED:
                    mPlayPauseButton
                            .setImageDrawable(mResources.getDrawable(R.drawable.ic_replay_circle_filled, null));
                    mPlayPauseButton.setContentDescription(mResources.getString(R.string.mcv2_replay_button_desc));
                    mIsStopped = true;
                    break;
                default:
                    break;
                }
                mPrevState = mPlaybackState.getState();
            }

            if (mPlaybackActions != mPlaybackState.getActions()) {
                long newActions = mPlaybackState.getActions();
                if ((newActions & PlaybackStateCompat.ACTION_PAUSE) != 0) {
                    mPlayPauseButton.setVisibility(View.VISIBLE);
                }
                if ((newActions & PlaybackStateCompat.ACTION_REWIND) != 0 && mMediaType != MEDIA_TYPE_MUSIC) {
                    if (mRewButton != null) {
                        mRewButton.setVisibility(View.VISIBLE);
                    }
                }
                if ((newActions & PlaybackStateCompat.ACTION_FAST_FORWARD) != 0 && mMediaType != MEDIA_TYPE_MUSIC) {
                    if (mFfwdButton != null) {
                        mFfwdButton.setVisibility(View.VISIBLE);
                    }
                }
                if ((newActions & PlaybackStateCompat.ACTION_SEEK_TO) != 0) {
                    mSeekAvailable = true;
                } else {
                    mSeekAvailable = false;
                }
                mPlaybackActions = newActions;
            }

            // Add buttons if custom actions are present.
            List<PlaybackStateCompat.CustomAction> customActions = mPlaybackState.getCustomActions();
            mCustomButtons.removeAllViews();
            if (customActions.size() > 0) {
                for (final PlaybackStateCompat.CustomAction action : customActions) {
                    ImageButton button = new ImageButton(getContext(), null /* AttributeSet */, 0 /* Style */);
                    // TODO: Apply R.style.BottomBarButton to this button using library context.
                    // Refer Constructor with argument (int defStyleRes) of View.java
                    button.setImageResource(action.getIcon());
                    button.setTooltipText(action.getName());
                    final String actionString = action.getAction().toString();
                    button.setOnClickListener(new OnClickListener() {
                        @Override
                        public void onClick(View v) {
                            // TODO: Currently, we are just sending extras that came from session.
                            // Is it the right behavior?
                            mControls.sendCustomAction(actionString, action.getExtras());
                            setVisibility(View.VISIBLE);
                        }
                    });
                    mCustomButtons.addView(button);
                }
            }
        }

        @Override
        public void onMetadataChanged(MediaMetadataCompat metadata) {
            mMetadata = metadata;
            updateDuration();
            updateTitle();
            updateAudioMetadata();
        }

        @Override
        public void onSessionEvent(String event, Bundle extras) {
            switch (event) {
            case EVENT_UPDATE_TRACK_STATUS:
                mVideoTrackCount = extras.getInt(KEY_VIDEO_TRACK_COUNT);
                // 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 = extras.getInt(KEY_AUDIO_TRACK_COUNT);
                mAudioTrackList = new ArrayList<String>();
                if (mAudioTrackCount > 0) {
                    // TODO: add more text about track info.
                    for (int i = 0; i < mAudioTrackCount; i++) {
                        String track = mResources.getString(R.string.MediaControlView2_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.MediaControlView2_audio_track_none_text));
                }
                if (mVideoTrackCount == 0 && mAudioTrackCount > 0) {
                    mMediaType = MEDIA_TYPE_MUSIC;
                }

                mSubtitleTrackCount = extras.getInt(KEY_SUBTITLE_TRACK_COUNT);
                mSubtitleDescriptionsList = new ArrayList<String>();
                if (mSubtitleTrackCount > 0) {
                    mSubtitleButton.setVisibility(View.VISIBLE);
                    mSubtitleButton.setEnabled(true);
                    mSubtitleDescriptionsList
                            .add(mResources.getString(R.string.MediaControlView2_subtitle_off_text));
                    for (int i = 0; i < mSubtitleTrackCount; i++) {
                        String track = mResources.getString(R.string.MediaControlView2_subtitle_track_number_text,
                                i + 1);
                        mSubtitleDescriptionsList.add(track);
                    }
                } else {
                    mSubtitleButton.setVisibility(View.GONE);
                    mSubtitleButton.setEnabled(false);
                }
                break;
            case EVENT_UPDATE_MEDIA_TYPE_STATUS:
                boolean newStatus = extras.getBoolean(KEY_STATE_IS_ADVERTISEMENT);
                if (newStatus != mIsAdvertisement) {
                    mIsAdvertisement = newStatus;
                    updateLayout();
                }
                break;
            }
        }
    }

    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
            // TODO: implement this.
            return 0;
        }

        @Override
        public Object getItem(int position) {
            // Auto-generated method stub--does not have any purpose here
            // TODO: implement this.
            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), null));
            }
            return row;
        }

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

    // TODO: extend this class from SettingsAdapter
    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
            // TODO: implement this.
            return 0;
        }

        @Override
        public Object getItem(int position) {
            // Auto-generated method stub--does not have any purpose here
            // TODO: implement this.
            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;
        }
    }
}