blob: 09de859d66b1323b30c74f7f12a8f296e25e87f3 [file] [log] [blame]
/*
* 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.session.IMediaController;
import android.media.session.IMediaControllerCallback;
import android.media.MediaMetadataRetriever;
import android.media.RemoteControlClient;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.os.RemoteException;
import android.text.TextUtils;
import android.util.Log;
import android.view.KeyEvent;
import java.util.ArrayList;
/**
* Allows an app to interact with an ongoing media session. Media buttons and
* other commands can be sent to the session. A callback may be registered to
* receive updates from the session, such as metadata and play state changes.
* <p>
* A MediaController can be created through {@link MediaSessionManager} if you
* hold the "android.permission.MEDIA_CONTENT_CONTROL" permission or directly if
* you have a {@link MediaSessionToken} from the session owner.
* <p>
* MediaController objects are thread-safe.
*/
public final class MediaController {
private static final String TAG = "MediaController";
private static final int MESSAGE_EVENT = 1;
private static final int MESSAGE_PLAYBACK_STATE = 2;
private static final int MESSAGE_METADATA = 3;
private static final int MESSAGE_ROUTE = 4;
private static final String KEY_EVENT = "event";
private static final String KEY_EXTRAS = "extras";
private final IMediaController mSessionBinder;
private final CallbackStub mCbStub = new CallbackStub();
private final ArrayList<Callback> mCbs = new ArrayList<Callback>();
private final Object mLock = new Object();
private boolean mCbRegistered = false;
/**
* If you have a {@link MediaSessionToken} from the owner of the session a
* controller can be created directly. It is up to the session creator to
* handle token distribution if desired.
*
* @see MediaSession#getSessionToken()
* @param token A token from the creator of the session
*/
public MediaController(MediaSessionToken token) {
mSessionBinder = token.getBinder();
}
/**
* @hide
*/
public MediaController(IMediaController sessionBinder) {
mSessionBinder = sessionBinder;
}
/**
* Sends a generic command to the session. It is up to the session creator
* to decide what commands and parameters they will support. As such,
* commands should only be sent to sessions that the controller owns.
*
* @param command The command to send
* @param params Any parameters to include with the command
*/
public void sendCommand(String command, Bundle params) {
if (TextUtils.isEmpty(command)) {
throw new IllegalArgumentException("command cannot be null or empty");
}
try {
mSessionBinder.sendCommand(command, params);
} catch (RemoteException e) {
Log.d(TAG, "Dead object in sendCommand.", e);
}
}
/**
* Send the specified media button to the session. Only media keys can be
* sent using this method.
*
* @param keycode The media button keycode, such as
* {@link KeyEvent#KEYCODE_MEDIA_PLAY}.
*/
public void sendMediaButton(int keycode) {
if (!KeyEvent.isMediaKey(keycode)) {
throw new IllegalArgumentException("May only send media buttons through "
+ "sendMediaButton");
}
// TODO do something better than key down/up events
KeyEvent event = new KeyEvent(KeyEvent.ACTION_UP, keycode);
try {
mSessionBinder.sendMediaButton(event);
} catch (RemoteException e) {
Log.d(TAG, "Dead object in sendMediaButton", e);
}
}
/**
* Adds a callback to receive updates from the Session. Updates will be
* posted on the caller's thread.
*
* @param cb The callback object, must not be null
*/
public void addCallback(Callback cb) {
addCallback(cb, null);
}
/**
* Adds a callback to receive updates from the session. Updates will be
* posted on the specified handler.
*
* @param cb Cannot be null.
* @param handler The handler to post updates on, if null the callers thread
* will be used
*/
public void addCallback(Callback cb, Handler handler) {
if (handler == null) {
handler = new Handler();
}
synchronized (mLock) {
addCallbackLocked(cb, handler);
}
}
/**
* Stop receiving updates on the specified callback. If an update has
* already been posted you may still receive it after calling this method.
*
* @param cb The callback to remove
*/
public void removeCallback(Callback cb) {
synchronized (mLock) {
removeCallbackLocked(cb);
}
}
/*
* @hide
*/
IMediaController getSessionBinder() {
return mSessionBinder;
}
private void addCallbackLocked(Callback cb, Handler handler) {
if (cb == null) {
throw new IllegalArgumentException("Callback cannot be null");
}
if (handler == null) {
throw new IllegalArgumentException("Handler cannot be null");
}
if (mCbs.contains(cb)) {
Log.w(TAG, "Callback is already added, ignoring");
return;
}
cb.setHandler(handler);
mCbs.add(cb);
// Only register one cb binder, track callbacks internally and notify
if (!mCbRegistered) {
try {
mSessionBinder.registerCallbackListener(mCbStub);
mCbRegistered = true;
} catch (RemoteException e) {
Log.d(TAG, "Dead object in registerCallback", e);
}
}
}
private void removeCallbackLocked(Callback cb) {
if (cb == null) {
throw new IllegalArgumentException("Callback cannot be null");
}
mCbs.remove(cb);
if (mCbs.size() == 0 && mCbRegistered) {
try {
mSessionBinder.unregisterCallbackListener(mCbStub);
} catch (RemoteException e) {
Log.d(TAG, "Dead object in unregisterCallback", e);
}
mCbRegistered = false;
}
}
private void pushOnEventLocked(String event, Bundle extras) {
for (int i = mCbs.size() - 1; i >= 0; i--) {
mCbs.get(i).postEvent(event, extras);
}
}
private void pushOnMetadataUpdateLocked(Bundle metadata) {
for (int i = mCbs.size() - 1; i >= 0; i--) {
mCbs.get(i).postMetadataUpdate(metadata);
}
}
private void pushOnPlaybackUpdateLocked(int newState) {
for (int i = mCbs.size() - 1; i >= 0; i--) {
mCbs.get(i).postPlaybackStateChange(newState);
}
}
private void pushOnRouteChangedLocked(Bundle routeDescriptor) {
for (int i = mCbs.size() - 1; i >= 0; i--) {
mCbs.get(i).postRouteChanged(routeDescriptor);
}
}
/**
* MediaSession callbacks will be posted on the thread that created the
* Callback object.
*/
public static abstract class Callback {
private Handler mHandler;
/**
* Override to handle custom events sent by the session owner.
* Controllers should only handle these for sessions they own.
*
* @param event
*/
public void onEvent(String event, Bundle extras) {
}
/**
* Override to handle updates to the playback state. Valid values are in
* {@link RemoteControlClient}. TODO put playstate values somewhere more
* generic.
*
* @param state
*/
public void onPlaybackStateChange(int state) {
}
/**
* Override to handle metadata changes for this session's media. The
* default supported fields are those in {@link MediaMetadataRetriever}.
*
* @param metadata
*/
public void onMetadataUpdate(Bundle metadata) {
}
/**
* Override to handle route changes for this session.
*
* @param route
*/
public void onRouteChanged(Bundle route) {
}
private void setHandler(Handler handler) {
mHandler = new MessageHandler(handler.getLooper(), this);
}
private void postEvent(String event, Bundle extras) {
Bundle eventBundle = new Bundle();
eventBundle.putString(KEY_EVENT, event);
eventBundle.putBundle(KEY_EXTRAS, extras);
Message msg = mHandler.obtainMessage(MESSAGE_EVENT, eventBundle);
mHandler.sendMessage(msg);
}
private void postPlaybackStateChange(final int state) {
Message msg = mHandler.obtainMessage(MESSAGE_PLAYBACK_STATE, state, 0);
mHandler.sendMessage(msg);
}
private void postMetadataUpdate(final Bundle metadata) {
Message msg = mHandler.obtainMessage(MESSAGE_METADATA, metadata);
mHandler.sendMessage(msg);
}
private void postRouteChanged(final Bundle descriptor) {
Message msg = mHandler.obtainMessage(MESSAGE_ROUTE, descriptor);
mHandler.sendMessage(msg);
}
}
private final class CallbackStub extends IMediaControllerCallback.Stub {
@Override
public void onEvent(String event, Bundle extras) throws RemoteException {
synchronized (mLock) {
pushOnEventLocked(event, extras);
}
}
@Override
public void onMetadataUpdate(Bundle metadata) throws RemoteException {
synchronized (mLock) {
pushOnMetadataUpdateLocked(metadata);
}
}
@Override
public void onPlaybackUpdate(final int newState) throws RemoteException {
synchronized (mLock) {
pushOnPlaybackUpdateLocked(newState);
}
}
@Override
public void onRouteChanged(Bundle mediaRouteDescriptor) throws RemoteException {
synchronized (mLock) {
pushOnRouteChangedLocked(mediaRouteDescriptor);
}
}
}
private final static class MessageHandler extends Handler {
private final MediaController.Callback mCb;
public MessageHandler(Looper looper, MediaController.Callback cb) {
super(looper);
mCb = cb;
}
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case MESSAGE_EVENT:
Bundle eventBundle = (Bundle) msg.obj;
String event = eventBundle.getString(KEY_EVENT);
Bundle extras = eventBundle.getBundle(KEY_EXTRAS);
mCb.onEvent(event, extras);
break;
case MESSAGE_PLAYBACK_STATE:
mCb.onPlaybackStateChange(msg.arg1);
break;
case MESSAGE_METADATA:
mCb.onMetadataUpdate((Bundle) msg.obj);
break;
case MESSAGE_ROUTE:
mCb.onRouteChanged((Bundle) msg.obj);
}
}
}
}