OSDN Git Service

Initial round of MediaSession APIs
authorRoboErik <epastern@google.com>
Thu, 13 Feb 2014 22:19:04 +0000 (14:19 -0800)
committerRoboErik <epastern@google.com>
Wed, 19 Feb 2014 21:41:37 +0000 (13:41 -0800)
This is far from complete but puts the basic components in place
for an app to interact with media sessions.

Change-Id: Icfe313f90ad76ae56badbe42b0e43fc5f68db36f

20 files changed:
Android.mk
api/current.txt
core/java/android/app/ContextImpl.java
core/java/android/content/Context.java
core/java/android/view/KeyEvent.java
media/java/android/media/IMediaController.aidl [new file with mode: 0644]
media/java/android/media/IMediaControllerCallback.aidl [new file with mode: 0644]
media/java/android/media/IMediaSession.aidl [new file with mode: 0644]
media/java/android/media/IMediaSessionCallback.aidl [new file with mode: 0644]
media/java/android/media/IMediaSessionManager.aidl [new file with mode: 0644]
media/java/android/media/MediaController.java [new file with mode: 0644]
media/java/android/media/MediaFocusControl.java
media/java/android/media/MediaSession.java [new file with mode: 0644]
media/java/android/media/MediaSessionManager.java [new file with mode: 0644]
media/java/android/media/MediaSessionToken.aidl [new file with mode: 0644]
media/java/android/media/MediaSessionToken.java [new file with mode: 0644]
media/java/android/media/RemoteController.java
services/core/java/com/android/server/media/MediaSessionRecord.java [new file with mode: 0644]
services/core/java/com/android/server/media/MediaSessionService.java [new file with mode: 0644]
services/java/com/android/server/SystemServer.java

index 8986af5..ea87efb 100644 (file)
@@ -260,12 +260,17 @@ LOCAL_SRC_FILES += \
        media/java/android/media/IAudioService.aidl \
        media/java/android/media/IAudioFocusDispatcher.aidl \
        media/java/android/media/IAudioRoutesObserver.aidl \
+       media/java/android/media/IMediaController.aidl \
+       media/java/android/media/IMediaControllerCallback.aidl \
        media/java/android/media/IMediaHTTPConnection.aidl \
        media/java/android/media/IMediaHTTPService.aidl \
        media/java/android/media/IMediaRouterClient.aidl \
        media/java/android/media/IMediaRouterService.aidl \
        media/java/android/media/IMediaScannerListener.aidl \
        media/java/android/media/IMediaScannerService.aidl \
+       media/java/android/media/IMediaSession.aidl \
+       media/java/android/media/IMediaSessionCallback.aidl \
+       media/java/android/media/IMediaSessionManager.aidl \
        media/java/android/media/IRemoteControlClient.aidl \
        media/java/android/media/IRemoteControlDisplay.aidl \
        media/java/android/media/IRemoteDisplayCallback.aidl \
index 3ea1aad..b761813 100644 (file)
@@ -6316,6 +6316,7 @@ package android.content {
     field public static final java.lang.String LAYOUT_INFLATER_SERVICE = "layout_inflater";
     field public static final java.lang.String LOCATION_SERVICE = "location";
     field public static final java.lang.String MEDIA_ROUTER_SERVICE = "media_router";
+    field public static final java.lang.String MEDIA_SESSION_SERVICE = "media_session";
     field public static final int MODE_APPEND = 32768; // 0x8000
     field public static final int MODE_ENABLE_WRITE_AHEAD_LOGGING = 8; // 0x8
     field public static final int MODE_MULTI_PROCESS = 4; // 0x4
@@ -13150,6 +13151,23 @@ package android.media {
     method public static final android.media.MediaCodecInfo getCodecInfoAt(int);
   }
 
+  public final class MediaController {
+    ctor public MediaController(android.media.MediaSessionToken);
+    method public void addCallback(android.media.MediaController.Callback);
+    method public void addCallback(android.media.MediaController.Callback, android.os.Handler);
+    method public void removeCallback(android.media.MediaController.Callback);
+    method public void sendCommand(java.lang.String, android.os.Bundle);
+    method public void sendMediaButton(int);
+  }
+
+  public static abstract class MediaController.Callback {
+    ctor public MediaController.Callback();
+    method public void onEvent(java.lang.String, android.os.Bundle);
+    method public void onMetadataUpdate(android.os.Bundle);
+    method public void onPlaybackStateChange(int);
+    method public void onRouteChanged(android.os.Bundle);
+  }
+
   public final class MediaCrypto {
     ctor public MediaCrypto(java.util.UUID, byte[]) throws android.media.MediaCryptoException;
     method public static final boolean isCryptoSchemeSupported(java.util.UUID);
@@ -13716,6 +13734,33 @@ package android.media {
     method public abstract void onScanCompleted(java.lang.String, android.net.Uri);
   }
 
+  public final class MediaSession {
+    method public void addCallback(android.media.MediaSession.Callback);
+    method public void addCallback(android.media.MediaSession.Callback, android.os.Handler);
+    method public android.media.MediaSessionToken getSessionToken();
+    method public void release();
+    method public void removeCallback(android.media.MediaSession.Callback);
+    method public void setPlaybackState(int);
+  }
+
+  public static abstract class MediaSession.Callback {
+    ctor public MediaSession.Callback();
+    method public void onCommand(java.lang.String, android.os.Bundle);
+    method public void onMediaButton(android.content.Intent);
+    method public void onRequestRouteChange(android.os.Bundle);
+  }
+
+  public final class MediaSessionManager {
+    method public android.media.MediaSession createSession(java.lang.String);
+    method public java.util.List<android.media.MediaController> getActiveSessions();
+  }
+
+  public class MediaSessionToken implements android.os.Parcelable {
+    method public int describeContents();
+    method public void writeToParcel(android.os.Parcel, int);
+    field public static final android.os.Parcelable.Creator CREATOR;
+  }
+
   public class MediaSyncEvent {
     method public static android.media.MediaSyncEvent createEvent(int) throws java.lang.IllegalArgumentException;
     method public int getAudioSessionId();
index 8d127c6..873db8e 100644 (file)
@@ -17,6 +17,7 @@
 package android.app;
 
 import android.os.Build;
+
 import com.android.internal.policy.PolicyManager;
 import com.android.internal.util.Preconditions;
 
@@ -62,6 +63,7 @@ import android.location.ILocationManager;
 import android.location.LocationManager;
 import android.media.AudioManager;
 import android.media.MediaRouter;
+import android.media.MediaSessionManager;
 import android.net.ConnectivityManager;
 import android.net.IConnectivityManager;
 import android.net.INetworkPolicyManager;
@@ -587,6 +589,12 @@ class ContextImpl extends Context {
             public Object createService(ContextImpl ctx) {
                 return new ConsumerIrManager(ctx);
             }});
+
+        registerService(MEDIA_SESSION_SERVICE, new ServiceFetcher() {
+            public Object createService(ContextImpl ctx) {
+                return new MediaSessionManager(ctx);
+            }
+        });
     }
 
     static ContextImpl getImpl(Context context) {
index 9f90de0..d05d1a1 100644 (file)
@@ -1840,7 +1840,7 @@ public abstract class Context {
      * @hide like {@link #stopService(Intent)} but for a specific user.
      */
     public abstract boolean stopServiceAsUser(Intent service, UserHandle user);
-    
+
     /**
      * Connect to an application service, creating it if needed.  This defines
      * a dependency between your application and the service.  The given
@@ -1989,7 +1989,8 @@ public abstract class Context {
             USER_SERVICE,
             //@hide: APP_OPS_SERVICE
             CAMERA_SERVICE,
-            PRINT_SERVICE
+            PRINT_SERVICE,
+            MEDIA_SESSION_SERVICE,
     })
     @Retention(RetentionPolicy.SOURCE)
     public @interface ServiceName {}
@@ -2351,6 +2352,15 @@ public abstract class Context {
 
     /**
      * Use with {@link #getSystemService} to retrieve a
+     * {@link android.media.MediaSessionManager} for managing media Sessions.
+     *
+     * @see #getSystemService
+     * @see android.media.MediaSessionManager
+     */
+    public static final String MEDIA_SESSION_SERVICE = "media_session";
+
+    /**
+     * Use with {@link #getSystemService} to retrieve a
      * {@link android.telephony.TelephonyManager} for handling management the
      * telephony features of the device.
      *
index 30b1e52..7e745d8 100644 (file)
@@ -1158,25 +1158,25 @@ public class KeyEvent extends InputEvent implements Parcelable {
      * This mask is set if the device woke because of this key event.
      */
     public static final int FLAG_WOKE_HERE = 0x1;
-    
+
     /**
      * This mask is set if the key event was generated by a software keyboard.
      */
     public static final int FLAG_SOFT_KEYBOARD = 0x2;
-    
+
     /**
      * This mask is set if we don't want the key event to cause us to leave
      * touch mode.
      */
     public static final int FLAG_KEEP_TOUCH_MODE = 0x4;
-    
+
     /**
      * This mask is set if an event was known to come from a trusted part
      * of the system.  That is, the event is known to come from the user,
      * and could not have been spoofed by a third party component.
      */
     public static final int FLAG_FROM_SYSTEM = 0x8;
-    
+
     /**
      * This mask is used for compatibility, to identify enter keys that are
      * coming from an IME whose enter key has been auto-labelled "next" or
@@ -1185,7 +1185,7 @@ public class KeyEvent extends InputEvent implements Parcelable {
      * receiving them.
      */
     public static final int FLAG_EDITOR_ACTION = 0x10;
-    
+
     /**
      * When associated with up key events, this indicates that the key press
      * has been canceled.  Typically this is used with virtual touch screen
@@ -1194,29 +1194,29 @@ public class KeyEvent extends InputEvent implements Parcelable {
      * event and should not perform the action normally associated with the
      * key.  Note that for this to work, the application can not perform an
      * action for a key until it receives an up or the long press timeout has
-     * expired. 
+     * expired.
      */
     public static final int FLAG_CANCELED = 0x20;
-    
+
     /**
      * This key event was generated by a virtual (on-screen) hard key area.
      * Typically this is an area of the touchscreen, outside of the regular
      * display, dedicated to "hardware" buttons.
      */
     public static final int FLAG_VIRTUAL_HARD_KEY = 0x40;
-    
+
     /**
      * This flag is set for the first key repeat that occurs after the
      * long press timeout.
      */
     public static final int FLAG_LONG_PRESS = 0x80;
-    
+
     /**
      * Set when a key event has {@link #FLAG_CANCELED} set because a long
-     * press action was executed while it was down. 
+     * press action was executed while it was down.
      */
     public static final int FLAG_CANCELED_LONG_PRESS = 0x100;
-    
+
     /**
      * Set for {@link #ACTION_UP} when this event's key code is still being
      * tracked from its initial down.  That is, somebody requested that tracking
@@ -1273,7 +1273,7 @@ public class KeyEvent extends InputEvent implements Parcelable {
     public static int getDeadChar(int accent, int c) {
         return KeyCharacterMap.getDeadChar(accent, c);
     }
-    
+
     static final boolean DEBUG = false;
     static final String TAG = "KeyEvent";
 
@@ -1303,10 +1303,10 @@ public class KeyEvent extends InputEvent implements Parcelable {
          * KeyEvent.startTracking()} to have the framework track the event
          * through its {@link #onKeyUp(int, KeyEvent)} and also call your
          * {@link #onKeyLongPress(int, KeyEvent)} if it occurs.
-         * 
+         *
          * @param keyCode The value in event.getKeyCode().
          * @param event Description of the key event.
-         * 
+         *
          * @return If you handled the event, return true.  If you want to allow
          *         the event to be handled by the next receiver, return false.
          */
@@ -1319,10 +1319,10 @@ public class KeyEvent extends InputEvent implements Parcelable {
          * order to receive this callback, someone in the event change
          * <em>must</em> return true from {@link #onKeyDown} <em>and</em>
          * call {@link KeyEvent#startTracking()} on the event.
-         * 
+         *
          * @param keyCode The value in event.getKeyCode().
          * @param event Description of the key event.
-         * 
+         *
          * @return If you handled the event, return true.  If you want to allow
          *         the event to be handled by the next receiver, return false.
          */
@@ -1330,10 +1330,10 @@ public class KeyEvent extends InputEvent implements Parcelable {
 
         /**
          * Called when a key up event has occurred.
-         * 
+         *
          * @param keyCode The value in event.getKeyCode().
          * @param event Description of the key event.
-         * 
+         *
          * @return If you handled the event, return true.  If you want to allow
          *         the event to be handled by the next receiver, return false.
          */
@@ -1342,11 +1342,11 @@ public class KeyEvent extends InputEvent implements Parcelable {
         /**
          * Called when multiple down/up pairs of the same key have occurred
          * in a row.
-         * 
+         *
          * @param keyCode The value in event.getKeyCode().
          * @param count Number of pairs as returned by event.getRepeatCount().
          * @param event Description of the key event.
-         * 
+         *
          * @return If you handled the event, return true.  If you want to allow
          *         the event to be handled by the next receiver, return false.
          */
@@ -1362,7 +1362,7 @@ public class KeyEvent extends InputEvent implements Parcelable {
 
     /**
      * Create a new key event.
-     * 
+     *
      * @param action Action code: either {@link #ACTION_DOWN},
      * {@link #ACTION_UP}, or {@link #ACTION_MULTIPLE}.
      * @param code The key code.
@@ -1376,7 +1376,7 @@ public class KeyEvent extends InputEvent implements Parcelable {
 
     /**
      * Create a new key event.
-     * 
+     *
      * @param downTime The time (in {@link android.os.SystemClock#uptimeMillis})
      * at which this key code originally went down.
      * @param eventTime The time (in {@link android.os.SystemClock#uptimeMillis})
@@ -1399,7 +1399,7 @@ public class KeyEvent extends InputEvent implements Parcelable {
 
     /**
      * Create a new key event.
-     * 
+     *
      * @param downTime The time (in {@link android.os.SystemClock#uptimeMillis})
      * at which this key code originally went down.
      * @param eventTime The time (in {@link android.os.SystemClock#uptimeMillis})
@@ -1424,7 +1424,7 @@ public class KeyEvent extends InputEvent implements Parcelable {
 
     /**
      * Create a new key event.
-     * 
+     *
      * @param downTime The time (in {@link android.os.SystemClock#uptimeMillis})
      * at which this key code originally went down.
      * @param eventTime The time (in {@link android.os.SystemClock#uptimeMillis})
@@ -1453,7 +1453,7 @@ public class KeyEvent extends InputEvent implements Parcelable {
 
     /**
      * Create a new key event.
-     * 
+     *
      * @param downTime The time (in {@link android.os.SystemClock#uptimeMillis})
      * at which this key code originally went down.
      * @param eventTime The time (in {@link android.os.SystemClock#uptimeMillis})
@@ -1484,7 +1484,7 @@ public class KeyEvent extends InputEvent implements Parcelable {
 
     /**
      * Create a new key event.
-     * 
+     *
      * @param downTime The time (in {@link android.os.SystemClock#uptimeMillis})
      * at which this key code originally went down.
      * @param eventTime The time (in {@link android.os.SystemClock#uptimeMillis})
@@ -1520,7 +1520,7 @@ public class KeyEvent extends InputEvent implements Parcelable {
      * action, repeat count and source will automatically be set to
      * {@link #KEYCODE_UNKNOWN}, {@link #ACTION_MULTIPLE}, 0, and
      * {@link InputDevice#SOURCE_KEYBOARD} for you.
-     * 
+     *
      * @param time The time (in {@link android.os.SystemClock#uptimeMillis})
      * at which this event occured.
      * @param characters The string of characters.
@@ -1558,10 +1558,10 @@ public class KeyEvent extends InputEvent implements Parcelable {
 
     /**
      * Copy an existing key event, modifying its time and repeat count.
-     * 
+     *
      * @deprecated Use {@link #changeTimeRepeat(KeyEvent, long, int)}
      * instead.
-     * 
+     *
      * @param origEvent The existing event to be copied.
      * @param eventTime The new event time
      * (in {@link android.os.SystemClock#uptimeMillis}) of the event.
@@ -1677,7 +1677,7 @@ public class KeyEvent extends InputEvent implements Parcelable {
     /**
      * Create a new key event that is the same as the given one, but whose
      * event time and repeat count are replaced with the given value.
-     * 
+     *
      * @param event The existing event to be copied.  This is not modified.
      * @param eventTime The new event time
      * (in {@link android.os.SystemClock#uptimeMillis}) of the event.
@@ -1687,11 +1687,11 @@ public class KeyEvent extends InputEvent implements Parcelable {
             int newRepeat) {
         return new KeyEvent(event, eventTime, newRepeat);
     }
-    
+
     /**
      * Create a new key event that is the same as the given one, but whose
      * event time and repeat count are replaced with the given value.
-     * 
+     *
      * @param event The existing event to be copied.  This is not modified.
      * @param eventTime The new event time
      * (in {@link android.os.SystemClock#uptimeMillis}) of the event.
@@ -1707,10 +1707,10 @@ public class KeyEvent extends InputEvent implements Parcelable {
         ret.mFlags = newFlags;
         return ret;
     }
-    
+
     /**
      * Copy an existing key event, modifying its action.
-     * 
+     *
      * @param origEvent The existing event to be copied.
      * @param action The new action code of the event.
      */
@@ -1732,18 +1732,18 @@ public class KeyEvent extends InputEvent implements Parcelable {
     /**
      * Create a new key event that is the same as the given one, but whose
      * action is replaced with the given value.
-     * 
+     *
      * @param event The existing event to be copied.  This is not modified.
      * @param action The new action code of the event.
      */
     public static KeyEvent changeAction(KeyEvent event, int action) {
         return new KeyEvent(event, action);
     }
-    
+
     /**
      * Create a new key event that is the same as the given one, but whose
      * flags are replaced with the given value.
-     * 
+     *
      * @param event The existing event to be copied.  This is not modified.
      * @param flags The new flags constant.
      */
@@ -1768,7 +1768,7 @@ public class KeyEvent extends InputEvent implements Parcelable {
     /**
      * Don't use in new code, instead explicitly check
      * {@link #getAction()}.
-     * 
+     *
      * @return If the action is ACTION_DOWN, returns true; else false.
      *
      * @deprecated
@@ -1780,7 +1780,7 @@ public class KeyEvent extends InputEvent implements Parcelable {
 
     /**
      * Is this a system key?  System keys can not be used for menu shortcuts.
-     * 
+     *
      * TODO: this information should come from a table somewhere.
      * TODO: should the dpad keys be here?  arguably, because they also shouldn't be menu shortcuts
      */
@@ -1849,6 +1849,30 @@ public class KeyEvent extends InputEvent implements Parcelable {
         }
     }
 
+    /**
+     * Whether this key is a media key, which can be send to apps that are
+     * interested in media key events.
+     *
+     * @hide
+     */
+    public static final boolean isMediaKey(int keyCode) {
+        switch (keyCode) {
+            case KeyEvent.KEYCODE_MEDIA_PLAY:
+            case KeyEvent.KEYCODE_MEDIA_PAUSE:
+            case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE:
+            case KeyEvent.KEYCODE_MUTE:
+            case KeyEvent.KEYCODE_HEADSETHOOK:
+            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:
+                return true;
+        }
+        return false;
+    }
+
     /** {@inheritDoc} */
     @Override
     public final int getDeviceId() {
@@ -2318,7 +2342,7 @@ public class KeyEvent extends InputEvent implements Parcelable {
     /**
      * Retrieve the action of this key event.  May be either
      * {@link #ACTION_DOWN}, {@link #ACTION_UP}, or {@link #ACTION_MULTIPLE}.
-     * 
+     *
      * @return The event action: ACTION_DOWN, ACTION_UP, or ACTION_MULTIPLE.
      */
     public final int getAction() {
@@ -2332,7 +2356,7 @@ public class KeyEvent extends InputEvent implements Parcelable {
     public final boolean isCanceled() {
         return (mFlags&FLAG_CANCELED) != 0;
     }
-    
+
     /**
      * Call this during {@link Callback#onKeyDown} to have the system track
      * the key through its final up (possibly including a long press).  Note
@@ -2343,7 +2367,7 @@ public class KeyEvent extends InputEvent implements Parcelable {
     public final void startTracking() {
         mFlags |= FLAG_START_TRACKING;
     }
-    
+
     /**
      * For {@link #ACTION_UP} events, indicates that the event is still being
      * tracked from its initial down event as per
@@ -2352,7 +2376,7 @@ public class KeyEvent extends InputEvent implements Parcelable {
     public final boolean isTracking() {
         return (mFlags&FLAG_TRACKING) != 0;
     }
-    
+
     /**
      * For {@link #ACTION_DOWN} events, indicates that the event has been
      * canceled as per {@link #FLAG_LONG_PRESS}.
@@ -2360,11 +2384,11 @@ public class KeyEvent extends InputEvent implements Parcelable {
     public final boolean isLongPress() {
         return (mFlags&FLAG_LONG_PRESS) != 0;
     }
-    
+
     /**
      * Retrieve the key code of the key event.  This is the physical key that
      * was pressed, <em>not</em> the Unicode character.
-     * 
+     *
      * @return The key code of the event.
      */
     public final int getKeyCode() {
@@ -2375,14 +2399,14 @@ public class KeyEvent extends InputEvent implements Parcelable {
      * For the special case of a {@link #ACTION_MULTIPLE} event with key
      * code of {@link #KEYCODE_UNKNOWN}, this is a raw string of characters
      * associated with the event.  In all other cases it is null.
-     * 
+     *
      * @return Returns a String of 1 or more characters associated with
      * the event.
      */
     public final String getCharacters() {
         return mCharacters;
     }
-    
+
     /**
      * Retrieve the hardware key id of this key event.  These values are not
      * reliable and vary from device to device.
@@ -2399,7 +2423,7 @@ public class KeyEvent extends InputEvent implements Parcelable {
      * events, this is the number of times the key has repeated with the first
      * down starting at 0 and counting up from there.  For multiple key
      * events, this is the number of down/up pairs that have occurred.
-     * 
+     *
      * @return The number of times the key has repeated.
      */
     public final int getRepeatCount() {
@@ -2413,7 +2437,7 @@ public class KeyEvent extends InputEvent implements Parcelable {
      * Note that when chording keys, this value is the down time of the
      * most recently pressed key, which may <em>not</em> be the same physical
      * key of this event.
-     * 
+     *
      * @return Returns the most recent key down time, in the
      * {@link android.os.SystemClock#uptimeMillis} time base
      */
@@ -2425,7 +2449,7 @@ public class KeyEvent extends InputEvent implements Parcelable {
      * Retrieve the time this event occurred,
      * in the {@link android.os.SystemClock#uptimeMillis} time base.
      *
-     * @return Returns the time this event occurred, 
+     * @return Returns the time this event occurred,
      * in the {@link android.os.SystemClock#uptimeMillis} time base.
      */
     @Override
@@ -2454,7 +2478,7 @@ public class KeyEvent extends InputEvent implements Parcelable {
 
     /**
      * Renamed to {@link #getDeviceId}.
-     * 
+     *
      * @hide
      * @deprecated use {@link #getDeviceId()} instead.
      */
@@ -2486,7 +2510,7 @@ public class KeyEvent extends InputEvent implements Parcelable {
     public char getDisplayLabel() {
         return getKeyCharacterMap().getDisplayLabel(mKeyCode);
     }
-    
+
     /**
      * Gets the Unicode character generated by the specified key and meta
      * key state combination.
@@ -2509,7 +2533,7 @@ public class KeyEvent extends InputEvent implements Parcelable {
     public int getUnicodeChar() {
         return getUnicodeChar(mMetaState);
     }
-    
+
     /**
      * Gets the Unicode character generated by the specified key and meta
      * key state combination.
@@ -2533,7 +2557,7 @@ public class KeyEvent extends InputEvent implements Parcelable {
     public int getUnicodeChar(int metaState) {
         return getKeyCharacterMap().get(mKeyCode, metaState);
     }
-    
+
     /**
      * Get the character conversion data for a given key code.
      *
@@ -2548,7 +2572,7 @@ public class KeyEvent extends InputEvent implements Parcelable {
     public boolean getKeyData(KeyData results) {
         return getKeyCharacterMap().getKeyData(mKeyCode, results);
     }
-    
+
     /**
      * Gets the first character in the character array that can be generated
      * by the specified key code.
@@ -2563,7 +2587,7 @@ public class KeyEvent extends InputEvent implements Parcelable {
     public char getMatch(char[] chars) {
         return getMatch(chars, 0);
     }
-    
+
     /**
      * Gets the first character in the character array that can be generated
      * by the specified key code.  If there are multiple choices, prefers
@@ -2576,7 +2600,7 @@ public class KeyEvent extends InputEvent implements Parcelable {
     public char getMatch(char[] chars, int metaState) {
         return getKeyCharacterMap().getMatch(mKeyCode, chars, metaState);
     }
-    
+
     /**
      * Gets the number or symbol associated with the key.
      * <p>
@@ -2600,7 +2624,7 @@ public class KeyEvent extends InputEvent implements Parcelable {
     public char getNumber() {
         return getKeyCharacterMap().getNumber(mKeyCode);
     }
-    
+
     /**
      * Returns true if this key produces a glyph.
      *
@@ -2609,7 +2633,7 @@ public class KeyEvent extends InputEvent implements Parcelable {
     public boolean isPrintingKey() {
         return getKeyCharacterMap().isPrintingKey(mKeyCode);
     }
-    
+
     /**
      * @deprecated Use {@link #dispatch(Callback, DispatcherState, Object)} instead.
      */
@@ -2617,16 +2641,16 @@ public class KeyEvent extends InputEvent implements Parcelable {
     public final boolean dispatch(Callback receiver) {
         return dispatch(receiver, null, null);
     }
-    
+
     /**
      * Deliver this key event to a {@link Callback} interface.  If this is
      * an ACTION_MULTIPLE event and it is not handled, then an attempt will
      * be made to deliver a single normal event.
-     * 
+     *
      * @param receiver The Callback that will be given the event.
      * @param state State information retained across events.
      * @param target The target of the dispatch, for use in tracking.
-     * 
+     *
      * @return The return value from the Callback method that was called.
      */
     public final boolean dispatch(Callback receiver, DispatcherState state,
@@ -2692,7 +2716,7 @@ public class KeyEvent extends InputEvent implements Parcelable {
         int mDownKeyCode;
         Object mDownTarget;
         SparseIntArray mActiveLongPresses = new SparseIntArray();
-        
+
         /**
          * Reset back to initial state.
          */
@@ -2702,7 +2726,7 @@ public class KeyEvent extends InputEvent implements Parcelable {
             mDownTarget = null;
             mActiveLongPresses.clear();
         }
-        
+
         /**
          * Stop any tracking associated with this target.
          */
@@ -2713,14 +2737,14 @@ public class KeyEvent extends InputEvent implements Parcelable {
                 mDownTarget = null;
             }
         }
-        
+
         /**
          * Start tracking the key code associated with the given event.  This
          * can only be called on a key down.  It will allow you to see any
          * long press associated with the key, and will result in
          * {@link KeyEvent#isTracking} return true on the long press and up
          * events.
-         * 
+         *
          * <p>This is only needed if you are directly dispatching events, rather
          * than handling them in {@link Callback#onKeyDown}.
          */
@@ -2733,7 +2757,7 @@ public class KeyEvent extends InputEvent implements Parcelable {
             mDownKeyCode = event.getKeyCode();
             mDownTarget = target;
         }
-        
+
         /**
          * Return true if the key event is for a key code that is currently
          * being tracked by the dispatcher.
@@ -2741,7 +2765,7 @@ public class KeyEvent extends InputEvent implements Parcelable {
         public boolean isTracking(KeyEvent event) {
             return mDownKeyCode == event.getKeyCode();
         }
-        
+
         /**
          * Keep track of the given event's key code as having performed an
          * action with a long press, so no action should occur on the up.
@@ -2751,7 +2775,7 @@ public class KeyEvent extends InputEvent implements Parcelable {
         public void performedLongPress(KeyEvent event) {
             mActiveLongPresses.put(event.getKeyCode(), 1);
         }
-        
+
         /**
          * Handle key up event to stop tracking.  This resets the dispatcher state,
          * and updates the key event state based on it.
@@ -2906,12 +2930,12 @@ public class KeyEvent extends InputEvent implements Parcelable {
             return new KeyEvent[size];
         }
     };
-    
+
     /** @hide */
     public static KeyEvent createFromParcelBody(Parcel in) {
         return new KeyEvent(in);
     }
-    
+
     private KeyEvent(Parcel in) {
         mDeviceId = in.readInt();
         mSource = in.readInt();
diff --git a/media/java/android/media/IMediaController.aidl b/media/java/android/media/IMediaController.aidl
new file mode 100644 (file)
index 0000000..fc3525a
--- /dev/null
@@ -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 (file)
index 0000000..b54d0cf
--- /dev/null
@@ -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 (file)
index 0000000..ed71d78
--- /dev/null
@@ -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 (file)
index 0000000..3aaf925
--- /dev/null
@@ -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 (file)
index 0000000..8bc0c3b
--- /dev/null
@@ -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 (file)
index 0000000..cedb0c3
--- /dev/null
@@ -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);
+            }
+        }
+    }
+
+}
index 25ab99d..b155cda 100644 (file)
@@ -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 (file)
index 0000000..038a9cf
--- /dev/null
@@ -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 (file)
index 0000000..90f0071
--- /dev/null
@@ -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 (file)
index 0000000..e2f1abc
--- /dev/null
@@ -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 (file)
index 0000000..885fda3
--- /dev/null
@@ -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];
+        }
+    };
+}
index cd3ce1f..3711585 100644 (file)
@@ -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;
diff --git a/services/core/java/com/android/server/media/MediaSessionRecord.java b/services/core/java/com/android/server/media/MediaSessionRecord.java
new file mode 100644 (file)
index 0000000..0d3fa84
--- /dev/null
@@ -0,0 +1,200 @@
+/*
+ * 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 com.android.server.media;
+
+import android.content.Intent;
+import android.media.IMediaController;
+import android.media.IMediaControllerCallback;
+import android.media.IMediaSession;
+import android.media.IMediaSessionCallback;
+import android.media.RemoteControlClient;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.util.Log;
+import android.view.KeyEvent;
+
+import java.util.ArrayList;
+
+/**
+ * This is the system implementation of a Session. Apps will interact with the
+ * MediaSession wrapper class instead.
+ */
+public class MediaSessionRecord implements IBinder.DeathRecipient {
+    private static final String TAG = "MediaSessionImpl";
+
+    private final int mPid;
+    private final String mPackageName;
+    private final String mTag;
+    private final ControllerStub mController;
+    private final SessionStub mSession;
+    private final SessionCb mSessionCb;
+    private final MediaSessionService mService;
+
+    private final ArrayList<IMediaControllerCallback> mSessionCallbacks =
+            new ArrayList<IMediaControllerCallback>();
+
+    private int mPlaybackState = RemoteControlClient.PLAYSTATE_NONE;
+
+    public MediaSessionRecord(int pid, String packageName, IMediaSessionCallback cb, String tag,
+            MediaSessionService service) {
+        mPid = pid;
+        mPackageName = packageName;
+        mTag = tag;
+        mController = new ControllerStub();
+        mSession = new SessionStub();
+        mSessionCb = new SessionCb(cb);
+        mService = service;
+    }
+
+    public IMediaSession getSessionBinder() {
+        return mSession;
+    }
+
+    public IMediaController getControllerBinder() {
+        return mController;
+    }
+
+    public void setPlaybackStateInternal(int state) {
+        mPlaybackState = state;
+        for (int i = mSessionCallbacks.size() - 1; i >= 0; i--) {
+            IMediaControllerCallback cb = mSessionCallbacks.get(i);
+            try {
+                cb.onPlaybackUpdate(state);
+            } catch (RemoteException e) {
+                Log.d(TAG, "SessionCallback object dead in setPlaybackState.", e);
+                mSessionCallbacks.remove(i);
+            }
+        }
+    }
+
+    @Override
+    public void binderDied() {
+        mService.sessionDied(this);
+    }
+
+    private void onDestroy() {
+        mService.destroySession(this);
+    }
+
+    private final class SessionStub extends IMediaSession.Stub {
+
+        @Override
+        public void setPlaybackState(int state) throws RemoteException {
+            setPlaybackStateInternal(state);
+        }
+
+        @Override
+        public void destroy() throws RemoteException {
+            onDestroy();
+        }
+
+        @Override
+        public void sendEvent(Bundle data) throws RemoteException {
+        }
+
+        @Override
+        public IMediaController getMediaSessionToken() throws RemoteException {
+            return mController;
+        }
+
+        @Override
+        public void setMetadata(Bundle metadata) throws RemoteException {
+        }
+
+        @Override
+        public void setRouteState(Bundle routeState) throws RemoteException {
+        }
+
+        @Override
+        public void setRoute(Bundle medaiRouteDescriptor) throws RemoteException {
+        }
+
+    }
+
+    class SessionCb {
+        private final IMediaSessionCallback mCb;
+
+        public SessionCb(IMediaSessionCallback cb) {
+            mCb = cb;
+        }
+
+        public void sendMediaButton(KeyEvent keyEvent) {
+            Intent mediaButtonIntent = new Intent(Intent.ACTION_MEDIA_BUTTON);
+            mediaButtonIntent.putExtra(Intent.EXTRA_KEY_EVENT, keyEvent);
+            try {
+                mCb.onMediaButton(mediaButtonIntent);
+            } catch (RemoteException e) {
+                Log.d(TAG, "Controller object dead in sendMediaRequest.", e);
+                onDestroy();
+            }
+        }
+
+        public void sendCommand(String command, Bundle extras) {
+            try {
+                mCb.onCommand(command, extras);
+            } catch (RemoteException e) {
+                Log.d(TAG, "Controller object dead in sendCommand.", e);
+                onDestroy();
+            }
+        }
+
+        public void registerCallbackListener(IMediaSessionCallback cb) {
+
+        }
+
+    }
+
+    class ControllerStub extends IMediaController.Stub {
+        /*
+         */
+        @Override
+        public void sendCommand(String command, Bundle extras) throws RemoteException {
+            mSessionCb.sendCommand(command, extras);
+        }
+
+        @Override
+        public void sendMediaButton(KeyEvent mediaButtonIntent) {
+            mSessionCb.sendMediaButton(mediaButtonIntent);
+        }
+
+        /*
+         */
+        @Override
+        public void registerCallbackListener(IMediaControllerCallback cb) throws RemoteException {
+            if (!mSessionCallbacks.contains(cb)) {
+                mSessionCallbacks.add(cb);
+            }
+        }
+
+        /*
+         */
+        @Override
+        public void unregisterCallbackListener(IMediaControllerCallback cb)
+                throws RemoteException {
+            mSessionCallbacks.remove(cb);
+        }
+
+        /*
+         */
+        @Override
+        public int getPlaybackState() throws RemoteException {
+            return mPlaybackState;
+        }
+    }
+
+}
diff --git a/services/core/java/com/android/server/media/MediaSessionService.java b/services/core/java/com/android/server/media/MediaSessionService.java
new file mode 100644 (file)
index 0000000..9c96c35
--- /dev/null
@@ -0,0 +1,128 @@
+/*
+ * 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 com.android.server.media;
+
+import android.content.Context;
+import android.media.IMediaSession;
+import android.media.IMediaSessionCallback;
+import android.media.IMediaSessionManager;
+import android.os.Binder;
+import android.os.RemoteException;
+import android.text.TextUtils;
+import android.util.Log;
+
+import com.android.server.SystemService;
+
+import java.util.ArrayList;
+
+/**
+ * System implementation of MediaSessionManager
+ */
+public class MediaSessionService extends SystemService {
+    private static final String TAG = "MediaSessionService";
+    private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
+
+    private final SessionManagerImpl mSessionManagerImpl;
+
+    private final ArrayList<MediaSessionRecord> mSessions
+            = new ArrayList<MediaSessionRecord>();
+    private final Object mLock = new Object();
+
+    public MediaSessionService(Context context) {
+        super(context);
+        mSessionManagerImpl = new SessionManagerImpl();
+    }
+
+    @Override
+    public void onStart() {
+        publishBinderService(Context.MEDIA_SESSION_SERVICE, mSessionManagerImpl);
+    }
+
+    void sessionDied(MediaSessionRecord session) {
+        synchronized (mSessions) {
+            destroySessionLocked(session);
+        }
+    }
+
+    void destroySession(MediaSessionRecord session) {
+        synchronized (mSessions) {
+            destroySessionLocked(session);
+        }
+    }
+
+    private void destroySessionLocked(MediaSessionRecord session) {
+        mSessions.remove(session);
+    }
+
+    private void enforcePackageName(String packageName, int uid) {
+        if (TextUtils.isEmpty(packageName)) {
+            throw new IllegalArgumentException("packageName may not be empty");
+        }
+        String[] packages = getContext().getPackageManager().getPackagesForUid(uid);
+        final int packageCount = packages.length;
+        for (int i = 0; i < packageCount; i++) {
+            if (packageName.equals(packages[i])) {
+                return;
+            }
+        }
+        throw new IllegalArgumentException("packageName is not owned by the calling process");
+    }
+
+    private MediaSessionRecord createSessionInternal(int pid, String packageName,
+            IMediaSessionCallback cb, String tag) {
+        synchronized (mLock) {
+            return createSessionLocked(pid, packageName, cb, tag);
+        }
+    }
+
+    private MediaSessionRecord createSessionLocked(int pid, String packageName,
+            IMediaSessionCallback cb, String tag) {
+        final MediaSessionRecord session = new MediaSessionRecord(pid, packageName, cb, tag, this);
+        try {
+            cb.asBinder().linkToDeath(session, 0);
+        } catch (RemoteException e) {
+            throw new RuntimeException("Media Session owner died prematurely.", e);
+        }
+        synchronized (mSessions) {
+            mSessions.add(session);
+        }
+        if (DEBUG) {
+            Log.d(TAG, "Created session for package " + packageName + " with tag " + tag);
+        }
+        return session;
+    }
+
+    class SessionManagerImpl extends IMediaSessionManager.Stub {
+        @Override
+        public IMediaSession createSession(String packageName, IMediaSessionCallback cb, String tag)
+                throws RemoteException {
+            final int pid = Binder.getCallingPid();
+            final int uid = Binder.getCallingUid();
+            final long token = Binder.clearCallingIdentity();
+            try {
+                enforcePackageName(packageName, uid);
+                if (cb == null) {
+                    throw new IllegalArgumentException("Controller callback cannot be null");
+                }
+                return createSessionInternal(pid, packageName, cb, tag).getSessionBinder();
+            } finally {
+                Binder.restoreCallingIdentity(token);
+            }
+        }
+    }
+
+}
index 85e5e23..49ed63d 100644 (file)
@@ -62,6 +62,7 @@ import com.android.server.input.InputManagerService;
 import com.android.server.lights.LightsManager;
 import com.android.server.lights.LightsService;
 import com.android.server.media.MediaRouterService;
+import com.android.server.media.MediaSessionService;
 import com.android.server.net.NetworkPolicyManagerService;
 import com.android.server.net.NetworkStatsService;
 import com.android.server.notification.NotificationManagerService;
@@ -889,6 +890,13 @@ public final class SystemServer {
                 reportWtf("starting Print Service", e);
             }
 
+            try {
+                Slog.i(TAG, "MediaSessionService");
+                mSystemServiceManager.startService(MediaSessionService.class);
+            } catch (Throwable e) {
+                reportWtf("starting MediaSessionService", e);
+            }
+
             if (!disableNonCoreServices) {
                 try {
                     Slog.i(TAG, "Media Router Service");