summaryrefslogtreecommitdiffstats
path: root/media/java/android/media/session/MediaController.java
diff options
context:
space:
mode:
Diffstat (limited to 'media/java/android/media/session/MediaController.java')
-rw-r--r--media/java/android/media/session/MediaController.java367
1 files changed, 367 insertions, 0 deletions
diff --git a/media/java/android/media/session/MediaController.java b/media/java/android/media/session/MediaController.java
new file mode 100644
index 0000000..09de859
--- /dev/null
+++ b/media/java/android/media/session/MediaController.java
@@ -0,0 +1,367 @@
+/*
+ * 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);
+ }
+ }
+ }
+
+}