summaryrefslogtreecommitdiffstats
path: root/media
diff options
context:
space:
mode:
Diffstat (limited to 'media')
-rw-r--r--media/java/android/media/AsyncPlayer.java42
-rw-r--r--media/java/android/media/AudioAttributes.java8
-rw-r--r--media/java/android/media/AudioManager.java228
-rw-r--r--media/java/android/media/AudioManagerInternal.java3
-rw-r--r--media/java/android/media/AudioService.java425
-rw-r--r--media/java/android/media/AudioTrack.java72
-rw-r--r--media/java/android/media/IAudioService.aidl6
-rw-r--r--media/java/android/media/MediaCodecInfo.java9
-rw-r--r--media/java/android/media/browse/MediaBrowser.java92
-rw-r--r--media/java/android/media/routing/IMediaRouteClientCallback.aidl41
-rw-r--r--media/java/android/media/routing/IMediaRouteService.aidl46
-rw-r--r--media/java/android/media/routing/IMediaRouter.aidl22
-rw-r--r--media/java/android/media/routing/IMediaRouterDelegate.aidl22
-rw-r--r--media/java/android/media/routing/IMediaRouterRoutingCallback.aidl22
-rw-r--r--media/java/android/media/routing/IMediaRouterStateCallback.aidl22
-rw-r--r--media/java/android/media/routing/MediaRouteSelector.aidl18
-rw-r--r--media/java/android/media/routing/MediaRouteSelector.java357
-rw-r--r--media/java/android/media/routing/MediaRouteService.java1023
-rw-r--r--media/java/android/media/routing/MediaRouter.java1886
-rw-r--r--media/java/android/media/routing/ParcelableConnectionInfo.aidl18
-rw-r--r--media/java/android/media/routing/ParcelableConnectionInfo.java71
-rw-r--r--media/java/android/media/routing/ParcelableDestinationInfo.aidl18
-rw-r--r--media/java/android/media/routing/ParcelableDestinationInfo.java65
-rw-r--r--media/java/android/media/routing/ParcelableRouteInfo.aidl18
-rw-r--r--media/java/android/media/routing/ParcelableRouteInfo.java64
-rw-r--r--media/java/android/media/session/ISession.aidl2
-rw-r--r--media/java/android/media/session/ISessionController.aidl4
-rw-r--r--media/java/android/media/session/MediaController.java12
-rw-r--r--media/java/android/media/session/MediaSession.java18
-rw-r--r--media/java/android/media/session/MediaSessionLegacyHelper.java8
-rw-r--r--media/java/android/media/session/MediaSessionManager.java8
-rw-r--r--media/java/android/media/tv/TvContract.java5
-rw-r--r--media/java/android/media/tv/TvInputService.java17
-rw-r--r--media/java/android/service/media/IMediaBrowserService.aidl2
-rw-r--r--media/java/android/service/media/MediaBrowserService.java110
-rw-r--r--media/jni/android_media_MediaMetadataRetriever.cpp3
-rw-r--r--media/tests/MediaDump/src/com/android/mediadump/VideoDumpActivity.java2
37 files changed, 4284 insertions, 505 deletions
diff --git a/media/java/android/media/AsyncPlayer.java b/media/java/android/media/AsyncPlayer.java
index 14b199e..dd5f6ba 100644
--- a/media/java/android/media/AsyncPlayer.java
+++ b/media/java/android/media/AsyncPlayer.java
@@ -16,6 +16,7 @@
package android.media;
+import android.annotation.NonNull;
import android.content.Context;
import android.net.Uri;
import android.os.PowerManager;
@@ -38,11 +39,11 @@ public class AsyncPlayer {
Context context;
Uri uri;
boolean looping;
- int stream;
+ AudioAttributes attributes;
long requestTime;
public String toString() {
- return "{ code=" + code + " looping=" + looping + " stream=" + stream
+ return "{ code=" + code + " looping=" + looping + " attr=" + attributes
+ " uri=" + uri + " }";
}
}
@@ -56,7 +57,7 @@ public class AsyncPlayer {
try {
if (mDebug) Log.d(mTag, "Starting playback");
MediaPlayer player = new MediaPlayer();
- player.setAudioStreamType(cmd.stream);
+ player.setAudioAttributes(cmd.attributes);
player.setDataSource(cmd.context, cmd.uri);
player.setLooping(cmd.looping);
player.prepare();
@@ -159,21 +160,52 @@ public class AsyncPlayer {
* (see {@link MediaPlayer#setLooping(boolean)})
* @param stream the AudioStream to use.
* (see {@link MediaPlayer#setAudioStreamType(int)})
+ * @deprecated use {@link #play(Context, Uri, boolean, AudioAttributes)} instead
*/
public void play(Context context, Uri uri, boolean looping, int stream) {
+ if (context == null || uri == null) {
+ return;
+ }
+ try {
+ play(context, uri, looping,
+ new AudioAttributes.Builder().setInternalLegacyStreamType(stream).build());
+ } catch (IllegalArgumentException e) {
+ Log.e(mTag, "Call to deprecated AsyncPlayer.play() method caused:", e);
+ }
+ }
+
+ /**
+ * Start playing the sound. It will actually start playing at some
+ * point in the future. There are no guarantees about latency here.
+ * Calling this before another audio file is done playing will stop
+ * that one and start the new one.
+ *
+ * @param context the non-null application's context.
+ * @param uri the non-null URI to play. (see {@link MediaPlayer#setDataSource(Context, Uri)})
+ * @param looping whether the audio should loop forever.
+ * (see {@link MediaPlayer#setLooping(boolean)})
+ * @param attributes the non-null {@link AudioAttributes} to use.
+ * (see {@link MediaPlayer#setAudioAttributes(AudioAttributes)})
+ * @throws IllegalArgumentException
+ */
+ public void play(@NonNull Context context, @NonNull Uri uri, boolean looping,
+ @NonNull AudioAttributes attributes) throws IllegalArgumentException {
+ if (context == null || uri == null || attributes == null) {
+ throw new IllegalArgumentException("Illegal null AsyncPlayer.play() argument");
+ }
Command cmd = new Command();
cmd.requestTime = SystemClock.uptimeMillis();
cmd.code = PLAY;
cmd.context = context;
cmd.uri = uri;
cmd.looping = looping;
- cmd.stream = stream;
+ cmd.attributes = attributes;
synchronized (mCmdQueue) {
enqueueLocked(cmd);
mState = PLAY;
}
}
-
+
/**
* Stop a previously played sound. It can't be played again or unpaused
* at this point. Calling this multiple times has no ill effects.
diff --git a/media/java/android/media/AudioAttributes.java b/media/java/android/media/AudioAttributes.java
index 489f552..ca242e4 100644
--- a/media/java/android/media/AudioAttributes.java
+++ b/media/java/android/media/AudioAttributes.java
@@ -709,7 +709,13 @@ public final class AudioAttributes implements Parcelable {
}
}
- /** @hide */
+ /**
+ * @hide
+ * Only use to get which stream type should be used for volume control, NOT for audio playback
+ * (all audio playback APIs are supposed to take AudioAttributes as input parameters)
+ * @param aa non-null AudioAttributes.
+ * @return a valid stream type for volume control that matches the attributes.
+ */
public static int toLegacyStreamType(AudioAttributes aa) {
// flags to stream type mapping
if ((aa.getFlags() & FLAG_AUDIBILITY_ENFORCED) == FLAG_AUDIBILITY_ENFORCED) {
diff --git a/media/java/android/media/AudioManager.java b/media/java/android/media/AudioManager.java
index 240faef..986ae46 100644
--- a/media/java/android/media/AudioManager.java
+++ b/media/java/android/media/AudioManager.java
@@ -295,68 +295,6 @@ public class AudioManager {
*/
public static final String EXTRA_ENCODINGS = "android.media.extra.ENCODINGS";
- /**
- * Broadcast Action: An analog audio speaker/headset plugged in or unplugged.
- *
- * <p>The intent will have the following extra values:
- * <ul>
- * <li><em>state</em> - 0 for unplugged, 1 for plugged. </li>
- * <li><em>name</em> - Headset type, human readable string </li>
- * </ul>
- * </ul>
- * @hide
- */
- @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
- public static final String ACTION_ANALOG_AUDIO_DOCK_PLUG =
- "android.media.action.ANALOG_AUDIO_DOCK_PLUG";
-
- /**
- * Broadcast Action: A digital audio speaker/headset plugged in or unplugged.
- *
- * <p>The intent will have the following extra values:
- * <ul>
- * <li><em>state</em> - 0 for unplugged, 1 for plugged. </li>
- * <li><em>name</em> - Headset type, human readable string </li>
- * </ul>
- * </ul>
- * @hide
- */
- @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
- public static final String ACTION_DIGITAL_AUDIO_DOCK_PLUG =
- "android.media.action.DIGITAL_AUDIO_DOCK_PLUG";
-
- /**
- * Broadcast Action: A USB audio accessory was plugged in or unplugged.
- *
- * <p>The intent will have the following extra values:
- * <ul>
- * <li><em>state</em> - 0 for unplugged, 1 for plugged. </li>
- * <li><em>card</em> - ALSA card number (integer) </li>
- * <li><em>device</em> - ALSA device number (integer) </li>
- * </ul>
- * </ul>
- * @hide
- */
- @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
- public static final String ACTION_USB_AUDIO_ACCESSORY_PLUG =
- "android.media.action.USB_AUDIO_ACCESSORY_PLUG";
-
- /**
- * Broadcast Action: A USB audio device was plugged in or unplugged.
- *
- * <p>The intent will have the following extra values:
- * <ul>
- * <li><em>state</em> - 0 for unplugged, 1 for plugged. </li>
- * <li><em>card</em> - ALSA card number (integer) </li>
- * <li><em>device</em> - ALSA device number (integer) </li>
- * </ul>
- * </ul>
- * @hide
- */
- @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
- public static final String ACTION_USB_AUDIO_DEVICE_PLUG =
- "android.media.action.USB_AUDIO_DEVICE_PLUG";
-
/** The audio stream for phone calls */
public static final int STREAM_VOICE_CALL = AudioSystem.STREAM_VOICE_CALL;
/** The audio stream for system sounds */
@@ -408,6 +346,31 @@ public class AudioManager {
*/
public static final int ADJUST_SAME = 0;
+ /**
+ * Mute the volume. Has no effect if the stream is already muted.
+ *
+ * @see #adjustVolume(int, int)
+ * @see #adjustStreamVolume(int, int, int)
+ */
+ public static final int ADJUST_MUTE = -100;
+
+ /**
+ * Unmute the volume. Has no effect if the stream is not muted.
+ *
+ * @see #adjustVolume(int, int)
+ * @see #adjustStreamVolume(int, int, int)
+ */
+ public static final int ADJUST_UNMUTE = 100;
+
+ /**
+ * Toggle the mute state. If muted the stream will be unmuted. If not muted
+ * the stream will be muted.
+ *
+ * @see #adjustVolume(int, int)
+ * @see #adjustStreamVolume(int, int, int)
+ */
+ public static final int ADJUST_TOGGLE_MUTE = 101;
+
// Flags should be powers of 2!
/**
@@ -839,13 +802,17 @@ public class AudioManager {
* screen is showing. Another example, if music is playing in the background
* and a call is not active, the music stream will be adjusted.
* <p>
- * This method should only be used by applications that replace the platform-wide
- * management of audio settings or the main telephony application.
- * <p>This method has no effect if the device implements a fixed volume policy
+ * This method should only be used by applications that replace the
+ * platform-wide management of audio settings or the main telephony
+ * application.
+ * <p>
+ * This method has no effect if the device implements a fixed volume policy
* as indicated by {@link #isVolumeFixed()}.
+ *
* @param direction The direction to adjust the volume. One of
- * {@link #ADJUST_LOWER}, {@link #ADJUST_RAISE}, or
- * {@link #ADJUST_SAME}.
+ * {@link #ADJUST_LOWER}, {@link #ADJUST_RAISE},
+ * {@link #ADJUST_SAME}, {@link #ADJUST_MUTE},
+ * {@link #ADJUST_UNMUTE}, or {@link #ADJUST_TOGGLE_MUTE}.
* @param flags One or more flags.
* @see #adjustSuggestedStreamVolume(int, int, int)
* @see #adjustStreamVolume(int, int, int)
@@ -870,16 +837,20 @@ public class AudioManager {
* Adjusts the volume of the most relevant stream, or the given fallback
* stream.
* <p>
- * This method should only be used by applications that replace the platform-wide
- * management of audio settings or the main telephony application.
- *
- * <p>This method has no effect if the device implements a fixed volume policy
+ * This method should only be used by applications that replace the
+ * platform-wide management of audio settings or the main telephony
+ * application.
+ * <p>
+ * This method has no effect if the device implements a fixed volume policy
* as indicated by {@link #isVolumeFixed()}.
+ *
* @param direction The direction to adjust the volume. One of
- * {@link #ADJUST_LOWER}, {@link #ADJUST_RAISE}, or
- * {@link #ADJUST_SAME}.
+ * {@link #ADJUST_LOWER}, {@link #ADJUST_RAISE},
+ * {@link #ADJUST_SAME}, {@link #ADJUST_MUTE},
+ * {@link #ADJUST_UNMUTE}, or {@link #ADJUST_TOGGLE_MUTE}.
* @param suggestedStreamType The stream type that will be used if there
- * isn't a relevant stream. {@link #USE_DEFAULT_STREAM_TYPE} is valid here.
+ * isn't a relevant stream. {@link #USE_DEFAULT_STREAM_TYPE} is
+ * valid here.
* @param flags One or more flags.
* @see #adjustVolume(int, int)
* @see #adjustStreamVolume(int, int, int)
@@ -1150,72 +1121,72 @@ public class AudioManager {
}
/**
- * Solo or unsolo a particular stream. All other streams are muted.
- * <p>
- * The solo command is protected against client process death: if a process
- * with an active solo request on a stream dies, all streams that were muted
- * because of this request will be unmuted automatically.
+ * Solo or unsolo a particular stream.
* <p>
- * The solo requests for a given stream are cumulative: the AudioManager
- * can receive several solo requests from one or more clients and the stream
- * will be unsoloed only when the same number of unsolo requests are received.
- * <p>
- * For a better user experience, applications MUST unsolo a soloed stream
- * in onPause() and solo is again in onResume() if appropriate.
- * <p>This method has no effect if the device implements a fixed volume policy
- * as indicated by {@link #isVolumeFixed()}.
+ * Do not use. This method has been deprecated and is now a no-op.
+ * {@link #requestAudioFocus} should be used for exclusive audio playback.
*
* @param streamType The stream to be soloed/unsoloed.
- * @param state The required solo state: true for solo ON, false for solo OFF
- *
+ * @param state The required solo state: true for solo ON, false for solo
+ * OFF
* @see #isVolumeFixed()
+ * @deprecated Do not use. If you need exclusive audio playback use
+ * {@link #requestAudioFocus}.
*/
+ @Deprecated
public void setStreamSolo(int streamType, boolean state) {
- IAudioService service = getService();
- try {
- service.setStreamSolo(streamType, state, mICallBack);
- } catch (RemoteException e) {
- Log.e(TAG, "Dead object in setStreamSolo", e);
- }
+ Log.w(TAG, "setStreamSolo has been deprecated. Do not use.");
}
/**
* Mute or unmute an audio stream.
* <p>
- * The mute command is protected against client process death: if a process
- * with an active mute request on a stream dies, this stream will be unmuted
- * automatically.
+ * This method should only be used by applications that replace the
+ * platform-wide management of audio settings or the main telephony
+ * application.
* <p>
- * The mute requests for a given stream are cumulative: the AudioManager
- * can receive several mute requests from one or more clients and the stream
- * will be unmuted only when the same number of unmute requests are received.
- * <p>
- * For a better user experience, applications MUST unmute a muted stream
- * in onPause() and mute is again in onResume() if appropriate.
- * <p>
- * This method should only be used by applications that replace the platform-wide
- * management of audio settings or the main telephony application.
- * <p>This method has no effect if the device implements a fixed volume policy
+ * This method has no effect if the device implements a fixed volume policy
* as indicated by {@link #isVolumeFixed()}.
+ * <p>
+ * This method was deprecated in API level 22. Prior to API level 22 this
+ * method had significantly different behavior and should be used carefully.
+ * The following applies only to pre-22 platforms:
+ * <ul>
+ * <li>The mute command is protected against client process death: if a
+ * process with an active mute request on a stream dies, this stream will be
+ * unmuted automatically.</li>
+ * <li>The mute requests for a given stream are cumulative: the AudioManager
+ * can receive several mute requests from one or more clients and the stream
+ * will be unmuted only when the same number of unmute requests are
+ * received.</li>
+ * <li>For a better user experience, applications MUST unmute a muted stream
+ * in onPause() and mute is again in onResume() if appropriate.</li>
+ * </ul>
*
* @param streamType The stream to be muted/unmuted.
- * @param state The required mute state: true for mute ON, false for mute OFF
- *
+ * @param state The required mute state: true for mute ON, false for mute
+ * OFF
* @see #isVolumeFixed()
+ * @deprecated Use {@link #adjustStreamVolume(int, int, int)} with
+ * {@link #ADJUST_MUTE} or {@link #ADJUST_UNMUTE} instead.
*/
+ @Deprecated
public void setStreamMute(int streamType, boolean state) {
- IAudioService service = getService();
- try {
- service.setStreamMute(streamType, state, mICallBack);
- } catch (RemoteException e) {
- Log.e(TAG, "Dead object in setStreamMute", e);
+ Log.w(TAG, "setStreamMute is deprecated. adjustStreamVolume should be used instead.");
+ int direction = state ? ADJUST_MUTE : ADJUST_UNMUTE;
+ if (streamType == AudioManager.USE_DEFAULT_STREAM_TYPE) {
+ adjustSuggestedStreamVolume(direction, streamType, 0);
+ } else {
+ adjustStreamVolume(streamType, direction, 0);
}
}
/**
- * get stream mute state.
+ * Returns the current mute state for a particular stream.
*
- * @hide
+ * @param streamType The stream to get mute state for.
+ * @return The mute state for the given stream.
+ * @see #adjustStreamVolume(int, int, int)
*/
public boolean isStreamMute(int streamType) {
IAudioService service = getService();
@@ -1228,29 +1199,6 @@ public class AudioManager {
}
/**
- * set master mute state.
- *
- * @hide
- */
- public void setMasterMute(boolean state) {
- setMasterMute(state, FLAG_SHOW_UI);
- }
-
- /**
- * set master mute state with optional flags.
- *
- * @hide
- */
- public void setMasterMute(boolean state, int flags) {
- IAudioService service = getService();
- try {
- service.setMasterMute(state, flags, mContext.getOpPackageName(), mICallBack);
- } catch (RemoteException e) {
- Log.e(TAG, "Dead object in setMasterMute", e);
- }
- }
-
- /**
* get master mute state.
*
* @hide
diff --git a/media/java/android/media/AudioManagerInternal.java b/media/java/android/media/AudioManagerInternal.java
index 616bdd1..873c142 100644
--- a/media/java/android/media/AudioManagerInternal.java
+++ b/media/java/android/media/AudioManagerInternal.java
@@ -41,9 +41,6 @@ public abstract class AudioManagerInternal {
public abstract void adjustMasterVolumeForUid(int steps, int flags, String callingPackage,
int uid);
- public abstract void setMasterMuteForUid(boolean state, int flags, String callingPackage,
- IBinder cb, int uid);
-
public abstract void setRingerModeDelegate(RingerModeDelegate delegate);
public abstract int getRingerModeInternal();
diff --git a/media/java/android/media/AudioService.java b/media/java/android/media/AudioService.java
index 9b76f89..98a43a8 100644
--- a/media/java/android/media/AudioService.java
+++ b/media/java/android/media/AudioService.java
@@ -153,11 +153,11 @@ public class AudioService extends IAudioService.Stub {
private final AppOpsManager mAppOps;
// the platform has no specific capabilities
- private static final int PLATFORM_DEFAULT = 0;
+ public static final int PLATFORM_DEFAULT = 0;
// the platform is voice call capable (a phone)
- private static final int PLATFORM_VOICE = 1;
+ public static final int PLATFORM_VOICE = 1;
// the platform is a television or a set-top box
- private static final int PLATFORM_TELEVISION = 2;
+ public static final int PLATFORM_TELEVISION = 2;
// the platform type affects volume and silent mode behavior
private final int mPlatformType;
@@ -546,15 +546,7 @@ public class AudioService extends IAudioService.Stub {
mContentResolver = context.getContentResolver();
mAppOps = (AppOpsManager)context.getSystemService(Context.APP_OPS_SERVICE);
- if (mContext.getResources().getBoolean(
- com.android.internal.R.bool.config_voice_capable)) {
- mPlatformType = PLATFORM_VOICE;
- } else if (context.getPackageManager().hasSystemFeature(
- PackageManager.FEATURE_LEANBACK)) {
- mPlatformType = PLATFORM_TELEVISION;
- } else {
- mPlatformType = PLATFORM_DEFAULT;
- }
+ mPlatformType = getPlatformType(context);
PowerManager pm = (PowerManager)context.getSystemService(Context.POWER_SERVICE);
mAudioEventWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "handleAudioEvent");
@@ -634,8 +626,6 @@ public class AudioService extends IAudioService.Stub {
new IntentFilter(BluetoothHeadset.ACTION_AUDIO_STATE_CHANGED);
intentFilter.addAction(BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED);
intentFilter.addAction(Intent.ACTION_DOCK_EVENT);
- intentFilter.addAction(AudioManager.ACTION_USB_AUDIO_ACCESSORY_PLUG);
- intentFilter.addAction(AudioManager.ACTION_USB_AUDIO_DEVICE_PLUG);
intentFilter.addAction(Intent.ACTION_SCREEN_ON);
intentFilter.addAction(Intent.ACTION_SCREEN_OFF);
intentFilter.addAction(Intent.ACTION_USER_SWITCHED);
@@ -669,6 +659,26 @@ public class AudioService extends IAudioService.Stub {
LocalServices.addService(AudioManagerInternal.class, new AudioServiceInternal());
}
+ /**
+ * Return the platform type that this is running on. One of:
+ * <ul>
+ * <li>{@link #PLATFORM_VOICE}</li>
+ * <li>{@link #PLATFORM_TELEVISION}</li>
+ * <li>{@link #PLATFORM_DEFAULT}</li>
+ * </ul>
+ */
+ public static int getPlatformType(Context context) {
+ if (context.getResources().getBoolean(
+ com.android.internal.R.bool.config_voice_capable)) {
+ return PLATFORM_VOICE;
+ } else if (context.getPackageManager().hasSystemFeature(
+ PackageManager.FEATURE_LEANBACK)) {
+ return PLATFORM_TELEVISION;
+ } else {
+ return PLATFORM_DEFAULT;
+ }
+ }
+
public void systemReady() {
sendMsg(mAudioHandler, MSG_SYSTEM_READY, SENDMSG_QUEUE,
0, 0, null, 0);
@@ -750,7 +760,7 @@ public class AudioService extends IAudioService.Stub {
setAllIndexes(mStreamStates[mStreamVolumeAlias[streamType]]);
}
// apply stream volume
- if (!mStreamStates[streamType].isMuted_syncVSS()) {
+ if (!mStreamStates[streamType].mIsMuted) {
mStreamStates[streamType].applyAllVolumes();
}
}
@@ -847,12 +857,6 @@ public class AudioService extends IAudioService.Stub {
mDockAudioMediaEnabled = Settings.Global.getInt(
cr, Settings.Global.DOCK_AUDIO_MEDIA_ENABLED, 0) == 1;
- if (mDockAudioMediaEnabled) {
- mBecomingNoisyIntentDevices |= AudioSystem.DEVICE_OUT_ANLG_DOCK_HEADSET;
- } else {
- mBecomingNoisyIntentDevices &= ~AudioSystem.DEVICE_OUT_ANLG_DOCK_HEADSET;
- }
-
sendMsg(mAudioHandler,
MSG_SET_FORCE_USE,
SENDMSG_QUEUE,
@@ -978,6 +982,7 @@ public class AudioService extends IAudioService.Stub {
if (DEBUG_VOL) Log.d(TAG, "adjustSuggestedStreamVolume() stream="+suggestedStreamType
+ ", flags=" + flags);
int streamType;
+ boolean isMute = isMuteAdjust(direction);
if (mVolumeControlStream != -1) {
streamType = mVolumeControlStream;
} else {
@@ -992,7 +997,8 @@ public class AudioService extends IAudioService.Stub {
}
// For notifications/ring, show the ui before making any adjustments
- if (mVolumeController.suppressAdjustment(resolvedStream, flags)) {
+ // Don't suppress mute/unmute requests
+ if (mVolumeController.suppressAdjustment(resolvedStream, flags, isMute)) {
direction = 0;
flags &= ~AudioManager.FLAG_PLAY_SOUND;
flags &= ~AudioManager.FLAG_VIBRATE;
@@ -1019,10 +1025,17 @@ public class AudioService extends IAudioService.Stub {
ensureValidDirection(direction);
ensureValidStreamType(streamType);
+ boolean isMuteAdjust = isMuteAdjust(direction);
+
// use stream type alias here so that streams with same alias have the same behavior,
// including with regard to silent mode control (e.g the use of STREAM_RING below and in
// checkForRingerModeChange() in place of STREAM_RING or STREAM_NOTIFICATION)
int streamTypeAlias = mStreamVolumeAlias[streamType];
+
+ if (isMuteAdjust && !isStreamAffectedByMute(streamTypeAlias)) {
+ return;
+ }
+
VolumeStreamState streamState = mStreamStates[streamTypeAlias];
final int device = getDeviceForStream(streamTypeAlias);
@@ -1108,13 +1121,37 @@ public class AudioService extends IAudioService.Stub {
}
}
- if ((direction == AudioManager.ADJUST_RAISE) &&
+ if (isMuteAdjust) {
+ boolean state;
+ if (direction == AudioManager.ADJUST_TOGGLE_MUTE) {
+ state = !streamState.mIsMuted;
+ } else {
+ state = direction == AudioManager.ADJUST_MUTE;
+ }
+ if (streamTypeAlias == AudioSystem.STREAM_MUSIC) {
+ setSystemAudioMute(state);
+ }
+ for (int stream = 0; stream < mStreamStates.length; stream++) {
+ if (streamTypeAlias == mStreamVolumeAlias[stream]) {
+ mStreamStates[stream].mute(state);
+
+ Intent intent = new Intent(AudioManager.STREAM_MUTE_CHANGED_ACTION);
+ intent.putExtra(AudioManager.EXTRA_VOLUME_STREAM_TYPE, stream);
+ intent.putExtra(AudioManager.EXTRA_STREAM_VOLUME_MUTED, state);
+ sendBroadcastToAll(intent);
+ }
+ }
+ } else if ((direction == AudioManager.ADJUST_RAISE) &&
!checkSafeMediaVolume(streamTypeAlias, aliasIndex + step, device)) {
- Log.e(TAG, "adjustStreamVolume() safe volume index = "+oldIndex);
+ Log.e(TAG, "adjustStreamVolume() safe volume index = " + oldIndex);
mVolumeController.postDisplaySafeVolumeWarning(flags);
- } else if (streamState.adjustIndex(direction * step, device)) {
- // Post message to set system volume (it in turn will post a message
- // to persist). Do not change volume if stream is muted.
+ } else if (streamState.adjustIndex(direction * step, device) || streamState.mIsMuted) {
+ // Post message to set system volume (it in turn will post a
+ // message to persist).
+ if (streamState.mIsMuted) {
+ // Unmute the stream if it was previously muted
+ streamState.mute(false);
+ }
sendMsg(mAudioHandler,
MSG_SET_DEVICE_VOLUME,
SENDMSG_QUEUE,
@@ -1124,7 +1161,7 @@ public class AudioService extends IAudioService.Stub {
0);
}
- // Check if volume update should be send to Hdmi system audio.
+ // Check if volume update should be sent to Hdmi system audio.
int newIndex = mStreamStates[streamType].getIndex(device);
if (streamTypeAlias == AudioSystem.STREAM_MUSIC) {
setSystemAudioVolume(oldIndex, newIndex, getStreamMaxVolume(streamType), flags);
@@ -1137,7 +1174,7 @@ public class AudioService extends IAudioService.Stub {
oldIndex != newIndex) {
synchronized (mHdmiPlaybackClient) {
int keyCode = (direction == -1) ? KeyEvent.KEYCODE_VOLUME_DOWN :
- KeyEvent.KEYCODE_VOLUME_UP;
+ KeyEvent.KEYCODE_VOLUME_UP;
mHdmiPlaybackClient.sendKeyEvent(keyCode, true);
mHdmiPlaybackClient.sendKeyEvent(keyCode, false);
}
@@ -1180,6 +1217,10 @@ public class AudioService extends IAudioService.Stub {
if (mUseFixedVolume) {
return;
}
+ if (isMuteAdjust(steps)) {
+ setMasterMuteInternal(steps, flags, callingPackage, uid);
+ return;
+ }
ensureValidSteps(steps);
int volume = Math.round(AudioSystem.getMasterVolume() * MAX_MASTER_VOLUME);
int delta = 0;
@@ -1508,46 +1549,6 @@ public class AudioService extends IAudioService.Stub {
}
}
- /** @see AudioManager#setStreamSolo(int, boolean) */
- public void setStreamSolo(int streamType, boolean state, IBinder cb) {
- if (mUseFixedVolume) {
- return;
- }
- int streamAlias = mStreamVolumeAlias[streamType];
- for (int stream = 0; stream < mStreamStates.length; stream++) {
- if (!isStreamAffectedByMute(streamAlias) || streamAlias == mStreamVolumeAlias[stream]) {
- continue;
- }
- mStreamStates[stream].mute(cb, state);
- }
- }
-
- /** @see AudioManager#setStreamMute(int, boolean) */
- public void setStreamMute(int streamType, boolean state, IBinder cb) {
- if (mUseFixedVolume) {
- return;
- }
- if (streamType == AudioManager.USE_DEFAULT_STREAM_TYPE) {
- streamType = getActiveStreamType(streamType);
- }
- int streamAlias = mStreamVolumeAlias[streamType];
- if (isStreamAffectedByMute(streamAlias)) {
- if (streamAlias == AudioSystem.STREAM_MUSIC) {
- setSystemAudioMute(state);
- }
- for (int stream = 0; stream < mStreamStates.length; stream++) {
- if (streamAlias == mStreamVolumeAlias[stream]) {
- mStreamStates[stream].mute(cb, state);
-
- Intent intent = new Intent(AudioManager.STREAM_MUTE_CHANGED_ACTION);
- intent.putExtra(AudioManager.EXTRA_VOLUME_STREAM_TYPE, stream);
- intent.putExtra(AudioManager.EXTRA_STREAM_VOLUME_MUTED, state);
- sendBroadcastToAll(intent);
- }
- }
- }
- }
-
private void setSystemAudioMute(boolean state) {
if (mHdmiManager == null || mHdmiTvClient == null) return;
synchronized (mHdmiManager) {
@@ -1569,7 +1570,7 @@ public class AudioService extends IAudioService.Stub {
streamType = getActiveStreamType(streamType);
}
synchronized (VolumeStreamState.class) {
- return mStreamStates[streamType].isMuted_syncVSS();
+ return mStreamStates[streamType].mIsMuted;
}
}
@@ -1673,20 +1674,17 @@ public class AudioService extends IAudioService.Stub {
}
}
- /** @see AudioManager#setMasterMute(boolean, int) */
- public void setMasterMute(boolean state, int flags, String callingPackage, IBinder cb) {
- setMasterMuteInternal(state, flags, callingPackage, cb, Binder.getCallingUid());
- }
-
- private void setMasterMuteInternal(boolean state, int flags, String callingPackage, IBinder cb,
- int uid) {
- if (mUseFixedVolume) {
- return;
- }
+ private void setMasterMuteInternal(int adjust, int flags, String callingPackage, int uid) {
if (mAppOps.noteOp(AppOpsManager.OP_AUDIO_MASTER_VOLUME, uid, callingPackage)
!= AppOpsManager.MODE_ALLOWED) {
return;
}
+ boolean state;
+ if (adjust == AudioManager.ADJUST_TOGGLE_MUTE) {
+ state = !AudioSystem.getMasterMute();
+ } else {
+ state = adjust == AudioManager.ADJUST_MUTE;
+ }
if (state != AudioSystem.getMasterMute()) {
setSystemAudioMute(state);
AudioSystem.setMasterMute(state);
@@ -1722,7 +1720,7 @@ public class AudioService extends IAudioService.Stub {
int index = mStreamStates[streamType].getIndex(device);
// by convention getStreamVolume() returns 0 when a stream is muted.
- if (mStreamStates[streamType].isMuted_syncVSS()) {
+ if (mStreamStates[streamType].mIsMuted) {
index = 0;
}
if (index != 0 && (mStreamVolumeAlias[streamType] == AudioSystem.STREAM_MUSIC) &&
@@ -1942,11 +1940,11 @@ public class AudioService extends IAudioService.Stub {
}
}
}
- mStreamStates[streamType].mute(null, false);
+ mStreamStates[streamType].mute(false);
mRingerModeMutedStreams &= ~(1 << streamType);
} else {
// mute
- mStreamStates[streamType].mute(null, true);
+ mStreamStates[streamType].mute(true);
mRingerModeMutedStreams |= (1 << streamType);
}
}
@@ -2434,13 +2432,9 @@ public class AudioService extends IAudioService.Stub {
streamState.readSettings();
synchronized (VolumeStreamState.class) {
// unmute stream that was muted but is not affect by mute anymore
- if (streamState.isMuted_syncVSS() && ((!isStreamAffectedByMute(streamType) &&
+ if (streamState.mIsMuted && ((!isStreamAffectedByMute(streamType) &&
!isStreamMutedByRingerMode(streamType)) || mUseFixedVolume)) {
- int size = streamState.mDeathHandlers.size();
- for (int i = 0; i < size; i++) {
- streamState.mDeathHandlers.get(i).mMuteCount = 1;
- streamState.mDeathHandlers.get(i).mute_syncVSS(false);
- }
+ streamState.mIsMuted = false;
}
}
}
@@ -3229,8 +3223,16 @@ public class AudioService extends IAudioService.Stub {
}
private void ensureValidDirection(int direction) {
- if (direction < AudioManager.ADJUST_LOWER || direction > AudioManager.ADJUST_RAISE) {
- throw new IllegalArgumentException("Bad direction " + direction);
+ switch (direction) {
+ case AudioManager.ADJUST_LOWER:
+ case AudioManager.ADJUST_RAISE:
+ case AudioManager.ADJUST_SAME:
+ case AudioManager.ADJUST_MUTE:
+ case AudioManager.ADJUST_UNMUTE:
+ case AudioManager.ADJUST_TOGGLE_MUTE:
+ break;
+ default:
+ throw new IllegalArgumentException("Bad direction " + direction);
}
}
@@ -3246,6 +3248,11 @@ public class AudioService extends IAudioService.Stub {
}
}
+ private boolean isMuteAdjust(int adjust) {
+ return adjust == AudioManager.ADJUST_MUTE || adjust == AudioManager.ADJUST_UNMUTE
+ || adjust == AudioManager.ADJUST_TOGGLE_MUTE;
+ }
+
private boolean isInCommunication() {
boolean IsInCall = false;
@@ -3475,11 +3482,11 @@ public class AudioService extends IAudioService.Stub {
public class VolumeStreamState {
private final int mStreamType;
+ private boolean mIsMuted;
private String mVolumeIndexSettingName;
private int mIndexMax;
private final ConcurrentHashMap<Integer, Integer> mIndex =
new ConcurrentHashMap<Integer, Integer>(8, 0.75f, 4);
- private ArrayList<VolumeDeathHandler> mDeathHandlers; //handles mute/solo clients death
private VolumeStreamState(String settingName, int streamType) {
@@ -3490,9 +3497,6 @@ public class AudioService extends IAudioService.Stub {
AudioSystem.initStreamVolume(streamType, 0, mIndexMax);
mIndexMax *= 10;
- // mDeathHandlers must be created before calling readSettings()
- mDeathHandlers = new ArrayList<VolumeDeathHandler>();
-
readSettings();
}
@@ -3557,7 +3561,7 @@ public class AudioService extends IAudioService.Stub {
// must be called while synchronized VolumeStreamState.class
public void applyDeviceVolume_syncVSS(int device) {
int index;
- if (isMuted_syncVSS()) {
+ if (mIsMuted) {
index = 0;
} else if (((device & AudioSystem.DEVICE_OUT_ALL_A2DP) != 0 && mAvrcpAbsVolSupported)
|| ((device & mFullVolumeDevices) != 0)) {
@@ -3573,7 +3577,7 @@ public class AudioService extends IAudioService.Stub {
// apply default volume first: by convention this will reset all
// devices volumes in audio policy manager to the supplied value
int index;
- if (isMuted_syncVSS()) {
+ if (mIsMuted) {
index = 0;
} else {
index = (getIndex(AudioSystem.DEVICE_OUT_DEFAULT) + 5)/10;
@@ -3586,7 +3590,7 @@ public class AudioService extends IAudioService.Stub {
Map.Entry entry = (Map.Entry)i.next();
int device = ((Integer)entry.getKey()).intValue();
if (device != AudioSystem.DEVICE_OUT_DEFAULT) {
- if (isMuted_syncVSS()) {
+ if (mIsMuted) {
index = 0;
} else if (((device & AudioSystem.DEVICE_OUT_ALL_A2DP) != 0 &&
mAvrcpAbsVolSupported)
@@ -3696,14 +3700,20 @@ public class AudioService extends IAudioService.Stub {
}
}
- public void mute(IBinder cb, boolean state) {
+ public void mute(boolean state) {
synchronized (VolumeStreamState.class) {
- VolumeDeathHandler handler = getDeathHandler_syncVSS(cb, state);
- if (handler == null) {
- Log.e(TAG, "Could not get client death handler for stream: "+mStreamType);
- return;
+ if (state != mIsMuted) {
+ mIsMuted = state;
+ // Set the new mute volume. This propagates the values to
+ // the audio system, otherwise the volume won't be changed
+ // at the lower level.
+ sendMsg(mAudioHandler,
+ MSG_SET_ALL_VOLUMES,
+ SENDMSG_QUEUE,
+ 0,
+ 0,
+ this, 0);
}
- handler.mute_syncVSS(state);
}
}
@@ -3741,117 +3751,9 @@ public class AudioService extends IAudioService.Stub {
return index;
}
- private class VolumeDeathHandler implements IBinder.DeathRecipient {
- private IBinder mICallback; // To be notified of client's death
- private int mMuteCount; // Number of active mutes for this client
-
- VolumeDeathHandler(IBinder cb) {
- mICallback = cb;
- }
-
- // must be called while synchronized VolumeStreamState.class
- public void mute_syncVSS(boolean state) {
- boolean updateVolume = false;
- if (state) {
- if (mMuteCount == 0) {
- // Register for client death notification
- try {
- // mICallback can be 0 if muted by AudioService
- if (mICallback != null) {
- mICallback.linkToDeath(this, 0);
- }
- VolumeStreamState.this.mDeathHandlers.add(this);
- // If the stream is not yet muted by any client, set level to 0
- if (!VolumeStreamState.this.isMuted_syncVSS()) {
- updateVolume = true;
- }
- } catch (RemoteException e) {
- // Client has died!
- binderDied();
- return;
- }
- } else {
- Log.w(TAG, "stream: "+mStreamType+" was already muted by this client");
- }
- mMuteCount++;
- } else {
- if (mMuteCount == 0) {
- Log.e(TAG, "unexpected unmute for stream: "+mStreamType);
- } else {
- mMuteCount--;
- if (mMuteCount == 0) {
- // Unregister from client death notification
- VolumeStreamState.this.mDeathHandlers.remove(this);
- // mICallback can be 0 if muted by AudioService
- if (mICallback != null) {
- mICallback.unlinkToDeath(this, 0);
- }
- if (!VolumeStreamState.this.isMuted_syncVSS()) {
- updateVolume = true;
- }
- }
- }
- }
- if (updateVolume) {
- sendMsg(mAudioHandler,
- MSG_SET_ALL_VOLUMES,
- SENDMSG_QUEUE,
- 0,
- 0,
- VolumeStreamState.this, 0);
- }
- }
-
- public void binderDied() {
- Log.w(TAG, "Volume service client died for stream: "+mStreamType);
- synchronized (VolumeStreamState.class) {
- if (mMuteCount != 0) {
- // Reset all active mute requests from this client.
- mMuteCount = 1;
- mute_syncVSS(false);
- }
- }
- }
- }
-
- private int muteCount() {
- int count = 0;
- int size = mDeathHandlers.size();
- for (int i = 0; i < size; i++) {
- count += mDeathHandlers.get(i).mMuteCount;
- }
- return count;
- }
-
- // must be called while synchronized VolumeStreamState.class
- private boolean isMuted_syncVSS() {
- return muteCount() != 0;
- }
-
- // must be called while synchronized VolumeStreamState.class
- private VolumeDeathHandler getDeathHandler_syncVSS(IBinder cb, boolean state) {
- VolumeDeathHandler handler;
- int size = mDeathHandlers.size();
- for (int i = 0; i < size; i++) {
- handler = mDeathHandlers.get(i);
- if (cb == handler.mICallback) {
- return handler;
- }
- }
- // If this is the first mute request for this client, create a new
- // client death handler. Otherwise, it is an out of sequence unmute request.
- if (state) {
- handler = new VolumeDeathHandler(cb);
- } else {
- Log.w(TAG, "stream was not muted by this client");
- handler = null;
- }
- return handler;
- }
-
private void dump(PrintWriter pw) {
- pw.print(" Mute count: ");
- pw.println(muteCount());
+ pw.print(" Muted: ");
+ pw.println(mIsMuted);
pw.print(" Max: ");
pw.println((mIndexMax + 5) / 10);
pw.print(" Current: ");
@@ -4190,7 +4092,9 @@ public class AudioService extends IAudioService.Stub {
}
private void setForceUse(int usage, int config) {
- AudioSystem.setForceUse(usage, config);
+ synchronized (mConnectedDevices) {
+ setForceUseInt_SyncDevices(usage, config);
+ }
}
private void onPersistSafeVolumeState(int state) {
@@ -4680,6 +4584,7 @@ public class AudioService extends IAudioService.Stub {
// Devices which removal triggers intent ACTION_AUDIO_BECOMING_NOISY. The intent is only
// sent if none of these devices is connected.
+ // Access synchronized on mConnectedDevices
int mBecomingNoisyIntentDevices =
AudioSystem.DEVICE_OUT_WIRED_HEADSET | AudioSystem.DEVICE_OUT_WIRED_HEADPHONE |
AudioSystem.DEVICE_OUT_ALL_A2DP | AudioSystem.DEVICE_OUT_HDMI |
@@ -4687,6 +4592,7 @@ public class AudioService extends IAudioService.Stub {
AudioSystem.DEVICE_OUT_ALL_USB | AudioSystem.DEVICE_OUT_LINE;
// must be called before removing the device from mConnectedDevices
+ // Called synchronized on mConnectedDevices
private int checkSendBecomingNoisyIntent(int device, int state) {
int delay = 0;
if ((state == 0) && ((device & mBecomingNoisyIntentDevices) != 0)) {
@@ -4742,12 +4648,6 @@ public class AudioService extends IAudioService.Stub {
connType = AudioRoutesInfo.MAIN_HEADPHONES;
intent.setAction(Intent.ACTION_HEADSET_PLUG);
intent.putExtra("microphone", 0);
- } else if (device == AudioSystem.DEVICE_OUT_ANLG_DOCK_HEADSET) {
- connType = AudioRoutesInfo.MAIN_DOCK_SPEAKERS;
- intent.setAction(AudioManager.ACTION_ANALOG_AUDIO_DOCK_PLUG);
- } else if (device == AudioSystem.DEVICE_OUT_DGTL_DOCK_HEADSET) {
- connType = AudioRoutesInfo.MAIN_DOCK_SPEAKERS;
- intent.setAction(AudioManager.ACTION_DIGITAL_AUDIO_DOCK_PLUG);
} else if (device == AudioSystem.DEVICE_OUT_HDMI ||
device == AudioSystem.DEVICE_OUT_HDMI_ARC) {
connType = AudioRoutesInfo.MAIN_HDMI;
@@ -4964,52 +4864,6 @@ public class AudioService extends IAudioService.Stub {
}
}
}
- } else if (action.equals(AudioManager.ACTION_USB_AUDIO_ACCESSORY_PLUG)) {
- state = intent.getIntExtra("state", 0);
-
- int alsaCard = intent.getIntExtra("card", -1);
- int alsaDevice = intent.getIntExtra("device", -1);
-
- String params = (alsaCard == -1 && alsaDevice == -1 ? ""
- : "card=" + alsaCard + ";device=" + alsaDevice);
-
- // Playback Device
- outDevice = AudioSystem.DEVICE_OUT_USB_ACCESSORY;
- setWiredDeviceConnectionState(outDevice, state, params);
- } else if (action.equals(AudioManager.ACTION_USB_AUDIO_DEVICE_PLUG)) {
- // FIXME Does not yet handle the case where the setting is changed
- // after device connection. Ideally we should handle the settings change
- // in SettingsObserver. Here we should log that a USB device is connected
- // and disconnected with its address (card , device) and force the
- // connection or disconnection when the setting changes.
- int isDisabled = Settings.Secure.getInt(mContentResolver,
- Settings.Secure.USB_AUDIO_AUTOMATIC_ROUTING_DISABLED, 0);
- if (isDisabled != 0) {
- return;
- }
-
- state = intent.getIntExtra("state", 0);
-
- int alsaCard = intent.getIntExtra("card", -1);
- int alsaDevice = intent.getIntExtra("device", -1);
- boolean hasPlayback = intent.getBooleanExtra("hasPlayback", false);
- boolean hasCapture = intent.getBooleanExtra("hasCapture", false);
- boolean hasMIDI = intent.getBooleanExtra("hasMIDI", false);
-
- String params = (alsaCard == -1 && alsaDevice == -1 ? ""
- : "card=" + alsaCard + ";device=" + alsaDevice);
-
- // Playback Device
- if (hasPlayback) {
- outDevice = AudioSystem.DEVICE_OUT_USB_DEVICE;
- setWiredDeviceConnectionState(outDevice, state, params);
- }
-
- // Capture Device
- if (hasCapture) {
- inDevice = AudioSystem.DEVICE_IN_USB_DEVICE;
- setWiredDeviceConnectionState(inDevice, state, params);
- }
} else if (action.equals(BluetoothHeadset.ACTION_AUDIO_STATE_CHANGED)) {
boolean broadcast = false;
int scoAudioState = AudioManager.SCO_AUDIO_STATE_ERROR;
@@ -5290,15 +5144,39 @@ public class AudioService extends IAudioService.Stub {
// Handles request to override default use of A2DP for media.
+ // Must be called synchronized on mConnectedDevices
public void setBluetoothA2dpOnInt(boolean on) {
synchronized (mBluetoothA2dpEnabledLock) {
mBluetoothA2dpEnabled = on;
mAudioHandler.removeMessages(MSG_SET_FORCE_BT_A2DP_USE);
- AudioSystem.setForceUse(AudioSystem.FOR_MEDIA,
+ setForceUseInt_SyncDevices(AudioSystem.FOR_MEDIA,
mBluetoothA2dpEnabled ? AudioSystem.FORCE_NONE : AudioSystem.FORCE_NO_BT_A2DP);
}
}
+ // Must be called synchronized on mConnectedDevices
+ private void setForceUseInt_SyncDevices(int usage, int config) {
+ switch (usage) {
+ case AudioSystem.FOR_MEDIA:
+ if (config == AudioSystem.FORCE_NO_BT_A2DP) {
+ mBecomingNoisyIntentDevices &= ~AudioSystem.DEVICE_OUT_ALL_A2DP;
+ } else { // config == AudioSystem.FORCE_NONE
+ mBecomingNoisyIntentDevices |= AudioSystem.DEVICE_OUT_ALL_A2DP;
+ }
+ break;
+ case AudioSystem.FOR_DOCK:
+ if (config == AudioSystem.FORCE_ANALOG_DOCK) {
+ mBecomingNoisyIntentDevices |= AudioSystem.DEVICE_OUT_ANLG_DOCK_HEADSET;
+ } else { // config == AudioSystem.FORCE_NONE
+ mBecomingNoisyIntentDevices &= ~AudioSystem.DEVICE_OUT_ANLG_DOCK_HEADSET;
+ }
+ break;
+ default:
+ // usage doesn't affect the broadcast of ACTION_AUDIO_BECOMING_NOISY
+ }
+ AudioSystem.setForceUse(usage, config);
+ }
+
@Override
public void setRingtonePlayer(IRingtonePlayer player) {
mContext.enforceCallingOrSelfPermission(REMOTE_AUDIO_PLAYBACK, null);
@@ -5680,7 +5558,10 @@ public class AudioService extends IAudioService.Stub {
Settings.Secure.LONG_PRESS_TIMEOUT, 500, UserHandle.USER_CURRENT);
}
- public boolean suppressAdjustment(int resolvedStream, int flags) {
+ public boolean suppressAdjustment(int resolvedStream, int flags, boolean isMute) {
+ if (isMute) {
+ return false;
+ }
boolean suppress = false;
if (resolvedStream == AudioSystem.STREAM_RING && mController != null) {
final long now = SystemClock.uptimeMillis();
@@ -5833,12 +5714,6 @@ public class AudioService extends IAudioService.Stub {
public void setRingerModeInternal(int ringerMode, String caller) {
AudioService.this.setRingerModeInternal(ringerMode, caller);
}
-
- @Override
- public void setMasterMuteForUid(boolean state, int flags, String callingPackage, IBinder cb,
- int uid) {
- setMasterMuteInternal(state, flags, callingPackage, cb, uid);
- }
}
//==========================================================================================
diff --git a/media/java/android/media/AudioTrack.java b/media/java/android/media/AudioTrack.java
index 547d87e..caccb6e 100644
--- a/media/java/android/media/AudioTrack.java
+++ b/media/java/android/media/AudioTrack.java
@@ -512,7 +512,7 @@ public class AudioTrack
throw new IllegalArgumentException("Unsupported channel configuration.");
}
mChannels = channelConfig;
- mChannelCount = Integer.bitCount(channelConfig);
+ mChannelCount = AudioFormat.channelCountFromOutChannelMask(channelConfig);
}
//--------------
@@ -546,7 +546,7 @@ public class AudioTrack
loge("Channel configuration features unsupported channels");
return false;
}
- final int channelCount = Integer.bitCount(channelConfig);
+ final int channelCount = AudioFormat.channelCountFromOutChannelMask(channelConfig);
if (channelCount > CHANNEL_COUNT_MAX) {
loge("Channel configuration contains too many channels " +
channelCount + ">" + CHANNEL_COUNT_MAX);
@@ -763,7 +763,10 @@ public class AudioTrack
* unsigned 32-bits. That is, the next position after 0x7FFFFFFF is (int) 0x80000000.
* This is a continuously advancing counter. It will wrap (overflow) periodically,
* for example approximately once every 27:03:11 hours:minutes:seconds at 44.1 kHz.
- * It is reset to zero by flush(), reload(), and stop().
+ * It is reset to zero by {@link #flush()}, {@link #reloadStaticData()}, and {@link #stop()}.
+ * If the track's creation mode is {@link #MODE_STATIC}, the return value indicates
+ * the total number of frames played since reset,
+ * <i>not</i> the current offset within the buffer.
*/
public int getPlaybackHeadPosition() {
return native_get_position();
@@ -825,7 +828,7 @@ public class AudioTrack
loge("getMinBufferSize(): Invalid channel configuration.");
return ERROR_BAD_VALUE;
} else {
- channelCount = Integer.bitCount(channelConfig);
+ channelCount = AudioFormat.channelCountFromOutChannelMask(channelConfig);
}
}
@@ -1048,7 +1051,8 @@ public class AudioTrack
/**
* Sets the period for the periodic notification event.
- * @param periodInFrames update period expressed in frames
+ * @param periodInFrames update period expressed in frames.
+ * Zero period means no position updates. A negative period is not allowed.
* @return error code or success, see {@link #SUCCESS}, {@link #ERROR_INVALID_OPERATION}
*/
public int setPositionNotificationPeriod(int periodInFrames) {
@@ -1060,12 +1064,19 @@ public class AudioTrack
/**
- * Sets the playback head position.
+ * Sets the playback head position within the static buffer.
* The track must be stopped or paused for the position to be changed,
* and must use the {@link #MODE_STATIC} mode.
- * @param positionInFrames playback head position expressed in frames
+ * @param positionInFrames playback head position within buffer, expressed in frames.
* Zero corresponds to start of buffer.
* The position must not be greater than the buffer size in frames, or negative.
+ * Though this method and {@link #getPlaybackHeadPosition()} have similar names,
+ * the position values have different meanings.
+ * <br>
+ * If looping is currently enabled and the new position is greater than or equal to the
+ * loop end marker, the behavior varies by API level: for API level 22 and above,
+ * the looping is first disabled and then the position is set.
+ * For earlier API levels, the behavior is unspecified.
* @return error code or success, see {@link #SUCCESS}, {@link #ERROR_BAD_VALUE},
* {@link #ERROR_INVALID_OPERATION}
*/
@@ -1085,17 +1096,29 @@ public class AudioTrack
* Similarly to setPlaybackHeadPosition,
* the track must be stopped or paused for the loop points to be changed,
* and must use the {@link #MODE_STATIC} mode.
- * @param startInFrames loop start marker expressed in frames
+ * @param startInFrames loop start marker expressed in frames.
* Zero corresponds to start of buffer.
* The start marker must not be greater than or equal to the buffer size in frames, or negative.
- * @param endInFrames loop end marker expressed in frames
+ * @param endInFrames loop end marker expressed in frames.
* The total buffer size in frames corresponds to end of buffer.
* The end marker must not be greater than the buffer size in frames.
* For looping, the end marker must not be less than or equal to the start marker,
* but to disable looping
* it is permitted for start marker, end marker, and loop count to all be 0.
- * @param loopCount the number of times the loop is looped.
+ * If any input parameters are out of range, this method returns {@link #ERROR_BAD_VALUE}.
+ * If the loop period (endInFrames - startInFrames) is too small for the implementation to
+ * support,
+ * {@link #ERROR_BAD_VALUE} is returned.
+ * The loop range is the interval [startInFrames, endInFrames).
+ * <br>
+ * For API level 22 and above, the position is left unchanged,
+ * unless it is greater than or equal to the loop end marker, in which case
+ * it is forced to the loop start marker.
+ * For earlier API levels, the effect on position is unspecified.
+ * @param loopCount the number of times the loop is looped; must be greater than or equal to -1.
* A value of -1 means infinite looping, and 0 disables looping.
+ * A value of positive N means to "loop" (go back) N times. For example,
+ * a value of one means to play the region two times in total.
* @return error code or success, see {@link #SUCCESS}, {@link #ERROR_BAD_VALUE},
* {@link #ERROR_INVALID_OPERATION}
*/
@@ -1131,7 +1154,12 @@ public class AudioTrack
//--------------------
/**
* Starts playing an AudioTrack.
- * If track's creation mode is {@link #MODE_STATIC}, you must have called write() prior.
+ * If track's creation mode is {@link #MODE_STATIC}, you must have called one of
+ * the {@link #write(byte[], int, int)}, {@link #write(short[], int, int)},
+ * or {@link #write(float[], int, int, int)} methods.
+ * If the mode is {@link #MODE_STREAM}, you can optionally prime the
+ * output buffer by writing up to bufferSizeInBytes (from constructor) before starting.
+ * This priming will avoid an immediate underrun, but is not required.
*
* @throws IllegalStateException
*/
@@ -1209,8 +1237,14 @@ public class AudioTrack
/**
* Flushes the audio data currently queued for playback. Any data that has
- * not been played back will be discarded. No-op if not stopped or paused,
+ * been written but not yet presented will be discarded. No-op if not stopped or paused,
* or if the track's creation mode is not {@link #MODE_STREAM}.
+ * <BR> Note that although data written but not yet presented is discarded, there is no
+ * guarantee that all of the buffer space formerly used by that data
+ * is available for a subsequent write.
+ * For example, a call to {@link #write(byte[], int, int)} with <code>sizeInBytes</code>
+ * less than or equal to the total buffer size
+ * may return a short actual transfer count.
*/
public void flush() {
if (mState == STATE_INITIALIZED) {
@@ -1449,9 +1483,17 @@ public class AudioTrack
}
/**
- * Notifies the native resource to reuse the audio data already loaded in the native
- * layer, that is to rewind to start of buffer.
- * The track's creation mode must be {@link #MODE_STATIC}.
+ * Sets the playback head position within the static buffer to zero,
+ * that is it rewinds to start of static buffer.
+ * The track must be stopped or paused, and
+ * the track's creation mode must be {@link #MODE_STATIC}.
+ * <p>
+ * For API level 22 and above, also resets the value returned by
+ * {@link #getPlaybackHeadPosition()} to zero.
+ * For earlier API levels, the reset behavior is unspecified.
+ * <p>
+ * {@link #setPlaybackHeadPosition(int)} to zero
+ * is recommended instead when the reset of {@link #getPlaybackHeadPosition} is not needed.
* @return error code or success, see {@link #SUCCESS}, {@link #ERROR_BAD_VALUE},
* {@link #ERROR_INVALID_OPERATION}
*/
diff --git a/media/java/android/media/IAudioService.aidl b/media/java/android/media/IAudioService.aidl
index fad3cec..bfb78a1 100644
--- a/media/java/android/media/IAudioService.aidl
+++ b/media/java/android/media/IAudioService.aidl
@@ -52,16 +52,10 @@ interface IAudioService {
void setMasterVolume(int index, int flags, String callingPackage);
- void setStreamSolo(int streamType, boolean state, IBinder cb);
-
- void setStreamMute(int streamType, boolean state, IBinder cb);
-
boolean isStreamMute(int streamType);
void forceRemoteSubmixFullVolume(boolean startForcing, IBinder cb);
- void setMasterMute(boolean state, int flags, String callingPackage, IBinder cb);
-
boolean isMasterMute();
int getStreamVolume(int streamType);
diff --git a/media/java/android/media/MediaCodecInfo.java b/media/java/android/media/MediaCodecInfo.java
index 6984575..6ac854f 100644
--- a/media/java/android/media/MediaCodecInfo.java
+++ b/media/java/android/media/MediaCodecInfo.java
@@ -208,6 +208,7 @@ public final class MediaCodecInfo {
// COLOR_FormatSurface indicates that the data will be a GraphicBuffer metadata reference.
// In OMX this is called OMX_COLOR_FormatAndroidOpaque.
public static final int COLOR_FormatSurface = 0x7F000789;
+ public static final int COLOR_Format32BitRGBA8888 = 0x7F00A000;
// This corresponds to YUV_420_888 format
public static final int COLOR_FormatYUV420Flexible = 0x7F420888;
public static final int COLOR_QCOM_FormatYUV420SemiPlanar = 0x7fa30c00;
@@ -880,8 +881,8 @@ public final class MediaCodecInfo {
(int)(mAspectRatioRange.getUpper().doubleValue() * height));
return range;
} catch (IllegalArgumentException e) {
- // should not be here
- Log.w(TAG, "could not get supported widths for " + height , e);
+ // height is not supported because there are no suitable widths
+ Log.v(TAG, "could not get supported widths for " + height);
throw new IllegalArgumentException("unsupported height");
}
}
@@ -924,8 +925,8 @@ public final class MediaCodecInfo {
(int)(width / mAspectRatioRange.getLower().doubleValue()));
return range;
} catch (IllegalArgumentException e) {
- // should not be here
- Log.w(TAG, "could not get supported heights for " + width , e);
+ // width is not supported because there are no suitable heights
+ Log.v(TAG, "could not get supported heights for " + width);
throw new IllegalArgumentException("unsupported width");
}
}
diff --git a/media/java/android/media/browse/MediaBrowser.java b/media/java/android/media/browse/MediaBrowser.java
index d260b05..ef8d169 100644
--- a/media/java/android/media/browse/MediaBrowser.java
+++ b/media/java/android/media/browse/MediaBrowser.java
@@ -33,6 +33,7 @@ import android.os.IBinder;
import android.os.Parcel;
import android.os.Parcelable;
import android.os.RemoteException;
+import android.os.ResultReceiver;
import android.service.media.MediaBrowserService;
import android.service.media.IMediaBrowserService;
import android.service.media.IMediaBrowserServiceCallbacks;
@@ -291,15 +292,17 @@ public final class MediaBrowser {
* the specified id and subscribes to receive updates when they change.
* <p>
* The list of subscriptions is maintained even when not connected and is
- * restored after reconnection. It is ok to subscribe while not connected
+ * restored after reconnection. It is ok to subscribe while not connected
* but the results will not be returned until the connection completes.
- * </p><p>
+ * </p>
+ * <p>
* If the id is already subscribed with a different callback then the new
- * callback will replace the previous one.
+ * callback will replace the previous one and the child data will be
+ * reloaded.
* </p>
*
* @param parentId The id of the parent media item whose list of children
- * will be subscribed.
+ * will be subscribed.
* @param callback The callback to receive the list of children.
*/
public void subscribe(@NonNull String parentId, @NonNull SubscriptionCallback callback) {
@@ -322,7 +325,7 @@ public final class MediaBrowser {
// If we are connected, tell the service that we are watching. If we aren't
// connected, the service will be told when we connect.
- if (mState == CONNECT_STATE_CONNECTED && newSubscription) {
+ if (mState == CONNECT_STATE_CONNECTED) {
try {
mServiceBinder.addSubscription(parentId, mServiceCallbacks);
} catch (RemoteException ex) {
@@ -345,8 +348,8 @@ public final class MediaBrowser {
*/
public void unsubscribe(@NonNull String parentId) {
// Check arguments.
- if (parentId == null) {
- throw new IllegalArgumentException("parentId is null");
+ if (TextUtils.isEmpty(parentId)) {
+ throw new IllegalArgumentException("parentId is empty.");
}
// Remove from our list.
@@ -365,6 +368,60 @@ public final class MediaBrowser {
}
/**
+ * Retrieves a specific {@link MediaItem} from the connected service. Not
+ * all services may support this, so falling back to subscribing to the
+ * parent's id should be used when unavailable.
+ *
+ * @param mediaId The id of the item to retrieve.
+ * @param cb The callback to receive the result on.
+ */
+ public void getMediaItem(@NonNull String mediaId, @NonNull final MediaItemCallback cb) {
+ if (TextUtils.isEmpty(mediaId)) {
+ throw new IllegalArgumentException("mediaId is empty.");
+ }
+ if (cb == null) {
+ throw new IllegalArgumentException("cb is null.");
+ }
+ if (mState != CONNECT_STATE_CONNECTED) {
+ Log.i(TAG, "Not connected, unable to retrieve the MediaItem.");
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ cb.onError();
+ }
+ });
+ return;
+ }
+ ResultReceiver receiver = new ResultReceiver(mHandler) {
+ @Override
+ protected void onReceiveResult(int resultCode, Bundle resultData) {
+ if (resultCode != 0 || resultData == null
+ || !resultData.containsKey(MediaBrowserService.KEY_MEDIA_ITEM)) {
+ cb.onError();
+ return;
+ }
+ Parcelable item = resultData.getParcelable(MediaBrowserService.KEY_MEDIA_ITEM);
+ if (!(item instanceof MediaItem)) {
+ cb.onError();
+ }
+ cb.onMediaItemLoaded((MediaItem) resultData.getParcelable(
+ MediaBrowserService.KEY_MEDIA_ITEM));
+ }
+ };
+ try {
+ mServiceBinder.getMediaItem(mediaId, receiver);
+ } catch (RemoteException e) {
+ Log.i(TAG, "Remote error getting media item.");
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ cb.onError();
+ }
+ });
+ }
+ }
+
+ /**
* For debugging.
*/
private static String getStateLabel(int state) {
@@ -688,6 +745,27 @@ public final class MediaBrowser {
}
/**
+ * Callback for receiving the result of {@link #getMediaItem}.
+ */
+ public static abstract class MediaItemCallback {
+
+ /**
+ * Called when the item has been returned by the browser service.
+ *
+ * @param item The item that was returned or null if it doesn't exist.
+ */
+ public void onMediaItemLoaded(MediaItem item) {
+ }
+
+ /**
+ * Called when the id doesn't exist or there was an error retrieving the
+ * item.
+ */
+ public void onError() {
+ }
+ }
+
+ /**
* ServiceConnection to the other app.
*/
private class MediaServiceConnection implements ServiceConnection {
diff --git a/media/java/android/media/routing/IMediaRouteClientCallback.aidl b/media/java/android/media/routing/IMediaRouteClientCallback.aidl
new file mode 100644
index 0000000..d90ea3b
--- /dev/null
+++ b/media/java/android/media/routing/IMediaRouteClientCallback.aidl
@@ -0,0 +1,41 @@
+/* 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.routing;
+
+import android.media.routing.MediaRouteSelector;
+import android.media.routing.ParcelableConnectionInfo;
+import android.media.routing.ParcelableDestinationInfo;
+import android.media.routing.ParcelableRouteInfo;
+import android.os.IBinder;
+import android.os.Bundle;
+
+/**
+ * @hide
+ */
+oneway interface IMediaRouteClientCallback {
+ void onDestinationFound(int seq, in ParcelableDestinationInfo destination,
+ in ParcelableRouteInfo[] routes);
+
+ void onDestinationLost(int seq, String id);
+
+ void onDiscoveryFailed(int seq, int error, in CharSequence message, in Bundle extras);
+
+ void onConnected(int seq, in ParcelableConnectionInfo connection);
+
+ void onDisconnected(int seq);
+
+ void onConnectionFailed(int seq, int error, in CharSequence message, in Bundle extras);
+}
diff --git a/media/java/android/media/routing/IMediaRouteService.aidl b/media/java/android/media/routing/IMediaRouteService.aidl
new file mode 100644
index 0000000..493ab6d
--- /dev/null
+++ b/media/java/android/media/routing/IMediaRouteService.aidl
@@ -0,0 +1,46 @@
+/* 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.routing;
+
+import android.media.routing.IMediaRouteClientCallback;
+import android.media.routing.MediaRouteSelector;
+import android.os.Bundle;
+
+/**
+ * Interface to an app's MediaRouteService.
+ * @hide
+ */
+oneway interface IMediaRouteService {
+ void registerClient(int clientUid, String clientPackageName,
+ in IMediaRouteClientCallback callback);
+
+ void unregisterClient(in IMediaRouteClientCallback callback);
+
+ void startDiscovery(in IMediaRouteClientCallback callback, int seq,
+ in List<MediaRouteSelector> selectors, int flags);
+
+ void stopDiscovery(in IMediaRouteClientCallback callback);
+
+ void connect(in IMediaRouteClientCallback callback, int seq,
+ String destinationId, String routeId, int flags, in Bundle extras);
+
+ void disconnect(in IMediaRouteClientCallback callback);
+
+ void pauseStream(in IMediaRouteClientCallback callback);
+
+ void resumeStream(in IMediaRouteClientCallback callback);
+}
+
diff --git a/media/java/android/media/routing/IMediaRouter.aidl b/media/java/android/media/routing/IMediaRouter.aidl
new file mode 100644
index 0000000..0abb258
--- /dev/null
+++ b/media/java/android/media/routing/IMediaRouter.aidl
@@ -0,0 +1,22 @@
+/* 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.routing;
+
+/** @hide */
+interface IMediaRouter {
+
+}
+
diff --git a/media/java/android/media/routing/IMediaRouterDelegate.aidl b/media/java/android/media/routing/IMediaRouterDelegate.aidl
new file mode 100644
index 0000000..35f84c8
--- /dev/null
+++ b/media/java/android/media/routing/IMediaRouterDelegate.aidl
@@ -0,0 +1,22 @@
+/* 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.routing;
+
+/** @hide */
+interface IMediaRouterDelegate {
+
+}
+
diff --git a/media/java/android/media/routing/IMediaRouterRoutingCallback.aidl b/media/java/android/media/routing/IMediaRouterRoutingCallback.aidl
new file mode 100644
index 0000000..173ae55
--- /dev/null
+++ b/media/java/android/media/routing/IMediaRouterRoutingCallback.aidl
@@ -0,0 +1,22 @@
+/* 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.routing;
+
+/** @hide */
+interface IMediaRouterRoutingCallback {
+
+}
+
diff --git a/media/java/android/media/routing/IMediaRouterStateCallback.aidl b/media/java/android/media/routing/IMediaRouterStateCallback.aidl
new file mode 100644
index 0000000..0299904
--- /dev/null
+++ b/media/java/android/media/routing/IMediaRouterStateCallback.aidl
@@ -0,0 +1,22 @@
+/* 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.routing;
+
+/** @hide */
+interface IMediaRouterStateCallback {
+
+}
+
diff --git a/media/java/android/media/routing/MediaRouteSelector.aidl b/media/java/android/media/routing/MediaRouteSelector.aidl
new file mode 100644
index 0000000..37bfa4a
--- /dev/null
+++ b/media/java/android/media/routing/MediaRouteSelector.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.routing;
+
+parcelable MediaRouteSelector;
diff --git a/media/java/android/media/routing/MediaRouteSelector.java b/media/java/android/media/routing/MediaRouteSelector.java
new file mode 100644
index 0000000..26a9b1c
--- /dev/null
+++ b/media/java/android/media/routing/MediaRouteSelector.java
@@ -0,0 +1,357 @@
+/*
+ * 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.routing;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.media.routing.MediaRouter.RouteFeatures;
+import android.os.Bundle;
+import android.os.IInterface;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.text.TextUtils;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * A media route selector consists of a set of constraints that are used to select
+ * the routes to which an application would like to connect. The constraints consist
+ * of a set of required or optional features and protocols. The constraints may also
+ * require the use of a specific media route service package or additional characteristics
+ * that are described by a bundle of extra parameters.
+ * <p>
+ * The application will typically create several different selectors that express
+ * various combinations of characteristics that it would like to use together when
+ * it connects to a destination media device. For each destination that is discovered,
+ * media route services will publish some number of routes and include information
+ * about which selector each route matches. The application will then choose among
+ * these routes to determine which best satisfies its desired purpose and connect to it.
+ * </p>
+ */
+public final class MediaRouteSelector implements Parcelable {
+ private final int mRequiredFeatures;
+ private final int mOptionalFeatures;
+ private final List<String> mRequiredProtocols;
+ private final List<String> mOptionalProtocols;
+ private final String mServicePackageName;
+ private final Bundle mExtras;
+
+ MediaRouteSelector(int requiredFeatures, int optionalFeatures,
+ List<String> requiredProtocols, List<String> optionalProtocols,
+ String servicePackageName, Bundle extras) {
+ mRequiredFeatures = requiredFeatures;
+ mOptionalFeatures = optionalFeatures;
+ mRequiredProtocols = requiredProtocols;
+ mOptionalProtocols = optionalProtocols;
+ mServicePackageName = servicePackageName;
+ mExtras = extras;
+ }
+
+ /**
+ * Gets the set of required route features.
+ *
+ * @return A set of required route feature flags.
+ */
+ public @RouteFeatures int getRequiredFeatures() {
+ return mRequiredFeatures;
+ }
+
+ /**
+ * Gets the set of optional route features.
+ *
+ * @return A set of optional route feature flags.
+ */
+ public @RouteFeatures int getOptionalFeatures() {
+ return mOptionalFeatures;
+ }
+
+ /**
+ * Gets the list of route protocols that a route must support in order to be selected.
+ * <p>
+ * Refer to <code>android.support.media.protocols.MediaRouteProtocol</code>
+ * for more information.
+ * </p>
+ *
+ * @return The list of fully qualified route protocol names.
+ */
+ public @NonNull List<String> getRequiredProtocols() {
+ return mRequiredProtocols;
+ }
+
+ /**
+ * Gets the list of optional route protocols that a client may use if they are available.
+ * <p>
+ * Refer to <code>android.support.media.protocols.MediaRouteProtocol</code>
+ * for more information.
+ * </p>
+ *
+ * @return The list of optional fully qualified route protocol names.
+ */
+ public @NonNull List<String> getOptionalProtocols() {
+ return mOptionalProtocols;
+ }
+
+ /**
+ * Returns true if the selector includes a required or optional request for
+ * the specified protocol using its fully qualified class name.
+ * <p>
+ * Refer to <code>android.support.media.protocols.MediaRouteProtocol</code>
+ * for more information.
+ * </p>
+ *
+ * @param clazz The protocol class.
+ * @return True if the protocol was requested.
+ */
+ public boolean containsProtocol(@NonNull Class<?> clazz) {
+ return containsProtocol(clazz.getName());
+ }
+
+ /**
+ * Returns true if the selector includes a required or optional request for
+ * the specified protocol.
+ * <p>
+ * Refer to <code>android.support.media.protocols.MediaRouteProtocol</code>
+ * for more information.
+ * </p>
+ *
+ * @param name The name of the protocol.
+ * @return True if the protocol was requested.
+ */
+ public boolean containsProtocol(@NonNull String name) {
+ return mRequiredProtocols.contains(name)
+ || mOptionalProtocols.contains(name);
+ }
+
+ /**
+ * Gets the package name of a specific media route service that this route selector
+ * requires.
+ *
+ * @return The required media route service package name, or null if none.
+ */
+ public @Nullable String getServicePackageName() {
+ return mServicePackageName;
+ }
+
+ /**
+ * Gets optional extras that may be used to select or configure routes for a
+ * particular purpose. Some extras may be used by media route services to apply
+ * additional constraints or parameters for the routes to be discovered.
+ *
+ * @return The optional extras, or null if none.
+ */
+ public @Nullable Bundle getExtras() {
+ return mExtras;
+ }
+
+ @Override
+ public String toString() {
+ return "MediaRouteSelector{ "
+ + ", requiredFeatures=0x" + Integer.toHexString(mRequiredFeatures)
+ + ", optionalFeatures=0x" + Integer.toHexString(mOptionalFeatures)
+ + ", requiredProtocols=" + mRequiredProtocols
+ + ", optionalProtocols=" + mOptionalProtocols
+ + ", servicePackageName=" + mServicePackageName
+ + ", extras=" + mExtras + " }";
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeInt(mRequiredFeatures);
+ dest.writeInt(mOptionalFeatures);
+ dest.writeStringList(mRequiredProtocols);
+ dest.writeStringList(mOptionalProtocols);
+ dest.writeString(mServicePackageName);
+ dest.writeBundle(mExtras);
+ }
+
+ public static final Parcelable.Creator<MediaRouteSelector> CREATOR =
+ new Parcelable.Creator<MediaRouteSelector>() {
+ @Override
+ public MediaRouteSelector createFromParcel(Parcel source) {
+ int requiredFeatures = source.readInt();
+ int optionalFeatures = source.readInt();
+ ArrayList<String> requiredProtocols = new ArrayList<String>();
+ ArrayList<String> optionalProtocols = new ArrayList<String>();
+ source.readStringList(requiredProtocols);
+ source.readStringList(optionalProtocols);
+ return new MediaRouteSelector(requiredFeatures, optionalFeatures,
+ requiredProtocols, optionalProtocols,
+ source.readString(), source.readBundle());
+ }
+
+ @Override
+ public MediaRouteSelector[] newArray(int size) {
+ return new MediaRouteSelector[size];
+ }
+ };
+
+ /**
+ * Builder for {@link MediaRouteSelector} objects.
+ */
+ public static final class Builder {
+ private int mRequiredFeatures;
+ private int mOptionalFeatures;
+ private final ArrayList<String> mRequiredProtocols = new ArrayList<String>();
+ private final ArrayList<String> mOptionalProtocols = new ArrayList<String>();
+ private String mServicePackageName;
+ private Bundle mExtras;
+
+ /**
+ * Creates an initially empty selector builder.
+ */
+ public Builder() {
+ }
+
+ /**
+ * Sets the set of required route features.
+ *
+ * @param features A set of required route feature flags.
+ */
+ public @NonNull Builder setRequiredFeatures(@RouteFeatures int features) {
+ mRequiredFeatures = features;
+ return this;
+ }
+
+ /**
+ * Sets the set of optional route features.
+ *
+ * @param features A set of optional route feature flags.
+ */
+ public @NonNull Builder setOptionalFeatures(@RouteFeatures int features) {
+ mOptionalFeatures = features;
+ return this;
+ }
+
+ /**
+ * Adds a route protocol that a route must support in order to be selected
+ * using its fully qualified class name.
+ * <p>
+ * Refer to <code>android.support.media.protocols.MediaRouteProtocol</code>
+ * for more information.
+ * </p>
+ *
+ * @param clazz The protocol class.
+ * @return this
+ */
+ public @NonNull Builder addRequiredProtocol(@NonNull Class<?> clazz) {
+ if (clazz == null) {
+ throw new IllegalArgumentException("clazz must not be null");
+ }
+ return addRequiredProtocol(clazz.getName());
+ }
+
+ /**
+ * Adds a route protocol that a route must support in order to be selected.
+ * <p>
+ * Refer to <code>android.support.media.protocols.MediaRouteProtocol</code>
+ * for more information.
+ * </p>
+ *
+ * @param name The fully qualified name of the required protocol.
+ * @return this
+ */
+ public @NonNull Builder addRequiredProtocol(@NonNull String name) {
+ if (TextUtils.isEmpty(name)) {
+ throw new IllegalArgumentException("name must not be null or empty");
+ }
+ mRequiredProtocols.add(name);
+ return this;
+ }
+
+ /**
+ * Adds an optional route protocol that a client may use if available
+ * using its fully qualified class name.
+ * <p>
+ * Refer to <code>android.support.media.protocols.MediaRouteProtocol</code>
+ * for more information.
+ * </p>
+ *
+ * @param clazz The protocol class.
+ * @return this
+ */
+ public @NonNull Builder addOptionalProtocol(@NonNull Class<?> clazz) {
+ if (clazz == null) {
+ throw new IllegalArgumentException("clazz must not be null");
+ }
+ return addOptionalProtocol(clazz.getName());
+ }
+
+ /**
+ * Adds an optional route protocol that a client may use if available.
+ * <p>
+ * Refer to <code>android.support.media.protocols.MediaRouteProtocol</code>
+ * for more information.
+ * </p>
+ *
+ * @param name The fully qualified name of the optional protocol.
+ * @return this
+ */
+ public @NonNull Builder addOptionalProtocol(@NonNull String name) {
+ if (TextUtils.isEmpty(name)) {
+ throw new IllegalArgumentException("name must not be null or empty");
+ }
+ mOptionalProtocols.add(name);
+ return this;
+ }
+
+ /**
+ * Sets the package name of the media route service to which this selector
+ * appertains.
+ * <p>
+ * If a package name is specified here then this selector will only be
+ * passed to media route services from that package. This has the effect
+ * of restricting the set of matching routes to just those that are offered
+ * by that package.
+ * </p>
+ *
+ * @param packageName The required service package name, or null if none.
+ * @return this
+ */
+ public @NonNull Builder setServicePackageName(@Nullable String packageName) {
+ mServicePackageName = packageName;
+ return this;
+ }
+
+ /**
+ * Sets optional extras that may be used to select or configure routes for a
+ * particular purpose. Some extras may be used by route services to specify
+ * additional constraints or parameters for the routes to be discovered.
+ *
+ * @param extras The optional extras, or null if none.
+ * @return this
+ */
+ public @NonNull Builder setExtras(@Nullable Bundle extras) {
+ mExtras = extras;
+ return this;
+ }
+
+ /**
+ * Builds the {@link MediaRouteSelector} object.
+ *
+ * @return The new media route selector instance.
+ */
+ public @NonNull MediaRouteSelector build() {
+ return new MediaRouteSelector(mRequiredFeatures, mOptionalFeatures,
+ mRequiredProtocols, mOptionalProtocols, mServicePackageName, mExtras);
+ }
+ }
+}
diff --git a/media/java/android/media/routing/MediaRouteService.java b/media/java/android/media/routing/MediaRouteService.java
new file mode 100644
index 0000000..4d5a8a9
--- /dev/null
+++ b/media/java/android/media/routing/MediaRouteService.java
@@ -0,0 +1,1023 @@
+/*
+ * 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.routing;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.SdkConstant;
+import android.app.Service;
+import android.content.Intent;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.media.routing.MediaRouter.ConnectionError;
+import android.media.routing.MediaRouter.ConnectionInfo;
+import android.media.routing.MediaRouter.ConnectionRequest;
+import android.media.routing.MediaRouter.DestinationInfo;
+import android.media.routing.MediaRouter.DiscoveryError;
+import android.media.routing.MediaRouter.DiscoveryRequest;
+import android.media.routing.MediaRouter.RouteInfo;
+import android.media.routing.MediaRouter.ServiceMetadata;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.util.ArrayMap;
+import android.util.Log;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Media route services implement strategies for discovering
+ * and establishing connections to media devices and their routes. These services
+ * are also known as media route providers.
+ * <p>
+ * Each media route service subclass is responsible for enabling applications
+ * and the system to interact with media devices of some kind.
+ * For example, one particular media route service implementation might
+ * offer support for discovering nearby wireless display devices and streaming
+ * video contents to them; another media route service implementation might
+ * offer support for discovering nearby speakers and streaming media appliances
+ * and sending commands to play content on request.
+ * </p><p>
+ * Subclasses must override the {@link #onCreateClientSession} method to return
+ * a {@link ClientSession} object that implements the {@link ClientSession#onStartDiscovery},
+ * {@link ClientSession#onStopDiscovery}, and {@link ClientSession#onConnect} methods
+ * to allow clients to discover and connect to media devices.
+ * </p><p>
+ * This object is not thread-safe. All callbacks are invoked on the main looper.
+ * </p>
+ *
+ * <h3>Clients</h3>
+ * <p>
+ * The clients of this API are media applications that would like to discover
+ * and connect to media devices. The client may also be the system, such as
+ * when the user initiates display mirroring via the Cast Screen function.
+ * </p><p>
+ * There may be multiple client sessions active at the same time. Each client
+ * session can request discovery and connect to routes independently of any
+ * other client. It is the responsibility of the media route service to maintain
+ * separate state for each client session and to ensure that clients cannot interfere
+ * with one another in harmful ways.
+ * </p><p>
+ * Notwithstanding the requirement to support any number of concurrent client
+ * sessions, the media route service may impose constraints on how many clients
+ * can connect to the same media device in a particular mode at the same time.
+ * In some cases, media devices may support connections from an arbitrary number
+ * of clients simultaneously but often it may be necessary to ensure that only
+ * one client is in control. When this happens, the media route service should
+ * report a connection error unless the connection request specifies that the
+ * client should take control of the media device (and forcibly disconnect other
+ * clients that may be using it).
+ * </p>
+ *
+ * <h3>Destinations</h3>
+ * <p>
+ * The media devices to which an application may send media content are referred
+ * to in the API as destinations. Each destination therefore represents a single
+ * independent device such as a speaker or TV set. Destinations are given meaningful
+ * names and descriptions to help the user associate them with devices in their
+ * environment.
+ * </p><p>
+ * Destinations may be local or remote and may be accessed through various means,
+ * often wirelessly. The user may install media route services to enable
+ * media applications to connect to a variety of destinations with different
+ * capabilities.
+ * </p>
+ *
+ * <h3>Routes</h3>
+ * <p>
+ * Routes represent possible usages or means of reaching and interacting with
+ * a destination. Since destinations may support many different features, they may
+ * each offer multiple routes for applications to choose from based on their needs.
+ * For example, one route might express the ability to stream locally rendered audio
+ * and video to the device; another route might express the ability to send a URL for
+ * the destination to download from the network and play all by itself.
+ * </p><p>
+ * Routes are discovered according to the set of capabilities that
+ * an application or the system is seeking to use at a particular time. For example,
+ * if an application wants to stream music to a destination then it will ask the
+ * {@link MediaRouter} to find routes to destinations can stream music and ignore
+ * all other destinations that cannot.
+ * </p><p>
+ * In general, the application will inspect the set of routes that have been
+ * offered then connect to the most appropriate route for its desired purpose.
+ * </p>
+ *
+ * <h3>Discovery</h3>
+ * <p>
+ * Discovery is the process of finding destinations based on a description of the
+ * kinds of routes that an application or the system would like to use.
+ * </p><p>
+ * Discovery begins when {@link ClientSession#onStartDiscovery} is called and ends when
+ * {@link ClientSession#onStopDiscovery} is called. There may be multiple simultaneous
+ * discovery requests in progress at the same time from different clients. It is up to
+ * the media route service to perform these requests in parallel or multiplex them
+ * as required.
+ * </p><p>
+ * Media route services are <em>strongly encouraged</em> to use the information
+ * in the discovery request to optimize discovery and avoid redundant work.
+ * In the case where no media device supported by the media route service
+ * could possibly offer the requested capabilities, the
+ * {@link ClientSession#onStartDiscovery} method should return <code>false</code> to
+ * let the system know that it can unbind from the media route service and
+ * release its resources.
+ * </p>
+ *
+ * <h3>Settings</h3>
+ * <p>
+ * Many kinds of devices can be discovered on demand simply by scanning the local network
+ * or using wireless protocols such as Bluetooth to find them. However, in some cases
+ * it may be necessary for the user to manually configure destinations before they
+ * can be used (or to adjust settings later). Actual user configuration of destinations
+ * is beyond the scope of this API but media route services may specify an activity
+ * in their manifest that the user can launch to perform these tasks.
+ * </p><p>
+ * Note that media route services that are installed from the store must be enabled
+ * by the user before they become available for applications to use.
+ * The {@link android.provider.Settings#ACTION_CAST_SETTINGS Settings.ACTION_CAST_SETTINGS}
+ * settings activity provides the ability for the user to configure media route services.
+ * </p>
+ *
+ * <h3>Manifest Declaration</h3>
+ * <p>
+ * Media route services must be declared in the manifest along with meta-data
+ * about the kinds of routes that they are capable of discovering. The system
+ * uses this information to optimize the set of services to which it binds in
+ * order to satisfy a particular discovery request.
+ * </p><p>
+ * To extend this class, you must declare the service in your manifest file with
+ * the {@link android.Manifest.permission#BIND_MEDIA_ROUTE_SERVICE} permission
+ * and include an intent filter with the {@link #SERVICE_INTERFACE} action. You must
+ * also add meta-data to describe the kinds of routes that your service is capable
+ * of discovering.
+ * </p><p>
+ * For example:
+ * </p><pre>
+ * &lt;service android:name=".MediaRouteProvider"
+ * android:label="&#64;string/service_name"
+ * android:permission="android.permission.BIND_MEDIA_ROUTE_SERVICE">
+ * &lt;intent-filter>
+ * &lt;action android:name="android.media.routing.MediaRouteService" />
+ * &lt;/intent-filter>
+ *
+ * TODO: INSERT METADATA DECLARATIONS HERE
+ *
+ * &lt;/service>
+ * </pre>
+ */
+public abstract class MediaRouteService extends Service {
+ private static final String TAG = "MediaRouteService";
+
+ private static final boolean DEBUG = true;
+
+ private final Handler mHandler;
+ private final BinderService mService;
+ private final ArrayMap<IBinder, ClientRecord> mClientRecords =
+ new ArrayMap<IBinder, ClientRecord>();
+
+ private ServiceMetadata mMetadata;
+
+ /**
+ * The {@link Intent} that must be declared as handled by the service.
+ */
+ @SdkConstant(SdkConstant.SdkConstantType.SERVICE_ACTION)
+ public static final String SERVICE_INTERFACE =
+ "android.media.routing.MediaRouteService";
+
+ /**
+ * Creates a media route service.
+ */
+ public MediaRouteService() {
+ mHandler = new Handler(true);
+ mService = new BinderService();
+ }
+
+ @Override
+ public @Nullable IBinder onBind(Intent intent) {
+ if (SERVICE_INTERFACE.equals(intent.getAction())) {
+ return mService;
+ }
+ return null;
+ }
+
+ /**
+ * Creates a new client session on behalf of a client.
+ * <p>
+ * The implementation should return a {@link ClientSession} for the client
+ * to use. The media route service must take care to manage the state of
+ * each client session independently from any others that might also be
+ * in use at the same time.
+ * </p>
+ *
+ * @param client Information about the client.
+ * @return The client session object, or null if the client is not allowed
+ * to interact with this media route service.
+ */
+ public abstract @Nullable ClientSession onCreateClientSession(@NonNull ClientInfo client);
+
+ /**
+ * Gets metadata about this service.
+ * <p>
+ * Use this method to obtain a {@link ServiceMetadata} object to provide when creating
+ * a {@link android.media.routing.MediaRouter.DestinationInfo.Builder}.
+ * </p>
+ *
+ * @return Metadata about this service.
+ */
+ public @NonNull ServiceMetadata getServiceMetadata() {
+ if (mMetadata == null) {
+ try {
+ mMetadata = new ServiceMetadata(this);
+ } catch (NameNotFoundException ex) {
+ Log.wtf(TAG, "Could not retrieve own service metadata!");
+ }
+ }
+ return mMetadata;
+ }
+
+ /**
+ * Enables a single client to access the functionality of the media route service.
+ */
+ public static abstract class ClientSession {
+ /**
+ * Starts discovery.
+ * <p>
+ * If the media route service is capable of discovering routes that satisfy
+ * the request then this method should start discovery and return true.
+ * Otherwise, this method should return false. If false is returned,
+ * then the framework will not call {@link #onStopDiscovery} since discovery
+ * was never actually started.
+ * </p><p>
+ * There may already be other discovery requests in progress at the same time
+ * for other clients; the media route service must keep track of them all.
+ * </p>
+ *
+ * @param req The discovery request to start.
+ * @param callback A callback to receive discovery events related to this
+ * particular request. The events that the service sends to this callback
+ * will be sent to the client that initiated the discovery request.
+ * @return True if discovery has started. False if the media route service
+ * is unable to discover routes that satisfy the request.
+ */
+ public abstract boolean onStartDiscovery(@NonNull DiscoveryRequest req,
+ @NonNull DiscoveryCallback callback);
+
+ /**
+ * Stops discovery.
+ * <p>
+ * If {@link #onStartDiscovery} returned true, then this method will eventually
+ * be called when the framework no longer requires this discovery request
+ * to be performed.
+ * </p><p>
+ * There may still be other discovery requests in progress for other clients;
+ * they must keep working until they have each been stopped by their client.
+ * </p>
+ */
+ public abstract void onStopDiscovery();
+
+ /**
+ * Starts connecting to a route.
+ *
+ * @param req The connection request.
+ * @param callback A callback to receive events connection events related
+ * to this particular request. The events that the service sends to this callback
+ * will be sent to the client that initiated the discovery request.
+ * @return True if the connection is in progress, or false if the client
+ * unable to connect to the requested route.
+ */
+ public abstract boolean onConnect(@NonNull ConnectionRequest req,
+ @NonNull ConnectionCallback callback);
+
+ /**
+ * Called when the client requests to disconnect from the route
+ * or abort a connection attempt in progress.
+ */
+ public abstract void onDisconnect();
+
+ /**
+ * Called when the client requests to pause streaming of content to
+ * live audio/video routes such as when it goes into the background.
+ * <p>
+ * The default implementation does nothing.
+ * </p>
+ */
+ public void onPauseStream() { }
+
+ /**
+ * Called when the application requests to resume streaming of content to
+ * live audio/video routes such as when it returns to the foreground.
+ * <p>
+ * The default implementation does nothing.
+ * </p>
+ */
+ public void onResumeStream() { }
+
+ /**
+ * Called when the client is releasing the session.
+ * <p>
+ * The framework automatically takes care of stopping discovery and
+ * terminating the connection politely before calling this method to release
+ * the session.
+ * </p><p>
+ * The default implementation does nothing.
+ * </p>
+ */
+ public void onRelease() { }
+ }
+
+ /**
+ * Provides events in response to a discovery request.
+ */
+ public final class DiscoveryCallback {
+ private final ClientRecord mRecord;
+
+ DiscoveryCallback(ClientRecord record) {
+ mRecord = record;
+ }
+
+ /**
+ * Called by the service when a destination is found that
+ * offers one or more routes that satisfy the discovery request.
+ * <p>
+ * This method should be called whenever the list of available routes
+ * at a destination changes or whenever the properties of the destination
+ * itself change.
+ * </p>
+ *
+ * @param destination The destination that was found.
+ * @param routes The list of that destination's routes that satisfy the
+ * discovery request.
+ */
+ public void onDestinationFound(final @NonNull DestinationInfo destination,
+ final @NonNull List<RouteInfo> routes) {
+ if (destination == null) {
+ throw new IllegalArgumentException("destination must not be null");
+ }
+ if (routes == null) {
+ throw new IllegalArgumentException("routes must not be null");
+ }
+ for (int i = 0; i < routes.size(); i++) {
+ if (routes.get(i).getDestination() != destination) {
+ throw new IllegalArgumentException("routes must refer to the "
+ + "destination");
+ }
+ }
+
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ mRecord.dispatchDestinationFound(DiscoveryCallback.this,
+ destination, routes);
+ }
+ });
+ }
+
+ /**
+ * Called by the service when a destination is no longer
+ * reachable or is no longer offering any routes that satisfy
+ * the discovery request.
+ *
+ * @param destination The destination that went away.
+ */
+ public void onDestinationLost(final @NonNull DestinationInfo destination) {
+ if (destination == null) {
+ throw new IllegalArgumentException("destination must not be null");
+ }
+
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ mRecord.dispatchDestinationLost(DiscoveryCallback.this, destination);
+ }
+ });
+ }
+
+ /**
+ * Called by the service when a discovery has failed in a non-recoverable manner.
+ *
+ * @param error The error code: one of
+ * {@link MediaRouter#DISCOVERY_ERROR_UNKNOWN},
+ * {@link MediaRouter#DISCOVERY_ERROR_ABORTED},
+ * or {@link MediaRouter#DISCOVERY_ERROR_NO_CONNECTIVITY}.
+ * @param message The localized error message, or null if none. This message
+ * may be shown to the user.
+ * @param extras Additional information about the error which a client
+ * may use, or null if none.
+ */
+ public void onDiscoveryFailed(final @DiscoveryError int error,
+ final @Nullable CharSequence message, final @Nullable Bundle extras) {
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ mRecord.dispatchDiscoveryFailed(DiscoveryCallback.this,
+ error, message, extras);
+ }
+ });
+ }
+ }
+
+ /**
+ * Provides events in response to a connection request.
+ */
+ public final class ConnectionCallback {
+ private final ClientRecord mRecord;
+
+ ConnectionCallback(ClientRecord record) {
+ mRecord = record;
+ }
+
+ /**
+ * Called by the service when the connection succeeds.
+ *
+ * @param connection Immutable information about the connection.
+ */
+ public void onConnected(final @NonNull ConnectionInfo connection) {
+ if (connection == null) {
+ throw new IllegalArgumentException("connection must not be null");
+ }
+
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ mRecord.dispatchConnected(ConnectionCallback.this, connection);
+ }
+ });
+ }
+
+ /**
+ * Called by the service when the connection is terminated normally.
+ * <p>
+ * Abnormal termination is reported via {@link #onConnectionFailed}.
+ * </p>
+ */
+ public void onDisconnected() {
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ mRecord.dispatchDisconnected(ConnectionCallback.this);
+ }
+ });
+ }
+
+ /**
+ * Called by the service when a connection attempt or connection in
+ * progress has failed in a non-recoverable manner.
+ *
+ * @param error The error code: one of
+ * {@link MediaRouter#CONNECTION_ERROR_ABORTED},
+ * {@link MediaRouter#CONNECTION_ERROR_UNAUTHORIZED},
+ * {@link MediaRouter#CONNECTION_ERROR_UNREACHABLE},
+ * {@link MediaRouter#CONNECTION_ERROR_BUSY},
+ * {@link MediaRouter#CONNECTION_ERROR_TIMEOUT},
+ * {@link MediaRouter#CONNECTION_ERROR_BROKEN},
+ * or {@link MediaRouter#CONNECTION_ERROR_BARGED}.
+ * @param message The localized error message, or null if none. This message
+ * may be shown to the user.
+ * @param extras Additional information about the error which a client
+ * may use, or null if none.
+ */
+ public void onConnectionFailed(final @ConnectionError int error,
+ final @Nullable CharSequence message, final @Nullable Bundle extras) {
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ mRecord.dispatchConnectionFailed(ConnectionCallback.this,
+ error, message, extras);
+ }
+ });
+ }
+ }
+
+ /**
+ * Identifies a client of the media route service.
+ */
+ public static final class ClientInfo {
+ private final int mUid;
+ private final String mPackageName;
+
+ ClientInfo(int uid, String packageName) {
+ mUid = uid;
+ mPackageName = packageName;
+ }
+
+ /**
+ * Gets the UID of the client application.
+ */
+ public int getUid() {
+ return mUid;
+ }
+
+ /**
+ * Gets the package name of the client application.
+ */
+ public @NonNull String getPackageName() {
+ return mPackageName;
+ }
+
+ @Override
+ public @NonNull String toString() {
+ return "ClientInfo{ uid=" + mUid + ", package=" + mPackageName + " }";
+ }
+ }
+
+ private final class BinderService extends IMediaRouteService.Stub {
+ @Override
+ public void registerClient(final int clientUid, final String clientPackageName,
+ final IMediaRouteClientCallback callback) {
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ ClientInfo client = new ClientInfo(clientUid, clientPackageName);
+ if (DEBUG) {
+ Log.d(TAG, "registerClient: client=" + client);
+ }
+
+ ClientSession session = onCreateClientSession(client);
+ if (session == null) {
+ // request refused by service
+ Log.w(TAG, "Media route service refused to create session for client: "
+ + "client=" + client);
+ return;
+ }
+
+ ClientRecord record = new ClientRecord(callback, client, session);
+ try {
+ callback.asBinder().linkToDeath(record, 0);
+ } catch (RemoteException ex) {
+ // client died prematurely
+ Log.w(TAG, "Client died prematurely while creating session: "
+ + "client=" + client);
+ record.release();
+ return;
+ }
+
+ mClientRecords.put(callback.asBinder(), record);
+ }
+ });
+ }
+
+ @Override
+ public void unregisterClient(IMediaRouteClientCallback callback) {
+ unregisterClient(callback, false);
+ }
+
+ void unregisterClient(final IMediaRouteClientCallback callback,
+ final boolean died) {
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ ClientRecord record = mClientRecords.remove(callback.asBinder());
+ if (record == null) {
+ return; // spurious
+ }
+
+ if (DEBUG) {
+ Log.d(TAG, "unregisterClient: client=" + record.getClientInfo()
+ + ", died=" + died);
+ }
+
+ record.release();
+ callback.asBinder().unlinkToDeath(record, 0);
+ }
+ });
+ }
+
+ @Override
+ public void startDiscovery(final IMediaRouteClientCallback callback,
+ final int seq, final List<MediaRouteSelector> selectors,
+ final int flags) {
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ ClientRecord record = mClientRecords.get(callback.asBinder());
+ if (record == null) {
+ return; // spurious
+ }
+
+ if (DEBUG) {
+ Log.d(TAG, "startDiscovery: client=" + record.getClientInfo()
+ + ", seq=" + seq + ", selectors=" + selectors
+ + ", flags=0x" + Integer.toHexString(flags));
+ }
+ record.startDiscovery(seq, selectors, flags);
+ }
+ });
+ }
+
+ @Override
+ public void stopDiscovery(final IMediaRouteClientCallback callback) {
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ ClientRecord record = mClientRecords.get(callback.asBinder());
+ if (record == null) {
+ return; // spurious
+ }
+
+ if (DEBUG) {
+ Log.d(TAG, "stopDiscovery: client=" + record.getClientInfo());
+ }
+ record.stopDiscovery();
+ }
+ });
+ }
+
+ @Override
+ public void connect(final IMediaRouteClientCallback callback,
+ final int seq, final String destinationId, final String routeId,
+ final int flags, final Bundle extras) {
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ ClientRecord record = mClientRecords.get(callback.asBinder());
+ if (record == null) {
+ return; // spurious
+ }
+
+ if (DEBUG) {
+ Log.d(TAG, "connect: client=" + record.getClientInfo()
+ + ", seq=" + seq + ", destinationId=" + destinationId
+ + ", routeId=" + routeId
+ + ", flags=0x" + Integer.toHexString(flags)
+ + ", extras=" + extras);
+ }
+ record.connect(seq, destinationId, routeId, flags, extras);
+ }
+ });
+ }
+
+ @Override
+ public void disconnect(final IMediaRouteClientCallback callback) {
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ ClientRecord record = mClientRecords.get(callback.asBinder());
+ if (record == null) {
+ return; // spurious
+ }
+
+ if (DEBUG) {
+ Log.d(TAG, "disconnect: client=" + record.getClientInfo());
+ }
+ record.disconnect();
+ }
+ });
+ }
+
+ @Override
+ public void pauseStream(final IMediaRouteClientCallback callback) {
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ ClientRecord record = mClientRecords.get(callback.asBinder());
+ if (record == null) {
+ return; // spurious
+ }
+
+ if (DEBUG) {
+ Log.d(TAG, "pauseStream: client=" + record.getClientInfo());
+ }
+ record.pauseStream();
+ }
+ });
+ }
+
+ @Override
+ public void resumeStream(final IMediaRouteClientCallback callback) {
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ ClientRecord record = mClientRecords.get(callback.asBinder());
+ if (record == null) {
+ return; // spurious
+ }
+
+ if (DEBUG) {
+ Log.d(TAG, "resumeStream: client=" + record.getClientInfo());
+ }
+ record.resumeStream();
+ }
+ });
+ }
+ }
+
+ // Must be accessed on handler
+ private final class ClientRecord implements IBinder.DeathRecipient {
+ private final IMediaRouteClientCallback mClientCallback;
+ private final ClientInfo mClient;
+ private final ClientSession mSession;
+
+ private int mDiscoverySeq;
+ private DiscoveryRequest mDiscoveryRequest;
+ private DiscoveryCallback mDiscoveryCallback;
+ private final ArrayMap<String, DestinationRecord> mDestinations =
+ new ArrayMap<String, DestinationRecord>();
+
+ private int mConnectionSeq;
+ private ConnectionRequest mConnectionRequest;
+ private ConnectionCallback mConnectionCallback;
+ private ConnectionInfo mConnection;
+ private boolean mConnectionPaused;
+
+ public ClientRecord(IMediaRouteClientCallback callback,
+ ClientInfo client, ClientSession session) {
+ mClientCallback = callback;
+ mClient = client;
+ mSession = session;
+ }
+
+ // Invoked on binder thread unlike all other methods in this class.
+ @Override
+ public void binderDied() {
+ mService.unregisterClient(mClientCallback, true);
+ }
+
+ public ClientInfo getClientInfo() {
+ return mClient;
+ }
+
+ public void release() {
+ stopDiscovery();
+ disconnect();
+ }
+
+ public void startDiscovery(int seq, List<MediaRouteSelector> selectors,
+ int flags) {
+ stopDiscovery();
+
+ mDiscoverySeq = seq;
+ mDiscoveryRequest = new DiscoveryRequest(selectors);
+ mDiscoveryRequest.setFlags(flags);
+ mDiscoveryCallback = new DiscoveryCallback(this);
+ boolean started = mSession.onStartDiscovery(mDiscoveryRequest, mDiscoveryCallback);
+ if (!started) {
+ dispatchDiscoveryFailed(mDiscoveryCallback,
+ MediaRouter.DISCOVERY_ERROR_ABORTED, null, null);
+ clearDiscovery();
+ }
+ }
+
+ public void stopDiscovery() {
+ if (mDiscoveryRequest != null) {
+ mSession.onStopDiscovery();
+ clearDiscovery();
+ }
+ }
+
+ private void clearDiscovery() {
+ mDestinations.clear();
+ mDiscoveryRequest = null;
+ mDiscoveryCallback = null;
+ }
+
+ public void connect(int seq, String destinationId, String routeId,
+ int flags, Bundle extras) {
+ disconnect();
+
+ mConnectionSeq = seq;
+ mConnectionCallback = new ConnectionCallback(this);
+
+ DestinationRecord destinationRecord = mDestinations.get(destinationId);
+ if (destinationRecord == null) {
+ Log.w(TAG, "Aborting connection to route since no matching destination "
+ + "was found in the list of known destinations: "
+ + "destinationId=" + destinationId);
+ dispatchConnectionFailed(mConnectionCallback,
+ MediaRouter.CONNECTION_ERROR_ABORTED, null, null);
+ clearConnection();
+ return;
+ }
+
+ RouteInfo route = destinationRecord.getRoute(routeId);
+ if (route == null) {
+ Log.w(TAG, "Aborting connection to route since no matching route "
+ + "was found in the list of known routes: "
+ + "destination=" + destinationRecord.destination
+ + ", routeId=" + routeId);
+ dispatchConnectionFailed(mConnectionCallback,
+ MediaRouter.CONNECTION_ERROR_ABORTED, null, null);
+ clearConnection();
+ return;
+ }
+
+ mConnectionRequest = new ConnectionRequest(route);
+ mConnectionRequest.setFlags(flags);
+ mConnectionRequest.setExtras(extras);
+ boolean started = mSession.onConnect(mConnectionRequest, mConnectionCallback);
+ if (!started) {
+ dispatchConnectionFailed(mConnectionCallback,
+ MediaRouter.CONNECTION_ERROR_ABORTED, null, null);
+ clearConnection();
+ }
+ }
+
+ public void disconnect() {
+ if (mConnectionRequest != null) {
+ mSession.onDisconnect();
+ clearConnection();
+ }
+ }
+
+ private void clearConnection() {
+ mConnectionRequest = null;
+ mConnectionCallback = null;
+ if (mConnection != null) {
+ mConnection.close();
+ mConnection = null;
+ }
+ mConnectionPaused = false;
+ }
+
+ public void pauseStream() {
+ if (mConnectionRequest != null && !mConnectionPaused) {
+ mConnectionPaused = true;
+ mSession.onPauseStream();
+ }
+ }
+
+ public void resumeStream() {
+ if (mConnectionRequest != null && mConnectionPaused) {
+ mConnectionPaused = false;
+ mSession.onResumeStream();
+ }
+ }
+
+ public void dispatchDestinationFound(DiscoveryCallback callback,
+ DestinationInfo destination, List<RouteInfo> routes) {
+ if (callback == mDiscoveryCallback) {
+ if (DEBUG) {
+ Log.d(TAG, "destinationFound: destination=" + destination
+ + ", routes=" + routes);
+ }
+ mDestinations.put(destination.getId(),
+ new DestinationRecord(destination, routes));
+
+ ParcelableDestinationInfo pdi = new ParcelableDestinationInfo();
+ pdi.id = destination.getId();
+ pdi.name = destination.getName();
+ pdi.description = destination.getDescription();
+ pdi.iconResourceId = destination.getIconResourceId();
+ pdi.extras = destination.getExtras();
+ ArrayList<ParcelableRouteInfo> pris = new ArrayList<ParcelableRouteInfo>();
+ for (RouteInfo route : routes) {
+ int selectorIndex = mDiscoveryRequest.getSelectors().indexOf(
+ route.getSelector());
+ if (selectorIndex < 0) {
+ Log.w(TAG, "Ignoring route because the selector does not match "
+ + "any of those that were originally supplied by the "
+ + "client's discovery request: destination=" + destination
+ + ", route=" + route);
+ continue;
+ }
+
+ ParcelableRouteInfo pri = new ParcelableRouteInfo();
+ pri.id = route.getId();
+ pri.selectorIndex = selectorIndex;
+ pri.features = route.getFeatures();
+ pri.protocols = route.getProtocols().toArray(
+ new String[route.getProtocols().size()]);
+ pri.extras = route.getExtras();
+ pris.add(pri);
+ }
+ try {
+ mClientCallback.onDestinationFound(mDiscoverySeq, pdi,
+ pris.toArray(new ParcelableRouteInfo[pris.size()]));
+ } catch (RemoteException ex) {
+ // binder death handled elsewhere
+ }
+ }
+ }
+
+ public void dispatchDestinationLost(DiscoveryCallback callback,
+ DestinationInfo destination) {
+ if (callback == mDiscoveryCallback) {
+ if (DEBUG) {
+ Log.d(TAG, "destinationLost: destination=" + destination);
+ }
+
+ if (mDestinations.get(destination.getId()).destination == destination) {
+ mDestinations.remove(destination.getId());
+ try {
+ mClientCallback.onDestinationLost(mDiscoverySeq, destination.getId());
+ } catch (RemoteException ex) {
+ // binder death handled elsewhere
+ }
+ }
+ }
+ }
+
+ public void dispatchDiscoveryFailed(DiscoveryCallback callback,
+ int error, CharSequence message, Bundle extras) {
+ if (callback == mDiscoveryCallback) {
+ if (DEBUG) {
+ Log.d(TAG, "discoveryFailed: error=" + error + ", message=" + message
+ + ", extras=" + extras);
+ }
+
+ try {
+ mClientCallback.onDiscoveryFailed(mDiscoverySeq, error, message, extras);
+ } catch (RemoteException ex) {
+ // binder death handled elsewhere
+ }
+ }
+ }
+
+ public void dispatchConnected(ConnectionCallback callback, ConnectionInfo connection) {
+ if (callback == mConnectionCallback) {
+ if (DEBUG) {
+ Log.d(TAG, "connected: connection=" + connection);
+ }
+ if (mConnection == null) {
+ mConnection = connection;
+
+ ParcelableConnectionInfo pci = new ParcelableConnectionInfo();
+ pci.audioAttributes = connection.getAudioAttributes();
+ pci.presentationDisplayId = connection.getPresentationDisplay() != null ?
+ connection.getPresentationDisplay().getDisplayId() : -1;
+ pci.protocolBinders = new IBinder[connection.getProtocols().size()];
+ for (int i = 0; i < pci.protocolBinders.length; i++) {
+ pci.protocolBinders[i] = connection.getProtocolBinder(i);
+ }
+ pci.extras = connection.getExtras();
+ try {
+ mClientCallback.onConnected(mConnectionSeq, pci);
+ } catch (RemoteException ex) {
+ // binder death handled elsewhere
+ }
+ } else {
+ Log.w(TAG, "Media route service called onConnected() while already "
+ + "connected.");
+ }
+ }
+ }
+
+ public void dispatchDisconnected(ConnectionCallback callback) {
+ if (callback == mConnectionCallback) {
+ if (DEBUG) {
+ Log.d(TAG, "disconnected");
+ }
+
+ if (mConnection != null) {
+ mConnection.close();
+ mConnection = null;
+
+ try {
+ mClientCallback.onDisconnected(mConnectionSeq);
+ } catch (RemoteException ex) {
+ // binder death handled elsewhere
+ }
+ }
+ }
+ }
+
+ public void dispatchConnectionFailed(ConnectionCallback callback,
+ int error, CharSequence message, Bundle extras) {
+ if (callback == mConnectionCallback) {
+ if (DEBUG) {
+ Log.d(TAG, "connectionFailed: error=" + error + ", message=" + message
+ + ", extras=" + extras);
+ }
+
+ try {
+ mClientCallback.onConnectionFailed(mConnectionSeq, error, message, extras);
+ } catch (RemoteException ex) {
+ // binder death handled elsewhere
+ }
+ }
+ }
+ }
+
+ private static final class DestinationRecord {
+ public final DestinationInfo destination;
+ public final List<RouteInfo> routes;
+
+ public DestinationRecord(DestinationInfo destination, List<RouteInfo> routes) {
+ this.destination = destination;
+ this.routes = routes;
+ }
+
+ public RouteInfo getRoute(String routeId) {
+ final int count = routes.size();
+ for (int i = 0; i < count; i++) {
+ RouteInfo route = routes.get(i);
+ if (route.getId().equals(routeId)) {
+ return route;
+ }
+ }
+ return null;
+ }
+ }
+}
diff --git a/media/java/android/media/routing/MediaRouter.java b/media/java/android/media/routing/MediaRouter.java
new file mode 100644
index 0000000..4f6d324
--- /dev/null
+++ b/media/java/android/media/routing/MediaRouter.java
@@ -0,0 +1,1886 @@
+/*
+ * 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.routing;
+
+import android.annotation.DrawableRes;
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.Presentation;
+import android.app.Service;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.content.pm.ServiceInfo;
+import android.graphics.drawable.Drawable;
+import android.hardware.display.DisplayManager;
+import android.media.AudioAttributes;
+import android.media.AudioManager;
+import android.media.AudioTrack;
+import android.media.VolumeProvider;
+import android.media.session.MediaController;
+import android.media.session.MediaSession;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.IInterface;
+import android.text.TextUtils;
+import android.util.ArrayMap;
+import android.view.Display;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.InvocationTargetException;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Media router allows applications to discover, connect to, control,
+ * and send content to nearby media devices known as destinations.
+ * <p>
+ * There are generally two participants involved in media routing: an
+ * application that wants to send media content to a destination and a
+ * {@link MediaRouteService media route service} that provides the
+ * service of transporting that content where it needs to go on behalf of the
+ * application.
+ * </p><p>
+ * To send media content to a destination, the application must ask the system
+ * to discover available routes to destinations that provide certain capabilities,
+ * establish a connection to a route, then send messages through the connection to
+ * control the routing of audio and video streams, launch remote applications,
+ * and invoke other functions of the destination.
+ * </p><p>
+ * Media router objects are thread-safe.
+ * </p>
+ *
+ * <h3>Destinations</h3>
+ * <p>
+ * The media devices to which an application may send media content are referred
+ * to in the API as destinations. Each destination therefore represents a single
+ * independent device such as a speaker or TV set. Destinations are given meaningful
+ * names and descriptions to help the user associate them with devices in their
+ * environment.
+ * </p><p>
+ * Destinations may be local or remote and may be accessed through various means,
+ * often wirelessly. The user may install media route services to enable
+ * media applications to connect to a variety of destinations with different
+ * capabilities.
+ * </p>
+ *
+ * <h3>Routes</h3>
+ * <p>
+ * Routes represent possible usages or means of reaching and interacting with
+ * a destination. Since destinations may support many different features, they may
+ * each offer multiple routes for applications to choose from based on their needs.
+ * For example, one route might express the ability to stream locally rendered audio
+ * and video to the device; another route might express the ability to send a URL for
+ * the destination to download from the network and play all by itself.
+ * </p><p>
+ * Routes are discovered according to the set of capabilities that
+ * an application or the system is seeking to use at a particular time. For example,
+ * if an application wants to stream music to a destination then it will ask the
+ * {@link MediaRouter} to find routes to destinations can stream music and ignore
+ * all other destinations that cannot.
+ * </p><p>
+ * In general, the application will inspect the set of routes that have been
+ * offered then connect to the most appropriate route for its desired purpose.
+ * </p>
+ *
+ * <h3>Route Selection</h3>
+ * <p>
+ * When the user open the media route chooser activity, the system will display
+ * a list of nearby media destinations which have been discovered. After the
+ * choice is made the application may connect to one of the routes offered by
+ * this destination and begin communicating with the destination.
+ * </p><p>
+ * Destinations are located through a process called discovery. During discovery,
+ * the system will start installed {@link MediaRouteService media route services}
+ * to scan the network for nearby devices that offer the kinds of capabilities that the
+ * application is seeking to use. The application specifies the capabilities it requires by
+ * adding {@link MediaRouteSelector media route selectors} to the media router
+ * using the {@link #addSelector} method. Only destinations that provide routes
+ * which satisfy at least one of these media route selectors will be discovered.
+ * </p><p>
+ * Once the user has selected a destination, the application will be given a chance
+ * to choose one of the routes to which it would like to connect. The application
+ * may switch to a different route from the same destination at a later time but
+ * in order to connect to a new destination, the application must once again launch
+ * the media route chooser activity to ask the user to choose a destination.
+ * </p>
+ *
+ * <h3>Route Protocols</h3>
+ * <p>
+ * Route protocols express capabilities offered by routes. Each media route selector
+ * must specify at least one required protocol by which the routes will be selected.
+ * </p><p>
+ * The framework provides several predefined <code>MediaRouteProtocols</code> which are
+ * defined in the <code>android-support-media-protocols.jar</code> support library.
+ * Applications must statically link this library to make use of these protocols.
+ * </p><p>
+ * The static library approach is used to enable ongoing extension and refinement
+ * of protocols in the SDK and interoperability with the media router implementation
+ * for older platform versions which is offered by the framework support library.
+ * </p><p>
+ * Media route services may also define custom media route protocols of their own
+ * to enable applications to access specialized capabilities of certain destinations
+ * assuming they have linked in the required protocol code.
+ * </p><p>
+ * Refer to <code>android.support.media.protocols.MediaRouteProtocol</code> for more information.
+ * </p>
+ *
+ * <h3>Connections</h3>
+ * <p>
+ * After connecting to a media route, the application can send commands to
+ * the route using any of the protocols that it requested. If the route supports live
+ * audio or video streaming then the application can create an {@link AudioTrack} or
+ * {@link Presentation} to route locally generated content to the destination.
+ * </p>
+ *
+ * <h3>Delegation</h3>
+ * <p>
+ * The creator of the media router is responsible for establishing the policy for
+ * discovering and connecting to destinations. UI components may observe the state
+ * of the media router by {@link #createDelegate creating} a {@link Delegate}.
+ * </p><p>
+ * The media router should also be attached to the {@link MediaSession media session}
+ * that is handling media playback lifecycle. This will allow
+ * authorized {@link MediaController media controllers}, possibly running in other
+ * processes, to provide UI to examine and change the media destination by
+ * {@link MediaController#createMediaRouterDelegate creating} a {@link Delegate}
+ * for the media router associated with the session.
+ * </p>
+ */
+public final class MediaRouter {
+ private final DisplayManager mDisplayManager;
+
+ private final Object mLock = new Object();
+
+ private RoutingCallback mRoutingCallback;
+ private Handler mRoutingCallbackHandler;
+
+ private boolean mReleased;
+ private int mDiscoveryState;
+ private int mConnectionState;
+ private final ArrayList<MediaRouteSelector> mSelectors =
+ new ArrayList<MediaRouteSelector>();
+ private final ArrayMap<DestinationInfo, List<RouteInfo>> mDiscoveredDestinations =
+ new ArrayMap<DestinationInfo, List<RouteInfo>>();
+ private RouteInfo mSelectedRoute;
+ private ConnectionInfo mConnection;
+
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(value = { DISCOVERY_STATE_STOPPED, DISCOVERY_STATE_STARTED })
+ public @interface DiscoveryState { }
+
+ /**
+ * Discovery state: Discovery is not currently in progress.
+ */
+ public static final int DISCOVERY_STATE_STOPPED = 0;
+
+ /**
+ * Discovery state: Discovery is being performed.
+ */
+ public static final int DISCOVERY_STATE_STARTED = 1;
+
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(flag = true, value = { DISCOVERY_FLAG_BACKGROUND })
+ public @interface DiscoveryFlags { }
+
+ /**
+ * Discovery flag: Indicates that the client has requested passive discovery in
+ * the background. The media route service should try to use less power and rely
+ * more on its internal caches to minimize its impact.
+ */
+ public static final int DISCOVERY_FLAG_BACKGROUND = 1 << 0;
+
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(value = { DISCOVERY_ERROR_UNKNOWN, DISCOVERY_ERROR_ABORTED,
+ DISCOVERY_ERROR_NO_CONNECTIVITY })
+ public @interface DiscoveryError { }
+
+ /**
+ * Discovery error: Unknown error; refer to the error message for details.
+ */
+ public static final int DISCOVERY_ERROR_UNKNOWN = 0;
+
+ /**
+ * Discovery error: The media router or media route service has decided not to
+ * handle the discovery request for some reason.
+ */
+ public static final int DISCOVERY_ERROR_ABORTED = 1;
+
+ /**
+ * Discovery error: The media route service is unable to perform discovery
+ * due to a lack of connectivity such as because the radio is disabled.
+ */
+ public static final int DISCOVERY_ERROR_NO_CONNECTIVITY = 2;
+
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(value = { CONNECTION_STATE_DISCONNECTED, CONNECTION_STATE_CONNECTING,
+ CONNECTION_STATE_CONNECTED })
+ public @interface ConnectionState { }
+
+ /**
+ * Connection state: No destination has been selected. Media content should
+ * be sent to the default output.
+ */
+ public static final int CONNECTION_STATE_DISCONNECTED = 0;
+
+ /**
+ * Connection state: The application is in the process of connecting to
+ * a route offered by the selected destination.
+ */
+ public static final int CONNECTION_STATE_CONNECTING = 1;
+
+ /**
+ * Connection state: The application has connected to a route offered by
+ * the selected destination.
+ */
+ public static final int CONNECTION_STATE_CONNECTED = 2;
+
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(flag = true, value = { CONNECTION_FLAG_BARGE })
+ public @interface ConnectionFlags { }
+
+ /**
+ * Connection flag: Indicates that the client has requested to barge in and evict
+ * other clients that might have already connected to the destination and that
+ * would otherwise prevent this client from connecting. When this flag is not
+ * set, the media route service should be polite and report
+ * {@link MediaRouter#CONNECTION_ERROR_BUSY} in case the destination is
+ * already occupied and cannot accept additional connections.
+ */
+ public static final int CONNECTION_FLAG_BARGE = 1 << 0;
+
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(value = { CONNECTION_ERROR_UNKNOWN, CONNECTION_ERROR_ABORTED,
+ CONNECTION_ERROR_UNAUTHORIZED, CONNECTION_ERROR_UNAUTHORIZED,
+ CONNECTION_ERROR_BUSY, CONNECTION_ERROR_TIMEOUT, CONNECTION_ERROR_BROKEN })
+ public @interface ConnectionError { }
+
+ /**
+ * Connection error: Unknown error; refer to the error message for details.
+ */
+ public static final int CONNECTION_ERROR_UNKNOWN = 0;
+
+ /**
+ * Connection error: The media router or media route service has decided not to
+ * handle the connection request for some reason.
+ */
+ public static final int CONNECTION_ERROR_ABORTED = 1;
+
+ /**
+ * Connection error: The device has refused the connection from this client.
+ * This error should be avoided because the media route service should attempt
+ * to filter out devices that the client cannot access as it performs discovery
+ * on behalf of that client.
+ */
+ public static final int CONNECTION_ERROR_UNAUTHORIZED = 2;
+
+ /**
+ * Connection error: The device is unreachable over the network.
+ */
+ public static final int CONNECTION_ERROR_UNREACHABLE = 3;
+
+ /**
+ * Connection error: The device is already busy serving another client and
+ * the connection request did not ask to barge in.
+ */
+ public static final int CONNECTION_ERROR_BUSY = 4;
+
+ /**
+ * Connection error: A timeout occurred during connection.
+ */
+ public static final int CONNECTION_ERROR_TIMEOUT = 5;
+
+ /**
+ * Connection error: The connection to the device was severed unexpectedly.
+ */
+ public static final int CONNECTION_ERROR_BROKEN = 6;
+
+ /**
+ * Connection error: The connection was terminated because a different client barged
+ * in and took control of the destination.
+ */
+ public static final int CONNECTION_ERROR_BARGED = 7;
+
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(value = { DISCONNECTION_REASON_APPLICATION_REQUEST,
+ DISCONNECTION_REASON_USER_REQUEST, DISCONNECTION_REASON_ERROR })
+ public @interface DisconnectionReason { }
+
+ /**
+ * Disconnection reason: The application requested disconnection itself.
+ */
+ public static final int DISCONNECTION_REASON_APPLICATION_REQUEST = 0;
+
+ /**
+ * Disconnection reason: The user requested disconnection.
+ */
+ public static final int DISCONNECTION_REASON_USER_REQUEST = 1;
+
+ /**
+ * Disconnection reason: An error occurred.
+ */
+ public static final int DISCONNECTION_REASON_ERROR = 2;
+
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(flag = true, value = { ROUTE_FEATURE_LIVE_AUDIO, ROUTE_FEATURE_LIVE_VIDEO })
+ public @interface RouteFeatures { }
+
+ /**
+ * Route feature: Live audio.
+ * <p>
+ * A route that supports live audio streams audio rendered by the application
+ * to the destination.
+ * </p><p>
+ * To take advantage of live audio routing, the application must render its
+ * media using the audio attributes specified by {@link #getPreferredAudioAttributes}.
+ * </p>
+ *
+ * @see #getPreferredAudioAttributes
+ * @see android.media.AudioAttributes
+ */
+ public static final int ROUTE_FEATURE_LIVE_AUDIO = 1 << 0;
+
+ /**
+ * Route feature: Live video.
+ * <p>
+ * A route that supports live video streams video rendered by the application
+ * to the destination.
+ * </p><p>
+ * To take advantage of live video routing, the application must render its
+ * media to a {@link android.app.Presentation presentation window} on the
+ * display specified by {@link #getPreferredPresentationDisplay}.
+ * </p>
+ *
+ * @see #getPreferredPresentationDisplay
+ * @see android.app.Presentation
+ */
+ public static final int ROUTE_FEATURE_LIVE_VIDEO = 1 << 1;
+
+ /**
+ * Creates a media router.
+ *
+ * @param context The context with which the router is associated.
+ */
+ public MediaRouter(@NonNull Context context) {
+ if (context == null) {
+ throw new IllegalArgumentException("context must not be null");
+ }
+
+ mDisplayManager = (DisplayManager)context.getSystemService(Context.DISPLAY_SERVICE);
+ }
+
+ /** @hide */
+ public IMediaRouter getBinder() {
+ // todo
+ return null;
+ }
+
+ /**
+ * Disconnects from the selected destination and releases the media router.
+ * <p>
+ * This method should be called by the application when it no longer requires
+ * the media router to ensure that all bound resources may be cleaned up.
+ * </p>
+ */
+ public void release() {
+ synchronized (mLock) {
+ mReleased = true;
+ // todo
+ }
+ }
+
+ /**
+ * Returns true if the media router has been released.
+ */
+ public boolean isReleased() {
+ synchronized (mLock) {
+ return mReleased;
+ }
+ }
+
+ /**
+ * Gets the current route discovery state.
+ *
+ * @return The current discovery state: one of {@link #DISCOVERY_STATE_STOPPED},
+ * {@link #DISCOVERY_STATE_STARTED}.
+ */
+ public @DiscoveryState int getDiscoveryState() {
+ synchronized (mLock) {
+ return mDiscoveryState;
+ }
+ }
+
+ /**
+ * Gets the current route connection state.
+ *
+ * @return The current state: one of {@link #CONNECTION_STATE_DISCONNECTED},
+ * {@link #CONNECTION_STATE_CONNECTING} or {@link #CONNECTION_STATE_CONNECTED}.
+ */
+ public @ConnectionState int getConnectionState() {
+ synchronized (mLock) {
+ return mConnectionState;
+ }
+ }
+
+ /**
+ * Creates a media router delegate through which the destination of the media
+ * router may be controlled.
+ * <p>
+ * This is the point of entry for UI code that initiates discovery and
+ * connection to routes.
+ * </p>
+ */
+ public @NonNull Delegate createDelegate() {
+ return null; // todo
+ }
+
+ /**
+ * Sets a callback to participate in route discovery, filtering, and connection
+ * establishment.
+ *
+ * @param callback The callback to set, or null if none.
+ * @param handler The handler to receive callbacks, or null to use the current thread.
+ */
+ public void setRoutingCallback(@Nullable RoutingCallback callback,
+ @Nullable Handler handler) {
+ synchronized (mLock) {
+ if (callback == null) {
+ mRoutingCallback = null;
+ mRoutingCallbackHandler = null;
+ } else {
+ mRoutingCallback = callback;
+ mRoutingCallbackHandler = handler != null ? handler : new Handler();
+ }
+ }
+ }
+
+ /**
+ * Adds a media route selector to use to find destinations that have
+ * routes with the specified capabilities during route discovery.
+ */
+ public void addSelector(@NonNull MediaRouteSelector selector) {
+ if (selector == null) {
+ throw new IllegalArgumentException("selector must not be null");
+ }
+
+ synchronized (mLock) {
+ if (!mSelectors.contains(selector)) {
+ mSelectors.add(selector);
+ // todo
+ }
+ }
+ }
+
+ /**
+ * Removes a media route selector.
+ */
+ public void removeSelector(@NonNull MediaRouteSelector selector) {
+ if (selector == null) {
+ throw new IllegalArgumentException("selector must not be null");
+ }
+
+ synchronized (mLock) {
+ if (mSelectors.remove(selector)) {
+ // todo
+ }
+ }
+ }
+
+ /**
+ * Removes all media route selectors.
+ * <p>
+ * Note that at least one selector must be added in order to perform discovery.
+ * </p>
+ */
+ public void clearSelectors() {
+ synchronized (mLock) {
+ if (!mSelectors.isEmpty()) {
+ mSelectors.clear();
+ // todo
+ }
+ }
+ }
+
+ /**
+ * Gets a list of all media route selectors to consider during discovery.
+ */
+ public @NonNull List<MediaRouteSelector> getSelectors() {
+ synchronized (mLock) {
+ return new ArrayList<MediaRouteSelector>(mSelectors);
+ }
+ }
+
+ /**
+ * Gets the connection to the currently selected route.
+ *
+ * @return The connection to the currently selected route, or null if not connected.
+ */
+ public @NonNull ConnectionInfo getConnection() {
+ synchronized (mLock) {
+ return mConnection;
+ }
+ }
+
+ /**
+ * Gets the list of discovered destinations.
+ * <p>
+ * This list is only valid while discovery is running and is null otherwise.
+ * </p>
+ *
+ * @return The list of discovered destinations, or null if discovery is not running.
+ */
+ public @NonNull List<DestinationInfo> getDiscoveredDestinations() {
+ synchronized (mLock) {
+ if (mDiscoveryState == DISCOVERY_STATE_STARTED) {
+ return new ArrayList<DestinationInfo>(mDiscoveredDestinations.keySet());
+ }
+ return null;
+ }
+ }
+
+ /**
+ * Gets the list of discovered routes for a particular destination.
+ * <p>
+ * This list is only valid while discovery is running and is null otherwise.
+ * </p>
+ *
+ * @param destination The destination for which to get the list of discovered routes.
+ * @return The list of discovered routes for the destination, or null if discovery
+ * is not running.
+ */
+ public @NonNull List<RouteInfo> getDiscoveredRoutes(@NonNull DestinationInfo destination) {
+ if (destination == null) {
+ throw new IllegalArgumentException("destination must not be null");
+ }
+ synchronized (mLock) {
+ if (mDiscoveryState == DISCOVERY_STATE_STARTED) {
+ List<RouteInfo> routes = mDiscoveredDestinations.get(destination);
+ if (routes != null) {
+ return new ArrayList<RouteInfo>(routes);
+ }
+ }
+ return null;
+ }
+ }
+
+ /**
+ * Gets the destination that has been selected.
+ *
+ * @return The selected destination, or null if disconnected.
+ */
+ public @Nullable DestinationInfo getSelectedDestination() {
+ synchronized (mLock) {
+ return mSelectedRoute != null ? mSelectedRoute.getDestination() : null;
+ }
+ }
+
+ /**
+ * Gets the route that has been selected.
+ *
+ * @return The selected destination, or null if disconnected.
+ */
+ public @Nullable RouteInfo getSelectedRoute() {
+ synchronized (mLock) {
+ return mSelectedRoute;
+ }
+ }
+
+ /**
+ * Gets the preferred audio attributes that should be used to stream live audio content
+ * based on the connected route.
+ * <p>
+ * Use an {@link AudioTrack} to send audio content to the destination with these
+ * audio attributes.
+ * </p><p>
+ * The preferred audio attributes may change when a connection is established but it
+ * will remain constant until disconnected.
+ * </p>
+ *
+ * @return The preferred audio attributes to use. When connected, returns the
+ * route's audio attributes or null if it does not support live audio streaming.
+ * Otherwise returns audio attributes associated with {@link AudioAttributes#USAGE_MEDIA}.
+ */
+ public @Nullable AudioAttributes getPreferredAudioAttributes() {
+ synchronized (mLock) {
+ if (mConnection != null) {
+ return mConnection.getAudioAttributes();
+ }
+ return new AudioAttributes.Builder()
+ .setLegacyStreamType(AudioManager.STREAM_MUSIC)
+ .build();
+ }
+ }
+
+ /**
+ * Gets the preferred presentation display that should be used to stream live video content
+ * based on the connected route.
+ * <p>
+ * Use a {@link Presentation} to send video content to the destination with this display.
+ * </p><p>
+ * The preferred presentation display may change when a connection is established but it
+ * will remain constant until disconnected.
+ * </p>
+ *
+ * @return The preferred presentation display to use. When connected, returns
+ * the route's presentation display or null if it does not support live video
+ * streaming. Otherwise returns the first available
+ * {@link DisplayManager#DISPLAY_CATEGORY_PRESENTATION presentation display},
+ * such as a mirrored wireless or HDMI display or null if none.
+ */
+ public @Nullable Display getPreferredPresentationDisplay() {
+ synchronized (mLock) {
+ if (mConnection != null) {
+ return mConnection.getPresentationDisplay();
+ }
+ Display[] displays = mDisplayManager.getDisplays(
+ DisplayManager.DISPLAY_CATEGORY_PRESENTATION);
+ return displays.length != 0 ? displays[0] : null;
+ }
+ }
+
+ /**
+ * Gets the preferred volume provider that should be used to control the volume
+ * of content rendered on the currently selected route.
+ * <p>
+ * The preferred volume provider may change when a connection is established but it
+ * will remain the same until disconnected.
+ * </p>
+ *
+ * @return The preferred volume provider to use, or null if the currently
+ * selected route does not support remote volume adjustment or if the connection
+ * is not yet established. If no route is selected, returns null to indicate
+ * that system volume control should be used.
+ */
+ public @Nullable VolumeProvider getPreferredVolumeProvider() {
+ synchronized (mLock) {
+ if (mConnection != null) {
+ return mConnection.getVolumeProvider();
+ }
+ return null;
+ }
+ }
+
+ /**
+ * Requests to pause streaming of live audio or video routes.
+ * Should be called when the application is going into the background and is
+ * no longer rendering content locally.
+ * <p>
+ * This method does nothing unless a connection has been established.
+ * </p>
+ */
+ public void pauseStream() {
+ // todo
+ }
+
+ /**
+ * Requests to resume streaming of live audio or video routes.
+ * May be called when the application is returning to the foreground and is
+ * about to resume rendering content locally.
+ * <p>
+ * This method does nothing unless a connection has been established.
+ * </p>
+ */
+ public void resumeStream() {
+ // todo
+ }
+
+ /**
+ * This class is used by UI components to let the user discover and
+ * select a destination to which the media router should connect.
+ * <p>
+ * This API has somewhat more limited functionality than the {@link MediaRouter}
+ * itself because it is designed to allow applications to control
+ * the destination of media router instances that belong to other processes.
+ * </p><p>
+ * To control the destination of your own media router, call
+ * {@link #createDelegate} to obtain a local delegate object.
+ * </p><p>
+ * To control the destination of a media router that belongs to another process,
+ * first obtain a {@link MediaController} that is associated with the media playback
+ * that is occurring in that process, then call
+ * {@link MediaController#createMediaRouterDelegate} to obtain an instance of
+ * its destination controls. Note that special permissions may be required to
+ * obtain the {@link MediaController} instance in the first place.
+ * </p>
+ */
+ public static final class Delegate {
+ /**
+ * Returns true if the media router has been released.
+ */
+ public boolean isReleased() {
+ // todo
+ return false;
+ }
+
+ /**
+ * Gets the current route discovery state.
+ *
+ * @return The current discovery state: one of {@link #DISCOVERY_STATE_STOPPED},
+ * {@link #DISCOVERY_STATE_STARTED}.
+ */
+ public @DiscoveryState int getDiscoveryState() {
+ // todo
+ return -1;
+ }
+
+ /**
+ * Gets the current route connection state.
+ *
+ * @return The current state: one of {@link #CONNECTION_STATE_DISCONNECTED},
+ * {@link #CONNECTION_STATE_CONNECTING} or {@link #CONNECTION_STATE_CONNECTED}.
+ */
+ public @ConnectionState int getConnectionState() {
+ // todo
+ return -1;
+ }
+
+ /**
+ * Gets the currently selected destination.
+ *
+ * @return The destination information, or null if none.
+ */
+ public @Nullable DestinationInfo getSelectedDestination() {
+ return null;
+ }
+
+ /**
+ * Gets the list of discovered destinations.
+ * <p>
+ * This list is only valid while discovery is running and is null otherwise.
+ * </p>
+ *
+ * @return The list of discovered destinations, or null if discovery is not running.
+ */
+ public @NonNull List<DestinationInfo> getDiscoveredDestinations() {
+ return null;
+ }
+
+ /**
+ * Adds a callback to receive state changes.
+ *
+ * @param callback The callback to set, or null if none.
+ * @param handler The handler to receive callbacks, or null to use the current thread.
+ */
+ public void addStateCallback(@Nullable StateCallback callback,
+ @Nullable Handler handler) {
+ if (callback == null) {
+ throw new IllegalArgumentException("callback must not be null");
+ }
+ if (handler == null) {
+ handler = new Handler();
+ }
+ // todo
+ }
+
+ /**
+ * Removes a callback for state changes.
+ *
+ * @param callback The callback to set, or null if none.
+ */
+ public void removeStateCallback(@Nullable StateCallback callback) {
+ // todo
+ }
+
+ /**
+ * Starts performing discovery.
+ * <p>
+ * Performing discovery is expensive. Make sure to call {@link #stopDiscovery}
+ * as soon as possible once a new destination has been selected to allow the system
+ * to stop services associated with discovery.
+ * </p>
+ *
+ * @param flags The discovery flags, such as {@link MediaRouter#DISCOVERY_FLAG_BACKGROUND}.
+ */
+ public void startDiscovery(@DiscoveryFlags int flags) {
+ // todo
+ }
+
+ /**
+ * Stops performing discovery.
+ */
+ public void stopDiscovery() {
+ // todo
+ }
+
+ /**
+ * Connects to a destination during route discovery.
+ * <p>
+ * This method may only be called while route discovery is active and the
+ * destination appears in the
+ * {@link #getDiscoveredDestinations list of discovered destinations}.
+ * If the media router is already connected to a route then it will first disconnect
+ * from the current route then connect to the new route.
+ * </p>
+ *
+ * @param destination The destination to which the media router should connect.
+ * @param flags The connection flags, such as {@link MediaRouter#CONNECTION_FLAG_BARGE}.
+ */
+ public void connect(@NonNull DestinationInfo destination, @DiscoveryFlags int flags) {
+ // todo
+ }
+
+ /**
+ * Disconnects from the currently selected destination.
+ * <p>
+ * Does nothing if not currently connected.
+ * </p>
+ *
+ * @param reason The reason for the disconnection: one of
+ * {@link #DISCONNECTION_REASON_APPLICATION_REQUEST},
+ * {@link #DISCONNECTION_REASON_USER_REQUEST}, or {@link #DISCONNECTION_REASON_ERROR}.
+ */
+ public void disconnect(@DisconnectionReason int reason) {
+ // todo
+ }
+ }
+
+ /**
+ * Describes immutable properties of a connection to a route.
+ */
+ public static final class ConnectionInfo {
+ private final RouteInfo mRoute;
+ private final AudioAttributes mAudioAttributes;
+ private final Display mPresentationDisplay;
+ private final VolumeProvider mVolumeProvider;
+ private final IBinder[] mProtocolBinders;
+ private final Object[] mProtocolInstances;
+ private final Bundle mExtras;
+ private final ArrayList<Closeable> mCloseables;
+
+ private static final Class<?>[] MEDIA_ROUTE_PROTOCOL_CTOR_PARAMETERS =
+ new Class<?>[] { IBinder.class };
+
+ ConnectionInfo(RouteInfo route,
+ AudioAttributes audioAttributes, Display display,
+ VolumeProvider volumeProvider, IBinder[] protocolBinders,
+ Bundle extras, ArrayList<Closeable> closeables) {
+ mRoute = route;
+ mAudioAttributes = audioAttributes;
+ mPresentationDisplay = display;
+ mVolumeProvider = volumeProvider;
+ mProtocolBinders = protocolBinders;
+ mProtocolInstances = new Object[mProtocolBinders.length];
+ mExtras = extras;
+ mCloseables = closeables;
+ }
+
+ /**
+ * Gets the route that is connected.
+ */
+ public @NonNull RouteInfo getRoute() {
+ return mRoute;
+ }
+
+ /**
+ * Gets the audio attributes which the client should use to stream audio
+ * to the destination, or null if the route does not support live audio streaming.
+ */
+ public @Nullable AudioAttributes getAudioAttributes() {
+ return mAudioAttributes;
+ }
+
+ /**
+ * Gets the display which the client should use to stream video to the
+ * destination using a {@link Presentation}, or null if the route does not
+ * support live video streaming.
+ */
+ public @Nullable Display getPresentationDisplay() {
+ return mPresentationDisplay;
+ }
+
+ /**
+ * Gets the route's volume provider, or null if none.
+ */
+ public @Nullable VolumeProvider getVolumeProvider() {
+ return mVolumeProvider;
+ }
+
+ /**
+ * Gets the set of supported route features.
+ */
+ public @RouteFeatures int getFeatures() {
+ return mRoute.getFeatures();
+ }
+
+ /**
+ * Gets the list of supported route protocols.
+ * <p>
+ * Refer to <code>android.support.media.protocols.MediaRouteProtocol</code>
+ * for more information.
+ * </p>
+ */
+ public @NonNull List<String> getProtocols() {
+ return mRoute.getProtocols();
+ }
+
+ /**
+ * Gets an instance of a route protocol object that wraps the protocol binder
+ * and provides easy access to the protocol's functionality.
+ * <p>
+ * This is a convenience method which invokes {@link #getProtocolBinder(String)}
+ * using the name of the provided class then passes the resulting {@link IBinder}
+ * to a single-argument constructor of that class.
+ * </p><p>
+ * Refer to <code>android.support.media.protocols.MediaRouteProtocol</code>
+ * for more information.
+ * </p>
+ */
+ @SuppressWarnings("unchecked")
+ public @Nullable <T> T getProtocolObject(Class<T> clazz) {
+ int index = getProtocols().indexOf(clazz.getName());
+ if (index < 0) {
+ return null;
+ }
+ if (mProtocolInstances[index] == null && mProtocolBinders[index] != null) {
+ final Constructor<T> ctor;
+ try {
+ ctor = clazz.getConstructor(MEDIA_ROUTE_PROTOCOL_CTOR_PARAMETERS);
+ } catch (NoSuchMethodException ex) {
+ throw new RuntimeException("Could not find public constructor "
+ + "with IBinder argument in protocol class: " + clazz.getName(), ex);
+ }
+ try {
+ mProtocolInstances[index] = ctor.newInstance(mProtocolBinders[index]);
+ } catch (InstantiationException | IllegalAccessException
+ | InvocationTargetException ex) {
+ throw new RuntimeException("Could create instance of protocol class: "
+ + clazz.getName(), ex);
+ }
+ }
+ return (T)mProtocolInstances[index];
+ }
+
+ /**
+ * Gets the {@link IBinder} that provides access to the specified route protocol
+ * or null if the protocol is not supported.
+ * <p>
+ * Refer to <code>android.support.media.protocols.MediaRouteProtocol</code>
+ * for more information.
+ * </p>
+ */
+ public @Nullable IBinder getProtocolBinder(@NonNull String name) {
+ int index = getProtocols().indexOf(name);
+ return index >= 0 ? mProtocolBinders[index] : null;
+ }
+
+ /**
+ * Gets the {@link IBinder} that provides access to the specified route protocol
+ * at the given index in the protocol list or null if the protocol is not supported.
+ * <p>
+ * Refer to <code>android.support.media.protocols.MediaRouteProtocol</code>
+ * for more information.
+ * </p>
+ */
+ public @Nullable IBinder getProtocolBinder(int index) {
+ return mProtocolBinders[index];
+ }
+
+ /**
+ * Gets optional extra media route service or protocol specific information about
+ * the connection. Use the service or protocol name as the prefix for
+ * any extras to avoid namespace collisions.
+ */
+ public @Nullable Bundle getExtras() {
+ return mExtras;
+ }
+
+ /**
+ * Closes all closeables associated with the connection when the connection
+ * is being torn down.
+ */
+ void close() {
+ final int count = mCloseables.size();
+ for (int i = 0; i < count; i++) {
+ try {
+ mCloseables.get(i).close();
+ } catch (IOException ex) {
+ }
+ }
+ }
+
+ @Override
+ public @NonNull String toString() {
+ return "ConnectionInfo{ route=" + mRoute
+ + ", audioAttributes=" + mAudioAttributes
+ + ", presentationDisplay=" + mPresentationDisplay
+ + ", volumeProvider=" + mVolumeProvider
+ + ", protocolBinders=" + mProtocolBinders + " }";
+ }
+
+ /**
+ * Builds {@link ConnectionInfo} objects.
+ */
+ public static final class Builder {
+ private final RouteInfo mRoute;
+ private AudioAttributes mAudioAttributes;
+ private Display mPresentationDisplay;
+ private VolumeProvider mVolumeProvider;
+ private final IBinder[] mProtocols;
+ private Bundle mExtras;
+ private final ArrayList<Closeable> mCloseables = new ArrayList<Closeable>();
+
+ /**
+ * Creates a builder for connection information.
+ *
+ * @param route The route that is connected.
+ */
+ public Builder(@NonNull RouteInfo route) {
+ if (route == null) {
+ throw new IllegalArgumentException("route");
+ }
+ mRoute = route;
+ mProtocols = new IBinder[route.getProtocols().size()];
+ }
+
+ /**
+ * Sets the audio attributes which the client should use to stream audio
+ * to the destination, or null if the route does not support live audio streaming.
+ */
+ public @NonNull Builder setAudioAttributes(
+ @Nullable AudioAttributes audioAttributes) {
+ mAudioAttributes = audioAttributes;
+ return this;
+ }
+
+ /**
+ * Sets the display which the client should use to stream video to the
+ * destination using a {@link Presentation}, or null if the route does not
+ * support live video streaming.
+ */
+ public @NonNull Builder setPresentationDisplay(@Nullable Display display) {
+ mPresentationDisplay = display;
+ return this;
+ }
+
+ /**
+ * Sets the route's volume provider, or null if none.
+ */
+ public @NonNull Builder setVolumeProvider(@Nullable VolumeProvider provider) {
+ mVolumeProvider = provider;
+ return this;
+ }
+
+ /**
+ * Sets the binder stub of a supported route protocol using
+ * the protocol's fully qualified class name. The protocol must be one
+ * of those that was indicated as being supported by the route.
+ * <p>
+ * If the stub implements {@link Closeable} then it will automatically
+ * be closed when the client disconnects from the route.
+ * </p><p>
+ * Refer to <code>android.support.media.protocols.MediaRouteProtocol</code>
+ * for more information.
+ * </p>
+ */
+ public @NonNull Builder setProtocolStub(@NonNull Class<?> clazz,
+ @NonNull IInterface stub) {
+ if (clazz == null) {
+ throw new IllegalArgumentException("clazz must not be null");
+ }
+ if (stub == null) {
+ throw new IllegalArgumentException("stub must not be null");
+ }
+ if (stub instanceof Closeable) {
+ mCloseables.add((Closeable)stub);
+ }
+ return setProtocolBinder(clazz.getName(), stub.asBinder());
+ }
+
+ /**
+ * Sets the binder interface of a supported route protocol by name.
+ * The protocol must be one of those that was indicated as being supported
+ * by the route.
+ * <p>
+ * Refer to <code>android.support.media.protocols.MediaRouteProtocol</code>
+ * for more information.
+ * </p>
+ */
+ public @NonNull Builder setProtocolBinder(@NonNull String name,
+ @NonNull IBinder binder) {
+ if (TextUtils.isEmpty(name)) {
+ throw new IllegalArgumentException("name must not be null or empty");
+ }
+ if (binder == null) {
+ throw new IllegalArgumentException("binder must not be null");
+ }
+ int index = mRoute.getProtocols().indexOf(name);
+ if (index < 0) {
+ throw new IllegalArgumentException("name must specify a protocol that "
+ + "the route actually declared that it supports: "
+ + "name=" + name + ", protocols=" + mRoute.getProtocols());
+ }
+ mProtocols[index] = binder;
+ return this;
+ }
+
+ /**
+ * Sets optional extra media route service or protocol specific information about
+ * the connection. Use the service or protocol name as the prefix for
+ * any extras to avoid namespace collisions.
+ */
+ public @NonNull Builder setExtras(@Nullable Bundle extras) {
+ mExtras = extras;
+ return this;
+ }
+
+ /**
+ * Builds the {@link ConnectionInfo} object.
+ */
+ public @NonNull ConnectionInfo build() {
+ return new ConnectionInfo(mRoute,
+ mAudioAttributes, mPresentationDisplay,
+ mVolumeProvider, mProtocols, mExtras, mCloseables);
+ }
+ }
+ }
+
+ /**
+ * Describes one particular way of routing media content to a destination
+ * according to the capabilities specified by a media route selector on behalf
+ * of an application.
+ */
+ public static final class RouteInfo {
+ private final String mId;
+ private final DestinationInfo mDestination;
+ private final MediaRouteSelector mSelector;
+ private final int mFeatures;
+ private final ArrayList<String> mProtocols;
+ private final Bundle mExtras;
+
+ RouteInfo(String id, DestinationInfo destination, MediaRouteSelector selector,
+ int features, ArrayList<String> protocols, Bundle extras) {
+ mId = id;
+ mDestination = destination;
+ mSelector = selector;
+ mFeatures = features;
+ mProtocols = protocols;
+ mExtras = extras;
+ }
+
+ /**
+ * Gets the route's stable identifier.
+ * <p>
+ * The id is intended to uniquely identify the route among all routes that
+ * are offered by a particular destination in such a way that the client can
+ * refer to it at a later time.
+ * </p>
+ */
+ public @NonNull String getId() {
+ return mId;
+ }
+
+ /**
+ * Gets the destination that is offering this route.
+ */
+ public @NonNull DestinationInfo getDestination() {
+ return mDestination;
+ }
+
+ /**
+ * Gets the media route selector provided by the client for which this
+ * route was created.
+ * <p>
+ * It is implied that this route supports all of the required capabilities
+ * that were expressed in the selector.
+ * </p>
+ */
+ public @NonNull MediaRouteSelector getSelector() {
+ return mSelector;
+ }
+
+ /**
+ * Gets the set of supported route features.
+ */
+ public @RouteFeatures int getFeatures() {
+ return mFeatures;
+ }
+
+ /**
+ * Gets the list of supported route protocols.
+ * <p>
+ * Refer to <code>android.support.media.protocols.MediaRouteProtocol</code>
+ * for more information.
+ * </p>
+ */
+ public @NonNull List<String> getProtocols() {
+ return mProtocols;
+ }
+
+ /**
+ * Gets optional extra information about the route, or null if none.
+ */
+ public @Nullable Bundle getExtras() {
+ return mExtras;
+ }
+
+ @Override
+ public @NonNull String toString() {
+ return "RouteInfo{ id=" + mId + ", destination=" + mDestination
+ + ", features=0x" + Integer.toHexString(mFeatures)
+ + ", selector=" + mSelector + ", protocols=" + mProtocols
+ + ", extras=" + mExtras + " }";
+ }
+
+ /**
+ * Builds {@link RouteInfo} objects.
+ */
+ public static final class Builder {
+ private final DestinationInfo mDestination;
+ private final String mId;
+ private final MediaRouteSelector mSelector;
+ private int mFeatures;
+ private final ArrayList<String> mProtocols = new ArrayList<String>();
+ private Bundle mExtras;
+
+ /**
+ * Creates a builder for route information.
+ *
+ * @param id The route's stable identifier.
+ * @param destination The destination of this route.
+ * @param selector The media route selector provided by the client for which
+ * this route was created. This must be one of the selectors that was
+ * included in the discovery request.
+ */
+ public Builder(@NonNull String id, @NonNull DestinationInfo destination,
+ @NonNull MediaRouteSelector selector) {
+ if (TextUtils.isEmpty(id)) {
+ throw new IllegalArgumentException("id must not be null or empty");
+ }
+ if (destination == null) {
+ throw new IllegalArgumentException("destination must not be null");
+ }
+ if (selector == null) {
+ throw new IllegalArgumentException("selector must not be null");
+ }
+ mDestination = destination;
+ mId = id;
+ mSelector = selector;
+ }
+
+ /**
+ * Sets the set of supported route features.
+ */
+ public @NonNull Builder setFeatures(@RouteFeatures int features) {
+ mFeatures = features;
+ return this;
+ }
+
+ /**
+ * Adds a supported route protocol using its fully qualified class name.
+ * <p>
+ * If the protocol was not requested by the client in its selector
+ * then it will be silently discarded.
+ * </p>
+ */
+ public @NonNull <T extends IInterface> Builder addProtocol(@NonNull Class<T> clazz) {
+ if (clazz == null) {
+ throw new IllegalArgumentException("clazz must not be null");
+ }
+ return addProtocol(clazz.getName());
+ }
+
+ /**
+ * Adds a supported route protocol by name.
+ * <p>
+ * If the protocol was not requested by the client in its selector
+ * then it will be silently discarded.
+ * </p>
+ */
+ public @NonNull Builder addProtocol(@NonNull String name) {
+ if (TextUtils.isEmpty(name)) {
+ throw new IllegalArgumentException("name must not be null");
+ }
+ if (mSelector.containsProtocol(name)) {
+ mProtocols.add(name);
+ }
+ return this;
+ }
+
+ /**
+ * Sets optional extra information about the route, or null if none.
+ */
+ public @NonNull Builder setExtras(@Nullable Bundle extras) {
+ mExtras = extras;
+ return this;
+ }
+
+ /**
+ * Builds the {@link RouteInfo} object.
+ * <p>
+ * Ensures that all required protocols have been supplied.
+ * </p>
+ */
+ public @NonNull RouteInfo build() {
+ int missingFeatures = mSelector.getRequiredFeatures() & ~mFeatures;
+ if (missingFeatures != 0) {
+ throw new IllegalStateException("The media route selector "
+ + "specified required features which this route does "
+ + "not appear to support so it should not have been published: "
+ + "missing 0x" + Integer.toHexString(missingFeatures));
+ }
+ for (String protocol : mSelector.getRequiredProtocols()) {
+ if (!mProtocols.contains(protocol)) {
+ throw new IllegalStateException("The media route selector "
+ + "specified required protocols which this route "
+ + "does not appear to support so it should not have "
+ + "been published: missing " + protocol);
+ }
+ }
+ return new RouteInfo(mId, mDestination, mSelector,
+ mFeatures, mProtocols, mExtras);
+ }
+ }
+ }
+
+ /**
+ * Describes a destination for media content such as a device,
+ * an individual port on a device, or a group of devices.
+ */
+ public static final class DestinationInfo {
+ private final String mId;
+ private final ServiceMetadata mService;
+ private final CharSequence mName;
+ private final CharSequence mDescription;
+ private final int mIconResourceId;
+ private final Bundle mExtras;
+
+ DestinationInfo(String id, ServiceMetadata service,
+ CharSequence name, CharSequence description,
+ int iconResourceId, Bundle extras) {
+ mId = id;
+ mService = service;
+ mName = name;
+ mDescription = description;
+ mIconResourceId = iconResourceId;
+ mExtras = extras;
+ }
+
+ /**
+ * Gets the destination's stable identifier.
+ * <p>
+ * The id is intended to uniquely identify the destination among all destinations
+ * provided by the media route service in such a way that the client can
+ * refer to it at a later time. Ideally, the id should be resilient to
+ * user-initiated actions such as changes to the name or description
+ * of the destination.
+ * </p>
+ */
+ public @NonNull String getId() {
+ return mId;
+ }
+
+ /**
+ * Gets metadata about the service that is providing access to this destination.
+ */
+ public @NonNull ServiceMetadata getServiceMetadata() {
+ return mService;
+ }
+
+ /**
+ * Gets the destination's name for display to the user.
+ */
+ public @NonNull CharSequence getName() {
+ return mName;
+ }
+
+ /**
+ * Gets the destination's description for display to the user, or null if none.
+ */
+ public @Nullable CharSequence getDescription() {
+ return mDescription;
+ }
+
+ /**
+ * Gets an icon resource from the service's package which is used
+ * to identify the destination, or -1 if none.
+ */
+ public @DrawableRes int getIconResourceId() {
+ return mIconResourceId;
+ }
+
+ /**
+ * Loads the icon drawable, or null if none.
+ */
+ public @Nullable Drawable loadIcon(@NonNull PackageManager pm) {
+ return mIconResourceId >= 0 ? mService.getDrawable(pm, mIconResourceId) : null;
+ }
+
+ /**
+ * Gets optional extra information about the destination, or null if none.
+ */
+ public @Nullable Bundle getExtras() {
+ return mExtras;
+ }
+
+ @Override
+ public @NonNull String toString() {
+ return "DestinationInfo{ id=" + mId + ", service=" + mService + ", name=" + mName
+ + ", description=" + mDescription + ", iconResourceId=" + mIconResourceId
+ + ", extras=" + mExtras + " }";
+ }
+
+ /**
+ * Builds {@link DestinationInfo} objects.
+ */
+ public static final class Builder {
+ private final String mId;
+ private final ServiceMetadata mService;
+ private final CharSequence mName;
+ private CharSequence mDescription;
+ private int mIconResourceId = -1;
+ private Bundle mExtras;
+
+ /**
+ * Creates a builder for destination information.
+ *
+ * @param id The destination's stable identifier.
+ * @param service Metatada about the service that is providing access to
+ * this destination.
+ * @param name The destination's name for display to the user.
+ */
+ public Builder(@NonNull String id, @NonNull ServiceMetadata service,
+ @NonNull CharSequence name) {
+ if (TextUtils.isEmpty(id)) {
+ throw new IllegalArgumentException("id must not be null or empty");
+ }
+ if (service == null) {
+ throw new IllegalArgumentException("service must not be null");
+ }
+ if (TextUtils.isEmpty(name)) {
+ throw new IllegalArgumentException("name must not be null or empty");
+ }
+ mId = id;
+ mService = service;
+ mName = name;
+ }
+
+ /**
+ * Sets the destination's description for display to the user, or null if none.
+ */
+ public @NonNull Builder setDescription(@Nullable CharSequence description) {
+ mDescription = description;
+ return this;
+ }
+
+ /**
+ * Sets an icon resource from this package used to identify the destination,
+ * or -1 if none.
+ */
+ public @NonNull Builder setIconResourceId(@DrawableRes int resid) {
+ mIconResourceId = resid;
+ return this;
+ }
+
+ /**
+ * Gets optional extra information about the destination, or null if none.
+ */
+ public @NonNull Builder setExtras(@Nullable Bundle extras) {
+ mExtras = extras;
+ return this;
+ }
+
+ /**
+ * Builds the {@link DestinationInfo} object.
+ */
+ public @NonNull DestinationInfo build() {
+ return new DestinationInfo(mId, mService, mName, mDescription,
+ mIconResourceId, mExtras);
+ }
+ }
+ }
+
+ /**
+ * Describes metadata about a {@link MediaRouteService} which is providing
+ * access to certain kinds of destinations.
+ */
+ public static final class ServiceMetadata {
+ private final ServiceInfo mService;
+ private CharSequence mLabel;
+ private Drawable mIcon;
+
+ ServiceMetadata(Service service) throws NameNotFoundException {
+ mService = service.getPackageManager().getServiceInfo(
+ new ComponentName(service, service.getClass()),
+ PackageManager.GET_META_DATA);
+ }
+
+ ServiceMetadata(ServiceInfo service) {
+ mService = service;
+ }
+
+ /**
+ * Gets the service's component information including it name, label and icon.
+ */
+ public @NonNull ServiceInfo getService() {
+ return mService;
+ }
+
+ /**
+ * Gets the service's component name.
+ */
+ public @NonNull ComponentName getComponentName() {
+ return new ComponentName(mService.packageName, mService.name);
+ }
+
+ /**
+ * Gets the service's package name.
+ */
+ public @NonNull String getPackageName() {
+ return mService.packageName;
+ }
+
+ /**
+ * Gets the service's name for display to the user, or null if none.
+ */
+ public @NonNull CharSequence getLabel(@NonNull PackageManager pm) {
+ if (mLabel == null) {
+ mLabel = mService.loadLabel(pm);
+ }
+ return mLabel;
+ }
+
+ /**
+ * Gets the icon drawable, or null if none.
+ */
+ public @Nullable Drawable getIcon(@NonNull PackageManager pm) {
+ if (mIcon == null) {
+ mIcon = mService.loadIcon(pm);
+ }
+ return mIcon;
+ }
+
+ // TODO: add service metadata
+
+ Drawable getDrawable(PackageManager pm, int resid) {
+ return pm.getDrawable(getPackageName(), resid, mService.applicationInfo);
+ }
+
+ @Override
+ public @NonNull String toString() {
+ return "ServiceInfo{ service=" + getComponentName().toShortString() + " }";
+ }
+ }
+
+ /**
+ * Describes a request to discover routes on behalf of an application.
+ */
+ public static final class DiscoveryRequest {
+ private final ArrayList<MediaRouteSelector> mSelectors =
+ new ArrayList<MediaRouteSelector>();
+ private int mFlags;
+
+ DiscoveryRequest(@NonNull List<MediaRouteSelector> selectors) {
+ setSelectors(selectors);
+ }
+
+ /**
+ * Sets the list of media route selectors to consider during discovery.
+ */
+ public void setSelectors(@NonNull List<MediaRouteSelector> selectors) {
+ if (selectors == null) {
+ throw new IllegalArgumentException("selectors");
+ }
+ mSelectors.clear();
+ mSelectors.addAll(selectors);
+ }
+
+ /**
+ * Gets the list of media route selectors to consider during discovery.
+ */
+ public @NonNull List<MediaRouteSelector> getSelectors() {
+ return mSelectors;
+ }
+
+ /**
+ * Gets discovery flags, such as {@link MediaRouter#DISCOVERY_FLAG_BACKGROUND}.
+ */
+ public @DiscoveryFlags int getFlags() {
+ return mFlags;
+ }
+
+ /**
+ * Sets discovery flags, such as {@link MediaRouter#DISCOVERY_FLAG_BACKGROUND}.
+ */
+ public void setFlags(@DiscoveryFlags int flags) {
+ mFlags = flags;
+ }
+
+ @Override
+ public @NonNull String toString() {
+ return "DiscoveryRequest{ selectors=" + mSelectors
+ + ", flags=0x" + Integer.toHexString(mFlags)
+ + " }";
+ }
+ }
+
+ /**
+ * Describes a request to connect to a previously discovered route on
+ * behalf of an application.
+ */
+ public static final class ConnectionRequest {
+ private RouteInfo mRoute;
+ private int mFlags;
+ private Bundle mExtras;
+
+ ConnectionRequest(@NonNull RouteInfo route) {
+ setRoute(route);
+ }
+
+ /**
+ * Gets the route to which to connect.
+ */
+ public @NonNull RouteInfo getRoute() {
+ return mRoute;
+ }
+
+ /**
+ * Sets the route to which to connect.
+ */
+ public void setRoute(@NonNull RouteInfo route) {
+ if (route == null) {
+ throw new IllegalArgumentException("route must not be null");
+ }
+ mRoute = route;
+ }
+
+ /**
+ * Gets connection flags, such as {@link MediaRouter#CONNECTION_FLAG_BARGE}.
+ */
+ public @ConnectionFlags int getFlags() {
+ return mFlags;
+ }
+
+ /**
+ * Sets connection flags, such as {@link MediaRouter#CONNECTION_FLAG_BARGE}.
+ */
+ public void setFlags(@ConnectionFlags int flags) {
+ mFlags = flags;
+ }
+
+ /**
+ * Gets optional extras supplied by the application as part of the call to
+ * connect, or null if none. The media route service may use this
+ * information to configure the route during connection.
+ */
+ public @Nullable Bundle getExtras() {
+ return mExtras;
+ }
+
+ /**
+ * Sets optional extras supplied by the application as part of the call to
+ * connect, or null if none. The media route service may use this
+ * information to configure the route during connection.
+ */
+ public void setExtras(@Nullable Bundle extras) {
+ mExtras = extras;
+ }
+
+ @Override
+ public @NonNull String toString() {
+ return "ConnectionRequest{ route=" + mRoute
+ + ", flags=0x" + Integer.toHexString(mFlags)
+ + ", extras=" + mExtras + " }";
+ }
+ }
+
+ /**
+ * Callback interface to specify policy for route discovery, filtering,
+ * and connection establishment as well as observe media router state changes.
+ */
+ public static abstract class RoutingCallback extends StateCallback {
+ /**
+ * Called to prepare a discovery request object to specify the desired
+ * media route selectors when the media router has been asked to start discovery.
+ * <p>
+ * By default, the discovery request contains all of the selectors which
+ * have been added to the media router. Subclasses may override the list of
+ * selectors by modifying the discovery request object before returning.
+ * </p>
+ *
+ * @param request The discovery request object which may be modified by
+ * this method to alter how discovery will be performed.
+ * @param selectors The immutable list of media route selectors which were
+ * added to the media router.
+ * @return True to allow discovery to proceed or false to abort it.
+ * By default, this methods returns true.
+ */
+ public boolean onPrepareDiscoveryRequest(@NonNull DiscoveryRequest request,
+ @NonNull List<MediaRouteSelector> selectors) {
+ return true;
+ }
+
+ /**
+ * Called to prepare a connection request object to specify the desired
+ * route and connection parameters when the media router has been asked to
+ * connect to a particular destination.
+ * <p>
+ * By default, the connection request specifies the first available route
+ * to the destination. Subclasses may override the route and destination
+ * or set additional connection parameters by modifying the connection request
+ * object before returning.
+ * </p>
+ *
+ * @param request The connection request object which may be modified by
+ * this method to alter how the connection will be established.
+ * @param destination The destination to which the media router was asked
+ * to connect.
+ * @param routes The list of routes that belong to that destination sorted
+ * in the same order as their matching media route selectors which were
+ * used during discovery.
+ * @return True to allow the connection to proceed or false to abort it.
+ * By default, this methods returns true.
+ */
+ public boolean onPrepareConnectionRequest(
+ @NonNull ConnectionRequest request,
+ @NonNull DestinationInfo destination, @NonNull List<RouteInfo> routes) {
+ return true;
+ }
+ }
+
+ /**
+ * Callback class to receive events from a {@link MediaRouter.Delegate}.
+ */
+ public static abstract class StateCallback {
+ /**
+ * Called when the media router has been released.
+ */
+ public void onReleased() { }
+
+ /**
+ * Called when the discovery state has changed.
+ *
+ * @param state The new discovery state: one of
+ * {@link #DISCOVERY_STATE_STOPPED} or {@link #DISCOVERY_STATE_STARTED}.
+ */
+ public void onDiscoveryStateChanged(@DiscoveryState int state) { }
+
+ /**
+ * Called when the connection state has changed.
+ *
+ * @param state The new connection state: one of
+ * {@link #CONNECTION_STATE_DISCONNECTED}, {@link #CONNECTION_STATE_CONNECTING}
+ * or {@link #CONNECTION_STATE_CONNECTED}.
+ */
+ public void onConnectionStateChanged(@ConnectionState int state) { }
+
+ /**
+ * Called when the selected destination has changed.
+ *
+ * @param destination The new selected destination, or null if none.
+ */
+ public void onSelectedDestinationChanged(@Nullable DestinationInfo destination) { }
+
+ /**
+ * Called when route discovery has started.
+ */
+ public void onDiscoveryStarted() { }
+
+ /**
+ * Called when route discovery has stopped normally.
+ * <p>
+ * Abnormal termination is reported via {@link #onDiscoveryFailed}.
+ * </p>
+ */
+ public void onDiscoveryStopped() { }
+
+ /**
+ * Called when discovery has failed in a non-recoverable manner.
+ *
+ * @param error The error code: one of
+ * {@link MediaRouter#DISCOVERY_ERROR_UNKNOWN},
+ * {@link MediaRouter#DISCOVERY_ERROR_ABORTED},
+ * or {@link MediaRouter#DISCOVERY_ERROR_NO_CONNECTIVITY}.
+ * @param message The localized error message, or null if none. This message
+ * may be shown to the user.
+ * @param extras Additional information about the error which a client
+ * may use, or null if none.
+ */
+ public void onDiscoveryFailed(@DiscoveryError int error, @Nullable CharSequence message,
+ @Nullable Bundle extras) { }
+
+ /**
+ * Called when a new destination is found or has changed during discovery.
+ * <p>
+ * Certain destinations may be omitted because they have been filtered
+ * out by the media router's routing callback.
+ * </p>
+ *
+ * @param destination The destination that was found.
+ */
+ public void onDestinationFound(@NonNull DestinationInfo destination) { }
+
+ /**
+ * Called when a destination is no longer reachable or is no longer
+ * offering any routes that satisfy the discovery request.
+ *
+ * @param destination The destination that went away.
+ */
+ public void onDestinationLost(@NonNull DestinationInfo destination) { }
+
+ /**
+ * Called when a connection attempt begins.
+ */
+ public void onConnecting() { }
+
+ /**
+ * Called when the connection succeeds.
+ */
+ public void onConnected() { }
+
+ /**
+ * Called when the connection is terminated normally.
+ * <p>
+ * Abnormal termination is reported via {@link #onConnectionFailed}.
+ * </p>
+ */
+ public void onDisconnected() { }
+
+ /**
+ * Called when a connection attempt or connection in
+ * progress has failed in a non-recoverable manner.
+ *
+ * @param error The error code: one of
+ * {@link MediaRouter#CONNECTION_ERROR_ABORTED},
+ * {@link MediaRouter#CONNECTION_ERROR_UNAUTHORIZED},
+ * {@link MediaRouter#CONNECTION_ERROR_UNREACHABLE},
+ * {@link MediaRouter#CONNECTION_ERROR_BUSY},
+ * {@link MediaRouter#CONNECTION_ERROR_TIMEOUT},
+ * {@link MediaRouter#CONNECTION_ERROR_BROKEN},
+ * or {@link MediaRouter#CONNECTION_ERROR_BARGED}.
+ * @param message The localized error message, or null if none. This message
+ * may be shown to the user.
+ * @param extras Additional information about the error which a client
+ * may use, or null if none.
+ */
+ public void onConnectionFailed(@ConnectionError int error,
+ @Nullable CharSequence message, @Nullable Bundle extras) { }
+ }
+}
diff --git a/media/java/android/media/routing/ParcelableConnectionInfo.aidl b/media/java/android/media/routing/ParcelableConnectionInfo.aidl
new file mode 100644
index 0000000..4a9ec94
--- /dev/null
+++ b/media/java/android/media/routing/ParcelableConnectionInfo.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.routing;
+
+parcelable ParcelableConnectionInfo;
diff --git a/media/java/android/media/routing/ParcelableConnectionInfo.java b/media/java/android/media/routing/ParcelableConnectionInfo.java
new file mode 100644
index 0000000..45cfe9f
--- /dev/null
+++ b/media/java/android/media/routing/ParcelableConnectionInfo.java
@@ -0,0 +1,71 @@
+/*
+ * 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.routing;
+
+import android.media.AudioAttributes;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+/**
+ * Internal parcelable representation of a media route connection.
+ */
+class ParcelableConnectionInfo implements Parcelable {
+ public AudioAttributes audioAttributes;
+ public int presentationDisplayId = -1;
+ // todo: volume
+ public IBinder[] protocolBinders;
+ public Bundle extras;
+
+ public static final Parcelable.Creator<ParcelableConnectionInfo> CREATOR =
+ new Parcelable.Creator<ParcelableConnectionInfo>() {
+ @Override
+ public ParcelableConnectionInfo createFromParcel(Parcel source) {
+ ParcelableConnectionInfo info = new ParcelableConnectionInfo();
+ if (source.readInt() != 0) {
+ info.audioAttributes = AudioAttributes.CREATOR.createFromParcel(source);
+ }
+ info.presentationDisplayId = source.readInt();
+ info.protocolBinders = source.createBinderArray();
+ info.extras = source.readBundle();
+ return info;
+ }
+
+ @Override
+ public ParcelableConnectionInfo[] newArray(int size) {
+ return new ParcelableConnectionInfo[size];
+ }
+ };
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ if (audioAttributes != null) {
+ dest.writeInt(1);
+ audioAttributes.writeToParcel(dest, flags);
+ } else {
+ dest.writeInt(0);
+ }
+ dest.writeInt(presentationDisplayId);
+ dest.writeBinderArray(protocolBinders);
+ dest.writeBundle(extras);
+ }
+}
diff --git a/media/java/android/media/routing/ParcelableDestinationInfo.aidl b/media/java/android/media/routing/ParcelableDestinationInfo.aidl
new file mode 100644
index 0000000..bf1c198
--- /dev/null
+++ b/media/java/android/media/routing/ParcelableDestinationInfo.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.routing;
+
+parcelable ParcelableDestinationInfo;
diff --git a/media/java/android/media/routing/ParcelableDestinationInfo.java b/media/java/android/media/routing/ParcelableDestinationInfo.java
new file mode 100644
index 0000000..eca5eec
--- /dev/null
+++ b/media/java/android/media/routing/ParcelableDestinationInfo.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.routing;
+
+import android.os.Bundle;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.text.TextUtils;
+
+/**
+ * Internal parcelable representation of a media destination.
+ */
+class ParcelableDestinationInfo implements Parcelable {
+ public String id;
+ public CharSequence name;
+ public CharSequence description;
+ public int iconResourceId;
+ public Bundle extras;
+
+ public static final Parcelable.Creator<ParcelableDestinationInfo> CREATOR =
+ new Parcelable.Creator<ParcelableDestinationInfo>() {
+ @Override
+ public ParcelableDestinationInfo createFromParcel(Parcel source) {
+ ParcelableDestinationInfo info = new ParcelableDestinationInfo();
+ info.id = source.readString();
+ info.name = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(source);
+ info.description = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(source);
+ info.iconResourceId = source.readInt();
+ info.extras = source.readBundle();
+ return info;
+ }
+
+ @Override
+ public ParcelableDestinationInfo[] newArray(int size) {
+ return new ParcelableDestinationInfo[size];
+ }
+ };
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeString(id);
+ TextUtils.writeToParcel(name, dest, flags);
+ TextUtils.writeToParcel(description, dest, flags);
+ dest.writeInt(iconResourceId);
+ dest.writeBundle(extras);
+ }
+}
diff --git a/media/java/android/media/routing/ParcelableRouteInfo.aidl b/media/java/android/media/routing/ParcelableRouteInfo.aidl
new file mode 100644
index 0000000..126afaa
--- /dev/null
+++ b/media/java/android/media/routing/ParcelableRouteInfo.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.routing;
+
+parcelable ParcelableRouteInfo;
diff --git a/media/java/android/media/routing/ParcelableRouteInfo.java b/media/java/android/media/routing/ParcelableRouteInfo.java
new file mode 100644
index 0000000..fb1a547
--- /dev/null
+++ b/media/java/android/media/routing/ParcelableRouteInfo.java
@@ -0,0 +1,64 @@
+/*
+ * 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.routing;
+
+import android.os.Bundle;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+/**
+ * Internal parcelable representation of a media route.
+ */
+class ParcelableRouteInfo implements Parcelable {
+ public String id;
+ public int selectorIndex; // index of selector within list used for discovery
+ public int features;
+ public String[] protocols;
+ public Bundle extras;
+
+ public static final Parcelable.Creator<ParcelableRouteInfo> CREATOR =
+ new Parcelable.Creator<ParcelableRouteInfo>() {
+ @Override
+ public ParcelableRouteInfo createFromParcel(Parcel source) {
+ ParcelableRouteInfo info = new ParcelableRouteInfo();
+ info.id = source.readString();
+ info.selectorIndex = source.readInt();
+ info.features = source.readInt();
+ info.protocols = source.createStringArray();
+ info.extras = source.readBundle();
+ return info;
+ }
+
+ @Override
+ public ParcelableRouteInfo[] newArray(int size) {
+ return new ParcelableRouteInfo[size];
+ }
+ };
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeString(id);
+ dest.writeInt(selectorIndex);
+ dest.writeInt(features);
+ dest.writeStringArray(protocols);
+ dest.writeBundle(extras);
+ }
+}
diff --git a/media/java/android/media/session/ISession.aidl b/media/java/android/media/session/ISession.aidl
index bd0019f..af3b72e 100644
--- a/media/java/android/media/session/ISession.aidl
+++ b/media/java/android/media/session/ISession.aidl
@@ -19,6 +19,7 @@ import android.app.PendingIntent;
import android.content.pm.ParceledListSlice;
import android.media.AudioAttributes;
import android.media.MediaMetadata;
+import android.media.routing.IMediaRouter;
import android.media.session.ISessionController;
import android.media.session.PlaybackState;
import android.media.session.MediaSession;
@@ -34,6 +35,7 @@ interface ISession {
ISessionController getController();
void setFlags(int flags);
void setActive(boolean active);
+ void setMediaRouter(in IMediaRouter router);
void setMediaButtonReceiver(in PendingIntent mbr);
void setLaunchPendingIntent(in PendingIntent pi);
void destroy();
diff --git a/media/java/android/media/session/ISessionController.aidl b/media/java/android/media/session/ISessionController.aidl
index d684688..e2d06d3 100644
--- a/media/java/android/media/session/ISessionController.aidl
+++ b/media/java/android/media/session/ISessionController.aidl
@@ -20,6 +20,8 @@ import android.content.Intent;
import android.content.pm.ParceledListSlice;
import android.media.MediaMetadata;
import android.media.Rating;
+import android.media.routing.IMediaRouterDelegate;
+import android.media.routing.IMediaRouterStateCallback;
import android.media.session.ISessionControllerCallback;
import android.media.session.ParcelableVolumeInfo;
import android.media.session.PlaybackState;
@@ -49,6 +51,8 @@ interface ISessionController {
void adjustVolume(int direction, int flags, String packageName);
void setVolumeTo(int value, int flags, String packageName);
+ IMediaRouterDelegate createMediaRouterDelegate(IMediaRouterStateCallback callback);
+
// These commands are for the TransportControls
void play();
void playFromMediaId(String uri, in Bundle extras);
diff --git a/media/java/android/media/session/MediaController.java b/media/java/android/media/session/MediaController.java
index dd6bd20..c23a139 100644
--- a/media/java/android/media/session/MediaController.java
+++ b/media/java/android/media/session/MediaController.java
@@ -26,6 +26,7 @@ import android.media.AudioManager;
import android.media.MediaMetadata;
import android.media.Rating;
import android.media.VolumeProvider;
+import android.media.routing.MediaRouter;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
@@ -119,6 +120,17 @@ public final class MediaController {
}
/**
+ * Creates a media router delegate through which the destination of the media
+ * router may be observed and controlled.
+ *
+ * @return The media router delegate, or null if the media session does
+ * not support media routing.
+ */
+ public @Nullable MediaRouter.Delegate createMediaRouterDelegate() {
+ return new MediaRouter.Delegate();
+ }
+
+ /**
* Send the specified media button event to the session. Only media keys can
* be sent by this method, other keys will be ignored.
*
diff --git a/media/java/android/media/session/MediaSession.java b/media/java/android/media/session/MediaSession.java
index df4bc78..57c291d 100644
--- a/media/java/android/media/session/MediaSession.java
+++ b/media/java/android/media/session/MediaSession.java
@@ -29,6 +29,7 @@ import android.media.MediaDescription;
import android.media.MediaMetadata;
import android.media.Rating;
import android.media.VolumeProvider;
+import android.media.routing.MediaRouter;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
@@ -224,6 +225,23 @@ public final class MediaSession {
}
/**
+ * Associates a {@link MediaRouter} with this session to control the destination
+ * of media content.
+ * <p>
+ * A media router may only be associated with at most one session at a time.
+ * </p>
+ *
+ * @param router The media router, or null to remove the current association.
+ */
+ public void setMediaRouter(@Nullable MediaRouter router) {
+ try {
+ mBinder.setMediaRouter(router != null ? router.getBinder() : null);
+ } catch (RemoteException e) {
+ Log.wtf(TAG, "Failure in setMediaButtonReceiver.", e);
+ }
+ }
+
+ /**
* Set a pending intent for your media button receiver to allow restarting
* playback after the session has been stopped. If your app is started in
* this way an {@link Intent#ACTION_MEDIA_BUTTON} intent will be sent via
diff --git a/media/java/android/media/session/MediaSessionLegacyHelper.java b/media/java/android/media/session/MediaSessionLegacyHelper.java
index 7ea269b..4ea22f9 100644
--- a/media/java/android/media/session/MediaSessionLegacyHelper.java
+++ b/media/java/android/media/session/MediaSessionLegacyHelper.java
@@ -221,14 +221,10 @@ public class MediaSessionLegacyHelper {
mSessionManager.dispatchAdjustVolume(AudioManager.USE_DEFAULT_STREAM_TYPE,
direction, flags);
} else if (isMute) {
- if (down) {
- // We need to send two volume events on down, one to mute
- // and one to show the UI
+ if (down && keyEvent.getRepeatCount() == 0) {
mSessionManager.dispatchAdjustVolume(AudioManager.USE_DEFAULT_STREAM_TYPE,
- MediaSessionManager.DIRECTION_MUTE, flags);
+ AudioManager.ADJUST_TOGGLE_MUTE, flags);
}
- mSessionManager.dispatchAdjustVolume(AudioManager.USE_DEFAULT_STREAM_TYPE,
- 0 /* direction, causes UI to show on down */, flags);
}
}
}
diff --git a/media/java/android/media/session/MediaSessionManager.java b/media/java/android/media/session/MediaSessionManager.java
index a4ef851..b4fff8f 100644
--- a/media/java/android/media/session/MediaSessionManager.java
+++ b/media/java/android/media/session/MediaSessionManager.java
@@ -59,14 +59,6 @@ public final class MediaSessionManager {
private Context mContext;
/**
- * Special flag for sending the mute key to dispatchAdjustVolume used by the
- * system.
- *
- * @hide
- */
- public static final int DIRECTION_MUTE = -99;
-
- /**
* @hide
*/
public MediaSessionManager(Context context) {
diff --git a/media/java/android/media/tv/TvContract.java b/media/java/android/media/tv/TvContract.java
index 5b92266..bc9722e 100644
--- a/media/java/android/media/tv/TvContract.java
+++ b/media/java/android/media/tv/TvContract.java
@@ -756,8 +756,9 @@ public final class TvContract {
* </p><p>
* Note that this sub-directory also supports opening the logo as an asset file in write
* mode. Callers can create or replace the primary logo associated with this channel by
- * opening the asset file and writing the full-size photo contents into it. When the file
- * is closed, the image will be parsed, sized down if necessary, and stored.
+ * opening the asset file and writing the full-size photo contents into it. (Make sure there
+ * is no padding around the logo image.) When the file is closed, the image will be parsed,
+ * sized down if necessary, and stored.
* </p><p>
* Usage example:
* <pre>
diff --git a/media/java/android/media/tv/TvInputService.java b/media/java/android/media/tv/TvInputService.java
index b7e766b..f29be0d 100644
--- a/media/java/android/media/tv/TvInputService.java
+++ b/media/java/android/media/tv/TvInputService.java
@@ -615,13 +615,16 @@ public abstract class TvInputService extends Service {
public void onSetMain(boolean isMain) {
}
- /**
- * Sets the {@link Surface} for the current input session on which the TV input renders
- * video.
- *
- * @param surface {@link Surface} an application passes to this TV input session.
- * @return {@code true} if the surface was set, {@code false} otherwise.
- */
+ /**
+ * Sets the {@link Surface} for the current input session on which the TV input renders video.
+ * <p>
+ * When {@code setSurface(null)} is called, the implementation should stop using the Surface
+ * object previously given and release any references to it.
+ *
+ * @param surface possibly {@code null} {@link Surface} an application passes to this TV input
+ * session.
+ * @return {@code true} if the surface was set, {@code false} otherwise.
+ */
public abstract boolean onSetSurface(Surface surface);
/**
diff --git a/media/java/android/service/media/IMediaBrowserService.aidl b/media/java/android/service/media/IMediaBrowserService.aidl
index 01285ee..f01fc07 100644
--- a/media/java/android/service/media/IMediaBrowserService.aidl
+++ b/media/java/android/service/media/IMediaBrowserService.aidl
@@ -6,6 +6,7 @@ import android.content.res.Configuration;
import android.service.media.IMediaBrowserServiceCallbacks;
import android.net.Uri;
import android.os.Bundle;
+import android.os.ResultReceiver;
/**
* Media API allows clients to browse through hierarchy of a user’s media collection,
@@ -18,4 +19,5 @@ oneway interface IMediaBrowserService {
void addSubscription(String uri, IMediaBrowserServiceCallbacks callbacks);
void removeSubscription(String uri, IMediaBrowserServiceCallbacks callbacks);
+ void getMediaItem(String uri, in ResultReceiver cb);
} \ No newline at end of file
diff --git a/media/java/android/service/media/MediaBrowserService.java b/media/java/android/service/media/MediaBrowserService.java
index 26aedbd..8287344 100644
--- a/media/java/android/service/media/MediaBrowserService.java
+++ b/media/java/android/service/media/MediaBrowserService.java
@@ -32,8 +32,10 @@ import android.os.Bundle;
import android.os.IBinder;
import android.os.Handler;
import android.os.RemoteException;
+import android.os.ResultReceiver;
import android.service.media.IMediaBrowserService;
import android.service.media.IMediaBrowserServiceCallbacks;
+import android.text.TextUtils;
import android.util.ArrayMap;
import android.util.Log;
@@ -74,6 +76,13 @@ public abstract class MediaBrowserService extends Service {
@SdkConstant(SdkConstantType.SERVICE_ACTION)
public static final String SERVICE_INTERFACE = "android.media.browse.MediaBrowserService";
+ /**
+ * A key for passing the MediaItem to the ResultReceiver in getMediaItem.
+ *
+ * @hide
+ */
+ public static final String KEY_MEDIA_ITEM = "media_item";
+
private final ArrayMap<IBinder, ConnectionRecord> mConnections = new ArrayMap();
private final Handler mHandler = new Handler();
private ServiceBinder mBinder;
@@ -261,6 +270,33 @@ public abstract class MediaBrowserService extends Service {
}
});
}
+
+ @Override
+ public void getMediaItem(final String mediaId, final ResultReceiver receiver) {
+ if (TextUtils.isEmpty(mediaId) || receiver == null) {
+ return;
+ }
+
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ final Result<MediaBrowser.MediaItem> result
+ = new Result<MediaBrowser.MediaItem>(mediaId) {
+ @Override
+ void onResultSent(MediaBrowser.MediaItem item) {
+ Bundle bundle = new Bundle();
+ bundle.putParcelable(KEY_MEDIA_ITEM, item);
+ receiver.send(0, bundle);
+ }
+ };
+ try {
+ MediaBrowserService.this.getMediaItem(mediaId, result);
+ } catch (UnsupportedOperationException e) {
+ receiver.send(-1, null);
+ }
+ }
+ });
+ }
}
@Override
@@ -284,20 +320,21 @@ public abstract class MediaBrowserService extends Service {
/**
* Called to get the root information for browsing by a particular client.
* <p>
- * The implementation should verify that the client package has
- * permission to access browse media information before returning
- * the root id; it should return null if the client is not
- * allowed to access this information.
+ * The implementation should verify that the client package has permission
+ * to access browse media information before returning the root id; it
+ * should return null if the client is not allowed to access this
+ * information.
* </p>
*
- * @param clientPackageName The package name of the application
- * which is requesting access to browse media.
- * @param clientUid The uid of the application which is requesting
- * access to browse media.
+ * @param clientPackageName The package name of the application which is
+ * requesting access to browse media.
+ * @param clientUid The uid of the application which is requesting access to
+ * browse media.
* @param rootHints An optional bundle of service-specific arguments to send
- * to the media browse service when connecting and retrieving the root id
- * for browsing, or null if none. The contents of this bundle may affect
- * the information returned when browsing.
+ * to the media browse service when connecting and retrieving the
+ * root id for browsing, or null if none. The contents of this
+ * bundle may affect the information returned when browsing.
+ * @return The {@link BrowserRoot} for accessing this app's content or null.
*/
public abstract @Nullable BrowserRoot onGetRoot(@NonNull String clientPackageName,
int clientUid, @Nullable Bundle rootHints);
@@ -305,24 +342,51 @@ public abstract class MediaBrowserService extends Service {
/**
* Called to get information about the children of a media item.
* <p>
- * Implementations must call result.{@link Result#sendResult result.sendResult} with the list
- * of children. If loading the children will be an expensive operation that should be performed
- * on another thread, result.{@link Result#detach result.detach} may be called before returning
- * from this function, and then {@link Result#sendResult result.sendResult} called when
- * the loading is complete.
+ * Implementations must call {@link Result#sendResult result.sendResult}
+ * with the list of children. If loading the children will be an expensive
+ * operation that should be performed on another thread,
+ * {@link Result#detach result.detach} may be called before returning from
+ * this function, and then {@link Result#sendResult result.sendResult}
+ * called when the loading is complete.
*
- * @param parentId The id of the parent media item whose
- * children are to be queried.
- * @return The list of children, or null if the id is invalid.
+ * @param parentId The id of the parent media item whose children are to be
+ * queried.
+ * @param result The Result to send the list of children to, or null if the
+ * id is invalid.
*/
public abstract void onLoadChildren(@NonNull String parentId,
@NonNull Result<List<MediaBrowser.MediaItem>> result);
/**
+ * Called to get a specific media item. The mediaId should be the same id
+ * that would be returned for this item when it is in a list of child items.
+ * <p>
+ * Implementations must call {@link Result#sendResult result.sendResult}. If
+ * loading the item will be an expensive operation {@link Result#detach
+ * result.detach} may be called before returning from this function, and
+ * then {@link Result#sendResult result.sendResult} called when the item has
+ * been loaded.
+ * <p>
+ * The default implementation throws an exception.
+ *
+ * @param mediaId The id for the specific
+ * {@link android.media.browse.MediaBrowser.MediaItem}.
+ * @param result The Result to send the item to, or null if the id is
+ * invalid.
+ * @throws UnsupportedOperationException
+ */
+ public void getMediaItem(String mediaId, Result<MediaBrowser.MediaItem> result)
+ throws UnsupportedOperationException {
+ throw new UnsupportedOperationException("getMediaItem is not supported.");
+ }
+
+ /**
* Call to set the media session.
* <p>
* This should be called as soon as possible during the service's startup.
* It may only be called once.
+ *
+ * @param token The token for the service's {@link MediaSession}.
*/
public void setSessionToken(final MediaSession.Token token) {
if (token == null) {
@@ -405,12 +469,10 @@ public abstract class MediaBrowserService extends Service {
*/
private void addSubscription(String id, ConnectionRecord connection) {
// Save the subscription
- final boolean added = connection.subscriptions.add(id);
+ connection.subscriptions.add(id);
- // If this is a new subscription, send the results
- if (added) {
- performLoadChildren(id, connection);
- }
+ // send the results
+ performLoadChildren(id, connection);
}
/**
diff --git a/media/jni/android_media_MediaMetadataRetriever.cpp b/media/jni/android_media_MediaMetadataRetriever.cpp
index fc7931e..93138fa 100644
--- a/media/jni/android_media_MediaMetadataRetriever.cpp
+++ b/media/jni/android_media_MediaMetadataRetriever.cpp
@@ -33,6 +33,7 @@
#include "android_media_Utils.h"
#include "android_util_Binder.h"
+#include "android/graphics/GraphicsJNI.h"
using namespace android;
@@ -254,7 +255,7 @@ static jobject android_media_MediaMetadataRetriever_getFrameAtTime(JNIEnv *env,
jobject config = env->CallStaticObjectMethod(
fields.configClazz,
fields.createConfigMethod,
- SkBitmap::kRGB_565_Config);
+ GraphicsJNI::colorTypeToLegacyBitmapConfig(kRGB_565_SkColorType));
uint32_t width, height;
bool swapWidthAndHeight = false;
diff --git a/media/tests/MediaDump/src/com/android/mediadump/VideoDumpActivity.java b/media/tests/MediaDump/src/com/android/mediadump/VideoDumpActivity.java
index 46cb64e..b8f5704 100644
--- a/media/tests/MediaDump/src/com/android/mediadump/VideoDumpActivity.java
+++ b/media/tests/MediaDump/src/com/android/mediadump/VideoDumpActivity.java
@@ -19,7 +19,7 @@ package com.android.mediadump;
import android.app.Activity;
import android.app.AlertDialog;
import android.content.Context;
-import android.content.DialogInterface;;
+import android.content.DialogInterface;
import android.content.Intent;
import android.content.SharedPreferences;
import android.os.Bundle;