diff options
| author | Jeff Brown <jeffbrown@google.com> | 2014-06-23 16:13:30 -0700 |
|---|---|---|
| committer | Jeff Brown <jeffbrown@google.com> | 2014-06-30 16:05:25 -0700 |
| commit | 24fa6c0dd42df057729e1a258388183f94da7f82 (patch) | |
| tree | 675616cc6407928c68b08a715f4809d46872c38f | |
| parent | d9eb04dbc96599246581624c2c0e07b31eae3ca9 (diff) | |
| download | android_frameworks_support-24fa6c0dd42df057729e1a258388183f94da7f82.tar.gz android_frameworks_support-24fa6c0dd42df057729e1a258388183f94da7f82.tar.bz2 android_frameworks_support-24fa6c0dd42df057729e1a258388183f94da7f82.zip | |
Add media session wrappers to support library.
This is an initial check-in which just wraps the API 21+ media session
features for use in the support library. It does not provide any
backwards compatibility.
Change-Id: Ife5f8ddeefa8c6920517fc6704cc19e22767b7d6
15 files changed, 4159 insertions, 0 deletions
diff --git a/v4/api21/android/support/v4/media/MediaMetadataCompatApi21.java b/v4/api21/android/support/v4/media/MediaMetadataCompatApi21.java new file mode 100644 index 0000000000..4332d3e266 --- /dev/null +++ b/v4/api21/android/support/v4/media/MediaMetadataCompatApi21.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.support.v4.media; + +import android.graphics.Bitmap; +import android.media.MediaMetadata; +import android.media.Rating; + +import java.util.Set; + +class MediaMetadataCompatApi21 { + public static Set<String> keySet(Object metadataObj) { + return ((MediaMetadata)metadataObj).keySet(); + } + + public static Bitmap getBitmap(Object metadataObj, String key) { + return ((MediaMetadata)metadataObj).getBitmap(key); + } + + public static long getLong(Object metadataObj, String key) { + return ((MediaMetadata)metadataObj).getLong(key); + } + + public static Object getRating(Object metadataObj, String key) { + return ((MediaMetadata)metadataObj).getRating(key); + } + + public static String getString(Object metadataObj, String key) { + return ((MediaMetadata)metadataObj).getString(key); + } + + public static class Builder { + public static Object newInstance() { + return new MediaMetadata.Builder(); + } + + public static void putBitmap(Object builderObj, String key, Bitmap value) { + ((MediaMetadata.Builder)builderObj).putBitmap(key, value); + } + + public static void putLong(Object builderObj, String key, long value) { + ((MediaMetadata.Builder)builderObj).putLong(key, value); + } + + public static void putRating(Object builderObj, String key, Object ratingObj) { + ((MediaMetadata.Builder)builderObj).putRating(key, (Rating)ratingObj); + } + + public static void putString(Object builderObj, String key, String value) { + ((MediaMetadata.Builder)builderObj).putString(key, value); + } + + public static Object build(Object builderObj) { + return ((MediaMetadata.Builder)builderObj).build(); + } + } +} diff --git a/v4/api21/android/support/v4/media/RatingCompatApi21.java b/v4/api21/android/support/v4/media/RatingCompatApi21.java new file mode 100644 index 0000000000..6c8b9b2b4f --- /dev/null +++ b/v4/api21/android/support/v4/media/RatingCompatApi21.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.support.v4.media; + +import android.media.Rating; + +class RatingCompatApi21 { + public static Object newUnratedRating(int ratingStyle) { + return Rating.newUnratedRating(ratingStyle); + } + + public static Object newHeartRating(boolean hasHeart) { + return Rating.newHeartRating(hasHeart); + } + + public static Object newThumbRating(boolean thumbIsUp) { + return Rating.newThumbRating(thumbIsUp); + } + + public static Object newStarRating(int starRatingStyle, float starRating) { + return Rating.newStarRating(starRatingStyle, starRating); + } + + public static Object newPercentageRating(float percent) { + return Rating.newPercentageRating(percent); + } + + public static boolean isRated(Object ratingObj) { + return ((Rating)ratingObj).isRated(); + } + + public static int getRatingStyle(Object ratingObj) { + return ((Rating)ratingObj).getRatingStyle(); + } + + public static boolean hasHeart(Object ratingObj) { + return ((Rating)ratingObj).hasHeart(); + } + + public static boolean isThumbUp(Object ratingObj) { + return ((Rating)ratingObj).isThumbUp(); + } + + public static float getStarRating(Object ratingObj) { + return ((Rating)ratingObj).getStarRating(); + } + + public static float getPercentRating(Object ratingObj) { + return ((Rating)ratingObj).getPercentRating(); + } +} diff --git a/v4/api21/android/support/v4/media/VolumeProviderCompatApi21.java b/v4/api21/android/support/v4/media/VolumeProviderCompatApi21.java new file mode 100644 index 0000000000..7f2f5b974c --- /dev/null +++ b/v4/api21/android/support/v4/media/VolumeProviderCompatApi21.java @@ -0,0 +1,51 @@ +/* + * 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.support.v4.media; + +import android.media.VolumeProvider; + +class VolumeProviderCompatApi21 { + public static Object createVolumeProvider(int volumeControl, int maxVolume, + final Delegate delegate) { + return new VolumeProvider(volumeControl, maxVolume) { + @Override + public int onGetCurrentVolume() { + return delegate.onGetCurrentVolume(); + } + + @Override + public void onSetVolumeTo(int volume) { + delegate.onSetVolumeTo(volume); + } + + @Override + public void onAdjustVolumeBy(int delta) { + delegate.onAdjustVolumeBy(delta); + } + }; + } + + public static void notifyVolumeChanged(Object volumeProviderObj) { + ((VolumeProvider)volumeProviderObj).notifyVolumeChanged(); + } + + public interface Delegate { + int onGetCurrentVolume(); + void onSetVolumeTo(int volume); + void onAdjustVolumeBy(int delta); + } +} diff --git a/v4/api21/android/support/v4/media/session/MediaControllerCompatApi21.java b/v4/api21/android/support/v4/media/session/MediaControllerCompatApi21.java new file mode 100644 index 0000000000..2a9cd7f346 --- /dev/null +++ b/v4/api21/android/support/v4/media/session/MediaControllerCompatApi21.java @@ -0,0 +1,164 @@ +/* + * 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.support.v4.media.session; + +import android.media.MediaMetadata; +import android.media.Rating; +import android.media.session.MediaController; +import android.media.session.MediaSessionToken; +import android.media.session.PlaybackState; +import android.os.Bundle; +import android.os.Handler; +import android.os.ResultReceiver; +import android.view.KeyEvent; + +class MediaControllerCompatApi21 { + public static Object fromToken(Object sessionToken) { + return MediaController.fromToken((MediaSessionToken)sessionToken); + } + + public static Object createCallback(Callback callback) { + return new CallbackProxy<Callback>(callback); + } + + public static void addCallback(Object controllerObj, Object callbackObj, Handler handler) { + ((MediaController)controllerObj).addCallback( + (MediaController.Callback)callbackObj, handler); + } + + public static void removeCallback(Object controllerObj, Object callbackObj) { + ((MediaController)controllerObj).removeCallback((MediaController.Callback)callbackObj); + } + + public static Object getTransportControls(Object controllerObj) { + return ((MediaController)controllerObj).getTransportControls(); + } + + public static Object getPlaybackState(Object controllerObj) { + return ((MediaController)controllerObj).getPlaybackState(); + } + + public static Object getMetadata(Object controllerObj) { + return ((MediaController)controllerObj).getMetadata(); + } + + public static int getRatingType(Object controllerObj) { + return ((MediaController)controllerObj).getRatingType(); + } + + public static Object getVolumeInfo(Object controllerObj) { + return ((MediaController)controllerObj).getVolumeInfo(); + } + + public static boolean dispatchMediaButtonEvent(Object controllerObj, KeyEvent event) { + return ((MediaController)controllerObj).dispatchMediaButtonEvent(event); + } + + public static void sendControlCommand(Object controllerObj, + String command, Bundle params, ResultReceiver cb) { + ((MediaController)controllerObj).sendControlCommand(command, params, cb); + } + + public static class TransportControls { + public static void play(Object controlsObj) { + ((MediaController.TransportControls)controlsObj).play(); + } + + public static void pause(Object controlsObj) { + ((MediaController.TransportControls)controlsObj).pause(); + } + + public static void stop(Object controlsObj) { + ((MediaController.TransportControls)controlsObj).stop(); + } + + public static void seekTo(Object controlsObj, long pos) { + ((MediaController.TransportControls)controlsObj).seekTo(pos); + } + + public static void fastForward(Object controlsObj) { + ((MediaController.TransportControls)controlsObj).fastForward(); + } + + public static void rewind(Object controlsObj) { + ((MediaController.TransportControls)controlsObj).rewind(); + } + + public static void skipToNext(Object controlsObj) { + ((MediaController.TransportControls)controlsObj).skipToNext(); + } + + public static void skipToPrevious(Object controlsObj) { + ((MediaController.TransportControls)controlsObj).skipToPrevious(); + } + + public static void setRating(Object controlsObj, Object ratingObj) { + ((MediaController.TransportControls)controlsObj).setRating((Rating)ratingObj); + } + } + + public static class VolumeInfo { + public static int getVolumeType(Object volumeInfoObj) { + return ((MediaController.VolumeInfo)volumeInfoObj).getVolumeType(); + } + + public static int getAudioStream(Object volumeInfoObj) { + return ((MediaController.VolumeInfo)volumeInfoObj).getAudioStream(); + } + + public static int getVolumeControl(Object volumeInfoObj) { + return ((MediaController.VolumeInfo)volumeInfoObj).getVolumeControl(); + } + + public static int getMaxVolume(Object volumeInfoObj) { + return ((MediaController.VolumeInfo)volumeInfoObj).getMaxVolume(); + } + + public static int getCurrentVolume(Object volumeInfoObj) { + return ((MediaController.VolumeInfo)volumeInfoObj).getCurrentVolume(); + } + } + + public static interface Callback { + public void onSessionEvent(String event, Bundle extras); + public void onPlaybackStateChanged(Object stateObj); + public void onMetadataChanged(Object metadataObj); + } + + static class CallbackProxy<T extends Callback> extends MediaController.Callback { + protected final T mCallback; + + public CallbackProxy(T callback) { + mCallback = callback; + } + + @Override + public void onSessionEvent(String event, Bundle extras) { + mCallback.onSessionEvent(event, extras); + } + + @Override + public void onPlaybackStateChanged(PlaybackState state) { + mCallback.onPlaybackStateChanged(state); + } + + @Override + public void onMetadataChanged(MediaMetadata metadata) { + mCallback.onMetadataChanged(metadata); + } + } +} diff --git a/v4/api21/android/support/v4/media/session/MediaSessionCompatApi21.java b/v4/api21/android/support/v4/media/session/MediaSessionCompatApi21.java new file mode 100644 index 0000000000..fcaa190dbf --- /dev/null +++ b/v4/api21/android/support/v4/media/session/MediaSessionCompatApi21.java @@ -0,0 +1,194 @@ +/* + * 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.support.v4.media.session; + +import android.content.Context; +import android.content.Intent; +import android.media.MediaMetadata; +import android.media.Rating; +import android.media.VolumeProvider; +import android.media.session.MediaSession; +import android.media.session.MediaSessionManager; +import android.media.session.PlaybackState; +import android.os.Bundle; +import android.os.Handler; +import android.os.Parcelable; +import android.os.ResultReceiver; + +class MediaSessionCompatApi21 { + public static Object createSession(Context context, String tag) { + MediaSessionManager mgr = (MediaSessionManager)context.getSystemService( + Context.MEDIA_SESSION_SERVICE); + return mgr.createSession(tag); + } + + public static Object createCallback(Callback callback) { + return new CallbackProxy<Callback>(callback); + } + + public static void addCallback(Object sessionObj, Object callbackObj, Handler handler) { + ((MediaSession)sessionObj).addCallback((MediaSession.Callback)callbackObj, handler); + } + + public static void removeCallback(Object sessionObj, Object callbackObj) { + ((MediaSession)sessionObj).removeCallback((MediaSession.Callback)callbackObj); + } + + public static void setFlags(Object sessionObj, int flags) { + ((MediaSession)sessionObj).setFlags(flags); + } + + public static void setPlaybackToLocal(Object sessionObj, int stream) { + ((MediaSession)sessionObj).setPlaybackToLocal(stream); + } + + public static void setPlaybackToRemote(Object sessionObj, Object volumeProviderObj) { + ((MediaSession)sessionObj).setPlaybackToRemote((VolumeProvider)volumeProviderObj); + } + + public static void setActive(Object sessionObj, boolean active) { + ((MediaSession)sessionObj).setActive(active); + } + + public static boolean isActive(Object sessionObj) { + return ((MediaSession)sessionObj).isActive(); + } + + public static void sendSessionEvent(Object sessionObj, String event, Bundle extras) { + ((MediaSession)sessionObj).sendSessionEvent(event, extras); + } + + public static void release(Object sessionObj) { + ((MediaSession)sessionObj).release(); + } + + public static Parcelable getSessionToken(Object sessionObj) { + return ((MediaSession)sessionObj).getSessionToken(); + } + + public static Object createTransportControlsCallback(TransportControlsCallback callback) { + return new TransportControlsCallbackProxy<TransportControlsCallback>(callback); + } + + public static void addTransportControlsCallback(Object sessionObj, Object callbackObj, + Handler handler) { + ((MediaSession)sessionObj).addTransportControlsCallback( + (MediaSession.TransportControlsCallback)callbackObj, handler); + } + + public static void removeTransportControlsCallback(Object sessionObj, Object callbackObj) { + ((MediaSession)sessionObj).removeTransportControlsCallback( + (MediaSession.TransportControlsCallback)callbackObj); + } + + public static void setPlaybackState(Object sessionObj, Object stateObj) { + ((MediaSession)sessionObj).setPlaybackState((PlaybackState)stateObj); + } + + public static void setMetadata(Object sessionObj, Object metadataObj) { + ((MediaSession)sessionObj).setMetadata((MediaMetadata)metadataObj); + } + + public static interface Callback { + public void onMediaButtonEvent(Intent mediaButtonIntent); + public void onControlCommand(String command, Bundle extras, ResultReceiver cb); + } + + public static interface TransportControlsCallback { + public void onPlay(); + public void onPause(); + public void onSkipToNext(); + public void onSkipToPrevious(); + public void onFastForward(); + public void onRewind(); + public void onStop(); + public void onSeekTo(long pos); + public void onSetRating(Object ratingObj); + } + + static class CallbackProxy<T extends Callback> extends MediaSession.Callback { + protected final T mCallback; + + public CallbackProxy(T callback) { + mCallback = callback; + } + + @Override + public void onMediaButtonEvent(Intent mediaButtonIntent) { + mCallback.onMediaButtonEvent(mediaButtonIntent); + } + + @Override + public void onControlCommand(String command, Bundle extras, ResultReceiver cb) { + mCallback.onControlCommand(command, extras, cb); + } + } + + static class TransportControlsCallbackProxy<T extends TransportControlsCallback> + extends MediaSession.TransportControlsCallback { + protected final T mCallback; + + public TransportControlsCallbackProxy(T callback) { + mCallback = callback; + } + + @Override + public void onPlay() { + mCallback.onPlay(); + } + + @Override + public void onPause() { + mCallback.onPause(); + } + + @Override + public void onSkipToNext() { + mCallback.onSkipToNext(); + } + + @Override + public void onSkipToPrevious() { + mCallback.onSkipToPrevious(); + } + + @Override + public void onFastForward() { + mCallback.onFastForward(); + } + + @Override + public void onRewind() { + mCallback.onRewind(); + } + + @Override + public void onStop() { + mCallback.onStop(); + } + + @Override + public void onSeekTo(long pos) { + mCallback.onSeekTo(pos); + } + + @Override + public void onSetRating(Rating rating) { + mCallback.onSetRating(rating); + } + } +} diff --git a/v4/api21/android/support/v4/media/session/PlaybackStateCompatApi21.java b/v4/api21/android/support/v4/media/session/PlaybackStateCompatApi21.java new file mode 100644 index 0000000000..60971c3193 --- /dev/null +++ b/v4/api21/android/support/v4/media/session/PlaybackStateCompatApi21.java @@ -0,0 +1,63 @@ +/* + * 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.support.v4.media.session; + +import android.media.session.PlaybackState; +import android.os.SystemClock; + +class PlaybackStateCompatApi21 { + public static int getState(Object stateObj) { + return ((PlaybackState)stateObj).getState(); + } + + public static long getPosition(Object stateObj) { + return ((PlaybackState)stateObj).getPosition(); + } + + public static long getBufferPosition(Object stateObj) { + return ((PlaybackState)stateObj).getBufferPosition(); + } + + public static float getPlaybackRate(Object stateObj) { + return ((PlaybackState)stateObj).getPlaybackRate(); + } + + public static long getActions(Object stateObj) { + return ((PlaybackState)stateObj).getActions(); + } + + public static CharSequence getErrorMessage(Object stateObj) { + return ((PlaybackState)stateObj).getErrorMessage(); + } + + public static long getLastPositionUpdateTime(Object stateObj) { + // TODO: this method is inaccessible + // return ((PlaybackState)stateObj).getLastPositionUpdateTime(); + return SystemClock.uptimeMillis(); + } + + public static Object newInstance(int state, long position, long bufferPosition, + float rate, long actions, CharSequence errorMessage, long updateTime) { + PlaybackState stateObj = new PlaybackState(); + stateObj.setState(state, position, rate); + stateObj.setBufferPosition(bufferPosition); + stateObj.setActions(actions); + stateObj.setErrorMessage(errorMessage); + // TODO: updateTime is inaccessible + return stateObj; + } +} diff --git a/v4/java/android/support/v4/media/MediaMetadataCompat.java b/v4/java/android/support/v4/media/MediaMetadataCompat.java new file mode 100644 index 0000000000..7f152766ae --- /dev/null +++ b/v4/java/android/support/v4/media/MediaMetadataCompat.java @@ -0,0 +1,526 @@ +/* + * 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.support.v4.media; + +import android.graphics.Bitmap; +import android.os.Build; +import android.os.Bundle; +import android.os.Parcel; +import android.os.Parcelable; +import android.support.v4.util.ArrayMap; +import android.util.Log; + +import java.util.Set; + +/** + * Contains metadata about an item, such as the title, artist, etc. + */ +public final class MediaMetadataCompat implements Parcelable { + private static final String TAG = "MediaMetadata"; + + /** + * The title of the media. + */ + public static final String METADATA_KEY_TITLE = "android.media.metadata.TITLE"; + + /** + * The artist of the media. + */ + public static final String METADATA_KEY_ARTIST = "android.media.metadata.ARTIST"; + + /** + * The duration of the media in ms. A negative duration indicates that the + * duration is unknown (or infinite). + */ + public static final String METADATA_KEY_DURATION = "android.media.metadata.DURATION"; + + /** + * The album title for the media. + */ + public static final String METADATA_KEY_ALBUM = "android.media.metadata.ALBUM"; + + /** + * The author of the media. + */ + public static final String METADATA_KEY_AUTHOR = "android.media.metadata.AUTHOR"; + + /** + * The writer of the media. + */ + public static final String METADATA_KEY_WRITER = "android.media.metadata.WRITER"; + + /** + * The composer of the media. + */ + public static final String METADATA_KEY_COMPOSER = "android.media.metadata.COMPOSER"; + + /** + * The compilation status of the media. + */ + public static final String METADATA_KEY_COMPILATION = "android.media.metadata.COMPILATION"; + + /** + * The date the media was created or published as TODO determine format. + */ + public static final String METADATA_KEY_DATE = "android.media.metadata.DATE"; + + /** + * The year the media was created or published as a long. + */ + public static final String METADATA_KEY_YEAR = "android.media.metadata.YEAR"; + + /** + * The genre of the media. + */ + public static final String METADATA_KEY_GENRE = "android.media.metadata.GENRE"; + + /** + * The track number for the media. + */ + public static final String METADATA_KEY_TRACK_NUMBER = "android.media.metadata.TRACK_NUMBER"; + + /** + * The number of tracks in the media's original source. + */ + public static final String METADATA_KEY_NUM_TRACKS = "android.media.metadata.NUM_TRACKS"; + + /** + * The disc number for the media's original source. + */ + public static final String METADATA_KEY_DISC_NUMBER = "android.media.metadata.DISC_NUMBER"; + + /** + * The artist for the album of the media's original source. + */ + public static final String METADATA_KEY_ALBUM_ARTIST = "android.media.metadata.ALBUM_ARTIST"; + + /** + * The artwork for the media as a {@link Bitmap}. + */ + public static final String METADATA_KEY_ART = "android.media.metadata.ART"; + + /** + * The artwork for the media as a Uri style String. + */ + public static final String METADATA_KEY_ART_URI = "android.media.metadata.ART_URI"; + + /** + * The artwork for the album of the media's original source as a + * {@link Bitmap}. + */ + public static final String METADATA_KEY_ALBUM_ART = "android.media.metadata.ALBUM_ART"; + + /** + * The artwork for the album of the media's original source as a Uri style + * String. + */ + public static final String METADATA_KEY_ALBUM_ART_URI = "android.media.metadata.ALBUM_ART_URI"; + + /** + * The user's rating for the media. + * + * @see RatingCompat + */ + public static final String METADATA_KEY_USER_RATING = "android.media.metadata.USER_RATING"; + + /** + * The overall rating for the media. + * + * @see RatingCompat + */ + public static final String METADATA_KEY_RATING = "android.media.metadata.RATING"; + + private static final int METADATA_TYPE_LONG = 0; + private static final int METADATA_TYPE_STRING = 1; + private static final int METADATA_TYPE_BITMAP = 2; + private static final int METADATA_TYPE_RATING = 3; + private static final ArrayMap<String, Integer> METADATA_KEYS_TYPE; + + static { + METADATA_KEYS_TYPE = new ArrayMap<String, Integer>(); + METADATA_KEYS_TYPE.put(METADATA_KEY_TITLE, METADATA_TYPE_STRING); + METADATA_KEYS_TYPE.put(METADATA_KEY_ARTIST, METADATA_TYPE_STRING); + METADATA_KEYS_TYPE.put(METADATA_KEY_DURATION, METADATA_TYPE_LONG); + METADATA_KEYS_TYPE.put(METADATA_KEY_ALBUM, METADATA_TYPE_STRING); + METADATA_KEYS_TYPE.put(METADATA_KEY_AUTHOR, METADATA_TYPE_STRING); + METADATA_KEYS_TYPE.put(METADATA_KEY_WRITER, METADATA_TYPE_STRING); + METADATA_KEYS_TYPE.put(METADATA_KEY_COMPOSER, METADATA_TYPE_STRING); + METADATA_KEYS_TYPE.put(METADATA_KEY_COMPILATION, METADATA_TYPE_STRING); + METADATA_KEYS_TYPE.put(METADATA_KEY_DATE, METADATA_TYPE_STRING); + METADATA_KEYS_TYPE.put(METADATA_KEY_YEAR, METADATA_TYPE_LONG); + METADATA_KEYS_TYPE.put(METADATA_KEY_GENRE, METADATA_TYPE_STRING); + METADATA_KEYS_TYPE.put(METADATA_KEY_TRACK_NUMBER, METADATA_TYPE_LONG); + METADATA_KEYS_TYPE.put(METADATA_KEY_NUM_TRACKS, METADATA_TYPE_LONG); + METADATA_KEYS_TYPE.put(METADATA_KEY_DISC_NUMBER, METADATA_TYPE_LONG); + METADATA_KEYS_TYPE.put(METADATA_KEY_ALBUM_ARTIST, METADATA_TYPE_STRING); + METADATA_KEYS_TYPE.put(METADATA_KEY_ART, METADATA_TYPE_BITMAP); + METADATA_KEYS_TYPE.put(METADATA_KEY_ART_URI, METADATA_TYPE_STRING); + METADATA_KEYS_TYPE.put(METADATA_KEY_ALBUM_ART, METADATA_TYPE_BITMAP); + METADATA_KEYS_TYPE.put(METADATA_KEY_ALBUM_ART_URI, METADATA_TYPE_STRING); + METADATA_KEYS_TYPE.put(METADATA_KEY_USER_RATING, METADATA_TYPE_RATING); + METADATA_KEYS_TYPE.put(METADATA_KEY_RATING, METADATA_TYPE_RATING); + } + + private final Bundle mBundle; + private Object mMetadataObj; + + private MediaMetadataCompat(Bundle bundle) { + mBundle = new Bundle(bundle); + } + + private MediaMetadataCompat(Parcel in) { + mBundle = in.readBundle(); + } + + /** + * Returns true if the given key is contained in the metadata + * + * @param key a String key + * @return true if the key exists in this metadata, false otherwise + */ + public boolean containsKey(String key) { + return mBundle.containsKey(key); + } + + /** + * Returns the value associated with the given key, or null if no mapping of + * the desired type exists for the given key or a null value is explicitly + * associated with the key. + * + * @param key The key the value is stored under + * @return a String value, or null + */ + public String getString(String key) { + return mBundle.getString(key); + } + + /** + * Returns the value associated with the given key, or 0L if no long exists + * for the given key. + * + * @param key The key the value is stored under + * @return a long value + */ + public long getLong(String key) { + return mBundle.getLong(key, 0); + } + + /** + * Return a {@link RatingCompat} for the given key or null if no rating exists for + * the given key. + * + * @param key The key the value is stored under + * @return A {@link RatingCompat} or null + */ + public RatingCompat getRating(String key) { + RatingCompat rating = null; + try { + rating = mBundle.getParcelable(key); + } catch (Exception e) { + // ignore, value was not a bitmap + Log.w(TAG, "Failed to retrieve a key as Rating.", e); + } + return rating; + } + + /** + * Return a {@link Bitmap} for the given key or null if no bitmap exists for + * the given key. + * + * @param key The key the value is stored under + * @return A {@link Bitmap} or null + */ + public Bitmap getBitmap(String key) { + Bitmap bmp = null; + try { + bmp = mBundle.getParcelable(key); + } catch (Exception e) { + // ignore, value was not a bitmap + Log.w(TAG, "Failed to retrieve a key as Bitmap.", e); + } + return bmp; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeBundle(mBundle); + } + + /** + * Get the number of fields in this metadata. + * + * @return The number of fields in the metadata. + */ + public int size() { + return mBundle.size(); + } + + /** + * Returns a Set containing the Strings used as keys in this metadata. + * + * @return a Set of String keys + */ + public Set<String> keySet() { + return mBundle.keySet(); + } + + /** + * Creates an instance from a framework {@link android.media.MediaMetadata} object. + * <p> + * This method is only supported on API 21+. + * </p> + * + * @param metadataObj A {@link android.media.MediaMetadata} object, or null if none. + * @return An equivalent {@link MediaMetadataCompat} object, or null if none. + */ + public static MediaMetadataCompat fromMediaMetadata(Object metadataObj) { + if (metadataObj == null || Build.VERSION.SDK_INT < 21) { + return null; + } + + Builder builder = new Builder(); + for (String key : MediaMetadataCompatApi21.keySet(metadataObj)) { + Integer type = METADATA_KEYS_TYPE.get(key); + if (type != null) { + switch (type) { + case METADATA_TYPE_BITMAP: + builder.putBitmap(key, + MediaMetadataCompatApi21.getBitmap(metadataObj, key)); + break; + case METADATA_TYPE_LONG: + builder.putLong(key, + MediaMetadataCompatApi21.getLong(metadataObj, key)); + break; + case METADATA_TYPE_RATING: + builder.putRating(key, RatingCompat.fromRating( + MediaMetadataCompatApi21.getRating(metadataObj, key))); + break; + case METADATA_TYPE_STRING: + builder.putString(key, + MediaMetadataCompatApi21.getString(metadataObj, key)); + break; + } + } + } + MediaMetadataCompat metadata = builder.build(); + metadata.mMetadataObj = metadataObj; + return metadata; + } + + /** + * Gets the underlying framework {@link android.media.MediaMetadata} object. + * <p> + * This method is only supported on API 21+. + * </p> + * + * @return An equivalent {@link android.media.MediaMetadata} object, or null if none. + */ + public Object getMediaMetadata() { + if (mMetadataObj != null || Build.VERSION.SDK_INT < 21) { + return mMetadataObj; + } + + Object builderObj = MediaMetadataCompatApi21.Builder.newInstance(); + for (String key : keySet()) { + Integer type = METADATA_KEYS_TYPE.get(key); + if (type != null) { + switch (type) { + case METADATA_TYPE_BITMAP: + MediaMetadataCompatApi21.Builder.putBitmap(builderObj, key, + getBitmap(key)); + break; + case METADATA_TYPE_LONG: + MediaMetadataCompatApi21.Builder.putLong(builderObj, key, + getLong(key)); + break; + case METADATA_TYPE_RATING: + MediaMetadataCompatApi21.Builder.putRating(builderObj, key, + getRating(key).getRating()); + break; + case METADATA_TYPE_STRING: + MediaMetadataCompatApi21.Builder.putString(builderObj, key, + getString(key)); + break; + } + } + } + mMetadataObj = MediaMetadataCompatApi21.Builder.build(builderObj); + return mMetadataObj; + } + + public static final Parcelable.Creator<MediaMetadataCompat> CREATOR = + new Parcelable.Creator<MediaMetadataCompat>() { + @Override + public MediaMetadataCompat createFromParcel(Parcel in) { + return new MediaMetadataCompat(in); + } + + @Override + public MediaMetadataCompat[] newArray(int size) { + return new MediaMetadataCompat[size]; + } + }; + + /** + * Use to build MediaMetadata objects. The system defined metadata keys must + * use the appropriate data type. + */ + public static final class Builder { + private final Bundle mBundle; + + /** + * Create an empty Builder. Any field that should be included in the + * {@link MediaMetadataCompat} must be added. + */ + public Builder() { + mBundle = new Bundle(); + } + + /** + * Create a Builder using a {@link MediaMetadataCompat} instance to set the + * initial values. All fields in the source metadata will be included in + * the new metadata. Fields can be overwritten by adding the same key. + * + * @param source + */ + public Builder(MediaMetadataCompat source) { + mBundle = new Bundle(source.mBundle); + } + + /** + * Put a String value into the metadata. Custom keys may be used, but if + * the METADATA_KEYs defined in this class are used they may only be one + * of the following: + * <ul> + * <li>{@link #METADATA_KEY_TITLE}</li> + * <li>{@link #METADATA_KEY_ARTIST}</li> + * <li>{@link #METADATA_KEY_ALBUM}</li> + * <li>{@link #METADATA_KEY_AUTHOR}</li> + * <li>{@link #METADATA_KEY_WRITER}</li> + * <li>{@link #METADATA_KEY_COMPOSER}</li> + * <li>{@link #METADATA_KEY_DATE}</li> + * <li>{@link #METADATA_KEY_GENRE}</li> + * <li>{@link #METADATA_KEY_ALBUM_ARTIST}</li> + * <li>{@link #METADATA_KEY_ART_URI}</li> + * <li>{@link #METADATA_KEY_ALBUM_ART_URI}</li> + * </ul> + * + * @param key The key for referencing this value + * @param value The String value to store + * @return The Builder to allow chaining + */ + public Builder putString(String key, String value) { + if (METADATA_KEYS_TYPE.containsKey(key)) { + if (METADATA_KEYS_TYPE.get(key) != METADATA_TYPE_STRING) { + throw new IllegalArgumentException("The " + key + + " key cannot be used to put a String"); + } + } + mBundle.putString(key, value); + return this; + } + + /** + * Put a long value into the metadata. Custom keys may be used, but if + * the METADATA_KEYs defined in this class are used they may only be one + * of the following: + * <ul> + * <li>{@link #METADATA_KEY_DURATION}</li> + * <li>{@link #METADATA_KEY_TRACK_NUMBER}</li> + * <li>{@link #METADATA_KEY_NUM_TRACKS}</li> + * <li>{@link #METADATA_KEY_DISC_NUMBER}</li> + * <li>{@link #METADATA_KEY_YEAR}</li> + * </ul> + * + * @param key The key for referencing this value + * @param value The String value to store + * @return The Builder to allow chaining + */ + public Builder putLong(String key, long value) { + if (METADATA_KEYS_TYPE.containsKey(key)) { + if (METADATA_KEYS_TYPE.get(key) != METADATA_TYPE_LONG) { + throw new IllegalArgumentException("The " + key + + " key cannot be used to put a long"); + } + } + mBundle.putLong(key, value); + return this; + } + + /** + * Put a {@link RatingCompat} into the metadata. Custom keys may be used, but + * if the METADATA_KEYs defined in this class are used they may only be + * one of the following: + * <ul> + * <li>{@link #METADATA_KEY_RATING}</li> + * <li>{@link #METADATA_KEY_USER_RATING}</li> + * </ul> + * + * @param key The key for referencing this value + * @param value The String value to store + * @return The Builder to allow chaining + */ + public Builder putRating(String key, RatingCompat value) { + if (METADATA_KEYS_TYPE.containsKey(key)) { + if (METADATA_KEYS_TYPE.get(key) != METADATA_TYPE_RATING) { + throw new IllegalArgumentException("The " + key + + " key cannot be used to put a Rating"); + } + } + mBundle.putParcelable(key, value); + return this; + } + + /** + * Put a {@link Bitmap} into the metadata. Custom keys may be used, but + * if the METADATA_KEYs defined in this class are used they may only be + * one of the following: + * <ul> + * <li>{@link #METADATA_KEY_ART}</li> + * <li>{@link #METADATA_KEY_ALBUM_ART}</li> + * </ul> + * + * @param key The key for referencing this value + * @param value The Bitmap to store + * @return The Builder to allow chaining + */ + public Builder putBitmap(String key, Bitmap value) { + if (METADATA_KEYS_TYPE.containsKey(key)) { + if (METADATA_KEYS_TYPE.get(key) != METADATA_TYPE_BITMAP) { + throw new IllegalArgumentException("The " + key + + " key cannot be used to put a Bitmap"); + } + } + mBundle.putParcelable(key, value); + return this; + } + + /** + * Creates a {@link MediaMetadataCompat} instance with the specified fields. + * + * @return The new MediaMetadata instance + */ + public MediaMetadataCompat build() { + return new MediaMetadataCompat(mBundle); + } + } + +} diff --git a/v4/java/android/support/v4/media/RatingCompat.java b/v4/java/android/support/v4/media/RatingCompat.java new file mode 100644 index 0000000000..c7da6ec4a0 --- /dev/null +++ b/v4/java/android/support/v4/media/RatingCompat.java @@ -0,0 +1,370 @@ +/* + * Copyright (C) 2013 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.support.v4.media; + +import android.os.Build; +import android.os.Parcel; +import android.os.Parcelable; +import android.util.Log; + +/** + * A class to encapsulate rating information used as content metadata. + * A rating is defined by its rating style (see {@link #RATING_HEART}, + * {@link #RATING_THUMB_UP_DOWN}, {@link #RATING_3_STARS}, {@link #RATING_4_STARS}, + * {@link #RATING_5_STARS} or {@link #RATING_PERCENTAGE}) and the actual rating value (which may + * be defined as "unrated"), both of which are defined when the rating instance is constructed + * through one of the factory methods. + */ +public final class RatingCompat implements Parcelable { + private final static String TAG = "Rating"; + + /** + * Indicates a rating style is not supported. A Rating will never have this + * type, but can be used by other classes to indicate they do not support + * Rating. + */ + public final static int RATING_NONE = 0; + + /** + * A rating style with a single degree of rating, "heart" vs "no heart". Can be used to + * indicate the content referred to is a favorite (or not). + */ + public final static int RATING_HEART = 1; + + /** + * A rating style for "thumb up" vs "thumb down". + */ + public final static int RATING_THUMB_UP_DOWN = 2; + + /** + * A rating style with 0 to 3 stars. + */ + public final static int RATING_3_STARS = 3; + + /** + * A rating style with 0 to 4 stars. + */ + public final static int RATING_4_STARS = 4; + + /** + * A rating style with 0 to 5 stars. + */ + public final static int RATING_5_STARS = 5; + + /** + * A rating style expressed as a percentage. + */ + public final static int RATING_PERCENTAGE = 6; + + private final static float RATING_NOT_RATED = -1.0f; + + private final int mRatingStyle; + private final float mRatingValue; + + private Object mRatingObj; // framework Rating object + + private RatingCompat(int ratingStyle, float rating) { + mRatingStyle = ratingStyle; + mRatingValue = rating; + } + + @Override + public String toString() { + return "Rating:style=" + mRatingStyle + " rating=" + + (mRatingValue < 0.0f ? "unrated" : String.valueOf(mRatingValue)); + } + + @Override + public int describeContents() { + return mRatingStyle; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(mRatingStyle); + dest.writeFloat(mRatingValue); + } + + public static final Parcelable.Creator<RatingCompat> CREATOR + = new Parcelable.Creator<RatingCompat>() { + /** + * Rebuilds a Rating previously stored with writeToParcel(). + * @param p Parcel object to read the Rating from + * @return a new Rating created from the data in the parcel + */ + @Override + public RatingCompat createFromParcel(Parcel p) { + return new RatingCompat(p.readInt(), p.readFloat()); + } + + @Override + public RatingCompat[] newArray(int size) { + return new RatingCompat[size]; + } + }; + + /** + * Return a Rating instance with no rating. + * Create and return a new Rating instance with no rating known for the given + * rating style. + * @param ratingStyle one of {@link #RATING_HEART}, {@link #RATING_THUMB_UP_DOWN}, + * {@link #RATING_3_STARS}, {@link #RATING_4_STARS}, {@link #RATING_5_STARS}, + * or {@link #RATING_PERCENTAGE}. + * @return null if an invalid rating style is passed, a new Rating instance otherwise. + */ + public static RatingCompat newUnratedRating(int ratingStyle) { + switch(ratingStyle) { + case RATING_HEART: + case RATING_THUMB_UP_DOWN: + case RATING_3_STARS: + case RATING_4_STARS: + case RATING_5_STARS: + case RATING_PERCENTAGE: + return new RatingCompat(ratingStyle, RATING_NOT_RATED); + default: + return null; + } + } + + /** + * Return a Rating instance with a heart-based rating. + * Create and return a new Rating instance with a rating style of {@link #RATING_HEART}, + * and a heart-based rating. + * @param hasHeart true for a "heart selected" rating, false for "heart unselected". + * @return a new Rating instance. + */ + public static RatingCompat newHeartRating(boolean hasHeart) { + return new RatingCompat(RATING_HEART, hasHeart ? 1.0f : 0.0f); + } + + /** + * Return a Rating instance with a thumb-based rating. + * Create and return a new Rating instance with a {@link #RATING_THUMB_UP_DOWN} + * rating style, and a "thumb up" or "thumb down" rating. + * @param thumbIsUp true for a "thumb up" rating, false for "thumb down". + * @return a new Rating instance. + */ + public static RatingCompat newThumbRating(boolean thumbIsUp) { + return new RatingCompat(RATING_THUMB_UP_DOWN, thumbIsUp ? 1.0f : 0.0f); + } + + /** + * Return a Rating instance with a star-based rating. + * Create and return a new Rating instance with one of the star-base rating styles + * and the given integer or fractional number of stars. Non integer values can for instance + * be used to represent an average rating value, which might not be an integer number of stars. + * @param starRatingStyle one of {@link #RATING_3_STARS}, {@link #RATING_4_STARS}, + * {@link #RATING_5_STARS}. + * @param starRating a number ranging from 0.0f to 3.0f, 4.0f or 5.0f according to + * the rating style. + * @return null if the rating style is invalid, or the rating is out of range, + * a new Rating instance otherwise. + */ + public static RatingCompat newStarRating(int starRatingStyle, float starRating) { + float maxRating = -1.0f; + switch(starRatingStyle) { + case RATING_3_STARS: + maxRating = 3.0f; + break; + case RATING_4_STARS: + maxRating = 4.0f; + break; + case RATING_5_STARS: + maxRating = 5.0f; + break; + default: + Log.e(TAG, "Invalid rating style (" + starRatingStyle + ") for a star rating"); + return null; + } + if ((starRating < 0.0f) || (starRating > maxRating)) { + Log.e(TAG, "Trying to set out of range star-based rating"); + return null; + } + return new RatingCompat(starRatingStyle, starRating); + } + + /** + * Return a Rating instance with a percentage-based rating. + * Create and return a new Rating instance with a {@link #RATING_PERCENTAGE} + * rating style, and a rating of the given percentage. + * @param percent the value of the rating + * @return null if the rating is out of range, a new Rating instance otherwise. + */ + public static RatingCompat newPercentageRating(float percent) { + if ((percent < 0.0f) || (percent > 100.0f)) { + Log.e(TAG, "Invalid percentage-based rating value"); + return null; + } else { + return new RatingCompat(RATING_PERCENTAGE, percent); + } + } + + /** + * Return whether there is a rating value available. + * @return true if the instance was not created with {@link #newUnratedRating(int)}. + */ + public boolean isRated() { + return mRatingValue >= 0.0f; + } + + /** + * Return the rating style. + * @return one of {@link #RATING_HEART}, {@link #RATING_THUMB_UP_DOWN}, + * {@link #RATING_3_STARS}, {@link #RATING_4_STARS}, {@link #RATING_5_STARS}, + * or {@link #RATING_PERCENTAGE}. + */ + public int getRatingStyle() { + return mRatingStyle; + } + + /** + * Return whether the rating is "heart selected". + * @return true if the rating is "heart selected", false if the rating is "heart unselected", + * if the rating style is not {@link #RATING_HEART} or if it is unrated. + */ + public boolean hasHeart() { + if (mRatingStyle != RATING_HEART) { + return false; + } else { + return (mRatingValue == 1.0f); + } + } + + /** + * Return whether the rating is "thumb up". + * @return true if the rating is "thumb up", false if the rating is "thumb down", + * if the rating style is not {@link #RATING_THUMB_UP_DOWN} or if it is unrated. + */ + public boolean isThumbUp() { + if (mRatingStyle != RATING_THUMB_UP_DOWN) { + return false; + } else { + return (mRatingValue == 1.0f); + } + } + + /** + * Return the star-based rating value. + * @return a rating value greater or equal to 0.0f, or a negative value if the rating style is + * not star-based, or if it is unrated. + */ + public float getStarRating() { + switch (mRatingStyle) { + case RATING_3_STARS: + case RATING_4_STARS: + case RATING_5_STARS: + if (isRated()) { + return mRatingValue; + } + default: + return -1.0f; + } + } + + /** + * Return the percentage-based rating value. + * @return a rating value greater or equal to 0.0f, or a negative value if the rating style is + * not percentage-based, or if it is unrated. + */ + public float getPercentRating() { + if ((mRatingStyle != RATING_PERCENTAGE) || !isRated()) { + return -1.0f; + } else { + return mRatingValue; + } + } + + /** + * Creates an instance from a framework {@link android.media.Rating} object. + * <p> + * This method is only supported on API 21+. + * </p> + * + * @param ratingObj A {@link android.media.Rating} object, or null if none. + * @return An equivalent {@link RatingCompat} object, or null if none. + */ + public static RatingCompat fromRating(Object ratingObj) { + if (ratingObj == null || Build.VERSION.SDK_INT < 21) { + return null; + } + + final int ratingStyle = RatingCompatApi21.getRatingStyle(ratingObj); + final RatingCompat rating; + if (RatingCompatApi21.isRated(ratingObj)) { + switch (ratingStyle) { + case RATING_HEART: + rating = newHeartRating(RatingCompatApi21.hasHeart(ratingObj)); + break; + case RATING_THUMB_UP_DOWN: + rating = newThumbRating(RatingCompatApi21.isThumbUp(ratingObj)); + break; + case RATING_3_STARS: + case RATING_4_STARS: + case RATING_5_STARS: + rating = newStarRating(ratingStyle, + RatingCompatApi21.getStarRating(ratingObj)); + break; + case RATING_PERCENTAGE: + rating = newPercentageRating(RatingCompatApi21.getPercentRating(ratingObj)); + break; + default: + return null; + } + } else { + rating = newUnratedRating(ratingStyle); + } + rating.mRatingObj = ratingObj; + return rating; + } + + /** + * Gets the underlying framework {@link android.media.Rating} object. + * <p> + * This method is only supported on API 21+. + * </p> + * + * @return An equivalent {@link android.media.Rating} object, or null if none. + */ + public Object getRating() { + if (mRatingObj != null || Build.VERSION.SDK_INT < 21) { + return mRatingObj; + } + + if (isRated()) { + switch (mRatingStyle) { + case RATING_HEART: + mRatingObj = RatingCompatApi21.newHeartRating(hasHeart()); + break; + case RATING_THUMB_UP_DOWN: + mRatingObj = RatingCompatApi21.newThumbRating(isThumbUp()); + break; + case RATING_3_STARS: + case RATING_4_STARS: + case RATING_5_STARS: + mRatingObj = RatingCompatApi21.newStarRating(mRatingStyle, getStarRating()); + break; + case RATING_PERCENTAGE: + mRatingObj = RatingCompatApi21.newPercentageRating(getPercentRating()); + default: + return null; + } + } else { + mRatingObj = RatingCompatApi21.newUnratedRating(mRatingStyle); + } + return mRatingObj; + } +}
\ No newline at end of file diff --git a/v4/java/android/support/v4/media/VolumeProviderCompat.java b/v4/java/android/support/v4/media/VolumeProviderCompat.java new file mode 100644 index 0000000000..24f44ed890 --- /dev/null +++ b/v4/java/android/support/v4/media/VolumeProviderCompat.java @@ -0,0 +1,168 @@ +/* + * 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.support.v4.media; + +import android.os.Build; +import android.support.v4.media.session.MediaSessionCompat; + +/** + * Handles requests to adjust or set the volume on a session. This is also used + * to push volume updates back to the session after a request has been handled. + * You can set a volume provider on a session by calling + * {@link MediaSessionCompat#setPlaybackToRemote}. + */ +public abstract class VolumeProviderCompat { + /** + * The volume is fixed and can not be modified. Requests to change volume + * should be ignored. + */ + public static final int VOLUME_CONTROL_FIXED = 0; + + /** + * The volume control uses relative adjustment via + * {@link #onAdjustVolumeBy(int)}. Attempts to set the volume to a specific + * value should be ignored. + */ + public static final int VOLUME_CONTROL_RELATIVE = 1; + + /** + * The volume control uses an absolute value. It may be adjusted using + * {@link #onAdjustVolumeBy(int)} or set directly using + * {@link #onSetVolumeTo(int)}. + */ + public static final int VOLUME_CONTROL_ABSOLUTE = 2; + + private final int mControlType; + private final int mMaxVolume; + private Callback mCallback; + + private Object mVolumeProviderObj; + + /** + * Create a new volume provider for handling volume events. You must specify + * the type of volume control and the maximum volume that can be used. + * + * @param volumeControl The method for controlling volume that is used by + * this provider. + * @param maxVolume The maximum allowed volume. + */ + public VolumeProviderCompat(int volumeControl, int maxVolume) { + mControlType = volumeControl; + mMaxVolume = maxVolume; + } + + /** + * Get the current volume of the remote playback. + * + * @return The current volume. + */ + public abstract int onGetCurrentVolume(); + + /** + * Get the volume control type that this volume provider uses. + * + * @return The volume control type for this volume provider + */ + public final int getVolumeControl() { + return mControlType; + } + + /** + * Get the maximum volume this provider allows. + * + * @return The max allowed volume. + */ + public final int getMaxVolume() { + return mMaxVolume; + } + + /** + * Notify the callback that the remote playback's volume has been changed. + */ + public final void notifyVolumeChanged() { + if (mCallback != null) { + mCallback.onVolumeChanged(this); + } + } + + /** + * Override to handle requests to set the volume of the current output. + * + * @param volume The volume to set the output to. + */ + public void onSetVolumeTo(int volume) { + } + + /** + * Override to handle requests to adjust the volume of the current + * output. + * + * @param delta The amount to change the volume + */ + public void onAdjustVolumeBy(int delta) { + } + + /** + * Sets a callback to receive volume changes. + * <p> + * Used internally by the support library. + * <p> + */ + public void setCallback(Callback callback) { + mCallback = callback; + } + + /** + * Gets the underlying framework {@link android.media.VolumeProvider} object. + * <p> + * This method is only supported on API 21+. + * </p> + * + * @return An equivalent {@link android.media.VolumeProvider} object, or null if none. + */ + public Object getVolumeProvider() { + if (mVolumeProviderObj != null || Build.VERSION.SDK_INT < 21) { + return mVolumeProviderObj; + } + + mVolumeProviderObj = VolumeProviderCompatApi21.createVolumeProvider( + mControlType, mMaxVolume, new VolumeProviderCompatApi21.Delegate() { + @Override + public int onGetCurrentVolume() { + return VolumeProviderCompat.this.onGetCurrentVolume(); + } + + @Override + public void onSetVolumeTo(int volume) { + VolumeProviderCompat.this.onSetVolumeTo(volume); + } + + @Override + public void onAdjustVolumeBy(int delta) { + VolumeProviderCompat.this.onAdjustVolumeBy(delta); + } + }); + return mVolumeProviderObj; + } + + /** + * Listens for changes to the volume. + */ + public static abstract class Callback { + public abstract void onVolumeChanged(VolumeProviderCompat volumeProvider); + } +}
\ No newline at end of file diff --git a/v4/java/android/support/v4/media/session/MediaControllerCompat.java b/v4/java/android/support/v4/media/session/MediaControllerCompat.java new file mode 100644 index 0000000000..0b75e08b5d --- /dev/null +++ b/v4/java/android/support/v4/media/session/MediaControllerCompat.java @@ -0,0 +1,617 @@ +/* + * 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.support.v4.media.session; + +import android.content.Context; +import android.os.Bundle; +import android.os.Handler; +import android.os.RemoteException; +import android.os.ResultReceiver; +import android.support.v4.media.MediaMetadataCompat; +import android.support.v4.media.RatingCompat; +import android.support.v4.media.VolumeProviderCompat; +import android.text.TextUtils; +import android.view.KeyEvent; + +/** + * Allows an app to interact with an ongoing media session. Media buttons and + * other commands can be sent to the session. A callback may be registered to + * receive updates from the session, such as metadata and play state changes. + * <p> + * A MediaController can be created if you have a {@link MediaSessionCompat.Token} + * from the session owner. + * <p> + * MediaController objects are thread-safe. + * <p> + * This is a helper for accessing features in {@link android.media.session.MediaSession} + * introduced after API level 4 in a backwards compatible fashion. + */ +public final class MediaControllerCompat { + private final MediaControllerImpl mImpl; + + /** + * Creates a media controller from a session. + * + * @param session The session to be controlled. + */ + public MediaControllerCompat(Context context, MediaSessionCompat session) { + if (session == null) { + throw new IllegalArgumentException("session must not be null"); + } + + if (android.os.Build.VERSION.SDK_INT >= 21) { + mImpl = new MediaControllerImplApi21(context, session); + } else { + mImpl = new MediaControllerImplBase(); + } + } + + /** + * Creates a media controller from a session token which may have + * been obtained from another process. + * + * @param sessionToken The token of the session to be controlled. + * @throws RemoteException if the session is not accessible. + */ + public MediaControllerCompat(Context context, MediaSessionCompat.Token sessionToken) + throws RemoteException { + if (sessionToken == null) { + throw new IllegalArgumentException("sessionToken must not be null"); + } + + if (android.os.Build.VERSION.SDK_INT >= 21) { + mImpl = new MediaControllerImplApi21(context, sessionToken); + } else { + mImpl = new MediaControllerImplBase(); + } + } + + /** + * Get a {@link TransportControls} instance for this session. + * + * @return A controls instance + */ + public TransportControls getTransportControls() { + return mImpl.getTransportControls(); + } + + /** + * Send the specified media button event to the session. Only media keys can + * be sent by this method, other keys will be ignored. + * + * @param keyEvent The media button event to dispatch. + * @return true if the event was sent to the session, false otherwise. + */ + public boolean dispatchMediaButtonEvent(KeyEvent keyEvent) { + if (keyEvent == null) { + throw new IllegalArgumentException("KeyEvent may not be null"); + } + return mImpl.dispatchMediaButtonEvent(keyEvent); + } + + /** + * Get the current playback state for this session. + * + * @return The current PlaybackState or null + */ + public PlaybackStateCompat getPlaybackState() { + return mImpl.getPlaybackState(); + } + + /** + * Get the current metadata for this session. + * + * @return The current MediaMetadata or null. + */ + public MediaMetadataCompat getMetadata() { + return mImpl.getMetadata(); + } + + /** + * Get the rating type supported by the session. One of: + * <ul> + * <li>{@link RatingCompat#RATING_NONE}</li> + * <li>{@link RatingCompat#RATING_HEART}</li> + * <li>{@link RatingCompat#RATING_THUMB_UP_DOWN}</li> + * <li>{@link RatingCompat#RATING_3_STARS}</li> + * <li>{@link RatingCompat#RATING_4_STARS}</li> + * <li>{@link RatingCompat#RATING_5_STARS}</li> + * <li>{@link RatingCompat#RATING_PERCENTAGE}</li> + * </ul> + * + * @return The supported rating type + */ + public int getRatingType() { + return mImpl.getRatingType(); + } + + /** + * Get the current volume info for this session. + * + * @return The current volume info or null. + */ + public VolumeInfo getVolumeInfo() { + return mImpl.getVolumeInfo(); + } + + /** + * Adds a callback to receive updates from the Session. Updates will be + * posted on the caller's thread. + * + * @param callback The callback object, must not be null. + */ + public void addCallback(Callback callback) { + addCallback(callback, null); + } + + /** + * Adds a callback to receive updates from the session. Updates will be + * posted on the specified handler's thread. + * + * @param callback The callback object, must not be null. + * @param handler The handler to post updates on. If null the callers thread + * will be used. + */ + public void addCallback(Callback callback, Handler handler) { + if (callback == null) { + throw new IllegalArgumentException("callback cannot be null"); + } + if (handler == null) { + handler = new Handler(); + } + mImpl.addCallback(callback, handler); + } + + /** + * Stop receiving updates on the specified callback. If an update has + * already been posted you may still receive it after calling this method. + * + * @param callback The callback to remove + */ + public void removeCallback(Callback callback) { + if (callback == null) { + throw new IllegalArgumentException("callback cannot be null"); + } + mImpl.removeCallback(callback); + } + + /** + * Sends a generic command to the session. It is up to the session creator + * to decide what commands and parameters they will support. As such, + * commands should only be sent to sessions that the controller owns. + * + * @param command The command to send + * @param params Any parameters to include with the command + * @param cb The callback to receive the result on + */ + public void sendControlCommand(String command, Bundle params, ResultReceiver cb) { + if (TextUtils.isEmpty(command)) { + throw new IllegalArgumentException("command cannot be null or empty"); + } + mImpl.sendControlCommand(command, params, cb); + } + + /** + * Gets the underlying framework {@link android.media.session.MediaController} object. + * <p> + * This method is only supported on API 21+. + * </p> + * + * @return The underlying {@link android.media.session.MediaController} object, + * or null if none. + */ + public Object getMediaController() { + return mImpl.getMediaController(); + } + + /** + * Callback for receiving updates on from the session. A Callback can be + * registered using {@link #addCallback} + */ + public static abstract class Callback { + final Object mCallbackObj; + + public Callback() { + if (android.os.Build.VERSION.SDK_INT >= 21) { + mCallbackObj = MediaControllerCompatApi21.createCallback(new StubApi21()); + } else { + mCallbackObj = null; + } + } + + /** + * Override to handle custom events sent by the session owner without a + * specified interface. Controllers should only handle these for + * sessions they own. + * + * @param event The event from the session. + * @param extras Optional parameters for the event. + */ + public void onSessionEvent(String event, Bundle extras) { + } + + /** + * Override to handle changes in playback state. + * + * @param state The new playback state of the session + */ + public void onPlaybackStateChanged(PlaybackStateCompat state) { + } + + /** + * Override to handle changes to the current metadata. + * + * @param metadata The current metadata for the session or null if none. + * @see MediaMetadata + */ + public void onMetadataChanged(MediaMetadataCompat metadata) { + } + + private class StubApi21 implements MediaControllerCompatApi21.Callback { + @Override + public void onSessionEvent(String event, Bundle extras) { + Callback.this.onSessionEvent(event, extras); + } + + @Override + public void onPlaybackStateChanged(Object stateObj) { + Callback.this.onPlaybackStateChanged( + PlaybackStateCompat.fromPlaybackState(stateObj)); + } + + @Override + public void onMetadataChanged(Object metadataObj) { + Callback.this.onMetadataChanged( + MediaMetadataCompat.fromMediaMetadata(metadataObj)); + } + } + } + + /** + * Interface for controlling media playback on a session. This allows an app + * to send media transport commands to the session. + */ + public static abstract class TransportControls { + TransportControls() { + } + + /** + * Request that the player start its playback at its current position. + */ + public abstract void play(); + + /** + * Request that the player pause its playback and stay at its current + * position. + */ + public abstract void pause(); + + /** + * Request that the player stop its playback; it may clear its state in + * whatever way is appropriate. + */ + public abstract void stop(); + + /** + * Move to a new location in the media stream. + * + * @param pos Position to move to, in milliseconds. + */ + public abstract void seekTo(long pos); + + /** + * Start fast forwarding. If playback is already fast forwarding this + * may increase the rate. + */ + public abstract void fastForward(); + + /** + * Skip to the next item. + */ + public abstract void skipToNext(); + + /** + * Start rewinding. If playback is already rewinding this may increase + * the rate. + */ + public abstract void rewind(); + + /** + * Skip to the previous item. + */ + public abstract void skipToPrevious(); + + /** + * Rate the current content. This will cause the rating to be set for + * the current user. The Rating type must match the type returned by + * {@link #getRatingType()}. + * + * @param rating The rating to set for the current content + */ + public abstract void setRating(RatingCompat rating); + } + + /** + * Holds information about the way volume is handled for this session. + */ + public static final class VolumeInfo { + private final int mVolumeType; + private final int mAudioStream; + private final int mVolumeControl; + private final int mMaxVolume; + private final int mCurrentVolume; + + VolumeInfo(int type, int stream, int control, int max, int current) { + mVolumeType = type; + mAudioStream = stream; + mVolumeControl = control; + mMaxVolume = max; + mCurrentVolume = current; + } + + /** + * Get the type of volume handling, either local or remote. One of: + * <ul> + * <li>{@link MediaSessionCompat#VOLUME_TYPE_LOCAL}</li> + * <li>{@link MediaSessionCompat#VOLUME_TYPE_REMOTE}</li> + * </ul> + * + * @return The type of volume handling this session is using. + */ + public int getVolumeType() { + return mVolumeType; + } + + /** + * Get the stream this is currently controlling volume on. When the volume + * type is {@link MediaSessionCompat#VOLUME_TYPE_REMOTE} this value does not + * have meaning and should be ignored. + * + * @return The stream this session is playing on. + */ + public int getAudioStream() { + return mAudioStream; + } + + /** + * Get the type of volume control that can be used. One of: + * <ul> + * <li>{@link VolumeProviderCompat#VOLUME_CONTROL_ABSOLUTE}</li> + * <li>{@link VolumeProviderCompat#VOLUME_CONTROL_RELATIVE}</li> + * <li>{@link VolumeProviderCompat#VOLUME_CONTROL_FIXED}</li> + * </ul> + * + * @return The type of volume control that may be used with this + * session. + */ + public int getVolumeControl() { + return mVolumeControl; + } + + /** + * Get the maximum volume that may be set for this session. + * + * @return The maximum allowed volume where this session is playing. + */ + public int getMaxVolume() { + return mMaxVolume; + } + + /** + * Get the current volume for this session. + * + * @return The current volume where this session is playing. + */ + public int getCurrentVolume() { + return mCurrentVolume; + } + } + + interface MediaControllerImpl { + void addCallback(Callback callback, Handler handler); + void removeCallback(Callback callback); + boolean dispatchMediaButtonEvent(KeyEvent keyEvent); + TransportControls getTransportControls(); + PlaybackStateCompat getPlaybackState(); + MediaMetadataCompat getMetadata(); + int getRatingType(); + VolumeInfo getVolumeInfo(); + void sendControlCommand(String command, Bundle params, ResultReceiver cb); + Object getMediaController(); + } + + // TODO: compatibility implementation + static class MediaControllerImplBase implements MediaControllerImpl { + @Override + public void addCallback(Callback callback, Handler handler) { + } + + @Override + public void removeCallback(Callback callback) { + } + + @Override + public boolean dispatchMediaButtonEvent(KeyEvent event) { + return false; + } + + @Override + public TransportControls getTransportControls() { + return null; + } + + @Override + public PlaybackStateCompat getPlaybackState() { + return null; + } + + @Override + public MediaMetadataCompat getMetadata() { + return null; + } + + @Override + public int getRatingType() { + return 0; + } + + @Override + public VolumeInfo getVolumeInfo() { + return null; + } + + @Override + public void sendControlCommand(String command, Bundle params, ResultReceiver cb) { + } + + @Override + public Object getMediaController() { + return null; + } + } + + static class MediaControllerImplApi21 implements MediaControllerImpl { + private final Object mControllerObj; + + public MediaControllerImplApi21(Context context, MediaSessionCompat session) { + mControllerObj = MediaControllerCompatApi21.fromToken( + session.getSessionToken().getToken()); + } + + public MediaControllerImplApi21(Context context, MediaSessionCompat.Token sessionToken) + throws RemoteException { + // TODO: refactor framework implementation + mControllerObj = MediaControllerCompatApi21.fromToken( + sessionToken.getToken()); + if (mControllerObj == null) throw new RemoteException(); + } + + @Override + public void addCallback(Callback callback, Handler handler) { + MediaControllerCompatApi21.addCallback(mControllerObj, callback.mCallbackObj, handler); + } + + @Override + public void removeCallback(Callback callback) { + MediaControllerCompatApi21.removeCallback(mControllerObj, callback.mCallbackObj); + } + + @Override + public boolean dispatchMediaButtonEvent(KeyEvent event) { + return MediaControllerCompatApi21.dispatchMediaButtonEvent(mControllerObj, event); + } + + @Override + public TransportControls getTransportControls() { + Object controlsObj = MediaControllerCompatApi21.getTransportControls(mControllerObj); + return controlsObj != null ? new TransportControlsApi21(controlsObj) : null; + } + + @Override + public PlaybackStateCompat getPlaybackState() { + Object stateObj = MediaControllerCompatApi21.getPlaybackState(mControllerObj); + return stateObj != null ? PlaybackStateCompat.fromPlaybackState(stateObj) : null; + } + + @Override + public MediaMetadataCompat getMetadata() { + Object metadataObj = MediaControllerCompatApi21.getMetadata(mControllerObj); + return metadataObj != null ? MediaMetadataCompat.fromMediaMetadata(metadataObj) : null; + } + + @Override + public int getRatingType() { + return MediaControllerCompatApi21.getRatingType(mControllerObj); + } + + @Override + public VolumeInfo getVolumeInfo() { + Object volumeInfoObj = MediaControllerCompatApi21.getVolumeInfo(mControllerObj); + return volumeInfoObj != null ? new VolumeInfo( + MediaControllerCompatApi21.VolumeInfo.getVolumeType(volumeInfoObj), + MediaControllerCompatApi21.VolumeInfo.getAudioStream(volumeInfoObj), + MediaControllerCompatApi21.VolumeInfo.getVolumeControl(volumeInfoObj), + MediaControllerCompatApi21.VolumeInfo.getMaxVolume(volumeInfoObj), + MediaControllerCompatApi21.VolumeInfo.getCurrentVolume(volumeInfoObj)) : null; + } + + @Override + public void sendControlCommand(String command, Bundle params, ResultReceiver cb) { + MediaControllerCompatApi21.sendControlCommand(mControllerObj, + command, params, cb); + } + + @Override + public Object getMediaController() { + return mControllerObj; + } + } + + static class TransportControlsApi21 extends TransportControls { + private final Object mControlsObj; + + public TransportControlsApi21(Object controlsObj) { + mControlsObj = controlsObj; + } + + @Override + public void play() { + MediaControllerCompatApi21.TransportControls.play(mControlsObj); + } + + @Override + public void pause() { + MediaControllerCompatApi21.TransportControls.pause(mControlsObj); + } + + @Override + public void stop() { + MediaControllerCompatApi21.TransportControls.stop(mControlsObj); + } + + @Override + public void seekTo(long pos) { + MediaControllerCompatApi21.TransportControls.seekTo(mControlsObj, pos); + } + + @Override + public void fastForward() { + MediaControllerCompatApi21.TransportControls.fastForward(mControlsObj); + } + + @Override + public void rewind() { + MediaControllerCompatApi21.TransportControls.rewind(mControlsObj); + } + + @Override + public void skipToNext() { + MediaControllerCompatApi21.TransportControls.skipToNext(mControlsObj); + } + + @Override + public void skipToPrevious() { + MediaControllerCompatApi21.TransportControls.skipToPrevious(mControlsObj); + } + + @Override + public void setRating(RatingCompat rating) { + MediaControllerCompatApi21.TransportControls.setRating(mControlsObj, + rating != null ? rating.getRating() : null); + } + } +} diff --git a/v4/java/android/support/v4/media/session/MediaSessionCompat.java b/v4/java/android/support/v4/media/session/MediaSessionCompat.java new file mode 100644 index 0000000000..9e8424cc0a --- /dev/null +++ b/v4/java/android/support/v4/media/session/MediaSessionCompat.java @@ -0,0 +1,711 @@ + +/* + * 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.support.v4.media.session; + +import android.content.Context; +import android.content.Intent; +import android.media.AudioManager; +import android.os.Bundle; +import android.os.Handler; +import android.os.Parcel; +import android.os.Parcelable; +import android.os.ResultReceiver; +import android.support.v4.media.MediaMetadataCompat; +import android.support.v4.media.RatingCompat; +import android.support.v4.media.VolumeProviderCompat; +import android.text.TextUtils; + +/** + * Allows interaction with media controllers, volume keys, media buttons, and + * transport controls. + * <p> + * A MediaSession should be created when an app wants to publish media playback + * information or handle media keys. In general an app only needs one session + * for all playback, though multiple sessions can be created to provide finer + * grain controls of media. + * <p> + * Once a session is created the owner of the session may pass its + * {@link #getSessionToken() session token} to other processes to allow them to + * create a {@link MediaControllerCompat} to interact with the session. + * <p> + * To receive commands, media keys, and other events a {@link Callback} must be + * set with {@link #addCallback(Callback)}. To receive transport control + * commands a {@link TransportControlsCallback} must be set with + * {@link #addTransportControlsCallback}. + * <p> + * When an app is finished performing playback it must call {@link #release()} + * to clean up the session and notify any controllers. + * <p> + * MediaSession objects are thread safe. + * <p> + * This is a helper for accessing features in {@link android.media.session.MediaSession} + * introduced after API level 4 in a backwards compatible fashion. + */ +public class MediaSessionCompat { + private final MediaSessionImpl mImpl; + + /** + * Set this flag on the session to indicate that it can handle media button + * events. + */ + public static final int FLAG_HANDLES_MEDIA_BUTTONS = 1 << 0; + + /** + * Set this flag on the session to indicate that it handles transport + * control commands through a {@link TransportControlsCallback}. + * The callback can be retrieved by calling {@link #addTransportControlsCallback}. + */ + public static final int FLAG_HANDLES_TRANSPORT_CONTROLS = 1 << 1; + + /** + * The session uses local playback. + */ + public static final int VOLUME_TYPE_LOCAL = 1; + + /** + * The session uses remote playback. + */ + public static final int VOLUME_TYPE_REMOTE = 2; + + /** + * Creates a new session. + * + * @param context The context. + * @param tag A short name for debugging purposes. + */ + public MediaSessionCompat(Context context, String tag) { + if (context == null) { + throw new IllegalArgumentException("context must not be null"); + } + if (TextUtils.isEmpty(tag)) { + throw new IllegalArgumentException("tag must not be null or empty"); + } + + if (android.os.Build.VERSION.SDK_INT >= 21) { + mImpl = new MediaSessionImplApi21(context, tag); + } else { + mImpl = new MediaSessionImplBase(); + } + } + + /** + * Add a callback to receive updates on for the MediaSession. This includes + * media button and volume events. The caller's thread will be used to post + * events. + * + * @param callback The callback object + */ + public void addCallback(Callback callback) { + addCallback(callback, null); + } + + /** + * Add a callback to receive updates for the MediaSession. This includes + * media button and volume events. + * + * @param callback The callback to receive updates on. + * @param handler The handler that events should be posted on. + */ + public void addCallback(Callback callback, Handler handler) { + if (callback == null) { + throw new IllegalArgumentException("callback must not be null"); + } + mImpl.addCallback(callback, handler != null ? handler : new Handler()); + } + + /** + * Remove a callback. It will no longer receive updates. + * + * @param callback The callback to remove. + */ + public void removeCallback(Callback callback) { + if (callback == null) { + throw new IllegalArgumentException("callback must not be null"); + } + mImpl.removeCallback(callback); + } + + /** + * Set any flags for the session. + * + * @param flags The flags to set for this session. + */ + public void setFlags(int flags) { + mImpl.setFlags(flags); + } + + /** + * Set the stream this session is playing on. This will affect the system's + * volume handling for this session. If {@link #setPlaybackToRemote} was + * previously called it will stop receiving volume commands and the system + * will begin sending volume changes to the appropriate stream. + * <p> + * By default sessions are on {@link AudioManager#STREAM_MUSIC}. + * + * @param stream The {@link AudioManager} stream this session is playing on. + */ + public void setPlaybackToLocal(int stream) { + mImpl.setPlaybackToLocal(stream); + } + + /** + * Configure this session to use remote volume handling. This must be called + * to receive volume button events, otherwise the system will adjust the + * current stream volume for this session. If {@link #setPlaybackToLocal} + * was previously called that stream will stop receiving volume changes for + * this session. + * + * @param volumeProvider The provider that will handle volume changes. May + * not be null. + */ + public void setPlaybackToRemote(VolumeProviderCompat volumeProvider) { + if (volumeProvider == null) { + throw new IllegalArgumentException("volumeProvider may not be null!"); + } + mImpl.setPlaybackToRemote(volumeProvider); + } + + /** + * Set if this session is currently active and ready to receive commands. If + * set to false your session's controller may not be discoverable. You must + * set the session to active before it can start receiving media button + * events or transport commands. + * + * @param active Whether this session is active or not. + */ + public void setActive(boolean active) { + mImpl.setActive(active); + } + + /** + * Get the current active state of this session. + * + * @return True if the session is active, false otherwise. + */ + public boolean isActive() { + return mImpl.isActive(); + } + + /** + * Send a proprietary event to all MediaControllers listening to this + * Session. It's up to the Controller/Session owner to determine the meaning + * of any events. + * + * @param event The name of the event to send + * @param extras Any extras included with the event + */ + public void sendSessionEvent(String event, Bundle extras) { + if (TextUtils.isEmpty(event)) { + throw new IllegalArgumentException("event cannot be null or empty"); + } + mImpl.sendSessionEvent(event, extras); + } + + /** + * This must be called when an app has finished performing playback. If + * playback is expected to start again shortly the session can be left open, + * but it must be released if your activity or service is being destroyed. + */ + public void release() { + mImpl.release(); + } + + /** + * Retrieve a token object that can be used by apps to create a + * {@link MediaControllerCompat} for interacting with this session. The owner of + * the session is responsible for deciding how to distribute these tokens. + * + * @return A token that can be used to create a MediaController for this + * session. + */ + public Token getSessionToken() { + return mImpl.getSessionToken(); + } + + /** + * Add a callback to receive transport controls on, such as play, rewind, or + * fast forward. + * + * @param callback The callback object. + */ + public void addTransportControlsCallback(TransportControlsCallback callback) { + addTransportControlsCallback(callback, null); + } + + /** + * Add a callback to receive transport controls on, such as play, rewind, or + * fast forward. The updates will be posted to the specified handler. If no + * handler is provided they will be posted to the caller's thread. + * + * @param callback The callback to receive updates on. + * @param handler The handler to post the updates on. + */ + public void addTransportControlsCallback(TransportControlsCallback callback, + Handler handler) { + if (callback == null) { + throw new IllegalArgumentException("Callback cannot be null"); + } + mImpl.addTransportControlsCallback(callback, handler != null ? handler : new Handler()); + } + + /** + * Stop receiving transport controls on the specified callback. If an update + * has already been posted you may still receive it after this call returns. + * + * @param callback The callback to stop receiving updates on. + */ + public void removeTransportControlsCallback(TransportControlsCallback callback) { + if (callback == null) { + throw new IllegalArgumentException("Callback cannot be null"); + } + mImpl.removeTransportControlsCallback(callback); + } + + /** + * Update the current playback state. + * + * @param state The current state of playback + */ + public void setPlaybackState(PlaybackStateCompat state) { + mImpl.setPlaybackState(state); + } + + /** + * Update the current metadata. New metadata can be created using + * {@link android.media.MediaMetadata.Builder}. + * + * @param metadata The new metadata + */ + public void setMetadata(MediaMetadataCompat metadata) { + mImpl.setMetadata(metadata); + } + + /** + * Gets the underlying framework {@link android.media.session.MediaSession} object. + * <p> + * This method is only supported on API 21+. + * </p> + * + * @return The underlying {@link android.media.session.MediaSession} object, + * or null if none. + */ + public Object getMediaSession() { + return mImpl.getMediaSession(); + } + + /** + * Receives generic commands or updates from controllers and the system. + * Callbacks may be registered using {@link #addCallback}. + */ + public abstract static class Callback { + final Object mCallbackObj; + + public Callback() { + if (android.os.Build.VERSION.SDK_INT >= 21) { + mCallbackObj = MediaSessionCompatApi21.createCallback(new StubApi21()); + } else { + mCallbackObj = null; + } + } + + /** + * Called when a media button is pressed and this session has the + * highest priority or a controller sends a media button event to the + * session. TODO determine if using Intents identical to the ones + * RemoteControlClient receives is useful + * <p> + * The intent will be of type {@link Intent#ACTION_MEDIA_BUTTON} with a + * KeyEvent in {@link Intent#EXTRA_KEY_EVENT} + * + * @param mediaButtonIntent an intent containing the KeyEvent as an + * extra + */ + public void onMediaButtonEvent(Intent mediaButtonIntent) { + } + + /** + * Called when a controller has sent a custom command to this session. + * The owner of the session may handle custom commands but is not + * required to. + * + * @param command The command name. + * @param extras Optional parameters for the command, may be null. + * @param cb A result receiver to which a result may be sent by the command, may be null. + */ + public void onControlCommand(String command, Bundle extras, ResultReceiver cb) { + } + + private class StubApi21 implements MediaSessionCompatApi21.Callback { + @Override + public void onMediaButtonEvent(Intent mediaButtonIntent) { + Callback.this.onMediaButtonEvent(mediaButtonIntent); + } + + @Override + public void onControlCommand( + String command, Bundle extras, ResultReceiver cb) { + Callback.this.onControlCommand(command, extras, cb); + } + } + } + + /** + * Receives transport control commands. Callbacks may be registered using + * {@link #addTransportControlsCallback}. + */ + public static abstract class TransportControlsCallback { + final Object mCallbackObj; + + public TransportControlsCallback() { + if (android.os.Build.VERSION.SDK_INT >= 21) { + mCallbackObj = MediaSessionCompatApi21.createTransportControlsCallback( + new StubApi21()); + } else { + mCallbackObj = null; + } + } + + /** + * Override to handle requests to begin playback. + */ + public void onPlay() { + } + + /** + * Override to handle requests to pause playback. + */ + public void onPause() { + } + + /** + * Override to handle requests to skip to the next media item. + */ + public void onSkipToNext() { + } + + /** + * Override to handle requests to skip to the previous media item. + */ + public void onSkipToPrevious() { + } + + /** + * Override to handle requests to fast forward. + */ + public void onFastForward() { + } + + /** + * Override to handle requests to rewind. + */ + public void onRewind() { + } + + /** + * Override to handle requests to stop playback. + */ + public void onStop() { + } + + /** + * Override to handle requests to seek to a specific position in ms. + * + * @param pos New position to move to, in milliseconds. + */ + public void onSeekTo(long pos) { + } + + /** + * Override to handle the item being rated. + * + * @param rating + */ + public void onSetRating(RatingCompat rating) { + } + + private class StubApi21 implements MediaSessionCompatApi21.TransportControlsCallback { + @Override + public void onPlay() { + TransportControlsCallback.this.onPlay(); + } + + @Override + public void onPause() { + TransportControlsCallback.this.onPause(); + } + + @Override + public void onSkipToNext() { + TransportControlsCallback.this.onSkipToNext(); + } + + @Override + public void onSkipToPrevious() { + TransportControlsCallback.this.onSkipToPrevious(); + } + + @Override + public void onFastForward() { + TransportControlsCallback.this.onFastForward(); + } + + @Override + public void onRewind() { + TransportControlsCallback.this.onRewind(); + } + + @Override + public void onStop() { + TransportControlsCallback.this.onStop(); + } + + @Override + public void onSeekTo(long pos) { + TransportControlsCallback.this.onSeekTo(pos); + } + + @Override + public void onSetRating(Object ratingObj) { + TransportControlsCallback.this.onSetRating(RatingCompat.fromRating(ratingObj)); + } + } + } + + /** + * Represents an ongoing session. This may be passed to apps by the session + * owner to allow them to create a {@link MediaControllerCompat} to communicate with + * the session. + */ + public static final class Token implements Parcelable { + private final Parcelable mInner; + + Token(Parcelable inner) { + mInner = inner; + } + + @Override + public int describeContents() { + return mInner.describeContents(); + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeParcelable(mInner, flags); + } + + /** + * Gets the underlying framework {@link android.media.session.MediaSessionToken} object. + * <p> + * This method is only supported on API 21+. + * </p> + * + * @return The underlying {@link android.media.session.MediaSessionToken} object, + * or null if none. + */ + public Object getToken() { + return mInner; + } + + public static final Parcelable.Creator<Token> CREATOR + = new Parcelable.Creator<Token>() { + @Override + public Token createFromParcel(Parcel in) { + return new Token(in.readParcelable(null)); + } + + @Override + public Token[] newArray(int size) { + return new Token[size]; + } + }; + } + + interface MediaSessionImpl { + void addCallback(Callback callback, Handler handler); + void removeCallback(Callback callback); + void setFlags(int flags); + void setPlaybackToLocal(int stream); + void setPlaybackToRemote(VolumeProviderCompat volumeProvider); + void setActive(boolean active); + boolean isActive(); + void sendSessionEvent(String event, Bundle extras); + void release(); + Token getSessionToken(); + void addTransportControlsCallback(TransportControlsCallback callback, Handler handler); + void removeTransportControlsCallback(TransportControlsCallback callback); + void setPlaybackState(PlaybackStateCompat state); + void setMetadata(MediaMetadataCompat metadata); + Object getMediaSession(); + } + + // TODO: compatibility implementation + static class MediaSessionImplBase implements MediaSessionImpl { + @Override + public void addCallback(Callback callback, Handler handler) { + } + + @Override + public void removeCallback(Callback callback) { + } + + @Override + public void setFlags(int flags) { + } + + @Override + public void setPlaybackToLocal(int stream) { + } + + @Override + public void setPlaybackToRemote(VolumeProviderCompat volumeProvider) { + } + + @Override + public void setActive(boolean active) { + } + + @Override + public boolean isActive() { + return false; + } + + @Override + public void sendSessionEvent(String event, Bundle extras) { + } + + @Override + public void release() { + } + + @Override + public Token getSessionToken() { + return null; + } + + @Override + public void addTransportControlsCallback(TransportControlsCallback callback, + Handler handler) { + } + + @Override + public void removeTransportControlsCallback(TransportControlsCallback callback) { + } + + @Override + public void setPlaybackState(PlaybackStateCompat state) { + } + + @Override + public void setMetadata(MediaMetadataCompat metadata) { + } + + @Override + public Object getMediaSession() { + return null; + } + } + + static class MediaSessionImplApi21 implements MediaSessionImpl { + private final Object mSessionObj; + private final Token mToken; + + public MediaSessionImplApi21(Context context, String tag) { + mSessionObj = MediaSessionCompatApi21.createSession(context, tag); + mToken = new Token(MediaSessionCompatApi21.getSessionToken(mSessionObj)); + } + + @Override + public void addCallback(Callback callback, Handler handler) { + MediaSessionCompatApi21.addCallback(mSessionObj, callback.mCallbackObj, handler); + } + + @Override + public void removeCallback(Callback callback) { + MediaSessionCompatApi21.removeCallback(mSessionObj, callback.mCallbackObj); + } + + @Override + public void setFlags(int flags) { + MediaSessionCompatApi21.setFlags(mSessionObj, flags); + } + + @Override + public void setPlaybackToLocal(int stream) { + MediaSessionCompatApi21.setPlaybackToLocal(mSessionObj, stream); + } + + @Override + public void setPlaybackToRemote(VolumeProviderCompat volumeProvider) { + MediaSessionCompatApi21.setPlaybackToRemote(mSessionObj, + volumeProvider.getVolumeProvider()); + } + + @Override + public void setActive(boolean active) { + MediaSessionCompatApi21.setActive(mSessionObj, active); + } + + @Override + public boolean isActive() { + return MediaSessionCompatApi21.isActive(mSessionObj); + } + + @Override + public void sendSessionEvent(String event, Bundle extras) { + MediaSessionCompatApi21.sendSessionEvent(mSessionObj, event, extras); + } + + @Override + public void release() { + MediaSessionCompatApi21.release(mSessionObj); + } + + @Override + public Token getSessionToken() { + return mToken; + } + + @Override + public void addTransportControlsCallback(TransportControlsCallback callback, + Handler handler) { + MediaSessionCompatApi21.addTransportControlsCallback( + mSessionObj, callback.mCallbackObj, handler); + } + + @Override + public void removeTransportControlsCallback(TransportControlsCallback callback) { + MediaSessionCompatApi21.removeTransportControlsCallback( + mSessionObj, callback.mCallbackObj); + } + + @Override + public void setPlaybackState(PlaybackStateCompat state) { + MediaSessionCompatApi21.setPlaybackState(mSessionObj, state.getPlaybackState()); + } + + @Override + public void setMetadata(MediaMetadataCompat metadata) { + MediaSessionCompatApi21.setMetadata(mSessionObj, metadata.getMediaMetadata()); + } + + @Override + public Object getMediaSession() { + return mSessionObj; + } + } +} diff --git a/v4/java/android/support/v4/media/session/PlaybackStateCompat.java b/v4/java/android/support/v4/media/session/PlaybackStateCompat.java new file mode 100644 index 0000000000..bccad9b417 --- /dev/null +++ b/v4/java/android/support/v4/media/session/PlaybackStateCompat.java @@ -0,0 +1,497 @@ +/* + * 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.support.v4.media.session; + +import android.os.Build; +import android.os.Parcel; +import android.os.Parcelable; +import android.os.SystemClock; +import android.text.TextUtils; + +/** + * Playback state for a {@link MediaSessionCompat}. This includes a state like + * {@link PlaybackStateCompat#STATE_PLAYING}, the current playback position, + * and the current control capabilities. + */ +public final class PlaybackStateCompat implements Parcelable { + /** + * Indicates this performer supports the stop command. + * + * @see Builder#setActions + */ + public static final long ACTION_STOP = 1 << 0; + + /** + * Indicates this performer supports the pause command. + * + * @see Builder#setActions + */ + public static final long ACTION_PAUSE = 1 << 1; + + /** + * Indicates this performer supports the play command. + * + * @see Builder#setActions + */ + public static final long ACTION_PLAY = 1 << 2; + + /** + * Indicates this performer supports the rewind command. + * + * @see Builder#setActions + */ + public static final long ACTION_REWIND = 1 << 3; + + /** + * Indicates this performer supports the previous command. + * + * @see Builder#setActions + */ + public static final long ACTION_SKIP_TO_PREVIOUS = 1 << 4; + + /** + * Indicates this performer supports the next command. + * + * @see Builder#setActions + */ + public static final long ACTION_SKIP_TO_NEXT = 1 << 5; + + /** + * Indicates this performer supports the fast forward command. + * + * @see Builder#setActions + */ + public static final long ACTION_FAST_FORWARD = 1 << 6; + + /** + * Indicates this performer supports the set rating command. + * + * @see Builder#setActions + */ + public static final long ACTION_SET_RATING = 1 << 7; + + /** + * Indicates this performer supports the seek to command. + * + * @see Builder#setActions + */ + public static final long ACTION_SEEK_TO = 1 << 8; + + /** + * Indicates this performer supports the play/pause toggle command. + * + * @see Builder#setActions + */ + public static final long ACTION_PLAY_PAUSE = 1 << 9; + + /** + * This is the default playback state and indicates that no media has been + * added yet, or the performer has been reset and has no content to play. + * + * @see Builder#setState + */ + public final static int STATE_NONE = 0; + + /** + * State indicating this item is currently stopped. + * + * @see Builder#setState + */ + public final static int STATE_STOPPED = 1; + + /** + * State indicating this item is currently paused. + * + * @see Builder#setState + */ + public final static int STATE_PAUSED = 2; + + /** + * State indicating this item is currently playing. + * + * @see Builder#setState + */ + public final static int STATE_PLAYING = 3; + + /** + * State indicating this item is currently fast forwarding. + * + * @see Builder#setState + */ + public final static int STATE_FAST_FORWARDING = 4; + + /** + * State indicating this item is currently rewinding. + * + * @see Builder#setState + */ + public final static int STATE_REWINDING = 5; + + /** + * State indicating this item is currently buffering and will begin playing + * when enough data has buffered. + * + * @see Builder#setState + */ + public final static int STATE_BUFFERING = 6; + + /** + * State indicating this item is currently in an error state. The error + * message should also be set when entering this state. + * + * @see Builder#setState + */ + public final static int STATE_ERROR = 7; + + /** + * State indicating the class doing playback is currently connecting to a + * route. Depending on the implementation you may return to the previous + * state when the connection finishes or enter {@link #STATE_NONE}. If + * the connection failed {@link #STATE_ERROR} should be used. + * @hide + */ + public final static int STATE_CONNECTING = 8; + + /** + * State indicating the player is currently skipping to the previous item. + * + * @see Builder#setState + */ + public final static int STATE_SKIPPING_TO_PREVIOUS = 9; + + /** + * State indicating the player is currently skipping to the next item. + * + * @see Builder#setState + */ + public final static int STATE_SKIPPING_TO_NEXT = 10; + + /** + * Use this value for the position to indicate the position is not known. + */ + public final static long PLAYBACK_POSITION_UNKNOWN = -1; + + private final int mState; + private final long mPosition; + private final long mBufferPosition; + private final float mRate; + private final long mActions; + private final CharSequence mErrorMessage; + private final long mUpdateTime; + + private Object mStateObj; + + private PlaybackStateCompat(int state, long position, long bufferPosition, + float rate, long actions, CharSequence errorMessage, long updateTime) { + mState = state; + mPosition = position; + mBufferPosition = bufferPosition; + mRate = rate; + mActions = actions; + mErrorMessage = errorMessage; + mUpdateTime = updateTime; + } + + private PlaybackStateCompat(Parcel in) { + mState = in.readInt(); + mPosition = in.readLong(); + mRate = in.readFloat(); + mUpdateTime = in.readLong(); + mBufferPosition = in.readLong(); + mActions = in.readLong(); + mErrorMessage = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in); + } + + @Override + public String toString() { + StringBuilder bob = new StringBuilder("PlaybackState {"); + bob.append("state=").append(mState); + bob.append(", position=").append(mPosition); + bob.append(", buffered position=").append(mBufferPosition); + bob.append(", rate=").append(mRate); + bob.append(", updated=").append(mUpdateTime); + bob.append(", actions=").append(mActions); + bob.append(", error=").append(mErrorMessage); + bob.append("}"); + return bob.toString(); + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(mState); + dest.writeLong(mPosition); + dest.writeFloat(mRate); + dest.writeLong(mUpdateTime); + dest.writeLong(mBufferPosition); + dest.writeLong(mActions); + TextUtils.writeToParcel(mErrorMessage, dest, flags); + } + + /** + * Get the current state of playback. One of the following: + * <ul> + * <li> {@link PlaybackStateCompat#STATE_NONE}</li> + * <li> {@link PlaybackStateCompat#STATE_STOPPED}</li> + * <li> {@link PlaybackStateCompat#STATE_PLAYING}</li> + * <li> {@link PlaybackStateCompat#STATE_PAUSED}</li> + * <li> {@link PlaybackStateCompat#STATE_FAST_FORWARDING}</li> + * <li> {@link PlaybackStateCompat#STATE_REWINDING}</li> + * <li> {@link PlaybackStateCompat#STATE_BUFFERING}</li> + * <li> {@link PlaybackStateCompat#STATE_ERROR}</li> + */ + public int getState() { + return mState; + } + + /** + * Get the current playback position in ms. + */ + public long getPosition() { + return mPosition; + } + + /** + * Get the current buffer position in ms. This is the farthest playback + * point that can be reached from the current position using only buffered + * content. + */ + public long getBufferPosition() { + return mBufferPosition; + } + + /** + * Get the current playback rate as a multiple of normal playback. This + * should be negative when rewinding. A value of 1 means normal playback and + * 0 means paused. + * + * @return The current rate of playback. + */ + public float getPlaybackRate() { + return mRate; + } + + /** + * Get the current actions available on this session. This should use a + * bitmask of the available actions. + * <ul> + * <li> {@link PlaybackStateCompat#ACTION_SKIP_TO_PREVIOUS}</li> + * <li> {@link PlaybackStateCompat#ACTION_REWIND}</li> + * <li> {@link PlaybackStateCompat#ACTION_PLAY}</li> + * <li> {@link PlaybackStateCompat#ACTION_PAUSE}</li> + * <li> {@link PlaybackStateCompat#ACTION_STOP}</li> + * <li> {@link PlaybackStateCompat#ACTION_FAST_FORWARD}</li> + * <li> {@link PlaybackStateCompat#ACTION_SKIP_TO_NEXT}</li> + * <li> {@link PlaybackStateCompat#ACTION_SEEK_TO}</li> + * <li> {@link PlaybackStateCompat#ACTION_SET_RATING}</li> + * </ul> + */ + public long getActions() { + return mActions; + } + + /** + * Get a user readable error message. This should be set when the state is + * {@link PlaybackStateCompat#STATE_ERROR}. + */ + public CharSequence getErrorMessage() { + return mErrorMessage; + } + + /** + * Get the elapsed real time at which position was last updated. If the + * position has never been set this will return 0; + * + * @return The last time the position was updated. + */ + public long getLastPositionUpdateTime() { + return mUpdateTime; + } + + /** + * Creates an instance from a framework {@link android.media.session.PlaybackState} object. + * <p> + * This method is only supported on API 21+. + * </p> + * + * @param stateObj A {@link android.media.session.PlaybackState} object, or null if none. + * @return An equivalent {@link PlaybackStateCompat} object, or null if none. + */ + public static PlaybackStateCompat fromPlaybackState(Object stateObj) { + if (stateObj == null || Build.VERSION.SDK_INT < 21) { + return null; + } + + PlaybackStateCompat state = new PlaybackStateCompat( + PlaybackStateCompatApi21.getState(stateObj), + PlaybackStateCompatApi21.getPosition(stateObj), + PlaybackStateCompatApi21.getBufferPosition(stateObj), + PlaybackStateCompatApi21.getPlaybackRate(stateObj), + PlaybackStateCompatApi21.getActions(stateObj), + PlaybackStateCompatApi21.getErrorMessage(stateObj), + PlaybackStateCompatApi21.getLastPositionUpdateTime(stateObj)); + state.mStateObj = stateObj; + return state; + } + + /** + * Gets the underlying framework {@link android.media.session.PlaybackState} object. + * <p> + * This method is only supported on API 21+. + * </p> + * + * @return An equivalent {@link android.media.session.PlaybackState} object, or null if none. + */ + public Object getPlaybackState() { + if (mStateObj != null || Build.VERSION.SDK_INT < 21) { + return mStateObj; + } + + mStateObj = PlaybackStateCompatApi21.newInstance(mState, mPosition, mBufferPosition, + mRate, mActions, mErrorMessage, mUpdateTime); + return mStateObj; + } + + public static final Parcelable.Creator<PlaybackStateCompat> CREATOR = + new Parcelable.Creator<PlaybackStateCompat>() { + @Override + public PlaybackStateCompat createFromParcel(Parcel in) { + return new PlaybackStateCompat(in); + } + + @Override + public PlaybackStateCompat[] newArray(int size) { + return new PlaybackStateCompat[size]; + } + }; + + /** + * Builder for {@link PlaybackStateCompat} objects. + */ + public static final class Builder { + private int mState; + private long mPosition; + private long mBufferPosition; + private float mRate; + private long mActions; + private CharSequence mErrorMessage; + private long mUpdateTime; + + /** + * Create an empty Builder. + */ + public Builder() { + } + + /** + * Create a Builder using a {@link PlaybackStateCompat} instance to set the + * initial values. + * + * @param source The playback state to copy. + */ + public Builder(PlaybackStateCompat source) { + mState = source.mState; + mPosition = source.mPosition; + mRate = source.mRate; + mUpdateTime = source.mUpdateTime; + mBufferPosition = source.mBufferPosition; + mActions = source.mActions; + mErrorMessage = source.mErrorMessage; + } + + /** + * Set the current state of playback. + * <p> + * The position must be in ms and indicates the current playback position + * within the track. If the position is unknown use + * {@link #PLAYBACK_POSITION_UNKNOWN}. + * <p> + * The rate is a multiple of normal playback and should be 0 when paused and + * negative when rewinding. Normal playback rate is 1.0. + * <p> + * The state must be one of the following: + * <ul> + * <li> {@link PlaybackStateCompat#STATE_NONE}</li> + * <li> {@link PlaybackStateCompat#STATE_STOPPED}</li> + * <li> {@link PlaybackStateCompat#STATE_PLAYING}</li> + * <li> {@link PlaybackStateCompat#STATE_PAUSED}</li> + * <li> {@link PlaybackStateCompat#STATE_FAST_FORWARDING}</li> + * <li> {@link PlaybackStateCompat#STATE_REWINDING}</li> + * <li> {@link PlaybackStateCompat#STATE_BUFFERING}</li> + * <li> {@link PlaybackStateCompat#STATE_ERROR}</li> + * </ul> + * + * @param state The current state of playback. + * @param position The position in the current track in ms. + * @param playbackRate The current rate of playback as a multiple of normal + * playback. + */ + public void setState(int state, long position, float playbackRate) { + this.mState = state; + this.mPosition = position; + this.mRate = playbackRate; + mUpdateTime = SystemClock.elapsedRealtime(); + } + + /** + * Set the current buffer position in ms. This is the farthest playback + * point that can be reached from the current position using only buffered + * content. + */ + public void setBufferPosition(long bufferPosition) { + mBufferPosition = bufferPosition; + } + + /** + * Set the current capabilities available on this session. This should use a + * bitmask of the available capabilities. + * <ul> + * <li> {@link PlaybackStateCompat#ACTION_SKIP_TO_PREVIOUS}</li> + * <li> {@link PlaybackStateCompat#ACTION_REWIND}</li> + * <li> {@link PlaybackStateCompat#ACTION_PLAY}</li> + * <li> {@link PlaybackStateCompat#ACTION_PAUSE}</li> + * <li> {@link PlaybackStateCompat#ACTION_STOP}</li> + * <li> {@link PlaybackStateCompat#ACTION_FAST_FORWARD}</li> + * <li> {@link PlaybackStateCompat#ACTION_SKIP_TO_NEXT}</li> + * <li> {@link PlaybackStateCompat#ACTION_SEEK_TO}</li> + * <li> {@link PlaybackStateCompat#ACTION_SET_RATING}</li> + * </ul> + */ + public void setActions(long capabilities) { + mActions = capabilities; + } + + /** + * Set a user readable error message. This should be set when the state is + * {@link PlaybackStateCompat#STATE_ERROR}. + */ + public void setErrorMessage(CharSequence errorMessage) { + mErrorMessage = errorMessage; + } + + /** + * Creates the playback state object. + */ + public PlaybackStateCompat build() { + return new PlaybackStateCompat(mState, mPosition, mBufferPosition, + mRate, mActions, mErrorMessage, mUpdateTime); + } + } +} diff --git a/v4/jellybean-mr1/android/support/v4/media/routing/MediaRouterJellybeanMr1.java b/v4/jellybean-mr1/android/support/v4/media/routing/MediaRouterJellybeanMr1.java new file mode 100644 index 0000000000..6e5cfd5d1a --- /dev/null +++ b/v4/jellybean-mr1/android/support/v4/media/routing/MediaRouterJellybeanMr1.java @@ -0,0 +1,176 @@ +/* + * Copyright (C) 2013 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.support.v4.media.routing; + +import android.content.Context; +import android.hardware.display.DisplayManager; +import android.os.Build; +import android.os.Handler; +import android.util.Log; +import android.view.Display; + +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +class MediaRouterJellybeanMr1 extends MediaRouterJellybean { + private static final String TAG = "MediaRouterJellybeanMr1"; + + public static Object createCallback(Callback callback) { + return new CallbackProxy<Callback>(callback); + } + + public static final class RouteInfo { + public static boolean isEnabled(Object routeObj) { + return ((android.media.MediaRouter.RouteInfo)routeObj).isEnabled(); + } + + public static Display getPresentationDisplay(Object routeObj) { + return ((android.media.MediaRouter.RouteInfo)routeObj).getPresentationDisplay(); + } + } + + public static interface Callback extends MediaRouterJellybean.Callback { + public void onRoutePresentationDisplayChanged(Object routeObj); + } + + /** + * Workaround the fact that the version of MediaRouter.addCallback() that accepts a + * flag to perform an active scan does not exist in JB MR1 so we need to force + * wifi display scans directly through the DisplayManager. + * Do not use on JB MR2 and above. + */ + public static final class ActiveScanWorkaround implements Runnable { + // Time between wifi display scans when actively scanning in milliseconds. + private static final int WIFI_DISPLAY_SCAN_INTERVAL = 15000; + + private final DisplayManager mDisplayManager; + private final Handler mHandler; + private Method mScanWifiDisplaysMethod; + + private boolean mActivelyScanningWifiDisplays; + + public ActiveScanWorkaround(Context context, Handler handler) { + if (Build.VERSION.SDK_INT != 17) { + throw new UnsupportedOperationException(); + } + + mDisplayManager = (DisplayManager) context.getSystemService(Context.DISPLAY_SERVICE); + mHandler = handler; + try { + mScanWifiDisplaysMethod = DisplayManager.class.getMethod("scanWifiDisplays"); + } catch (NoSuchMethodException ex) { + } + } + + public void setActiveScanRouteTypes(int routeTypes) { + // On JB MR1, there is no API to scan wifi display routes. + // Instead we must make a direct call into the DisplayManager to scan + // wifi displays on this version but only when live video routes are requested. + // See also the JellybeanMr2Impl implementation of this method. + // This was fixed in JB MR2 by adding a new overload of addCallback() to + // enable active scanning on request. + if ((routeTypes & MediaRouterJellybean.ROUTE_TYPE_LIVE_VIDEO) != 0) { + if (!mActivelyScanningWifiDisplays) { + if (mScanWifiDisplaysMethod != null) { + mActivelyScanningWifiDisplays = true; + mHandler.post(this); + } else { + Log.w(TAG, "Cannot scan for wifi displays because the " + + "DisplayManager.scanWifiDisplays() method is " + + "not available on this device."); + } + } + } else { + if (mActivelyScanningWifiDisplays) { + mActivelyScanningWifiDisplays = false; + mHandler.removeCallbacks(this); + } + } + } + + @Override + public void run() { + if (mActivelyScanningWifiDisplays) { + try { + mScanWifiDisplaysMethod.invoke(mDisplayManager); + } catch (IllegalAccessException ex) { + Log.w(TAG, "Cannot scan for wifi displays.", ex); + } catch (InvocationTargetException ex) { + Log.w(TAG, "Cannot scan for wifi displays.", ex); + } + mHandler.postDelayed(this, WIFI_DISPLAY_SCAN_INTERVAL); + } + } + } + + /** + * Workaround the fact that the isConnecting() method does not exist in JB MR1. + * Do not use on JB MR2 and above. + */ + public static final class IsConnectingWorkaround { + private Method mGetStatusCodeMethod; + private int mStatusConnecting; + + public IsConnectingWorkaround() { + if (Build.VERSION.SDK_INT != 17) { + throw new UnsupportedOperationException(); + } + + try { + Field statusConnectingField = + android.media.MediaRouter.RouteInfo.class.getField("STATUS_CONNECTING"); + mStatusConnecting = statusConnectingField.getInt(null); + mGetStatusCodeMethod = + android.media.MediaRouter.RouteInfo.class.getMethod("getStatusCode"); + } catch (NoSuchFieldException ex) { + } catch (NoSuchMethodException ex) { + } catch (IllegalAccessException ex) { + } + } + + public boolean isConnecting(Object routeObj) { + android.media.MediaRouter.RouteInfo route = + (android.media.MediaRouter.RouteInfo)routeObj; + + if (mGetStatusCodeMethod != null) { + try { + int statusCode = (Integer)mGetStatusCodeMethod.invoke(route); + return statusCode == mStatusConnecting; + } catch (IllegalAccessException ex) { + } catch (InvocationTargetException ex) { + } + } + + // Assume not connecting. + return false; + } + } + + static class CallbackProxy<T extends Callback> + extends MediaRouterJellybean.CallbackProxy<T> { + public CallbackProxy(T callback) { + super(callback); + } + + @Override + public void onRoutePresentationDisplayChanged(android.media.MediaRouter router, + android.media.MediaRouter.RouteInfo route) { + mCallback.onRoutePresentationDisplayChanged(route); + } + } +} diff --git a/v4/jellybean-mr2/android/support/v4/media/routing/MediaRouterJellybeanMr2.java b/v4/jellybean-mr2/android/support/v4/media/routing/MediaRouterJellybeanMr2.java new file mode 100644 index 0000000000..92a1607a01 --- /dev/null +++ b/v4/jellybean-mr2/android/support/v4/media/routing/MediaRouterJellybeanMr2.java @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2013 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.support.v4.media.routing; + +class MediaRouterJellybeanMr2 extends MediaRouterJellybeanMr1 { + public static Object getDefaultRoute(Object routerObj) { + return ((android.media.MediaRouter)routerObj).getDefaultRoute(); + } + + public static void addCallback(Object routerObj, int types, Object callbackObj, int flags) { + ((android.media.MediaRouter)routerObj).addCallback(types, + (android.media.MediaRouter.Callback)callbackObj, flags); + } + + public static final class RouteInfo { + public static CharSequence getDescription(Object routeObj) { + return ((android.media.MediaRouter.RouteInfo)routeObj).getDescription(); + } + + public static boolean isConnecting(Object routeObj) { + return ((android.media.MediaRouter.RouteInfo)routeObj).isConnecting(); + } + } + + public static final class UserRouteInfo { + public static void setDescription(Object routeObj, CharSequence description) { + ((android.media.MediaRouter.UserRouteInfo)routeObj).setDescription(description); + } + } +} diff --git a/v4/jellybean/android/support/v4/media/routing/MediaRouterJellybean.java b/v4/jellybean/android/support/v4/media/routing/MediaRouterJellybean.java new file mode 100644 index 0000000000..3cf67271e4 --- /dev/null +++ b/v4/jellybean/android/support/v4/media/routing/MediaRouterJellybean.java @@ -0,0 +1,442 @@ +/* + * Copyright (C) 2013 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.support.v4.media.routing; + +import android.content.Context; +import android.graphics.drawable.Drawable; +import android.os.Build; +import android.util.Log; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.List; + +class MediaRouterJellybean { + private static final String TAG = "MediaRouterJellybean"; + + public static final int ROUTE_TYPE_LIVE_AUDIO = 0x1; + public static final int ROUTE_TYPE_LIVE_VIDEO = 0x2; + public static final int ROUTE_TYPE_USER = 0x00800000; + + public static final int ALL_ROUTE_TYPES = + MediaRouterJellybean.ROUTE_TYPE_LIVE_AUDIO + | MediaRouterJellybean.ROUTE_TYPE_LIVE_VIDEO + | MediaRouterJellybean.ROUTE_TYPE_USER; + + public static Object getMediaRouter(Context context) { + return context.getSystemService(Context.MEDIA_ROUTER_SERVICE); + } + + @SuppressWarnings({ "rawtypes", "unchecked" }) + public static List getRoutes(Object routerObj) { + final android.media.MediaRouter router = (android.media.MediaRouter)routerObj; + final int count = router.getRouteCount(); + List out = new ArrayList(count); + for (int i = 0; i < count; i++) { + out.add(router.getRouteAt(i)); + } + return out; + } + + @SuppressWarnings({ "rawtypes", "unchecked" }) + public static List getCategories(Object routerObj) { + final android.media.MediaRouter router = (android.media.MediaRouter)routerObj; + final int count = router.getCategoryCount(); + List out = new ArrayList(count); + for (int i = 0; i < count; i++) { + out.add(router.getCategoryAt(i)); + } + return out; + } + + public static Object getSelectedRoute(Object routerObj, int type) { + return ((android.media.MediaRouter)routerObj).getSelectedRoute(type); + } + + public static void selectRoute(Object routerObj, int types, Object routeObj) { + ((android.media.MediaRouter)routerObj).selectRoute(types, + (android.media.MediaRouter.RouteInfo)routeObj); + } + + public static void addCallback(Object routerObj, int types, Object callbackObj) { + ((android.media.MediaRouter)routerObj).addCallback(types, + (android.media.MediaRouter.Callback)callbackObj); + } + + public static void removeCallback(Object routerObj, Object callbackObj) { + ((android.media.MediaRouter)routerObj).removeCallback( + (android.media.MediaRouter.Callback)callbackObj); + } + + public static Object createRouteCategory(Object routerObj, + String name, boolean isGroupable) { + return ((android.media.MediaRouter)routerObj).createRouteCategory(name, isGroupable); + } + + public static Object createUserRoute(Object routerObj, Object categoryObj) { + return ((android.media.MediaRouter)routerObj).createUserRoute( + (android.media.MediaRouter.RouteCategory)categoryObj); + } + + public static void addUserRoute(Object routerObj, Object routeObj) { + ((android.media.MediaRouter)routerObj).addUserRoute( + (android.media.MediaRouter.UserRouteInfo)routeObj); + } + + public static void removeUserRoute(Object routerObj, Object routeObj) { + ((android.media.MediaRouter)routerObj).removeUserRoute( + (android.media.MediaRouter.UserRouteInfo)routeObj); + } + + public static Object createCallback(Callback callback) { + return new CallbackProxy<Callback>(callback); + } + + public static Object createVolumeCallback(VolumeCallback callback) { + return new VolumeCallbackProxy<VolumeCallback>(callback); + } + + public static final class RouteInfo { + public static CharSequence getName(Object routeObj, Context context) { + return ((android.media.MediaRouter.RouteInfo)routeObj).getName(context); + } + + public static CharSequence getStatus(Object routeObj) { + return ((android.media.MediaRouter.RouteInfo)routeObj).getStatus(); + } + + public static int getSupportedTypes(Object routeObj) { + return ((android.media.MediaRouter.RouteInfo)routeObj).getSupportedTypes(); + } + + public static Object getCategory(Object routeObj) { + return ((android.media.MediaRouter.RouteInfo)routeObj).getCategory(); + } + + public static Drawable getIconDrawable(Object routeObj) { + return ((android.media.MediaRouter.RouteInfo)routeObj).getIconDrawable(); + } + + public static int getPlaybackType(Object routeObj) { + return ((android.media.MediaRouter.RouteInfo)routeObj).getPlaybackType(); + } + + public static int getPlaybackStream(Object routeObj) { + return ((android.media.MediaRouter.RouteInfo)routeObj).getPlaybackStream(); + } + + public static int getVolume(Object routeObj) { + return ((android.media.MediaRouter.RouteInfo)routeObj).getVolume(); + } + + public static int getVolumeMax(Object routeObj) { + return ((android.media.MediaRouter.RouteInfo)routeObj).getVolumeMax(); + } + + public static int getVolumeHandling(Object routeObj) { + return ((android.media.MediaRouter.RouteInfo)routeObj).getVolumeHandling(); + } + + public static Object getTag(Object routeObj) { + return ((android.media.MediaRouter.RouteInfo)routeObj).getTag(); + } + + public static void setTag(Object routeObj, Object tag) { + ((android.media.MediaRouter.RouteInfo)routeObj).setTag(tag); + } + + public static void requestSetVolume(Object routeObj, int volume) { + ((android.media.MediaRouter.RouteInfo)routeObj).requestSetVolume(volume); + } + + public static void requestUpdateVolume(Object routeObj, int direction) { + ((android.media.MediaRouter.RouteInfo)routeObj).requestUpdateVolume(direction); + } + + public static Object getGroup(Object routeObj) { + return ((android.media.MediaRouter.RouteInfo)routeObj).getGroup(); + } + + public static boolean isGroup(Object routeObj) { + return routeObj instanceof android.media.MediaRouter.RouteGroup; + } + } + + public static final class RouteGroup { + @SuppressWarnings({ "rawtypes", "unchecked" }) + public static List getGroupedRoutes(Object groupObj) { + final android.media.MediaRouter.RouteGroup group = + (android.media.MediaRouter.RouteGroup)groupObj; + final int count = group.getRouteCount(); + List out = new ArrayList(count); + for (int i = 0; i < count; i++) { + out.add(group.getRouteAt(i)); + } + return out; + } + } + + public static final class UserRouteInfo { + public static void setName(Object routeObj, CharSequence name) { + ((android.media.MediaRouter.UserRouteInfo)routeObj).setName(name); + } + + public static void setStatus(Object routeObj, CharSequence status) { + ((android.media.MediaRouter.UserRouteInfo)routeObj).setStatus(status); + } + + public static void setIconDrawable(Object routeObj, Drawable icon) { + ((android.media.MediaRouter.UserRouteInfo)routeObj).setIconDrawable(icon); + } + + public static void setPlaybackType(Object routeObj, int type) { + ((android.media.MediaRouter.UserRouteInfo)routeObj).setPlaybackType(type); + } + + public static void setPlaybackStream(Object routeObj, int stream) { + ((android.media.MediaRouter.UserRouteInfo)routeObj).setPlaybackStream(stream); + } + + public static void setVolume(Object routeObj, int volume) { + ((android.media.MediaRouter.UserRouteInfo)routeObj).setVolume(volume); + } + + public static void setVolumeMax(Object routeObj, int volumeMax) { + ((android.media.MediaRouter.UserRouteInfo)routeObj).setVolumeMax(volumeMax); + } + + public static void setVolumeHandling(Object routeObj, int volumeHandling) { + ((android.media.MediaRouter.UserRouteInfo)routeObj).setVolumeHandling(volumeHandling); + } + + public static void setVolumeCallback(Object routeObj, Object volumeCallbackObj) { + ((android.media.MediaRouter.UserRouteInfo)routeObj).setVolumeCallback( + (android.media.MediaRouter.VolumeCallback)volumeCallbackObj); + } + + public static void setRemoteControlClient(Object routeObj, Object rccObj) { + ((android.media.MediaRouter.UserRouteInfo)routeObj).setRemoteControlClient( + (android.media.RemoteControlClient)rccObj); + } + } + + public static final class RouteCategory { + public static CharSequence getName(Object categoryObj, Context context) { + return ((android.media.MediaRouter.RouteCategory)categoryObj).getName(context); + } + + @SuppressWarnings({ "rawtypes", "unchecked" }) + public static List getRoutes(Object categoryObj) { + ArrayList out = new ArrayList(); + ((android.media.MediaRouter.RouteCategory)categoryObj).getRoutes(out); + return out; + } + + public static int getSupportedTypes(Object categoryObj) { + return ((android.media.MediaRouter.RouteCategory)categoryObj).getSupportedTypes(); + } + + public static boolean isGroupable(Object categoryObj) { + return ((android.media.MediaRouter.RouteCategory)categoryObj).isGroupable(); + } + } + + public static interface Callback { + public void onRouteSelected(int type, Object routeObj); + public void onRouteUnselected(int type, Object routeObj); + public void onRouteAdded(Object routeObj); + public void onRouteRemoved(Object routeObj); + public void onRouteChanged(Object routeObj); + public void onRouteGrouped(Object routeObj, Object groupObj, int index); + public void onRouteUngrouped(Object routeObj, Object groupObj); + public void onRouteVolumeChanged(Object routeObj); + } + + public static interface VolumeCallback { + public void onVolumeSetRequest(Object routeObj, int volume); + public void onVolumeUpdateRequest(Object routeObj, int direction); + } + + /** + * Workaround for limitations of selectRoute() on JB and JB MR1. + * Do not use on JB MR2 and above. + */ + public static final class SelectRouteWorkaround { + private Method mSelectRouteIntMethod; + + public SelectRouteWorkaround() { + if (Build.VERSION.SDK_INT < 16 || Build.VERSION.SDK_INT > 17) { + throw new UnsupportedOperationException(); + } + try { + mSelectRouteIntMethod = android.media.MediaRouter.class.getMethod( + "selectRouteInt", int.class, android.media.MediaRouter.RouteInfo.class); + } catch (NoSuchMethodException ex) { + } + } + + public void selectRoute(Object routerObj, int types, Object routeObj) { + android.media.MediaRouter router = (android.media.MediaRouter)routerObj; + android.media.MediaRouter.RouteInfo route = + (android.media.MediaRouter.RouteInfo)routeObj; + + int routeTypes = route.getSupportedTypes(); + if ((routeTypes & ROUTE_TYPE_USER) == 0) { + // Handle non-user routes. + // On JB and JB MR1, the selectRoute() API only supports programmatically + // selecting user routes. So instead we rely on the hidden selectRouteInt() + // method on these versions of the platform. + // This limitation was removed in JB MR2. + if (mSelectRouteIntMethod != null) { + try { + mSelectRouteIntMethod.invoke(router, types, route); + return; // success! + } catch (IllegalAccessException ex) { + Log.w(TAG, "Cannot programmatically select non-user route. " + + "Media routing may not work.", ex); + } catch (InvocationTargetException ex) { + Log.w(TAG, "Cannot programmatically select non-user route. " + + "Media routing may not work.", ex); + } + } else { + Log.w(TAG, "Cannot programmatically select non-user route " + + "because the platform is missing the selectRouteInt() " + + "method. Media routing may not work."); + } + } + + // Default handling. + router.selectRoute(types, route); + } + } + + /** + * Workaround the fact that the getDefaultRoute() method does not exist in JB and JB MR1. + * Do not use on JB MR2 and above. + */ + public static final class GetDefaultRouteWorkaround { + private Method mGetSystemAudioRouteMethod; + + public GetDefaultRouteWorkaround() { + if (Build.VERSION.SDK_INT < 16 || Build.VERSION.SDK_INT > 17) { + throw new UnsupportedOperationException(); + } + try { + mGetSystemAudioRouteMethod = + android.media.MediaRouter.class.getMethod("getSystemAudioRoute"); + } catch (NoSuchMethodException ex) { + } + } + + public Object getDefaultRoute(Object routerObj) { + android.media.MediaRouter router = (android.media.MediaRouter)routerObj; + + if (mGetSystemAudioRouteMethod != null) { + try { + return mGetSystemAudioRouteMethod.invoke(router); + } catch (IllegalAccessException ex) { + } catch (InvocationTargetException ex) { + } + } + + // Could not find the method or it does not work. + // Return the first route and hope for the best. + return router.getRouteAt(0); + } + } + + static class CallbackProxy<T extends Callback> + extends android.media.MediaRouter.Callback { + protected final T mCallback; + + public CallbackProxy(T callback) { + mCallback = callback; + } + + @Override + public void onRouteSelected(android.media.MediaRouter router, + int type, android.media.MediaRouter.RouteInfo route) { + mCallback.onRouteSelected(type, route); + } + + @Override + public void onRouteUnselected(android.media.MediaRouter router, + int type, android.media.MediaRouter.RouteInfo route) { + mCallback.onRouteUnselected(type, route); + } + + @Override + public void onRouteAdded(android.media.MediaRouter router, + android.media.MediaRouter.RouteInfo route) { + mCallback.onRouteAdded(route); + } + + @Override + public void onRouteRemoved(android.media.MediaRouter router, + android.media.MediaRouter.RouteInfo route) { + mCallback.onRouteRemoved(route); + } + + @Override + public void onRouteChanged(android.media.MediaRouter router, + android.media.MediaRouter.RouteInfo route) { + mCallback.onRouteChanged(route); + } + + @Override + public void onRouteGrouped(android.media.MediaRouter router, + android.media.MediaRouter.RouteInfo route, + android.media.MediaRouter.RouteGroup group, int index) { + mCallback.onRouteGrouped(route, group, index); + } + + @Override + public void onRouteUngrouped(android.media.MediaRouter router, + android.media.MediaRouter.RouteInfo route, + android.media.MediaRouter.RouteGroup group) { + mCallback.onRouteUngrouped(route, group); + } + + @Override + public void onRouteVolumeChanged(android.media.MediaRouter router, + android.media.MediaRouter.RouteInfo route) { + mCallback.onRouteVolumeChanged(route); + } + } + + static class VolumeCallbackProxy<T extends VolumeCallback> + extends android.media.MediaRouter.VolumeCallback { + protected final T mCallback; + + public VolumeCallbackProxy(T callback) { + mCallback = callback; + } + + @Override + public void onVolumeSetRequest(android.media.MediaRouter.RouteInfo route, + int volume) { + mCallback.onVolumeSetRequest(route, volume); + } + + @Override + public void onVolumeUpdateRequest(android.media.MediaRouter.RouteInfo route, + int direction) { + mCallback.onVolumeUpdateRequest(route, direction); + } + } +} |
