/* * 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.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. *

* 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. *

* MediaController objects are thread-safe. */ public final class MediaController { private static final String TAG = "MediaController"; 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 IMediaController mSessionBinder; private final CallbackStub mCbStub = new CallbackStub(this); private final ArrayList mCallbacks = new ArrayList(); private final Object mLock = new Object(); private boolean mCbRegistered = false; private TransportController mTransportController; private MediaController(IMediaController sessionBinder) { mSessionBinder = sessionBinder; } /** * @hide */ public static MediaController fromBinder(IMediaController 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); } } /* * @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 (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(Bundle routeDescriptor) { synchronized (mLock) { for (int i = mCallbacks.size() - 1; i >= 0; i--) { mCallbacks.get(i).post(MSG_ROUTE, null, routeDescriptor); } } } /** * 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 */ public void onRouteChanged(Bundle route) { } } private final static class CallbackStub extends IMediaControllerCallback.Stub { private final WeakReference mController; public CallbackStub(MediaController controller) { mController = new WeakReference(controller); } @Override public void onEvent(String event, Bundle extras) { MediaController controller = mController.get(); if (controller != null) { controller.postEvent(event, extras); } } @Override public void onRouteChanged(Bundle mediaRouteDescriptor) { MediaController controller = mController.get(); if (controller != null) { controller.postRouteChanged(mediaRouteDescriptor); } } @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(msg.getData()); } } public void post(int what, Object obj, Bundle data) { obtainMessage(what, obj).sendToTarget(); } } }