MediaSession.java revision 07c7077c54717dbbf2c401ea32d00fa6df6d77c6
1/* 2 * Copyright (C) 2014 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 android.media.session; 18 19import android.content.Intent; 20import android.media.Rating; 21import android.media.session.ISessionController; 22import android.media.session.ISession; 23import android.media.session.ISessionCallback; 24import android.os.Bundle; 25import android.os.Handler; 26import android.os.Looper; 27import android.os.Message; 28import android.os.RemoteException; 29import android.os.ResultReceiver; 30import android.text.TextUtils; 31import android.util.ArrayMap; 32import android.util.Log; 33 34import java.lang.ref.WeakReference; 35import java.util.ArrayList; 36import java.util.List; 37 38/** 39 * Allows interaction with media controllers, media routes, volume keys, media 40 * buttons, and transport controls. 41 * <p> 42 * A MediaSession should be created when an app wants to publish media playback 43 * information or negotiate with a media route. In general an app only needs one 44 * session for all playback, though multiple sessions can be created for sending 45 * media to multiple routes or to provide finer grain controls of media. 46 * <p> 47 * A MediaSession is created by calling 48 * {@link SessionManager#createSession(String)}. Once a session is created 49 * apps that have the MEDIA_CONTENT_CONTROL permission can interact with the 50 * session through {@link SessionManager#getActiveSessions()}. The owner of 51 * the session may also use {@link #getSessionToken()} to allow apps without 52 * this permission to create a {@link SessionController} to interact with this 53 * session. 54 * <p> 55 * To receive commands, media keys, and other events a Callback must be set with 56 * {@link #addCallback(Callback)}. 57 * <p> 58 * When an app is finished performing playback it must call {@link #release()} 59 * to clean up the session and notify any controllers. 60 * <p> 61 * MediaSession objects are thread safe 62 */ 63public final class Session { 64 private static final String TAG = "Session"; 65 66 private static final int MSG_MEDIA_BUTTON = 1; 67 private static final int MSG_COMMAND = 2; 68 private static final int MSG_ROUTE_CHANGE = 3; 69 private static final int MSG_ROUTE_CONNECTED = 4; 70 71 private static final String KEY_COMMAND = "command"; 72 private static final String KEY_EXTRAS = "extras"; 73 private static final String KEY_CALLBACK = "callback"; 74 75 private final Object mLock = new Object(); 76 77 private final SessionToken mSessionToken; 78 private final ISession mBinder; 79 private final CallbackStub mCbStub; 80 81 private final ArrayList<MessageHandler> mCallbacks = new ArrayList<MessageHandler>(); 82 // TODO route interfaces 83 private final ArrayMap<String, RouteInterface.EventListener> mInterfaceListeners 84 = new ArrayMap<String, RouteInterface.EventListener>(); 85 86 private TransportPerformer mPerformer; 87 private Route mRoute; 88 89 private boolean mPublished = false;; 90 91 /** 92 * @hide 93 */ 94 public Session(ISession binder, CallbackStub cbStub) { 95 mBinder = binder; 96 mCbStub = cbStub; 97 ISessionController controllerBinder = null; 98 try { 99 controllerBinder = mBinder.getController(); 100 } catch (RemoteException e) { 101 throw new RuntimeException("Dead object in MediaSessionController constructor: ", e); 102 } 103 mSessionToken = new SessionToken(controllerBinder); 104 } 105 106 /** 107 * Set the callback to receive updates on. 108 * 109 * @param callback The callback object 110 */ 111 public void addCallback(Callback callback) { 112 addCallback(callback, null); 113 } 114 115 /** 116 * Add a callback to receive updates for the MediaSession. This includes 117 * events like route updates, media buttons, and focus changes. 118 * 119 * @param callback The callback to receive updates on. 120 * @param handler The handler that events should be posted on. 121 */ 122 public void addCallback(Callback callback, Handler handler) { 123 if (callback == null) { 124 throw new IllegalArgumentException("Callback cannot be null"); 125 } 126 synchronized (mLock) { 127 if (getHandlerForCallbackLocked(callback) != null) { 128 Log.w(TAG, "Callback is already added, ignoring"); 129 return; 130 } 131 if (handler == null) { 132 handler = new Handler(); 133 } 134 MessageHandler msgHandler = new MessageHandler(handler.getLooper(), callback); 135 mCallbacks.add(msgHandler); 136 } 137 } 138 139 /** 140 * Remove a callback. It will no longer receive updates. 141 * 142 * @param callback The callback to remove. 143 */ 144 public void removeCallback(Callback callback) { 145 synchronized (mLock) { 146 removeCallbackLocked(callback); 147 } 148 } 149 150 /** 151 * Start using a TransportPerformer with this media session. This must be 152 * called before calling publish and cannot be called more than once. 153 * Calling this will allow MediaControllers to retrieve a 154 * TransportController. 155 * 156 * @see TransportController 157 * @return The TransportPerformer created for this session 158 */ 159 public TransportPerformer setTransportPerformerEnabled() { 160 if (mPerformer != null) { 161 throw new IllegalStateException("setTransportPerformer can only be called once."); 162 } 163 if (mPublished) { 164 throw new IllegalStateException("setTransportPerformer cannot be called after publish"); 165 } 166 167 mPerformer = new TransportPerformer(mBinder); 168 try { 169 mBinder.setTransportPerformerEnabled(); 170 } catch (RemoteException e) { 171 Log.wtf(TAG, "Failure in setTransportPerformerEnabled.", e); 172 } 173 return mPerformer; 174 } 175 176 /** 177 * Retrieves the TransportPerformer used by this session. If called before 178 * {@link #setTransportPerformerEnabled} null will be returned. 179 * 180 * @return The TransportPerformer associated with this session or null 181 */ 182 public TransportPerformer getTransportPerformer() { 183 return mPerformer; 184 } 185 186 /** 187 * Call after you have finished setting up the session. This will make it 188 * available to listeners and begin pushing updates to MediaControllers. 189 * This can only be called once. 190 */ 191 public void publish() { 192 if (mPublished) { 193 throw new RuntimeException("publish() may only be called once."); 194 } 195 try { 196 mBinder.publish(); 197 } catch (RemoteException e) { 198 Log.wtf(TAG, "Failure in publish.", e); 199 } 200 mPublished = true; 201 } 202 203 /** 204 * Send a proprietary event to all MediaControllers listening to this 205 * Session. It's up to the Controller/Session owner to determine the meaning 206 * of any events. 207 * 208 * @param event The name of the event to send 209 * @param extras Any extras included with the event 210 */ 211 public void sendEvent(String event, Bundle extras) { 212 if (TextUtils.isEmpty(event)) { 213 throw new IllegalArgumentException("event cannot be null or empty"); 214 } 215 try { 216 mBinder.sendEvent(event, extras); 217 } catch (RemoteException e) { 218 Log.wtf(TAG, "Error sending event", e); 219 } 220 } 221 222 /** 223 * This must be called when an app has finished performing playback. If 224 * playback is expected to start again shortly the session can be left open, 225 * but it must be released if your activity or service is being destroyed. 226 */ 227 public void release() { 228 try { 229 mBinder.destroy(); 230 } catch (RemoteException e) { 231 Log.wtf(TAG, "Error releasing session: ", e); 232 } 233 } 234 235 /** 236 * Retrieve a token object that can be used by apps to create a 237 * {@link SessionController} for interacting with this session. The owner of 238 * the session is responsible for deciding how to distribute these tokens. 239 * 240 * @return A token that can be used to create a MediaController for this 241 * session 242 */ 243 public SessionToken getSessionToken() { 244 return mSessionToken; 245 } 246 247 /** 248 * Connect to the current route using the specified request. 249 * <p> 250 * Connection updates will be sent to the callback's 251 * {@link Callback#onRouteConnected(Route)} and 252 * {@link Callback#onRouteDisconnected(Route, int)} methods. If the 253 * connection fails {@link Callback#onRouteDisconnected(Route, int)} 254 * will be called. 255 * <p> 256 * If you already have a connection to this route it will be disconnected 257 * before the new connection is established. TODO add an easy way to compare 258 * MediaRouteOptions. 259 * 260 * @param route The route the app is trying to connect to. 261 * @param request The connection request to use. 262 */ 263 public void connect(RouteInfo route, RouteOptions request) { 264 if (route == null) { 265 throw new IllegalArgumentException("Must specify the route"); 266 } 267 if (request == null) { 268 throw new IllegalArgumentException("Must specify the connection request"); 269 } 270 try { 271 mBinder.connectToRoute(route, request); 272 } catch (RemoteException e) { 273 Log.wtf(TAG, "Error starting connection to route", e); 274 } 275 } 276 277 /** 278 * Disconnect from the current route. After calling you will be switched 279 * back to the default route. 280 * 281 * @param route The route to disconnect from. 282 */ 283 public void disconnect(RouteInfo route) { 284 // TODO 285 } 286 287 /** 288 * Set the list of route options your app is interested in connecting to. It 289 * will be used for picking valid routes. 290 * 291 * @param options The set of route options your app may use to connect. 292 */ 293 public void setRouteOptions(List<RouteOptions> options) { 294 try { 295 mBinder.setRouteOptions(options); 296 } catch (RemoteException e) { 297 Log.wtf(TAG, "Error setting route options.", e); 298 } 299 } 300 301 /** 302 * @hide 303 * TODO allow multiple listeners for the same interface, allow removal 304 */ 305 public void addInterfaceListener(String iface, 306 RouteInterface.EventListener listener) { 307 mInterfaceListeners.put(iface, listener); 308 } 309 310 /** 311 * @hide 312 */ 313 public boolean sendRouteCommand(RouteCommand command, ResultReceiver cb) { 314 try { 315 mBinder.sendRouteCommand(command, cb); 316 } catch (RemoteException e) { 317 Log.wtf(TAG, "Error sending command to route.", e); 318 return false; 319 } 320 return true; 321 } 322 323 private MessageHandler getHandlerForCallbackLocked(Callback cb) { 324 if (cb == null) { 325 throw new IllegalArgumentException("Callback cannot be null"); 326 } 327 for (int i = mCallbacks.size() - 1; i >= 0; i--) { 328 MessageHandler handler = mCallbacks.get(i); 329 if (cb == handler.mCallback) { 330 return handler; 331 } 332 } 333 return null; 334 } 335 336 private boolean removeCallbackLocked(Callback cb) { 337 if (cb == null) { 338 throw new IllegalArgumentException("Callback cannot be null"); 339 } 340 for (int i = mCallbacks.size() - 1; i >= 0; i--) { 341 MessageHandler handler = mCallbacks.get(i); 342 if (cb == handler.mCallback) { 343 mCallbacks.remove(i); 344 return true; 345 } 346 } 347 return false; 348 } 349 350 private void postCommand(String command, Bundle extras, ResultReceiver resultCb) { 351 Command cmd = new Command(command, extras, resultCb); 352 synchronized (mLock) { 353 for (int i = mCallbacks.size() - 1; i >= 0; i--) { 354 mCallbacks.get(i).post(MSG_COMMAND, cmd); 355 } 356 } 357 } 358 359 private void postMediaButton(Intent mediaButtonIntent) { 360 synchronized (mLock) { 361 for (int i = mCallbacks.size() - 1; i >= 0; i--) { 362 mCallbacks.get(i).post(MSG_MEDIA_BUTTON, mediaButtonIntent); 363 } 364 } 365 } 366 367 private void postRequestRouteChange(RouteInfo route) { 368 synchronized (mLock) { 369 for (int i = mCallbacks.size() - 1; i >= 0; i--) { 370 mCallbacks.get(i).post(MSG_ROUTE_CHANGE, route); 371 } 372 } 373 } 374 375 private void postRouteConnected(RouteInfo route, RouteOptions options) { 376 synchronized (mLock) { 377 mRoute = new Route(route, options, this); 378 for (int i = mCallbacks.size() - 1; i >= 0; i--) { 379 mCallbacks.get(i).post(MSG_ROUTE_CONNECTED, mRoute); 380 } 381 } 382 } 383 384 /** 385 * Receives commands or updates from controllers and routes. An app can 386 * specify what commands and buttons it supports by setting them on the 387 * MediaSession (TODO). 388 */ 389 public abstract static class Callback { 390 391 public Callback() { 392 } 393 394 /** 395 * Called when a media button is pressed and this session has the 396 * highest priority or a controller sends a media button event to the 397 * session. TODO determine if using Intents identical to the ones 398 * RemoteControlClient receives is useful 399 * <p> 400 * The intent will be of type {@link Intent#ACTION_MEDIA_BUTTON} with a 401 * KeyEvent in {@link Intent#EXTRA_KEY_EVENT} 402 * 403 * @param mediaButtonIntent an intent containing the KeyEvent as an 404 * extra 405 */ 406 public void onMediaButton(Intent mediaButtonIntent) { 407 } 408 409 /** 410 * Called when a controller has sent a custom command to this session. 411 * The owner of the session may handle custom commands but is not 412 * required to. 413 * 414 * @param command 415 * @param extras optional 416 */ 417 public void onCommand(String command, Bundle extras, ResultReceiver cb) { 418 } 419 420 /** 421 * Called when the user has selected a different route to connect to. 422 * The app is responsible for connecting to the new route and migrating 423 * ongoing playback if necessary. 424 * 425 * @param route 426 */ 427 public void onRequestRouteChange(RouteInfo route) { 428 } 429 430 /** 431 * Called when a route has successfully connected. Calls to the route 432 * are now valid. 433 * 434 * @param route The route that was connected 435 */ 436 public void onRouteConnected(Route route) { 437 } 438 439 /** 440 * Called when a route was disconnected. Further calls to the route will 441 * fail. If available a reason for being disconnected will be provided. 442 * <p> 443 * Valid reasons are: 444 * <ul> 445 * </ul> 446 * 447 * @param route The route that disconnected 448 * @param reason The reason for the disconnect 449 */ 450 public void onRouteDisconnected(Route route, int reason) { 451 } 452 } 453 454 /** 455 * @hide 456 */ 457 public static class CallbackStub extends ISessionCallback.Stub { 458 private WeakReference<Session> mMediaSession; 459 460 public void setMediaSession(Session session) { 461 mMediaSession = new WeakReference<Session>(session); 462 } 463 464 @Override 465 public void onCommand(String command, Bundle extras, ResultReceiver cb) 466 throws RemoteException { 467 Session session = mMediaSession.get(); 468 if (session != null) { 469 session.postCommand(command, extras, cb); 470 } 471 } 472 473 @Override 474 public void onMediaButton(Intent mediaButtonIntent) throws RemoteException { 475 Session session = mMediaSession.get(); 476 if (session != null) { 477 session.postMediaButton(mediaButtonIntent); 478 } 479 } 480 481 @Override 482 public void onRequestRouteChange(RouteInfo route) throws RemoteException { 483 Session session = mMediaSession.get(); 484 if (session != null) { 485 session.postRequestRouteChange(route); 486 } 487 } 488 489 @Override 490 public void onRouteConnected(RouteInfo route, RouteOptions options) { 491 Session session = mMediaSession.get(); 492 if (session != null) { 493 session.postRouteConnected(route, options); 494 } 495 } 496 497 @Override 498 public void onPlay() throws RemoteException { 499 Session session = mMediaSession.get(); 500 if (session != null) { 501 TransportPerformer tp = session.getTransportPerformer(); 502 if (tp != null) { 503 tp.onPlay(); 504 } 505 } 506 } 507 508 @Override 509 public void onPause() throws RemoteException { 510 Session session = mMediaSession.get(); 511 if (session != null) { 512 TransportPerformer tp = session.getTransportPerformer(); 513 if (tp != null) { 514 tp.onPause(); 515 } 516 } 517 } 518 519 @Override 520 public void onStop() throws RemoteException { 521 Session session = mMediaSession.get(); 522 if (session != null) { 523 TransportPerformer tp = session.getTransportPerformer(); 524 if (tp != null) { 525 tp.onStop(); 526 } 527 } 528 } 529 530 @Override 531 public void onNext() throws RemoteException { 532 Session session = mMediaSession.get(); 533 if (session != null) { 534 TransportPerformer tp = session.getTransportPerformer(); 535 if (tp != null) { 536 tp.onNext(); 537 } 538 } 539 } 540 541 @Override 542 public void onPrevious() throws RemoteException { 543 Session session = mMediaSession.get(); 544 if (session != null) { 545 TransportPerformer tp = session.getTransportPerformer(); 546 if (tp != null) { 547 tp.onPrevious(); 548 } 549 } 550 } 551 552 @Override 553 public void onFastForward() throws RemoteException { 554 Session session = mMediaSession.get(); 555 if (session != null) { 556 TransportPerformer tp = session.getTransportPerformer(); 557 if (tp != null) { 558 tp.onFastForward(); 559 } 560 } 561 } 562 563 @Override 564 public void onRewind() throws RemoteException { 565 Session session = mMediaSession.get(); 566 if (session != null) { 567 TransportPerformer tp = session.getTransportPerformer(); 568 if (tp != null) { 569 tp.onRewind(); 570 } 571 } 572 } 573 574 @Override 575 public void onSeekTo(long pos) throws RemoteException { 576 Session session = mMediaSession.get(); 577 if (session != null) { 578 TransportPerformer tp = session.getTransportPerformer(); 579 if (tp != null) { 580 tp.onSeekTo(pos); 581 } 582 } 583 } 584 585 @Override 586 public void onRate(Rating rating) throws RemoteException { 587 Session session = mMediaSession.get(); 588 if (session != null) { 589 TransportPerformer tp = session.getTransportPerformer(); 590 if (tp != null) { 591 tp.onRate(rating); 592 } 593 } 594 } 595 596 @Override 597 public void onRouteEvent(RouteEvent event) throws RemoteException { 598 Session session = mMediaSession.get(); 599 if (session != null) { 600 RouteInterface.EventListener iface 601 = session.mInterfaceListeners.get(event.getIface()); 602 Log.d(TAG, "Received route event on iface " + event.getIface() + ". Listener is " 603 + iface); 604 if (iface != null) { 605 iface.onEvent(event.getEvent(), event.getExtras()); 606 } 607 } 608 } 609 610 @Override 611 public void onRouteStateChange(int state) throws RemoteException { 612 // TODO 613 614 } 615 616 } 617 618 private class MessageHandler extends Handler { 619 private Session.Callback mCallback; 620 621 public MessageHandler(Looper looper, Session.Callback callback) { 622 super(looper, null, true); 623 mCallback = callback; 624 } 625 626 @Override 627 public void handleMessage(Message msg) { 628 synchronized (mLock) { 629 if (mCallback == null) { 630 return; 631 } 632 switch (msg.what) { 633 case MSG_MEDIA_BUTTON: 634 mCallback.onMediaButton((Intent) msg.obj); 635 break; 636 case MSG_COMMAND: 637 Command cmd = (Command) msg.obj; 638 mCallback.onCommand(cmd.command, cmd.extras, cmd.stub); 639 break; 640 case MSG_ROUTE_CHANGE: 641 mCallback.onRequestRouteChange((RouteInfo) msg.obj); 642 break; 643 case MSG_ROUTE_CONNECTED: 644 mCallback.onRouteConnected((Route) msg.obj); 645 break; 646 } 647 } 648 } 649 650 public void post(int what, Object obj) { 651 obtainMessage(what, obj).sendToTarget(); 652 } 653 } 654 655 private static final class Command { 656 public final String command; 657 public final Bundle extras; 658 public final ResultReceiver stub; 659 660 public Command(String command, Bundle extras, ResultReceiver stub) { 661 this.command = command; 662 this.extras = extras; 663 this.stub = stub; 664 } 665 } 666} 667