| /* |
| * Copyright 2018 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package androidx.media2.widget; |
| |
| |
| import android.animation.Animator; |
| import android.animation.AnimatorListenerAdapter; |
| import android.animation.AnimatorSet; |
| import android.animation.ValueAnimator; |
| import android.content.Context; |
| import android.content.DialogInterface; |
| import android.content.res.Resources; |
| import android.graphics.drawable.ColorDrawable; |
| import android.graphics.drawable.Drawable; |
| import android.net.Uri; |
| import android.util.AttributeSet; |
| import android.util.Log; |
| import android.util.SparseArray; |
| import android.view.LayoutInflater; |
| import android.view.MotionEvent; |
| import android.view.View; |
| import android.view.ViewGroup; |
| 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.ListView; |
| import android.widget.PopupWindow; |
| import android.widget.SeekBar; |
| import android.widget.SeekBar.OnSeekBarChangeListener; |
| import android.widget.TextView; |
| |
| import androidx.annotation.IdRes; |
| import androidx.annotation.NonNull; |
| import androidx.annotation.Nullable; |
| import androidx.appcompat.app.AlertDialog; |
| import androidx.core.content.ContextCompat; |
| import androidx.core.view.ViewCompat; |
| |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.Formatter; |
| import java.util.List; |
| import java.util.Locale; |
| |
| /** |
| * A View that contains the controls for {@link androidx.media2.session.MediaController} or {@link |
| * androidx.media2.common.SessionPlayer}. 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, select audio track, and adjust playback speed. |
| * |
| * <p>For simple use cases not requiring communication with {@link |
| * androidx.media2.session.MediaSession}, apps need to create a {@link |
| * androidx.media2.common.SessionPlayer} (e.g. {@link androidx.media2.player.MediaPlayer}) and set |
| * it to this view by calling {@link #setPlayer}. For more advanced use cases that require {@link |
| * androidx.media2.session.MediaSession} (e.g. handling media key events, integrating with other |
| * androidx.media2.session.MediaSession apps as Assistant), apps need to create a {@link |
| * androidx.media2.session.MediaController} attached to the {@link |
| * androidx.media2.session.MediaSession} and set it to this view by calling {@link |
| * #setMediaController}. |
| * |
| * <p>The easiest way to use a MediaControlView is by creating a {@link VideoView}, which internally |
| * creates a MediaControlView instance and handles all the commands from buttons inside |
| * MediaControlView. It is also possible to create a MediaControlView programmatically and add it to |
| * a custom video view. For more information, refer to {@link VideoView}. |
| * |
| * <p>By default, each button in the MediaControlView is visible only when its corresponding {@link |
| * androidx.media2.session.SessionCommand} is included in the active {@link |
| * androidx.media2.session.SessionCommandGroup}. For more details, refer to {@link |
| * androidx.media2.session.MediaSession#setAllowedCommands}. |
| * |
| * <p> |
| * |
| * <h3>UI transitions</h3> |
| * |
| * The UI of an app can be in one of three modes: |
| * |
| * <ul> |
| * <li>In <b>full</b> mode all the views, such as progress bar, title, transport controls, and |
| * other icons are visible. |
| * <li>In <b>progress-bar only</b> mode the progress bar is the only visible element. The title, |
| * transport controls, and other icons are hidden. |
| * <li>In <b>None</b> mode all the views are hidden. |
| * </ul> |
| * |
| * When the UI mode changes, MediaControlView animates the transition. The animation does not start |
| * immediately, there is a default delay interval of 2000ms before the animation begins. You can |
| * change this interval by calling {@link VideoView#setMediaControlView(MediaControlView, long)}. |
| * |
| * <p>User actions can change the scheduled transition during the delay interval according to the |
| * following logic: |
| * |
| * <ol> |
| * <li>In Full mode |
| * <ul> |
| * <li>If a touch/trackball event is received during the interval, the UI changes to None |
| * mode. |
| * <li>If no touch/trackball event is received during the interval, the UI changes to |
| * progress-bar only mode. |
| * </ul> |
| * <li>In Progress-bar only mode |
| * <ul> |
| * <li>If a touch/trackball event is received, the UI changes to Full mode. |
| * <li>If no touch/trackball event is received, the UI changes to None mode. |
| * </ul> |
| * <li>In None mode, if a touch/trackball event is received, the UI changes to Full mode. |
| * </ol> |
| * |
| * All touch/trackballs events are ignored while the system is animating the change between modes. |
| * |
| * <p> |
| * |
| * <h3>Customization</h3> |
| * |
| * The following customizations are supported: |
| * |
| * <ul> |
| * <li>Set focus to the play/pause button by calling {@link #requestPlayButtonFocus()}. |
| * <li>Set full screen behavior by calling {@link #setOnFullScreenListener(OnFullScreenListener)}. |
| * Calling this method will also show the full screen button. |
| * </ul> |
| * |
| * <p> |
| * |
| * <h3>Displaying metadata</h3> |
| * |
| * MediaControlView supports displaying metadata by calling {@link |
| * androidx.media2.common.MediaItem#setMetadata(androidx.media2.common.MediaMetadata)}. Metadata |
| * display is different for two different media types: video (with or without sound) and audio(sound |
| * only, no video) |
| * |
| * <p>The following table shows the metadata displayed on VideoView and the default values assigned |
| * if the keys are not set: |
| * |
| * <table> |
| * <tr><th>Key</th><th>Default</th></tr> |
| * <tr><td>{@link androidx.media2.common.MediaMetadata#METADATA_KEY_TITLE}</td> |
| * <td>{@link androidx.media2.widget.R.string#mcv2_music_title_unknown_text}</td></tr> |
| * <tr><td>{@link androidx.media2.common.MediaMetadata#METADATA_KEY_ARTIST}</td> |
| * <td>{@link androidx.media2.widget.R.string#mcv2_music_artist_unknown_text}</td></tr> |
| * <tr><td>{@link androidx.media2.common.MediaMetadata#METADATA_KEY_ALBUM_ART}</td> |
| * <td>{@link androidx.media2.widget.R.drawable#media2_widget_ic_default_album_image}</td></tr> |
| * </table> |
| * |
| * <p>For video media, {@link androidx.media2.common.MediaMetadata#METADATA_KEY_TITLE} metadata is |
| * supported. If the value is not set, the following default value will be shown: {@link |
| * androidx.media2.widget.R.string#mcv2_non_music_title_unknown_text} |
| * |
| * @deprecated androidx.media2 is deprecated. Please migrate to <a |
| * href="https://developer.android.com/guide/topics/media/media3">androidx.media3</a>. |
| */ |
| @Deprecated |
| public class MediaControlView extends MediaViewGroup { |
| private static final String TAG = "MediaControlView"; |
| static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); |
| |
| private static final int SETTINGS_MODE_AUDIO_TRACK = 0; |
| private static final int SETTINGS_MODE_PLAYBACK_SPEED = 1; |
| private static final int SETTINGS_MODE_SUBTITLE_TRACK = 2; |
| private static final int SETTINGS_MODE_MAIN = 3; |
| private static final int PLAYBACK_SPEED_1x_INDEX = 3; |
| |
| private static final int SIZE_TYPE_UNDEFINED = -1; |
| 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 PLAY_BUTTON_PAUSE = 0; |
| private static final int PLAY_BUTTON_PLAY = 1; |
| private static final int PLAY_BUTTON_REPLAY = 2; |
| |
| // Int for defining the UX state where all the views (TitleBar, ProgressBar, BottomBar) are |
| // all visible. |
| private static final int UX_STATE_ALL_VISIBLE = 0; |
| // Int for defining the UX state where only the ProgressBar view is visible. |
| private static final int UX_STATE_ONLY_PROGRESS_VISIBLE = 1; |
| // Int for defining the UX state where none of the views are visible. |
| private static final int UX_STATE_NONE_VISIBLE = 2; |
| // Int for defining the UX state where the views are being animated (shown or hidden). |
| private static final int UX_STATE_ANIMATING = 3; |
| |
| private static final long DISABLE_DELAYED_ANIMATION = -1; |
| private static final long DEFAULT_DELAYED_ANIMATION_INTERVAL_MS = 2000; |
| private static final long DEFAULT_PROGRESS_UPDATE_TIME_MS = 1000; |
| private static final long REWIND_TIME_MS = 10000; |
| private static final long FORWARD_TIME_MS = 30000; |
| private static final long AD_SKIP_WAIT_TIME_MS = 5000; |
| private static final long HIDE_TIME_MS = 250; |
| private static final long SHOW_TIME_MS = 250; |
| private static final int MAX_PROGRESS = 1000; |
| private static final int MAX_SCALE_LEVEL = 10000; |
| private static final int RESOURCE_NON_EXISTENT = -1; |
| private static final int SEEK_POSITION_NOT_SET = -1; |
| private static final String RESOURCE_EMPTY = ""; |
| |
| private boolean mIsAttachedToVideoView = false; |
| |
| Resources mResources; |
| PlayerWrapper mPlayer; |
| OnFullScreenListener mOnFullScreenListener; |
| private AccessibilityManager mAccessibilityManager; |
| private int mEmbeddedSettingsItemWidth; |
| private int mFullSettingsItemWidth; |
| private int mSettingsItemHeight; |
| private int mSettingsWindowMargin; |
| int mSettingsMode; |
| int mSelectedSubtitleTrackIndex; |
| int mSelectedAudioTrackIndex; |
| int mSelectedSpeedIndex; |
| int mSizeType = SIZE_TYPE_UNDEFINED; |
| int mUxState; |
| long mDuration; |
| long mDelayedAnimationIntervalMs; |
| long mCurrentSeekPosition; |
| long mNextSeekPosition; |
| boolean mDragging; |
| boolean mIsFullScreen; |
| boolean mIsShowingReplayButton; |
| boolean mOverflowIsShowing; |
| boolean mSeekAvailable; |
| boolean mIsAdvertisement; |
| boolean mNeedToHideBars; |
| boolean mNeedToShowBars; |
| boolean mWasPlaying; |
| |
| private SparseArray<View> mTransportControlsMap = new SparseArray<>(); |
| |
| // Relating to Title Bar View |
| private View mTitleBar; |
| private TextView mTitleView; |
| private View mAdExternalLink; |
| |
| // Relating to Center View |
| ViewGroup mCenterView; |
| private View mCenterViewBackground; |
| private View mEmbeddedTransportControls; |
| private View mMinimalTransportControls; |
| |
| // Relating to Minimal Fullscreen View |
| ViewGroup mMinimalFullScreenView; |
| ImageButton mMinimalFullScreenButton; |
| |
| // Relating to Progress Bar View |
| private ViewGroup mProgressBar; |
| SeekBar mProgress; |
| |
| // Relating to Bottom Bar View |
| private View mBottomBarBackground; |
| |
| // Relating to Bottom Bar Left View |
| private ViewGroup mBottomBarLeft; |
| private View mFullTransportControls; |
| private ViewGroup mTimeView; |
| private TextView mEndTime; |
| TextView mCurrentTime; |
| private TextView mAdSkipView; |
| private StringBuilder mFormatBuilder; |
| private Formatter mFormatter; |
| |
| // Relating to Bottom Bar Right View |
| ViewGroup mBasicControls; |
| ViewGroup mExtraControls; |
| ImageButton mSubtitleButton; |
| ImageButton mFullScreenButton; |
| private TextView mAdRemainingView; |
| |
| // Relating to Settings List View |
| private ListView mSettingsListView; |
| private PopupWindow mSettingsWindow; |
| SettingsAdapter mSettingsAdapter; |
| SubSettingsAdapter mSubSettingsAdapter; |
| private List<String> mSettingsMainTextsList; |
| List<String> mSettingsSubTextsList; |
| private List<Integer> mSettingsIconIdsList; |
| List<String> mSubtitleDescriptionsList; |
| int mVideoTrackCount; |
| List<androidx.media2.common.SessionPlayer.TrackInfo> mAudioTracks = new ArrayList<>(); |
| List<androidx.media2.common.SessionPlayer.TrackInfo> mSubtitleTracks = new ArrayList<>(); |
| List<String> mAudioTrackDescriptionList; |
| List<String> mPlaybackSpeedTextList; |
| List<Integer> mPlaybackSpeedMultBy100List; |
| int mCustomPlaybackSpeedIndex; |
| |
| AnimatorSet mHideMainBarsAnimator; |
| AnimatorSet mHideProgressBarAnimator; |
| AnimatorSet mHideAllBarsAnimator; |
| AnimatorSet mShowMainBarsAnimator; |
| AnimatorSet mShowAllBarsAnimator; |
| ValueAnimator mOverflowShowAnimator; |
| ValueAnimator mOverflowHideAnimator; |
| |
| public MediaControlView(@NonNull Context context) { |
| this(context, null); |
| } |
| |
| public MediaControlView(@NonNull Context context, @Nullable AttributeSet attrs) { |
| this(context, attrs, 0); |
| } |
| |
| public MediaControlView(@NonNull Context context, @Nullable AttributeSet attrs, |
| int defStyleAttr) { |
| super(context, attrs, defStyleAttr); |
| |
| mResources = context.getResources(); |
| inflate(context, R.layout.media2_widget_media_controller, this); |
| initControllerView(); |
| mDelayedAnimationIntervalMs = DEFAULT_DELAYED_ANIMATION_INTERVAL_MS; |
| mAccessibilityManager = (AccessibilityManager) context.getSystemService( |
| Context.ACCESSIBILITY_SERVICE); |
| } |
| |
| /** |
| * Sets {@link androidx.media2.session.MediaController} to control playback with this view. |
| * Setting a androidx.media2.session.MediaController will unset any |
| * androidx.media2.session.MediaController or androidx.media2.common.SessionPlayer that was |
| * previously set. |
| * |
| * <p>It will throw {@link IllegalStateException} if this MediaControlView belongs to a {@link |
| * VideoView} by {@link androidx.media2.widget.R.attr#enableControlView} or by {@link |
| * VideoView#setMediaControlView}. Use {@link VideoView#setMediaController} instead. |
| * |
| * <p>Note that MediaControlView allows controlling playback through its UI components, but |
| * calling the corresponding methods (e.g. {@link |
| * androidx.media2.session.MediaController#play()}, {@link |
| * androidx.media2.session.MediaController#pause()}) will work as well. |
| * |
| * @param controller the controller |
| * @see #setPlayer |
| */ |
| public void setMediaController(@NonNull androidx.media2.session.MediaController controller) { |
| if (controller == null) { |
| throw new NullPointerException("controller must not be null"); |
| } |
| if (mIsAttachedToVideoView) { |
| throw new IllegalStateException("It's attached to VideoView. Use VideoView's method."); |
| } |
| setMediaControllerInternal(controller); |
| } |
| |
| void setMediaControllerInternal(@NonNull androidx.media2.session.MediaController controller) { |
| if (mPlayer != null) { |
| mPlayer.detachCallback(); |
| } |
| mPlayer = new PlayerWrapper(controller, ContextCompat.getMainExecutor(getContext()), |
| new PlayerCallback()); |
| if (ViewCompat.isAttachedToWindow(this)) { |
| mPlayer.attachCallback(); |
| } |
| } |
| |
| /** |
| * Sets {@link androidx.media2.common.SessionPlayer} to control playback with this view. Setting |
| * a androidx.media2.common.SessionPlayer will unset any androidx.media2.session.MediaController |
| * or androidx.media2.common.SessionPlayer that was previously set. |
| * |
| * <p>It will throw {@link IllegalStateException} if this MediaControlView belongs to a {@link |
| * VideoView} by {@link androidx.media2.widget.R.attr#enableControlView} or by {@link |
| * VideoView#setMediaControlView}. Use {@link VideoView#setPlayer} instead. |
| * |
| * <p>Note that MediaControlView allows controlling playback through its UI components, but |
| * calling the corresponding methods (e.g. {@link androidx.media2.common.SessionPlayer#play()}, |
| * {@link androidx.media2.common.SessionPlayer#pause()}) will work as well. |
| * |
| * @param player the player |
| * @see #setMediaController |
| */ |
| public void setPlayer(@NonNull androidx.media2.common.SessionPlayer player) { |
| if (player == null) { |
| throw new NullPointerException("player must not be null"); |
| } |
| if (mIsAttachedToVideoView) { |
| throw new IllegalStateException("It's attached to VideoView. Use VideoView's method."); |
| } |
| setPlayerInternal(player); |
| } |
| |
| void setPlayerInternal(@NonNull androidx.media2.common.SessionPlayer player) { |
| if (mPlayer != null) { |
| mPlayer.detachCallback(); |
| } |
| mPlayer = new PlayerWrapper(player, ContextCompat.getMainExecutor(getContext()), |
| new PlayerCallback()); |
| if (ViewCompat.isAttachedToWindow(this)) { |
| mPlayer.attachCallback(); |
| } |
| } |
| |
| /** |
| * Sets a listener to be called when the fullscreen mode should be changed. |
| * A non-null listener needs to be set in order to display the fullscreen button. |
| * |
| * @param listener The listener to be called. A value of <code>null</code> removes any |
| * existing listener and hides the fullscreen button. |
| */ |
| public void setOnFullScreenListener(@Nullable OnFullScreenListener listener) { |
| if (listener == null) { |
| mOnFullScreenListener = null; |
| mFullScreenButton.setVisibility(View.GONE); |
| } else { |
| mOnFullScreenListener = listener; |
| mFullScreenButton.setVisibility(View.VISIBLE); |
| } |
| } |
| |
| /** |
| * Requests focus for the play/pause button. |
| */ |
| public void requestPlayButtonFocus() { |
| ImageButton button = findControlButton(mSizeType, R.id.pause); |
| if (button != null) { |
| button.requestFocus(); |
| } |
| } |
| |
| /** |
| * Interface definition of a callback to be invoked to inform the fullscreen mode is changed. |
| * Application should handle the fullscreen mode accordingly. |
| * |
| * @deprecated androidx.media2 is deprecated. Please migrate to <a |
| * href="https://developer.android.com/guide/topics/media/media3">androidx.media3</a>. |
| */ |
| @Deprecated |
| public interface OnFullScreenListener { |
| /** |
| * Called to indicate a fullscreen mode change. |
| */ |
| void onFullScreen(@NonNull View view, boolean fullScreen); |
| } |
| |
| @Override |
| public CharSequence getAccessibilityClassName() { |
| // Class name may be obfuscated by Proguard. Hardcode the string for accessibility usage. |
| return "androidx.media2.widget.MediaControlView"; |
| } |
| |
| @Override |
| public boolean onTouchEvent(MotionEvent ev) { |
| if (mPlayer == null) { |
| return super.onTouchEvent(ev); |
| } |
| if (ev.getAction() == MotionEvent.ACTION_UP) { |
| if (!isCurrentItemMusic() || mSizeType != SIZE_TYPE_FULL) { |
| if (mUxState == UX_STATE_ALL_VISIBLE) { |
| hideMediaControlView(); |
| } else { |
| showMediaControlView(); |
| } |
| } |
| } |
| return true; |
| } |
| |
| @Override |
| public boolean onTrackballEvent(MotionEvent ev) { |
| if (mPlayer == null) { |
| return super.onTrackballEvent(ev); |
| } |
| if (ev.getAction() == MotionEvent.ACTION_UP) { |
| if (!isCurrentItemMusic() || mSizeType != SIZE_TYPE_FULL) { |
| if (mUxState == UX_STATE_ALL_VISIBLE) { |
| hideMediaControlView(); |
| } else { |
| showMediaControlView(); |
| } |
| } |
| } |
| return true; |
| } |
| |
| @Override |
| protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { |
| final int width = resolveSize(getSuggestedMinimumWidth(), widthMeasureSpec); |
| final int height = resolveSize(getSuggestedMinimumHeight(), heightMeasureSpec); |
| |
| int childWidth = width - getPaddingLeft() - getPaddingRight(); |
| int childHeight = height - getPaddingTop() - getPaddingBottom(); |
| int childState = 0; |
| |
| if (childWidth < 0) { |
| childWidth = 0; |
| childState |= View.MEASURED_STATE_TOO_SMALL; |
| } |
| if (childHeight < 0) { |
| childHeight = 0; |
| childState |= (View.MEASURED_STATE_TOO_SMALL >> View.MEASURED_HEIGHT_STATE_SHIFT); |
| } |
| |
| final int count = getChildCount(); |
| for (int i = 0; i < count; i++) { |
| final View child = getChildAt(i); |
| if (child.getVisibility() == View.GONE) { |
| continue; |
| } |
| LayoutParams lp = child.getLayoutParams(); |
| |
| int childWidthSpec; |
| if (lp.width == LayoutParams.MATCH_PARENT) { |
| childWidthSpec = MeasureSpec.makeMeasureSpec(childWidth, MeasureSpec.EXACTLY); |
| } else if (lp.width == LayoutParams.WRAP_CONTENT) { |
| childWidthSpec = MeasureSpec.makeMeasureSpec(childWidth, MeasureSpec.UNSPECIFIED); |
| } else { |
| childWidthSpec = MeasureSpec.makeMeasureSpec(lp.width, MeasureSpec.EXACTLY); |
| } |
| |
| int childHeightSpec; |
| if (lp.height == LayoutParams.MATCH_PARENT) { |
| childHeightSpec = MeasureSpec.makeMeasureSpec(childHeight, MeasureSpec.EXACTLY); |
| } else if (lp.height == LayoutParams.WRAP_CONTENT) { |
| childHeightSpec = MeasureSpec.makeMeasureSpec(childHeight, MeasureSpec.UNSPECIFIED); |
| } else { |
| childHeightSpec = MeasureSpec.makeMeasureSpec(lp.height, MeasureSpec.EXACTLY); |
| } |
| |
| child.measure(childWidthSpec, childHeightSpec); |
| childState |= child.getMeasuredState(); |
| } |
| |
| setMeasuredDimension( |
| resolveSizeAndState(width, widthMeasureSpec, childState), |
| resolveSizeAndState(height, heightMeasureSpec, |
| childState << View.MEASURED_HEIGHT_STATE_SHIFT)); |
| } |
| |
| @Override |
| protected void onLayout(boolean changed, int left, int top, int right, int bottom) { |
| final int width = right - left - getPaddingLeft() - getPaddingRight(); |
| final int height = bottom - top - getPaddingTop() - getPaddingBottom(); |
| |
| final int fullWidth = mBottomBarLeft.getMeasuredWidth() |
| + mTimeView.getMeasuredWidth() |
| + mBasicControls.getMeasuredWidth(); |
| final int fullHeight = mTitleBar.getMeasuredHeight() |
| + mProgressBar.getMeasuredHeight() |
| + mBottomBarBackground.getMeasuredHeight(); |
| |
| final int embeddedWidth = mTimeView.getMeasuredWidth() |
| + mBasicControls.getMeasuredWidth(); |
| final int embeddedHeight = mTitleBar.getMeasuredHeight() |
| + mEmbeddedTransportControls.getMeasuredHeight() |
| + mProgressBar.getMeasuredHeight() |
| + mBottomBarBackground.getMeasuredHeight(); |
| |
| int sizeType; |
| if (mIsAdvertisement || (fullWidth <= width && fullHeight <= height)) { |
| sizeType = SIZE_TYPE_FULL; |
| } else if (embeddedWidth <= width && embeddedHeight <= height) { |
| sizeType = SIZE_TYPE_EMBEDDED; |
| } else { |
| sizeType = SIZE_TYPE_MINIMAL; |
| } |
| |
| if (mSizeType != sizeType) { |
| mSizeType = sizeType; |
| updateLayoutForSizeChange(sizeType); |
| } |
| |
| mTitleBar.setVisibility( |
| sizeType != SIZE_TYPE_MINIMAL ? View.VISIBLE : View.INVISIBLE); |
| mCenterViewBackground.setVisibility( |
| sizeType != SIZE_TYPE_FULL ? View.VISIBLE : View.INVISIBLE); |
| mEmbeddedTransportControls.setVisibility( |
| sizeType == SIZE_TYPE_EMBEDDED ? View.VISIBLE : View.INVISIBLE); |
| mMinimalTransportControls.setVisibility( |
| sizeType == SIZE_TYPE_MINIMAL ? View.VISIBLE : View.INVISIBLE); |
| mBottomBarBackground.setVisibility( |
| sizeType != SIZE_TYPE_MINIMAL ? View.VISIBLE : View.INVISIBLE); |
| mBottomBarLeft.setVisibility( |
| sizeType == SIZE_TYPE_FULL ? View.VISIBLE : View.INVISIBLE); |
| mTimeView.setVisibility( |
| sizeType != SIZE_TYPE_MINIMAL ? View.VISIBLE : View.INVISIBLE); |
| mBasicControls.setVisibility( |
| sizeType != SIZE_TYPE_MINIMAL ? View.VISIBLE : View.INVISIBLE); |
| mMinimalFullScreenButton.setVisibility( |
| sizeType == SIZE_TYPE_MINIMAL ? View.VISIBLE : View.INVISIBLE); |
| |
| final int childLeft = getPaddingLeft(); |
| final int childRight = childLeft + width; |
| final int childTop = getPaddingTop(); |
| final int childBottom = childTop + height; |
| |
| layoutChild(mTitleBar, |
| childLeft, |
| childTop); |
| layoutChild(mCenterView, |
| childLeft, |
| childTop); |
| layoutChild(mBottomBarBackground, |
| childLeft, |
| childBottom - mBottomBarBackground.getMeasuredHeight()); |
| layoutChild(mBottomBarLeft, |
| childLeft, |
| childBottom - mBottomBarLeft.getMeasuredHeight()); |
| layoutChild(mTimeView, |
| sizeType == SIZE_TYPE_FULL |
| ? childRight - mBasicControls.getMeasuredWidth() |
| - mTimeView.getMeasuredWidth() |
| : childLeft, |
| childBottom - mTimeView.getMeasuredHeight()); |
| layoutChild(mBasicControls, |
| childRight - mBasicControls.getMeasuredWidth(), |
| childBottom - mBasicControls.getMeasuredHeight()); |
| layoutChild(mExtraControls, |
| childRight, |
| childBottom - mExtraControls.getMeasuredHeight()); |
| layoutChild(mProgressBar, |
| childLeft, |
| sizeType == SIZE_TYPE_MINIMAL |
| ? childBottom - mProgressBar.getMeasuredHeight() |
| : childBottom - mProgressBar.getMeasuredHeight() |
| - mResources.getDimensionPixelSize( |
| R.dimen.media2_widget_custom_progress_margin_bottom)); |
| layoutChild(mMinimalFullScreenView, |
| childLeft, |
| childBottom - mMinimalFullScreenView.getMeasuredHeight()); |
| } |
| |
| private void layoutChild(View child, int left, int top) { |
| child.layout(left, top, left + child.getMeasuredWidth(), top + child.getMeasuredHeight()); |
| } |
| |
| @Override |
| void onVisibilityAggregatedCompat(boolean isVisible) { |
| super.onVisibilityAggregatedCompat(isVisible); |
| if (mPlayer == null) return; |
| if (isVisible) { |
| removeCallbacks(mUpdateProgress); |
| post(mUpdateProgress); |
| } else { |
| removeCallbacks(mUpdateProgress); |
| } |
| } |
| |
| void setDelayedAnimationInterval(long interval) { |
| mDelayedAnimationIntervalMs = interval; |
| } |
| |
| @Override |
| protected void onAttachedToWindow() { |
| super.onAttachedToWindow(); |
| |
| if (mPlayer != null) { |
| mPlayer.attachCallback(); |
| } |
| } |
| |
| @Override |
| protected void onDetachedFromWindow() { |
| super.onDetachedFromWindow(); |
| |
| if (mPlayer != null) { |
| mPlayer.detachCallback(); |
| } |
| } |
| |
| void setAttachedToVideoView(boolean attached) { |
| mIsAttachedToVideoView = attached; |
| } |
| |
| /////////////////////////////////////////////////// |
| // Protected or private methods |
| /////////////////////////////////////////////////// |
| |
| static View inflateLayout(Context context, int resId) { |
| LayoutInflater inflater = (LayoutInflater) context |
| .getSystemService(Context.LAYOUT_INFLATER_SERVICE); |
| return inflater.inflate(resId, null); |
| } |
| |
| private void initControllerView() { |
| // Relating to Title Bar View |
| mTitleBar = findViewById(R.id.title_bar); |
| mTitleView = findViewById(R.id.title_text); |
| mAdExternalLink = findViewById(R.id.ad_external_link); |
| |
| // Relating to Center View |
| mCenterView = findViewById(R.id.center_view); |
| mCenterViewBackground = findViewById(R.id.center_view_background); |
| mEmbeddedTransportControls = initTransportControls(R.id.embedded_transport_controls); |
| mMinimalTransportControls = initTransportControls(R.id.minimal_transport_controls); |
| |
| // Relating to Minimal Size FullScreen View |
| mMinimalFullScreenView = findViewById(R.id.minimal_fullscreen_view); |
| mMinimalFullScreenButton = findViewById(R.id.minimal_fullscreen); |
| mMinimalFullScreenButton.setOnClickListener(mFullScreenListener); |
| |
| // Relating to Progress Bar View |
| mProgressBar = findViewById(R.id.progress_bar); |
| mProgress = findViewById(R.id.progress); |
| mProgress.setOnSeekBarChangeListener(mSeekListener); |
| mProgress.setMax(MAX_PROGRESS); |
| mCurrentSeekPosition = SEEK_POSITION_NOT_SET; |
| mNextSeekPosition = SEEK_POSITION_NOT_SET; |
| |
| // Relating to Bottom Bar View |
| mBottomBarBackground = findViewById(R.id.bottom_bar_background); |
| |
| // Relating to Bottom Bar Left View |
| mBottomBarLeft = findViewById(R.id.bottom_bar_left); |
| mFullTransportControls = initTransportControls(R.id.full_transport_controls); |
| mTimeView = findViewById(R.id.time); |
| mEndTime = findViewById(R.id.time_end); |
| mCurrentTime = findViewById(R.id.time_current); |
| mAdSkipView = findViewById(R.id.ad_skip_time); |
| mFormatBuilder = new StringBuilder(); |
| mFormatter = new Formatter(mFormatBuilder, Locale.getDefault()); |
| |
| // Relating to Bottom Bar Right View |
| mBasicControls = findViewById(R.id.basic_controls); |
| mExtraControls = findViewById(R.id.extra_controls); |
| mSubtitleButton = findViewById(R.id.subtitle); |
| mSubtitleButton.setOnClickListener(mSubtitleListener); |
| mFullScreenButton = findViewById(R.id.fullscreen); |
| mFullScreenButton.setOnClickListener(mFullScreenListener); |
| ImageButton overflowShowButton = findViewById(R.id.overflow_show); |
| overflowShowButton.setOnClickListener(mOverflowShowListener); |
| ImageButton overflowHideButton = findViewById(R.id.overflow_hide); |
| overflowHideButton.setOnClickListener(mOverflowHideListener); |
| ImageButton settingsButton = findViewById(R.id.settings); |
| settingsButton.setOnClickListener(mSettingsButtonListener); |
| mAdRemainingView = findViewById(R.id.ad_remaining); |
| |
| // Relating to Settings List View |
| initializeSettingsLists(); |
| mSettingsListView = (ListView) inflateLayout(getContext(), |
| R.layout.media2_widget_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); |
| |
| // TransportControlsMap |
| mTransportControlsMap.append(SIZE_TYPE_EMBEDDED, mEmbeddedTransportControls); |
| mTransportControlsMap.append(SIZE_TYPE_FULL, mFullTransportControls); |
| mTransportControlsMap.append(SIZE_TYPE_MINIMAL, mMinimalTransportControls); |
| |
| mEmbeddedSettingsItemWidth = mResources.getDimensionPixelSize( |
| R.dimen.media2_widget_embedded_settings_width); |
| mFullSettingsItemWidth = mResources.getDimensionPixelSize( |
| R.dimen.media2_widget_full_settings_width); |
| mSettingsItemHeight = mResources.getDimensionPixelSize( |
| R.dimen.media2_widget_settings_height); |
| mSettingsWindowMargin = mResources.getDimensionPixelSize( |
| R.dimen.media2_widget_settings_offset); |
| mSettingsWindow = new PopupWindow(mSettingsListView, mEmbeddedSettingsItemWidth, |
| LayoutParams.WRAP_CONTENT, true); |
| mSettingsWindow.setBackgroundDrawable(new ColorDrawable()); |
| mSettingsWindow.setOnDismissListener(mSettingsDismissListener); |
| |
| float titleBarHeight = mResources.getDimension(R.dimen.media2_widget_title_bar_height); |
| float progressBarHeight = mResources.getDimension( |
| R.dimen.media2_widget_custom_progress_thumb_size); |
| float bottomBarHeight = mResources.getDimension(R.dimen.media2_widget_bottom_bar_height); |
| |
| View[] bottomBarGroup = { mBottomBarBackground, mBottomBarLeft, mTimeView, mBasicControls, |
| mExtraControls, mProgressBar }; |
| |
| 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(); |
| int scaleLevel = mSizeType == SIZE_TYPE_MINIMAL ? 0 : MAX_SCALE_LEVEL; |
| mProgress.getThumb().setLevel((int) (scaleLevel * alpha)); |
| |
| mCenterView.setAlpha(alpha); |
| mMinimalFullScreenView.setAlpha(alpha); |
| } |
| }); |
| fadeOutAnimator.addListener(new AnimatorListenerAdapter() { |
| @Override |
| public void onAnimationEnd(Animator animation) { |
| mCenterView.setVisibility(View.INVISIBLE); |
| mMinimalFullScreenView.setVisibility(View.INVISIBLE); |
| } |
| }); |
| |
| 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(); |
| int scaleLevel = mSizeType == SIZE_TYPE_MINIMAL ? 0 : MAX_SCALE_LEVEL; |
| mProgress.getThumb().setLevel((int) (scaleLevel * alpha)); |
| |
| mCenterView.setAlpha(alpha); |
| mMinimalFullScreenView.setAlpha(alpha); |
| } |
| }); |
| fadeInAnimator.addListener(new AnimatorListenerAdapter() { |
| @Override |
| public void onAnimationStart(Animator animation) { |
| mCenterView.setVisibility(View.VISIBLE); |
| mMinimalFullScreenView.setVisibility(View.VISIBLE); |
| } |
| }); |
| |
| mHideMainBarsAnimator = new AnimatorSet(); |
| mHideMainBarsAnimator.play(fadeOutAnimator) |
| .with(AnimatorUtil.ofTranslationY(0, -titleBarHeight, mTitleBar)) |
| .with(AnimatorUtil.ofTranslationYTogether(0, bottomBarHeight, bottomBarGroup)); |
| mHideMainBarsAnimator.setDuration(HIDE_TIME_MS); |
| mHideMainBarsAnimator.addListener(new AnimatorListenerAdapter() { |
| @Override |
| public void onAnimationStart(Animator animation) { |
| mUxState = UX_STATE_ANIMATING; |
| } |
| |
| @Override |
| public void onAnimationEnd(Animator animation) { |
| mUxState = UX_STATE_ONLY_PROGRESS_VISIBLE; |
| if (mNeedToShowBars) { |
| post(mShowAllBars); |
| mNeedToShowBars = false; |
| } |
| } |
| }); |
| |
| mHideProgressBarAnimator = AnimatorUtil.ofTranslationYTogether( |
| bottomBarHeight, bottomBarHeight + progressBarHeight, bottomBarGroup); |
| mHideProgressBarAnimator.setDuration(HIDE_TIME_MS); |
| mHideProgressBarAnimator.addListener(new AnimatorListenerAdapter() { |
| @Override |
| public void onAnimationStart(Animator animation) { |
| mUxState = UX_STATE_ANIMATING; |
| } |
| |
| @Override |
| public void onAnimationEnd(Animator animation) { |
| mUxState = UX_STATE_NONE_VISIBLE; |
| if (mNeedToShowBars) { |
| post(mShowAllBars); |
| mNeedToShowBars = false; |
| } |
| } |
| }); |
| |
| mHideAllBarsAnimator = new AnimatorSet(); |
| mHideAllBarsAnimator.play(fadeOutAnimator) |
| .with(AnimatorUtil.ofTranslationY(0, -titleBarHeight, mTitleBar)) |
| .with(AnimatorUtil.ofTranslationYTogether( |
| 0, bottomBarHeight + progressBarHeight, bottomBarGroup)); |
| mHideAllBarsAnimator.setDuration(HIDE_TIME_MS); |
| mHideAllBarsAnimator.addListener(new AnimatorListenerAdapter() { |
| @Override |
| public void onAnimationStart(Animator animation) { |
| mUxState = UX_STATE_ANIMATING; |
| } |
| |
| @Override |
| public void onAnimationEnd(Animator animation) { |
| mUxState = UX_STATE_NONE_VISIBLE; |
| if (mNeedToShowBars) { |
| post(mShowAllBars); |
| mNeedToShowBars = false; |
| } |
| } |
| }); |
| |
| mShowMainBarsAnimator = new AnimatorSet(); |
| mShowMainBarsAnimator.play(fadeInAnimator) |
| .with(AnimatorUtil.ofTranslationY(-titleBarHeight, 0, mTitleBar)) |
| .with(AnimatorUtil.ofTranslationYTogether(bottomBarHeight, 0, bottomBarGroup)); |
| mShowMainBarsAnimator.setDuration(SHOW_TIME_MS); |
| mShowMainBarsAnimator.addListener(new AnimatorListenerAdapter() { |
| @Override |
| public void onAnimationStart(Animator animation) { |
| mUxState = UX_STATE_ANIMATING; |
| } |
| |
| @Override |
| public void onAnimationEnd(Animator animation) { |
| mUxState = UX_STATE_ALL_VISIBLE; |
| } |
| }); |
| |
| mShowAllBarsAnimator = new AnimatorSet(); |
| mShowAllBarsAnimator.play(fadeInAnimator) |
| .with(AnimatorUtil.ofTranslationY(-titleBarHeight, 0, mTitleBar)) |
| .with(AnimatorUtil.ofTranslationYTogether( |
| bottomBarHeight + progressBarHeight, 0, bottomBarGroup)); |
| mShowAllBarsAnimator.setDuration(SHOW_TIME_MS); |
| mShowAllBarsAnimator.addListener(new AnimatorListenerAdapter() { |
| @Override |
| public void onAnimationStart(Animator animation) { |
| mUxState = UX_STATE_ANIMATING; |
| } |
| |
| @Override |
| public void onAnimationEnd(Animator animation) { |
| mUxState = UX_STATE_ALL_VISIBLE; |
| } |
| }); |
| |
| mOverflowShowAnimator = ValueAnimator.ofFloat(0.0f, 1.0f); |
| mOverflowShowAnimator.setDuration(SHOW_TIME_MS); |
| mOverflowShowAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { |
| @Override |
| public void onAnimationUpdate(ValueAnimator animation) { |
| animateOverflow((float) animation.getAnimatedValue()); |
| } |
| }); |
| mOverflowShowAnimator.addListener(new AnimatorListenerAdapter() { |
| @Override |
| public void onAnimationStart(Animator animation) { |
| mExtraControls.setVisibility(View.VISIBLE); |
| } |
| |
| @Override |
| public void onAnimationEnd(Animator animation) { |
| mBasicControls.setVisibility(View.INVISIBLE); |
| |
| findFullSizedControlButton(R.id.ffwd).setVisibility( |
| mPlayer != null && mPlayer.canSeekForward() ? View.INVISIBLE : 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((float) animation.getAnimatedValue()); |
| } |
| }); |
| mOverflowHideAnimator.addListener(new AnimatorListenerAdapter() { |
| @Override |
| public void onAnimationStart(Animator animation) { |
| mBasicControls.setVisibility(View.VISIBLE); |
| |
| findFullSizedControlButton(R.id.ffwd).setVisibility( |
| mPlayer != null && mPlayer.canSeekForward() ? View.VISIBLE : View.GONE); |
| } |
| |
| @Override |
| public void onAnimationEnd(Animator animation) { |
| mExtraControls.setVisibility(View.GONE); |
| } |
| }); |
| } |
| |
| final Runnable mUpdateProgress = new Runnable() { |
| @Override |
| public void run() { |
| boolean isShowing = getVisibility() == View.VISIBLE; |
| if (!mDragging && isShowing && mPlayer != null && mPlayer.isPlaying()) { |
| long pos = setProgress(); |
| postDelayedRunnable(mUpdateProgress, |
| DEFAULT_PROGRESS_UPDATE_TIME_MS - (pos % DEFAULT_PROGRESS_UPDATE_TIME_MS)); |
| } |
| } |
| }; |
| |
| String stringForTime(long timeMs) { |
| long totalSeconds = timeMs / 1000; |
| |
| long seconds = totalSeconds % 60; |
| long minutes = (totalSeconds / 60) % 60; |
| long hours = totalSeconds / 3600; |
| |
| mFormatBuilder.setLength(0); |
| if (hours > 0) { |
| return mFormatter.format("%d:%02d:%02d", hours, minutes, seconds).toString(); |
| } else { |
| return mFormatter.format("%02d:%02d", minutes, seconds).toString(); |
| } |
| } |
| |
| long setProgress() { |
| ensurePlayerIsNotNull(); |
| |
| int positionOnProgressBar = 0; |
| long currentPosition = mPlayer.getCurrentPosition(); |
| if (currentPosition > mDuration) { |
| currentPosition = mDuration; |
| } |
| if (mDuration > 0) { |
| positionOnProgressBar = (int) (MAX_PROGRESS * currentPosition / mDuration); |
| } |
| if (mProgress != null && currentPosition != mDuration) { |
| mProgress.setProgress(positionOnProgressBar); |
| // If the media is a local file, there is no need to set a buffer, so set secondary |
| // progress to maximum. |
| if (mPlayer.getBufferPercentage() < 0) { |
| mProgress.setSecondaryProgress(MAX_PROGRESS); |
| } else { |
| mProgress.setSecondaryProgress((int) mPlayer.getBufferPercentage() * 10); |
| } |
| } |
| |
| if (mEndTime != null) { |
| mEndTime.setText(stringForTime(mDuration)); |
| } |
| if (mCurrentTime != null) { |
| mCurrentTime.setText(stringForTime(currentPosition)); |
| } |
| |
| if (mIsAdvertisement) { |
| // Update the remaining number of seconds until the first 5 seconds of the |
| // advertisement. |
| if (mAdSkipView != null) { |
| if (currentPosition <= AD_SKIP_WAIT_TIME_MS) { |
| if (mAdSkipView.getVisibility() == View.GONE) { |
| mAdSkipView.setVisibility(View.VISIBLE); |
| } |
| String skipTimeText = mResources.getString( |
| R.string.MediaControlView_ad_skip_wait_time, |
| ((AD_SKIP_WAIT_TIME_MS - currentPosition) / 1000 + 1)); |
| mAdSkipView.setText(skipTimeText); |
| } else { |
| if (mAdSkipView.getVisibility() == View.VISIBLE) { |
| mAdSkipView.setVisibility(View.GONE); |
| findFullSizedControlButton(R.id.next).setEnabled(true); |
| findFullSizedControlButton(R.id.next).clearColorFilter(); |
| } |
| } |
| } |
| // Update the remaining number of seconds of the advertisement. |
| if (mAdRemainingView != null) { |
| long remainingTime = |
| (mDuration - currentPosition < 0) ? 0 : (mDuration - currentPosition); |
| String remainingTimeText = mResources.getString( |
| R.string.MediaControlView_ad_remaining_time, |
| stringForTime(remainingTime)); |
| mAdRemainingView.setText(remainingTimeText); |
| } |
| } |
| return currentPosition; |
| } |
| |
| void togglePausePlayState() { |
| ensurePlayerIsNotNull(); |
| |
| if (mPlayer.isPlaying()) { |
| mPlayer.pause(); |
| updatePlayButton(PLAY_BUTTON_PLAY); |
| } else { |
| if (mIsShowingReplayButton) { |
| mPlayer.seekTo(0); |
| } |
| mPlayer.play(); |
| updatePlayButton(PLAY_BUTTON_PAUSE); |
| } |
| } |
| |
| private void showMediaControlView() { |
| if (mUxState == UX_STATE_ANIMATING) { |
| return; |
| } |
| removeCallbacks(mHideMainBars); |
| removeCallbacks(mHideProgressBar); |
| post(mShowAllBars); |
| } |
| |
| private void hideMediaControlView() { |
| if (shouldNotHideBars() || mUxState == UX_STATE_ANIMATING) { |
| return; |
| } |
| removeCallbacks(mHideMainBars); |
| removeCallbacks(mHideProgressBar); |
| post(mHideAllBars); |
| } |
| |
| final Runnable mShowAllBars = new Runnable() { |
| @Override |
| public void run() { |
| switch (mUxState) { |
| case UX_STATE_NONE_VISIBLE: |
| mShowAllBarsAnimator.start(); |
| break; |
| case UX_STATE_ONLY_PROGRESS_VISIBLE: |
| mShowMainBarsAnimator.start(); |
| break; |
| case UX_STATE_ANIMATING: |
| mNeedToShowBars = true; |
| } |
| |
| if (mPlayer.isPlaying()) { |
| postDelayedRunnable(mHideMainBars, mDelayedAnimationIntervalMs); |
| } |
| } |
| }; |
| |
| private final Runnable mHideAllBars = new Runnable() { |
| @Override |
| public void run() { |
| if (shouldNotHideBars()) { |
| return; |
| } |
| mHideAllBarsAnimator.start(); |
| } |
| }; |
| |
| Runnable mHideMainBars = new Runnable() { |
| @Override |
| public void run() { |
| if (!mPlayer.isPlaying() || shouldNotHideBars()) { |
| return; |
| } |
| mHideMainBarsAnimator.start(); |
| postDelayedRunnable(mHideProgressBar, mDelayedAnimationIntervalMs); |
| } |
| }; |
| |
| final Runnable mHideProgressBar = new Runnable() { |
| @Override |
| public void run() { |
| if (!mPlayer.isPlaying() || shouldNotHideBars()) { |
| return; |
| } |
| mHideProgressBarAnimator.start(); |
| } |
| }; |
| |
| // There are two scenarios that can trigger the seekbar listener to trigger: |
| // |
| // The first is the user using the touchpad to adjust the position of the |
| // seekbar's thumb. In this case onStartTrackingTouch is called followed by |
| // a number of onProgressChanged notifications, concluded by onStopTrackingTouch. |
| // We're setting the field "mDragging" to true for the duration of the dragging |
| // session to avoid jumps in the position in case of ongoing playback. |
| // |
| // The second scenario involves the user operating the scroll ball, in this |
| // case there WON'T BE onStartTrackingTouch/onStopTrackingTouch notifications, |
| // we will simply apply the updated position without suspending regular updates. |
| private final OnSeekBarChangeListener mSeekListener = new OnSeekBarChangeListener() { |
| @Override |
| public void onStartTrackingTouch(SeekBar bar) { |
| if (mPlayer == null || !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 (mIsShowingReplayButton) { |
| updateReplayButton(false); |
| } |
| |
| if (isCurrentMediaItemFromNetwork() && mPlayer.isPlaying()) { |
| mWasPlaying = true; |
| mPlayer.pause(); |
| } |
| } |
| |
| @Override |
| public void onProgressChanged(SeekBar bar, int progress, boolean fromUser) { |
| if (mPlayer == null || !mSeekAvailable) { |
| return; |
| } |
| if (!fromUser) { |
| // We're not interested in programmatically generated changes to |
| // the progress bar's position. |
| return; |
| } |
| // Check if progress bar is being dragged since this method may be called after |
| // onStopTrackingTouch() is called. |
| if (mDragging && mDuration > 0) { |
| long newPosition = ((mDuration * progress) / MAX_PROGRESS); |
| // Do not seek if the current media item has a http scheme URL to improve seek |
| // performance. |
| boolean shouldSeekNow = !isCurrentMediaItemFromNetwork(); |
| seekTo(newPosition, shouldSeekNow); |
| } |
| } |
| |
| @Override |
| public void onStopTrackingTouch(SeekBar bar) { |
| if (mPlayer == null || !mSeekAvailable) { |
| return; |
| } |
| mDragging = false; |
| |
| long latestSeekPosition = getLatestSeekPosition(); |
| // Reset existing seek positions since we only need to seek to the latest position. |
| if (isCurrentMediaItemFromNetwork()) { |
| mCurrentSeekPosition = SEEK_POSITION_NOT_SET; |
| mNextSeekPosition = SEEK_POSITION_NOT_SET; |
| } |
| seekTo(latestSeekPosition, true); |
| |
| if (mWasPlaying) { |
| mWasPlaying = false; |
| mPlayer.play(); |
| } |
| } |
| }; |
| |
| private final OnClickListener mPlayPauseListener = new OnClickListener() { |
| @Override |
| public void onClick(View v) { |
| if (mPlayer == null) return; |
| resetHideCallbacks(); |
| togglePausePlayState(); |
| } |
| }; |
| |
| private final OnClickListener mRewListener = new OnClickListener() { |
| @Override |
| public void onClick(View v) { |
| if (mPlayer == null) return; |
| resetHideCallbacks(); |
| removeCallbacks(mUpdateProgress); |
| |
| // If replay button is shown, seek to 10 seconds before the end of the media. |
| boolean stoppedWithDuration = mIsShowingReplayButton && mDuration != 0; |
| long currentPosition = stoppedWithDuration ? mDuration : getLatestSeekPosition(); |
| long seekPosition = Math.max(currentPosition - REWIND_TIME_MS, 0); |
| seekTo(seekPosition, /* shouldSeekNow= */ true); |
| if (stoppedWithDuration) { |
| updateReplayButton(/* toBeShown */ false); |
| } |
| } |
| }; |
| |
| private final OnClickListener mFfwdListener = new OnClickListener() { |
| @Override |
| public void onClick(View v) { |
| if (mPlayer == null) return; |
| resetHideCallbacks(); |
| removeCallbacks(mUpdateProgress); |
| |
| long latestSeekPosition = getLatestSeekPosition(); |
| seekTo(Math.min(latestSeekPosition + FORWARD_TIME_MS, mDuration), true); |
| |
| // Note: In some edge cases, mDuration might be less than actual duration of |
| // the stream. If controller is in playing state, it should not show replay |
| // button even when the seekPosition >= mDuration. |
| if (latestSeekPosition + FORWARD_TIME_MS >= mDuration && !mPlayer.isPlaying()) { |
| updateReplayButton(/* toBeShown */ true); |
| } |
| } |
| }; |
| |
| private final OnClickListener mNextListener = new OnClickListener() { |
| @Override |
| public void onClick(View v) { |
| if (mPlayer == null) return; |
| resetHideCallbacks(); |
| mPlayer.skipToNextItem(); |
| } |
| }; |
| |
| private final OnClickListener mPrevListener = new OnClickListener() { |
| @Override |
| public void onClick(View v) { |
| if (mPlayer == null) return; |
| resetHideCallbacks(); |
| mPlayer.skipToPreviousItem(); |
| } |
| }; |
| |
| private final OnClickListener mSubtitleListener = new OnClickListener() { |
| @Override |
| public void onClick(View v) { |
| if (mPlayer == null) return; |
| removeCallbacks(mHideMainBars); |
| removeCallbacks(mHideProgressBar); |
| |
| mSettingsMode = SETTINGS_MODE_SUBTITLE_TRACK; |
| mSubSettingsAdapter.setTexts(mSubtitleDescriptionsList); |
| mSubSettingsAdapter.setCheckPosition(mSelectedSubtitleTrackIndex + 1); |
| displaySettingsWindow(mSubSettingsAdapter); |
| } |
| }; |
| |
| private final OnClickListener mFullScreenListener = new OnClickListener() { |
| @Override |
| public void onClick(View v) { |
| if (mOnFullScreenListener == null) { |
| return; |
| } |
| |
| final boolean isEnteringFullScreen = !mIsFullScreen; |
| if (isEnteringFullScreen) { |
| mFullScreenButton.setImageDrawable(ContextCompat.getDrawable(getContext(), |
| R.drawable.media2_widget_ic_fullscreen_exit)); |
| mMinimalFullScreenButton.setImageDrawable(ContextCompat.getDrawable(getContext(), |
| R.drawable.media2_widget_ic_fullscreen_exit)); |
| } else { |
| mFullScreenButton.setImageDrawable(ContextCompat.getDrawable(getContext(), |
| R.drawable.media2_widget_ic_fullscreen)); |
| mMinimalFullScreenButton.setImageDrawable(ContextCompat.getDrawable(getContext(), |
| R.drawable.media2_widget_ic_fullscreen)); |
| } |
| mIsFullScreen = isEnteringFullScreen; |
| mOnFullScreenListener.onFullScreen(MediaControlView.this, |
| mIsFullScreen); |
| } |
| }; |
| |
| private final OnClickListener mOverflowShowListener = new OnClickListener() { |
| @Override |
| public void onClick(View v) { |
| if (mPlayer == null) return; |
| resetHideCallbacks(); |
| |
| mOverflowIsShowing = true; |
| mOverflowShowAnimator.start(); |
| } |
| }; |
| |
| private final OnClickListener mOverflowHideListener = new OnClickListener() { |
| @Override |
| public void onClick(View v) { |
| if (mPlayer == null) return; |
| resetHideCallbacks(); |
| |
| mOverflowIsShowing = false; |
| mOverflowHideAnimator.start(); |
| } |
| }; |
| |
| private final OnClickListener mSettingsButtonListener = new OnClickListener() { |
| @Override |
| public void onClick(View v) { |
| if (mPlayer == null) return; |
| 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(mAudioTrackDescriptionList); |
| mSubSettingsAdapter.setCheckPosition(mSelectedAudioTrackIndex); |
| mSettingsMode = SETTINGS_MODE_AUDIO_TRACK; |
| } else if (position == SETTINGS_MODE_PLAYBACK_SPEED) { |
| mSubSettingsAdapter.setTexts(mPlaybackSpeedTextList); |
| mSubSettingsAdapter.setCheckPosition(mSelectedSpeedIndex); |
| mSettingsMode = SETTINGS_MODE_PLAYBACK_SPEED; |
| } |
| displaySettingsWindow(mSubSettingsAdapter); |
| break; |
| case SETTINGS_MODE_AUDIO_TRACK: |
| if (position != mSelectedAudioTrackIndex) { |
| if (mAudioTracks.size() > 0) { |
| mPlayer.selectTrack(mAudioTracks.get(position)); |
| } |
| } |
| dismissSettingsWindow(); |
| break; |
| case SETTINGS_MODE_PLAYBACK_SPEED: |
| if (position != mSelectedSpeedIndex) { |
| float speed = mPlaybackSpeedMultBy100List.get(position) / 100.0f; |
| mPlayer.setPlaybackSpeed(speed); |
| } |
| dismissSettingsWindow(); |
| break; |
| case SETTINGS_MODE_SUBTITLE_TRACK: |
| if (position != mSelectedSubtitleTrackIndex + 1) { |
| if (position > 0) { |
| mPlayer.selectTrack(mSubtitleTracks.get(position - 1)); |
| } else { |
| mPlayer.deselectTrack(mSubtitleTracks.get(mSelectedSubtitleTrackIndex)); |
| } |
| } |
| dismissSettingsWindow(); |
| break; |
| } |
| } |
| }; |
| |
| private PopupWindow.OnDismissListener mSettingsDismissListener = |
| new PopupWindow.OnDismissListener() { |
| @Override |
| public void onDismiss() { |
| if (mNeedToHideBars) { |
| postDelayedRunnable(mHideMainBars, mDelayedAnimationIntervalMs); |
| } |
| } |
| }; |
| |
| void updateTimeViews(androidx.media2.common.MediaItem item) { |
| if (item == null) { |
| mProgress.setProgress(0); |
| mCurrentTime.setText(mResources.getString(R.string.MediaControlView_time_placeholder)); |
| mEndTime.setText(mResources.getString(R.string.MediaControlView_time_placeholder)); |
| return; |
| } |
| |
| ensurePlayerIsNotNull(); |
| |
| long duration = mPlayer.getDurationMs(); |
| if (duration > 0) { |
| mDuration = duration; |
| setProgress(); |
| } |
| } |
| |
| void updateTitleView(androidx.media2.common.MediaItem item) { |
| if (item == null) { |
| mTitleView.setText(null); |
| return; |
| } |
| |
| if (!isCurrentItemMusic()) { |
| CharSequence title = mPlayer.getTitle(); |
| if (title == null) { |
| title = mResources.getString(R.string.mcv2_non_music_title_unknown_text); |
| } |
| mTitleView.setText(title.toString()); |
| } else { |
| CharSequence title = mPlayer.getTitle(); |
| if (title == null) { |
| title = mResources.getString(R.string.mcv2_music_title_unknown_text); |
| } |
| CharSequence artist = mPlayer.getArtistText(); |
| if (artist == null) { |
| artist = mResources.getString(R.string.mcv2_music_artist_unknown_text); |
| } |
| // Update title for Embedded size type |
| mTitleView.setText(title.toString() + " - " + artist.toString()); |
| } |
| } |
| |
| void updateLayoutForAd() { |
| ensurePlayerIsNotNull(); |
| |
| if (mIsAdvertisement) { |
| findFullSizedControlButton(R.id.rew).setVisibility(View.GONE); |
| findFullSizedControlButton(R.id.ffwd).setVisibility(View.GONE); |
| findFullSizedControlButton(R.id.prev).setVisibility(View.GONE); |
| |
| findFullSizedControlButton(R.id.next).setVisibility(View.VISIBLE); |
| findFullSizedControlButton(R.id.next).setEnabled(false); |
| findFullSizedControlButton(R.id.next).setColorFilter(R.color.media2_widget_gray); |
| |
| mTimeView.setVisibility(View.GONE); |
| mAdSkipView.setVisibility(View.VISIBLE); |
| mAdRemainingView.setVisibility(View.VISIBLE); |
| mAdExternalLink.setVisibility(View.VISIBLE); |
| |
| mProgress.setEnabled(false); |
| } else { |
| findFullSizedControlButton(R.id.rew).setVisibility( |
| mPlayer.canSeekBackward() ? View.VISIBLE : View.GONE); |
| findFullSizedControlButton(R.id.ffwd).setVisibility( |
| mPlayer.canSeekForward() ? View.VISIBLE : View.GONE); |
| findFullSizedControlButton(R.id.prev).setVisibility( |
| mPlayer.canSkipToPrevious() ? View.VISIBLE : View.GONE); |
| |
| findFullSizedControlButton(R.id.next).setVisibility( |
| mPlayer.canSkipToNext() ? View.VISIBLE : View.GONE); |
| findFullSizedControlButton(R.id.next).setEnabled(true); |
| findFullSizedControlButton(R.id.next).clearColorFilter(); |
| |
| mTimeView.setVisibility(View.VISIBLE); |
| mAdSkipView.setVisibility(View.GONE); |
| mAdRemainingView.setVisibility(View.GONE); |
| mAdExternalLink.setVisibility(View.GONE); |
| |
| mProgress.setEnabled(mSeekAvailable); |
| } |
| } |
| |
| private void updateLayoutForSizeChange(int sizeType) { |
| switch (sizeType) { |
| case SIZE_TYPE_FULL: |
| case SIZE_TYPE_EMBEDDED: |
| // Relating to Progress Bar |
| mProgress.getThumb().setLevel(MAX_SCALE_LEVEL); |
| break; |
| case SIZE_TYPE_MINIMAL: |
| // Relating to Progress Bar |
| mProgress.getThumb().setLevel(0); |
| break; |
| } |
| |
| // Update play/pause and ffwd buttons based on whether currently the replay button is shown |
| // or not. |
| updateReplayButton(mIsShowingReplayButton); |
| } |
| |
| private View initTransportControls(int id) { |
| View v = findViewById(id); |
| ImageButton playPauseButton = v.findViewById(R.id.pause); |
| if (playPauseButton != null) { |
| playPauseButton.setOnClickListener(mPlayPauseListener); |
| } |
| ImageButton ffwdButton = v.findViewById(R.id.ffwd); |
| if (ffwdButton != null) { |
| ffwdButton.setOnClickListener(mFfwdListener); |
| } |
| ImageButton rewButton = v.findViewById(R.id.rew); |
| if (rewButton != null) { |
| rewButton.setOnClickListener(mRewListener); |
| } |
| ImageButton nextButton = v.findViewById(R.id.next); |
| if (nextButton != null) { |
| nextButton.setOnClickListener(mNextListener); |
| } |
| ImageButton prevButton = v.findViewById(R.id.prev); |
| if (prevButton != null) { |
| prevButton.setOnClickListener(mPrevListener); |
| } |
| return v; |
| } |
| |
| private void initializeSettingsLists() { |
| mSettingsMainTextsList = new ArrayList<String>(); |
| mSettingsMainTextsList.add( |
| mResources.getString(R.string.MediaControlView_audio_track_text)); |
| mSettingsMainTextsList.add( |
| mResources.getString(R.string.MediaControlView_playback_speed_text)); |
| |
| mSettingsSubTextsList = new ArrayList<String>(); |
| mSettingsSubTextsList.add( |
| mResources.getString(R.string.MediaControlView_audio_track_none_text)); |
| String normalSpeed = mResources.getString(R.string.MediaControlView_playback_speed_normal); |
| mSettingsSubTextsList.add(normalSpeed); |
| mSettingsSubTextsList.add(RESOURCE_EMPTY); |
| |
| mSettingsIconIdsList = new ArrayList<Integer>(); |
| mSettingsIconIdsList.add(R.drawable.media2_widget_ic_audiotrack); |
| mSettingsIconIdsList.add(R.drawable.media2_widget_ic_speed); |
| |
| mAudioTrackDescriptionList = new ArrayList<String>(); |
| mAudioTrackDescriptionList.add( |
| mResources.getString(R.string.MediaControlView_audio_track_none_text)); |
| |
| mPlaybackSpeedTextList = new ArrayList<String>(Arrays.asList( |
| mResources.getStringArray(R.array.MediaControlView_playback_speeds))); |
| // Select the normal speed (1x) as the default value. |
| mPlaybackSpeedTextList.add(PLAYBACK_SPEED_1x_INDEX, normalSpeed); |
| mSelectedSpeedIndex = PLAYBACK_SPEED_1x_INDEX; |
| |
| mPlaybackSpeedMultBy100List = new ArrayList<Integer>(); |
| int[] speeds = mResources.getIntArray(R.array.media2_widget_speed_multiplied_by_100); |
| for (int i = 0; i < speeds.length; i++) { |
| mPlaybackSpeedMultBy100List.add(speeds[i]); |
| } |
| mCustomPlaybackSpeedIndex = -1; |
| } |
| |
| @Nullable |
| ImageButton findControlButton(int sizeType, @IdRes int id) { |
| View transportControl = mTransportControlsMap.get(sizeType); |
| if (transportControl == null) { |
| return null; |
| } |
| return transportControl.findViewById(id); |
| } |
| |
| @NonNull |
| ImageButton findFullSizedControlButton(@IdRes int id) { |
| ImageButton button = findControlButton(SIZE_TYPE_FULL, id); |
| if (button == null) { |
| throw new IllegalArgumentException("Couldn't find a view that has the given id"); |
| } |
| return button; |
| } |
| |
| /** |
| * @return true iff the current media item is from network. |
| */ |
| boolean isCurrentMediaItemFromNetwork() { |
| ensurePlayerIsNotNull(); |
| |
| androidx.media2.common.MediaItem currentMediaItem = mPlayer.getCurrentMediaItem(); |
| |
| if (!(currentMediaItem instanceof androidx.media2.common.UriMediaItem)) { |
| return false; |
| } |
| |
| Uri uri = ((androidx.media2.common.UriMediaItem) currentMediaItem).getUri(); |
| return UriUtil.isFromNetwork(uri); |
| } |
| |
| void displaySettingsWindow(BaseAdapter adapter) { |
| // Set Adapter |
| mSettingsListView.setAdapter(adapter); |
| |
| // Set width of window |
| int itemWidth = (mSizeType == SIZE_TYPE_EMBEDDED) |
| ? mEmbeddedSettingsItemWidth : mFullSettingsItemWidth; |
| mSettingsWindow.setWidth(itemWidth); |
| |
| // Calculate height of window |
| int maxHeight = getHeight() - mSettingsWindowMargin * 2; |
| int totalHeight = adapter.getCount() * mSettingsItemHeight; |
| int height = (totalHeight < maxHeight) ? totalHeight : maxHeight; |
| mSettingsWindow.setHeight(height); |
| |
| // Show window |
| mNeedToHideBars = false; |
| mSettingsWindow.dismiss(); |
| // Workaround for b/123271636. |
| if (height > 0) { |
| int xoff = getWidth() - mSettingsWindow.getWidth() - mSettingsWindowMargin; |
| int yoff = -mSettingsWindow.getHeight() - mSettingsWindowMargin; |
| mSettingsWindow.showAsDropDown(this, xoff, yoff); |
| mNeedToHideBars = true; |
| } |
| } |
| |
| void dismissSettingsWindow() { |
| mNeedToHideBars = true; |
| mSettingsWindow.dismiss(); |
| } |
| |
| void animateOverflow(float animatedValue) { |
| int extraControlWidth = mExtraControls.getWidth(); |
| int extraControlTranslationX = -1 * (int) (extraControlWidth * animatedValue); |
| mExtraControls.setTranslationX(extraControlTranslationX); |
| |
| mTimeView.setAlpha(1 - animatedValue); |
| mBasicControls.setAlpha(1 - animatedValue); |
| |
| int transportControlLeftWidth = findFullSizedControlButton(R.id.pause).getLeft(); |
| int transportControlTranslationX = -1 * (int) (transportControlLeftWidth * animatedValue); |
| mFullTransportControls.setTranslationX(transportControlTranslationX); |
| findFullSizedControlButton(R.id.ffwd).setAlpha(1 - animatedValue); |
| } |
| |
| void resetHideCallbacks() { |
| removeCallbacks(mHideMainBars); |
| removeCallbacks(mHideProgressBar); |
| postDelayedRunnable(mHideMainBars, mDelayedAnimationIntervalMs); |
| } |
| |
| void updateAllowedCommands() { |
| ensurePlayerIsNotNull(); |
| |
| boolean canPause = mPlayer.canPause(); |
| boolean canRew = mPlayer.canSeekBackward(); |
| boolean canFfwd = mPlayer.canSeekForward(); |
| boolean canPrev = mPlayer.canSkipToPrevious(); |
| boolean canNext = mPlayer.canSkipToNext(); |
| boolean canSeekTo = mPlayer.canSeekTo(); |
| |
| int n = mTransportControlsMap.size(); |
| for (int i = 0; i < n; i++) { |
| int sizeType = mTransportControlsMap.keyAt(i); |
| |
| View playPauseButton = findControlButton(sizeType, R.id.pause); |
| if (playPauseButton != null) { |
| playPauseButton.setVisibility(canPause ? View.VISIBLE : View.GONE); |
| } |
| View rewButton = findControlButton(sizeType, R.id.rew); |
| if (rewButton != null) { |
| rewButton.setVisibility(canRew ? View.VISIBLE : View.GONE); |
| } |
| View ffwdButton = findControlButton(sizeType, R.id.ffwd); |
| if (ffwdButton != null) { |
| ffwdButton.setVisibility(canFfwd ? View.VISIBLE : View.GONE); |
| } |
| View prevButton = findControlButton(sizeType, R.id.prev); |
| if (prevButton != null) { |
| prevButton.setVisibility(canPrev ? View.VISIBLE : View.GONE); |
| } |
| View nextButton = findControlButton(sizeType, R.id.next); |
| if (nextButton != null) { |
| nextButton.setVisibility(canNext ? View.VISIBLE : View.GONE); |
| } |
| } |
| mSeekAvailable = canSeekTo; |
| mProgress.setEnabled(canSeekTo); |
| updateSubtitleButtonVisibility(); |
| } |
| |
| void updateSubtitleButtonVisibility() { |
| // 1. If player doesn't support select/deselect track, subtitle button will not be shown. |
| // 2. If there's no valid track information, subtitle button will not be shown. |
| // The second criteria prevents the case that "cc" button is shortly shown and disappears |
| // when the media item is a music without subtitle tracks. |
| if (!mPlayer.canSelectDeselectTrack() |
| || (mVideoTrackCount == 0 && mAudioTracks.isEmpty() && mSubtitleTracks.isEmpty())) { |
| mSubtitleButton.setVisibility(View.GONE); |
| mSubtitleButton.setEnabled(false); |
| return; |
| } |
| |
| if (mSubtitleTracks.isEmpty()) { |
| // For Audio only media item, CC button will not be shown when there's |
| // no subtitle tracks. |
| if (isCurrentItemMusic()) { |
| mSubtitleButton.setVisibility(View.GONE); |
| mSubtitleButton.setEnabled(false); |
| } else { |
| mSubtitleButton.setVisibility(View.VISIBLE); |
| mSubtitleButton.setAlpha(0.5f); |
| mSubtitleButton.setEnabled(false); |
| } |
| } else { |
| mSubtitleButton.setVisibility(View.VISIBLE); |
| mSubtitleButton.setAlpha(1.0f); |
| mSubtitleButton.setEnabled(true); |
| } |
| } |
| |
| void updatePrevNextButtons(int prevIndex, int nextIndex) { |
| int n = mTransportControlsMap.size(); |
| for (int i = 0; i < n; i++) { |
| int sizeType = mTransportControlsMap.keyAt(i); |
| View prevButton = findControlButton(sizeType, R.id.prev); |
| if (prevButton != null) { |
| if (prevIndex > androidx.media2.common.SessionPlayer.INVALID_ITEM_INDEX) { |
| prevButton.setAlpha(1.0f); |
| prevButton.setEnabled(true); |
| } else { |
| prevButton.setAlpha(0.5f); |
| prevButton.setEnabled(false); |
| } |
| } |
| View nextButton = findControlButton(sizeType, R.id.next); |
| if (nextButton != null) { |
| if (nextIndex > androidx.media2.common.SessionPlayer.INVALID_ITEM_INDEX) { |
| nextButton.setAlpha(1.0f); |
| nextButton.setEnabled(true); |
| } else { |
| nextButton.setAlpha(0.5f); |
| nextButton.setEnabled(false); |
| } |
| } |
| } |
| } |
| |
| boolean shouldNotHideBars() { |
| return (isCurrentItemMusic() && mSizeType == SIZE_TYPE_FULL) |
| || mAccessibilityManager.isTouchExplorationEnabled() |
| || mPlayer.getPlayerState() |
| == androidx.media2.common.SessionPlayer.PLAYER_STATE_ERROR |
| || mPlayer.getPlayerState() |
| == androidx.media2.common.SessionPlayer.PLAYER_STATE_IDLE; |
| } |
| |
| void seekTo(long newPosition, boolean shouldSeekNow) { |
| ensurePlayerIsNotNull(); |
| |
| int positionOnProgressBar = (mDuration <= 0) |
| ? 0 : (int) (MAX_PROGRESS * newPosition / mDuration); |
| mProgress.setProgress(positionOnProgressBar); |
| mCurrentTime.setText(stringForTime(newPosition)); |
| |
| if (mCurrentSeekPosition == SEEK_POSITION_NOT_SET) { |
| // If current seek position is not set, update its value and seek now if necessary. |
| mCurrentSeekPosition = newPosition; |
| |
| if (shouldSeekNow) { |
| mPlayer.seekTo(mCurrentSeekPosition); |
| } |
| } else { |
| // If current seek position is already set, update the next seek position. |
| mNextSeekPosition = newPosition; |
| } |
| } |
| |
| long getLatestSeekPosition() { |
| ensurePlayerIsNotNull(); |
| |
| if (mNextSeekPosition != SEEK_POSITION_NOT_SET) { |
| return mNextSeekPosition; |
| } else if (mCurrentSeekPosition != SEEK_POSITION_NOT_SET) { |
| return mCurrentSeekPosition; |
| } |
| return mPlayer.getCurrentPosition(); |
| } |
| |
| void removeCustomSpeedFromList() { |
| mPlaybackSpeedMultBy100List.remove(mCustomPlaybackSpeedIndex); |
| mPlaybackSpeedTextList.remove(mCustomPlaybackSpeedIndex); |
| mCustomPlaybackSpeedIndex = -1; |
| } |
| |
| void updateSelectedSpeed(int selectedSpeedIndex, String selectedSpeedText) { |
| mSelectedSpeedIndex = selectedSpeedIndex; |
| mSettingsSubTextsList.set(SETTINGS_MODE_PLAYBACK_SPEED, selectedSpeedText); |
| mSubSettingsAdapter.setTexts(mPlaybackSpeedTextList); |
| mSubSettingsAdapter.setCheckPosition(mSelectedSpeedIndex); |
| } |
| |
| void updateReplayButton(boolean toBeShown) { |
| ImageButton ffwdButton = findControlButton(mSizeType, R.id.ffwd); |
| if (toBeShown) { |
| mIsShowingReplayButton = true; |
| updatePlayButton(PLAY_BUTTON_REPLAY); |
| if (ffwdButton != null) { |
| ffwdButton.setAlpha(0.5f); |
| ffwdButton.setEnabled(false); |
| } |
| } else { |
| mIsShowingReplayButton = false; |
| if (mPlayer != null && mPlayer.isPlaying()) { |
| updatePlayButton(PLAY_BUTTON_PAUSE); |
| } else { |
| updatePlayButton(PLAY_BUTTON_PLAY); |
| } |
| if (ffwdButton != null) { |
| ffwdButton.setAlpha(1.0f); |
| ffwdButton.setEnabled(true); |
| } |
| } |
| } |
| |
| void updatePlayButton(int type) { |
| ImageButton playButton = findControlButton(mSizeType, R.id.pause); |
| if (playButton == null) { |
| return; |
| } |
| Drawable drawable; |
| String description; |
| if (type == PLAY_BUTTON_PAUSE) { |
| drawable = ContextCompat.getDrawable(getContext(), |
| R.drawable.media2_widget_ic_pause_circle_filled); |
| description = mResources.getString(R.string.mcv2_pause_button_desc); |
| } else if (type == PLAY_BUTTON_PLAY) { |
| drawable = ContextCompat.getDrawable(getContext(), |
| R.drawable.media2_widget_ic_play_circle_filled); |
| description = mResources.getString(R.string.mcv2_play_button_desc); |
| } else if (type == PLAY_BUTTON_REPLAY) { |
| drawable = ContextCompat.getDrawable(getContext(), |
| R.drawable.media2_widget_ic_replay_circle_filled); |
| description = mResources.getString(R.string.mcv2_replay_button_desc); |
| } else { |
| throw new IllegalArgumentException("unknown type " + type); |
| } |
| playButton.setImageDrawable(drawable); |
| playButton.setContentDescription(description); |
| } |
| |
| void postDelayedRunnable(Runnable runnable, long interval) { |
| if (interval != DISABLE_DELAYED_ANIMATION) { |
| postDelayed(runnable, interval); |
| } |
| } |
| |
| void ensurePlayerIsNotNull() { |
| if (mPlayer == null) { |
| throw new IllegalStateException("mPlayer must not be null"); |
| } |
| } |
| |
| void updateTracks( |
| PlayerWrapper player, List<androidx.media2.common.SessionPlayer.TrackInfo> trackInfos) { |
| // Update video track count, audio & subtitle track lists. |
| mVideoTrackCount = 0; |
| mAudioTracks = new ArrayList<>(); |
| mSubtitleTracks = new ArrayList<>(); |
| mSelectedAudioTrackIndex = 0; |
| // Default is -1 since subtitle selection always includes "Off" item |
| mSelectedSubtitleTrackIndex = -1; |
| androidx.media2.common.SessionPlayer.TrackInfo audioTrack = |
| player.getSelectedTrack( |
| androidx.media2.common.SessionPlayer.TrackInfo.MEDIA_TRACK_TYPE_AUDIO); |
| androidx.media2.common.SessionPlayer.TrackInfo subtitleTrack = |
| player.getSelectedTrack( |
| androidx.media2.common.SessionPlayer.TrackInfo.MEDIA_TRACK_TYPE_SUBTITLE); |
| for (int i = 0; i < trackInfos.size(); i++) { |
| int trackType = trackInfos.get(i).getTrackType(); |
| if (trackType |
| == androidx.media2.common.SessionPlayer.TrackInfo.MEDIA_TRACK_TYPE_VIDEO) { |
| mVideoTrackCount++; |
| } else if (trackType |
| == androidx.media2.common.SessionPlayer.TrackInfo.MEDIA_TRACK_TYPE_AUDIO) { |
| if (trackInfos.get(i).equals(audioTrack)) { |
| mSelectedAudioTrackIndex = mAudioTracks.size(); |
| } |
| mAudioTracks.add(trackInfos.get(i)); |
| } else if (trackType |
| == androidx.media2.common.SessionPlayer.TrackInfo.MEDIA_TRACK_TYPE_SUBTITLE) { |
| if (trackInfos.get(i).equals(subtitleTrack)) { |
| mSelectedSubtitleTrackIndex = mSubtitleTracks.size(); |
| } |
| mSubtitleTracks.add(trackInfos.get(i)); |
| } |
| } |
| |
| // Update audio description list. |
| mAudioTrackDescriptionList = new ArrayList<>(); |
| if (mAudioTracks.isEmpty()) { |
| mAudioTrackDescriptionList.add( |
| mResources.getString(R.string.MediaControlView_audio_track_none_text)); |
| } else { |
| for (int i = 0; i < mAudioTracks.size(); i++) { |
| mAudioTrackDescriptionList.add(mResources.getString( |
| R.string.MediaControlView_audio_track_number_text, i + 1)); |
| } |
| } |
| |
| // Update text for audio displayed inside the Settings window. |
| mSettingsSubTextsList.set(SETTINGS_MODE_AUDIO_TRACK, |
| mAudioTrackDescriptionList.get(mSelectedAudioTrackIndex)); |
| |
| // Update subtitle description list and subtitle button visibility. |
| mSubtitleDescriptionsList = new ArrayList<>(); |
| if (!mSubtitleTracks.isEmpty()) { |
| mSubtitleDescriptionsList.add(mResources.getString( |
| R.string.MediaControlView_subtitle_off_text)); |
| for (int i = 0; i < mSubtitleTracks.size(); i++) { |
| String lang = mSubtitleTracks.get(i).getLanguage().getISO3Language(); |
| String trackDescription; |
| if (lang.equals("und")) { |
| trackDescription = mResources.getString( |
| R.string.MediaControlView_subtitle_track_number_text, i + 1); |
| } else { |
| trackDescription = mResources.getString( |
| R.string.MediaControlView_subtitle_track_number_and_lang_text, |
| i + 1, lang); |
| } |
| mSubtitleDescriptionsList.add(trackDescription); |
| } |
| } |
| updateSubtitleButtonVisibility(); |
| } |
| |
| private boolean hasActualVideo() { |
| if (mVideoTrackCount > 0) { |
| return true; |
| } |
| androidx.media2.common.VideoSize videoSize = mPlayer.getVideoSize(); |
| if (videoSize.getHeight() > 0 && videoSize.getWidth() > 0) { |
| Log.w(TAG, "video track count is zero, but it renders video. size: " + videoSize); |
| return true; |
| } |
| return false; |
| } |
| |
| private boolean isCurrentItemMusic() { |
| return !hasActualVideo() && mAudioTracks.size() > 0; |
| } |
| |
| 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; |
| } |
| |
| @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 = inflateLayout(getContext(), R.layout.media2_widget_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( |
| ContextCompat.getDrawable(getContext(), mIconIds.get(position))); |
| } |
| return row; |
| } |
| |
| public void setSubTexts(List<String> subTexts) { |
| mSubTexts = subTexts; |
| } |
| } |
| |
| private class SubSettingsAdapter extends BaseAdapter { |
| private List<String> mTexts; |
| private int mCheckPosition; |
| |
| SubSettingsAdapter(List<String> texts, int checkPosition) { |
| mTexts = texts; |
| mCheckPosition = checkPosition; |
| } |
| |
| public String getMainText(int position) { |
| if (mTexts != null) { |
| if (position < mTexts.size()) { |
| return mTexts.get(position); |
| } |
| } |
| return RESOURCE_EMPTY; |
| } |
| |
| @Override |
| public int getCount() { |
| return (mTexts == null) ? 0 : mTexts.size(); |
| } |
| |
| @Override |
| public long getItemId(int position) { |
| // Auto-generated method stub--does not have any purpose here |
| return 0; |
| } |
| |
| @Override |
| public Object getItem(int position) { |
| // Auto-generated method stub--does not have any purpose here |
| return null; |
| } |
| |
| @Override |
| public View getView(int position, View convertView, ViewGroup container) { |
| View row = inflateLayout(getContext(), R.layout.media2_widget_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; |
| } |
| } |
| |
| // TODO (b/122440911): Enable advertisement mode |
| class PlayerCallback extends PlayerWrapper.PlayerCallback { |
| @Override |
| public void onPlayerStateChanged(@NonNull PlayerWrapper player, int state) { |
| if (player != mPlayer) return; |
| |
| if (DEBUG) { |
| Log.d(TAG, "onPlayerStateChanged(state: " + state + ")"); |
| } |
| |
| updateTimeViews(player.getCurrentMediaItem()); |
| |
| // 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. |
| switch (state) { |
| case androidx.media2.common.SessionPlayer.PLAYER_STATE_PLAYING: |
| removeCallbacks(mUpdateProgress); |
| post(mUpdateProgress); |
| resetHideCallbacks(); |
| updateReplayButton(false); |
| break; |
| case androidx.media2.common.SessionPlayer.PLAYER_STATE_PAUSED: |
| updatePlayButton(PLAY_BUTTON_PLAY); |
| removeCallbacks(mUpdateProgress); |
| removeCallbacks(mHideMainBars); |
| removeCallbacks(mHideProgressBar); |
| post(mShowAllBars); |
| break; |
| case androidx.media2.common.SessionPlayer.PLAYER_STATE_ERROR: |
| updatePlayButton(PLAY_BUTTON_PLAY); |
| removeCallbacks(mUpdateProgress); |
| if (getWindowToken() != null) { |
| new AlertDialog.Builder(getContext()) |
| .setMessage(R.string.mcv2_playback_error_text) |
| .setPositiveButton(R.string.mcv2_error_dialog_button, |
| new DialogInterface.OnClickListener() { |
| @Override |
| public void onClick( |
| DialogInterface dialogInterface, |
| int i) { |
| dialogInterface.dismiss(); |
| } |
| }) |
| .setCancelable(true) |
| .show(); |
| } |
| break; |
| } |
| } |
| |
| @Override |
| public void onSeekCompleted(@NonNull PlayerWrapper player, long position) { |
| if (player != mPlayer) return; |
| |
| if (DEBUG) { |
| Log.d(TAG, "onSeekCompleted(): " + position); |
| } |
| // Update progress bar and time text. |
| int positionOnProgressBar = (mDuration <= 0) |
| ? 0 : (int) (MAX_PROGRESS * position / mDuration); |
| mProgress.setProgress(positionOnProgressBar); |
| mCurrentTime.setText(stringForTime(position)); |
| |
| if (mNextSeekPosition != SEEK_POSITION_NOT_SET) { |
| mCurrentSeekPosition = mNextSeekPosition; |
| |
| // If the next seek position is set, seek to that position. |
| player.seekTo(mNextSeekPosition); |
| mNextSeekPosition = SEEK_POSITION_NOT_SET; |
| } else { |
| mCurrentSeekPosition = SEEK_POSITION_NOT_SET; |
| |
| // If the next seek position is not set and the progress bar thumb is not being |
| // dragged, start to update progress. |
| if (!mDragging) { |
| removeCallbacks(mUpdateProgress); |
| removeCallbacks(mHideMainBars); |
| post(mUpdateProgress); |
| postDelayedRunnable(mHideMainBars, mDelayedAnimationIntervalMs); |
| } |
| } |
| } |
| |
| @Override |
| public void onCurrentMediaItemChanged( |
| @NonNull PlayerWrapper player, |
| @Nullable androidx.media2.common.MediaItem mediaItem) { |
| if (player != mPlayer) return; |
| |
| if (DEBUG) { |
| Log.d(TAG, "onCurrentMediaItemChanged(): " + mediaItem); |
| } |
| updateTimeViews(mediaItem); |
| updateTitleView(mediaItem); |
| updatePrevNextButtons(player.getPreviousMediaItemIndex(), |
| player.getNextMediaItemIndex()); |
| } |
| |
| @Override |
| void onPlaylistChanged( |
| @NonNull PlayerWrapper player, |
| @Nullable List<androidx.media2.common.MediaItem> list, |
| @Nullable androidx.media2.common.MediaMetadata metadata) { |
| if (player != mPlayer) return; |
| |
| if (DEBUG) { |
| Log.d(TAG, "onPlaylistChanged(): list: " + list + ", metadata: " + metadata); |
| } |
| updatePrevNextButtons(player.getPreviousMediaItemIndex(), |
| player.getNextMediaItemIndex()); |
| } |
| |
| @Override |
| public void onPlaybackCompleted(@NonNull PlayerWrapper player) { |
| if (player != mPlayer) return; |
| |
| if (DEBUG) { |
| Log.d(TAG, "onPlaybackCompleted()"); |
| } |
| updateReplayButton(true); |
| // The progress bar and current time text may not have been updated. |
| mProgress.setProgress(MAX_PROGRESS); |
| mCurrentTime.setText(stringForTime(mDuration)); |
| } |
| |
| @Override |
| public void onAllowedCommandsChanged( |
| @NonNull PlayerWrapper player, |
| @NonNull androidx.media2.session.SessionCommandGroup commands) { |
| if (player != mPlayer) return; |
| |
| updateAllowedCommands(); |
| } |
| |
| @Override |
| public void onPlaybackSpeedChanged(@NonNull PlayerWrapper player, float speed) { |
| if (player != mPlayer) return; |
| |
| int customSpeedMultBy100 = Math.round(speed * 100); |
| // An application may set a custom playback speed that is not included in the |
| // default playback speed list. The code below handles adding/removing the custom |
| // playback speed to the default list. |
| if (mCustomPlaybackSpeedIndex != -1) { |
| // Remove existing custom playback speed |
| removeCustomSpeedFromList(); |
| } |
| |
| if (mPlaybackSpeedMultBy100List.contains(customSpeedMultBy100)) { |
| for (int i = 0; i < mPlaybackSpeedMultBy100List.size(); i++) { |
| if (customSpeedMultBy100 == mPlaybackSpeedMultBy100List.get(i)) { |
| updateSelectedSpeed(i, mPlaybackSpeedTextList.get(i)); |
| break; |
| } |
| } |
| } else { |
| String customSpeedText = mResources.getString( |
| R.string.MediaControlView_custom_playback_speed_text, |
| customSpeedMultBy100 / 100.0f); |
| |
| for (int i = 0; i < mPlaybackSpeedMultBy100List.size(); i++) { |
| if (customSpeedMultBy100 < mPlaybackSpeedMultBy100List.get(i)) { |
| mPlaybackSpeedMultBy100List.add(i, customSpeedMultBy100); |
| mPlaybackSpeedTextList.add(i, customSpeedText); |
| updateSelectedSpeed(i, customSpeedText); |
| break; |
| } |
| // Add to end of list if the custom speed value is greater than all the |
| // value in the default speed list. |
| if (i == mPlaybackSpeedMultBy100List.size() - 1 |
| && customSpeedMultBy100 > mPlaybackSpeedMultBy100List.get(i)) { |
| mPlaybackSpeedMultBy100List.add(customSpeedMultBy100); |
| mPlaybackSpeedTextList.add(customSpeedText); |
| updateSelectedSpeed(i + 1, customSpeedText); |
| } |
| } |
| mCustomPlaybackSpeedIndex = mSelectedSpeedIndex; |
| } |
| } |
| |
| @Override |
| void onTracksChanged( |
| @NonNull PlayerWrapper player, |
| @NonNull List<androidx.media2.common.SessionPlayer.TrackInfo> tracks) { |
| if (player != mPlayer) return; |
| |
| if (DEBUG) { |
| Log.d(TAG, "onandroidx.media2.common.SessionPlayer.TrackInfoChanged(): " + tracks); |
| } |
| |
| updateTracks(player, tracks); |
| updateTimeViews(player.getCurrentMediaItem()); |
| updateTitleView(player.getCurrentMediaItem()); |
| } |
| |
| @Override |
| void onTrackSelected( |
| @NonNull PlayerWrapper player, |
| @NonNull androidx.media2.common.SessionPlayer.TrackInfo trackInfo) { |
| if (player != mPlayer) return; |
| |
| if (DEBUG) { |
| Log.d(TAG, "onTrackSelected(): " + trackInfo); |
| } |
| if (trackInfo.getTrackType() |
| == androidx.media2.common.SessionPlayer.TrackInfo.MEDIA_TRACK_TYPE_SUBTITLE) { |
| for (int i = 0; i < mSubtitleTracks.size(); i++) { |
| if (mSubtitleTracks.get(i).equals(trackInfo)) { |
| mSelectedSubtitleTrackIndex = i; |
| |
| if (mSettingsMode == SETTINGS_MODE_SUBTITLE_TRACK) { |
| mSubSettingsAdapter.setCheckPosition(mSelectedSubtitleTrackIndex + 1); |
| } |
| mSubtitleButton.setImageDrawable(ContextCompat.getDrawable(getContext(), |
| R.drawable.media2_widget_ic_subtitle_on)); |
| mSubtitleButton.setContentDescription( |
| mResources.getString(R.string.mcv2_cc_is_on)); |
| break; |
| } |
| } |
| } else if (trackInfo.getTrackType() |
| == androidx.media2.common.SessionPlayer.TrackInfo.MEDIA_TRACK_TYPE_AUDIO) { |
| for (int i = 0; i < mAudioTracks.size(); i++) { |
| if (mAudioTracks.get(i).equals(trackInfo)) { |
| mSelectedAudioTrackIndex = i; |
| |
| mSettingsSubTextsList.set(SETTINGS_MODE_AUDIO_TRACK, |
| mSubSettingsAdapter.getMainText(mSelectedAudioTrackIndex)); |
| break; |
| } |
| } |
| } |
| } |
| |
| @Override |
| void onTrackDeselected( |
| @NonNull PlayerWrapper player, |
| @NonNull androidx.media2.common.SessionPlayer.TrackInfo trackInfo) { |
| if (player != mPlayer) return; |
| |
| if (DEBUG) { |
| Log.d(TAG, "onTrackDeselected(): " + trackInfo); |
| } |
| if (trackInfo.getTrackType() |
| == androidx.media2.common.SessionPlayer.TrackInfo.MEDIA_TRACK_TYPE_SUBTITLE) { |
| for (int i = 0; i < mSubtitleTracks.size(); i++) { |
| if (mSubtitleTracks.get(i).equals(trackInfo)) { |
| mSelectedSubtitleTrackIndex = -1; |
| |
| if (mSettingsMode == SETTINGS_MODE_SUBTITLE_TRACK) { |
| mSubSettingsAdapter.setCheckPosition(mSelectedSubtitleTrackIndex + 1); |
| } |
| mSubtitleButton.setImageDrawable(ContextCompat.getDrawable(getContext(), |
| R.drawable.media2_widget_ic_subtitle_off)); |
| mSubtitleButton.setContentDescription( |
| mResources.getString(R.string.mcv2_cc_is_off)); |
| break; |
| } |
| } |
| } |
| } |
| |
| @Override |
| void onVideoSizeChanged( |
| @NonNull PlayerWrapper player, |
| @NonNull androidx.media2.common.VideoSize videoSize) { |
| if (player != mPlayer) return; |
| |
| if (DEBUG) { |
| Log.d(TAG, "onVideoSizeChanged(): " + videoSize); |
| } |
| if (mVideoTrackCount == 0 && videoSize.getHeight() > 0 && videoSize.getWidth() > 0) { |
| List<androidx.media2.common.SessionPlayer.TrackInfo> tracks = player.getTracks(); |
| if (tracks != null) { |
| updateTracks(player, tracks); |
| } |
| } |
| } |
| } |
| } |