/*
* Copyright (C) 2014 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 android.media.session;
import android.content.Intent;
import android.media.Rating;
import android.media.session.ISessionController;
import android.media.session.ISession;
import android.media.session.ISessionCallback;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.os.RemoteException;
import android.os.ResultReceiver;
import android.text.TextUtils;
import android.util.ArrayMap;
import android.util.Log;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.List;
/**
* Allows interaction with media controllers, media routes, volume keys, media
* buttons, and transport controls.
*
* A MediaSession should be created when an app wants to publish media playback
* information or negotiate with a media route. In general an app only needs one
* session for all playback, though multiple sessions can be created for sending
* media to multiple routes or to provide finer grain controls of media.
*
* A MediaSession is created by calling
* {@link SessionManager#createSession(String)}. Once a session is created
* apps that have the MEDIA_CONTENT_CONTROL permission can interact with the
* session through {@link SessionManager#getActiveSessions()}. The owner of
* the session may also use {@link #getSessionToken()} to allow apps without
* this permission to create a {@link SessionController} to interact with this
* session.
*
* To receive commands, media keys, and other events a Callback must be set with
* {@link #addCallback(Callback)}.
*
* When an app is finished performing playback it must call {@link #release()}
* to clean up the session and notify any controllers.
*
* MediaSession objects are thread safe
*/
public final class Session {
private static final String TAG = "Session";
private static final int MSG_MEDIA_BUTTON = 1;
private static final int MSG_COMMAND = 2;
private static final int MSG_ROUTE_CHANGE = 3;
private static final int MSG_ROUTE_CONNECTED = 4;
private static final String KEY_COMMAND = "command";
private static final String KEY_EXTRAS = "extras";
private static final String KEY_CALLBACK = "callback";
private final Object mLock = new Object();
private final SessionToken mSessionToken;
private final ISession mBinder;
private final CallbackStub mCbStub;
private final ArrayList mCallbacks = new ArrayList();
// TODO route interfaces
private final ArrayMap mInterfaceListeners
= new ArrayMap();
private TransportPerformer mPerformer;
private Route mRoute;
private boolean mPublished = false;;
/**
* @hide
*/
public Session(ISession binder, CallbackStub cbStub) {
mBinder = binder;
mCbStub = cbStub;
ISessionController controllerBinder = null;
try {
controllerBinder = mBinder.getController();
} catch (RemoteException e) {
throw new RuntimeException("Dead object in MediaSessionController constructor: ", e);
}
mSessionToken = new SessionToken(controllerBinder);
}
/**
* Set the callback to receive updates on.
*
* @param callback The callback object
*/
public void addCallback(Callback callback) {
addCallback(callback, null);
}
/**
* Add a callback to receive updates for the MediaSession. This includes
* events like route updates, media buttons, and focus changes.
*
* @param callback The callback to receive updates on.
* @param handler The handler that events should be posted on.
*/
public void addCallback(Callback callback, Handler handler) {
if (callback == null) {
throw new IllegalArgumentException("Callback cannot be null");
}
synchronized (mLock) {
if (getHandlerForCallbackLocked(callback) != null) {
Log.w(TAG, "Callback is already added, ignoring");
return;
}
if (handler == null) {
handler = new Handler();
}
MessageHandler msgHandler = new MessageHandler(handler.getLooper(), callback);
mCallbacks.add(msgHandler);
}
}
/**
* Remove a callback. It will no longer receive updates.
*
* @param callback The callback to remove.
*/
public void removeCallback(Callback callback) {
synchronized (mLock) {
removeCallbackLocked(callback);
}
}
/**
* Start using a TransportPerformer with this media session. This must be
* called before calling publish and cannot be called more than once.
* Calling this will allow MediaControllers to retrieve a
* TransportController.
*
* @see TransportController
* @return The TransportPerformer created for this session
*/
public TransportPerformer setTransportPerformerEnabled() {
if (mPerformer != null) {
throw new IllegalStateException("setTransportPerformer can only be called once.");
}
if (mPublished) {
throw new IllegalStateException("setTransportPerformer cannot be called after publish");
}
mPerformer = new TransportPerformer(mBinder);
try {
mBinder.setTransportPerformerEnabled();
} catch (RemoteException e) {
Log.wtf(TAG, "Failure in setTransportPerformerEnabled.", e);
}
return mPerformer;
}
/**
* Retrieves the TransportPerformer used by this session. If called before
* {@link #setTransportPerformerEnabled} null will be returned.
*
* @return The TransportPerformer associated with this session or null
*/
public TransportPerformer getTransportPerformer() {
return mPerformer;
}
/**
* Call after you have finished setting up the session. This will make it
* available to listeners and begin pushing updates to MediaControllers.
* This can only be called once.
*/
public void publish() {
if (mPublished) {
throw new RuntimeException("publish() may only be called once.");
}
try {
mBinder.publish();
} catch (RemoteException e) {
Log.wtf(TAG, "Failure in publish.", e);
}
mPublished = true;
}
/**
* Send a proprietary event to all MediaControllers listening to this
* Session. It's up to the Controller/Session owner to determine the meaning
* of any events.
*
* @param event The name of the event to send
* @param extras Any extras included with the event
*/
public void sendEvent(String event, Bundle extras) {
if (TextUtils.isEmpty(event)) {
throw new IllegalArgumentException("event cannot be null or empty");
}
try {
mBinder.sendEvent(event, extras);
} catch (RemoteException e) {
Log.wtf(TAG, "Error sending event", e);
}
}
/**
* This must be called when an app has finished performing playback. If
* playback is expected to start again shortly the session can be left open,
* but it must be released if your activity or service is being destroyed.
*/
public void release() {
try {
mBinder.destroy();
} catch (RemoteException e) {
Log.wtf(TAG, "Error releasing session: ", e);
}
}
/**
* Retrieve a token object that can be used by apps to create a
* {@link SessionController} for interacting with this session. The owner of
* the session is responsible for deciding how to distribute these tokens.
*
* @return A token that can be used to create a MediaController for this
* session
*/
public SessionToken getSessionToken() {
return mSessionToken;
}
/**
* Connect to the current route using the specified request.
*
* Connection updates will be sent to the callback's
* {@link Callback#onRouteConnected(Route)} and
* {@link Callback#onRouteDisconnected(Route, int)} methods. If the
* connection fails {@link Callback#onRouteDisconnected(Route, int)}
* will be called.
*
* If you already have a connection to this route it will be disconnected
* before the new connection is established. TODO add an easy way to compare
* MediaRouteOptions.
*
* @param route The route the app is trying to connect to.
* @param request The connection request to use.
*/
public void connect(RouteInfo route, RouteOptions request) {
if (route == null) {
throw new IllegalArgumentException("Must specify the route");
}
if (request == null) {
throw new IllegalArgumentException("Must specify the connection request");
}
try {
mBinder.connectToRoute(route, request);
} catch (RemoteException e) {
Log.wtf(TAG, "Error starting connection to route", e);
}
}
/**
* Disconnect from the current route. After calling you will be switched
* back to the default route.
*
* @param route The route to disconnect from.
*/
public void disconnect(RouteInfo route) {
// TODO
}
/**
* Set the list of route options your app is interested in connecting to. It
* will be used for picking valid routes.
*
* @param options The set of route options your app may use to connect.
*/
public void setRouteOptions(List options) {
try {
mBinder.setRouteOptions(options);
} catch (RemoteException e) {
Log.wtf(TAG, "Error setting route options.", e);
}
}
/**
* @hide
* TODO allow multiple listeners for the same interface, allow removal
*/
public void addInterfaceListener(String iface,
RouteInterface.EventListener listener) {
mInterfaceListeners.put(iface, listener);
}
/**
* @hide
*/
public boolean sendRouteCommand(RouteCommand command, ResultReceiver cb) {
try {
mBinder.sendRouteCommand(command, cb);
} catch (RemoteException e) {
Log.wtf(TAG, "Error sending command to route.", e);
return false;
}
return true;
}
private MessageHandler getHandlerForCallbackLocked(Callback cb) {
if (cb == null) {
throw new IllegalArgumentException("Callback cannot be null");
}
for (int i = mCallbacks.size() - 1; i >= 0; i--) {
MessageHandler handler = mCallbacks.get(i);
if (cb == handler.mCallback) {
return handler;
}
}
return null;
}
private boolean removeCallbackLocked(Callback cb) {
if (cb == null) {
throw new IllegalArgumentException("Callback cannot be null");
}
for (int i = mCallbacks.size() - 1; i >= 0; i--) {
MessageHandler handler = mCallbacks.get(i);
if (cb == handler.mCallback) {
mCallbacks.remove(i);
return true;
}
}
return false;
}
private void postCommand(String command, Bundle extras, ResultReceiver resultCb) {
Command cmd = new Command(command, extras, resultCb);
synchronized (mLock) {
for (int i = mCallbacks.size() - 1; i >= 0; i--) {
mCallbacks.get(i).post(MSG_COMMAND, cmd);
}
}
}
private void postMediaButton(Intent mediaButtonIntent) {
synchronized (mLock) {
for (int i = mCallbacks.size() - 1; i >= 0; i--) {
mCallbacks.get(i).post(MSG_MEDIA_BUTTON, mediaButtonIntent);
}
}
}
private void postRequestRouteChange(RouteInfo route) {
synchronized (mLock) {
for (int i = mCallbacks.size() - 1; i >= 0; i--) {
mCallbacks.get(i).post(MSG_ROUTE_CHANGE, route);
}
}
}
private void postRouteConnected(RouteInfo route, RouteOptions options) {
synchronized (mLock) {
mRoute = new Route(route, options, this);
for (int i = mCallbacks.size() - 1; i >= 0; i--) {
mCallbacks.get(i).post(MSG_ROUTE_CONNECTED, mRoute);
}
}
}
/**
* Receives commands or updates from controllers and routes. An app can
* specify what commands and buttons it supports by setting them on the
* MediaSession (TODO).
*/
public abstract static class Callback {
public Callback() {
}
/**
* Called when a media button is pressed and this session has the
* highest priority or a controller sends a media button event to the
* session. TODO determine if using Intents identical to the ones
* RemoteControlClient receives is useful
*
* The intent will be of type {@link Intent#ACTION_MEDIA_BUTTON} with a
* KeyEvent in {@link Intent#EXTRA_KEY_EVENT}
*
* @param mediaButtonIntent an intent containing the KeyEvent as an
* extra
*/
public void onMediaButton(Intent mediaButtonIntent) {
}
/**
* Called when a controller has sent a custom command to this session.
* The owner of the session may handle custom commands but is not
* required to.
*
* @param command
* @param extras optional
*/
public void onCommand(String command, Bundle extras, ResultReceiver cb) {
}
/**
* Called when the user has selected a different route to connect to.
* The app is responsible for connecting to the new route and migrating
* ongoing playback if necessary.
*
* @param route
*/
public void onRequestRouteChange(RouteInfo route) {
}
/**
* Called when a route has successfully connected. Calls to the route
* are now valid.
*
* @param route The route that was connected
*/
public void onRouteConnected(Route route) {
}
/**
* Called when a route was disconnected. Further calls to the route will
* fail. If available a reason for being disconnected will be provided.
*
* Valid reasons are:
*
*
* @param route The route that disconnected
* @param reason The reason for the disconnect
*/
public void onRouteDisconnected(Route route, int reason) {
}
}
/**
* @hide
*/
public static class CallbackStub extends ISessionCallback.Stub {
private WeakReference mMediaSession;
public void setMediaSession(Session session) {
mMediaSession = new WeakReference(session);
}
@Override
public void onCommand(String command, Bundle extras, ResultReceiver cb)
throws RemoteException {
Session session = mMediaSession.get();
if (session != null) {
session.postCommand(command, extras, cb);
}
}
@Override
public void onMediaButton(Intent mediaButtonIntent) throws RemoteException {
Session session = mMediaSession.get();
if (session != null) {
session.postMediaButton(mediaButtonIntent);
}
}
@Override
public void onRequestRouteChange(RouteInfo route) throws RemoteException {
Session session = mMediaSession.get();
if (session != null) {
session.postRequestRouteChange(route);
}
}
@Override
public void onRouteConnected(RouteInfo route, RouteOptions options) {
Session session = mMediaSession.get();
if (session != null) {
session.postRouteConnected(route, options);
}
}
@Override
public void onPlay() throws RemoteException {
Session session = mMediaSession.get();
if (session != null) {
TransportPerformer tp = session.getTransportPerformer();
if (tp != null) {
tp.onPlay();
}
}
}
@Override
public void onPause() throws RemoteException {
Session session = mMediaSession.get();
if (session != null) {
TransportPerformer tp = session.getTransportPerformer();
if (tp != null) {
tp.onPause();
}
}
}
@Override
public void onStop() throws RemoteException {
Session session = mMediaSession.get();
if (session != null) {
TransportPerformer tp = session.getTransportPerformer();
if (tp != null) {
tp.onStop();
}
}
}
@Override
public void onNext() throws RemoteException {
Session session = mMediaSession.get();
if (session != null) {
TransportPerformer tp = session.getTransportPerformer();
if (tp != null) {
tp.onNext();
}
}
}
@Override
public void onPrevious() throws RemoteException {
Session session = mMediaSession.get();
if (session != null) {
TransportPerformer tp = session.getTransportPerformer();
if (tp != null) {
tp.onPrevious();
}
}
}
@Override
public void onFastForward() throws RemoteException {
Session session = mMediaSession.get();
if (session != null) {
TransportPerformer tp = session.getTransportPerformer();
if (tp != null) {
tp.onFastForward();
}
}
}
@Override
public void onRewind() throws RemoteException {
Session session = mMediaSession.get();
if (session != null) {
TransportPerformer tp = session.getTransportPerformer();
if (tp != null) {
tp.onRewind();
}
}
}
@Override
public void onSeekTo(long pos) throws RemoteException {
Session session = mMediaSession.get();
if (session != null) {
TransportPerformer tp = session.getTransportPerformer();
if (tp != null) {
tp.onSeekTo(pos);
}
}
}
@Override
public void onRate(Rating rating) throws RemoteException {
Session session = mMediaSession.get();
if (session != null) {
TransportPerformer tp = session.getTransportPerformer();
if (tp != null) {
tp.onRate(rating);
}
}
}
@Override
public void onRouteEvent(RouteEvent event) throws RemoteException {
Session session = mMediaSession.get();
if (session != null) {
RouteInterface.EventListener iface
= session.mInterfaceListeners.get(event.getIface());
Log.d(TAG, "Received route event on iface " + event.getIface() + ". Listener is "
+ iface);
if (iface != null) {
iface.onEvent(event.getEvent(), event.getExtras());
}
}
}
@Override
public void onRouteStateChange(int state) throws RemoteException {
// TODO
}
}
private class MessageHandler extends Handler {
private Session.Callback mCallback;
public MessageHandler(Looper looper, Session.Callback callback) {
super(looper, null, true);
mCallback = callback;
}
@Override
public void handleMessage(Message msg) {
synchronized (mLock) {
if (mCallback == null) {
return;
}
switch (msg.what) {
case MSG_MEDIA_BUTTON:
mCallback.onMediaButton((Intent) msg.obj);
break;
case MSG_COMMAND:
Command cmd = (Command) msg.obj;
mCallback.onCommand(cmd.command, cmd.extras, cmd.stub);
break;
case MSG_ROUTE_CHANGE:
mCallback.onRequestRouteChange((RouteInfo) msg.obj);
break;
case MSG_ROUTE_CONNECTED:
mCallback.onRouteConnected((Route) msg.obj);
break;
}
}
}
public void post(int what, Object obj) {
obtainMessage(what, obj).sendToTarget();
}
}
private static final class Command {
public final String command;
public final Bundle extras;
public final ResultReceiver stub;
public Command(String command, Bundle extras, ResultReceiver stub) {
this.command = command;
this.extras = extras;
this.stub = stub;
}
}
}