diff options
Diffstat (limited to 'media')
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> + * <service android:name=".MediaRouteProvider" + * android:label="@string/service_name" + * android:permission="android.permission.BIND_MEDIA_ROUTE_SERVICE"> + * <intent-filter> + * <action android:name="android.media.routing.MediaRouteService" /> + * </intent-filter> + * + * TODO: INSERT METADATA DECLARATIONS HERE + * + * </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; |