diff options
author | RoboErik <epastern@google.com> | 2014-02-20 00:25:10 +0000 |
---|---|---|
committer | Android (Google) Code Review <android-gerrit@google.com> | 2014-02-20 00:25:10 +0000 |
commit | 332886854438809e956fb232b69879e33b5dc2bb (patch) | |
tree | ddda4e05eba4ed396691ff90b8f2e498d9c8e699 /media | |
parent | ff699570f62113b4df5b0efd74b9df8b9dbcd1a9 (diff) | |
parent | 01fe661ae5da3739215d93922412df4b24c859a2 (diff) | |
download | frameworks_base-332886854438809e956fb232b69879e33b5dc2bb.zip frameworks_base-332886854438809e956fb232b69879e33b5dc2bb.tar.gz frameworks_base-332886854438809e956fb232b69879e33b5dc2bb.tar.bz2 |
Merge "Initial round of MediaSession APIs"
Diffstat (limited to 'media')
-rw-r--r-- | media/java/android/media/IMediaController.aidl | 34 | ||||
-rw-r--r-- | media/java/android/media/IMediaControllerCallback.aidl | 28 | ||||
-rw-r--r-- | media/java/android/media/IMediaSession.aidl | 33 | ||||
-rw-r--r-- | media/java/android/media/IMediaSessionCallback.aidl | 29 | ||||
-rw-r--r-- | media/java/android/media/IMediaSessionManager.aidl | 28 | ||||
-rw-r--r-- | media/java/android/media/MediaController.java | 363 | ||||
-rw-r--r-- | media/java/android/media/MediaFocusControl.java | 46 | ||||
-rw-r--r-- | media/java/android/media/MediaSession.java | 302 | ||||
-rw-r--r-- | media/java/android/media/MediaSessionManager.java | 89 | ||||
-rw-r--r-- | media/java/android/media/MediaSessionToken.aidl | 18 | ||||
-rw-r--r-- | media/java/android/media/MediaSessionToken.java | 65 | ||||
-rw-r--r-- | media/java/android/media/RemoteController.java | 2 |
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; |