summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJeff Brown <jeffbrown@google.com>2014-06-23 16:13:30 -0700
committerJeff Brown <jeffbrown@google.com>2014-06-30 16:05:25 -0700
commit24fa6c0dd42df057729e1a258388183f94da7f82 (patch)
tree675616cc6407928c68b08a715f4809d46872c38f
parentd9eb04dbc96599246581624c2c0e07b31eae3ca9 (diff)
downloadandroid_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
-rw-r--r--v4/api21/android/support/v4/media/MediaMetadataCompatApi21.java71
-rw-r--r--v4/api21/android/support/v4/media/RatingCompatApi21.java65
-rw-r--r--v4/api21/android/support/v4/media/VolumeProviderCompatApi21.java51
-rw-r--r--v4/api21/android/support/v4/media/session/MediaControllerCompatApi21.java164
-rw-r--r--v4/api21/android/support/v4/media/session/MediaSessionCompatApi21.java194
-rw-r--r--v4/api21/android/support/v4/media/session/PlaybackStateCompatApi21.java63
-rw-r--r--v4/java/android/support/v4/media/MediaMetadataCompat.java526
-rw-r--r--v4/java/android/support/v4/media/RatingCompat.java370
-rw-r--r--v4/java/android/support/v4/media/VolumeProviderCompat.java168
-rw-r--r--v4/java/android/support/v4/media/session/MediaControllerCompat.java617
-rw-r--r--v4/java/android/support/v4/media/session/MediaSessionCompat.java711
-rw-r--r--v4/java/android/support/v4/media/session/PlaybackStateCompat.java497
-rw-r--r--v4/jellybean-mr1/android/support/v4/media/routing/MediaRouterJellybeanMr1.java176
-rw-r--r--v4/jellybean-mr2/android/support/v4/media/routing/MediaRouterJellybeanMr2.java44
-rw-r--r--v4/jellybean/android/support/v4/media/routing/MediaRouterJellybean.java442
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);
+ }
+ }
+}