/*
* Copyright (C) 2013 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 com.example.android.mediarouter.provider;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.IntentFilter.MalformedMimeTypeException;
import android.content.res.Resources;
import android.media.AudioManager;
import android.media.MediaRouter;
import android.net.Uri;
import android.os.Bundle;
import android.support.v7.media.MediaControlIntent;
import android.support.v7.media.MediaRouteDescriptor;
import android.support.v7.media.MediaRouteProvider;
import android.support.v7.media.MediaRouteProviderDescriptor;
import android.support.v7.media.MediaRouter.ControlRequestCallback;
import android.support.v7.media.MediaSessionStatus;
import android.util.Log;
import com.example.android.mediarouter.player.Player;
import com.example.android.mediarouter.player.PlaylistItem;
import com.example.android.mediarouter.R;
import com.example.android.mediarouter.player.SessionManager;
import java.util.ArrayList;
/**
* Demonstrates how to create a custom media route provider.
*
* @see SampleMediaRouteProviderService
*/
public final class SampleMediaRouteProvider extends MediaRouteProvider {
private static final String TAG = "SampleMediaRouteProvider";
private static final String FIXED_VOLUME_ROUTE_ID = "fixed";
private static final String VARIABLE_VOLUME_BASIC_ROUTE_ID = "variable_basic";
private static final String VARIABLE_VOLUME_QUEUING_ROUTE_ID = "variable_queuing";
private static final String VARIABLE_VOLUME_SESSION_ROUTE_ID = "variable_session";
private static final int VOLUME_MAX = 10;
/**
* A custom media control intent category for special requests that are
* supported by this provider's routes.
*/
public static final String CATEGORY_SAMPLE_ROUTE =
"com.example.android.mediarouteprovider.CATEGORY_SAMPLE_ROUTE";
/**
* A custom media control intent action for special requests that are
* supported by this provider's routes.
* <p>
* This particular request is designed to return a bundle of not very
* interesting statistics for demonstration purposes.
* </p>
*
* @see #DATA_PLAYBACK_COUNT
*/
public static final String ACTION_GET_STATISTICS =
"com.example.android.mediarouteprovider.ACTION_GET_STATISTICS";
/**
* {@link #ACTION_GET_STATISTICS} result data: Number of times the
* playback action was invoked.
*/
public static final String DATA_PLAYBACK_COUNT =
"com.example.android.mediarouteprovider.EXTRA_PLAYBACK_COUNT";
private static final ArrayList<IntentFilter> CONTROL_FILTERS_BASIC;
private static final ArrayList<IntentFilter> CONTROL_FILTERS_QUEUING;
private static final ArrayList<IntentFilter> CONTROL_FILTERS_SESSION;
static {
IntentFilter f1 = new IntentFilter();
f1.addCategory(CATEGORY_SAMPLE_ROUTE);
f1.addAction(ACTION_GET_STATISTICS);
IntentFilter f2 = new IntentFilter();
f2.addCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK);
f2.addAction(MediaControlIntent.ACTION_PLAY);
f2.addDataScheme("http");
f2.addDataScheme("https");
f2.addDataScheme("rtsp");
f2.addDataScheme("file");
addDataTypeUnchecked(f2, "video/*");
IntentFilter f3 = new IntentFilter();
f3.addCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK);
f3.addAction(MediaControlIntent.ACTION_SEEK);
f3.addAction(MediaControlIntent.ACTION_GET_STATUS);
f3.addAction(MediaControlIntent.ACTION_PAUSE);
f3.addAction(MediaControlIntent.ACTION_RESUME);
f3.addAction(MediaControlIntent.ACTION_STOP);
IntentFilter f4 = new IntentFilter();
f4.addCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK);
f4.addAction(MediaControlIntent.ACTION_ENQUEUE);
f4.addDataScheme("http");
f4.addDataScheme("https");
f4.addDataScheme("rtsp");
f4.addDataScheme("file");
addDataTypeUnchecked(f4, "video/*");
IntentFilter f5 = new IntentFilter();
f5.addCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK);
f5.addAction(MediaControlIntent.ACTION_REMOVE);
IntentFilter f6 = new IntentFilter();
f6.addCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK);
f6.addAction(MediaControlIntent.ACTION_START_SESSION);
f6.addAction(MediaControlIntent.ACTION_GET_SESSION_STATUS);
f6.addAction(MediaControlIntent.ACTION_END_SESSION);
CONTROL_FILTERS_BASIC = new ArrayList<IntentFilter>();
CONTROL_FILTERS_BASIC.add(f1);
CONTROL_FILTERS_BASIC.add(f2);
CONTROL_FILTERS_BASIC.add(f3);
CONTROL_FILTERS_QUEUING =
new ArrayList<IntentFilter>(CONTROL_FILTERS_BASIC);
CONTROL_FILTERS_QUEUING.add(f4);
CONTROL_FILTERS_QUEUING.add(f5);
CONTROL_FILTERS_SESSION =
new ArrayList<IntentFilter>(CONTROL_FILTERS_QUEUING);
CONTROL_FILTERS_SESSION.add(f6);
}
private static void addDataTypeUnchecked(IntentFilter filter, String type) {
try {
filter.addDataType(type);
} catch (MalformedMimeTypeException ex) {
throw new RuntimeException(ex);
}
}
private int mVolume = 5;
private int mEnqueueCount;
public SampleMediaRouteProvider(Context context) {
super(context);
publishRoutes();
}
@Override
public RouteController onCreateRouteController(String routeId) {
return new SampleRouteController(routeId);
}
private void publishRoutes() {
Resources r = getContext().getResources();
MediaRouteDescriptor routeDescriptor1 = new MediaRouteDescriptor.Builder(
FIXED_VOLUME_ROUTE_ID,
r.getString(R.string.fixed_volume_route_name))
.setDescription(r.getString(R.string.sample_route_description))
.addControlFilters(CONTROL_FILTERS_BASIC)
.setPlaybackStream(AudioManager.STREAM_MUSIC)
.setPlaybackType(MediaRouter.RouteInfo.PLAYBACK_TYPE_REMOTE)
.setVolumeHandling(MediaRouter.RouteInfo.PLAYBACK_VOLUME_FIXED)
.setVolume(VOLUME_MAX)
.build();
MediaRouteDescriptor routeDescriptor2 = new MediaRouteDescriptor.Builder(
VARIABLE_VOLUME_BASIC_ROUTE_ID,
r.getString(R.string.variable_volume_basic_route_name))
.setDescription(r.getString(R.string.sample_route_description))
.addControlFilters(CONTROL_FILTERS_BASIC)
.setPlaybackStream(AudioManager.STREAM_MUSIC)
.setPlaybackType(MediaRouter.RouteInfo.PLAYBACK_TYPE_REMOTE)
.setVolumeHandling(MediaRouter.RouteInfo.PLAYBACK_VOLUME_VARIABLE)
.setVolumeMax(VOLUME_MAX)
.setVolume(mVolume)
.build();
MediaRouteDescriptor routeDescriptor3 = new MediaRouteDescriptor.Builder(
VARIABLE_VOLUME_QUEUING_ROUTE_ID,
r.getString(R.string.variable_volume_queuing_route_name))
.setDescription(r.getString(R.string.sample_route_description))
.addControlFilters(CONTROL_FILTERS_QUEUING)
.setPlaybackStream(AudioManager.STREAM_MUSIC)
.setPlaybackType(MediaRouter.RouteInfo.PLAYBACK_TYPE_REMOTE)
.setVolumeHandling(MediaRouter.RouteInfo.PLAYBACK_VOLUME_VARIABLE)
.setVolumeMax(VOLUME_MAX)
.setVolume(mVolume)
.build();
MediaRouteDescriptor routeDescriptor4 = new MediaRouteDescriptor.Builder(
VARIABLE_VOLUME_SESSION_ROUTE_ID,
r.getString(R.string.variable_volume_session_route_name))
.setDescription(r.getString(R.string.sample_route_description))
.addControlFilters(CONTROL_FILTERS_SESSION)
.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(routeDescriptor1)
.addRoute(routeDescriptor2)
.addRoute(routeDescriptor3)
.addRoute(routeDescriptor4)
.build();
setDescriptor(providerDescriptor);
}
private final class SampleRouteController extends MediaRouteProvider.RouteController {
private final String mRouteId;
private final SessionManager mSessionManager = new SessionManager("mrp");
private final Player mPlayer;
private PendingIntent mSessionReceiver;
public SampleRouteController(String routeId) {
mRouteId = routeId;
mPlayer = Player.create(getContext(), null);
mSessionManager.setPlayer(mPlayer);
mSessionManager.setCallback(new SessionManager.Callback() {
@Override
public void onStatusChanged() {
}
@Override
public void onItemChanged(PlaylistItem item) {
handleStatusChange(item);
}
});
Log.d(TAG, mRouteId + ": Controller created");
}
@Override
public void onRelease() {
Log.d(TAG, mRouteId + ": Controller released");
mPlayer.release();
}
@Override
public void onSelect() {
Log.d(TAG, mRouteId + ": Selected");
mPlayer.connect(null);
}
@Override
public void onUnselect() {
Log.d(TAG, mRouteId + ": Unselected");
mPlayer.release();
}
@Override
public void onSetVolume(int volume) {
Log.d(TAG, mRouteId + ": Set volume to " + volume);
if (!mRouteId.equals(FIXED_VOLUME_ROUTE_ID)) {
setVolumeInternal(volume);
}
}
@Override
public void onUpdateVolume(int delta) {
Log.d(TAG, mRouteId + ": Update volume by " + delta);
if (!mRouteId.equals(FIXED_VOLUME_ROUTE_ID)) {
setVolumeInternal(mVolume + delta);
}
}
@Override
public boolean onControlRequest(Intent intent, ControlRequestCallback callback) {
Log.d(TAG, mRouteId + ": Received control request " + intent);
String action = intent.getAction();
if (intent.hasCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK)) {
boolean success = false;
if (action.equals(MediaControlIntent.ACTION_PLAY)) {
success = handlePlay(intent, callback);
} else if (action.equals(MediaControlIntent.ACTION_ENQUEUE)) {
success = handleEnqueue(intent, callback);
} else if (action.equals(MediaControlIntent.ACTION_REMOVE)) {
success = handleRemove(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);
}
Log.d(TAG, mSessionManager.toString());
return success;
}
if (action.equals(ACTION_GET_STATISTICS)
&& intent.hasCategory(CATEGORY_SAMPLE_ROUTE)) {
Bundle data = new Bundle();
data.putInt(DATA_PLAYBACK_COUNT, mEnqueueCount);
if (callback != null) {
callback.onResult(data);
}
return true;
}
return false;
}
private void setVolumeInternal(int volume) {
if (volume >= 0 && volume <= VOLUME_MAX) {
mVolume = volume;
Log.d(TAG, mRouteId + ": New volume is " + mVolume);
AudioManager audioManager =
(AudioManager)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.d(TAG, "handlePlay fails because of bad sid="+sid);
return false;
}
if (mSessionManager.hasSession()) {
mSessionManager.stop();
}
return handleEnqueue(intent, callback);
}
private boolean handleEnqueue(Intent intent, ControlRequestCallback callback) {
String sid = intent.getStringExtra(MediaControlIntent.EXTRA_SESSION_ID);
if (sid != null && !sid.equals(mSessionManager.getSessionId())) {
Log.d(TAG, "handleEnqueue fails because of bad sid="+sid);
return false;
}
Uri uri = intent.getData();
if (uri == null) {
Log.d(TAG, "handleEnqueue fails because of bad uri="+uri);
return false;
}
boolean enqueue = intent.getAction().equals(MediaControlIntent.ACTION_ENQUEUE);
String mime = intent.getType();
long pos = intent.getLongExtra(MediaControlIntent.EXTRA_ITEM_CONTENT_POSITION, 0);
Bundle metadata = intent.getBundleExtra(MediaControlIntent.EXTRA_ITEM_METADATA);
Bundle headers = intent.getBundleExtra(MediaControlIntent.EXTRA_ITEM_HTTP_HEADERS);
PendingIntent receiver = (PendingIntent)intent.getParcelableExtra(
MediaControlIntent.EXTRA_ITEM_STATUS_UPDATE_RECEIVER);
Log.d(TAG, mRouteId + ": Received " + (enqueue?"enqueue":"play") + " request"
+ ", uri=" + uri
+ ", mime=" + mime
+ ", sid=" + sid
+ ", pos=" + pos
+ ", metadata=" + metadata
+ ", headers=" + headers
+ ", receiver=" + receiver);
PlaylistItem item = mSessionManager.add(uri, mime, receiver);
if (callback != null) {
if (item != 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);
} else {
callback.onError("Failed to open " + uri.toString(), null);
}
}
mEnqueueCount +=1;
return true;
}
private boolean handleRemove(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);
PlaylistItem item = mSessionManager.remove(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 remove" +
", sid=" + sid + ", iid=" + iid, null);
}
}
return (item != null);
}
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.d(TAG, mRouteId + ": Received seek request, pos=" + pos);
PlaylistItem item = mSessionManager.seek(iid, pos);
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 seek" +
", sid=" + sid + ", iid=" + iid + ", pos=" + pos, null);
}
}
return (item != null);
}
private boolean handleGetStatus(Intent intent, ControlRequestCallback callback) {
String sid = intent.getStringExtra(MediaControlIntent.EXTRA_SESSION_ID);
String iid = intent.getStringExtra(MediaControlIntent.EXTRA_ITEM_ID);
Log.d(TAG, mRouteId + ": Received getStatus request, sid=" + sid + ", iid=" + iid);
PlaylistItem 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) {
String sid = mSessionManager.startSession();
Log.d(TAG, "StartSession returns sessionId "+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());
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(PlaylistItem 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(getContext(), 0, intent);
Log.d(TAG, mRouteId + ": Sending status update from provider");
} catch (PendingIntent.CanceledException e) {
Log.d(TAG, mRouteId + ": Failed to send status update!");
}
}
}
}
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(getContext(), 0, intent);
Log.d(TAG, mRouteId + ": Sending session status update from provider");
} catch (PendingIntent.CanceledException e) {
Log.d(TAG, mRouteId + ": Failed to send session status update!");
}
}
}
}
}