summaryrefslogtreecommitdiffstats
path: root/media
diff options
context:
space:
mode:
authorRoboErik <epastern@google.com>2014-02-13 14:19:04 -0800
committerRoboErik <epastern@google.com>2014-02-19 13:41:37 -0800
commit01fe661ae5da3739215d93922412df4b24c859a2 (patch)
treefbc2bb43edec44c553256de377312741f87f434f /media
parentd63b4314b85e982a1d70d4064af59851f476dd36 (diff)
downloadframeworks_base-01fe661ae5da3739215d93922412df4b24c859a2.zip
frameworks_base-01fe661ae5da3739215d93922412df4b24c859a2.tar.gz
frameworks_base-01fe661ae5da3739215d93922412df4b24c859a2.tar.bz2
Initial round of MediaSession APIs
This is far from complete but puts the basic components in place for an app to interact with media sessions. Change-Id: Icfe313f90ad76ae56badbe42b0e43fc5f68db36f
Diffstat (limited to 'media')
-rw-r--r--media/java/android/media/IMediaController.aidl34
-rw-r--r--media/java/android/media/IMediaControllerCallback.aidl28
-rw-r--r--media/java/android/media/IMediaSession.aidl33
-rw-r--r--media/java/android/media/IMediaSessionCallback.aidl29
-rw-r--r--media/java/android/media/IMediaSessionManager.aidl28
-rw-r--r--media/java/android/media/MediaController.java363
-rw-r--r--media/java/android/media/MediaFocusControl.java46
-rw-r--r--media/java/android/media/MediaSession.java302
-rw-r--r--media/java/android/media/MediaSessionManager.java89
-rw-r--r--media/java/android/media/MediaSessionToken.aidl18
-rw-r--r--media/java/android/media/MediaSessionToken.java65
-rw-r--r--media/java/android/media/RemoteController.java2
12 files changed, 1002 insertions, 35 deletions
diff --git a/media/java/android/media/IMediaController.aidl b/media/java/android/media/IMediaController.aidl
new file mode 100644
index 0000000..fc3525a
--- /dev/null
+++ b/media/java/android/media/IMediaController.aidl
@@ -0,0 +1,34 @@
+/* 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;
+
+import android.content.Intent;
+import android.media.IMediaControllerCallback;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.view.KeyEvent;
+
+/**
+ * Interface to a MediaSession in the system.
+ * @hide
+ */
+interface IMediaController {
+ void sendCommand(String command, in Bundle extras);
+ void sendMediaButton(in KeyEvent mediaButton);
+ void registerCallbackListener(in IMediaControllerCallback cb);
+ void unregisterCallbackListener(in IMediaControllerCallback cb);
+ int getPlaybackState();
+} \ No newline at end of file
diff --git a/media/java/android/media/IMediaControllerCallback.aidl b/media/java/android/media/IMediaControllerCallback.aidl
new file mode 100644
index 0000000..b54d0cf
--- /dev/null
+++ b/media/java/android/media/IMediaControllerCallback.aidl
@@ -0,0 +1,28 @@
+/* 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;
+
+import android.os.Bundle;
+
+/**
+ * @hide
+ */
+oneway interface IMediaControllerCallback {
+ void onEvent(String event, in Bundle extras);
+ void onMetadataUpdate(in Bundle metadata);
+ void onPlaybackUpdate(int newState);
+ void onRouteChanged(in Bundle route);
+} \ No newline at end of file
diff --git a/media/java/android/media/IMediaSession.aidl b/media/java/android/media/IMediaSession.aidl
new file mode 100644
index 0000000..ed71d78
--- /dev/null
+++ b/media/java/android/media/IMediaSession.aidl
@@ -0,0 +1,33 @@
+/* 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;
+
+import android.media.IMediaController;
+import android.os.Bundle;
+
+/**
+ * Interface to a MediaSession in the system.
+ * @hide
+ */
+interface IMediaSession {
+ void sendEvent(in Bundle data);
+ IMediaController getMediaSessionToken();
+ void setPlaybackState(int state);
+ void setMetadata(in Bundle metadata);
+ void setRouteState(in Bundle routeState);
+ void setRoute(in Bundle mediaRouteDescriptor);
+ void destroy();
+} \ No newline at end of file
diff --git a/media/java/android/media/IMediaSessionCallback.aidl b/media/java/android/media/IMediaSessionCallback.aidl
new file mode 100644
index 0000000..3aaf925
--- /dev/null
+++ b/media/java/android/media/IMediaSessionCallback.aidl
@@ -0,0 +1,29 @@
+/* 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;
+
+import android.content.Intent;
+import android.os.Bundle;
+import android.os.IBinder;
+
+/**
+ * @hide
+ */
+oneway interface IMediaSessionCallback {
+ void onCommand(String command, in Bundle extras);
+ void onMediaButton(in Intent mediaRequestIntent);
+ void onRequestRouteChange(in Bundle route);
+} \ No newline at end of file
diff --git a/media/java/android/media/IMediaSessionManager.aidl b/media/java/android/media/IMediaSessionManager.aidl
new file mode 100644
index 0000000..8bc0c3b
--- /dev/null
+++ b/media/java/android/media/IMediaSessionManager.aidl
@@ -0,0 +1,28 @@
+/* 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;
+
+import android.media.IMediaSession;
+import android.media.IMediaSessionCallback;
+import android.os.Bundle;
+
+/**
+ * Interface to the MediaSessionManagerService
+ * @hide
+ */
+interface IMediaSessionManager {
+ IMediaSession createSession(String packageName, in IMediaSessionCallback cb, String tag);
+} \ No newline at end of file
diff --git a/media/java/android/media/MediaController.java b/media/java/android/media/MediaController.java
new file mode 100644
index 0000000..cedb0c3
--- /dev/null
+++ b/media/java/android/media/MediaController.java
@@ -0,0 +1,363 @@
+/*
+ * 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;
+
+import android.content.Intent;
+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_BUTTON_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);
+ }
+ }
+ }
+
+}
diff --git a/media/java/android/media/MediaFocusControl.java b/media/java/android/media/MediaFocusControl.java
index 25ab99d..b155cda 100644
--- a/media/java/android/media/MediaFocusControl.java
+++ b/media/java/android/media/MediaFocusControl.java
@@ -262,7 +262,7 @@ public class MediaFocusControl implements OnFinished {
final Iterator<DisplayInfoForServer> displayIterator = mRcDisplays.iterator();
while (displayIterator.hasNext()) {
final DisplayInfoForServer di =
- (DisplayInfoForServer) displayIterator.next();
+ displayIterator.next();
if (di.mClientNotifListComp != null) {
boolean wasEnabled = di.mEnabled;
di.mEnabled = isComponentInStringArray(di.mClientNotifListComp,
@@ -538,7 +538,7 @@ public class MediaFocusControl implements OnFinished {
// evaluated it, traversal order doesn't matter here)
Iterator<FocusRequester> stackIterator = mFocusStack.iterator();
while(stackIterator.hasNext()) {
- FocusRequester fr = (FocusRequester)stackIterator.next();
+ FocusRequester fr = stackIterator.next();
if(fr.hasSameClient(clientToRemove)) {
Log.i(TAG, "AudioFocus removeFocusStackEntry(): removing entry for "
+ clientToRemove);
@@ -562,7 +562,7 @@ public class MediaFocusControl implements OnFinished {
// evaluated it, traversal order doesn't matter here)
Iterator<FocusRequester> stackIterator = mFocusStack.iterator();
while(stackIterator.hasNext()) {
- FocusRequester fr = (FocusRequester)stackIterator.next();
+ FocusRequester fr = stackIterator.next();
if(fr.hasSameBinder(cb)) {
Log.i(TAG, "AudioFocus removeFocusStackEntry(): removing entry for " + cb);
stackIterator.remove();
@@ -930,33 +930,11 @@ public class MediaFocusControl implements OnFinished {
}
}
- protected static boolean isMediaKeyCode(int keyCode) {
- switch (keyCode) {
- case KeyEvent.KEYCODE_MUTE:
- case KeyEvent.KEYCODE_HEADSETHOOK:
- case KeyEvent.KEYCODE_MEDIA_PLAY:
- case KeyEvent.KEYCODE_MEDIA_PAUSE:
- case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE:
- case KeyEvent.KEYCODE_MEDIA_STOP:
- case KeyEvent.KEYCODE_MEDIA_NEXT:
- case KeyEvent.KEYCODE_MEDIA_PREVIOUS:
- case KeyEvent.KEYCODE_MEDIA_REWIND:
- case KeyEvent.KEYCODE_MEDIA_RECORD:
- case KeyEvent.KEYCODE_MEDIA_FAST_FORWARD:
- case KeyEvent.KEYCODE_MEDIA_CLOSE:
- case KeyEvent.KEYCODE_MEDIA_EJECT:
- case KeyEvent.KEYCODE_MEDIA_AUDIO_TRACK:
- return true;
- default:
- return false;
- }
- }
-
private static boolean isValidMediaKeyEvent(KeyEvent keyEvent) {
if (keyEvent == null) {
return false;
}
- return MediaFocusControl.isMediaKeyCode(keyEvent.getKeyCode());
+ return KeyEvent.isMediaKey(keyEvent.getKeyCode());
}
/**
@@ -1383,7 +1361,7 @@ public class MediaFocusControl implements OnFinished {
synchronized(mRCStack) {
final Iterator<DisplayInfoForServer> displayIterator = mRcDisplays.iterator();
while (displayIterator.hasNext()) {
- final DisplayInfoForServer di = (DisplayInfoForServer) displayIterator.next();
+ final DisplayInfoForServer di = displayIterator.next();
pw.println(" IRCD: " + di.mRcDisplay +
" -- w:" + di.mArtworkExpectedWidth +
" -- h:" + di.mArtworkExpectedHeight +
@@ -1410,7 +1388,7 @@ public class MediaFocusControl implements OnFinished {
// (using an iterator on the stack so we can safely remove an entry after having
// evaluated it, traversal order doesn't matter here)
while(stackIterator.hasNext()) {
- RemoteControlStackEntry rcse = (RemoteControlStackEntry)stackIterator.next();
+ RemoteControlStackEntry rcse = stackIterator.next();
if (removeAll && packageName.equals(rcse.mMediaIntent.getCreatorPackage())) {
// a stack entry is from the package being removed, remove it from the stack
stackIterator.remove();
@@ -2075,7 +2053,7 @@ public class MediaFocusControl implements OnFinished {
// remove the display from the list
final Iterator<DisplayInfoForServer> displayIterator = mRcDisplays.iterator();
while (displayIterator.hasNext()) {
- final DisplayInfoForServer di = (DisplayInfoForServer) displayIterator.next();
+ final DisplayInfoForServer di = displayIterator.next();
if (di.mRcDisplay == mRcDisplay) {
if (DEBUG_RC) Log.w(TAG, " RCD removed from list");
displayIterator.remove();
@@ -2099,7 +2077,7 @@ public class MediaFocusControl implements OnFinished {
private void plugRemoteControlDisplaysIntoClient_syncRcStack(IRemoteControlClient rcc) {
final Iterator<DisplayInfoForServer> displayIterator = mRcDisplays.iterator();
while (displayIterator.hasNext()) {
- final DisplayInfoForServer di = (DisplayInfoForServer) displayIterator.next();
+ final DisplayInfoForServer di = displayIterator.next();
try {
rcc.plugRemoteControlDisplay(di.mRcDisplay, di.mArtworkExpectedWidth,
di.mArtworkExpectedHeight);
@@ -2137,7 +2115,7 @@ public class MediaFocusControl implements OnFinished {
private boolean rcDisplayIsPluggedIn_syncRcStack(IRemoteControlDisplay rcd) {
final Iterator<DisplayInfoForServer> displayIterator = mRcDisplays.iterator();
while (displayIterator.hasNext()) {
- final DisplayInfoForServer di = (DisplayInfoForServer) displayIterator.next();
+ final DisplayInfoForServer di = displayIterator.next();
if (di.mRcDisplay.asBinder().equals(rcd.asBinder())) {
return true;
}
@@ -2216,7 +2194,7 @@ public class MediaFocusControl implements OnFinished {
boolean displayWasPluggedIn = false;
final Iterator<DisplayInfoForServer> displayIterator = mRcDisplays.iterator();
while (displayIterator.hasNext() && !displayWasPluggedIn) {
- final DisplayInfoForServer di = (DisplayInfoForServer) displayIterator.next();
+ final DisplayInfoForServer di = displayIterator.next();
if (di.mRcDisplay.asBinder().equals(rcd.asBinder())) {
displayWasPluggedIn = true;
di.release();
@@ -2258,7 +2236,7 @@ public class MediaFocusControl implements OnFinished {
final Iterator<DisplayInfoForServer> displayIterator = mRcDisplays.iterator();
boolean artworkSizeUpdate = false;
while (displayIterator.hasNext() && !artworkSizeUpdate) {
- final DisplayInfoForServer di = (DisplayInfoForServer) displayIterator.next();
+ final DisplayInfoForServer di = displayIterator.next();
if (di.mRcDisplay.asBinder().equals(rcd.asBinder())) {
if ((di.mArtworkExpectedWidth != w) || (di.mArtworkExpectedHeight != h)) {
di.mArtworkExpectedWidth = w;
@@ -2305,7 +2283,7 @@ public class MediaFocusControl implements OnFinished {
// (display stack traversal order doesn't matter).
final Iterator<DisplayInfoForServer> displayIterator = mRcDisplays.iterator();
while (displayIterator.hasNext()) {
- final DisplayInfoForServer di = (DisplayInfoForServer) displayIterator.next();
+ final DisplayInfoForServer di = displayIterator.next();
if (di.mRcDisplay.asBinder().equals(rcd.asBinder())) {
di.mWantsPositionSync = wantsSync;
rcdRegistered = true;
diff --git a/media/java/android/media/MediaSession.java b/media/java/android/media/MediaSession.java
new file mode 100644
index 0000000..038a9cf
--- /dev/null
+++ b/media/java/android/media/MediaSession.java
@@ -0,0 +1,302 @@
+/*
+ * 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;
+
+import android.content.Intent;
+import android.media.IMediaSession;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.os.RemoteException;
+import android.util.Log;
+
+import java.util.ArrayList;
+
+/**
+ * Allows interaction with media controllers, media routes, volume keys, media
+ * buttons, and transport controls.
+ * <p>
+ * A MediaSession should be created when an app wants to publish media playback
+ * information or negotiate with a media route. In general an app only needs one
+ * session for all playback, though multiple sessions can be created for sending
+ * media to multiple routes or to provide finer grain controls of media.
+ * <p>
+ * A MediaSession is created by calling
+ * {@link MediaSessionManager#createSession(String)}. Once a session is created
+ * apps that have the MEDIA_CONTENT_CONTROL permission can interact with the
+ * session through {@link MediaSessionManager#listActiveSessions()}. The owner
+ * of the session may also use {@link #getSessionToken()} to allow apps without
+ * this permission to create a {@link MediaController} to interact with this
+ * session.
+ * <p>
+ * To receive commands, media keys, and other events a Callback must be set with
+ * {@link #addCallback(Callback)}.
+ * <p>
+ * When an app is finished performing playback it must call {@link #release()}
+ * to clean up the session and notify any controllers.
+ * <p>
+ * MediaSession objects are thread safe
+ */
+public final class MediaSession {
+ private static final String TAG = "MediaSession";
+
+ private static final int MESSAGE_MEDIA_BUTTON = 1;
+ private static final int MESSAGE_COMMAND = 2;
+ private static final int MESSAGE_ROUTE_CHANGE = 3;
+
+ private static final String KEY_COMMAND = "command";
+ private static final String KEY_EXTRAS = "extras";
+
+ private final Object mLock = new Object();
+
+ private final MediaSessionToken mSessionToken;
+ private final IMediaSession mBinder;
+ private final CallbackStub mCbStub;
+
+ private final ArrayList<Callback> mCallbacks = new ArrayList<Callback>();
+
+ /**
+ * @hide
+ */
+ public MediaSession(IMediaSession binder, CallbackStub cbStub) {
+ mBinder = binder;
+ mCbStub = cbStub;
+ IMediaController controllerBinder = null;
+ try {
+ controllerBinder = mBinder.getMediaSessionToken();
+ } catch (RemoteException e) {
+ throw new RuntimeException("Dead object in MediaSessionController constructor: ", e);
+ }
+ mSessionToken = new MediaSessionToken(controllerBinder);
+ }
+
+ /**
+ * Set the callback to receive updates on.
+ *
+ * @param callback The callback object
+ */
+ public void addCallback(Callback callback) {
+ addCallback(callback, null);
+ }
+
+ public void addCallback(Callback callback, Handler handler) {
+ if (callback == null) {
+ throw new IllegalArgumentException("Callback cannot be null");
+ }
+ synchronized (mLock) {
+ if (mCallbacks.contains(callback)) {
+ Log.w(TAG, "Callback is already added, ignoring");
+ }
+ if (handler == null) {
+ handler = new Handler();
+ }
+ MessageHandler msgHandler = new MessageHandler(handler.getLooper(), callback);
+ callback.setHandler(msgHandler);
+ mCallbacks.add(callback);
+ }
+ }
+
+ public void removeCallback(Callback callback) {
+ mCallbacks.remove(callback);
+ }
+
+ /**
+ * Publish the current playback state to the system and any controllers.
+ * Valid values are defined in {@link RemoteControlClient}. TODO move play
+ * states somewhere else.
+ *
+ * @param state
+ */
+ public void setPlaybackState(int state) {
+ try {
+ mBinder.setPlaybackState(state);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Dead object in setPlaybackState: ", e);
+ }
+ }
+
+ /**
+ * This must be called when an app has finished performing playback. If
+ * playback is expected to start again shortly the session can be left open,
+ * but it must be released if your activity or service is being destroyed.
+ */
+ public void release() {
+ try {
+ mBinder.destroy();
+ } catch (RemoteException e) {
+ Log.e(TAG, "Dead object in onDestroy: ", e);
+ }
+ }
+
+ /**
+ * Retrieve a token object that can be used by apps to create a
+ * {@link MediaController} for interacting with this session. The owner of
+ * the session is responsible for deciding how to distribute these tokens.
+ *
+ * @return A token that can be used to create a MediaController for this
+ * session
+ */
+ public MediaSessionToken getSessionToken() {
+ return mSessionToken;
+ }
+
+ private void postCommand(String command, Bundle extras) {
+ Bundle commandBundle = new Bundle();
+ commandBundle.putString(KEY_COMMAND, command);
+ commandBundle.putBundle(KEY_EXTRAS, extras);
+ synchronized (mLock) {
+ for (int i = mCallbacks.size() - 1; i >= 0; i--) {
+ Callback cb = mCallbacks.get(i);
+ Message msg = cb.mHandler.obtainMessage(MESSAGE_COMMAND, commandBundle);
+ cb.mHandler.sendMessage(msg);
+ }
+ }
+ }
+
+ private void postMediaButton(Intent mediaButtonIntent) {
+ synchronized (mLock) {
+ for (int i = mCallbacks.size() - 1; i >= 0; i--) {
+ Callback cb = mCallbacks.get(i);
+ Message msg = cb.mHandler.obtainMessage(MESSAGE_MEDIA_BUTTON, mediaButtonIntent);
+ cb.mHandler.sendMessage(msg);
+ }
+ }
+ }
+
+ private void postRequestRouteChange(Bundle mediaRouteDescriptor) {
+ synchronized (mLock) {
+ for (int i = mCallbacks.size() - 1; i >= 0; i--) {
+ Callback cb = mCallbacks.get(i);
+ Message msg = cb.mHandler.obtainMessage(MESSAGE_ROUTE_CHANGE, mediaRouteDescriptor);
+ cb.mHandler.sendMessage(msg);
+ }
+ }
+ }
+
+ /**
+ * Receives commands or updates from controllers and routes. An app can
+ * specify what commands and buttons it supports by setting them on the
+ * MediaSession (TODO).
+ */
+ public abstract static class Callback {
+ private MessageHandler mHandler;
+
+ public Callback() {
+ }
+
+ /**
+ * Called when a media button is pressed and this session has the
+ * highest priority or a controller sends a media button event to the
+ * session. TODO determine if using Intents identical to the ones
+ * RemoteControlClient receives is useful
+ * <p>
+ * The intent will be of type {@link Intent#ACTION_MEDIA_BUTTON} with a
+ * KeyEvent in {@link Intent#EXTRA_KEY_EVENT}
+ *
+ * @param mediaButtonIntent an intent containing the KeyEvent as an
+ * extra
+ */
+ public void onMediaButton(Intent mediaButtonIntent) {
+ }
+
+ /**
+ * Called when a controller has sent a custom command to this session.
+ * The owner of the session may handle custom commands but is not
+ * required to.
+ *
+ * @param command
+ * @param extras optional
+ */
+ public void onCommand(String command, Bundle extras) {
+ }
+
+ /**
+ * Called when the user has selected a different route to connect to.
+ * The app is responsible for connecting to the new route and migrating
+ * ongoing playback if necessary.
+ *
+ * @param descriptor
+ */
+ public void onRequestRouteChange(Bundle descriptor) {
+ }
+
+ private void setHandler(MessageHandler handler) {
+ mHandler = handler;
+ }
+ }
+
+ /**
+ * @hide
+ */
+ public static class CallbackStub extends IMediaSessionCallback.Stub {
+ private MediaSession mMediaSession;
+
+ public void setMediaSession(MediaSession session) {
+ mMediaSession = session;
+ }
+
+ @Override
+ public void onCommand(String command, Bundle extras) throws RemoteException {
+ mMediaSession.postCommand(command, extras);
+ }
+
+ @Override
+ public void onMediaButton(Intent mediaButtonIntent) throws RemoteException {
+ mMediaSession.postMediaButton(mediaButtonIntent);
+ }
+
+ @Override
+ public void onRequestRouteChange(Bundle mediaRouteDescriptor) throws RemoteException {
+ mMediaSession.postRequestRouteChange(mediaRouteDescriptor);
+ }
+
+ }
+
+ private class MessageHandler extends Handler {
+ private MediaSession.Callback mCallback;
+
+ public MessageHandler(Looper looper, MediaSession.Callback callback) {
+ super(looper);
+ mCallback = callback;
+ }
+
+ @Override
+ public void handleMessage(Message msg) {
+ synchronized (mLock) {
+ if (mCallback == null) {
+ return;
+ }
+ switch (msg.what) {
+ case MESSAGE_MEDIA_BUTTON:
+ mCallback.onMediaButton((Intent) msg.obj);
+ break;
+ case MESSAGE_COMMAND:
+ Bundle commandBundle = (Bundle) msg.obj;
+ String command = commandBundle.getString(KEY_COMMAND);
+ Bundle extras = commandBundle.getBundle(KEY_EXTRAS);
+ mCallback.onCommand(command, extras);
+ break;
+ case MESSAGE_ROUTE_CHANGE:
+ mCallback.onRequestRouteChange((Bundle) msg.obj);
+ break;
+ }
+ }
+ msg.recycle();
+ }
+ }
+}
diff --git a/media/java/android/media/MediaSessionManager.java b/media/java/android/media/MediaSessionManager.java
new file mode 100644
index 0000000..90f0071
--- /dev/null
+++ b/media/java/android/media/MediaSessionManager.java
@@ -0,0 +1,89 @@
+/*
+ * 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;
+
+import android.content.Context;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.util.Log;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * MediaSessionManager allows the creation and control of MediaSessions in the
+ * system. A MediaSession enables publishing information about ongoing media and
+ * interacting with MediaControllers and MediaRoutes.
+ * <p>
+ * Use <code>Context.getSystemService(Context.MEDIA_SESSION_SERVICE)</code> to
+ * get an instance of this class.
+ * <p>
+ *
+ * @see MediaSession
+ * @see MediaController
+ */
+public final class MediaSessionManager {
+ private static final String TAG = "MediaSessionManager";
+
+ private final IMediaSessionManager mService;
+
+ private Context mContext;
+
+ /**
+ * @hide
+ */
+ public MediaSessionManager(Context context) {
+ // Consider rewriting like DisplayManagerGlobal
+ // Decide if we need context
+ mContext = context;
+ IBinder b = ServiceManager.getService(Context.MEDIA_SESSION_SERVICE);
+ mService = IMediaSessionManager.Stub.asInterface(b);
+ }
+
+ /**
+ * Creates a new session.
+ *
+ * @param tag A short name for debugging purposes
+ * @return a {@link MediaSession} for the new session
+ */
+ public MediaSession createSession(String tag) {
+ try {
+ MediaSession.CallbackStub cbStub = new MediaSession.CallbackStub();
+ MediaSession session = new MediaSession(mService
+ .createSession(mContext.getPackageName(), cbStub, tag), cbStub);
+ cbStub.setMediaSession(session);
+
+ return session;
+ } catch (RemoteException e) {
+ Log.e(TAG, "Failed to create session: ", e);
+ return null;
+ }
+ }
+
+ /**
+ * Get a list of controllers for all ongoing sessions. This requires the
+ * android.Manifest.permission.MEDIA_CONTENT_CONTROL permission be held by
+ * the calling app.
+ *
+ * @return a list of controllers for ongoing sessions
+ */
+ public List<MediaController> getActiveSessions() {
+ // TODO
+ return new ArrayList<MediaController>();
+ }
+}
diff --git a/media/java/android/media/MediaSessionToken.aidl b/media/java/android/media/MediaSessionToken.aidl
new file mode 100644
index 0000000..e2f1abc
--- /dev/null
+++ b/media/java/android/media/MediaSessionToken.aidl
@@ -0,0 +1,18 @@
+/* Copyright 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;
+
+parcelable MediaSessionToken;
diff --git a/media/java/android/media/MediaSessionToken.java b/media/java/android/media/MediaSessionToken.java
new file mode 100644
index 0000000..885fda3
--- /dev/null
+++ b/media/java/android/media/MediaSessionToken.java
@@ -0,0 +1,65 @@
+/*
+ * 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;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+
+public class MediaSessionToken implements Parcelable {
+ private IMediaController mBinder;
+
+ /**
+ * @hide
+ */
+ MediaSessionToken(IMediaController binder) {
+ mBinder = binder;
+ }
+
+ private MediaSessionToken(Parcel in) {
+ mBinder = IMediaController.Stub.asInterface(in.readStrongBinder());
+ }
+
+ /**
+ * @hide
+ */
+ IMediaController getBinder() {
+ return mBinder;
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeStrongBinder(mBinder.asBinder());
+ }
+
+ public static final Parcelable.Creator<MediaSessionToken> CREATOR
+ = new Parcelable.Creator<MediaSessionToken>() {
+ @Override
+ public MediaSessionToken createFromParcel(Parcel in) {
+ return new MediaSessionToken(in);
+ }
+
+ @Override
+ public MediaSessionToken[] newArray(int size) {
+ return new MediaSessionToken[size];
+ }
+ };
+}
diff --git a/media/java/android/media/RemoteController.java b/media/java/android/media/RemoteController.java
index cd3ce1f..3711585 100644
--- a/media/java/android/media/RemoteController.java
+++ b/media/java/android/media/RemoteController.java
@@ -264,7 +264,7 @@ public final class RemoteController
* @throws IllegalArgumentException
*/
public boolean sendMediaKeyEvent(KeyEvent keyEvent) throws IllegalArgumentException {
- if (!MediaFocusControl.isMediaKeyCode(keyEvent.getKeyCode())) {
+ if (!KeyEvent.isMediaKey(keyEvent.getKeyCode())) {
throw new IllegalArgumentException("not a media key event");
}
final PendingIntent pi;