VideoViewImplBase.java

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

package androidx.media2.widget;

import static androidx.media2.MediaSession.SessionResult.RESULT_CODE_INVALID_STATE;
import static androidx.media2.MediaSession.SessionResult.RESULT_CODE_SUCCESS;

import android.annotation.SuppressLint;
import android.content.Context;
import android.content.pm.ActivityInfo;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Point;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.media.MediaMetadataRetriever;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Bundle;
import android.util.AttributeSet;
import android.util.DisplayMetrics;
import android.util.Log;
import android.util.SparseArray;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup.LayoutParams;
import android.view.WindowManager;
import android.widget.ImageView;
import android.widget.TextView;

import androidx.annotation.GuardedBy;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.core.content.ContextCompat;
import androidx.media.AudioAttributesCompat;
import androidx.media2.FileMediaItem;
import androidx.media2.MediaItem;
import androidx.media2.MediaMetadata;
import androidx.media2.MediaPlayer;
import androidx.media2.MediaPlayer2;
import androidx.media2.MediaSession;
import androidx.media2.RemoteSessionPlayer;
import androidx.media2.SessionCommand;
import androidx.media2.SessionCommandGroup;
import androidx.media2.SessionPlayer;
import androidx.media2.SessionToken;
import androidx.media2.SubtitleData;
import androidx.media2.UriMediaItem;
import androidx.media2.VideoSize;
import androidx.media2.subtitle.Cea708CaptionRenderer;
import androidx.media2.subtitle.ClosedCaptionRenderer;
import androidx.media2.subtitle.SubtitleController;
import androidx.media2.subtitle.SubtitleTrack;
import androidx.mediarouter.media.MediaControlIntent;
import androidx.mediarouter.media.MediaRouteSelector;
import androidx.mediarouter.media.MediaRouter;
import androidx.palette.graphics.Palette;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Executor;

/**
 * Base implementation of VideoView.
 */
@RequiresApi(19)
class VideoViewImplBase implements VideoViewImpl, VideoViewInterface.SurfaceListener {
    private static final String TAG = "VideoViewImplBase";
    static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);

    private static final int STATE_ERROR = -1;
    private static final int STATE_IDLE = 0;
    private static final int STATE_PREPARING = 1;
    private static final int STATE_PREPARED = 2;
    private static final int STATE_PLAYING = 3;
    private static final int STATE_PAUSED = 4;
    private static final int STATE_PLAYBACK_COMPLETED = 5;

    private static final int INVALID_TRACK_INDEX = -1;
    private static final int SIZE_TYPE_EMBEDDED = 0;
    private static final int SIZE_TYPE_FULL = 1;

    private static final String SUBTITLE_TRACK_LANG_UNDEFINED = "und";

    private AudioAttributesCompat mAudioAttributes;

    private VideoView.OnViewTypeChangedListener mViewTypeChangedListener;

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

    VideoViewPlayer mMediaPlayer;
    MediaItem mMediaItem;
    MediaControlView mMediaControlView;
    MediaSession mMediaSession;
    private String mTitle;
    Executor mCallbackExecutor;

    View mCurrentMusicView;
    View mMusicFullLandscapeView;
    View mMusicFullPortraitView;
    View mMusicEmbeddedView;
    private Drawable mMusicAlbumDrawable;
    private String mMusicArtistText;

    final Object mLock = new Object();
    @GuardedBy("mLock")
    boolean mCurrentItemIsMusic;

    private int mPrevWidth;
    int mDominantColor;
    private int mSizeType;

    int mTargetState = STATE_IDLE;
    int mCurrentState = STATE_IDLE;
    long mSeekWhenPrepared;  // recording the seek position while preparing

    private ArrayList<Integer> mVideoTrackIndices;
    ArrayList<Integer> mAudioTrackIndices;
    SparseArray<SubtitleTrack> mSubtitleTracks;
    private SubtitleController mSubtitleController;

    // selected audio/subtitle track index as MediaPlayer returns
    int mSelectedAudioTrackIndex;
    int mSelectedSubtitleTrackIndex;

    private SubtitleAnchorView mSubtitleAnchorView;

    VideoView mInstance;

    private MediaRouter mMediaRouter;
    @SuppressWarnings("WeakerAccess") /* synthetic access */
    MediaRouteSelector mRouteSelector;
    MediaRouter.RouteInfo mRoute;
    RoutePlayer mRoutePlayer;

    private final MediaRouter.Callback mRouterCallback = new MediaRouter.Callback() {
        @Override
        public void onRouteSelected(MediaRouter router, MediaRouter.RouteInfo route) {
            if (route.supportsControlCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK)) {
                // Save local playback state and position
                int localPlaybackState = mCurrentState;
                long localPlaybackPosition = (mMediaSession == null)
                        ? 0 : mMediaSession.getPlayer().getCurrentPosition();

                // Update player
                resetPlayer();
                mRoute = route;
                mRoutePlayer = new RoutePlayer(mInstance.getContext(), mRouteSelector, route);
                // TODO: Replace with MediaSession#setPlaylist once b/110811730 is fixed.
                mRoutePlayer.setMediaItem(mMediaItem);
                mRoutePlayer.setCurrentPosition(localPlaybackPosition);
                ensureSessionWithPlayer(mRoutePlayer);
                if (localPlaybackState == STATE_PLAYING) {
                    mMediaSession.getPlayer().play();
                }
            }
        }

        @Override
        public void onRouteUnselected(MediaRouter router, MediaRouter.RouteInfo route, int reason) {
            long currentPosition = 0;
            int currentState = 0;
            if (mRoute != null && mRoutePlayer != null) {
                currentPosition = mRoutePlayer.getCurrentPosition();
                currentState = mRoutePlayer.getPlayerState();
                mRoutePlayer.close();
                mRoutePlayer = null;
            }
            if (mRoute == route) {
                mRoute = null;
            }
            if (reason != MediaRouter.UNSELECT_REASON_ROUTE_CHANGED) {
                openVideo();
                mMediaSession.getPlayer().seekTo(currentPosition);
                if (currentState == SessionPlayer.PLAYER_STATE_PLAYING) {
                    mMediaSession.getPlayer().play();
                }
            }
        }
    };

    @Override
    public void initialize(
            VideoView instance, Context context,
            @Nullable AttributeSet attrs, int defStyleAttr) {
        mInstance = instance;

        mSelectedSubtitleTrackIndex = INVALID_TRACK_INDEX;

        mAudioAttributes = new AudioAttributesCompat.Builder()
                .setUsage(AudioAttributesCompat.USAGE_MEDIA)
                .setContentType(AudioAttributesCompat.CONTENT_TYPE_MOVIE).build();

        mCallbackExecutor = ContextCompat.getMainExecutor(context);

        mInstance.setFocusable(true);
        mInstance.setFocusableInTouchMode(true);
        mInstance.requestFocus();

        mTextureView = new VideoTextureView(context);
        mSurfaceView = new VideoSurfaceView(context);
        LayoutParams params = new LayoutParams(LayoutParams.MATCH_PARENT,
                LayoutParams.MATCH_PARENT);
        mTextureView.setLayoutParams(params);
        mSurfaceView.setLayoutParams(params);
        mTextureView.setSurfaceListener(this);
        mSurfaceView.setSurfaceListener(this);

        mInstance.addView(mTextureView);
        mInstance.addView(mSurfaceView);

        mSubtitleAnchorView = new SubtitleAnchorView(context);
        mSubtitleAnchorView.setLayoutParams(params);
        mSubtitleAnchorView.setBackgroundColor(0);
        mInstance.addView(mSubtitleAnchorView);

        LayoutInflater inflater = (LayoutInflater) mInstance.getContext()
                .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
        mMusicFullLandscapeView = inflater.inflate(R.layout.full_landscape_music, null);
        mMusicFullPortraitView = inflater.inflate(R.layout.full_portrait_music, null);
        mMusicEmbeddedView = inflater.inflate(R.layout.embedded_music, null);

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

        // 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;

        MediaRouteSelector.Builder builder = new MediaRouteSelector.Builder();
        builder.addControlCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK);
        builder.addControlCategory(MediaControlIntent.CATEGORY_LIVE_AUDIO);
        builder.addControlCategory(MediaControlIntent.CATEGORY_LIVE_VIDEO);
        mRouteSelector = builder.build();
    }

    /**
     * Sets MediaControlView instance. It will replace the previously assigned MediaControlView
     * instance if any.
     *
     * @param mediaControlView a media control view2 instance.
     * @param intervalMs a time interval in milliseconds until VideoView hides MediaControlView.
     */
    @Override
    public void setMediaControlView(@NonNull MediaControlView mediaControlView, long intervalMs) {
        mMediaControlView = mediaControlView;
        mMediaControlView.setShowControllerInterval(intervalMs);

        if (mInstance.isAttachedToWindow()) {
            attachMediaControlView();
        }
    }

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

    /**
     * Returns {@link SessionToken} so that developers create their own
     * {@link androidx.media2.MediaController} instance. This method should be called when
     * VideoView is attached to window or after {@link #setMediaItem} is called.
     *
     * @throws IllegalStateException if internal MediaSession is not created yet.
     */
    @Override
    @NonNull
    public SessionToken getSessionToken() {
        if (mMediaSession == null) {
            throw new IllegalStateException("MediaSession instance is not available.");
        }
        return mMediaSession.getToken();
    }

    /**
     * Sets the {@link AudioAttributesCompat} to be used during the playback of the video.
     *
     * @param attributes non-null <code>AudioAttributesCompat</code>.
     */
    @Override
    public void setAudioAttributes(@NonNull AudioAttributesCompat attributes) {
        if (attributes == null) {
            throw new IllegalArgumentException("Illegal null AudioAttributes");
        }
        mAudioAttributes = attributes;
    }

    /**
     * Sets {@link MediaItem} object to render using VideoView.
     * @param mediaItem the MediaItem to play
     */
    @Override
    public void setMediaItem(@NonNull MediaItem mediaItem) {
        mSeekWhenPrepared = 0;
        mMediaItem = mediaItem;
        openVideo();
    }

    /**
     * Selects which view will be used to render video between SurfaceView and TextureView.
     *
     * @param viewType the view type to render video
     * <ul>
     * <li>{@link VideoView#VIEW_TYPE_SURFACEVIEW}
     * <li>{@link VideoView#VIEW_TYPE_TEXTUREVIEW}
     * </ul>
     */
    @Override
    public void setViewType(@VideoView.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;
        ((View) targetView).setVisibility(View.VISIBLE);
        targetView.takeOver();
        mInstance.requestLayout();
    }

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

    /**
     * Registers a callback to be invoked when a view type change is done.
     * {@see #setViewType(int)}
     * @param l The callback that will be run
     */
    @Override
    public void setOnViewTypeChangedListener(VideoView.OnViewTypeChangedListener l) {
        mViewTypeChangedListener = l;
    }

    @Override
    public void onAttachedToWindowImpl() {
        // Note: MediaPlayer2 and MediaSession instances are created in onAttachedToWindow()
        // and closed in onDetachedFromWindow().
        if (mMediaPlayer == null) {
            mMediaPlayer = new VideoViewPlayer(mInstance.getContext());

            mSurfaceView.setMediaPlayer(mMediaPlayer);
            mTextureView.setMediaPlayer(mMediaPlayer);
            mCurrentView.assignSurfaceToMediaPlayer(mMediaPlayer);

            if (mMediaSession != null) {
                mMediaSession.updatePlayer(mMediaPlayer);
            }
        } else {
            if (!mCurrentView.assignSurfaceToMediaPlayer(mMediaPlayer)) {
                Log.w(TAG, "failed to assign surface");
            }
        }

        ensureSessionWithPlayer(mMediaPlayer);

        attachMediaControlView();
        mMediaRouter = MediaRouter.getInstance(mInstance.getContext());
        // TODO: Revisit once ag/4207152 is merged.
        mMediaRouter.setMediaSessionCompat(mMediaSession.getSessionCompat());
        mMediaRouter.addCallback(mRouteSelector, mRouterCallback,
                MediaRouter.CALLBACK_FLAG_PERFORM_ACTIVE_SCAN);
    }

    @Override
    public void onDetachedFromWindowImpl() {
        try {
            mMediaPlayer.close();
        } catch (Exception e) {
        }
        mMediaSession.close();
        mMediaPlayer = null;
        mMediaSession = null;
    }

    @Override
    public void onVisibilityAggregatedImpl(boolean isVisible) {
        if (isMediaPrepared()) {
            if (!isVisible && mCurrentState == STATE_PLAYING) {
                mMediaSession.getPlayer().pause();
            } else if (isVisible && mTargetState == STATE_PLAYING) {
                mMediaSession.getPlayer().play();
            }
        }
    }

    @Override
    public void onTouchEventImpl(MotionEvent ev) {
        if (DEBUG) {
            Log.d(TAG, "onTouchEvent(). mCurrentState=" + mCurrentState
                    + ", mTargetState=" + mTargetState);
        }
    }

    @Override
    public void onTrackballEventImpl(MotionEvent ev) {
        if (DEBUG) {
            Log.d(TAG, "onTrackBallEvent(). mCurrentState=" + mCurrentState
                    + ", mTargetState=" + mTargetState);
        }
    }

    @Override
    public void onMeasureImpl(int widthMeasureSpec, int heightMeasureSpec) {
        synchronized (mLock) {
            if (mCurrentItemIsMusic) {
                int currWidth = mInstance.getMeasuredWidth();
                if (mPrevWidth != currWidth) {
                    Point screenSize = new Point();
                    WindowManager winManager = (WindowManager) mInstance.getContext()
                            .getApplicationContext().getSystemService(Context.WINDOW_SERVICE);
                    winManager.getDefaultDisplay().getSize(screenSize);
                    int screenWidth = screenSize.x;
                    if (currWidth == screenWidth) {
                        int orientation = retrieveOrientation();
                        if (orientation == ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE) {
                            updateCurrentMusicView(mMusicFullLandscapeView);
                        } else {
                            updateCurrentMusicView(mMusicFullPortraitView);
                        }

                        if (mSizeType != SIZE_TYPE_FULL) {
                            mSizeType = SIZE_TYPE_FULL;
                        }
                    } else {
                        if (mSizeType != SIZE_TYPE_EMBEDDED) {
                            mSizeType = SIZE_TYPE_EMBEDDED;
                            updateCurrentMusicView(mMusicEmbeddedView);
                        }
                    }
                    mPrevWidth = currWidth;
                }
            }
        }
    }

    ///////////////////////////////////////////////////
    // Implements VideoViewInterface.SurfaceListener
    ///////////////////////////////////////////////////

    @Override
    public void onSurfaceCreated(View view, int width, int height) {
        if (DEBUG) {
            Log.d(TAG, "onSurfaceCreated(). mCurrentState=" + mCurrentState
                    + ", mTargetState=" + mTargetState + ", width/height: " + width + "/" + height
                    + ", " + view.toString());
        }
        if (view == mTargetView) {
            ((VideoViewInterface) view).takeOver();
        }
        if (needToStart()) {
            mMediaSession.getPlayer().play();
        }
    }

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

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

    @Override
    public void onSurfaceTakeOverDone(VideoViewInterface view) {
        if (view != mTargetView) {
            if (DEBUG) {
                Log.d(TAG, "onSurfaceTakeOverDone(). view is not targetView. ignore.: " + view);
            }
            return;
        }
        if (DEBUG) {
            Log.d(TAG, "onSurfaceTakeOverDone(). Now current view is: " + view);
        }
        if (mCurrentState != STATE_PLAYING) {
            mMediaSession.getPlayer().seekTo(mMediaSession.getPlayer().getCurrentPosition());
        }
        if (view != mCurrentView) {
            ((View) mCurrentView).setVisibility(View.GONE);
            mCurrentView = view;
            if (mViewTypeChangedListener != null) {
                mViewTypeChangedListener.onViewTypeChanged(mInstance, view.getViewType());
            }
        }

        if (needToStart()) {
            mMediaSession.getPlayer().play();
        }
    }

    ///////////////////////////////////////////////////
    // Protected or private methods
    ///////////////////////////////////////////////////
    private void attachMediaControlView() {
        // Get MediaController from MediaSession and set it inside MediaControlView
        mMediaControlView.setSessionToken(mMediaSession.getToken());

        LayoutParams params =
                new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
        mInstance.addView(mMediaControlView, params);
    }

    void ensureSessionWithPlayer(SessionPlayer player) {
        if (mMediaSession != null) {
            SessionPlayer oldPlayer = mMediaSession.getPlayer();
            if (oldPlayer == player) {
                return;
            }
            oldPlayer.unregisterPlayerCallback(mMediaPlayerCallback);
            mMediaSession.updatePlayer(player);
        } else {
            final Context context = mInstance.getContext();
            mMediaSession = new MediaSession.Builder(context, player)
                    .setId("VideoView_" + mInstance.toString())
                    .setSessionCallback(mCallbackExecutor, new MediaSessionCallback())
                    .build();
        }
        player.registerPlayerCallback(mCallbackExecutor, mMediaPlayerCallback);
    }

    private boolean isMediaPrepared() {
        return mMediaSession != null
                && mMediaSession.getPlayer().getPlayerState() != SessionPlayer.PLAYER_STATE_ERROR
                && mMediaSession.getPlayer().getPlayerState() != SessionPlayer.PLAYER_STATE_IDLE;
    }

    boolean needToStart() {
        return (mMediaPlayer != null || mRoutePlayer != null) && isWaitingPlayback();
    }

    private boolean isWaitingPlayback() {
        return mCurrentState != STATE_PLAYING && mTargetState == STATE_PLAYING;
    }

    // Creates a MediaPlayer instance and prepare media item.
    void openVideo() {
        if (DEBUG) {
            Log.d(TAG, "openVideo()");
        }
        if (mMediaItem != null) {
            resetPlayer();
            if (isRemotePlayback()) {
                mRoutePlayer.setMediaItem(mMediaItem);
                return;
            }
        }

        try {
            if (mMediaPlayer == null) {
                mMediaPlayer = new VideoViewPlayer(mInstance.getContext());
            }
            mSurfaceView.setMediaPlayer(mMediaPlayer);
            mTextureView.setMediaPlayer(mMediaPlayer);
            if (!mCurrentView.assignSurfaceToMediaPlayer(mMediaPlayer)) {
                Log.w(TAG, "failed to assign surface");
            }
            mMediaPlayer.setAudioAttributes(mAudioAttributes);

            ensureSessionWithPlayer(mMediaPlayer);
            mMediaPlayer.setMediaItem(mMediaItem);

            final Context context = mInstance.getContext();
            mSubtitleController = new SubtitleController(context);
            mSubtitleController.registerRenderer(new ClosedCaptionRenderer(context));
            mSubtitleController.registerRenderer(new Cea708CaptionRenderer(context));
            mSubtitleController.setAnchor(mSubtitleAnchorView);

            // we don't set the target state here either, but preserve the
            // target state that was there before.
            mCurrentState = STATE_PREPARING;
            mMediaSession.getPlayer().prepare();
        } catch (IllegalArgumentException ex) {
            Log.w(TAG, "Unable to open content: " + mMediaItem, ex);
            mCurrentState = STATE_ERROR;
            mTargetState = STATE_ERROR;
        }
    }

    /*
     * Reset the media player in any state
     */
    void resetPlayer() {
        if (mMediaPlayer != null) {
            mMediaPlayer.reset();
            mTextureView.setMediaPlayer(null);
            mSurfaceView.setMediaPlayer(null);
            mCurrentState = STATE_IDLE;
            mTargetState = STATE_IDLE;
            mSelectedSubtitleTrackIndex = INVALID_TRACK_INDEX;
            mSelectedAudioTrackIndex = INVALID_TRACK_INDEX;
        }
    }

    boolean isRemotePlayback() {
        return mRoutePlayer != null
                && mMediaSession != null
                && (mMediaSession.getPlayer() instanceof RemoteSessionPlayer);
    }

    void selectSubtitleTrack(int trackIndex) {
        if (!isMediaPrepared()) {
            return;
        }
        SubtitleTrack track = mSubtitleTracks.get(trackIndex);
        if (track != null) {
            mMediaPlayer.selectTrack(trackIndex);
            mSubtitleController.selectTrack(track);
            mSelectedSubtitleTrackIndex = trackIndex;
            mSubtitleAnchorView.setVisibility(View.VISIBLE);

            Bundle data = new Bundle();
            data.putInt(MediaControlView.KEY_SELECTED_SUBTITLE_INDEX,
                    mSubtitleTracks.indexOfKey(trackIndex));
            mMediaSession.broadcastCustomCommand(
                    new SessionCommand(MediaControlView.EVENT_UPDATE_SUBTITLE_SELECTED, null),
                    data);
        }
    }

    void deselectSubtitleTrack() {
        if (!isMediaPrepared() || mSelectedSubtitleTrackIndex == INVALID_TRACK_INDEX) {
            return;
        }
        mMediaPlayer.deselectTrack(mSelectedSubtitleTrackIndex);
        mSelectedSubtitleTrackIndex = INVALID_TRACK_INDEX;
        mSubtitleAnchorView.setVisibility(View.GONE);

        mMediaSession.broadcastCustomCommand(
                new SessionCommand(MediaControlView.EVENT_UPDATE_SUBTITLE_DESELECTED, null),
                null);
    }

    // TODO: move this method inside callback to make sure it runs inside the callback thread.
    Bundle extractTrackInfoData() {
        List<MediaPlayer.TrackInfo> trackInfos = mMediaPlayer.getTrackInfo();
        mVideoTrackIndices = new ArrayList<>();
        mAudioTrackIndices = new ArrayList<>();
        mSubtitleTracks = new SparseArray<>();
        ArrayList<String> subtitleTracksLanguageList = new ArrayList<>();
        mSubtitleController.reset();
        for (int i = 0; i < trackInfos.size(); ++i) {
            int trackType = trackInfos.get(i).getTrackType();
            if (trackType == MediaPlayer2.TrackInfo.MEDIA_TRACK_TYPE_VIDEO) {
                mVideoTrackIndices.add(i);
            } else if (trackType == MediaPlayer2.TrackInfo.MEDIA_TRACK_TYPE_AUDIO) {
                mAudioTrackIndices.add(i);
            } else if (trackType == MediaPlayer2.TrackInfo.MEDIA_TRACK_TYPE_SUBTITLE) {
                SubtitleTrack track = mSubtitleController.addTrack(trackInfos.get(i).getFormat());
                if (track != null) {
                    mSubtitleTracks.put(i, track);
                    String language =
                            (trackInfos.get(i).getLanguage().equals(SUBTITLE_TRACK_LANG_UNDEFINED))
                                    ? "" : trackInfos.get(i).getLanguage();
                    subtitleTracksLanguageList.add(language);
                }
            }
        }
        // Select first tracks as default
        if (mAudioTrackIndices.size() > 0) {
            mSelectedAudioTrackIndex = 0;
        }

        synchronized (mLock) {
            mCurrentItemIsMusic = mVideoTrackIndices.size() == 0 && mAudioTrackIndices.size() > 0;
        }

        Bundle data = new Bundle();
        data.putInt(MediaControlView.KEY_VIDEO_TRACK_COUNT, mVideoTrackIndices.size());
        data.putInt(MediaControlView.KEY_AUDIO_TRACK_COUNT, mAudioTrackIndices.size());
        data.putInt(MediaControlView.KEY_SUBTITLE_TRACK_COUNT, mSubtitleTracks.size());
        data.putStringArrayList(MediaControlView.KEY_SUBTITLE_TRACK_LANGUAGE_LIST,
                subtitleTracksLanguageList);
        return data;
    }

    // TODO: move this method inside callback to make sure it runs inside the callback thread.
    MediaMetadata extractMetadata() {
        MediaMetadataRetriever retriever = null;
        String path = "";
        try {
            if (mMediaItem == null) {
                return null;
            } else if (mMediaItem instanceof UriMediaItem) {
                Uri uri = ((UriMediaItem) mMediaItem).getUri();

                // Save file name as title since the file may not have a title Metadata.
                if (UriUtil.isFromNetwork(uri)) {
                    path = uri.getPath();
                } else if ("file".equals(uri.getScheme())) {
                    path = uri.getLastPathSegment();
                } else {
                    // TODO: needs default title. b/120515913
                }
                retriever = new MediaMetadataRetriever();
                retriever.setDataSource(mInstance.getContext(), uri);
            } else if (mMediaItem instanceof FileMediaItem) {
                retriever = new MediaMetadataRetriever();
                retriever.setDataSource(
                        ((FileMediaItem) mMediaItem).getFileDescriptor(),
                        ((FileMediaItem) mMediaItem).getFileDescriptorOffset(),
                        ((FileMediaItem) mMediaItem).getFileDescriptorLength());
            }
        } catch (IllegalArgumentException e) {
            Log.v(TAG, "Cannot retrieve metadata for this media file.");
            retriever = null;
        }

        MediaMetadata metadata = mMediaItem.getMetadata();

        synchronized (mLock) {
            if (!mCurrentItemIsMusic) {
                mTitle = extractString(metadata,
                        MediaMetadata.METADATA_KEY_TITLE, retriever,
                        MediaMetadataRetriever.METADATA_KEY_TITLE, path);
            } else {
                Resources resources = mInstance.getResources();
                mTitle = extractString(metadata,
                        MediaMetadata.METADATA_KEY_TITLE, retriever,
                        MediaMetadataRetriever.METADATA_KEY_TITLE,
                        resources.getString(R.string.mcv2_music_title_unknown_text));
                mMusicArtistText = extractString(metadata,
                        MediaMetadata.METADATA_KEY_ARTIST,
                        retriever,
                        MediaMetadataRetriever.METADATA_KEY_ARTIST,
                        resources.getString(R.string.mcv2_music_artist_unknown_text));
                mMusicAlbumDrawable = extractAlbumArt(metadata, retriever,
                        resources.getDrawable(R.drawable.ic_default_album_image));
            }

            if (retriever != null) {
                retriever.release();
            }

            // Set duration and title values as MediaMetadata for MediaControlView
            MediaMetadata.Builder builder = new MediaMetadata.Builder();

            if (mCurrentItemIsMusic) {
                builder.putString(MediaMetadata.METADATA_KEY_ARTIST, mMusicArtistText);
            }
            builder.putString(MediaMetadata.METADATA_KEY_TITLE, mTitle);
            builder.putLong(
                    MediaMetadata.METADATA_KEY_DURATION, mMediaSession.getPlayer().getDuration());
            builder.putString(
                    MediaMetadata.METADATA_KEY_MEDIA_ID, mMediaItem.getMediaId());
            builder.putLong(MediaMetadata.METADATA_KEY_PLAYABLE, 1);
            return builder.build();
        }
    }

    // TODO: move this method inside callback to make sure it runs inside the callback thread.
    private String extractString(MediaMetadata metadata, String stringKey,
            MediaMetadataRetriever retriever, int intKey, String defaultValue) {
        String value = null;

        if (metadata != null) {
            value = metadata.getString(stringKey);
            if (value != null && !value.isEmpty()) {
                return value;
            }
        }
        if (retriever != null) {
            value = retriever.extractMetadata(intKey);
        }
        return value == null ? defaultValue : value;
    }

    // TODO: move this method inside callback to make sure it runs inside the callback thread.
    private Drawable extractAlbumArt(MediaMetadata metadata, MediaMetadataRetriever retriever,
            Drawable defaultDrawable) {
        Bitmap bitmap = null;

        if (metadata != null && metadata.containsKey(MediaMetadata.METADATA_KEY_ALBUM_ART)) {
            bitmap = metadata.getBitmap(MediaMetadata.METADATA_KEY_ALBUM_ART);
        } else if (retriever != null) {
            byte[] album = retriever.getEmbeddedPicture();
            if (album != null) {
                bitmap = BitmapFactory.decodeByteArray(album, 0, album.length);
            }
        }
        if (bitmap != null) {
            Palette.Builder builder = Palette.from(bitmap);
            builder.generate(new Palette.PaletteAsyncListener() {
                @Override
                public void onGenerated(Palette palette) {
                    mDominantColor = palette.getDominantColor(0);
                    if (mCurrentMusicView != null) {
                        mCurrentMusicView.setBackgroundColor(mDominantColor);
                    }
                }
            });
            return new BitmapDrawable(bitmap);
        }
        return defaultDrawable;
    }

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

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

    void updateCurrentMusicView(View newMusicView) {
        newMusicView.setBackgroundColor(mDominantColor);

        ImageView albumView = newMusicView.findViewById(R.id.album);
        if (albumView != null) {
            albumView.setImageDrawable(mMusicAlbumDrawable);
        }

        TextView titleView = newMusicView.findViewById(R.id.title);
        if (titleView != null) {
            titleView.setText(mTitle);
        }

        TextView artistView = newMusicView.findViewById(R.id.artist);
        if (artistView != null) {
            artistView.setText(mMusicArtistText);
        }

        mInstance.removeView(mCurrentMusicView);
        mInstance.addView(newMusicView, 0);
        mCurrentMusicView = newMusicView;
    }

    @SuppressLint("SyntheticAccessor")
    MediaPlayer.PlayerCallback mMediaPlayerCallback =
            new MediaPlayer.PlayerCallback() {
                @Override
                public void onVideoSizeChanged(
                        MediaPlayer mp, MediaItem dsd, VideoSize size) {
                    if (DEBUG) {
                        Log.d(TAG, "onVideoSizeChanged(): size: " + size.getWidth() + "/"
                                + size.getHeight());
                    }
                    if (mp != mMediaPlayer) {
                        if (DEBUG) {
                            Log.w(TAG, "onVideoSizeChanged() is ignored. mp is already gone.");
                        }
                        return;
                    }
                    mTextureView.forceLayout();
                    mSurfaceView.forceLayout();
                    mInstance.requestLayout();
                }

                @Override
                public void onInfo(
                        MediaPlayer mp, MediaItem dsd, int what, int extra) {
                    if (DEBUG) {
                        Log.d(TAG, "onInfo()");
                    }
                    if (mp != mMediaPlayer) {
                        if (DEBUG) {
                            Log.w(TAG, "onInfo() is ignored. mp is already gone.");
                        }
                        return;
                    }
                    if (what == MediaPlayer2.MEDIA_INFO_METADATA_UPDATE) {
                        Bundle data = extractTrackInfoData();
                        if (data != null) {
                            mMediaSession.broadcastCustomCommand(
                                    new SessionCommand(MediaControlView.EVENT_UPDATE_TRACK_STATUS,
                                            null), data);
                        }
                    }
                }

                @Override
                public void onError(
                        MediaPlayer mp, MediaItem dsd, int frameworkErr, int implErr) {
                    if (DEBUG) {
                        Log.d(TAG, "Error: " + frameworkErr + "," + implErr);
                    }
                    if (mp != mMediaPlayer) {
                        if (DEBUG) {
                            Log.w(TAG, "onError() is ignored. mp is already gone.");
                        }
                        return;
                    }
                    if (mCurrentState != STATE_ERROR) {
                        mCurrentState = STATE_ERROR;
                        mTargetState = STATE_ERROR;
                    }
                }

                @Override
                public void onSubtitleData(
                        MediaPlayer mp, MediaItem dsd, SubtitleData data) {
                    if (DEBUG) {
                        Log.d(TAG, "onSubtitleData(): getTrackIndex: " + data.getTrackIndex()
                                + ", getCurrentPosition: " + mp.getCurrentPosition()
                                + ", getStartTimeUs(): " + data.getStartTimeUs()
                                + ", diff: "
                                + (data.getStartTimeUs() / 1000 - mp.getCurrentPosition())
                                + "ms, getDurationUs(): " + data.getDurationUs());
                    }
                    if (mp != mMediaPlayer) {
                        if (DEBUG) {
                            Log.w(TAG, "onSubtitleData() is ignored. mp is already gone.");
                        }
                        return;
                    }
                    final int index = data.getTrackIndex();
                    if (index != mSelectedSubtitleTrackIndex) {
                        return;
                    }
                    SubtitleTrack track = mSubtitleTracks.get(index);
                    if (track != null) {
                        track.onData(data);
                    }
                }

                @Override
                public void onPlayerStateChanged(@NonNull SessionPlayer player,
                        @SessionPlayer.PlayerState int state) {
                    switch (state) {
                        case SessionPlayer.PLAYER_STATE_IDLE:
                            mCurrentState = STATE_IDLE;
                            break;
                        case SessionPlayer.PLAYER_STATE_PLAYING:
                            mCurrentState = STATE_PLAYING;
                            break;
                        case SessionPlayer.PLAYER_STATE_PAUSED:
                            if (mCurrentState == STATE_PREPARING) {
                                onPrepared(player);
                            }
                            mCurrentState = STATE_PAUSED;
                            break;
                        case SessionPlayer.PLAYER_STATE_ERROR:
                            mCurrentState = STATE_ERROR;
                            break;
                    }
                }

                private void onPrepared(SessionPlayer player) {
                    if (DEBUG) {
                        Log.d(TAG, "OnPreparedListener(): "
                                + ", mCurrentState=" + mCurrentState
                                + ", mTargetState=" + mTargetState);
                    }
                    mCurrentState = STATE_PREPARED;

                    if (mMediaSession != null) {
                        Bundle data = extractTrackInfoData();
                        if (data != null) {
                            mMediaSession.broadcastCustomCommand(
                                    new SessionCommand(MediaControlView.EVENT_UPDATE_TRACK_STATUS,
                                            null), data);
                        }

                        // Run extractMetadata() in another thread to prevent StrictMode violation.
                        // extractMetadata() contains file IO indirectly,
                        // via MediaMetadataRetriever.
                        MetadataExtractTask task = new MetadataExtractTask();
                        task.execute();
                    }

                    if (mMediaControlView != null) {
                        mMediaControlView.setEnabled(true);

                        Uri uri = (mMediaItem instanceof UriMediaItem)
                                ? ((UriMediaItem) mMediaItem).getUri() : null;
                        if (uri != null && UriUtil.isFromNetwork(uri)) {
                            mMediaControlView.setRouteSelector(mRouteSelector);
                        } else {
                            mMediaControlView.setRouteSelector(null);
                        }
                    }

                    // mSeekWhenPrepared may be changed after seekTo() call
                    long seekToPosition = mSeekWhenPrepared;
                    if (seekToPosition != 0) {
                        mMediaSession.getPlayer().seekTo(seekToPosition);
                    }

                    if (player instanceof VideoViewPlayer) {
                        if (needToStart()) {
                            mMediaSession.getPlayer().play();
                        }
                    }
                }

                private void onCompletion(MediaPlayer mp, MediaItem dsd) {
                    mCurrentState = STATE_PLAYBACK_COMPLETED;
                    mTargetState = STATE_PLAYBACK_COMPLETED;
                }
            };

    class MediaSessionCallback extends MediaSession.SessionCallback {
        @Override
        public SessionCommandGroup onConnect(
                @NonNull MediaSession session,
                @NonNull MediaSession.ControllerInfo controller) {
            if (session != mMediaSession) {
                if (DEBUG) {
                    Log.w(TAG, "onConnect() is ignored. session is already gone.");
                }
            }
            SessionCommandGroup.Builder commandsBuilder = new SessionCommandGroup.Builder()
                    .addCommand(SessionCommand.COMMAND_CODE_PLAYER_PAUSE)
                    .addCommand(SessionCommand.COMMAND_CODE_PLAYER_PLAY)
                    .addCommand(SessionCommand.COMMAND_CODE_PLAYER_PREPARE)
                    .addCommand(SessionCommand.COMMAND_CODE_PLAYER_SET_SPEED)
                    .addCommand(SessionCommand.COMMAND_CODE_SESSION_FAST_FORWARD)
                    .addCommand(SessionCommand.COMMAND_CODE_SESSION_REWIND)
                    .addCommand(SessionCommand.COMMAND_CODE_PLAYER_SEEK_TO)
                    .addCommand(SessionCommand.COMMAND_CODE_VOLUME_SET_VOLUME)
                    .addCommand(SessionCommand.COMMAND_CODE_VOLUME_ADJUST_VOLUME)
                    .addCommand(SessionCommand.COMMAND_CODE_SESSION_PLAY_FROM_URI)
                    .addCommand(SessionCommand.COMMAND_CODE_SESSION_PREPARE_FROM_URI)
                    .addCommand(SessionCommand.COMMAND_CODE_PLAYER_GET_PLAYLIST)
                    .addCommand(SessionCommand.COMMAND_CODE_PLAYER_GET_PLAYLIST_METADATA)
                    .addCommand(new SessionCommand(
                            MediaControlView.COMMAND_SELECT_AUDIO_TRACK, null))
                    .addCommand(new SessionCommand(
                            MediaControlView.COMMAND_SHOW_SUBTITLE, null))
                    .addCommand(new SessionCommand(
                            MediaControlView.COMMAND_HIDE_SUBTITLE, null));
            return commandsBuilder.build();
        }

        @Override
        public MediaSession.SessionResult onCustomCommand(@NonNull MediaSession session,
                @NonNull MediaSession.ControllerInfo controller,
                @NonNull SessionCommand customCommand, @Nullable Bundle args) {
            if (session != mMediaSession) {
                if (DEBUG) {
                    Log.w(TAG, "onCustomCommand() is ignored. session is already gone.");
                }
            }
            if (isRemotePlayback()) {
                // TODO: call mRoutePlayer.onCommand()
                return new MediaSession.SessionResult(RESULT_CODE_SUCCESS, null);
            }
            switch (customCommand.getCustomCommand()) {
                case MediaControlView.COMMAND_SHOW_SUBTITLE:
                    int subtitleIndex = args != null ? args.getInt(
                            MediaControlView.KEY_SELECTED_SUBTITLE_INDEX,
                            INVALID_TRACK_INDEX) : INVALID_TRACK_INDEX;
                    if (subtitleIndex != INVALID_TRACK_INDEX) {
                        int subtitleTrackIndex = mSubtitleTracks.keyAt(subtitleIndex);
                        if (subtitleTrackIndex != mSelectedSubtitleTrackIndex) {
                            selectSubtitleTrack(subtitleTrackIndex);
                        }
                    }
                    break;
                case MediaControlView.COMMAND_HIDE_SUBTITLE:
                    deselectSubtitleTrack();
                    break;
                case MediaControlView.COMMAND_SELECT_AUDIO_TRACK:
                    int audioIndex = (args != null)
                            ? args.getInt(MediaControlView.KEY_SELECTED_AUDIO_INDEX,
                            INVALID_TRACK_INDEX) : INVALID_TRACK_INDEX;
                    if (audioIndex != INVALID_TRACK_INDEX) {
                        int audioTrackIndex = mAudioTrackIndices.get(audioIndex);
                        if (audioTrackIndex != mSelectedAudioTrackIndex) {
                            mSelectedAudioTrackIndex = audioTrackIndex;
                            mMediaPlayer.selectTrack(mSelectedAudioTrackIndex);
                        }
                    }
                    break;
            }
            return new MediaSession.SessionResult(RESULT_CODE_SUCCESS, null);
        }

        @Override
        public int onCommandRequest(@NonNull MediaSession session,
                @NonNull MediaSession.ControllerInfo controller,
                @NonNull SessionCommand command) {
            if (session != mMediaSession) {
                if (DEBUG) {
                    Log.w(TAG, "onCommandRequest() is ignored. session is already gone.");
                }
            }
            switch (command.getCommandCode()) {
                case SessionCommand.COMMAND_CODE_PLAYER_PLAY:
                    mTargetState = STATE_PLAYING;
                    synchronized (mLock) {
                        if (!mCurrentView.hasAvailableSurface() && !mCurrentItemIsMusic) {
                            Log.d(TAG, "surface is not available");
                            return RESULT_CODE_INVALID_STATE;
                        }
                    }
                    break;
                case SessionCommand.COMMAND_CODE_PLAYER_PAUSE:
                    mTargetState = STATE_PAUSED;
                    break;
                case SessionCommand.COMMAND_CODE_PLAYER_SEEK_TO:
                    mSeekWhenPrepared = 0;
                    break;
            }
            return RESULT_CODE_SUCCESS;
        }
    }

    private class MetadataExtractTask extends AsyncTask<Void, Void, MediaMetadata> {
        MetadataExtractTask() {
        }

        @Override
        protected MediaMetadata doInBackground(Void... params) {
            return extractMetadata();
        }

        @Override
        @SuppressLint("SyntheticAccessor")
        protected void onPostExecute(MediaMetadata metadata) {
            if (metadata != null) {
                mMediaItem.setMetadata(metadata);
            }

            synchronized (mLock) {
                if (mCurrentItemIsMusic) {
                    // Update Music View to reflect the new metadata
                    mInstance.removeView(mSurfaceView);
                    mInstance.removeView(mTextureView);
                    updateCurrentMusicView(mMusicEmbeddedView);
                }
            }
        }
    }
}