SampleMediaRouteProvider.java revision 8e006e629800b4a2643416f97bca2711af728837
1/* 2 * Copyright (C) 2013 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17package com.example.android.supportv7.media; 18 19import com.example.android.supportv7.R; 20 21import android.content.Context; 22import android.content.Intent; 23import android.content.IntentFilter; 24import android.content.IntentFilter.MalformedMimeTypeException; 25import android.content.res.Resources; 26import android.media.AudioManager; 27import android.media.MediaRouter; 28import android.net.Uri; 29import android.os.Bundle; 30import android.app.PendingIntent; 31import android.support.v7.media.MediaControlIntent; 32import android.support.v7.media.MediaItemStatus; 33import android.support.v7.media.MediaRouteProvider; 34import android.support.v7.media.MediaRouter.ControlRequestCallback; 35import android.support.v7.media.MediaRouteProviderDescriptor; 36import android.support.v7.media.MediaRouteDescriptor; 37import android.util.Log; 38import android.view.Gravity; 39import android.view.Surface; 40import android.view.SurfaceHolder; 41 42import java.util.ArrayList; 43 44/** 45 * Demonstrates how to create a custom media route provider. 46 * 47 * @see SampleMediaRouteProviderService 48 */ 49final class SampleMediaRouteProvider extends MediaRouteProvider { 50 private static final String TAG = "SampleMediaRouteProvider"; 51 52 private static final String FIXED_VOLUME_ROUTE_ID = "fixed"; 53 private static final String VARIABLE_VOLUME_ROUTE_ID = "variable"; 54 private static final int VOLUME_MAX = 10; 55 56 /** 57 * A custom media control intent category for special requests that are 58 * supported by this provider's routes. 59 */ 60 public static final String CATEGORY_SAMPLE_ROUTE = 61 "com.example.android.supportv7.media.CATEGORY_SAMPLE_ROUTE"; 62 63 /** 64 * A custom media control intent action for special requests that are 65 * supported by this provider's routes. 66 * <p> 67 * This particular request is designed to return a bundle of not very 68 * interesting statistics for demonstration purposes. 69 * </p> 70 * 71 * @see #DATA_PLAYBACK_COUNT 72 */ 73 public static final String ACTION_GET_STATISTICS = 74 "com.example.android.supportv7.media.ACTION_GET_STATISTICS"; 75 76 /** 77 * {@link #ACTION_GET_STATISTICS} result data: Number of times the 78 * playback action was invoked. 79 */ 80 public static final String DATA_PLAYBACK_COUNT = 81 "com.example.android.supportv7.media.EXTRA_PLAYBACK_COUNT"; 82 83 /* 84 * Set ENABLE_QUEUEING to true to test queuing on MRP. This will make 85 * MRP expose the following two experimental hidden APIs: 86 * ACTION_ENQUEUE 87 * ACTION_REMOVE 88 */ 89 public static final boolean ENABLE_QUEUEING = false; 90 91 private static final ArrayList<IntentFilter> CONTROL_FILTERS; 92 static { 93 IntentFilter f1 = new IntentFilter(); 94 f1.addCategory(CATEGORY_SAMPLE_ROUTE); 95 f1.addAction(ACTION_GET_STATISTICS); 96 97 IntentFilter f2 = new IntentFilter(); 98 f2.addCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK); 99 f2.addAction(MediaControlIntent.ACTION_PLAY); 100 f2.addDataScheme("http"); 101 f2.addDataScheme("https"); 102 f2.addDataScheme("rtsp"); 103 f2.addDataScheme("file"); 104 addDataTypeUnchecked(f2, "video/*"); 105 106 IntentFilter f3 = new IntentFilter(); 107 f3.addCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK); 108 f3.addAction(MediaControlIntent.ACTION_SEEK); 109 f3.addAction(MediaControlIntent.ACTION_GET_STATUS); 110 f3.addAction(MediaControlIntent.ACTION_PAUSE); 111 f3.addAction(MediaControlIntent.ACTION_RESUME); 112 f3.addAction(MediaControlIntent.ACTION_STOP); 113 114 IntentFilter f4 = new IntentFilter(); 115 f4.addCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK); 116 f4.addAction(MediaControlIntent.ACTION_ENQUEUE); 117 f4.addDataScheme("http"); 118 f4.addDataScheme("https"); 119 f4.addDataScheme("rtsp"); 120 f4.addDataScheme("file"); 121 addDataTypeUnchecked(f4, "video/*"); 122 123 IntentFilter f5 = new IntentFilter(); 124 f5.addCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK); 125 f5.addAction(MediaControlIntent.ACTION_REMOVE); 126 127 CONTROL_FILTERS = new ArrayList<IntentFilter>(); 128 CONTROL_FILTERS.add(f1); 129 CONTROL_FILTERS.add(f2); 130 CONTROL_FILTERS.add(f3); 131 if (ENABLE_QUEUEING) { 132 CONTROL_FILTERS.add(f4); 133 CONTROL_FILTERS.add(f5); 134 } 135 } 136 137 private static void addDataTypeUnchecked(IntentFilter filter, String type) { 138 try { 139 filter.addDataType(type); 140 } catch (MalformedMimeTypeException ex) { 141 throw new RuntimeException(ex); 142 } 143 } 144 145 private int mVolume = 5; 146 private int mEnqueueCount; 147 148 public SampleMediaRouteProvider(Context context) { 149 super(context); 150 151 publishRoutes(); 152 } 153 154 @Override 155 public RouteController onCreateRouteController(String routeId) { 156 return new SampleRouteController(routeId); 157 } 158 159 private void publishRoutes() { 160 Resources r = getContext().getResources(); 161 162 MediaRouteDescriptor routeDescriptor1 = new MediaRouteDescriptor.Builder( 163 FIXED_VOLUME_ROUTE_ID, 164 r.getString(R.string.fixed_volume_route_name)) 165 .setDescription(r.getString(R.string.sample_route_description)) 166 .addControlFilters(CONTROL_FILTERS) 167 .setPlaybackStream(AudioManager.STREAM_MUSIC) 168 .setPlaybackType(MediaRouter.RouteInfo.PLAYBACK_TYPE_REMOTE) 169 .setVolumeHandling(MediaRouter.RouteInfo.PLAYBACK_VOLUME_FIXED) 170 .setVolume(VOLUME_MAX) 171 .build(); 172 173 MediaRouteDescriptor routeDescriptor2 = new MediaRouteDescriptor.Builder( 174 VARIABLE_VOLUME_ROUTE_ID, 175 r.getString(R.string.variable_volume_route_name)) 176 .setDescription(r.getString(R.string.sample_route_description)) 177 .addControlFilters(CONTROL_FILTERS) 178 .setPlaybackStream(AudioManager.STREAM_MUSIC) 179 .setPlaybackType(MediaRouter.RouteInfo.PLAYBACK_TYPE_REMOTE) 180 .setVolumeHandling(MediaRouter.RouteInfo.PLAYBACK_VOLUME_VARIABLE) 181 .setVolumeMax(VOLUME_MAX) 182 .setVolume(mVolume) 183 .build(); 184 185 MediaRouteProviderDescriptor providerDescriptor = 186 new MediaRouteProviderDescriptor.Builder() 187 .addRoute(routeDescriptor1) 188 .addRoute(routeDescriptor2) 189 .build(); 190 setDescriptor(providerDescriptor); 191 } 192 193 private final class SampleRouteController extends MediaRouteProvider.RouteController { 194 private final String mRouteId; 195 private final OverlayDisplayWindow mOverlay; 196 private final MediaPlayerWrapper mMediaPlayer; 197 private final MediaSessionManager mSessionManager; 198 199 public SampleRouteController(String routeId) { 200 mRouteId = routeId; 201 mMediaPlayer = new MediaPlayerWrapper(getContext()); 202 mSessionManager = new MediaSessionManager(); 203 mSessionManager.setCallback(mMediaPlayer); 204 205 // Create an overlay display window (used for simulating the remote playback only) 206 mOverlay = OverlayDisplayWindow.create(getContext(), 207 getContext().getResources().getString( 208 R.string.sample_media_route_provider_remote), 209 1024, 768, Gravity.CENTER); 210 mOverlay.setOverlayWindowListener(new OverlayDisplayWindow.OverlayWindowListener() { 211 @Override 212 public void onWindowCreated(Surface surface) { 213 mMediaPlayer.setSurface(surface); 214 } 215 216 @Override 217 public void onWindowCreated(SurfaceHolder surfaceHolder) { 218 mMediaPlayer.setSurface(surfaceHolder); 219 } 220 221 @Override 222 public void onWindowDestroyed() { 223 } 224 }); 225 226 mMediaPlayer.setCallback(new MediaPlayerCallback()); 227 Log.d(TAG, mRouteId + ": Controller created"); 228 } 229 230 @Override 231 public void onRelease() { 232 Log.d(TAG, mRouteId + ": Controller released"); 233 mMediaPlayer.release(); 234 } 235 236 @Override 237 public void onSelect() { 238 Log.d(TAG, mRouteId + ": Selected"); 239 mOverlay.show(); 240 } 241 242 @Override 243 public void onUnselect() { 244 Log.d(TAG, mRouteId + ": Unselected"); 245 mMediaPlayer.onStop(); 246 mOverlay.dismiss(); 247 } 248 249 @Override 250 public void onSetVolume(int volume) { 251 Log.d(TAG, mRouteId + ": Set volume to " + volume); 252 if (mRouteId.equals(VARIABLE_VOLUME_ROUTE_ID)) { 253 setVolumeInternal(volume); 254 } 255 } 256 257 @Override 258 public void onUpdateVolume(int delta) { 259 Log.d(TAG, mRouteId + ": Update volume by " + delta); 260 if (mRouteId.equals(VARIABLE_VOLUME_ROUTE_ID)) { 261 setVolumeInternal(mVolume + delta); 262 } 263 } 264 265 @Override 266 public boolean onControlRequest(Intent intent, ControlRequestCallback callback) { 267 Log.d(TAG, mRouteId + ": Received control request " + intent); 268 String action = intent.getAction(); 269 if (intent.hasCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK)) { 270 boolean success = false; 271 if (action.equals(MediaControlIntent.ACTION_PLAY)) { 272 success = handlePlay(intent, callback); 273 } else if (action.equals(MediaControlIntent.ACTION_ENQUEUE)) { 274 success = handleEnqueue(intent, callback); 275 } else if (action.equals(MediaControlIntent.ACTION_REMOVE)) { 276 success = handleRemove(intent, callback); 277 } else if (action.equals(MediaControlIntent.ACTION_SEEK)) { 278 success = handleSeek(intent, callback); 279 } else if (action.equals(MediaControlIntent.ACTION_GET_STATUS)) { 280 success = handleGetStatus(intent, callback); 281 } else if (action.equals(MediaControlIntent.ACTION_PAUSE)) { 282 success = handlePause(intent, callback); 283 } else if (action.equals(MediaControlIntent.ACTION_RESUME)) { 284 success = handleResume(intent, callback); 285 } else if (action.equals(MediaControlIntent.ACTION_STOP)) { 286 success = handleStop(intent, callback); 287 } 288 Log.d(TAG, mSessionManager.toString()); 289 return success; 290 } 291 292 if (action.equals(ACTION_GET_STATISTICS) 293 && intent.hasCategory(CATEGORY_SAMPLE_ROUTE)) { 294 Bundle data = new Bundle(); 295 data.putInt(DATA_PLAYBACK_COUNT, mEnqueueCount); 296 if (callback != null) { 297 callback.onResult(data); 298 } 299 return true; 300 } 301 return false; 302 } 303 304 private void setVolumeInternal(int volume) { 305 if (volume >= 0 && volume <= VOLUME_MAX) { 306 mVolume = volume; 307 Log.d(TAG, mRouteId + ": New volume is " + mVolume); 308 AudioManager audioManager = 309 (AudioManager)getContext().getSystemService(Context.AUDIO_SERVICE); 310 audioManager.setStreamVolume(AudioManager.STREAM_MUSIC, volume, 0); 311 publishRoutes(); 312 } 313 } 314 315 private boolean handlePlay(Intent intent, ControlRequestCallback callback) { 316 String sid = intent.getStringExtra(MediaControlIntent.EXTRA_SESSION_ID); 317 if (sid == null || mSessionManager.stop(sid)) { 318 Log.d(TAG, "handleEnqueue"); 319 return handleEnqueue(intent, callback); 320 } 321 return false; 322 } 323 324 private boolean handleEnqueue(Intent intent, ControlRequestCallback callback) { 325 if (intent.getData() == null) { 326 return false; 327 } 328 329 mEnqueueCount +=1; 330 331 boolean enqueue = intent.getAction().equals(MediaControlIntent.ACTION_ENQUEUE); 332 Uri uri = intent.getData(); 333 String sid = intent.getStringExtra(MediaControlIntent.EXTRA_SESSION_ID); 334 long pos = intent.getLongExtra(MediaControlIntent.EXTRA_ITEM_CONTENT_POSITION, 0); 335 Bundle metadata = intent.getBundleExtra(MediaControlIntent.EXTRA_ITEM_METADATA); 336 Bundle headers = intent.getBundleExtra(MediaControlIntent.EXTRA_ITEM_HTTP_HEADERS); 337 PendingIntent receiver = (PendingIntent)intent.getParcelableExtra( 338 MediaControlIntent.EXTRA_ITEM_STATUS_UPDATE_RECEIVER); 339 340 Log.d(TAG, mRouteId + ": Received " + (enqueue?"enqueue":"play") + " request" 341 + ", uri=" + uri 342 + ", sid=" + sid 343 + ", pos=" + pos 344 + ", metadata=" + metadata 345 + ", headers=" + headers 346 + ", receiver=" + receiver); 347 MediaQueueItem item = mSessionManager.enqueue(sid, uri, receiver); 348 if (callback != null) { 349 if (item != null) { 350 Bundle result = new Bundle(); 351 result.putString(MediaControlIntent.EXTRA_SESSION_ID, item.getSessionId()); 352 result.putString(MediaControlIntent.EXTRA_ITEM_ID, item.getItemId()); 353 result.putBundle(MediaControlIntent.EXTRA_ITEM_STATUS, 354 item.getStatus().asBundle()); 355 callback.onResult(result); 356 } else { 357 callback.onError("Failed to open " + uri.toString(), null); 358 } 359 } 360 return true; 361 } 362 363 private boolean handleRemove(Intent intent, ControlRequestCallback callback) { 364 String sid = intent.getStringExtra(MediaControlIntent.EXTRA_SESSION_ID); 365 String iid = intent.getStringExtra(MediaControlIntent.EXTRA_ITEM_ID); 366 MediaQueueItem item = mSessionManager.remove(sid, iid); 367 if (callback != null) { 368 if (item != null) { 369 Bundle result = new Bundle(); 370 result.putBundle(MediaControlIntent.EXTRA_ITEM_STATUS, 371 item.getStatus().asBundle()); 372 callback.onResult(result); 373 } else { 374 callback.onError("Failed to remove" + 375 ", sid=" + sid + ", iid=" + iid, null); 376 } 377 } 378 return (item != null); 379 } 380 381 private boolean handleSeek(Intent intent, ControlRequestCallback callback) { 382 String sid = intent.getStringExtra(MediaControlIntent.EXTRA_SESSION_ID); 383 String iid = intent.getStringExtra(MediaControlIntent.EXTRA_ITEM_ID); 384 long pos = intent.getLongExtra(MediaControlIntent.EXTRA_ITEM_CONTENT_POSITION, 0); 385 Log.d(TAG, mRouteId + ": Received seek request, pos=" + pos); 386 MediaQueueItem item = mSessionManager.seek(sid, iid, pos); 387 if (callback != null) { 388 if (item != null) { 389 Bundle result = new Bundle(); 390 result.putBundle(MediaControlIntent.EXTRA_ITEM_STATUS, 391 item.getStatus().asBundle()); 392 callback.onResult(result); 393 } else { 394 callback.onError("Failed to seek" + 395 ", sid=" + sid + ", iid=" + iid + ", pos=" + pos, null); 396 } 397 } 398 return (item != null); 399 } 400 401 private boolean handleGetStatus(Intent intent, ControlRequestCallback callback) { 402 String sid = intent.getStringExtra(MediaControlIntent.EXTRA_SESSION_ID); 403 String iid = intent.getStringExtra(MediaControlIntent.EXTRA_ITEM_ID); 404 MediaQueueItem item = mSessionManager.getStatus(sid, iid); 405 if (callback != null) { 406 if (item != null) { 407 Bundle result = new Bundle(); 408 result.putBundle(MediaControlIntent.EXTRA_ITEM_STATUS, 409 item.getStatus().asBundle()); 410 callback.onResult(result); 411 } else { 412 callback.onError("Failed to get status" + 413 ", sid=" + sid + ", iid=" + iid, null); 414 } 415 } 416 return (item != null); 417 } 418 419 private boolean handlePause(Intent intent, ControlRequestCallback callback) { 420 String sid = intent.getStringExtra(MediaControlIntent.EXTRA_SESSION_ID); 421 boolean success = mSessionManager.pause(sid); 422 if (callback != null) { 423 if (success) { 424 callback.onResult(null); 425 } else { 426 callback.onError("Failed to pause, sid=" + sid, null); 427 } 428 } 429 return success; 430 } 431 432 private boolean handleResume(Intent intent, ControlRequestCallback callback) { 433 String sid = intent.getStringExtra(MediaControlIntent.EXTRA_SESSION_ID); 434 boolean success = mSessionManager.resume(sid); 435 if (callback != null) { 436 if (success) { 437 callback.onResult(null); 438 } else { 439 callback.onError("Failed to resume, sid=" + sid, null); 440 } 441 } 442 return success; 443 } 444 445 private boolean handleStop(Intent intent, ControlRequestCallback callback) { 446 String sid = intent.getStringExtra(MediaControlIntent.EXTRA_SESSION_ID); 447 boolean success = mSessionManager.stop(sid); 448 if (callback != null) { 449 if (success) { 450 callback.onResult(null); 451 } else { 452 callback.onError("Failed to stop, sid=" + sid, null); 453 } 454 } 455 return success; 456 } 457 458 private void handleFinish(boolean error) { 459 MediaQueueItem item = mSessionManager.finish(error); 460 if (item != null) { 461 handleStatusChange(item); 462 } 463 } 464 465 private void handleStatusChange(MediaQueueItem item) { 466 if (item == null) { 467 item = mSessionManager.getCurrentItem(); 468 } 469 if (item != null) { 470 PendingIntent receiver = item.getUpdateReceiver(); 471 if (receiver != null) { 472 Intent intent = new Intent(); 473 intent.putExtra(MediaControlIntent.EXTRA_SESSION_ID, item.getSessionId()); 474 intent.putExtra(MediaControlIntent.EXTRA_ITEM_ID, item.getItemId()); 475 intent.putExtra(MediaControlIntent.EXTRA_ITEM_STATUS, 476 item.getStatus().asBundle()); 477 try { 478 receiver.send(getContext(), 0, intent); 479 Log.d(TAG, mRouteId + ": Sending status update from provider"); 480 } catch (PendingIntent.CanceledException e) { 481 Log.d(TAG, mRouteId + ": Failed to send status update!"); 482 } 483 } 484 } 485 } 486 487 private final class MediaPlayerCallback extends MediaPlayerWrapper.Callback { 488 @Override 489 public void onError() { 490 handleFinish(true); 491 } 492 493 @Override 494 public void onCompletion() { 495 handleFinish(false); 496 } 497 498 @Override 499 public void onStatusChanged() { 500 handleStatusChange(null); 501 } 502 503 @Override 504 public void onSizeChanged(int width, int height) { 505 mOverlay.updateAspectRatio(width, height); 506 } 507 } 508 } 509}