VideoView.java

/*
 * Copyright 2018 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package androidx.media2.widget;

import android.content.Context;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
import android.util.Log;
import android.view.Surface;
import android.view.View;

import androidx.annotation.IntDef;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.content.ContextCompat;
import androidx.core.view.ViewCompat;
import androidx.palette.graphics.Palette;

import com.google.common.util.concurrent.ListenableFuture;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;

/**
 * A high level view for media playback that can be integrated with either a {@link
 * androidx.media2.common.SessionPlayer} or a {@link androidx.media2.session.MediaController}.
 * Developers can easily implement a video rendering application using this class. By default, a
 * {@link MediaControlView} is attached so the playback control buttons are displayed on top of
 * VideoView.
 *
 * <p>Contents:
 *
 * <ol>
 *   <li><a href="UseCases">Using VideoView with androidx.media2.common.SessionPlayer or
 *       androidx.media2.session.MediaController</a>
 *   <li><a href="UseWithMCV">Using VideoView with MediaControlView</a>
 *   <li><a href="ViewType">Choosing a view type</a>
 *   <li><a href="LegacyVideoView">Comparison with android.widget.VideoView</a>
 *   <li><a href="DisplayMetadata">Displaying Metadata</a>
 * </ol>
 *
 * <h3 id="UseCases">Using VideoView with androidx.media2.common.SessionPlayer or
 * androidx.media2.session.MediaController</h3>
 *
 * <ul>
 *   <li>For simple use cases that do not require communication with a {@link
 *       androidx.media2.session.MediaSession}, apps need to create a player instance that extends
 *       {@link androidx.media2.common.SessionPlayer} (e.g. {@link
 *       androidx.media2.player.MediaPlayer}) and link it to this view by calling {@link
 *       #setPlayer}.
 *   <li>For more advanced use cases that require a {@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} that's attached to the {@link
 *       androidx.media2.session.MediaSession} and link it to this view by calling {@link
 *       #setMediaController}.
 * </ul>
 *
 * <h3 id="UseWithMCV">Using VideoView with MediaControlView</h3>
 *
 * {@link VideoView} is working with {@link MediaControlView} and a MediaControlView instance is
 * attached to VideoView by default.
 *
 * <p>If you want to attach a custom {@link MediaControlView}, assign the custom media control
 * widget using {@link #setMediaControlView}.
 *
 * <p>If you don't want to use {@link MediaControlView}, set the VideoView attribute {@link
 * androidx.media2.widget.R.attr#enableControlView} to false.
 *
 * <h3 id="ViewType">Choosing a view type</h3>
 *
 * VideoView can render videos on a TextureView or SurfaceView. The default is SurfaceView which can
 * be changed by using the {@link #setViewType(int)} method or by setting the {@link
 * androidx.media2.widget.R.attr#viewType} attribute in the layout file.
 *
 * <p>SurfaceView is recommended in most cases for saving battery life. TextureView might be
 * preferred for supporting various UIs such as animation and translucency.
 *
 * <h3 id="LegacyVideoView">Comparison with android.widget.VideoView</h3>
 *
 * These are the main differences between the media2 VideoView widget and the older android widget:
 *
 * <ul>
 *   <li>{@link android.widget.VideoView android.widget.VideoView} creates a {@link
 *       android.media.MediaPlayer} instance internally and wraps playback APIs around it.
 *       <p>{@link VideoView androidx.media2.widget.VideoView} does not create a player instance
 *       internally. Instead, either a {@link androidx.media2.common.SessionPlayer} or a {@link
 *       androidx.media2.session.MediaController} instance should be created externally and link to
 *       {@link VideoView} using {@link #setPlayer(androidx.media2.common.SessionPlayer)} or {@link
 *       #setMediaController(androidx.media2.session.MediaController)}, respectively.
 *   <li>{@link android.widget.VideoView android.widget.VideoView} inherits from the SurfaceView
 *       class.
 *       <p>{@link VideoView androidx.media2.widget.VideoView} inherits from ViewGroup and can
 *       render videos using SurfaceView or TextureView, depending on your choice.
 *   <li>A {@link VideoView} can respond to media key events if you call {@link #setMediaController}
 *       to link it to a {@link androidx.media2.session.MediaController} that's connected to an
 *       active {@link androidx.media2.session.MediaSession}.
 * </ul>
 *
 * <h3 id="DisplayMetadata">Displaying Metadata</h3>
 *
 * When you play music only (sound with no video), VideoView can display album art and other
 * metadata by calling {@link
 * androidx.media2.common.MediaItem#setMetadata(androidx.media2.common.MediaMetadata)}. The
 * following table shows the metadata displayed by the 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>Note: VideoView does not retain its full state when going into the background. In particular,
 * it does not save, and does not restore the current play state, play position, selected tracks.
 * Applications should save and restore these on their own in {@link
 * android.app.Activity#onSaveInstanceState} and {@link
 * android.app.Activity#onRestoreInstanceState}.
 *
 * <p>Attributes :
 *
 * <ul>
 *   <li>{@link androidx.media2.widget.R.attr#enableControlView}
 *   <li>{@link androidx.media2.widget.R.attr#viewType}
 * </ul>
 *
 * <p>Example of attributes for a VideoView with TextureView and no attached control view:
 *
 * <pre>{@code
 * <androidx.media2.widget.VideoView
 *     android:id="@+id/video_view"
 *     widget:enableControlView="false"
 *     widget:viewType="textureView"
 * />
 * }</pre>
 *
 * @see MediaControlView
 * @see androidx.media2.common.SessionPlayer
 * @see androidx.media2.session.MediaController
 * @deprecated androidx.media2 is deprecated. Please migrate to <a
 *     href="https://developer.android.com/guide/topics/media/media3">androidx.media3</a>.
 */
@Deprecated
public class VideoView extends SelectiveLayout {
    @IntDef({
            VIEW_TYPE_TEXTUREVIEW,
            VIEW_TYPE_SURFACEVIEW
    })
    @Retention(RetentionPolicy.SOURCE)
    /* package */ @interface ViewType {}

    /**
     * Indicates video is rendering on SurfaceView.
     *
     * @see #setViewType
     */
    public static final int VIEW_TYPE_SURFACEVIEW = 0;

    /**
     * Indicates video is rendering on TextureView.
     *
     * @see #setViewType
     */
    public static final int VIEW_TYPE_TEXTUREVIEW = 1;

    private static final String TAG = "VideoView";
    static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);

    VideoView.OnViewTypeChangedListener mViewTypeChangedListener;

    VideoViewInterface mCurrentView;
    VideoViewInterface mTargetView;
    VideoTextureView mTextureView;
    VideoSurfaceView mSurfaceView;

    PlayerWrapper mPlayer;
    MediaControlView mMediaControlView;

    MusicView mMusicView;

    SelectiveLayout.LayoutParams mSelectiveLayoutParams;

    int mVideoTrackCount;
    int mAudioTrackCount;
    Map<androidx.media2.common.SessionPlayer.TrackInfo, SubtitleTrack> mSubtitleTracks;
    SubtitleController mSubtitleController;

    // selected subtitle track info as MediaPlayer returns
    androidx.media2.common.SessionPlayer.TrackInfo mSelectedSubtitleTrackInfo;

    SubtitleAnchorView mSubtitleAnchorView;

    private final VideoViewInterface.SurfaceListener mSurfaceListener =
            new VideoViewInterface.SurfaceListener() {
        @Override
        public void onSurfaceCreated(@NonNull View view, int width, int height) {
            if (DEBUG) {
                Log.d(TAG, "onSurfaceCreated()"
                        + ", width/height: " + width + "/" + height
                        + ", " + view.toString());
            }
            if (view == mTargetView && VideoView.this.isAggregatedVisible()) {
                mTargetView.assignSurfaceToPlayerWrapper(mPlayer);
            }
        }

        @Override
        public void onSurfaceDestroyed(@NonNull View view) {
            if (DEBUG) {
                Log.d(TAG, "onSurfaceDestroyed(). " + view.toString());
            }
        }

        @Override
        public void onSurfaceChanged(@NonNull View view, int width, int height) {
            if (DEBUG) {
                Log.d(TAG, "onSurfaceChanged(). width/height: " + width + "/" + height
                        + ", " + view.toString());
            }
        }

        @Override
        public void onSurfaceTakeOverDone(@NonNull VideoViewInterface view) {
            if (view != mTargetView) {
                Log.w(TAG, "onSurfaceTakeOverDone(). view is not targetView. ignore.: " + view);
                return;
            }
            if (DEBUG) {
                Log.d(TAG, "onSurfaceTakeOverDone(). Now current view is: " + view);
            }
            if (view != mCurrentView) {
                ((View) mCurrentView).setVisibility(View.GONE);
                mCurrentView = view;
                if (mViewTypeChangedListener != null) {
                    mViewTypeChangedListener.onViewTypeChanged(VideoView.this, view.getViewType());
                }
            }
        }
    };

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

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

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

    private void initialize(Context context, @Nullable AttributeSet attrs) {
        mSelectedSubtitleTrackInfo = null;

        setFocusable(true);
        setFocusableInTouchMode(true);
        requestFocus();

        mTextureView = new VideoTextureView(context);
        mSurfaceView = new VideoSurfaceView(context);
        mTextureView.setSurfaceListener(mSurfaceListener);
        mSurfaceView.setSurfaceListener(mSurfaceListener);

        addView(mTextureView);
        addView(mSurfaceView);

        mSelectiveLayoutParams = new SelectiveLayout.LayoutParams();
        mSelectiveLayoutParams.forceMatchParent = true;

        mSubtitleAnchorView = new SubtitleAnchorView(context);
        mSubtitleAnchorView.setBackgroundColor(0);
        addView(mSubtitleAnchorView, mSelectiveLayoutParams);

        SubtitleController.Listener listener =
                new SubtitleController.Listener() {
                    @Override
                    public void onSubtitleTrackSelected(SubtitleTrack track) {
                        // Track deselected
                        if (track == null) {
                            mSelectedSubtitleTrackInfo = null;
                            mSubtitleAnchorView.setVisibility(View.GONE);
                            return;
                        }

                        // Track selected
                        androidx.media2.common.SessionPlayer.TrackInfo info = null;
                        for (Map.Entry<
                                        androidx.media2.common.SessionPlayer.TrackInfo,
                                        SubtitleTrack>
                                pair : mSubtitleTracks.entrySet()) {
                            if (pair.getValue() == track) {
                                info = pair.getKey();
                                break;
                            }
                        }
                        if (info != null) {
                            mSelectedSubtitleTrackInfo = info;
                            mSubtitleAnchorView.setVisibility(View.VISIBLE);
                        }
                    }
                };
        mSubtitleController = new SubtitleController(context, null, listener);
        mSubtitleController.registerRenderer(new Cea608CaptionRenderer(context));
        mSubtitleController.registerRenderer(new Cea708CaptionRenderer(context));
        mSubtitleController.setAnchor(mSubtitleAnchorView);

        mMusicView = new MusicView(context);
        mMusicView.setVisibility(View.GONE);
        addView(mMusicView, mSelectiveLayoutParams);

        boolean enableControlView = (attrs == null) || attrs.getAttributeBooleanValue(
                "http://schemas.android.com/apk/res-auto",
                "enableControlView", true);
        if (enableControlView) {
            mMediaControlView = new MediaControlView(context);
            mMediaControlView.setAttachedToVideoView(true);
            addView(mMediaControlView, mSelectiveLayoutParams);
        }

        // Choose surface view by default
        int viewType = (attrs == null) ? VideoView.VIEW_TYPE_SURFACEVIEW
                : attrs.getAttributeIntValue(
                        "http://schemas.android.com/apk/res-auto",
                        "viewType", VideoView.VIEW_TYPE_SURFACEVIEW);
        if (viewType == VideoView.VIEW_TYPE_SURFACEVIEW) {
            if (DEBUG) {
                Log.d(TAG, "viewType attribute is surfaceView.");
            }
            mTextureView.setVisibility(View.GONE);
            mSurfaceView.setVisibility(View.VISIBLE);
            mCurrentView = mSurfaceView;
        } else if (viewType == VideoView.VIEW_TYPE_TEXTUREVIEW) {
            if (DEBUG) {
                Log.d(TAG, "viewType attribute is textureView.");
            }
            mTextureView.setVisibility(View.VISIBLE);
            mSurfaceView.setVisibility(View.GONE);
            mCurrentView = mTextureView;
        }
        mTargetView = mCurrentView;
    }

    /**
     * Sets {@link androidx.media2.session.MediaController} to display media content. Setting a
     * {@link androidx.media2.session.MediaController} will unset any {@link
     * androidx.media2.session.MediaController} or {@link androidx.media2.common.SessionPlayer} that
     * was previously set.
     *
     * <p>If VideoView has a {@link MediaControlView} instance, this controller will also be set to
     * it.
     *
     * <p>Calling this method will automatically set VideoView's surface to {@link
     * androidx.media2.session.MediaController} by calling {@link
     * androidx.media2.session.MediaController#setSurface(Surface)}. If the {@link
     * androidx.media2.session.MediaController} is connected to a {@link
     * androidx.media2.session.MediaSession} and that {@link androidx.media2.session.MediaSession}
     * is associated with a {@link androidx.media2.common.SessionPlayer}, VideoView's surface will
     * be set to that {@link androidx.media2.common.SessionPlayer}.
     *
     * @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 (mPlayer != null) {
            mPlayer.detachCallback();
        }
        mPlayer = new PlayerWrapper(controller, ContextCompat.getMainExecutor(getContext()),
                new PlayerCallback());
        if (ViewCompat.isAttachedToWindow(this)) {
            mPlayer.attachCallback();
        }
        if (this.isAggregatedVisible()) {
            mTargetView.assignSurfaceToPlayerWrapper(mPlayer);
        } else {
            resetPlayerSurfaceWithNullAsync();
        }

        if (mMediaControlView != null) {
            mMediaControlView.setMediaControllerInternal(controller);
        }
    }

    /**
     * Sets {@link androidx.media2.common.SessionPlayer} to display media content. Setting a
     * androidx.media2.common.SessionPlayer will unset any androidx.media2.session.MediaController
     * or androidx.media2.common.SessionPlayer that was previously set.
     *
     * <p>If VideoView has a {@link MediaControlView} instance, this player will also be set to it.
     *
     * <p>Calling this method will automatically set VideoView's surface to {@link
     * androidx.media2.common.SessionPlayer} by calling {@link
     * androidx.media2.common.SessionPlayer#setSurface(Surface)}.
     *
     * @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 (mPlayer != null) {
            mPlayer.detachCallback();
        }
        mPlayer = new PlayerWrapper(player, ContextCompat.getMainExecutor(getContext()),
                new PlayerCallback());
        if (ViewCompat.isAttachedToWindow(this)) {
            mPlayer.attachCallback();
        }
        if (this.isAggregatedVisible()) {
            mTargetView.assignSurfaceToPlayerWrapper(mPlayer);
        } else {
            resetPlayerSurfaceWithNullAsync();
        }

        if (mMediaControlView != null) {
            mMediaControlView.setPlayerInternal(player);
        }
    }

    /**
     * Sets {@link MediaControlView} instance. It will replace the previously assigned {@link
     * MediaControlView} instance if any.
     *
     * <p>If a {@link androidx.media2.session.MediaController} or a {@link
     * androidx.media2.common.SessionPlayer} instance has been set to {@link VideoView}, the same
     * instance will be set to {@link MediaControlView}.
     *
     * @param mediaControlView a {@link MediaControlView} instance.
     * @param intervalMs time interval in milliseconds until {@link MediaControlView} transitions
     *     into a different mode. -1 can be set to disable all UI transitions. See {@link
     *     MediaControlView} Javadoc Section "UI transitions" for details.
     */
    public void setMediaControlView(@NonNull MediaControlView mediaControlView, long intervalMs) {
        if (mMediaControlView != null) {
            removeView(mMediaControlView);
            mMediaControlView.setAttachedToVideoView(false);
        }
        addView(mediaControlView, mSelectiveLayoutParams);
        mediaControlView.setAttachedToVideoView(true);

        mMediaControlView = mediaControlView;
        mMediaControlView.setDelayedAnimationInterval(intervalMs);

        if (mPlayer != null) {
            if (mPlayer.mController != null) {
                mMediaControlView.setMediaControllerInternal(mPlayer.mController);
            } else if (mPlayer.mPlayer != null) {
                mMediaControlView.setPlayerInternal(mPlayer.mPlayer);
            }
        }
    }

    /**
     * Returns {@link MediaControlView} instance which is currently attached to VideoView by default
     * or by {@link #setMediaControlView} method.
     */
    @Nullable
    public MediaControlView getMediaControlView() {
        return mMediaControlView;
    }

    /**
     * Selects which view will be used to render video between SurfaceView and TextureView.
     * <p>
     * Note: There are two known issues on API level 28+ devices.
     * <ul>
     * <li> When changing view type to SurfaceView from TextureView in "paused" playback state,
     * a blank screen can be shown.
     * <li> When changing view type to TextureView from SurfaceView repeatedly in "paused" playback
     * state, the lastly rendered frame on TextureView can be shown.
     * </ul>
     * @param viewType the view type to render video
     * <ul>
     * <li>{@link #VIEW_TYPE_SURFACEVIEW}
     * <li>{@link #VIEW_TYPE_TEXTUREVIEW}
     * </ul>
     */
    public void setViewType(@ViewType int viewType) {
        if (viewType == mTargetView.getViewType()) {
            Log.d(TAG, "setViewType with the same type (" + viewType + ") is ignored.");
            return;
        }
        VideoViewInterface targetView;
        if (viewType == VideoView.VIEW_TYPE_TEXTUREVIEW) {
            Log.d(TAG, "switching to TextureView");
            targetView = mTextureView;
        } else if (viewType == VideoView.VIEW_TYPE_SURFACEVIEW) {
            Log.d(TAG, "switching to SurfaceView");
            targetView = mSurfaceView;
        } else {
            throw new IllegalArgumentException("Unknown view type: " + viewType);
        }

        mTargetView = targetView;
        if (this.isAggregatedVisible()) {
            targetView.assignSurfaceToPlayerWrapper(mPlayer);
        }
        ((View) targetView).setVisibility(View.VISIBLE);
        requestLayout();
    }

    /**
     * Returns view type.
     *
     * @return view type. See {@see setViewType}.
     */
    @ViewType
    public int getViewType() {
        return mCurrentView.getViewType();
    }

    /**
     * Sets a listener to be called when a view type change is done.
     *
     * @see #setViewType(int)
     *
     * @param listener The listener to be called. A value of <code>null</code> removes any existing
     * listener.
     */
    public void setOnViewTypeChangedListener(@Nullable OnViewTypeChangedListener listener) {
        mViewTypeChangedListener = listener;
    }

    @Override
    protected void onAttachedToWindow() {
        super.onAttachedToWindow();
        if (mPlayer != null) {
            mPlayer.attachCallback();
        }
    }

    @Override
    void onVisibilityAggregatedCompat(boolean isVisible) {
        super.onVisibilityAggregatedCompat(isVisible);
        if (mPlayer == null) {
            return;
        }

        if (isVisible) {
            mTargetView.assignSurfaceToPlayerWrapper(mPlayer);
        } else {
            if (mPlayer == null || mPlayer.hasDisconnectedController()) {
                Log.w(TAG, "Surface is being destroyed, but player will not be informed "
                        + "as the associated media controller is disconnected.");
                return;
            }
            resetPlayerSurfaceWithNull();
        }
    }

    @Override
    protected void onDetachedFromWindow() {
        super.onDetachedFromWindow();
        if (mPlayer != null) {
            mPlayer.detachCallback();
        }
    }

    @Override
    public CharSequence getAccessibilityClassName() {
        // Class name may be obfuscated by Proguard. Hardcode the string for accessibility usage.
        return "androidx.media2.widget.VideoView";
    }

    ///////////////////////////////////////////////////
    // Protected or private methods
    ///////////////////////////////////////////////////
    boolean isMediaPrepared() {
        return mPlayer != null
                && mPlayer.getPlayerState()
                        != androidx.media2.common.SessionPlayer.PLAYER_STATE_ERROR
                && mPlayer.getPlayerState()
                        != androidx.media2.common.SessionPlayer.PLAYER_STATE_IDLE;
    }

    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.getWidth() + "/" + videoSize.getHeight());
            return true;
        }
        return false;
    }

    boolean isCurrentItemMusic() {
        return !hasActualVideo() && mAudioTrackCount > 0;
    }

    void updateTracks(
            PlayerWrapper player, List<androidx.media2.common.SessionPlayer.TrackInfo> trackInfos) {
        mSubtitleTracks = new LinkedHashMap<>();
        mVideoTrackCount = 0;
        mAudioTrackCount = 0;
        for (int i = 0; i < trackInfos.size(); i++) {
            androidx.media2.common.SessionPlayer.TrackInfo trackInfo = trackInfos.get(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) {
                mAudioTrackCount++;
            } else if (trackType
                    == androidx.media2.common.SessionPlayer.TrackInfo.MEDIA_TRACK_TYPE_SUBTITLE) {
                SubtitleTrack track = mSubtitleController.addTrack(trackInfo.getFormat());
                if (track != null) {
                    mSubtitleTracks.put(trackInfo, track);
                }
            }
        }
        mSelectedSubtitleTrackInfo =
                player.getSelectedTrack(
                        androidx.media2.common.SessionPlayer.TrackInfo.MEDIA_TRACK_TYPE_SUBTITLE);
    }

    void updateMusicView(androidx.media2.common.MediaItem item) {
        boolean shouldShowMusicView = item != null && isCurrentItemMusic();
        if (shouldShowMusicView) {
            mMusicView.setVisibility(View.VISIBLE);

            androidx.media2.common.MediaMetadata metadata = item.getMetadata();
            Resources resources = getResources();

            Drawable albumDrawable = getAlbumArt(metadata,
                    ContextCompat.getDrawable(
                            getContext(), R.drawable.media2_widget_ic_default_album_image));
            String title =
                    getString(
                            metadata,
                            androidx.media2.common.MediaMetadata.METADATA_KEY_TITLE,
                            resources.getString(R.string.mcv2_music_title_unknown_text));
            String artist =
                    getString(
                            metadata,
                            androidx.media2.common.MediaMetadata.METADATA_KEY_ARTIST,
                            resources.getString(R.string.mcv2_music_artist_unknown_text));

            mMusicView.setAlbumDrawable(albumDrawable);
            mMusicView.setTitleText(title);
            mMusicView.setArtistText(artist);
        } else {
            mMusicView.setVisibility(View.GONE);
            mMusicView.setAlbumDrawable(null);
            mMusicView.setTitleText(null);
            mMusicView.setArtistText(null);
        }
    }

    @SuppressWarnings("WeakerAccess") /* synthetic access */
    void resetPlayerSurfaceWithNull() {
        try {
            int resultCode = mPlayer.setSurface(null).get(100, TimeUnit.MILLISECONDS)
                    .getResultCode();
            if (resultCode != androidx.media2.common.BaseResult.RESULT_SUCCESS) {
                Log.e(TAG, "calling setSurface(null) was not "
                        + "successful. ResultCode: " + resultCode);
            }
        } catch (ExecutionException | InterruptedException | TimeoutException e) {
            Log.e(TAG, "calling setSurface(null) was not successful.", e);
        }
    }

    @SuppressWarnings("WeakerAccess") /* synthetic access */
    void resetPlayerSurfaceWithNullAsync() {
        ListenableFuture<? extends androidx.media2.common.BaseResult> future =
                mPlayer.setSurface(null);
        future.addListener(
                new Runnable() {
                    @Override
                    public void run() {
                        try {
                            int resultCode = future.get().getResultCode();
                            if (resultCode != androidx.media2.common.BaseResult.RESULT_SUCCESS) {
                                Log.e(
                                        TAG,
                                        "calling setSurface(null) was not "
                                                + "successful. ResultCode: "
                                                + resultCode);
                            }
                        } catch (ExecutionException | InterruptedException e) {
                            Log.e(TAG, "calling setSurface(null) was not successful.", e);
                        }
                    }
                },
                ContextCompat.getMainExecutor(getContext()));
    }

    private Drawable getAlbumArt(
            @NonNull androidx.media2.common.MediaMetadata metadata, Drawable defaultDrawable) {
        Drawable drawable = defaultDrawable;
        Bitmap bitmap = null;

        if (metadata != null
                && metadata.containsKey(
                        androidx.media2.common.MediaMetadata.METADATA_KEY_ALBUM_ART)) {
            bitmap =
                    metadata.getBitmap(androidx.media2.common.MediaMetadata.METADATA_KEY_ALBUM_ART);
        }
        if (bitmap != null) {
            Palette.Builder builder = Palette.from(bitmap);
            builder.generate(new Palette.PaletteAsyncListener() {
                @Override
                public void onGenerated(Palette palette) {
                    int dominantColor = palette.getDominantColor(0);
                    mMusicView.setBackgroundColor(dominantColor);
                }
            });
            drawable = new BitmapDrawable(getResources(), bitmap);
        } else {
            mMusicView.setBackgroundColor(ContextCompat.getColor(getContext(),
                    R.color.media2_widget_music_view_default_background));
        }
        return drawable;
    }

    private String getString(
            @NonNull androidx.media2.common.MediaMetadata metadata,
            String stringKey,
            String defaultValue) {
        String value = (metadata == null) ? defaultValue : metadata.getString(stringKey);
        return value == null ? defaultValue : value;
    }

    class PlayerCallback extends PlayerWrapper.PlayerCallback {
        @Override
        void onConnected(@NonNull PlayerWrapper player) {
            if (DEBUG) {
                Log.d(TAG, "onConnected()");
            }
            if (shouldIgnoreCallback(player)) return;
            if (VideoView.this.isAggregatedVisible()) {
                mTargetView.assignSurfaceToPlayerWrapper(mPlayer);
            }
        }

        @Override
        void onVideoSizeChanged(
                @NonNull PlayerWrapper player,
                @NonNull androidx.media2.common.VideoSize videoSize) {
            if (DEBUG) {
                Log.d(TAG, "onandroidx.media2.common.VideoSizeChanged(): size: " + videoSize);
            }
            if (shouldIgnoreCallback(player)) return;
            if (mVideoTrackCount == 0 && videoSize.getHeight() > 0 && videoSize.getWidth() > 0) {
                if (isMediaPrepared()) {
                    List<androidx.media2.common.SessionPlayer.TrackInfo> trackInfos =
                            player.getTracks();
                    if (trackInfos != null) {
                        updateTracks(player, trackInfos);
                    }
                }
            }
            mTextureView.forceLayout();
            mSurfaceView.forceLayout();
            requestLayout();
        }

        @Override
        void onSubtitleData(
                @NonNull PlayerWrapper player,
                @NonNull androidx.media2.common.MediaItem item,
                @NonNull androidx.media2.common.SessionPlayer.TrackInfo track,
                @NonNull androidx.media2.common.SubtitleData data) {
            if (DEBUG) {
                Log.d(
                        TAG,
                        "onandroidx.media2.common.SubtitleData():"
                                + " androidx.media2.common.SessionPlayer.TrackInfo: "
                                + track
                                + ", getCurrentPosition: "
                                + player.getCurrentPosition()
                                + ", getStartTimeUs(): "
                                + data.getStartTimeUs()
                                + ", diff: "
                                + (data.getStartTimeUs() / 1000 - player.getCurrentPosition())
                                + "ms, getDurationUs(): "
                                + data.getDurationUs());
            }
            if (shouldIgnoreCallback(player)) return;
            if (!track.equals(mSelectedSubtitleTrackInfo)) {
                return;
            }
            SubtitleTrack subtitleTrack = mSubtitleTracks.get(track);
            if (subtitleTrack != null) {
                subtitleTrack.onData(data);
            }
        }

        @Override
        void onPlayerStateChanged(@NonNull PlayerWrapper player, int state) {
            if (DEBUG) {
                Log.d(TAG, "onPlayerStateChanged(): state: " + state);
            }
            if (shouldIgnoreCallback(player)) return;
            if (state == androidx.media2.common.SessionPlayer.PLAYER_STATE_ERROR) {
                // TODO: Show error state (b/123498635)
            }
        }

        @Override
        void onCurrentMediaItemChanged(
                @NonNull PlayerWrapper player, @Nullable androidx.media2.common.MediaItem item) {
            if (DEBUG) {
                Log.d(
                        TAG,
                        "onCurrentMediaItemChanged():"
                                + " androidx.media2.common.MediaItem: "
                                + item);
            }
            if (shouldIgnoreCallback(player)) return;

            updateMusicView(item);
        }

        @Override
        void onTracksChanged(
                @NonNull PlayerWrapper player,
                @NonNull List<androidx.media2.common.SessionPlayer.TrackInfo> tracks) {
            if (DEBUG) {
                Log.d(
                        TAG,
                        "onandroidx.media2.common.SessionPlayer.TrackInfoChanged(): tracks: "
                                + tracks);
            }
            if (shouldIgnoreCallback(player)) return;
            updateTracks(player, tracks);
            updateMusicView(player.getCurrentMediaItem());
        }

        @Override
        void onTrackSelected(
                @NonNull PlayerWrapper player,
                @NonNull androidx.media2.common.SessionPlayer.TrackInfo trackInfo) {
            if (DEBUG) {
                Log.d(TAG, "onTrackSelected(): selected track: " + trackInfo);
            }
            if (shouldIgnoreCallback(player)) return;
            SubtitleTrack subtitleTrack = mSubtitleTracks.get(trackInfo);
            if (subtitleTrack != null) {
                mSubtitleController.selectTrack(subtitleTrack);
            }
        }

        @Override
        void onTrackDeselected(
                @NonNull PlayerWrapper player,
                @NonNull androidx.media2.common.SessionPlayer.TrackInfo trackInfo) {
            if (DEBUG) {
                Log.d(TAG, "onTrackDeselected(): deselected track: " + trackInfo);
            }
            if (shouldIgnoreCallback(player)) return;
            SubtitleTrack subtitleTrack = mSubtitleTracks.get(trackInfo);
            if (subtitleTrack != null) {
                mSubtitleController.selectTrack(null);
            }
        }

        private boolean shouldIgnoreCallback(@NonNull PlayerWrapper player) {
            if (player != mPlayer) {
                if (DEBUG) {
                    try {
                        final String methodName =
                                new Throwable().getStackTrace()[1].getMethodName();
                        Log.w(TAG, methodName + " should be ignored. player is already gone.");
                    } catch (IndexOutOfBoundsException e) {
                        Log.w(TAG, "A PlayerCallback should be ignored. player is already gone.");
                    }
                }
                return true;
            }
            return false;
        }
    }

    /**
     * Interface definition of a callback to be invoked when the view type has been changed.
     *
     * @deprecated androidx.media2 is deprecated. Please migrate to <a
     *     href="https://developer.android.com/guide/topics/media/media3">androidx.media3</a>.
     */
    @Deprecated
    public interface OnViewTypeChangedListener {
        /**
         * Called when the view type has been changed.
         * @see #setViewType(int)
         * @param view the View whose view type is changed
         * @param viewType
         * <ul>
         * <li>{@link #VIEW_TYPE_SURFACEVIEW}
         * <li>{@link #VIEW_TYPE_TEXTUREVIEW}
         * </ul>
         */
        void onViewTypeChanged(@NonNull View view, @ViewType int viewType);
    }
}