/*
* 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.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.res.Resources;
import android.graphics.Point;
import android.graphics.drawable.GradientDrawable;
import android.media.AudioManager;
import android.os.Bundle;
import android.os.ResultReceiver;
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.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.IntDef;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.annotation.RestrictTo;
import androidx.media.BaseMediaPlayer;
import androidx.media.MediaController2;
import androidx.media.MediaItem2;
import androidx.media.MediaMetadata2;
import androidx.media.SessionCommand2;
import androidx.media.SessionCommandGroup2;
import androidx.media.SessionToken2;
import androidx.mediarouter.app.MediaRouteButton;
import androidx.mediarouter.media.MediaRouteSelector;
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 java.util.concurrent.Executor;
/**
* A View that contains the controls for {@link android.media.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, mute/unmute, and adjust playback speed.
*
* <p>
* <em> MediaControlView2 can be initialized in two different ways: </em>
* 1) When initializing {@link VideoView2} a default MediaControlView2 is created.
* 2) Initialize MediaControlView2 programmatically and add it to a {@link ViewGroup} instance.
*
* In the first option, VideoView2 automatically connects MediaControlView2 to MediaController,
* which is necessary to communicate with MediaSession. In the second option, however, the
* developer needs to manually retrieve a MediaController instance from MediaSession and set it to
* MediaControlView2.
*
* <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 {@link View#setVisibility(int)}.
* The values supported are View.VISIBLE and View.GONE.
*
* <p>
* In addition, the following customizations are supported:
* 1) Set focus to the play/pause button by calling requestPlayButtonFocus().
* 2) Set full screen mode
*
*/
@RequiresApi(21) // TODO correct minSdk API use incompatibilities and remove before release.
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";
private 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_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";
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";
// 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;
private static final int SIZE_TYPE_MINIMAL = 2;
private static final int UX_STATE_FULL = 0;
private static final int UX_STATE_PROGRESS_BAR_ONLY = 1;
private static final int UX_STATE_EMPTY = 2;
private static final int UX_STATE_ANIMATING = 3;
private static final long DEFAULT_SHOW_CONTROLLER_INTERVAL_MS = 2000;
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 int HIDE_TIME_MS = 250;
private static final int SHOW_TIME_MS = 250;
private static final String RESOURCE_EMPTY = "";
private Resources mResources;
private ControllerInterface mController;
private OnFullScreenListener mOnFullScreenListener;
private AccessibilityManager mAccessibilityManager;
private SessionCommandGroup2 mAllowedCommands;
private int mDuration;
private int mPrevState;
private int mPrevWidth;
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 mSettingsItemHeight;
private int mSettingsWindowMargin;
private int mMediaType;
private int mSizeType;
private int mUxState;
private long mPlaybackActions;
private long mShowControllerIntervalMs;
private boolean mDragging;
private boolean mIsFullScreen;
private boolean mOverflowIsShowing;
private boolean mIsStopped;
private boolean mSeekAvailable;
private boolean mIsAdvertisement;
private boolean mIsMute;
private boolean mNeedUxUpdate;
private boolean mNeedToHideBars;
// 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;
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 View mProgressBar;
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 mOverflowShowButton;
private ImageButton mOverflowHideButton;
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;
private AnimatorSet mHideMainBarsAnimator;
private AnimatorSet mHideProgressBarAnimator;
private AnimatorSet mHideAllBarsAnimator;
private AnimatorSet mShowMainBarsAnimator;
private AnimatorSet mShowAllBarsAnimator;
private ValueAnimator mOverflowShowAnimator;
private ValueAnimator mOverflowHideAnimator;
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) {
super(context, attrs, defStyleAttr);
mResources = context.getResources();
// Inflate MediaControlView2 from XML
mRoot = makeControllerView();
addView(mRoot);
mShowControllerIntervalMs = DEFAULT_SHOW_CONTROLLER_INTERVAL_MS;
mAccessibilityManager = (AccessibilityManager) context.getSystemService(
Context.ACCESSIBILITY_SERVICE);
}
/**
* Sets MediaSession2 token to control corresponding MediaSession2. It makes it possible to
* send and receive data between MediaControlView2 and VideoView2.
* @hide TODO: unhide
*/
@RestrictTo(LIBRARY_GROUP)
public void setMediaSessionToken(SessionToken2 token) {
if (mController != null) {
mController.close();
}
mController = new Controller2(token);
if (mController.hasMetadata()) {
updateDuration();
updateTitle();
}
}
/**
* Sets MediaControllerCompat to control corresponding MediaSession2.
* This is temporally provided for working with MP1 on lower devices.
* @hide
*/
// TODO: Remove with impl_with_mp1 once MP2 compat starts supporting lower devices.
@RestrictTo(LIBRARY_GROUP)
public void setController(MediaControllerCompat controllerCompat) {
mController = new ControllerCompat(controllerCompat);
if (mController.hasMetadata()) {
updateDuration();
updateTitle();
}
}
/**
* 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) {
mOnFullScreenListener = l;
}
/**
* 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) {
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 (mOverflowShowButton != null) {
mOverflowShowButton.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) {
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() || mNeedUxUpdate) {
// Dismiss SettingsWindow if it is showing.
mSettingsWindow.dismiss();
// Hide Overflow if it is showing.
if (mOverflowIsShowing) {
mOverflowHideAnimator.start();
}
// The following views may not have been initialized yet.
if (mTransportControls.getWidth() == 0 || mTimeView.getWidth() == 0) {
return;
}
// Update layout if necessary
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 iconSize = mResources.getDimensionPixelSize(R.dimen.mcv2_icon_size);
if (mMediaType == MEDIA_TYPE_DEFAULT) {
// Max number of icons inside BottomBarRightView for Music mode is 4.
int maxIconCount = 4;
updateLayout(maxIconCount, iconSize, 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, iconSize, currWidth, currHeight, screenWidth,
screenHeight);
}
mPrevWidth = currWidth;
// By default, show the full view when view size is changed.
if (mUxState != UX_STATE_FULL) {
removeCallbacks(mHideMainBars);
removeCallbacks(mHideProgressBar);
post(mShowMainBars);
}
}
// 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);
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 (mMuteButton != null) {
mMuteButton.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
///////////////////////////////////////////////////
private boolean isPlaying() {
return mController.isPlaying();
}
private int getCurrentPosition() {
return mController.getCurrentPosition();
}
private int getBufferPercentage() {
if (mDuration == 0) {
return 0;
}
long bufferedPos = mController.getBufferedPosition();
return (bufferedPos < 0) ? -1 : (int) (bufferedPos * 100 / mDuration);
}
private boolean canPause() {
return mController.canPause();
}
private boolean canSeekBackward() {
return mController.canSeekBackward();
}
private boolean canSeekForward() {
return mController.canSeekForward();
}
/**
* 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;
}
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);
}
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_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
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));
}
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
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);
}
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);
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;
GradientDrawable thumb = (GradientDrawable)
mResources.getDrawable(R.drawable.custom_progress_thumb);
int newSize = (int) (mResources.getDimensionPixelSize(
R.dimen.mcv2_custom_progress_thumb_size) * alpha);
thumb.setSize(newSize, newSize);
seekBar.setThumb(thumb);
mTransportControls.setAlpha(alpha);
if (mSizeType == SIZE_TYPE_MINIMAL) {
mFullScreenButton.setAlpha(alpha);
}
if (alpha == 0.0f) {
mTransportControls.setVisibility(View.GONE);
} else if (alpha == 1.0f) {
setEnabled(false);
}
}
});
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;
GradientDrawable thumb =
(GradientDrawable) mResources.getDrawable(R.drawable.custom_progress_thumb);
int newSize = (int) (mResources.getDimensionPixelSize(
R.dimen.mcv2_custom_progress_thumb_size) * alpha);
thumb.setSize(newSize, newSize);
seekBar.setThumb(thumb);
mTransportControls.setAlpha(alpha);
if (mSizeType == SIZE_TYPE_MINIMAL) {
mFullScreenButton.setAlpha(alpha);
}
if (alpha == 0.0f) {
mTransportControls.setVisibility(View.VISIBLE);
} else if (alpha == 1.0f) {
setEnabled(true);
}
}
});
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_PROGRESS_BAR_ONLY;
}
});
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_EMPTY;
}
});
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_EMPTY;
}
});
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_FULL;
}
});
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_FULL;
}
});
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) {
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) {
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
*/
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);
}
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() {
boolean isShowing = getVisibility() == View.VISIBLE;
if (!mDragging && isShowing && isPlaying()) {
int pos = setProgress();
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()) {
mController.pause();
mPlayPauseButton.setImageDrawable(
mResources.getDrawable(R.drawable.ic_play_circle_filled, null));
mPlayPauseButton.setContentDescription(
mResources.getString(R.string.mcv2_play_button_desc));
} else {
mController.play();
mPlayPauseButton.setImageDrawable(
mResources.getDrawable(R.drawable.ic_pause_circle_filled, null));
mPlayPauseButton.setContentDescription(
mResources.getString(R.string.mcv2_pause_button_desc));
}
}
private void toggleMediaControlViewVisibility() {
if ((mMediaType == MEDIA_TYPE_MUSIC && mSizeType == SIZE_TYPE_FULL)
|| mShowControllerIntervalMs == 0
|| mAccessibilityManager.isTouchExplorationEnabled()
|| mUxState == UX_STATE_ANIMATING) {
return;
}
removeCallbacks(mHideMainBars);
removeCallbacks(mHideProgressBar);
switch (mUxState) {
case UX_STATE_EMPTY:
post(mShowAllBars);
break;
case UX_STATE_PROGRESS_BAR_ONLY:
post(mShowMainBars);
break;
case UX_STATE_FULL:
post(mHideAllBars);
break;
}
}
private final Runnable mShowAllBars = new Runnable() {
@Override
public void run() {
mShowAllBarsAnimator.start();
if (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() {
mHideAllBarsAnimator.start();
}
};
private final Runnable mHideMainBars = new Runnable() {
@Override
public void run() {
if (!isPlaying()) {
return;
}
mHideMainBarsAnimator.start();
postDelayed(mHideProgressBar, mShowControllerIntervalMs);
}
};
private final Runnable mHideProgressBar = new Runnable() {
@Override
public void run() {
if (!isPlaying()) {
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 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);
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) {
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);
mController.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);
postDelayed(mHideMainBars, mShowControllerIntervalMs);
}
};
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();
int pos = getCurrentPosition() - REWIND_TIME_MS;
mController.seekTo(pos);
setProgress();
}
};
private final OnClickListener mFfwdListener = new OnClickListener() {
@Override
public void onClick(View v) {
resetHideCallbacks();
int pos = getCurrentPosition() + FORWARD_TIME_MS;
mController.seekTo(pos);
setProgress();
}
};
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, null));
} else {
mFullScreenButton.setImageDrawable(
mResources.getDrawable(R.drawable.ic_fullscreen, null));
}
mIsFullScreen = isEnteringFullScreen;
mOnFullScreenListener.onFullScreen(MediaControlView2.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 mMuteButtonListener = new OnClickListener() {
@Override
public void onClick(View v) {
resetHideCallbacks();
if (!mIsMute) {
mMuteButton.setImageDrawable(
mResources.getDrawable(R.drawable.ic_mute, null));
mMuteButton.setContentDescription(
mResources.getString(R.string.mcv2_muted_button_desc));
mIsMute = true;
mController.volumeMute();
} else {
mMuteButton.setImageDrawable(
mResources.getDrawable(R.drawable.ic_unmute, null));
mMuteButton.setContentDescription(
mResources.getString(R.string.mcv2_unmuted_button_desc));
mIsMute = false;
mController.volumeUnmute();
}
}
};
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;
} else if (position == SETTINGS_MODE_HELP) {
dismissSettingsWindow();
return;
}
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) {
mSelectedSpeedIndex = position;
float speed = mPlaybackSpeedList.get(position);
mController.setSpeed(speed);
mSettingsSubTextsList.set(SETTINGS_MODE_PLAYBACK_SPEED,
mSubSettingsAdapter.getMainText(position));
}
dismissSettingsWindow();
break;
case SETTINGS_MODE_HELP:
break;
case SETTINGS_MODE_SUBTITLE_TRACK:
if (position != mSelectedSubtitleTrackIndex) {
mSelectedSubtitleTrackIndex = position;
if (position > 0) {
mController.showSubtitle(position - 1);
mSubtitleButton.setImageDrawable(
mResources.getDrawable(R.drawable.ic_subtitle_on, null));
mSubtitleButton.setContentDescription(
mResources.getString(R.string.mcv2_cc_is_on));
} else {
mController.hideSubtitle();
mSubtitleButton.setImageDrawable(
mResources.getDrawable(R.drawable.ic_subtitle_off, null));
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);
}
}
};
private void updateDuration() {
if (mController.hasMetadata()) {
mDuration = mController.getDurationMs();
setProgress();
}
}
private void updateTitle() {
if (mController.hasMetadata()) {
mTitleView.setText(mController.getTitle());
}
}
// 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 (mController.hasMetadata()) {
String titleText = mController.getTitle();
String artistText = mController.getArtistText();
if (titleText == null) {
titleText = mResources.getString(R.string.mcv2_music_title_unknown_text);
}
if (artistText == null) {
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 iconSize, int currWidth,
int currHeight, int screenWidth, int screenHeight) {
int bottomBarRightWidthMax = iconSize * maxIconCount;
int fullWidth = mTransportControls.getWidth() + mTimeView.getWidth()
+ bottomBarRightWidthMax;
int embeddedWidth = mTimeView.getWidth() + bottomBarRightWidthMax;
int screenMaxLength = Math.max(screenWidth, screenHeight);
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 {
mUxState = UX_STATE_EMPTY;
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
mMinimalExtraView.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
GradientDrawable thumb = (GradientDrawable) mResources.getDrawable(
R.drawable.custom_progress_thumb);
if (mUxState == UX_STATE_FULL) {
int originalSize = mResources.getDimensionPixelSize(
R.dimen.mcv2_custom_progress_thumb_size);
thumb.setSize(originalSize, originalSize);
}
seeker.setThumb(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);
mTitleView.setPadding(
0,
mTitleView.getPaddingTop(),
mTitleView.getPaddingRight(),
mTitleView.getPaddingBottom());
// Relating to Full Screen Button
mMinimalExtraView.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));
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
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);
}
}
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
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;
}
private void dismissSettingsWindow() {
mNeedToHideBars = true;
mSettingsWindow.dismiss();
}
private 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) {
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());
}
}
private void resetHideCallbacks() {
removeCallbacks(mHideMainBars);
removeCallbacks(mHideProgressBar);
postDelayed(mHideMainBars, mShowControllerIntervalMs);
}
private void updateAllowedCommands(SessionCommandGroup2 commands) {
if (DEBUG) {
Log.d(TAG, "updateAllowedCommands(): commands: " + commands);
}
if (mAllowedCommands == commands) {
return;
}
mAllowedCommands = commands;
if (commands.hasCommand(SessionCommand2.COMMAND_CODE_PLAYBACK_PAUSE)) {
mPlayPauseButton.setVisibility(View.VISIBLE);
} else {
mPlayPauseButton.setVisibility(View.GONE);
}
if (commands.hasCommand(SessionCommand2.COMMAND_CODE_SESSION_REWIND)
&& mMediaType != MEDIA_TYPE_MUSIC) {
if (mRewButton != null) {
mRewButton.setVisibility(View.VISIBLE);
}
} else {
if (mRewButton != null) {
mRewButton.setVisibility(View.GONE);
}
}
if (commands.hasCommand(SessionCommand2.COMMAND_CODE_SESSION_FAST_FORWARD)
&& mMediaType != MEDIA_TYPE_MUSIC) {
if (mFfwdButton != null) {
mFfwdButton.setVisibility(View.VISIBLE);
}
} else {
if (mFfwdButton != null) {
mFfwdButton.setVisibility(View.GONE);
}
}
mSeekAvailable = commands.hasCommand(SessionCommand2.COMMAND_CODE_PLAYBACK_SEEK_TO);
if (commands.hasCommand(new SessionCommand2(COMMAND_SHOW_SUBTITLE, null))
&& commands.hasCommand(new SessionCommand2(COMMAND_HIDE_SUBTITLE, null))) {
mSubtitleButton.setVisibility(View.VISIBLE);
} else {
mSubtitleButton.setVisibility(View.GONE);
}
if (commands.hasCommand(new SessionCommand2(COMMAND_MUTE, null))
&& commands.hasCommand(new SessionCommand2(COMMAND_UNMUTE, null))) {
mMuteButton.setVisibility(View.VISIBLE);
} else {
mMuteButton.setVisibility(View.GONE);
}
}
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), null));
}
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;
}
}
interface ControllerInterface {
boolean hasMetadata();
boolean isPlaying();
int getCurrentPosition();
long getBufferedPosition();
boolean canPause();
boolean canSeekForward();
boolean canSeekBackward();
void pause();
void play();
void seekTo(int posMs);
void skipToNextItem();
void skipToPreviousItem();
void volumeMute();
void volumeUnmute();
void setSpeed(float speed);
void selectAudioTrack(int trackIndex);
void showSubtitle(int trackIndex);
void hideSubtitle();
int getDurationMs();
String getTitle();
String getArtistText();
void close();
}
class Controller2 implements ControllerInterface {
private MediaController2 mController2;
private int mPlaybackState;
private MediaMetadata2 mMediaMetadata2;
private Executor mCallbackExecutor;
Controller2(SessionToken2 token) {
mCallbackExecutor = MainHandlerExecutor
.getExecutor(MediaControlView2.this.getContext());
mController2 = new MediaController2(getContext(), token, mCallbackExecutor,
new MediaControllerCallback());
mPlaybackState = mController2.getPlayerState();
MediaItem2 currentItem = mController2.getCurrentMediaItem();
mMediaMetadata2 = currentItem != null ? currentItem.getMetadata() : null;
}
@Override
public boolean hasMetadata() {
return mMediaMetadata2 != null;
}
@Override
public boolean isPlaying() {
return mPlaybackState == BaseMediaPlayer.PLAYER_STATE_PLAYING;
}
@Override
public int getCurrentPosition() {
int currentPosition = (int) mController2.getCurrentPosition();
return (currentPosition < 0) ? 0 : currentPosition;
}
@Override
public long getBufferedPosition() {
return mController2.getBufferedPosition();
}
@Override
public boolean canPause() {
return true;
//return mAllowedCommands.hasCommand(SessionCommand2.COMMAND_CODE_PLAYBACK_PAUSE);
}
@Override
public boolean canSeekBackward() {
return true;
//return mAllowedCommands.hasCommand(SessionCommand2.COMMAND_CODE_SESSION_REWIND);
}
@Override
public boolean canSeekForward() {
return true;
//return mAllowedCommands.hasCommand(SessionCommand2.COMMAND_CODE_SESSION_FAST_FORWARD);
}
@Override
public void pause() {
mController2.pause();
}
@Override
public void play() {
mController2.play();
}
@Override
public void seekTo(int posMs) {
mController2.seekTo(posMs);
}
@Override
public void skipToNextItem() {
mController2.skipToNextItem();
}
@Override
public void skipToPreviousItem() {
mController2.skipToPreviousItem();
}
@Override
public void volumeMute() {
mController2.adjustVolume(AudioManager.ADJUST_MUTE,
AudioManager.FLAG_REMOVE_SOUND_AND_VIBRATE);
}
@Override
public void volumeUnmute() {
mController2.adjustVolume(AudioManager.ADJUST_UNMUTE,
AudioManager.FLAG_REMOVE_SOUND_AND_VIBRATE);
}
@Override
public void setSpeed(float speed) {
mController2.setPlaybackSpeed(speed);
}
@Override
public void selectAudioTrack(int trackIndex) {
Bundle extra = new Bundle();
extra.putInt(KEY_SELECTED_AUDIO_INDEX, trackIndex);
mController2.sendCustomCommand(
new SessionCommand2(COMMAND_SELECT_AUDIO_TRACK, null),
extra, null);
}
@Override
public void showSubtitle(int trackIndex) {
Bundle extra = new Bundle();
extra.putInt(KEY_SELECTED_SUBTITLE_INDEX, trackIndex);
mController2.sendCustomCommand(
new SessionCommand2(COMMAND_SHOW_SUBTITLE, null), extra, null);
}
@Override
public void hideSubtitle() {
mController2.sendCustomCommand(
new SessionCommand2(COMMAND_HIDE_SUBTITLE, null), null, null);
}
@Override
public int getDurationMs() {
if (mMediaMetadata2 != null) {
if (mMediaMetadata2.containsKey(MediaMetadata2.METADATA_KEY_DURATION)) {
return (int) mMediaMetadata2.getLong(MediaMetadata2.METADATA_KEY_DURATION);
}
}
return -1;
}
@Override
public String getTitle() {
if (mMediaMetadata2 != null) {
if (mMediaMetadata2.containsKey(MediaMetadata2.METADATA_KEY_TITLE)) {
return mMediaMetadata2.getString(MediaMetadata2.METADATA_KEY_TITLE);
}
}
return null;
}
@Override
public String getArtistText() {
if (mMediaMetadata2 != null) {
if (mMediaMetadata2.containsKey(MediaMetadata2.METADATA_KEY_ARTIST)) {
return mMediaMetadata2.getString(MediaMetadata2.METADATA_KEY_ARTIST);
}
}
return null;
}
@Override
public void close() {
mController2.close();
}
class MediaControllerCallback extends MediaController2.ControllerCallback {
@Override
public void onPlayerStateChanged(@NonNull MediaController2 controller,
@BaseMediaPlayer.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 BaseMediaPlayer.PLAYER_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);
resetHideCallbacks();
mIsStopped = false;
break;
case BaseMediaPlayer.PLAYER_STATE_PAUSED:
mPlayPauseButton.setImageDrawable(
mResources.getDrawable(R.drawable.ic_play_circle_filled, null));
mPlayPauseButton.setContentDescription(
mResources.getString(R.string.mcv2_play_button_desc));
removeCallbacks(mUpdateProgress);
break;
default:
break;
}
mPrevState = mPlaybackState;
}
}
@Override
public void onCurrentMediaItemChanged(@NonNull MediaController2 controller,
@Nullable MediaItem2 mediaItem) {
// This logic is for detecting end of playback, but it's not working. (b/79715323)
Log.d(TAG, "onCurrentMediaItemChanged()" + mediaItem);
if (mediaItem == null) {
mPlayPauseButton.setImageDrawable(
mResources.getDrawable(
R.drawable.ic_replay_circle_filled, null));
mPlayPauseButton.setContentDescription(
mResources.getString(R.string.mcv2_replay_button_desc));
mIsStopped = true;
}
}
@Override
public void onConnected(@NonNull MediaController2 controller,
@NonNull SessionCommandGroup2 allowedCommands) {
updateAllowedCommands(allowedCommands);
}
@Override
public void onAllowedCommandsChanged(@NonNull MediaController2 controller,
@NonNull SessionCommandGroup2 commands) {
updateAllowedCommands(commands);
}
@Override
public void onPlaylistChanged(@NonNull MediaController2 controller,
@NonNull List<MediaItem2> list,
@Nullable MediaMetadata2 metadata) {
if (DEBUG) {
Log.d(TAG, "onPlaylistChanged(): list: " + list);
}
// Note that currently MediaControlView2 assumes single media item to play.
MediaItem2 mediaItem = list.isEmpty() ? null : list.get(0);
mMediaMetadata2 = (mediaItem != null) ? mediaItem.getMetadata() : null;
updateDuration();
updateTitle();
updateAudioMetadata();
}
@Override
public void onCustomCommand(@NonNull MediaController2 controller,
@NonNull SessionCommand2 command, @Nullable Bundle args,
@Nullable ResultReceiver receiver) {
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.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 = (args != null)
? args.getInt(KEY_SUBTITLE_TRACK_COUNT) : 0;
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 isAd = (args != null)
&& args.getBoolean(KEY_STATE_IS_ADVERTISEMENT);
if (isAd != mIsAdvertisement) {
mIsAdvertisement = isAd;
updateLayout();
}
break;
}
}
}
}
class ControllerCompat implements ControllerInterface {
private MediaControllerCompat mControllerCompat;
private MediaControllerCompat.TransportControls mControls;
private MediaMetadataCompat mMediaMetadata;
private PlaybackStateCompat mPlaybackState;
ControllerCompat(MediaControllerCompat controllerCompat) {
mControllerCompat = controllerCompat;
if (controllerCompat != null) {
mControls = mControllerCompat.getTransportControls();
mPlaybackState = mControllerCompat.getPlaybackState();
mMediaMetadata = mControllerCompat.getMetadata();
updateDuration();
updateTitle();
mControllerCompat.registerCallback(new MediaControllerCompatCallback());
}
}
@Override
public boolean hasMetadata() {
return mMediaMetadata != null;
}
@Override
public boolean isPlaying() {
return mPlaybackState != null
&& mPlaybackState.getState() == PlaybackStateCompat.STATE_PLAYING;
}
@Override
public int getCurrentPosition() {
mPlaybackState = mControllerCompat.getPlaybackState();
return (mPlaybackState != null) ? (int) mPlaybackState.getPosition() : 0;
}
@Override
public long getBufferedPosition() {
mPlaybackState = mControllerCompat.getPlaybackState();
if (mPlaybackState != null) {
return mPlaybackState.getBufferedPosition();
}
return 0;
}
@Override
public boolean canPause() {
if (mPlaybackState != null) {
return (mPlaybackState.getActions() & PlaybackStateCompat.ACTION_PAUSE) != 0;
}
return true;
}
@Override
public boolean canSeekBackward() {
if (mPlaybackState != null) {
return (mPlaybackState.getActions() & PlaybackStateCompat.ACTION_REWIND) != 0;
}
return true;
}
@Override
public boolean canSeekForward() {
if (mPlaybackState != null) {
return (mPlaybackState.getActions() & PlaybackStateCompat.ACTION_FAST_FORWARD) != 0;
}
return true;
}
@Override
public void pause() {
mControls.pause();
}
@Override
public void play() {
mControls.play();
}
@Override
public void seekTo(int posMs) {
mControls.seekTo(posMs);
}
@Override
public void skipToNextItem() {
mControls.skipToNext();
}
@Override
public void skipToPreviousItem() {
mControls.skipToPrevious();
}
@Override
public void volumeMute() {
mControllerCompat.sendCommand(COMMAND_MUTE, null, null);
}
@Override
public void volumeUnmute() {
mControllerCompat.sendCommand(COMMAND_UNMUTE, null, null);
}
@Override
public void setSpeed(float speed) {
Bundle extra = new Bundle();
extra.putFloat(KEY_PLAYBACK_SPEED, speed);
mControllerCompat.sendCommand(COMMAND_SET_PLAYBACK_SPEED, extra, null);
}
@Override
public void selectAudioTrack(int trackIndex) {
Bundle extra = new Bundle();
extra.putInt(KEY_SELECTED_AUDIO_INDEX, trackIndex);
mControllerCompat.sendCommand(COMMAND_SELECT_AUDIO_TRACK, extra, null);
}
@Override
public void showSubtitle(int trackIndex) {
Bundle extra = new Bundle();
extra.putInt(KEY_SELECTED_SUBTITLE_INDEX, trackIndex);
mControllerCompat.sendCommand(COMMAND_SHOW_SUBTITLE, extra, null);
}
@Override
public void hideSubtitle() {
mControllerCompat.sendCommand(COMMAND_HIDE_SUBTITLE, null, null);
}
@Override
public int getDurationMs() {
return mMediaMetadata == null
? 0 : (int) mMediaMetadata.getLong(MediaMetadataCompat.METADATA_KEY_DURATION);
}
@Override
public String getTitle() {
return mMediaMetadata == null
? null : mMediaMetadata.getString(MediaMetadataCompat.METADATA_KEY_TITLE);
}
@Override
public String getArtistText() {
return mMediaMetadata == null
? null : mMediaMetadata.getString(MediaMetadataCompat.METADATA_KEY_ARTIST);
}
@Override
public void close() {
// Nothing.
}
private class MediaControllerCompatCallback 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);
resetHideCallbacks();
mIsStopped = false;
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 */);
// Refer Constructor with argument (int defStyleRes) of View.java
button.setImageResource(action.getIcon());
final String actionString = action.getAction().toString();
button.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
mControls.sendCustomAction(actionString, action.getExtras());
setVisibility(View.VISIBLE);
}
});
mCustomButtons.addView(button);
}
}
}
@Override
public void onMetadataChanged(MediaMetadataCompat metadata) {
mMediaMetadata = 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) {
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;
}
}
}
}
}