diff options
author | RoboErik <epastern@google.com> | 2014-02-13 14:19:04 -0800 |
---|---|---|
committer | RoboErik <epastern@google.com> | 2014-02-19 13:41:37 -0800 |
commit | 01fe661ae5da3739215d93922412df4b24c859a2 (patch) | |
tree | fbc2bb43edec44c553256de377312741f87f434f | |
parent | d63b4314b85e982a1d70d4064af59851f476dd36 (diff) | |
download | frameworks_base-01fe661ae5da3739215d93922412df4b24c859a2.zip frameworks_base-01fe661ae5da3739215d93922412df4b24c859a2.tar.gz frameworks_base-01fe661ae5da3739215d93922412df4b24c859a2.tar.bz2 |
Initial round of MediaSession APIs
This is far from complete but puts the basic components in place
for an app to interact with media sessions.
Change-Id: Icfe313f90ad76ae56badbe42b0e43fc5f68db36f
20 files changed, 1503 insertions, 108 deletions
@@ -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 \ diff --git a/api/current.txt b/api/current.txt index 3ea1aad..b761813 100644 --- a/api/current.txt +++ b/api/current.txt @@ -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(); diff --git a/core/java/android/app/ContextImpl.java b/core/java/android/app/ContextImpl.java index 8d127c6..873db8e 100644 --- a/core/java/android/app/ContextImpl.java +++ b/core/java/android/app/ContextImpl.java @@ -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) { diff --git a/core/java/android/content/Context.java b/core/java/android/content/Context.java index 9f90de0..d05d1a1 100644 --- a/core/java/android/content/Context.java +++ b/core/java/android/content/Context.java @@ -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. * diff --git a/core/java/android/view/KeyEvent.java b/core/java/android/view/KeyEvent.java index 30b1e52..7e745d8 100644 --- a/core/java/android/view/KeyEvent.java +++ b/core/java/android/view/KeyEvent.java @@ -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 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; 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 index 0000000..0d3fa84 --- /dev/null +++ b/services/core/java/com/android/server/media/MediaSessionRecord.java @@ -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 index 0000000..9c96c35 --- /dev/null +++ b/services/core/java/com/android/server/media/MediaSessionService.java @@ -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); + } + } + } + +} diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java index 85e5e23..49ed63d 100644 --- a/services/java/com/android/server/SystemServer.java +++ b/services/java/com/android/server/SystemServer.java @@ -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"); |