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.java369
1 files changed, 369 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..642ac2f
--- /dev/null
+++ b/media/java/android/media/session/MediaController.java
@@ -0,0 +1,369 @@
+/*
+ * 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.media.MediaMetadata;
+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.Log;
+import android.view.KeyEvent;
+
+import java.lang.ref.WeakReference;
+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 = "SessionController";
+
+ private static final int MSG_EVENT = 1;
+ private static final int MESSAGE_PLAYBACK_STATE = 2;
+ private static final int MESSAGE_METADATA = 3;
+ private static final int MSG_ROUTE = 4;
+
+ private final ISessionController mSessionBinder;
+
+ private final CallbackStub mCbStub = new CallbackStub(this);
+ private final ArrayList<MessageHandler> mCallbacks = new ArrayList<MessageHandler>();
+ private final Object mLock = new Object();
+
+ private boolean mCbRegistered = false;
+
+ private TransportController mTransportController;
+
+ private MediaController(ISessionController sessionBinder) {
+ mSessionBinder = sessionBinder;
+ }
+
+ /**
+ * @hide
+ */
+ public static MediaController fromBinder(ISessionController sessionBinder) {
+ MediaController controller = new MediaController(sessionBinder);
+ try {
+ controller.mSessionBinder.registerCallbackListener(controller.mCbStub);
+ if (controller.mSessionBinder.isTransportControlEnabled()) {
+ controller.mTransportController = new TransportController(sessionBinder);
+ }
+ } catch (RemoteException e) {
+ Log.wtf(TAG, "MediaController created with expired token", e);
+ controller = null;
+ }
+ return controller;
+ }
+
+ /**
+ * Get a new MediaController for a MediaSessionToken. If successful the
+ * controller returned will be connected to the session that generated the
+ * token.
+ *
+ * @param token The session token to use
+ * @return A controller for the session or null
+ */
+ public static MediaController fromToken(MediaSessionToken token) {
+ return fromBinder(token.getBinder());
+ }
+
+ /**
+ * Get a TransportController if the session supports it. If it is not
+ * supported null will be returned.
+ *
+ * @return A TransportController or null
+ */
+ public TransportController getTransportController() {
+ return mTransportController;
+ }
+
+ /**
+ * 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's thread.
+ *
+ * @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);
+ }
+ }
+
+ /**
+ * 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
+ * @param cb The callback to receive the result on
+ */
+ public void sendCommand(String command, Bundle params, ResultReceiver cb) {
+ if (TextUtils.isEmpty(command)) {
+ throw new IllegalArgumentException("command cannot be null or empty");
+ }
+ try {
+ mSessionBinder.sendCommand(command, params, cb);
+ } catch (RemoteException e) {
+ Log.d(TAG, "Dead object in sendCommand.", e);
+ }
+ }
+
+ /**
+ * Request that the route picker be shown for this session. This should
+ * generally be called in response to a user action.
+ *
+ * @hide
+ */
+ public void showRoutePicker() {
+ try {
+ mSessionBinder.showRoutePicker();
+ } catch (RemoteException e) {
+ Log.d(TAG, "Dead object in showRoutePicker", e);
+ }
+ }
+
+ /*
+ * @hide
+ */
+ ISessionController 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 (getHandlerForCallbackLocked(cb) != null) {
+ Log.w(TAG, "Callback is already added, ignoring");
+ return;
+ }
+ MessageHandler holder = new MessageHandler(handler.getLooper(), cb);
+ mCallbacks.add(holder);
+
+ if (!mCbRegistered) {
+ try {
+ mSessionBinder.registerCallbackListener(mCbStub);
+ mCbRegistered = true;
+ } catch (RemoteException e) {
+ Log.d(TAG, "Dead object in registerCallback", e);
+ }
+ }
+ }
+
+ 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 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 void postEvent(String event, Bundle extras) {
+ synchronized (mLock) {
+ for (int i = mCallbacks.size() - 1; i >= 0; i--) {
+ mCallbacks.get(i).post(MSG_EVENT, event, extras);
+ }
+ }
+ }
+
+ private void postRouteChanged(RouteInfo route) {
+ synchronized (mLock) {
+ for (int i = mCallbacks.size() - 1; i >= 0; i--) {
+ mCallbacks.get(i).post(MSG_ROUTE, route, null);
+ }
+ }
+ }
+
+ /**
+ * Callback for receiving updates on from the session. A Callback can be
+ * registered using {@link #addCallback}
+ */
+ public static abstract class Callback {
+ /**
+ * Override to handle custom events sent by the session owner without a
+ * specified interface. Controllers should only handle these for
+ * sessions they own.
+ *
+ * @param event
+ */
+ public void onEvent(String event, Bundle extras) {
+ }
+
+ /**
+ * Override to handle route changes for this session.
+ *
+ * @param route The new route
+ * @hide
+ */
+ public void onRouteChanged(RouteInfo route) {
+ }
+ }
+
+ private final static class CallbackStub extends ISessionControllerCallback.Stub {
+ private final WeakReference<MediaController> mController;
+
+ public CallbackStub(MediaController controller) {
+ mController = new WeakReference<MediaController>(controller);
+ }
+
+ @Override
+ public void onEvent(String event, Bundle extras) {
+ MediaController controller = mController.get();
+ if (controller != null) {
+ controller.postEvent(event, extras);
+ }
+ }
+
+ @Override
+ public void onRouteChanged(RouteInfo route) {
+ MediaController controller = mController.get();
+ if (controller != null) {
+ controller.postRouteChanged(route);
+ }
+ }
+
+ @Override
+ public void onPlaybackStateChanged(PlaybackState state) {
+ MediaController controller = mController.get();
+ if (controller != null) {
+ TransportController tc = controller.getTransportController();
+ if (tc != null) {
+ tc.postPlaybackStateChanged(state);
+ }
+ }
+ }
+
+ @Override
+ public void onMetadataChanged(MediaMetadata metadata) {
+ MediaController controller = mController.get();
+ if (controller != null) {
+ TransportController tc = controller.getTransportController();
+ if (tc != null) {
+ tc.postMetadataChanged(metadata);
+ }
+ }
+ }
+
+ }
+
+ private final static class MessageHandler extends Handler {
+ private final MediaController.Callback mCallback;
+
+ public MessageHandler(Looper looper, MediaController.Callback cb) {
+ super(looper, null, true);
+ mCallback = cb;
+ }
+
+ @Override
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+ case MSG_EVENT:
+ mCallback.onEvent((String) msg.obj, msg.getData());
+ break;
+ case MSG_ROUTE:
+ mCallback.onRouteChanged((RouteInfo) msg.obj);
+ }
+ }
+
+ public void post(int what, Object obj, Bundle data) {
+ obtainMessage(what, obj).sendToTarget();
+ }
+ }
+
+}