diff options
Diffstat (limited to 'media/java/android/media/session/MediaController.java')
-rw-r--r-- | media/java/android/media/session/MediaController.java | 367 |
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); + } + } + } + +} |