Merge "Initial round of MediaSession APIs"

This commit is contained in:
RoboErik
2014-02-20 00:25:10 +00:00
committed by Android (Google) Code Review
20 changed files with 1503 additions and 108 deletions

View 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 \

View File

@@ -6317,6 +6317,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
@@ -13156,6 +13157,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);
@@ -13722,6 +13740,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();

View 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) {

View 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 {}
@@ -2349,6 +2350,15 @@ public abstract class Context {
*/
public static final String MEDIA_ROUTER_SERVICE = "media_router";
/**
* 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

View 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();

View File

@@ -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();
}

View File

@@ -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);
}

View File

@@ -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();
}

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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);
}
}
}
}

View 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;

View File

@@ -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();
}
}
}

View File

@@ -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>();
}
}

View File

@@ -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;

View File

@@ -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];
}
};
}

View 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;

View File

@@ -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;
}
}
}

View File

@@ -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);
}
}
}
}

View 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;
@@ -881,6 +882,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");