chromium/components/media_router/test/android/cast_emulator/src/org/chromium/components/media_router/cast_emulator/remote/RemotePlaybackRoutePublisher.java

// Copyright 2014 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

package org.chromium.components.media_router.cast_emulator.remote;

import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.IntentFilter.MalformedMimeTypeException;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.media.AudioManager;
import android.media.MediaRouter;
import android.net.Uri;
import android.os.Bundle;

import androidx.mediarouter.media.MediaControlIntent;
import androidx.mediarouter.media.MediaRouteDescriptor;
import androidx.mediarouter.media.MediaRouteProvider;
import androidx.mediarouter.media.MediaRouteProviderDescriptor;
import androidx.mediarouter.media.MediaRouter.ControlRequestCallback;
import androidx.mediarouter.media.MediaSessionStatus;

import com.google.android.gms.cast.CastMediaControlIntent;

import org.chromium.base.Log;
import org.chromium.components.media_router.cast_emulator.RoutePublisher;

import java.util.ArrayList;
import java.util.List;

/**
 * Media route publisher for testing remote playback from Chrome.
 *
 * @see TestMediaRouteProviderService
 */
public final class RemotePlaybackRoutePublisher implements RoutePublisher {
    private static final String MANIFEST_CAST_KEY =
            "com.google.android.apps.chrome.tests.support.CAST_ID";

    private static final String TAG = "CastEmulator";

    private static final String VARIABLE_VOLUME_SESSION_ROUTE_ID = "variable_session";
    private static final int VOLUME_MAX = 10;

    private int mVolume = 5;

    private final MediaRouteProvider mProvider;
    private final List<String> mControlCategories = new ArrayList<String>();

    public RemotePlaybackRoutePublisher(MediaRouteProvider provider) {
        mProvider = provider;

        mControlCategories.add(CastMediaControlIntent.categoryForRemotePlayback(getCastId()));
        mControlCategories.add(CastMediaControlIntent.categoryForRemotePlayback());
    }

    @Override
    public boolean supportsRoute(String routeId) {
        return routeId.equals(VARIABLE_VOLUME_SESSION_ROUTE_ID);
    }

    @Override
    public MediaRouteProvider.RouteController onCreateRouteController(String routeId) {
        return new TestMediaController(routeId);
    }

    @Override
    public boolean supportsControlCategory(String controlCategory) {
        return mControlCategories.contains(controlCategory);
    }

    @Override
    public void publishRoutes() {
        Log.i(TAG, "publishing routes");

        String castId = getCastId();

        IntentFilter f1 = new IntentFilter();
        f1.addCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK);
        f1.addCategory(CastMediaControlIntent.categoryForRemotePlayback(castId));
        f1.addAction(MediaControlIntent.ACTION_PLAY);
        f1.addDataScheme("http");
        f1.addDataScheme("https");
        addDataTypeUnchecked(f1, "video/*");

        IntentFilter f2 = new IntentFilter();
        f2.addCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK);
        f2.addCategory(CastMediaControlIntent.categoryForRemotePlayback());
        f2.addAction(MediaControlIntent.ACTION_SEEK);
        f2.addAction(MediaControlIntent.ACTION_GET_STATUS);
        f2.addAction(MediaControlIntent.ACTION_PAUSE);
        f2.addAction(MediaControlIntent.ACTION_RESUME);
        f2.addAction(MediaControlIntent.ACTION_STOP);
        f2.addAction(MediaControlIntent.ACTION_START_SESSION);
        f2.addAction(MediaControlIntent.ACTION_GET_SESSION_STATUS);
        f2.addAction(MediaControlIntent.ACTION_END_SESSION);
        f2.addAction(CastMediaControlIntent.ACTION_SYNC_STATUS);

        ArrayList<IntentFilter> controlFilters = new ArrayList<IntentFilter>();
        controlFilters.add(f1);
        controlFilters.add(f2);

        MediaRouteDescriptor testRouteDescriptor =
                new MediaRouteDescriptor.Builder(
                                VARIABLE_VOLUME_SESSION_ROUTE_ID, "Cast Test Route")
                        .setDescription("Cast Test Route")
                        .addControlFilters(controlFilters)
                        .setPlaybackStream(AudioManager.STREAM_MUSIC)
                        .setPlaybackType(MediaRouter.RouteInfo.PLAYBACK_TYPE_REMOTE)
                        .setVolumeHandling(MediaRouter.RouteInfo.PLAYBACK_VOLUME_VARIABLE)
                        .setVolumeMax(VOLUME_MAX)
                        .setVolume(mVolume)
                        .build();

        MediaRouteProviderDescriptor providerDescriptor =
                new MediaRouteProviderDescriptor.Builder().addRoute(testRouteDescriptor).build();
        mProvider.setDescriptor(providerDescriptor);
    }

    private String getCastId() {
        String castId = CastMediaControlIntent.DEFAULT_MEDIA_RECEIVER_APPLICATION_ID;
        try {
            // Downstream cast uses a different, private, castId; so read this from
            // the manifest.
            ApplicationInfo ai;
            ai =
                    mProvider
                            .getContext()
                            .getPackageManager()
                            .getApplicationInfo(
                                    mProvider.getContext().getPackageName(),
                                    PackageManager.GET_META_DATA);
            Bundle bundle = ai.metaData;
            if (bundle != null) {
                castId = bundle.getString(MANIFEST_CAST_KEY, castId);
            }
        } catch (NameNotFoundException e) {
            // Should never happen, do nothing - use default
        }
        return castId;
    }

    private final class TestMediaController extends MediaRouteProvider.RouteController {
        private final String mRouteId;
        private final LocalSessionManager mSessionManager =
                new LocalSessionManager(mProvider.getContext());
        private PendingIntent mSessionReceiver;
        private Bundle mMetadata;

        public TestMediaController(String routeId) {
            mRouteId = routeId;
            mSessionManager.setCallback(
                    new LocalSessionManager.Callback() {
                        @Override
                        public void onItemChanged(MediaItem item) {
                            handleStatusChange(item);
                        }
                    });
            setVolumeInternal(mVolume);
            Log.v(TAG, "%s: Controller created", mRouteId);
        }

        @Override
        public void onRelease() {
            Log.v(TAG, "%s: Controller released", mRouteId);
        }

        @Override
        public void onSelect() {
            Log.v(TAG, "%s: Selected", mRouteId);
        }

        @Override
        public void onUnselect() {
            Log.v(TAG, "%s: Unselected", mRouteId);
        }

        @Override
        public void onSetVolume(int volume) {
            Log.v(TAG, "%s: Set volume to %d", mRouteId, volume);
            setVolumeInternal(volume);
        }

        @Override
        public void onUpdateVolume(int delta) {
            Log.v(TAG, "%s: Update volume by %d", mRouteId, delta);
            setVolumeInternal(mVolume + delta);
        }

        @Override
        public boolean onControlRequest(Intent intent, ControlRequestCallback callback) {
            Log.v(TAG, "%s: Received control request %s", mRouteId, intent);
            String action = intent.getAction();
            if (intent.hasCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK)
                    || intent.hasCategory(CastMediaControlIntent.categoryForRemotePlayback())) {
                boolean success = false;
                if (action.equals(MediaControlIntent.ACTION_PLAY)) {
                    success = handlePlay(intent, callback);
                } else if (action.equals(MediaControlIntent.ACTION_SEEK)) {
                    success = handleSeek(intent, callback);
                } else if (action.equals(MediaControlIntent.ACTION_GET_STATUS)) {
                    success = handleGetStatus(intent, callback);
                } else if (action.equals(MediaControlIntent.ACTION_PAUSE)) {
                    success = handlePause(intent, callback);
                } else if (action.equals(MediaControlIntent.ACTION_RESUME)) {
                    success = handleResume(intent, callback);
                } else if (action.equals(MediaControlIntent.ACTION_STOP)) {
                    success = handleStop(intent, callback);
                } else if (action.equals(MediaControlIntent.ACTION_START_SESSION)) {
                    success = handleStartSession(intent, callback);
                } else if (action.equals(MediaControlIntent.ACTION_GET_SESSION_STATUS)) {
                    success = handleGetSessionStatus(intent, callback);
                } else if (action.equals(MediaControlIntent.ACTION_END_SESSION)) {
                    success = handleEndSession(intent, callback);
                } else if (action.equals(CastMediaControlIntent.ACTION_SYNC_STATUS)) {
                    success = handleSyncStatus(intent, callback);
                }
                Log.v(TAG, mSessionManager.getSessionStatusString());
                return success;
            }

            return false;
        }

        /**
         * @param intent
         * @param callback
         * @return
         */
        private boolean handleSyncStatus(Intent intent, ControlRequestCallback callback) {
            String sid = intent.getStringExtra(MediaControlIntent.EXTRA_SESSION_ID);
            Log.v(TAG, "%s: Received syncStatus request, sid=%s", mRouteId, sid);

            MediaItem item = mSessionManager.getCurrentItem();
            if (callback != null) {
                Bundle result = new Bundle();
                if (item != null) {
                    String iid = item.getItemId();
                    result.putString(MediaControlIntent.EXTRA_ITEM_ID, iid);
                    result.putBundle(
                            MediaControlIntent.EXTRA_ITEM_STATUS, item.getStatus().asBundle());
                    if (mMetadata != null) {
                        result.putBundle(MediaControlIntent.EXTRA_ITEM_METADATA, mMetadata);
                    }
                }
                callback.onResult(result);
            }
            return true;
        }

        private void setVolumeInternal(int volume) {
            if (volume >= 0 && volume <= VOLUME_MAX) {
                mVolume = volume;
                Log.v(TAG, "%s: New volume is %d", mRouteId, mVolume);
                AudioManager audioManager =
                        (AudioManager)
                                mProvider.getContext().getSystemService(Context.AUDIO_SERVICE);
                audioManager.setStreamVolume(AudioManager.STREAM_MUSIC, volume, 0);
                publishRoutes();
            }
        }

        private boolean handlePlay(Intent intent, ControlRequestCallback callback) {
            String sid = intent.getStringExtra(MediaControlIntent.EXTRA_SESSION_ID);
            if (sid != null && !sid.equals(mSessionManager.getSessionId())) {
                Log.v(TAG, "handlePlay fails because of bad sid=%s", sid);
                return false;
            }
            if (mSessionManager.hasSession()) {
                mSessionManager.stop();
            }

            Uri uri = intent.getData();
            if (uri == null) {
                Log.v(TAG, "handlePlay fails because of null uri");
                return false;
            }

            mMetadata = intent.getBundleExtra(MediaControlIntent.EXTRA_ITEM_METADATA);
            PendingIntent receiver =
                    (PendingIntent)
                            intent.getParcelableExtra(
                                    MediaControlIntent.EXTRA_ITEM_STATUS_UPDATE_RECEIVER);
            long position = intent.getLongExtra(MediaControlIntent.EXTRA_ITEM_CONTENT_POSITION, 0);

            Log.v(
                    TAG,
                    "%s: Received play request {%s}",
                    mRouteId,
                    getMediaControlIntentDebugString(intent));
            // Add the video to the session manager.
            MediaItem item = mSessionManager.add(uri, intent.getType(), receiver, position);
            // And start it playing.
            mSessionManager.resume();
            if (callback != null) {
                Bundle result = new Bundle();
                result.putString(MediaControlIntent.EXTRA_SESSION_ID, item.getSessionId());
                result.putString(MediaControlIntent.EXTRA_ITEM_ID, item.getItemId());
                result.putBundle(MediaControlIntent.EXTRA_ITEM_STATUS, item.getStatus().asBundle());
                callback.onResult(result);
            }
            return true;
        }

        private boolean handleSeek(Intent intent, ControlRequestCallback callback) {
            String sid = intent.getStringExtra(MediaControlIntent.EXTRA_SESSION_ID);
            if (sid == null || !sid.equals(mSessionManager.getSessionId())) {
                return false;
            }

            String iid = intent.getStringExtra(MediaControlIntent.EXTRA_ITEM_ID);
            long pos = intent.getLongExtra(MediaControlIntent.EXTRA_ITEM_CONTENT_POSITION, 0);
            Log.v(TAG, "%s: Received seek request, pos=%d", mRouteId, pos);
            MediaItem item = mSessionManager.seek(iid, pos);
            if (callback != null) {
                Bundle result = new Bundle();
                result.putBundle(MediaControlIntent.EXTRA_ITEM_STATUS, item.getStatus().asBundle());
                callback.onResult(result);
            }
            return true;
        }

        private boolean handleGetStatus(Intent intent, ControlRequestCallback callback) {
            String sid = intent.getStringExtra(MediaControlIntent.EXTRA_SESSION_ID);
            String iid = intent.getStringExtra(MediaControlIntent.EXTRA_ITEM_ID);
            Log.v(TAG, "%s: Received getStatus request, sid=%s, iid=%s", mRouteId, sid, iid);
            MediaItem item = mSessionManager.getStatus(iid);
            if (callback != null) {
                if (item != null) {
                    Bundle result = new Bundle();
                    result.putBundle(
                            MediaControlIntent.EXTRA_ITEM_STATUS, item.getStatus().asBundle());
                    callback.onResult(result);
                } else {
                    callback.onError("Failed to get status, sid=" + sid + ", iid=" + iid, null);
                }
            }
            return (item != null);
        }

        private boolean handlePause(Intent intent, ControlRequestCallback callback) {
            String sid = intent.getStringExtra(MediaControlIntent.EXTRA_SESSION_ID);
            boolean success = (sid != null) && sid.equals(mSessionManager.getSessionId());
            mSessionManager.pause();
            if (callback != null) {
                if (success) {
                    callback.onResult(new Bundle());
                    handleSessionStatusChange(sid);
                } else {
                    callback.onError("Failed to pause, sid=" + sid, null);
                }
            }
            return success;
        }

        private boolean handleResume(Intent intent, ControlRequestCallback callback) {
            String sid = intent.getStringExtra(MediaControlIntent.EXTRA_SESSION_ID);
            boolean success = (sid != null) && sid.equals(mSessionManager.getSessionId());
            mSessionManager.resume();
            if (callback != null) {
                if (success) {
                    callback.onResult(new Bundle());
                    handleSessionStatusChange(sid);
                } else {
                    callback.onError("Failed to resume, sid=" + sid, null);
                }
            }
            return success;
        }

        private boolean handleStop(Intent intent, ControlRequestCallback callback) {
            String sid = intent.getStringExtra(MediaControlIntent.EXTRA_SESSION_ID);
            boolean success = (sid != null) && sid.equals(mSessionManager.getSessionId());
            mSessionManager.stop();
            if (callback != null) {
                if (success) {
                    callback.onResult(new Bundle());
                    handleSessionStatusChange(sid);
                } else {
                    callback.onError("Failed to stop, sid=" + sid, null);
                }
            }
            return success;
        }

        private boolean handleStartSession(Intent intent, ControlRequestCallback callback) {
            boolean relaunch =
                    intent.getBooleanExtra(
                            CastMediaControlIntent.EXTRA_CAST_RELAUNCH_APPLICATION, true);
            String sid = mSessionManager.startSession(relaunch);
            Log.v(TAG, "StartSession returns sessionId %s", sid);
            if (callback != null) {
                if (sid != null) {
                    Bundle result = new Bundle();
                    result.putString(MediaControlIntent.EXTRA_SESSION_ID, sid);
                    result.putBundle(
                            MediaControlIntent.EXTRA_SESSION_STATUS,
                            mSessionManager.getSessionStatus(sid).asBundle());
                    Log.v(TAG, "StartSession sends result of $s", result);
                    callback.onResult(result);
                    mSessionReceiver =
                            (PendingIntent)
                                    intent.getParcelableExtra(
                                            MediaControlIntent
                                                    .EXTRA_SESSION_STATUS_UPDATE_RECEIVER);
                    handleSessionStatusChange(sid);
                } else {
                    callback.onError("Failed to start session.", null);
                }
            }
            return (sid != null);
        }

        private boolean handleGetSessionStatus(Intent intent, ControlRequestCallback callback) {
            String sid = intent.getStringExtra(MediaControlIntent.EXTRA_SESSION_ID);

            MediaSessionStatus sessionStatus = mSessionManager.getSessionStatus(sid);
            if (callback != null) {
                if (sessionStatus != null) {
                    Bundle result = new Bundle();
                    result.putBundle(
                            MediaControlIntent.EXTRA_SESSION_STATUS,
                            mSessionManager.getSessionStatus(sid).asBundle());
                    callback.onResult(result);
                } else {
                    callback.onError("Failed to get session status, sid=" + sid, null);
                }
            }
            return (sessionStatus != null);
        }

        private boolean handleEndSession(Intent intent, ControlRequestCallback callback) {
            String sid = intent.getStringExtra(MediaControlIntent.EXTRA_SESSION_ID);
            boolean success =
                    (sid != null)
                            && sid.equals(mSessionManager.getSessionId())
                            && mSessionManager.endSession();
            if (callback != null) {
                if (success) {
                    Bundle result = new Bundle();
                    MediaSessionStatus sessionStatus =
                            new MediaSessionStatus.Builder(MediaSessionStatus.SESSION_STATE_ENDED)
                                    .build();
                    result.putBundle(
                            MediaControlIntent.EXTRA_SESSION_STATUS, sessionStatus.asBundle());
                    callback.onResult(result);
                    handleSessionStatusChange(sid);
                    mSessionReceiver = null;
                } else {
                    callback.onError("Failed to end session, sid=" + sid, null);
                }
            }
            return success;
        }

        private void handleStatusChange(MediaItem item) {
            if (item == null) {
                item = mSessionManager.getCurrentItem();
            }
            if (item != null) {
                PendingIntent receiver = item.getUpdateReceiver();
                if (receiver != null) {
                    Intent intent = new Intent();
                    intent.putExtra(MediaControlIntent.EXTRA_SESSION_ID, item.getSessionId());
                    intent.putExtra(MediaControlIntent.EXTRA_ITEM_ID, item.getItemId());
                    intent.putExtra(
                            MediaControlIntent.EXTRA_ITEM_STATUS, item.getStatus().asBundle());
                    try {
                        receiver.send(mProvider.getContext(), 0, intent);
                        Log.v(TAG, "%s: Sending status update from provider", mRouteId);
                    } catch (PendingIntent.CanceledException e) {
                        Log.v(TAG, "%s: Failed to send status update!", mRouteId);
                    }
                }
            }
        }

        private void handleSessionStatusChange(String sid) {
            if (mSessionReceiver != null) {
                Intent intent = new Intent();
                intent.putExtra(MediaControlIntent.EXTRA_SESSION_ID, sid);
                intent.putExtra(
                        MediaControlIntent.EXTRA_SESSION_STATUS,
                        mSessionManager.getSessionStatus(sid).asBundle());
                try {
                    mSessionReceiver.send(mProvider.getContext(), 0, intent);
                    Log.v(TAG, "%s: Sending session status update from provider", mRouteId);
                } catch (PendingIntent.CanceledException e) {
                    Log.v(TAG, "%s: Failed to send session status update!", mRouteId);
                }
            }
        }
    }

    private String getMediaControlIntentDebugString(Intent intent) {
        return "uri="
                + intent.getData()
                + ", mime="
                + intent.getType()
                + ", sid="
                + intent.getStringExtra(MediaControlIntent.EXTRA_SESSION_ID)
                + ", pos="
                + intent.getLongExtra(MediaControlIntent.EXTRA_ITEM_CONTENT_POSITION, 0)
                + ", metadata="
                + intent.getBundleExtra(MediaControlIntent.EXTRA_ITEM_METADATA)
                + ", headers="
                + intent.getBundleExtra(MediaControlIntent.EXTRA_ITEM_HTTP_HEADERS);
    }

    private static void addDataTypeUnchecked(IntentFilter filter, String type) {
        try {
            filter.addDataType(type);
        } catch (MalformedMimeTypeException ex) {
            throw new RuntimeException(ex);
        }
    }
}