RoutePlayer.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.annotation.RestrictTo.Scope.LIBRARY_GROUP;
import static androidx.media2.SessionPlayer.PlayerResult.RESULT_CODE_BAD_VALUE;
import static androidx.media2.SessionPlayer.PlayerResult.RESULT_CODE_INVALID_STATE;
import static androidx.media2.SessionPlayer.PlayerResult.RESULT_CODE_SUCCESS;
import static androidx.media2.SessionPlayer.PlayerResult.RESULT_CODE_UNKNOWN_ERROR;

import android.content.Context;
import android.os.Bundle;
import android.os.SystemClock;
import android.text.TextUtils;
import android.util.Log;

import androidx.annotation.RequiresApi;
import androidx.annotation.RestrictTo;
import androidx.concurrent.futures.ResolvableFuture;
import androidx.core.util.Pair;
import androidx.media.AudioAttributesCompat;
import androidx.media2.MediaItem;
import androidx.media2.MediaMetadata;
import androidx.media2.RemoteSessionPlayer;
import androidx.media2.SessionPlayer;
import androidx.media2.UriMediaItem;
import androidx.mediarouter.media.MediaItemStatus;
import androidx.mediarouter.media.MediaRouteSelector;
import androidx.mediarouter.media.MediaRouter;
import androidx.mediarouter.media.MediaSessionStatus;
import androidx.mediarouter.media.RemotePlaybackClient;
import androidx.mediarouter.media.RemotePlaybackClient.ItemActionCallback;
import androidx.mediarouter.media.RemotePlaybackClient.SessionActionCallback;
import androidx.mediarouter.media.RemotePlaybackClient.StatusCallback;

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

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

/**
 * @hide
 */
@RestrictTo(LIBRARY_GROUP)
@RequiresApi(19)
public class RoutePlayer extends RemoteSessionPlayer {
    private static final String TAG = "RoutePlayer";
    static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
    private static final int ITEM_NONE = -1;

    String mItemId;
    int mCurrentPlayerState;
    long mDuration;
    long mLastStatusChangedTime;
    long mPosition;
    boolean mCanResume;
    @SuppressWarnings("WeakerAccess") /* synthetic access */
    MediaRouter.RouteInfo mSelectedRoute;
    @SuppressWarnings("WeakerAccess") /* synthetic access */
    final List<ResolvableFuture<PlayerResult>> mPendingVolumeResult = new ArrayList<>();

    private MediaItem mItem;
    private MediaRouter mMediaRouter;
    private RemotePlaybackClient mClient;

    private MediaRouter.Callback mRouterCallback = new MediaRouter.Callback() {
        @Override
        public void onRouteVolumeChanged(MediaRouter router, MediaRouter.RouteInfo route) {
            if (TextUtils.equals(route.getId(), mSelectedRoute.getId())) {
                final int volume = route.getVolume();
                for (int i = 0; i < mPendingVolumeResult.size(); i++) {
                    mPendingVolumeResult.get(i).set(new PlayerResult(
                            RESULT_CODE_SUCCESS, getCurrentMediaItem()));
                }
                mPendingVolumeResult.clear();
                List<Pair<PlayerCallback, Executor>> callbacks = getCallbacks();
                for (Pair<PlayerCallback, Executor> pair : callbacks) {
                    if (pair.first instanceof RemoteSessionPlayer.Callback) {
                        final RemoteSessionPlayer.PlayerCallback callback = pair.first;
                        pair.second.execute(new Runnable() {
                            @Override
                            public void run() {
                                ((RemoteSessionPlayer.Callback) callback)
                                        .onVolumeChanged(RoutePlayer.this, volume);
                            }
                        });
                    }
                }
            }
        }
    };

    private StatusCallback mStatusCallback = new StatusCallback() {
        @Override
        public void onItemStatusChanged(Bundle data,
                String sessionId, MediaSessionStatus sessionStatus,
                String itemId, MediaItemStatus itemStatus) {
            if (DEBUG && !isSessionActive(sessionStatus)) {
                Log.v(TAG, "onItemStatusChanged() is called, but session is not active.");
            }
            mLastStatusChangedTime = SystemClock.elapsedRealtime();
            mPosition = itemStatus.getContentPosition();
            mCurrentPlayerState = convertPlaybackStateToPlayerState(itemStatus.getPlaybackState());

            List<Pair<PlayerCallback, Executor>> callbacks = getCallbacks();
            for (Pair<PlayerCallback, Executor> pair : callbacks) {
                final PlayerCallback callback = pair.first;
                pair.second.execute(new Runnable() {
                    @Override
                    public void run() {
                        callback.onPlayerStateChanged(RoutePlayer.this, mCurrentPlayerState);
                    }
                });
            }
        }
    };

    public RoutePlayer(Context context, MediaRouteSelector selector,
            MediaRouter.RouteInfo route) {
        mMediaRouter = MediaRouter.getInstance(context);
        mMediaRouter.addCallback(selector, mRouterCallback);
        mSelectedRoute = route;

        mClient = new RemotePlaybackClient(context, route);
        mClient.setStatusCallback(mStatusCallback);
        if (mClient.isSessionManagementSupported()) {
            mClient.startSession(null, new SessionActionCallback() {
                @Override
                public void onResult(Bundle data,
                        String sessionId, MediaSessionStatus sessionStatus) {
                    if (DEBUG && !isSessionActive(sessionStatus)) {
                        Log.v(TAG, "RoutePlayer has been initialized, but session is not"
                                + "active.");
                    }
                }
            });
        }
    }

    @Override
    public ListenableFuture<PlayerResult> play() {
        if (mItem == null) {
            return createResult(RESULT_CODE_BAD_VALUE);
        }

        // RemotePlaybackClient cannot call resume(..) without calling pause(..) first.
        if (!mCanResume) {
            return playInternal();
        }

        if (mClient.isSessionManagementSupported()) {
            final ResolvableFuture<PlayerResult> result = ResolvableFuture.create();
            mClient.resume(null, new SessionActionCallback() {
                @Override
                public void onResult(Bundle data,
                        String sessionId, MediaSessionStatus sessionStatus) {
                    if (DEBUG && !isSessionActive(sessionStatus)) {
                        Log.v(TAG, "play() is called, but session is not active.");
                    }
                    // Do nothing since this returns the buffering state--
                    // StatusCallback#onItemStatusChanged is called when the session reaches the
                    // play state.
                    result.set(new PlayerResult(RESULT_CODE_SUCCESS, getCurrentMediaItem()));
                }
            });
        }
        return createResult(RESULT_CODE_INVALID_STATE);
    }

    @Override
    public ListenableFuture<PlayerResult> prepare() {
        return createResult();
    }

    @Override
    public ListenableFuture<PlayerResult> pause() {
        if (mClient.isSessionManagementSupported()) {
            final ResolvableFuture<PlayerResult> result = ResolvableFuture.create();
            mClient.pause(null, new SessionActionCallback() {
                @Override
                public void onResult(Bundle data,
                        String sessionId, MediaSessionStatus sessionStatus) {
                    if (DEBUG && !isSessionActive(sessionStatus)) {
                        Log.v(TAG, "pause() is called, but session is not active.");
                    }
                    mCanResume = true;
                    // Do not update playback state here since this returns the buffering state--
                    // StatusCallback#onItemStatusChanged is called when the session reaches the
                    // pause state.
                    result.set(new PlayerResult(RESULT_CODE_SUCCESS, getCurrentMediaItem()));
                }
            });
        }
        return createResult(RESULT_CODE_INVALID_STATE);
    }

    @Override
    public ListenableFuture<PlayerResult> seekTo(long pos) {
        if (mClient.isSessionManagementSupported()) {
            final ResolvableFuture<PlayerResult> result = ResolvableFuture.create();
            mClient.seek(mItemId, pos, null, new ItemActionCallback() {
                @Override
                public void onResult(Bundle data,
                        String sessionId, MediaSessionStatus sessionStatus,
                        String itemId, final MediaItemStatus itemStatus) {
                    if (DEBUG && !isSessionActive(sessionStatus)) {
                        Log.v(TAG, "seekTo(long) is called, but session is not active.");
                    }
                    if (itemStatus != null) {
                        List<Pair<PlayerCallback, Executor>> callbacks = getCallbacks();
                        for (Pair<PlayerCallback, Executor> pair : callbacks) {
                            final PlayerCallback callback = pair.first;
                            pair.second.execute(new Runnable() {
                                @Override
                                public void run() {
                                    callback.onSeekCompleted(RoutePlayer.this,
                                            itemStatus.getContentPosition());
                                }
                            });
                        }
                    } else {
                        result.set(new PlayerResult(RESULT_CODE_UNKNOWN_ERROR,
                                getCurrentMediaItem()));
                    }
                }
            });
        }
        return createResult(RESULT_CODE_INVALID_STATE);
    }

    @Override
    public long getCurrentPosition() {
        long expectedPosition = mPosition;
        if (mCurrentPlayerState == PLAYER_STATE_PLAYING) {
            expectedPosition = mPosition + (SystemClock.elapsedRealtime() - mLastStatusChangedTime);
        }
        return expectedPosition;
    }

    @Override
    public long getDuration() {
        return mDuration;
    }

    @Override
    public long getBufferedPosition() {
        return 0;
    }

    @Override
    public int getPlayerState() {
        return mCurrentPlayerState;
    }

    @Override
    public int getBufferingState() {
        return SessionPlayer.BUFFERING_STATE_UNKNOWN;
    }

    @Override
    public ListenableFuture<PlayerResult> setAudioAttributes(AudioAttributesCompat attributes) {
        // TODO: implement
        return createResult(RESULT_CODE_INVALID_STATE);
    }

    @Override
    public AudioAttributesCompat getAudioAttributes() {
        return null;
    }

    @Override
    public ListenableFuture<PlayerResult> setMediaItem(MediaItem item) {
        mItem = item;
        return createResult();
    }

    @Override
    public MediaItem getCurrentMediaItem() {
        return mItem;
    }

    @Override
    public int getCurrentMediaItemIndex() {
        return ITEM_NONE;
    }

    @Override
    public int getPreviousMediaItemIndex() {
        return ITEM_NONE;
    }

    @Override
    public int getNextMediaItemIndex() {
        return ITEM_NONE;
    }

    @Override
    public ListenableFuture<PlayerResult> setPlaybackSpeed(float speed) {
        // Do nothing
        return createResult(RESULT_CODE_INVALID_STATE);
    }

    @Override
    public float getPlaybackSpeed() {
        return 1.0f;
    }

    @Override
    public int getVolume() {
        return mSelectedRoute.getVolume();
    }

    @Override
    public Future<PlayerResult> adjustVolume(int direction) {
        mSelectedRoute.requestUpdateVolume(direction);

        ResolvableFuture<PlayerResult> result = ResolvableFuture.create();
        mPendingVolumeResult.add(result);
        return result;
    }

    @Override
    public Future<PlayerResult> setVolume(int volume) {
        mSelectedRoute.requestSetVolume(volume);

        ResolvableFuture<PlayerResult> result = ResolvableFuture.create();
        mPendingVolumeResult.add(result);
        return result;
    }

    @Override
    public int getMaxVolume() {
        return mSelectedRoute.getVolumeMax();
    }

    @Override
    public int getVolumeControlType() {
        return mSelectedRoute.getVolumeHandling();
    }

    @Override
    public ListenableFuture<PlayerResult> setPlaylist(List<MediaItem> list,
            MediaMetadata metadata) {
        // TODO: implement
        return createResult(RESULT_CODE_INVALID_STATE);
    }

    @Override
    public ListenableFuture<PlayerResult> addPlaylistItem(int index, MediaItem item) {
        // TODO: implement
        return createResult(RESULT_CODE_INVALID_STATE);
    }

    @Override
    public ListenableFuture<PlayerResult> removePlaylistItem(int index) {
        // TODO: implement
        return createResult(RESULT_CODE_INVALID_STATE);
    }

    @Override
    public ListenableFuture<PlayerResult> replacePlaylistItem(int index, MediaItem item) {
        // TODO: implement
        return createResult(RESULT_CODE_INVALID_STATE);
    }

    @Override
    public ListenableFuture<PlayerResult> skipToPreviousPlaylistItem() {
        // TODO: implement
        return createResult(RESULT_CODE_INVALID_STATE);
    }

    @Override
    public ListenableFuture<PlayerResult> skipToNextPlaylistItem() {
        // TODO: implement
        return createResult(RESULT_CODE_INVALID_STATE);
    }

    @Override
    public ListenableFuture<PlayerResult> skipToPlaylistItem(int index) {
        // TODO: implement
        return createResult(RESULT_CODE_INVALID_STATE);
    }

    @Override
    public ListenableFuture<PlayerResult> updatePlaylistMetadata(MediaMetadata metadata) {
        // TODO: implement
        return createResult(RESULT_CODE_INVALID_STATE);
    }

    @Override
    public ListenableFuture<PlayerResult> setRepeatMode(int repeatMode) {
        // TODO: implement
        return createResult(RESULT_CODE_INVALID_STATE);
    }

    @Override
    public ListenableFuture<PlayerResult> setShuffleMode(int shuffleMode) {
        // TODO: implement
        return createResult(RESULT_CODE_INVALID_STATE);
    }

    @Override
    public List<MediaItem> getPlaylist() {
        List<MediaItem> list = new ArrayList<>();
        list.add(mItem);
        return list;
    }

    @Override
    public MediaMetadata getPlaylistMetadata() {
        return null;
    }

    @Override
    public int getRepeatMode() {
        return SessionPlayer.REPEAT_MODE_NONE;
    }

    @Override
    public int getShuffleMode() {
        return SessionPlayer.SHUFFLE_MODE_NONE;
    }

    @Override
    public void close() {
        if (mClient != null) {
            try {
                mClient.release();
            } catch (IllegalArgumentException e) {
                Log.d(TAG, "Receiver not registered");
            }
            mClient = null;
        }
        mMediaRouter.removeCallback(mRouterCallback);
    }

    void setCurrentPosition(long position) {
        mPosition = position;
    }

    boolean isSessionActive(MediaSessionStatus status) {
        if (status == null || status.getSessionState() == MediaSessionStatus.SESSION_STATE_ENDED
                || status.getSessionState() == MediaSessionStatus.SESSION_STATE_INVALIDATED) {
            return false;
        }
        return true;
    }

    int convertPlaybackStateToPlayerState(int playbackState) {
        int playerState = PLAYER_STATE_IDLE;
        switch (playbackState) {
            case MediaItemStatus.PLAYBACK_STATE_PENDING:
            case MediaItemStatus.PLAYBACK_STATE_FINISHED:
            case MediaItemStatus.PLAYBACK_STATE_CANCELED:
                playerState = PLAYER_STATE_IDLE;
                break;
            case MediaItemStatus.PLAYBACK_STATE_PLAYING:
                playerState = PLAYER_STATE_PLAYING;
                break;
            case MediaItemStatus.PLAYBACK_STATE_PAUSED:
            case MediaItemStatus.PLAYBACK_STATE_BUFFERING:
                playerState = PLAYER_STATE_PAUSED;
                break;
            case MediaItemStatus.PLAYBACK_STATE_INVALIDATED:
            case MediaItemStatus.PLAYBACK_STATE_ERROR:
                playerState =  PLAYER_STATE_ERROR;
                break;
        }
        return playerState;
    }

    private ListenableFuture<PlayerResult> playInternal() {
        if (!(mItem instanceof UriMediaItem)) {
            Log.w(TAG, "Data source type is not Uri." + mItem);
            return createResult(RESULT_CODE_BAD_VALUE);
        }
        final ResolvableFuture<PlayerResult> result = ResolvableFuture.create();
        mClient.play(((UriMediaItem) mItem).getUri(), "video/mp4", null, mPosition, null,
                new ItemActionCallback() {
                    @Override
                    public void onResult(Bundle data, String sessionId,
                            MediaSessionStatus sessionStatus,
                            String itemId, MediaItemStatus itemStatus) {
                        if (DEBUG && !isSessionActive(sessionStatus)) {
                            Log.v(TAG, "play() is called, but session is not active.");
                        }
                        mItemId = itemId;
                        if (itemStatus != null) {
                            mDuration = itemStatus.getContentDuration();
                        }
                        // Do not update playback state here since this returns the buffering state.
                        // StatusCallback#onItemStatusChanged is called when the session reaches the
                        // play state.
                        result.set(new PlayerResult(RESULT_CODE_SUCCESS, getCurrentMediaItem()));
                    }
                });
        return result;
    }

    private ListenableFuture<PlayerResult> createResult() {
        return createResult(RESULT_CODE_SUCCESS);
    }

    private ListenableFuture<PlayerResult> createResult(int code) {
        ResolvableFuture<PlayerResult> result = ResolvableFuture.create();
        result.set(new PlayerResult(code, getCurrentMediaItem()));
        return result;
    }
}