From 42ea7eecd149161ed192d3029f0d77d1d08a4aa5 Mon Sep 17 00:00:00 2001 From: RoboErik Date: Fri, 16 May 2014 16:27:35 -0700 Subject: Rename session classes to have media prefix, hide routes This renames several of the core session classes to be prefixed with Media. It also adds @hide to all route references for now. Change-Id: Idb42897d490cf24626e4e93ca975b2cb93ec054c --- media/java/android/media/MediaMetadata.aidl | 18 + media/java/android/media/MediaMetadata.java | 472 +++++++++++++ media/java/android/media/MediaMetadataEditor.java | 1 - media/java/android/media/RemoteControlClient.java | 5 +- .../media/routeprovider/RouteConnection.java | 1 + .../media/routeprovider/RouteInterfaceHandler.java | 9 +- .../RoutePlaybackControlsHandler.java | 1 + .../media/routeprovider/RouteProviderService.java | 1 + .../android/media/routeprovider/RouteRequest.java | 11 +- media/java/android/media/session/ISession.aidl | 2 +- .../android/media/session/ISessionController.aidl | 2 +- .../media/session/ISessionControllerCallback.aidl | 2 +- .../android/media/session/MediaController.java | 369 ++++++++++ .../java/android/media/session/MediaMetadata.aidl | 18 - .../java/android/media/session/MediaMetadata.java | 475 ------------- media/java/android/media/session/MediaSession.java | 777 +++++++++++++++++++++ .../android/media/session/MediaSessionInfo.java | 87 +++ .../media/session/MediaSessionLegacyHelper.java | 22 +- .../android/media/session/MediaSessionManager.java | 168 +++++ .../android/media/session/MediaSessionToken.aidl | 18 + .../android/media/session/MediaSessionToken.java | 66 ++ .../java/android/media/session/PlaybackState.java | 3 +- media/java/android/media/session/Route.java | 7 +- media/java/android/media/session/RouteInfo.java | 1 + .../java/android/media/session/RouteInterface.java | 7 +- media/java/android/media/session/RouteOptions.java | 1 + .../media/session/RoutePlaybackControls.java | 2 + media/java/android/media/session/Session.java | 765 -------------------- .../android/media/session/SessionController.java | 365 ---------- media/java/android/media/session/SessionInfo.java | 87 --- .../java/android/media/session/SessionManager.java | 168 ----- media/java/android/media/session/SessionToken.aidl | 18 - media/java/android/media/session/SessionToken.java | 66 -- .../android/media/session/TransportController.java | 1 + .../android/media/session/TransportPerformer.java | 1 + 35 files changed, 2021 insertions(+), 1996 deletions(-) create mode 100644 media/java/android/media/MediaMetadata.aidl create mode 100644 media/java/android/media/MediaMetadata.java create mode 100644 media/java/android/media/session/MediaController.java delete mode 100644 media/java/android/media/session/MediaMetadata.aidl delete mode 100644 media/java/android/media/session/MediaMetadata.java create mode 100644 media/java/android/media/session/MediaSession.java create mode 100644 media/java/android/media/session/MediaSessionInfo.java create mode 100644 media/java/android/media/session/MediaSessionManager.java create mode 100644 media/java/android/media/session/MediaSessionToken.aidl create mode 100644 media/java/android/media/session/MediaSessionToken.java delete mode 100644 media/java/android/media/session/Session.java delete mode 100644 media/java/android/media/session/SessionController.java delete mode 100644 media/java/android/media/session/SessionInfo.java delete mode 100644 media/java/android/media/session/SessionManager.java delete mode 100644 media/java/android/media/session/SessionToken.aidl delete mode 100644 media/java/android/media/session/SessionToken.java (limited to 'media') diff --git a/media/java/android/media/MediaMetadata.aidl b/media/java/android/media/MediaMetadata.aidl new file mode 100644 index 0000000..66ee483 --- /dev/null +++ b/media/java/android/media/MediaMetadata.aidl @@ -0,0 +1,18 @@ +/* Copyright 2014, The Android Open Source Project +** +** Licensed under the Apache License, Version 2.0 (the "License"); +** you may not use this file except in compliance with the License. +** You may obtain a copy of the License at +** +** http://www.apache.org/licenses/LICENSE-2.0 +** +** Unless required by applicable law or agreed to in writing, software +** distributed under the License is distributed on an "AS IS" BASIS, +** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +** See the License for the specific language governing permissions and +** limitations under the License. +*/ + +package android.media; + +parcelable MediaMetadata; diff --git a/media/java/android/media/MediaMetadata.java b/media/java/android/media/MediaMetadata.java new file mode 100644 index 0000000..ff73a10 --- /dev/null +++ b/media/java/android/media/MediaMetadata.java @@ -0,0 +1,472 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package android.media; + +import android.graphics.Bitmap; +import android.os.Bundle; +import android.os.Parcel; +import android.os.Parcelable; +import android.util.ArrayMap; +import android.util.Log; +import android.util.SparseArray; + +/** + * Contains metadata about an item, such as the title, artist, etc. + */ +public final class MediaMetadata 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 Rating + */ + public static final String METADATA_KEY_USER_RATING = "android.media.metadata.USER_RATING"; + + /** + * The overall rating for the media. + * + * @see Rating + */ + public static final String METADATA_KEY_RATING = "android.media.metadata.RATING"; + + private static final int METADATA_TYPE_INVALID = -1; + 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 METADATA_KEYS_TYPE; + + static { + METADATA_KEYS_TYPE = new ArrayMap(); + 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 static final SparseArray EDITOR_KEY_MAPPING; + + static { + EDITOR_KEY_MAPPING = new SparseArray(); + EDITOR_KEY_MAPPING.put(MediaMetadataEditor.BITMAP_KEY_ARTWORK, METADATA_KEY_ART); + EDITOR_KEY_MAPPING.put(MediaMetadataEditor.RATING_KEY_BY_OTHERS, METADATA_KEY_RATING); + EDITOR_KEY_MAPPING.put(MediaMetadataEditor.RATING_KEY_BY_USER, METADATA_KEY_USER_RATING); + EDITOR_KEY_MAPPING.put(MediaMetadataRetriever.METADATA_KEY_ALBUM, METADATA_KEY_ALBUM); + EDITOR_KEY_MAPPING.put(MediaMetadataRetriever.METADATA_KEY_ALBUMARTIST, + METADATA_KEY_ALBUM_ARTIST); + EDITOR_KEY_MAPPING.put(MediaMetadataRetriever.METADATA_KEY_ARTIST, METADATA_KEY_ARTIST); + EDITOR_KEY_MAPPING.put(MediaMetadataRetriever.METADATA_KEY_AUTHOR, METADATA_KEY_AUTHOR); + EDITOR_KEY_MAPPING.put(MediaMetadataRetriever.METADATA_KEY_CD_TRACK_NUMBER, + METADATA_KEY_TRACK_NUMBER); + EDITOR_KEY_MAPPING.put(MediaMetadataRetriever.METADATA_KEY_COMPOSER, METADATA_KEY_COMPOSER); + EDITOR_KEY_MAPPING.put(MediaMetadataRetriever.METADATA_KEY_COMPILATION, + METADATA_KEY_COMPILATION); + EDITOR_KEY_MAPPING.put(MediaMetadataRetriever.METADATA_KEY_DATE, METADATA_KEY_DATE); + EDITOR_KEY_MAPPING.put(MediaMetadataRetriever.METADATA_KEY_DISC_NUMBER, + METADATA_KEY_DISC_NUMBER); + EDITOR_KEY_MAPPING.put(MediaMetadataRetriever.METADATA_KEY_DURATION, METADATA_KEY_DURATION); + EDITOR_KEY_MAPPING.put(MediaMetadataRetriever.METADATA_KEY_GENRE, METADATA_KEY_GENRE); + EDITOR_KEY_MAPPING.put(MediaMetadataRetriever.METADATA_KEY_NUM_TRACKS, + METADATA_KEY_NUM_TRACKS); + EDITOR_KEY_MAPPING.put(MediaMetadataRetriever.METADATA_KEY_TITLE, METADATA_KEY_TITLE); + EDITOR_KEY_MAPPING.put(MediaMetadataRetriever.METADATA_KEY_WRITER, METADATA_KEY_WRITER); + EDITOR_KEY_MAPPING.put(MediaMetadataRetriever.METADATA_KEY_YEAR, METADATA_KEY_YEAR); + } + + private final Bundle mBundle; + + private MediaMetadata(Bundle bundle) { + mBundle = new Bundle(bundle); + } + + private MediaMetadata(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 Rating} 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 Rating} or null + */ + public Rating getRating(String key) { + Rating rating = null; + try { + rating = mBundle.getParcelable(key); + } catch (Exception e) { + // ignore, value was not a bitmap + Log.d(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.d(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(); + } + + /** + * Helper for getting the String key used by {@link MediaMetadata} from the + * integer key that {@link MediaMetadataEditor} uses. + * + * @param editorKey The key used by the editor + * @return The key used by this class or null if no mapping exists + * @hide + */ + public static String getKeyFromMetadataEditorKey(int editorKey) { + return EDITOR_KEY_MAPPING.get(editorKey, null); + } + + public static final Parcelable.Creator CREATOR + = new Parcelable.Creator() { + @Override + public MediaMetadata createFromParcel(Parcel in) { + return new MediaMetadata(in); + } + + @Override + public MediaMetadata[] newArray(int size) { + return new MediaMetadata[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 MediaMetadata} must be added. + */ + public Builder() { + mBundle = new Bundle(); + } + + /** + * Create a Builder using a {@link MediaMetadata} 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(MediaMetadata 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: + *
    + *
  • {@link #METADATA_KEY_TITLE}
  • + *
  • {@link #METADATA_KEY_ARTIST}
  • + *
  • {@link #METADATA_KEY_ALBUM}
  • + *
  • {@link #METADATA_KEY_AUTHOR}
  • + *
  • {@link #METADATA_KEY_WRITER}
  • + *
  • {@link #METADATA_KEY_COMPOSER}
  • + *
  • {@link #METADATA_KEY_DATE}
  • + *
  • {@link #METADATA_KEY_GENRE}
  • + *
  • {@link #METADATA_KEY_ALBUM_ARTIST}
  • + *
  • {@link #METADATA_KEY_ART_URI}
  • + *
  • {@link #METADATA_KEY_ALBUM_ART_URI}
  • + *
+ * + * @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: + *
    + *
  • {@link #METADATA_KEY_DURATION}
  • + *
  • {@link #METADATA_KEY_TRACK_NUMBER}
  • + *
  • {@link #METADATA_KEY_NUM_TRACKS}
  • + *
  • {@link #METADATA_KEY_DISC_NUMBER}
  • + *
  • {@link #METADATA_KEY_YEAR}
  • + *
+ * + * @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 Rating} 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: + *
    + *
  • {@link #METADATA_KEY_RATING}
  • + *
  • {@link #METADATA_KEY_USER_RATING}
  • + *
+ * + * @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, Rating 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: + *
    + *
  • {@link #METADATA_KEY_ART}
  • + *
  • {@link #METADATA_KEY_ALBUM_ART}
  • + *
+ * + * @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 MediaMetadata} instance with the specified fields. + * + * @return The new MediaMetadata instance + */ + public MediaMetadata build() { + return new MediaMetadata(mBundle); + } + } + +} diff --git a/media/java/android/media/MediaMetadataEditor.java b/media/java/android/media/MediaMetadataEditor.java index 1a4e8da..ca44e9d 100644 --- a/media/java/android/media/MediaMetadataEditor.java +++ b/media/java/android/media/MediaMetadataEditor.java @@ -17,7 +17,6 @@ package android.media; import android.graphics.Bitmap; -import android.media.session.MediaMetadata; import android.os.Bundle; import android.os.Parcelable; import android.util.Log; diff --git a/media/java/android/media/RemoteControlClient.java b/media/java/android/media/RemoteControlClient.java index 8368df9..37f45c2 100644 --- a/media/java/android/media/RemoteControlClient.java +++ b/media/java/android/media/RemoteControlClient.java @@ -24,10 +24,9 @@ import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Paint; import android.graphics.RectF; -import android.media.session.MediaMetadata; import android.media.session.MediaSessionLegacyHelper; import android.media.session.PlaybackState; -import android.media.session.Session; +import android.media.session.MediaSession; import android.media.session.TransportPerformer; import android.os.Bundle; import android.os.Handler; @@ -341,7 +340,7 @@ public class RemoteControlClient */ public final static int FLAG_INFORMATION_REQUEST_ALBUM_ART = 1 << 3; - private Session mSession; + private MediaSession mSession; /** * Class constructor. diff --git a/media/java/android/media/routeprovider/RouteConnection.java b/media/java/android/media/routeprovider/RouteConnection.java index 9214ff8..43692c1 100644 --- a/media/java/android/media/routeprovider/RouteConnection.java +++ b/media/java/android/media/routeprovider/RouteConnection.java @@ -40,6 +40,7 @@ import java.util.List; * interfaces. Use {@link #addRouteInterface(String)} to add an interface and * {@link #getRouteInterface(String)} to retrieve the interface's handle anytime * after it has been added. + * @hide */ public final class RouteConnection { private static final String TAG = "RouteConnection"; diff --git a/media/java/android/media/routeprovider/RouteInterfaceHandler.java b/media/java/android/media/routeprovider/RouteInterfaceHandler.java index 9693dc6..e7f8bbf 100644 --- a/media/java/android/media/routeprovider/RouteInterfaceHandler.java +++ b/media/java/android/media/routeprovider/RouteInterfaceHandler.java @@ -16,7 +16,7 @@ package android.media.routeprovider; import android.media.session.Route; -import android.media.session.Session; +import android.media.session.MediaSession; import android.media.session.RouteInterface; import android.os.Bundle; import android.os.Handler; @@ -33,7 +33,7 @@ import java.util.ArrayList; * connected media route. *

* A {@link RouteProviderService} may expose multiple interfaces on a - * {@link RouteConnection} for a {@link Session} to interact with. A + * {@link RouteConnection} for a {@link MediaSession} to interact with. A * provider creates an interface with * {@link RouteConnection#addRouteInterface(String)} to allow messages to be * routed appropriately. Events are then sent through a specific interface and @@ -47,6 +47,7 @@ import java.util.ArrayList; * It is recommended you wrap this interface with a standard implementation to * avoid errors, but for simple interfaces this class may be used directly. TODO * add link to sample code. + * @hide */ public final class RouteInterfaceHandler { private static final String TAG = "RouteInterfaceHandler"; @@ -184,7 +185,7 @@ public final class RouteInterfaceHandler { public abstract static class CommandListener { /** * This is called when a command is received that matches this - * interface. Commands are sent by a {@link Session} that is + * interface. Commands are sent by a {@link MediaSession} that is * connected to the route this interface is registered with. * * @param iface The interface the command was received on. @@ -197,7 +198,7 @@ public final class RouteInterfaceHandler { * true may be returned if the command will be handled * asynchronously. * @see Route - * @see Session + * @see MediaSession */ public abstract boolean onCommand(RouteInterfaceHandler iface, String command, Bundle args, ResultReceiver cb); diff --git a/media/java/android/media/routeprovider/RoutePlaybackControlsHandler.java b/media/java/android/media/routeprovider/RoutePlaybackControlsHandler.java index dcef79a..f2c40d2 100644 --- a/media/java/android/media/routeprovider/RoutePlaybackControlsHandler.java +++ b/media/java/android/media/routeprovider/RoutePlaybackControlsHandler.java @@ -28,6 +28,7 @@ import android.util.Log; * Standard wrapper for using playback controls over a {@link RouteInterfaceHandler}. * This is the provider half of the interface. Sessions should use * {@link RoutePlaybackControls} to interact with this interface. + * @hide */ public final class RoutePlaybackControlsHandler { private static final String TAG = "RoutePlaybackControls"; diff --git a/media/java/android/media/routeprovider/RouteProviderService.java b/media/java/android/media/routeprovider/RouteProviderService.java index 6ebfb5b..a6ef0bb 100644 --- a/media/java/android/media/routeprovider/RouteProviderService.java +++ b/media/java/android/media/routeprovider/RouteProviderService.java @@ -64,6 +64,7 @@ import java.util.List; * </intent-filter> * </service> * + * @hide */ public abstract class RouteProviderService extends Service { private static final String TAG = "RouteProvider"; diff --git a/media/java/android/media/routeprovider/RouteRequest.java b/media/java/android/media/routeprovider/RouteRequest.java index 68475c0..2ba75de 100644 --- a/media/java/android/media/routeprovider/RouteRequest.java +++ b/media/java/android/media/routeprovider/RouteRequest.java @@ -16,7 +16,7 @@ package android.media.routeprovider; import android.media.session.RouteOptions; -import android.media.session.SessionInfo; +import android.media.session.MediaSessionInfo; import android.os.Parcel; import android.os.Parcelable; @@ -30,16 +30,17 @@ import java.io.PrintWriter; * provides the full set of connection parameters they would like to use for a * connection. An app that can connect in multiple ways will be represented by * multiple requests. + * @hide */ public final class RouteRequest implements Parcelable { - private final SessionInfo mSessionInfo; + private final MediaSessionInfo mSessionInfo; private final RouteOptions mOptions; private final boolean mActive; /** * @hide */ - public RouteRequest(SessionInfo info, RouteOptions connRequest, + public RouteRequest(MediaSessionInfo info, RouteOptions connRequest, boolean active) { mSessionInfo = info; mOptions = connRequest; @@ -47,7 +48,7 @@ public final class RouteRequest implements Parcelable { } private RouteRequest(Parcel in) { - mSessionInfo = SessionInfo.CREATOR.createFromParcel(in); + mSessionInfo = MediaSessionInfo.CREATOR.createFromParcel(in); mOptions = RouteOptions.CREATOR.createFromParcel(in); mActive = in.readInt() != 0; } @@ -57,7 +58,7 @@ public final class RouteRequest implements Parcelable { * * @return Info on the session making the request */ - public SessionInfo getSessionInfo() { + public MediaSessionInfo getSessionInfo() { return mSessionInfo; } diff --git a/media/java/android/media/session/ISession.aidl b/media/java/android/media/session/ISession.aidl index 096550f..c4233c3 100644 --- a/media/java/android/media/session/ISession.aidl +++ b/media/java/android/media/session/ISession.aidl @@ -15,8 +15,8 @@ package android.media.session; +import android.media.MediaMetadata; import android.media.session.ISessionController; -import android.media.session.MediaMetadata; import android.media.session.RouteOptions; import android.media.session.RouteCommand; import android.media.session.RouteInfo; diff --git a/media/java/android/media/session/ISessionController.aidl b/media/java/android/media/session/ISessionController.aidl index e2e046f..5ddb6db 100644 --- a/media/java/android/media/session/ISessionController.aidl +++ b/media/java/android/media/session/ISessionController.aidl @@ -16,9 +16,9 @@ package android.media.session; import android.content.Intent; +import android.media.MediaMetadata; import android.media.Rating; import android.media.session.ISessionControllerCallback; -import android.media.session.MediaMetadata; import android.media.session.PlaybackState; import android.os.Bundle; import android.os.ResultReceiver; diff --git a/media/java/android/media/session/ISessionControllerCallback.aidl b/media/java/android/media/session/ISessionControllerCallback.aidl index bc1ae05..e823153 100644 --- a/media/java/android/media/session/ISessionControllerCallback.aidl +++ b/media/java/android/media/session/ISessionControllerCallback.aidl @@ -15,7 +15,7 @@ package android.media.session; -import android.media.session.MediaMetadata; +import android.media.MediaMetadata; import android.media.session.RouteInfo; import android.media.session.PlaybackState; import android.os.Bundle; diff --git a/media/java/android/media/session/MediaController.java b/media/java/android/media/session/MediaController.java new file mode 100644 index 0000000..642ac2f --- /dev/null +++ b/media/java/android/media/session/MediaController.java @@ -0,0 +1,369 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.media.session; + +import android.media.MediaMetadata; +import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; +import android.os.Message; +import android.os.RemoteException; +import android.os.ResultReceiver; +import android.text.TextUtils; +import android.util.Log; +import android.view.KeyEvent; + +import java.lang.ref.WeakReference; +import java.util.ArrayList; + +/** + * Allows an app to interact with an ongoing media session. Media buttons and + * other commands can be sent to the session. A callback may be registered to + * receive updates from the session, such as metadata and play state changes. + *

+ * A MediaController can be created through {@link MediaSessionManager} if you + * hold the "android.permission.MEDIA_CONTENT_CONTROL" permission or directly if + * you have a {@link MediaSessionToken} from the session owner. + *

+ * MediaController objects are thread-safe. + */ +public final class MediaController { + private static final String TAG = "SessionController"; + + private static final int MSG_EVENT = 1; + private static final int MESSAGE_PLAYBACK_STATE = 2; + private static final int MESSAGE_METADATA = 3; + private static final int MSG_ROUTE = 4; + + private final ISessionController mSessionBinder; + + private final CallbackStub mCbStub = new CallbackStub(this); + private final ArrayList mCallbacks = new ArrayList(); + private final Object mLock = new Object(); + + private boolean mCbRegistered = false; + + private TransportController mTransportController; + + private MediaController(ISessionController sessionBinder) { + mSessionBinder = sessionBinder; + } + + /** + * @hide + */ + public static MediaController fromBinder(ISessionController sessionBinder) { + MediaController controller = new MediaController(sessionBinder); + try { + controller.mSessionBinder.registerCallbackListener(controller.mCbStub); + if (controller.mSessionBinder.isTransportControlEnabled()) { + controller.mTransportController = new TransportController(sessionBinder); + } + } catch (RemoteException e) { + Log.wtf(TAG, "MediaController created with expired token", e); + controller = null; + } + return controller; + } + + /** + * Get a new MediaController for a MediaSessionToken. If successful the + * controller returned will be connected to the session that generated the + * token. + * + * @param token The session token to use + * @return A controller for the session or null + */ + public static MediaController fromToken(MediaSessionToken token) { + return fromBinder(token.getBinder()); + } + + /** + * Get a TransportController if the session supports it. If it is not + * supported null will be returned. + * + * @return A TransportController or null + */ + public TransportController getTransportController() { + return mTransportController; + } + + /** + * Send the specified media button to the session. Only media keys can be + * sent using this method. + * + * @param keycode The media button keycode, such as + * {@link KeyEvent#KEYCODE_MEDIA_PLAY}. + */ + public void sendMediaButton(int keycode) { + if (!KeyEvent.isMediaKey(keycode)) { + throw new IllegalArgumentException("May only send media buttons through " + + "sendMediaButton"); + } + // TODO do something better than key down/up events + KeyEvent event = new KeyEvent(KeyEvent.ACTION_UP, keycode); + try { + mSessionBinder.sendMediaButton(event); + } catch (RemoteException e) { + Log.d(TAG, "Dead object in sendMediaButton", e); + } + } + + /** + * Adds a callback to receive updates from the Session. Updates will be + * posted on the caller's thread. + * + * @param cb The callback object, must not be null + */ + public void addCallback(Callback cb) { + addCallback(cb, null); + } + + /** + * Adds a callback to receive updates from the session. Updates will be + * posted on the specified handler's thread. + * + * @param cb Cannot be null. + * @param handler The handler to post updates on. If null the callers thread + * will be used + */ + public void addCallback(Callback cb, Handler handler) { + if (handler == null) { + handler = new Handler(); + } + synchronized (mLock) { + addCallbackLocked(cb, handler); + } + } + + /** + * Stop receiving updates on the specified callback. If an update has + * already been posted you may still receive it after calling this method. + * + * @param cb The callback to remove + */ + public void removeCallback(Callback cb) { + synchronized (mLock) { + removeCallbackLocked(cb); + } + } + + /** + * 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 sendCommand(String command, Bundle params, ResultReceiver cb) { + if (TextUtils.isEmpty(command)) { + throw new IllegalArgumentException("command cannot be null or empty"); + } + try { + mSessionBinder.sendCommand(command, params, cb); + } catch (RemoteException e) { + Log.d(TAG, "Dead object in sendCommand.", e); + } + } + + /** + * Request that the route picker be shown for this session. This should + * generally be called in response to a user action. + * + * @hide + */ + public void showRoutePicker() { + try { + mSessionBinder.showRoutePicker(); + } catch (RemoteException e) { + Log.d(TAG, "Dead object in showRoutePicker", e); + } + } + + /* + * @hide + */ + ISessionController getSessionBinder() { + return mSessionBinder; + } + + private void addCallbackLocked(Callback cb, Handler handler) { + if (cb == null) { + throw new IllegalArgumentException("Callback cannot be null"); + } + if (handler == null) { + throw new IllegalArgumentException("Handler cannot be null"); + } + if (getHandlerForCallbackLocked(cb) != null) { + Log.w(TAG, "Callback is already added, ignoring"); + return; + } + MessageHandler holder = new MessageHandler(handler.getLooper(), cb); + mCallbacks.add(holder); + + if (!mCbRegistered) { + try { + mSessionBinder.registerCallbackListener(mCbStub); + mCbRegistered = true; + } catch (RemoteException e) { + Log.d(TAG, "Dead object in registerCallback", e); + } + } + } + + private boolean removeCallbackLocked(Callback cb) { + if (cb == null) { + throw new IllegalArgumentException("Callback cannot be null"); + } + for (int i = mCallbacks.size() - 1; i >= 0; i--) { + MessageHandler handler = mCallbacks.get(i); + if (cb == handler.mCallback) { + mCallbacks.remove(i); + return true; + } + } + return false; + } + + private MessageHandler getHandlerForCallbackLocked(Callback cb) { + if (cb == null) { + throw new IllegalArgumentException("Callback cannot be null"); + } + for (int i = mCallbacks.size() - 1; i >= 0; i--) { + MessageHandler handler = mCallbacks.get(i); + if (cb == handler.mCallback) { + return handler; + } + } + return null; + } + + private void postEvent(String event, Bundle extras) { + synchronized (mLock) { + for (int i = mCallbacks.size() - 1; i >= 0; i--) { + mCallbacks.get(i).post(MSG_EVENT, event, extras); + } + } + } + + private void postRouteChanged(RouteInfo route) { + synchronized (mLock) { + for (int i = mCallbacks.size() - 1; i >= 0; i--) { + mCallbacks.get(i).post(MSG_ROUTE, route, null); + } + } + } + + /** + * Callback for receiving updates on from the session. A Callback can be + * registered using {@link #addCallback} + */ + public static abstract class Callback { + /** + * 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 + */ + public void onEvent(String event, Bundle extras) { + } + + /** + * Override to handle route changes for this session. + * + * @param route The new route + * @hide + */ + public void onRouteChanged(RouteInfo route) { + } + } + + private final static class CallbackStub extends ISessionControllerCallback.Stub { + private final WeakReference mController; + + public CallbackStub(MediaController controller) { + mController = new WeakReference(controller); + } + + @Override + public void onEvent(String event, Bundle extras) { + MediaController controller = mController.get(); + if (controller != null) { + controller.postEvent(event, extras); + } + } + + @Override + public void onRouteChanged(RouteInfo route) { + MediaController controller = mController.get(); + if (controller != null) { + controller.postRouteChanged(route); + } + } + + @Override + public void onPlaybackStateChanged(PlaybackState state) { + MediaController controller = mController.get(); + if (controller != null) { + TransportController tc = controller.getTransportController(); + if (tc != null) { + tc.postPlaybackStateChanged(state); + } + } + } + + @Override + public void onMetadataChanged(MediaMetadata metadata) { + MediaController controller = mController.get(); + if (controller != null) { + TransportController tc = controller.getTransportController(); + if (tc != null) { + tc.postMetadataChanged(metadata); + } + } + } + + } + + private final static class MessageHandler extends Handler { + private final MediaController.Callback mCallback; + + public MessageHandler(Looper looper, MediaController.Callback cb) { + super(looper, null, true); + mCallback = cb; + } + + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case MSG_EVENT: + mCallback.onEvent((String) msg.obj, msg.getData()); + break; + case MSG_ROUTE: + mCallback.onRouteChanged((RouteInfo) msg.obj); + } + } + + public void post(int what, Object obj, Bundle data) { + obtainMessage(what, obj).sendToTarget(); + } + } + +} diff --git a/media/java/android/media/session/MediaMetadata.aidl b/media/java/android/media/session/MediaMetadata.aidl deleted file mode 100644 index 4431d9d..0000000 --- a/media/java/android/media/session/MediaMetadata.aidl +++ /dev/null @@ -1,18 +0,0 @@ -/* Copyright 2014, The Android Open Source Project -** -** Licensed under the Apache License, Version 2.0 (the "License"); -** you may not use this file except in compliance with the License. -** You may obtain a copy of the License at -** -** http://www.apache.org/licenses/LICENSE-2.0 -** -** Unless required by applicable law or agreed to in writing, software -** distributed under the License is distributed on an "AS IS" BASIS, -** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -** See the License for the specific language governing permissions and -** limitations under the License. -*/ - -package android.media.session; - -parcelable MediaMetadata; diff --git a/media/java/android/media/session/MediaMetadata.java b/media/java/android/media/session/MediaMetadata.java deleted file mode 100644 index 8a8af45..0000000 --- a/media/java/android/media/session/MediaMetadata.java +++ /dev/null @@ -1,475 +0,0 @@ -/* - * Copyright (C) 2014 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package android.media.session; - -import android.graphics.Bitmap; -import android.media.MediaMetadataEditor; -import android.media.MediaMetadataRetriever; -import android.media.Rating; -import android.os.Bundle; -import android.os.Parcel; -import android.os.Parcelable; -import android.util.ArrayMap; -import android.util.Log; -import android.util.SparseArray; - -/** - * Contains metadata about an item, such as the title, artist, etc. - */ -public final class MediaMetadata 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 Rating - */ - public static final String METADATA_KEY_USER_RATING = "android.media.metadata.USER_RATING"; - - /** - * The overall rating for the media. - * - * @see Rating - */ - public static final String METADATA_KEY_RATING = "android.media.metadata.RATING"; - - private static final int METADATA_TYPE_INVALID = -1; - 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 METADATA_KEYS_TYPE; - - static { - METADATA_KEYS_TYPE = new ArrayMap(); - 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 static final SparseArray EDITOR_KEY_MAPPING; - - static { - EDITOR_KEY_MAPPING = new SparseArray(); - EDITOR_KEY_MAPPING.put(MediaMetadataEditor.BITMAP_KEY_ARTWORK, METADATA_KEY_ART); - EDITOR_KEY_MAPPING.put(MediaMetadataEditor.RATING_KEY_BY_OTHERS, METADATA_KEY_RATING); - EDITOR_KEY_MAPPING.put(MediaMetadataEditor.RATING_KEY_BY_USER, METADATA_KEY_USER_RATING); - EDITOR_KEY_MAPPING.put(MediaMetadataRetriever.METADATA_KEY_ALBUM, METADATA_KEY_ALBUM); - EDITOR_KEY_MAPPING.put(MediaMetadataRetriever.METADATA_KEY_ALBUMARTIST, - METADATA_KEY_ALBUM_ARTIST); - EDITOR_KEY_MAPPING.put(MediaMetadataRetriever.METADATA_KEY_ARTIST, METADATA_KEY_ARTIST); - EDITOR_KEY_MAPPING.put(MediaMetadataRetriever.METADATA_KEY_AUTHOR, METADATA_KEY_AUTHOR); - EDITOR_KEY_MAPPING.put(MediaMetadataRetriever.METADATA_KEY_CD_TRACK_NUMBER, - METADATA_KEY_TRACK_NUMBER); - EDITOR_KEY_MAPPING.put(MediaMetadataRetriever.METADATA_KEY_COMPOSER, METADATA_KEY_COMPOSER); - EDITOR_KEY_MAPPING.put(MediaMetadataRetriever.METADATA_KEY_COMPILATION, - METADATA_KEY_COMPILATION); - EDITOR_KEY_MAPPING.put(MediaMetadataRetriever.METADATA_KEY_DATE, METADATA_KEY_DATE); - EDITOR_KEY_MAPPING.put(MediaMetadataRetriever.METADATA_KEY_DISC_NUMBER, - METADATA_KEY_DISC_NUMBER); - EDITOR_KEY_MAPPING.put(MediaMetadataRetriever.METADATA_KEY_DURATION, METADATA_KEY_DURATION); - EDITOR_KEY_MAPPING.put(MediaMetadataRetriever.METADATA_KEY_GENRE, METADATA_KEY_GENRE); - EDITOR_KEY_MAPPING.put(MediaMetadataRetriever.METADATA_KEY_NUM_TRACKS, - METADATA_KEY_NUM_TRACKS); - EDITOR_KEY_MAPPING.put(MediaMetadataRetriever.METADATA_KEY_TITLE, METADATA_KEY_TITLE); - EDITOR_KEY_MAPPING.put(MediaMetadataRetriever.METADATA_KEY_WRITER, METADATA_KEY_WRITER); - EDITOR_KEY_MAPPING.put(MediaMetadataRetriever.METADATA_KEY_YEAR, METADATA_KEY_YEAR); - } - - private final Bundle mBundle; - - private MediaMetadata(Bundle bundle) { - mBundle = new Bundle(bundle); - } - - private MediaMetadata(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 Rating} 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 Rating} or null - */ - public Rating getRating(String key) { - Rating rating = null; - try { - rating = mBundle.getParcelable(key); - } catch (Exception e) { - // ignore, value was not a bitmap - Log.d(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.d(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(); - } - - /** - * Helper for getting the String key used by {@link MediaMetadata} from the - * integer key that {@link MediaMetadataEditor} uses. - * - * @param editorKey The key used by the editor - * @return The key used by this class or null if no mapping exists - * @hide - */ - public static String getKeyFromMetadataEditorKey(int editorKey) { - return EDITOR_KEY_MAPPING.get(editorKey, null); - } - - public static final Parcelable.Creator CREATOR - = new Parcelable.Creator() { - @Override - public MediaMetadata createFromParcel(Parcel in) { - return new MediaMetadata(in); - } - - @Override - public MediaMetadata[] newArray(int size) { - return new MediaMetadata[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 MediaMetadata} must be added. - */ - public Builder() { - mBundle = new Bundle(); - } - - /** - * Create a Builder using a {@link MediaMetadata} 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(MediaMetadata 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: - *

    - *
  • {@link #METADATA_KEY_TITLE}
  • - *
  • {@link #METADATA_KEY_ARTIST}
  • - *
  • {@link #METADATA_KEY_ALBUM}
  • - *
  • {@link #METADATA_KEY_AUTHOR}
  • - *
  • {@link #METADATA_KEY_WRITER}
  • - *
  • {@link #METADATA_KEY_COMPOSER}
  • - *
  • {@link #METADATA_KEY_DATE}
  • - *
  • {@link #METADATA_KEY_GENRE}
  • - *
  • {@link #METADATA_KEY_ALBUM_ARTIST}
  • - *
  • {@link #METADATA_KEY_ART_URI}
  • - *
  • {@link #METADATA_KEY_ALBUM_ART_URI}
  • - *
- * - * @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: - *
    - *
  • {@link #METADATA_KEY_DURATION}
  • - *
  • {@link #METADATA_KEY_TRACK_NUMBER}
  • - *
  • {@link #METADATA_KEY_NUM_TRACKS}
  • - *
  • {@link #METADATA_KEY_DISC_NUMBER}
  • - *
  • {@link #METADATA_KEY_YEAR}
  • - *
- * - * @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 Rating} 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: - *
    - *
  • {@link #METADATA_KEY_RATING}
  • - *
  • {@link #METADATA_KEY_USER_RATING}
  • - *
- * - * @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, Rating 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: - *
    - *
  • {@link #METADATA_KEY_ART}
  • - *
  • {@link #METADATA_KEY_ALBUM_ART}
  • - *
- * - * @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 MediaMetadata} instance with the specified fields. - * - * @return The new MediaMetadata instance - */ - public MediaMetadata build() { - return new MediaMetadata(mBundle); - } - } - -} diff --git a/media/java/android/media/session/MediaSession.java b/media/java/android/media/session/MediaSession.java new file mode 100644 index 0000000..5b9adaa --- /dev/null +++ b/media/java/android/media/session/MediaSession.java @@ -0,0 +1,777 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.media.session; + +import android.content.Intent; +import android.media.Rating; +import android.media.session.ISessionController; +import android.media.session.ISession; +import android.media.session.ISessionCallback; +import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; +import android.os.Message; +import android.os.RemoteException; +import android.os.ResultReceiver; +import android.text.TextUtils; +import android.util.ArrayMap; +import android.util.Log; + +import java.lang.ref.WeakReference; +import java.util.ArrayList; +import java.util.List; + +/** + * Allows interaction with media controllers, volume keys, media buttons, and + * transport controls. + *

+ * 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. + *

+ * A MediaSession is created by calling + * {@link MediaSessionManager#createSession(String)}. Once a session is created + * apps that have the MEDIA_CONTENT_CONTROL permission can interact with the + * session through + * {@link MediaSessionManager#getActiveSessions(android.content.ComponentName)}. + * The owner of the session may also use {@link #getSessionToken()} to allow + * apps without this permission to create a {@link MediaController} to interact + * with this session. + *

+ * To receive commands, media keys, and other events a Callback must be set with + * {@link #addCallback(Callback)}. + *

+ * When an app is finished performing playback it must call {@link #release()} + * to clean up the session and notify any controllers. + *

+ * MediaSession objects are thread safe + */ +public final class MediaSession { + private static final String TAG = "Session"; + + /** + * 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 commands through + * the {@link TransportPerformer}. The performer can be retrieved by calling + * {@link #getTransportPerformer()}. + */ + public static final int FLAG_HANDLES_TRANSPORT_CONTROLS = 1 << 1; + + /** + * System only flag for a session that needs to have priority over all other + * sessions. This flag ensures this session will receive media button events + * regardless of the current ordering in the system. + * + * @hide + */ + public static final int FLAG_EXCLUSIVE_GLOBAL_PRIORITY = 1 << 16; + + /** + * Indicates the session was disconnected because the user that the session + * belonged to is stopping. + * @hide + */ + public static final int DISCONNECT_REASON_USER_STOPPING = 1; + + /** + * Indicates the session was disconnected because the provider disconnected + * the route. + * @hide + */ + public static final int DISCONNECT_REASON_PROVIDER_DISCONNECTED = 2; + + /** + * Indicates the session was disconnected because the route has changed. + * @hide + */ + public static final int DISCONNECT_REASON_ROUTE_CHANGED = 3; + + /** + * Indicates the session was disconnected because the session owner + * requested it disconnect. + * @hide + */ + public static final int DISCONNECT_REASON_SESSION_DISCONNECTED = 4; + + /** + * Indicates the session was disconnected because it was destroyed. + * @hide + */ + public static final int DISCONNECT_REASON_SESSION_DESTROYED = 5; + + /** + * Status code indicating the call was handled. + * + * @hide + */ + public static final int RESULT_SUCCESS = 0; + + private static final int MSG_MEDIA_BUTTON = 1; + private static final int MSG_COMMAND = 2; + private static final int MSG_ROUTE_CHANGE = 3; + private static final int MSG_ROUTE_CONNECTED = 4; + private static final int MSG_ROUTE_DISCONNECTED = 5; + + private static final String KEY_COMMAND = "command"; + private static final String KEY_EXTRAS = "extras"; + private static final String KEY_CALLBACK = "callback"; + + private final Object mLock = new Object(); + + private final MediaSessionToken mSessionToken; + private final ISession mBinder; + private final CallbackStub mCbStub; + + private final ArrayList mCallbacks = new ArrayList(); + // TODO route interfaces + private final ArrayMap mInterfaceListeners + = new ArrayMap(); + + private TransportPerformer mPerformer; + private Route mRoute; + + private boolean mActive = false;; + + /** + * @hide + */ + public MediaSession(ISession binder, CallbackStub cbStub) { + mBinder = binder; + mCbStub = cbStub; + ISessionController controllerBinder = null; + try { + controllerBinder = mBinder.getController(); + } catch (RemoteException e) { + throw new RuntimeException("Dead object in MediaSessionController constructor: ", e); + } + mSessionToken = new MediaSessionToken(controllerBinder); + mPerformer = new TransportPerformer(mBinder); + } + + /** + * Set the callback to receive updates on. + * + * @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 cannot be null"); + } + synchronized (mLock) { + if (getHandlerForCallbackLocked(callback) != null) { + Log.w(TAG, "Callback is already added, ignoring"); + return; + } + if (handler == null) { + handler = new Handler(); + } + MessageHandler msgHandler = new MessageHandler(handler.getLooper(), callback); + mCallbacks.add(msgHandler); + } + } + + /** + * Remove a callback. It will no longer receive updates. + * + * @param callback The callback to remove. + */ + public void removeCallback(Callback callback) { + synchronized (mLock) { + removeCallbackLocked(callback); + } + } + + /** + * Retrieves the {@link TransportPerformer} for this session. To receive + * commands through the performer you must also set the + * {@link #FLAG_HANDLES_TRANSPORT_CONTROLS} flag using + * {@link #setFlags(int)}. + * + * @return The performer associated with this session. + */ + public TransportPerformer getTransportPerformer() { + return mPerformer; + } + + /** + * Set any flags for the session. + * + * @param flags The flags to set for this session. + */ + public void setFlags(int flags) { + try { + mBinder.setFlags(flags); + } catch (RemoteException e) { + Log.wtf(TAG, "Failure in setFlags.", e); + } + } + + /** + * 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) { + if (mActive == active) { + return; + } + try { + mBinder.setActive(active); + mActive = active; + } catch (RemoteException e) { + Log.wtf(TAG, "Failure in setActive.", e); + } + } + + /** + * Get the current active state of this session. + * + * @return True if the session is active, false otherwise. + */ + public boolean isActive() { + return mActive; + } + + /** + * 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 sendEvent(String event, Bundle extras) { + if (TextUtils.isEmpty(event)) { + throw new IllegalArgumentException("event cannot be null or empty"); + } + try { + mBinder.sendEvent(event, extras); + } catch (RemoteException e) { + Log.wtf(TAG, "Error sending event", e); + } + } + + /** + * This must be called when an app has finished performing playback. If + * playback is expected to start again shortly the session can be left open, + * but it must be released if your activity or service is being destroyed. + */ + public void release() { + try { + mBinder.destroy(); + } catch (RemoteException e) { + Log.wtf(TAG, "Error releasing session: ", e); + } + } + + /** + * Retrieve a token object that can be used by apps to create a + * {@link MediaController} for interacting with this session. The owner of + * the session is responsible for deciding how to distribute these tokens. + * + * @return A token that can be used to create a MediaController for this + * session + */ + public MediaSessionToken getSessionToken() { + return mSessionToken; + } + + /** + * Connect to the current route using the specified request. + *

+ * Connection updates will be sent to the callback's + * {@link Callback#onRouteConnected(Route)} and + * {@link Callback#onRouteDisconnected(Route, int)} methods. If the + * connection fails {@link Callback#onRouteDisconnected(Route, int)} will be + * called. + *

+ * If you already have a connection to this route it will be disconnected + * before the new connection is established. TODO add an easy way to compare + * MediaRouteOptions. + * + * @param route The route the app is trying to connect to. + * @param request The connection request to use. + * @hide + */ + public void connect(RouteInfo route, RouteOptions request) { + if (route == null) { + throw new IllegalArgumentException("Must specify the route"); + } + if (request == null) { + throw new IllegalArgumentException("Must specify the connection request"); + } + try { + mBinder.connectToRoute(route, request); + } catch (RemoteException e) { + Log.wtf(TAG, "Error starting connection to route", e); + } + } + + /** + * Disconnect from the current route. After calling you will be switched + * back to the default route. + * + * @hide + */ + public void disconnect() { + if (mRoute != null) { + try { + mBinder.disconnectFromRoute(mRoute.getRouteInfo()); + } catch (RemoteException e) { + Log.wtf(TAG, "Error disconnecting from route"); + } + } + } + + /** + * Set the list of route options your app is interested in connecting to. It + * will be used for picking valid routes. + * + * @param options The set of route options your app may use to connect. + * @hide + */ + public void setRouteOptions(List options) { + try { + mBinder.setRouteOptions(options); + } catch (RemoteException e) { + Log.wtf(TAG, "Error setting route options.", e); + } + } + + /** + * @hide + * TODO allow multiple listeners for the same interface, allow removal + */ + public void addInterfaceListener(String iface, + RouteInterface.EventListener listener) { + mInterfaceListeners.put(iface, listener); + } + + /** + * @hide + */ + public boolean sendRouteCommand(RouteCommand command, ResultReceiver cb) { + try { + mBinder.sendRouteCommand(command, cb); + } catch (RemoteException e) { + Log.wtf(TAG, "Error sending command to route.", e); + return false; + } + return true; + } + + private MessageHandler getHandlerForCallbackLocked(Callback cb) { + if (cb == null) { + throw new IllegalArgumentException("Callback cannot be null"); + } + for (int i = mCallbacks.size() - 1; i >= 0; i--) { + MessageHandler handler = mCallbacks.get(i); + if (cb == handler.mCallback) { + return handler; + } + } + return null; + } + + private boolean removeCallbackLocked(Callback cb) { + if (cb == null) { + throw new IllegalArgumentException("Callback cannot be null"); + } + for (int i = mCallbacks.size() - 1; i >= 0; i--) { + MessageHandler handler = mCallbacks.get(i); + if (cb == handler.mCallback) { + mCallbacks.remove(i); + return true; + } + } + return false; + } + + private void postCommand(String command, Bundle extras, ResultReceiver resultCb) { + Command cmd = new Command(command, extras, resultCb); + synchronized (mLock) { + for (int i = mCallbacks.size() - 1; i >= 0; i--) { + mCallbacks.get(i).post(MSG_COMMAND, cmd); + } + } + } + + private void postMediaButton(Intent mediaButtonIntent) { + synchronized (mLock) { + for (int i = mCallbacks.size() - 1; i >= 0; i--) { + mCallbacks.get(i).post(MSG_MEDIA_BUTTON, mediaButtonIntent); + } + } + } + + private void postRequestRouteChange(RouteInfo route) { + synchronized (mLock) { + for (int i = mCallbacks.size() - 1; i >= 0; i--) { + mCallbacks.get(i).post(MSG_ROUTE_CHANGE, route); + } + } + } + + private void postRouteConnected(RouteInfo route, RouteOptions options) { + synchronized (mLock) { + mRoute = new Route(route, options, this); + for (int i = mCallbacks.size() - 1; i >= 0; i--) { + mCallbacks.get(i).post(MSG_ROUTE_CONNECTED, mRoute); + } + } + } + + private void postRouteDisconnected(RouteInfo route, int reason) { + synchronized (mLock) { + if (mRoute != null && TextUtils.equals(mRoute.getRouteInfo().getId(), route.getId())) { + for (int i = mCallbacks.size() - 1; i >= 0; i--) { + mCallbacks.get(i).post(MSG_ROUTE_DISCONNECTED, mRoute, reason); + } + } + } + } + + /** + * Receives commands or updates from controllers and routes. An app can + * specify what commands and buttons it supports by setting them on the + * MediaSession (TODO). + */ + public abstract static class Callback { + + public Callback() { + } + + /** + * Called when a media button is pressed and this session has the + * highest priority or a controller sends a media button event to the + * session. TODO determine if using Intents identical to the ones + * RemoteControlClient receives is useful + *

+ * The intent will be of type {@link Intent#ACTION_MEDIA_BUTTON} with a + * KeyEvent in {@link Intent#EXTRA_KEY_EVENT} + * + * @param mediaButtonIntent an intent containing the KeyEvent as an + * extra + */ + public void onMediaButton(Intent mediaButtonIntent) { + } + + /** + * Called when a controller has sent a custom command to this session. + * The owner of the session may handle custom commands but is not + * required to. + * + * @param command + * @param extras optional + */ + public void onCommand(String command, Bundle extras, ResultReceiver cb) { + } + + /** + * Called when the user has selected a different route to connect to. + * The app is responsible for connecting to the new route and migrating + * ongoing playback if necessary. + * + * @param route + * @hide + */ + public void onRequestRouteChange(RouteInfo route) { + } + + /** + * Called when a route has successfully connected. Calls to the route + * are now valid. + * + * @param route The route that was connected + * @hide + */ + public void onRouteConnected(Route route) { + } + + /** + * Called when a route was disconnected. Further calls to the route will + * fail. If available a reason for being disconnected will be provided. + *

+ * Valid reasons are: + *

    + *
  • {@link #DISCONNECT_REASON_USER_STOPPING}
  • + *
  • {@link #DISCONNECT_REASON_PROVIDER_DISCONNECTED}
  • + *
  • {@link #DISCONNECT_REASON_ROUTE_CHANGED}
  • + *
  • {@link #DISCONNECT_REASON_SESSION_DISCONNECTED}
  • + *
  • {@link #DISCONNECT_REASON_SESSION_DESTROYED}
  • + *
+ * + * @param route The route that disconnected + * @param reason The reason for the disconnect + * @hide + */ + public void onRouteDisconnected(Route route, int reason) { + } + } + + /** + * @hide + */ + public static class CallbackStub extends ISessionCallback.Stub { + private WeakReference mMediaSession; + + public void setMediaSession(MediaSession session) { + mMediaSession = new WeakReference(session); + } + + @Override + public void onCommand(String command, Bundle extras, ResultReceiver cb) + throws RemoteException { + MediaSession session = mMediaSession.get(); + if (session != null) { + session.postCommand(command, extras, cb); + } + } + + @Override + public void onMediaButton(Intent mediaButtonIntent, ResultReceiver cb) + throws RemoteException { + MediaSession session = mMediaSession.get(); + if (session != null) { + session.postMediaButton(mediaButtonIntent); + } + if (cb != null) { + cb.send(RESULT_SUCCESS, null); + } + } + + @Override + public void onRequestRouteChange(RouteInfo route) throws RemoteException { + MediaSession session = mMediaSession.get(); + if (session != null) { + session.postRequestRouteChange(route); + } + } + + @Override + public void onRouteConnected(RouteInfo route, RouteOptions options) { + MediaSession session = mMediaSession.get(); + if (session != null) { + session.postRouteConnected(route, options); + } + } + + @Override + public void onRouteDisconnected(RouteInfo route, int reason) { + MediaSession session = mMediaSession.get(); + if (session != null) { + session.postRouteDisconnected(route, reason); + } + } + + @Override + public void onPlay() throws RemoteException { + MediaSession session = mMediaSession.get(); + if (session != null) { + TransportPerformer tp = session.getTransportPerformer(); + if (tp != null) { + tp.onPlay(); + } + } + } + + @Override + public void onPause() throws RemoteException { + MediaSession session = mMediaSession.get(); + if (session != null) { + TransportPerformer tp = session.getTransportPerformer(); + if (tp != null) { + tp.onPause(); + } + } + } + + @Override + public void onStop() throws RemoteException { + MediaSession session = mMediaSession.get(); + if (session != null) { + TransportPerformer tp = session.getTransportPerformer(); + if (tp != null) { + tp.onStop(); + } + } + } + + @Override + public void onNext() throws RemoteException { + MediaSession session = mMediaSession.get(); + if (session != null) { + TransportPerformer tp = session.getTransportPerformer(); + if (tp != null) { + tp.onNext(); + } + } + } + + @Override + public void onPrevious() throws RemoteException { + MediaSession session = mMediaSession.get(); + if (session != null) { + TransportPerformer tp = session.getTransportPerformer(); + if (tp != null) { + tp.onPrevious(); + } + } + } + + @Override + public void onFastForward() throws RemoteException { + MediaSession session = mMediaSession.get(); + if (session != null) { + TransportPerformer tp = session.getTransportPerformer(); + if (tp != null) { + tp.onFastForward(); + } + } + } + + @Override + public void onRewind() throws RemoteException { + MediaSession session = mMediaSession.get(); + if (session != null) { + TransportPerformer tp = session.getTransportPerformer(); + if (tp != null) { + tp.onRewind(); + } + } + } + + @Override + public void onSeekTo(long pos) throws RemoteException { + MediaSession session = mMediaSession.get(); + if (session != null) { + TransportPerformer tp = session.getTransportPerformer(); + if (tp != null) { + tp.onSeekTo(pos); + } + } + } + + @Override + public void onRate(Rating rating) throws RemoteException { + MediaSession session = mMediaSession.get(); + if (session != null) { + TransportPerformer tp = session.getTransportPerformer(); + if (tp != null) { + tp.onRate(rating); + } + } + } + + @Override + public void onRouteEvent(RouteEvent event) throws RemoteException { + MediaSession session = mMediaSession.get(); + if (session != null) { + RouteInterface.EventListener iface + = session.mInterfaceListeners.get(event.getIface()); + Log.d(TAG, "Received route event on iface " + event.getIface() + ". Listener is " + + iface); + if (iface != null) { + iface.onEvent(event.getEvent(), event.getExtras()); + } + } + } + + @Override + public void onRouteStateChange(int state) throws RemoteException { + // TODO + + } + + } + + private class MessageHandler extends Handler { + private MediaSession.Callback mCallback; + + public MessageHandler(Looper looper, MediaSession.Callback callback) { + super(looper, null, true); + mCallback = callback; + } + + @Override + public void handleMessage(Message msg) { + synchronized (mLock) { + if (mCallback == null) { + return; + } + switch (msg.what) { + case MSG_MEDIA_BUTTON: + mCallback.onMediaButton((Intent) msg.obj); + break; + case MSG_COMMAND: + Command cmd = (Command) msg.obj; + mCallback.onCommand(cmd.command, cmd.extras, cmd.stub); + break; + case MSG_ROUTE_CHANGE: + mCallback.onRequestRouteChange((RouteInfo) msg.obj); + break; + case MSG_ROUTE_CONNECTED: + mCallback.onRouteConnected((Route) msg.obj); + break; + case MSG_ROUTE_DISCONNECTED: + mCallback.onRouteDisconnected((Route) msg.obj, msg.arg1); + break; + } + } + } + + public void post(int what, Object obj) { + obtainMessage(what, obj).sendToTarget(); + } + + public void post(int what, Object obj, int arg1) { + obtainMessage(what, arg1, 0, obj).sendToTarget(); + } + } + + private static final class Command { + public final String command; + public final Bundle extras; + public final ResultReceiver stub; + + public Command(String command, Bundle extras, ResultReceiver stub) { + this.command = command; + this.extras = extras; + this.stub = stub; + } + } +} diff --git a/media/java/android/media/session/MediaSessionInfo.java b/media/java/android/media/session/MediaSessionInfo.java new file mode 100644 index 0000000..3d8d33f --- /dev/null +++ b/media/java/android/media/session/MediaSessionInfo.java @@ -0,0 +1,87 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package android.media.session; + +import android.os.Parcel; +import android.os.Parcelable; + +/** + * Information about a media session, including the owner's package name. + */ +public final class MediaSessionInfo implements Parcelable { + private final String mId; + private final String mPackageName; + + /** + * @hide + */ + public MediaSessionInfo(String id, String packageName) { + mId = id; + mPackageName = packageName; + } + + private MediaSessionInfo(Parcel in) { + mId = in.readString(); + mPackageName = in.readString(); + } + + /** + * Get the package name of the owner of this session. + * + * @return The owner's package name + */ + public String getPackageName() { + return mPackageName; + } + + /** + * Get the unique id for this session. + * + * @return The id for the session. + */ + public String getId() { + return mId; + } + + @Override + public String toString() { + return "SessionInfo {id=" + mId + ", pkg=" + mPackageName + "}"; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeString(mId); + dest.writeString(mPackageName); + } + + public static final Parcelable.Creator CREATOR + = new Parcelable.Creator() { + @Override + public MediaSessionInfo createFromParcel(Parcel in) { + return new MediaSessionInfo(in); + } + + @Override + public MediaSessionInfo[] newArray(int size) { + return new MediaSessionInfo[size]; + } + }; +} diff --git a/media/java/android/media/session/MediaSessionLegacyHelper.java b/media/java/android/media/session/MediaSessionLegacyHelper.java index e1eae09..2e02a66 100644 --- a/media/java/android/media/session/MediaSessionLegacyHelper.java +++ b/media/java/android/media/session/MediaSessionLegacyHelper.java @@ -40,7 +40,7 @@ public class MediaSessionLegacyHelper { private static final Object sLock = new Object(); private static MediaSessionLegacyHelper sInstance; - private SessionManager mSessionManager; + private MediaSessionManager mSessionManager; private Handler mHandler = new Handler(Looper.getMainLooper()); // The legacy APIs use PendingIntents to register/unregister media button // receivers and these are associated with RCC. @@ -48,7 +48,7 @@ public class MediaSessionLegacyHelper { = new ArrayMap(); private MediaSessionLegacyHelper(Context context) { - mSessionManager = (SessionManager) context + mSessionManager = (MediaSessionManager) context .getSystemService(Context.MEDIA_SESSION_SERVICE); } @@ -64,7 +64,7 @@ public class MediaSessionLegacyHelper { return sInstance; } - public Session getSession(PendingIntent pi) { + public MediaSession getSession(PendingIntent pi) { SessionHolder holder = mSessions.get(pi); return holder == null ? null : holder.mSession; } @@ -96,7 +96,7 @@ public class MediaSessionLegacyHelper { } performer.addListener(listener, mHandler); holder.mRccListener = listener; - holder.mFlags |= Session.FLAG_HANDLES_TRANSPORT_CONTROLS; + holder.mFlags |= MediaSession.FLAG_HANDLES_TRANSPORT_CONTROLS; holder.mSession.setFlags(holder.mFlags); holder.update(); if (DEBUG) { @@ -112,7 +112,7 @@ public class MediaSessionLegacyHelper { if (holder != null && holder.mRccListener != null) { holder.mSession.getTransportPerformer().removeListener(holder.mRccListener); holder.mRccListener = null; - holder.mFlags &= ~Session.FLAG_HANDLES_TRANSPORT_CONTROLS; + holder.mFlags &= ~MediaSession.FLAG_HANDLES_TRANSPORT_CONTROLS; holder.mSession.setFlags(holder.mFlags); holder.update(); if (DEBUG) { @@ -137,7 +137,7 @@ public class MediaSessionLegacyHelper { return; } holder.mMediaButtonListener = new MediaButtonListener(pi, context); - holder.mFlags |= Session.FLAG_HANDLES_MEDIA_BUTTONS; + holder.mFlags |= MediaSession.FLAG_HANDLES_MEDIA_BUTTONS; holder.mSession.setFlags(holder.mFlags); holder.mSession.getTransportPerformer().addListener(holder.mMediaButtonListener, mHandler); @@ -155,7 +155,7 @@ public class MediaSessionLegacyHelper { SessionHolder holder = getHolder(pi, false); if (holder != null && holder.mMediaButtonListener != null) { holder.mSession.getTransportPerformer().removeListener(holder.mMediaButtonListener); - holder.mFlags &= ~Session.FLAG_HANDLES_MEDIA_BUTTONS; + holder.mFlags &= ~MediaSession.FLAG_HANDLES_MEDIA_BUTTONS; holder.mSession.setFlags(holder.mFlags); holder.mMediaButtonListener = null; @@ -171,7 +171,7 @@ public class MediaSessionLegacyHelper { private SessionHolder getHolder(PendingIntent pi, boolean createIfMissing) { SessionHolder holder = mSessions.get(pi); if (holder == null && createIfMissing) { - Session session = mSessionManager.createSession(TAG); + MediaSession session = mSessionManager.createSession(TAG); session.setActive(true); holder = new SessionHolder(session, pi); mSessions.put(pi, holder); @@ -189,7 +189,7 @@ public class MediaSessionLegacyHelper { } } - private static final class MediaButtonReceiver extends Session.Callback { + private static final class MediaButtonReceiver extends MediaSession.Callback { private final PendingIntent mPendingIntent; private final Context mContext; @@ -266,14 +266,14 @@ public class MediaSessionLegacyHelper { } private class SessionHolder { - public final Session mSession; + public final MediaSession mSession; public final PendingIntent mPi; public MediaButtonListener mMediaButtonListener; public MediaButtonReceiver mMediaButtonReceiver; public TransportPerformer.Listener mRccListener; public int mFlags; - public SessionHolder(Session session, PendingIntent pi) { + public SessionHolder(MediaSession session, PendingIntent pi) { mSession = session; mPi = pi; } diff --git a/media/java/android/media/session/MediaSessionManager.java b/media/java/android/media/session/MediaSessionManager.java new file mode 100644 index 0000000..0589a7d --- /dev/null +++ b/media/java/android/media/session/MediaSessionManager.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.media.session; + +import android.content.ComponentName; +import android.content.Context; +import android.media.session.ISessionManager; +import android.os.IBinder; +import android.os.RemoteException; +import android.os.ServiceManager; +import android.os.UserHandle; +import android.service.notification.NotificationListenerService; +import android.util.Log; +import android.view.KeyEvent; + +import java.util.ArrayList; +import java.util.List; + +/** + * MediaSessionManager allows the creation and control of MediaSessions in the + * system. A MediaSession enables publishing information about ongoing media and + * interacting with MediaControllers and MediaRoutes. + *

+ * Use Context.getSystemService(Context.MEDIA_SESSION_SERVICE) to + * get an instance of this class. + *

+ * + * @see MediaSession + * @see MediaController + */ +public final class MediaSessionManager { + private static final String TAG = "SessionManager"; + + private final ISessionManager mService; + + private Context mContext; + + /** + * @hide + */ + public MediaSessionManager(Context context) { + // Consider rewriting like DisplayManagerGlobal + // Decide if we need context + mContext = context; + IBinder b = ServiceManager.getService(Context.MEDIA_SESSION_SERVICE); + mService = ISessionManager.Stub.asInterface(b); + } + + /** + * Creates a new session. + * + * @param tag A short name for debugging purposes + * @return a {@link MediaSession} for the new session + */ + public MediaSession createSession(String tag) { + return createSessionAsUser(tag, UserHandle.myUserId()); + } + + /** + * Creates a new session as the specified user. To create a session as a + * user other than your own you must hold the + * {@link android.Manifest.permission#INTERACT_ACROSS_USERS_FULL} + * permission. + * + * @param tag A short name for debugging purposes + * @param userId The user id to create the session as. + * @return a {@link MediaSession} for the new session + * @hide + */ + public MediaSession createSessionAsUser(String tag, int userId) { + try { + MediaSession.CallbackStub cbStub = new MediaSession.CallbackStub(); + MediaSession session = new MediaSession(mService + .createSession(mContext.getPackageName(), cbStub, tag, userId), cbStub); + cbStub.setMediaSession(session); + + return session; + } catch (RemoteException e) { + Log.e(TAG, "Failed to create session: ", e); + return null; + } + } + + /** + * Get a list of controllers for all ongoing sessions. This requires the + * android.Manifest.permission.MEDIA_CONTENT_CONTROL permission be held by + * the calling app. You may also retrieve this list if your app is an + * enabled notification listener using the + * {@link NotificationListenerService} APIs, in which case you must pass the + * {@link ComponentName} of your enabled listener. + * + * @param notificationListener The enabled notification listener component. + * May be null. + * @return A list of controllers for ongoing sessions + */ + public List getActiveSessions(ComponentName notificationListener) { + return getActiveSessionsForUser(notificationListener, UserHandle.myUserId()); + } + + /** + * Get active sessions for a specific user. To retrieve actions for a user + * other than your own you must hold the + * {@link android.Manifest.permission#INTERACT_ACROSS_USERS_FULL} permission + * in addition to any other requirements. If you are an enabled notification + * listener you may only get sessions for the users you are enabled for. + * + * @param notificationListener The enabled notification listener component. + * May be null. + * @param userId The user id to fetch sessions for. + * @return A list of controllers for ongoing sessions. + * @hide + */ + public List getActiveSessionsForUser(ComponentName notificationListener, + int userId) { + ArrayList controllers = new ArrayList(); + try { + List binders = mService.getSessions(notificationListener, userId); + for (int i = binders.size() - 1; i >= 0; i--) { + MediaController controller = MediaController.fromBinder(ISessionController.Stub + .asInterface(binders.get(i))); + controllers.add(controller); + } + } catch (RemoteException e) { + Log.e(TAG, "Failed to get active sessions: ", e); + } + return controllers; + } + + /** + * Send a media key event. The receiver will be selected automatically. + * + * @param keyEvent The KeyEvent to send. + * @hide + */ + public void dispatchMediaKeyEvent(KeyEvent keyEvent) { + dispatchMediaKeyEvent(keyEvent, false); + } + + /** + * Send a media key event. The receiver will be selected automatically. + * + * @param keyEvent The KeyEvent to send + * @param needWakeLock true if a wake lock should be held while sending the + * key + * @hide + */ + public void dispatchMediaKeyEvent(KeyEvent keyEvent, boolean needWakeLock) { + try { + mService.dispatchMediaKeyEvent(keyEvent, needWakeLock); + } catch (RemoteException e) { + Log.e(TAG, "Failed to send key event.", e); + } + } +} diff --git a/media/java/android/media/session/MediaSessionToken.aidl b/media/java/android/media/session/MediaSessionToken.aidl new file mode 100644 index 0000000..5812682 --- /dev/null +++ b/media/java/android/media/session/MediaSessionToken.aidl @@ -0,0 +1,18 @@ +/* Copyright 2014, The Android Open Source Project +** +** Licensed under the Apache License, Version 2.0 (the "License"); +** you may not use this file except in compliance with the License. +** You may obtain a copy of the License at +** +** http://www.apache.org/licenses/LICENSE-2.0 +** +** Unless required by applicable law or agreed to in writing, software +** distributed under the License is distributed on an "AS IS" BASIS, +** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +** See the License for the specific language governing permissions and +** limitations under the License. +*/ + +package android.media.session; + +parcelable MediaSessionToken; diff --git a/media/java/android/media/session/MediaSessionToken.java b/media/java/android/media/session/MediaSessionToken.java new file mode 100644 index 0000000..f5569a4 --- /dev/null +++ b/media/java/android/media/session/MediaSessionToken.java @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.media.session; + +import android.media.session.ISessionController; +import android.os.Parcel; +import android.os.Parcelable; + +public class MediaSessionToken implements Parcelable { + private ISessionController mBinder; + + /** + * @hide + */ + MediaSessionToken(ISessionController binder) { + mBinder = binder; + } + + private MediaSessionToken(Parcel in) { + mBinder = ISessionController.Stub.asInterface(in.readStrongBinder()); + } + + /** + * @hide + */ + ISessionController getBinder() { + return mBinder; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeStrongBinder(mBinder.asBinder()); + } + + public static final Parcelable.Creator CREATOR + = new Parcelable.Creator() { + @Override + public MediaSessionToken createFromParcel(Parcel in) { + return new MediaSessionToken(in); + } + + @Override + public MediaSessionToken[] newArray(int size) { + return new MediaSessionToken[size]; + } + }; +} diff --git a/media/java/android/media/session/PlaybackState.java b/media/java/android/media/session/PlaybackState.java index 3254e5d..7ef38eaa 100644 --- a/media/java/android/media/session/PlaybackState.java +++ b/media/java/android/media/session/PlaybackState.java @@ -21,7 +21,7 @@ import android.os.Parcelable; import android.os.SystemClock; /** - * Playback state for a {@link Session}. This includes a state like + * Playback state for a {@link MediaSession}. This includes a state like * {@link PlaybackState#PLAYSTATE_PLAYING}, the current playback position, * and the current control capabilities. */ @@ -160,6 +160,7 @@ public final class PlaybackState implements Parcelable { * route. Depending on the implementation you may return to the previous * state when the connection finishes or enter {@link #PLAYSTATE_NONE}. If * the connection failed {@link #PLAYSTATE_ERROR} should be used. + * @hide */ public final static int PLAYSTATE_CONNECTING = 8; diff --git a/media/java/android/media/session/Route.java b/media/java/android/media/session/Route.java index c9530a6..935eb5b 100644 --- a/media/java/android/media/session/Route.java +++ b/media/java/android/media/session/Route.java @@ -28,17 +28,18 @@ import java.util.List; * to. The MediaRoute must be used to get {@link RouteInterface} * instances which can be used to communicate over a specific interface on the * route. + * @hide */ public final class Route { private static final String TAG = "Route"; private final RouteInfo mInfo; - private final Session mSession; + private final MediaSession mSession; private final RouteOptions mOptions; /** * @hide */ - public Route(RouteInfo info, RouteOptions options, Session session) { + public Route(RouteInfo info, RouteOptions options, MediaSession session) { if (info == null || options == null) { throw new IllegalStateException("Route info was not valid!"); } @@ -93,7 +94,7 @@ public final class Route { /** * @hide */ - Session getSession() { + MediaSession getSession() { return mSession; } } diff --git a/media/java/android/media/session/RouteInfo.java b/media/java/android/media/session/RouteInfo.java index 17df969..02f78f9 100644 --- a/media/java/android/media/session/RouteInfo.java +++ b/media/java/android/media/session/RouteInfo.java @@ -25,6 +25,7 @@ import java.util.List; /** * Information about a route, including its display name, a way to identify it, * and the ways it can be connected to. + * @hide */ public final class RouteInfo implements Parcelable { private final String mName; diff --git a/media/java/android/media/session/RouteInterface.java b/media/java/android/media/session/RouteInterface.java index e9c9fd3..8de4d89 100644 --- a/media/java/android/media/session/RouteInterface.java +++ b/media/java/android/media/session/RouteInterface.java @@ -25,7 +25,7 @@ import android.util.Log; import java.util.ArrayList; /** - * A route can support multiple interfaces for a {@link Session} to + * A route can support multiple interfaces for a {@link MediaSession} to * interact with. To use a specific interface with a route a * MediaSessionRouteInterface needs to be retrieved from the route. An * implementation of the specific interface, like @@ -33,6 +33,7 @@ import java.util.ArrayList; * and reduce errors on that interface. * * @see RoutePlaybackControls for an example + * @hide */ public final class RouteInterface { private static final String TAG = "RouteInterface"; @@ -67,7 +68,7 @@ public final class RouteInterface { private final Route mRoute; private final String mIface; - private final Session mSession; + private final MediaSession mSession; private final Object mLock = new Object(); private final ArrayList mListeners = new ArrayList(); @@ -75,7 +76,7 @@ public final class RouteInterface { /** * @hide */ - RouteInterface(Route route, String iface, Session session) { + RouteInterface(Route route, String iface, MediaSession session) { mRoute = route; mIface = iface; mSession = session; diff --git a/media/java/android/media/session/RouteOptions.java b/media/java/android/media/session/RouteOptions.java index 5105867..b4fb341 100644 --- a/media/java/android/media/session/RouteOptions.java +++ b/media/java/android/media/session/RouteOptions.java @@ -34,6 +34,7 @@ import java.util.List; * appropriate route options when it is ready to connect to the route. Each * route options instance must specify a complete set of capabilities to request * when the connection is established. + * @hide */ public final class RouteOptions implements Parcelable { private static final String TAG = "RouteOptions"; diff --git a/media/java/android/media/session/RoutePlaybackControls.java b/media/java/android/media/session/RoutePlaybackControls.java index a3ffb58..8211983 100644 --- a/media/java/android/media/session/RoutePlaybackControls.java +++ b/media/java/android/media/session/RoutePlaybackControls.java @@ -15,6 +15,7 @@ */ package android.media.session; +import android.media.MediaMetadata; import android.os.Bundle; import android.os.Handler; import android.os.ResultReceiver; @@ -23,6 +24,7 @@ import android.os.ResultReceiver; * A standard media control interface for Routes that support queueing and * transport controls. Routes may support multiple interfaces for MediaSessions * to interact with. + * @hide */ public final class RoutePlaybackControls { private static final String TAG = "RoutePlaybackControls"; diff --git a/media/java/android/media/session/Session.java b/media/java/android/media/session/Session.java deleted file mode 100644 index e439772..0000000 --- a/media/java/android/media/session/Session.java +++ /dev/null @@ -1,765 +0,0 @@ -/* - * Copyright (C) 2014 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package android.media.session; - -import android.content.Intent; -import android.media.Rating; -import android.media.session.ISessionController; -import android.media.session.ISession; -import android.media.session.ISessionCallback; -import android.os.Bundle; -import android.os.Handler; -import android.os.Looper; -import android.os.Message; -import android.os.RemoteException; -import android.os.ResultReceiver; -import android.text.TextUtils; -import android.util.ArrayMap; -import android.util.Log; - -import java.lang.ref.WeakReference; -import java.util.ArrayList; -import java.util.List; - -/** - * Allows interaction with media controllers, media routes, volume keys, media - * buttons, and transport controls. - *

- * A MediaSession should be created when an app wants to publish media playback - * information or negotiate with a media route. In general an app only needs one - * session for all playback, though multiple sessions can be created for sending - * media to multiple routes or to provide finer grain controls of media. - *

- * A MediaSession is created by calling - * {@link SessionManager#createSession(String)}. Once a session is created apps - * that have the MEDIA_CONTENT_CONTROL permission can interact with the session - * through - * {@link SessionManager#getActiveSessions(android.content.ComponentName)}. The - * owner of the session may also use {@link #getSessionToken()} to allow apps - * without this permission to create a {@link SessionController} to interact - * with this session. - *

- * To receive commands, media keys, and other events a Callback must be set with - * {@link #addCallback(Callback)}. - *

- * When an app is finished performing playback it must call {@link #release()} - * to clean up the session and notify any controllers. - *

- * MediaSession objects are thread safe - */ -public final class Session { - private static final String TAG = "Session"; - - /** - * 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 commands through - * the {@link TransportPerformer}. The performer can be retrieved by calling - * {@link #getTransportPerformer()}. - */ - public static final int FLAG_HANDLES_TRANSPORT_CONTROLS = 1 << 1; - - /** - * System only flag for a session that needs to have priority over all other - * sessions. This flag ensures this session will receive media button events - * regardless of the current ordering in the system. - * - * @hide - */ - public static final int FLAG_EXCLUSIVE_GLOBAL_PRIORITY = 1 << 16; - - /** - * Indicates the session was disconnected because the user that the session - * belonged to is stopping. - */ - public static final int DISCONNECT_REASON_USER_STOPPING = 1; - - /** - * Indicates the session was disconnected because the provider disconnected - * the route. - */ - public static final int DISCONNECT_REASON_PROVIDER_DISCONNECTED = 2; - - /** - * Indicates the session was disconnected because the route has changed. - */ - public static final int DISCONNECT_REASON_ROUTE_CHANGED = 3; - - /** - * Indicates the session was disconnected because the session owner - * requested it disconnect. - */ - public static final int DISCONNECT_REASON_SESSION_DISCONNECTED = 4; - - /** - * Indicates the session was disconnected because it was destroyed. - */ - public static final int DISCONNECT_REASON_SESSION_DESTROYED = 5; - - /** - * Status code indicating the call was handled. - * - * @hide - */ - public static final int RESULT_SUCCESS = 0; - - private static final int MSG_MEDIA_BUTTON = 1; - private static final int MSG_COMMAND = 2; - private static final int MSG_ROUTE_CHANGE = 3; - private static final int MSG_ROUTE_CONNECTED = 4; - private static final int MSG_ROUTE_DISCONNECTED = 5; - - private static final String KEY_COMMAND = "command"; - private static final String KEY_EXTRAS = "extras"; - private static final String KEY_CALLBACK = "callback"; - - private final Object mLock = new Object(); - - private final SessionToken mSessionToken; - private final ISession mBinder; - private final CallbackStub mCbStub; - - private final ArrayList mCallbacks = new ArrayList(); - // TODO route interfaces - private final ArrayMap mInterfaceListeners - = new ArrayMap(); - - private TransportPerformer mPerformer; - private Route mRoute; - - private boolean mActive = false;; - - /** - * @hide - */ - public Session(ISession binder, CallbackStub cbStub) { - mBinder = binder; - mCbStub = cbStub; - ISessionController controllerBinder = null; - try { - controllerBinder = mBinder.getController(); - } catch (RemoteException e) { - throw new RuntimeException("Dead object in MediaSessionController constructor: ", e); - } - mSessionToken = new SessionToken(controllerBinder); - mPerformer = new TransportPerformer(mBinder); - } - - /** - * Set the callback to receive updates on. - * - * @param callback The callback object - */ - public void addCallback(Callback callback) { - addCallback(callback, null); - } - - /** - * Add a callback to receive updates for the MediaSession. This includes - * events like route updates, media buttons, and focus changes. - * - * @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 cannot be null"); - } - synchronized (mLock) { - if (getHandlerForCallbackLocked(callback) != null) { - Log.w(TAG, "Callback is already added, ignoring"); - return; - } - if (handler == null) { - handler = new Handler(); - } - MessageHandler msgHandler = new MessageHandler(handler.getLooper(), callback); - mCallbacks.add(msgHandler); - } - } - - /** - * Remove a callback. It will no longer receive updates. - * - * @param callback The callback to remove. - */ - public void removeCallback(Callback callback) { - synchronized (mLock) { - removeCallbackLocked(callback); - } - } - - /** - * Retrieves the {@link TransportPerformer} for this session. To receive - * commands through the performer you must also set the - * {@link #FLAG_HANDLES_TRANSPORT_CONTROLS} flag using - * {@link #setFlags(int)}. - * - * @return The performer associated with this session. - */ - public TransportPerformer getTransportPerformer() { - return mPerformer; - } - - /** - * Set any flags for the session. - * - * @param flags The flags to set for this session. - */ - public void setFlags(int flags) { - try { - mBinder.setFlags(flags); - } catch (RemoteException e) { - Log.wtf(TAG, "Failure in setFlags.", e); - } - } - - /** - * 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) { - if (mActive == active) { - return; - } - try { - mBinder.setActive(active); - mActive = active; - } catch (RemoteException e) { - Log.wtf(TAG, "Failure in setActive.", e); - } - } - - /** - * Get the current active state of this session. - * - * @return True if the session is active, false otherwise. - */ - public boolean isActive() { - return mActive; - } - - /** - * 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 sendEvent(String event, Bundle extras) { - if (TextUtils.isEmpty(event)) { - throw new IllegalArgumentException("event cannot be null or empty"); - } - try { - mBinder.sendEvent(event, extras); - } catch (RemoteException e) { - Log.wtf(TAG, "Error sending event", e); - } - } - - /** - * This must be called when an app has finished performing playback. If - * playback is expected to start again shortly the session can be left open, - * but it must be released if your activity or service is being destroyed. - */ - public void release() { - try { - mBinder.destroy(); - } catch (RemoteException e) { - Log.wtf(TAG, "Error releasing session: ", e); - } - } - - /** - * Retrieve a token object that can be used by apps to create a - * {@link SessionController} 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 SessionToken getSessionToken() { - return mSessionToken; - } - - /** - * Connect to the current route using the specified request. - *

- * Connection updates will be sent to the callback's - * {@link Callback#onRouteConnected(Route)} and - * {@link Callback#onRouteDisconnected(Route, int)} methods. If the - * connection fails {@link Callback#onRouteDisconnected(Route, int)} - * will be called. - *

- * If you already have a connection to this route it will be disconnected - * before the new connection is established. TODO add an easy way to compare - * MediaRouteOptions. - * - * @param route The route the app is trying to connect to. - * @param request The connection request to use. - */ - public void connect(RouteInfo route, RouteOptions request) { - if (route == null) { - throw new IllegalArgumentException("Must specify the route"); - } - if (request == null) { - throw new IllegalArgumentException("Must specify the connection request"); - } - try { - mBinder.connectToRoute(route, request); - } catch (RemoteException e) { - Log.wtf(TAG, "Error starting connection to route", e); - } - } - - /** - * Disconnect from the current route. After calling you will be switched - * back to the default route. - */ - public void disconnect() { - if (mRoute != null) { - try { - mBinder.disconnectFromRoute(mRoute.getRouteInfo()); - } catch (RemoteException e) { - Log.wtf(TAG, "Error disconnecting from route"); - } - } - } - - /** - * Set the list of route options your app is interested in connecting to. It - * will be used for picking valid routes. - * - * @param options The set of route options your app may use to connect. - */ - public void setRouteOptions(List options) { - try { - mBinder.setRouteOptions(options); - } catch (RemoteException e) { - Log.wtf(TAG, "Error setting route options.", e); - } - } - - /** - * @hide - * TODO allow multiple listeners for the same interface, allow removal - */ - public void addInterfaceListener(String iface, - RouteInterface.EventListener listener) { - mInterfaceListeners.put(iface, listener); - } - - /** - * @hide - */ - public boolean sendRouteCommand(RouteCommand command, ResultReceiver cb) { - try { - mBinder.sendRouteCommand(command, cb); - } catch (RemoteException e) { - Log.wtf(TAG, "Error sending command to route.", e); - return false; - } - return true; - } - - private MessageHandler getHandlerForCallbackLocked(Callback cb) { - if (cb == null) { - throw new IllegalArgumentException("Callback cannot be null"); - } - for (int i = mCallbacks.size() - 1; i >= 0; i--) { - MessageHandler handler = mCallbacks.get(i); - if (cb == handler.mCallback) { - return handler; - } - } - return null; - } - - private boolean removeCallbackLocked(Callback cb) { - if (cb == null) { - throw new IllegalArgumentException("Callback cannot be null"); - } - for (int i = mCallbacks.size() - 1; i >= 0; i--) { - MessageHandler handler = mCallbacks.get(i); - if (cb == handler.mCallback) { - mCallbacks.remove(i); - return true; - } - } - return false; - } - - private void postCommand(String command, Bundle extras, ResultReceiver resultCb) { - Command cmd = new Command(command, extras, resultCb); - synchronized (mLock) { - for (int i = mCallbacks.size() - 1; i >= 0; i--) { - mCallbacks.get(i).post(MSG_COMMAND, cmd); - } - } - } - - private void postMediaButton(Intent mediaButtonIntent) { - synchronized (mLock) { - for (int i = mCallbacks.size() - 1; i >= 0; i--) { - mCallbacks.get(i).post(MSG_MEDIA_BUTTON, mediaButtonIntent); - } - } - } - - private void postRequestRouteChange(RouteInfo route) { - synchronized (mLock) { - for (int i = mCallbacks.size() - 1; i >= 0; i--) { - mCallbacks.get(i).post(MSG_ROUTE_CHANGE, route); - } - } - } - - private void postRouteConnected(RouteInfo route, RouteOptions options) { - synchronized (mLock) { - mRoute = new Route(route, options, this); - for (int i = mCallbacks.size() - 1; i >= 0; i--) { - mCallbacks.get(i).post(MSG_ROUTE_CONNECTED, mRoute); - } - } - } - - private void postRouteDisconnected(RouteInfo route, int reason) { - synchronized (mLock) { - if (mRoute != null && TextUtils.equals(mRoute.getRouteInfo().getId(), route.getId())) { - for (int i = mCallbacks.size() - 1; i >= 0; i--) { - mCallbacks.get(i).post(MSG_ROUTE_DISCONNECTED, mRoute, reason); - } - } - } - } - - /** - * Receives commands or updates from controllers and routes. An app can - * specify what commands and buttons it supports by setting them on the - * MediaSession (TODO). - */ - public abstract static class Callback { - - public Callback() { - } - - /** - * Called when a media button is pressed and this session has the - * highest priority or a controller sends a media button event to the - * session. TODO determine if using Intents identical to the ones - * RemoteControlClient receives is useful - *

- * The intent will be of type {@link Intent#ACTION_MEDIA_BUTTON} with a - * KeyEvent in {@link Intent#EXTRA_KEY_EVENT} - * - * @param mediaButtonIntent an intent containing the KeyEvent as an - * extra - */ - public void onMediaButton(Intent mediaButtonIntent) { - } - - /** - * Called when a controller has sent a custom command to this session. - * The owner of the session may handle custom commands but is not - * required to. - * - * @param command - * @param extras optional - */ - public void onCommand(String command, Bundle extras, ResultReceiver cb) { - } - - /** - * Called when the user has selected a different route to connect to. - * The app is responsible for connecting to the new route and migrating - * ongoing playback if necessary. - * - * @param route - */ - public void onRequestRouteChange(RouteInfo route) { - } - - /** - * Called when a route has successfully connected. Calls to the route - * are now valid. - * - * @param route The route that was connected - */ - public void onRouteConnected(Route route) { - } - - /** - * Called when a route was disconnected. Further calls to the route will - * fail. If available a reason for being disconnected will be provided. - *

- * Valid reasons are: - *

    - *
  • {@link #DISCONNECT_REASON_USER_STOPPING}
  • - *
  • {@link #DISCONNECT_REASON_PROVIDER_DISCONNECTED}
  • - *
  • {@link #DISCONNECT_REASON_ROUTE_CHANGED}
  • - *
  • {@link #DISCONNECT_REASON_SESSION_DISCONNECTED}
  • - *
  • {@link #DISCONNECT_REASON_SESSION_DESTROYED}
  • - *
- * - * @param route The route that disconnected - * @param reason The reason for the disconnect - */ - public void onRouteDisconnected(Route route, int reason) { - } - } - - /** - * @hide - */ - public static class CallbackStub extends ISessionCallback.Stub { - private WeakReference mMediaSession; - - public void setMediaSession(Session session) { - mMediaSession = new WeakReference(session); - } - - @Override - public void onCommand(String command, Bundle extras, ResultReceiver cb) - throws RemoteException { - Session session = mMediaSession.get(); - if (session != null) { - session.postCommand(command, extras, cb); - } - } - - @Override - public void onMediaButton(Intent mediaButtonIntent, ResultReceiver cb) - throws RemoteException { - Session session = mMediaSession.get(); - if (session != null) { - session.postMediaButton(mediaButtonIntent); - } - if (cb != null) { - cb.send(RESULT_SUCCESS, null); - } - } - - @Override - public void onRequestRouteChange(RouteInfo route) throws RemoteException { - Session session = mMediaSession.get(); - if (session != null) { - session.postRequestRouteChange(route); - } - } - - @Override - public void onRouteConnected(RouteInfo route, RouteOptions options) { - Session session = mMediaSession.get(); - if (session != null) { - session.postRouteConnected(route, options); - } - } - - @Override - public void onRouteDisconnected(RouteInfo route, int reason) { - Session session = mMediaSession.get(); - if (session != null) { - session.postRouteDisconnected(route, reason); - } - } - - @Override - public void onPlay() throws RemoteException { - Session session = mMediaSession.get(); - if (session != null) { - TransportPerformer tp = session.getTransportPerformer(); - if (tp != null) { - tp.onPlay(); - } - } - } - - @Override - public void onPause() throws RemoteException { - Session session = mMediaSession.get(); - if (session != null) { - TransportPerformer tp = session.getTransportPerformer(); - if (tp != null) { - tp.onPause(); - } - } - } - - @Override - public void onStop() throws RemoteException { - Session session = mMediaSession.get(); - if (session != null) { - TransportPerformer tp = session.getTransportPerformer(); - if (tp != null) { - tp.onStop(); - } - } - } - - @Override - public void onNext() throws RemoteException { - Session session = mMediaSession.get(); - if (session != null) { - TransportPerformer tp = session.getTransportPerformer(); - if (tp != null) { - tp.onNext(); - } - } - } - - @Override - public void onPrevious() throws RemoteException { - Session session = mMediaSession.get(); - if (session != null) { - TransportPerformer tp = session.getTransportPerformer(); - if (tp != null) { - tp.onPrevious(); - } - } - } - - @Override - public void onFastForward() throws RemoteException { - Session session = mMediaSession.get(); - if (session != null) { - TransportPerformer tp = session.getTransportPerformer(); - if (tp != null) { - tp.onFastForward(); - } - } - } - - @Override - public void onRewind() throws RemoteException { - Session session = mMediaSession.get(); - if (session != null) { - TransportPerformer tp = session.getTransportPerformer(); - if (tp != null) { - tp.onRewind(); - } - } - } - - @Override - public void onSeekTo(long pos) throws RemoteException { - Session session = mMediaSession.get(); - if (session != null) { - TransportPerformer tp = session.getTransportPerformer(); - if (tp != null) { - tp.onSeekTo(pos); - } - } - } - - @Override - public void onRate(Rating rating) throws RemoteException { - Session session = mMediaSession.get(); - if (session != null) { - TransportPerformer tp = session.getTransportPerformer(); - if (tp != null) { - tp.onRate(rating); - } - } - } - - @Override - public void onRouteEvent(RouteEvent event) throws RemoteException { - Session session = mMediaSession.get(); - if (session != null) { - RouteInterface.EventListener iface - = session.mInterfaceListeners.get(event.getIface()); - Log.d(TAG, "Received route event on iface " + event.getIface() + ". Listener is " - + iface); - if (iface != null) { - iface.onEvent(event.getEvent(), event.getExtras()); - } - } - } - - @Override - public void onRouteStateChange(int state) throws RemoteException { - // TODO - - } - - } - - private class MessageHandler extends Handler { - private Session.Callback mCallback; - - public MessageHandler(Looper looper, Session.Callback callback) { - super(looper, null, true); - mCallback = callback; - } - - @Override - public void handleMessage(Message msg) { - synchronized (mLock) { - if (mCallback == null) { - return; - } - switch (msg.what) { - case MSG_MEDIA_BUTTON: - mCallback.onMediaButton((Intent) msg.obj); - break; - case MSG_COMMAND: - Command cmd = (Command) msg.obj; - mCallback.onCommand(cmd.command, cmd.extras, cmd.stub); - break; - case MSG_ROUTE_CHANGE: - mCallback.onRequestRouteChange((RouteInfo) msg.obj); - break; - case MSG_ROUTE_CONNECTED: - mCallback.onRouteConnected((Route) msg.obj); - break; - case MSG_ROUTE_DISCONNECTED: - mCallback.onRouteDisconnected((Route) msg.obj, msg.arg1); - break; - } - } - } - - public void post(int what, Object obj) { - obtainMessage(what, obj).sendToTarget(); - } - - public void post(int what, Object obj, int arg1) { - obtainMessage(what, arg1, 0, obj).sendToTarget(); - } - } - - private static final class Command { - public final String command; - public final Bundle extras; - public final ResultReceiver stub; - - public Command(String command, Bundle extras, ResultReceiver stub) { - this.command = command; - this.extras = extras; - this.stub = stub; - } - } -} diff --git a/media/java/android/media/session/SessionController.java b/media/java/android/media/session/SessionController.java deleted file mode 100644 index dc4f7d9..0000000 --- a/media/java/android/media/session/SessionController.java +++ /dev/null @@ -1,365 +0,0 @@ -/* - * Copyright (C) 2014 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package android.media.session; - -import android.os.Bundle; -import android.os.Handler; -import android.os.Looper; -import android.os.Message; -import android.os.RemoteException; -import android.os.ResultReceiver; -import android.text.TextUtils; -import android.util.Log; -import android.view.KeyEvent; - -import java.lang.ref.WeakReference; -import java.util.ArrayList; - -/** - * Allows an app to interact with an ongoing media session. Media buttons and - * other commands can be sent to the session. A callback may be registered to - * receive updates from the session, such as metadata and play state changes. - *

- * A MediaController can be created through {@link SessionManager} if you - * hold the "android.permission.MEDIA_CONTENT_CONTROL" permission or directly if - * you have a {@link SessionToken} from the session owner. - *

- * MediaController objects are thread-safe. - */ -public final class SessionController { - private static final String TAG = "SessionController"; - - private static final int MSG_EVENT = 1; - private static final int MESSAGE_PLAYBACK_STATE = 2; - private static final int MESSAGE_METADATA = 3; - private static final int MSG_ROUTE = 4; - - private final ISessionController mSessionBinder; - - private final CallbackStub mCbStub = new CallbackStub(this); - private final ArrayList mCallbacks = new ArrayList(); - private final Object mLock = new Object(); - - private boolean mCbRegistered = false; - - private TransportController mTransportController; - - private SessionController(ISessionController sessionBinder) { - mSessionBinder = sessionBinder; - } - - /** - * @hide - */ - public static SessionController fromBinder(ISessionController sessionBinder) { - SessionController controller = new SessionController(sessionBinder); - try { - controller.mSessionBinder.registerCallbackListener(controller.mCbStub); - if (controller.mSessionBinder.isTransportControlEnabled()) { - controller.mTransportController = new TransportController(sessionBinder); - } - } catch (RemoteException e) { - Log.wtf(TAG, "MediaController created with expired token", e); - controller = null; - } - return controller; - } - - /** - * Get a new MediaController for a MediaSessionToken. If successful the - * controller returned will be connected to the session that generated the - * token. - * - * @param token The session token to use - * @return A controller for the session or null - */ - public static SessionController fromToken(SessionToken token) { - return fromBinder(token.getBinder()); - } - - /** - * Get a TransportController if the session supports it. If it is not - * supported null will be returned. - * - * @return A TransportController or null - */ - public TransportController getTransportController() { - return mTransportController; - } - - /** - * Send the specified media button to the session. Only media keys can be - * sent using this method. - * - * @param keycode The media button keycode, such as - * {@link KeyEvent#KEYCODE_MEDIA_PLAY}. - */ - public void sendMediaButton(int keycode) { - if (!KeyEvent.isMediaKey(keycode)) { - throw new IllegalArgumentException("May only send media buttons through " - + "sendMediaButton"); - } - // TODO do something better than key down/up events - KeyEvent event = new KeyEvent(KeyEvent.ACTION_UP, keycode); - try { - mSessionBinder.sendMediaButton(event); - } catch (RemoteException e) { - Log.d(TAG, "Dead object in sendMediaButton", e); - } - } - - /** - * Adds a callback to receive updates from the Session. Updates will be - * posted on the caller's thread. - * - * @param cb The callback object, must not be null - */ - public void addCallback(Callback cb) { - addCallback(cb, null); - } - - /** - * Adds a callback to receive updates from the session. Updates will be - * posted on the specified handler's thread. - * - * @param cb Cannot be null. - * @param handler The handler to post updates on. If null the callers thread - * will be used - */ - public void addCallback(Callback cb, Handler handler) { - if (handler == null) { - handler = new Handler(); - } - synchronized (mLock) { - addCallbackLocked(cb, handler); - } - } - - /** - * Stop receiving updates on the specified callback. If an update has - * already been posted you may still receive it after calling this method. - * - * @param cb The callback to remove - */ - public void removeCallback(Callback cb) { - synchronized (mLock) { - removeCallbackLocked(cb); - } - } - - /** - * 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 sendCommand(String command, Bundle params, ResultReceiver cb) { - if (TextUtils.isEmpty(command)) { - throw new IllegalArgumentException("command cannot be null or empty"); - } - try { - mSessionBinder.sendCommand(command, params, cb); - } catch (RemoteException e) { - Log.d(TAG, "Dead object in sendCommand.", e); - } - } - - /** - * Request that the route picker be shown for this session. This should - * generally be called in response to a user action. - */ - public void showRoutePicker() { - try { - mSessionBinder.showRoutePicker(); - } catch (RemoteException e) { - Log.d(TAG, "Dead object in showRoutePicker", e); - } - } - - /* - * @hide - */ - ISessionController getSessionBinder() { - return mSessionBinder; - } - - private void addCallbackLocked(Callback cb, Handler handler) { - if (cb == null) { - throw new IllegalArgumentException("Callback cannot be null"); - } - if (handler == null) { - throw new IllegalArgumentException("Handler cannot be null"); - } - if (getHandlerForCallbackLocked(cb) != null) { - Log.w(TAG, "Callback is already added, ignoring"); - return; - } - MessageHandler holder = new MessageHandler(handler.getLooper(), cb); - mCallbacks.add(holder); - - if (!mCbRegistered) { - try { - mSessionBinder.registerCallbackListener(mCbStub); - mCbRegistered = true; - } catch (RemoteException e) { - Log.d(TAG, "Dead object in registerCallback", e); - } - } - } - - private boolean removeCallbackLocked(Callback cb) { - if (cb == null) { - throw new IllegalArgumentException("Callback cannot be null"); - } - for (int i = mCallbacks.size() - 1; i >= 0; i--) { - MessageHandler handler = mCallbacks.get(i); - if (cb == handler.mCallback) { - mCallbacks.remove(i); - return true; - } - } - return false; - } - - private MessageHandler getHandlerForCallbackLocked(Callback cb) { - if (cb == null) { - throw new IllegalArgumentException("Callback cannot be null"); - } - for (int i = mCallbacks.size() - 1; i >= 0; i--) { - MessageHandler handler = mCallbacks.get(i); - if (cb == handler.mCallback) { - return handler; - } - } - return null; - } - - private void postEvent(String event, Bundle extras) { - synchronized (mLock) { - for (int i = mCallbacks.size() - 1; i >= 0; i--) { - mCallbacks.get(i).post(MSG_EVENT, event, extras); - } - } - } - - private void postRouteChanged(RouteInfo route) { - synchronized (mLock) { - for (int i = mCallbacks.size() - 1; i >= 0; i--) { - mCallbacks.get(i).post(MSG_ROUTE, route, null); - } - } - } - - /** - * Callback for receiving updates on from the session. A Callback can be - * registered using {@link #addCallback} - */ - public static abstract class Callback { - /** - * 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 - */ - public void onEvent(String event, Bundle extras) { - } - - /** - * Override to handle route changes for this session. - * - * @param route - */ - public void onRouteChanged(RouteInfo route) { - } - } - - private final static class CallbackStub extends ISessionControllerCallback.Stub { - private final WeakReference mController; - - public CallbackStub(SessionController controller) { - mController = new WeakReference(controller); - } - - @Override - public void onEvent(String event, Bundle extras) { - SessionController controller = mController.get(); - if (controller != null) { - controller.postEvent(event, extras); - } - } - - @Override - public void onRouteChanged(RouteInfo route) { - SessionController controller = mController.get(); - if (controller != null) { - controller.postRouteChanged(route); - } - } - - @Override - public void onPlaybackStateChanged(PlaybackState state) { - SessionController controller = mController.get(); - if (controller != null) { - TransportController tc = controller.getTransportController(); - if (tc != null) { - tc.postPlaybackStateChanged(state); - } - } - } - - @Override - public void onMetadataChanged(MediaMetadata metadata) { - SessionController controller = mController.get(); - if (controller != null) { - TransportController tc = controller.getTransportController(); - if (tc != null) { - tc.postMetadataChanged(metadata); - } - } - } - - } - - private final static class MessageHandler extends Handler { - private final SessionController.Callback mCallback; - - public MessageHandler(Looper looper, SessionController.Callback cb) { - super(looper, null, true); - mCallback = cb; - } - - @Override - public void handleMessage(Message msg) { - switch (msg.what) { - case MSG_EVENT: - mCallback.onEvent((String) msg.obj, msg.getData()); - break; - case MSG_ROUTE: - mCallback.onRouteChanged((RouteInfo) msg.obj); - } - } - - public void post(int what, Object obj, Bundle data) { - obtainMessage(what, obj).sendToTarget(); - } - } - -} diff --git a/media/java/android/media/session/SessionInfo.java b/media/java/android/media/session/SessionInfo.java deleted file mode 100644 index 2b65528..0000000 --- a/media/java/android/media/session/SessionInfo.java +++ /dev/null @@ -1,87 +0,0 @@ -/* - * Copyright (C) 2014 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package android.media.session; - -import android.os.Parcel; -import android.os.Parcelable; - -/** - * Information about a media session, including the owner's package name. - */ -public final class SessionInfo implements Parcelable { - private final String mId; - private final String mPackageName; - - /** - * @hide - */ - public SessionInfo(String id, String packageName) { - mId = id; - mPackageName = packageName; - } - - private SessionInfo(Parcel in) { - mId = in.readString(); - mPackageName = in.readString(); - } - - /** - * Get the package name of the owner of this session. - * - * @return The owner's package name - */ - public String getPackageName() { - return mPackageName; - } - - /** - * Get the unique id for this session. - * - * @return The id for the session. - */ - public String getId() { - return mId; - } - - @Override - public String toString() { - return "SessionInfo {id=" + mId + ", pkg=" + mPackageName + "}"; - } - - @Override - public int describeContents() { - return 0; - } - - @Override - public void writeToParcel(Parcel dest, int flags) { - dest.writeString(mId); - dest.writeString(mPackageName); - } - - public static final Parcelable.Creator CREATOR - = new Parcelable.Creator() { - @Override - public SessionInfo createFromParcel(Parcel in) { - return new SessionInfo(in); - } - - @Override - public SessionInfo[] newArray(int size) { - return new SessionInfo[size]; - } - }; -} diff --git a/media/java/android/media/session/SessionManager.java b/media/java/android/media/session/SessionManager.java deleted file mode 100644 index 1838132..0000000 --- a/media/java/android/media/session/SessionManager.java +++ /dev/null @@ -1,168 +0,0 @@ -/* - * Copyright (C) 2014 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package android.media.session; - -import android.content.ComponentName; -import android.content.Context; -import android.media.session.ISessionManager; -import android.os.IBinder; -import android.os.RemoteException; -import android.os.ServiceManager; -import android.os.UserHandle; -import android.service.notification.NotificationListenerService; -import android.util.Log; -import android.view.KeyEvent; - -import java.util.ArrayList; -import java.util.List; - -/** - * MediaSessionManager allows the creation and control of MediaSessions in the - * system. A MediaSession enables publishing information about ongoing media and - * interacting with MediaControllers and MediaRoutes. - *

- * Use Context.getSystemService(Context.MEDIA_SESSION_SERVICE) to - * get an instance of this class. - *

- * - * @see Session - * @see SessionController - */ -public final class SessionManager { - private static final String TAG = "SessionManager"; - - private final ISessionManager mService; - - private Context mContext; - - /** - * @hide - */ - public SessionManager(Context context) { - // Consider rewriting like DisplayManagerGlobal - // Decide if we need context - mContext = context; - IBinder b = ServiceManager.getService(Context.MEDIA_SESSION_SERVICE); - mService = ISessionManager.Stub.asInterface(b); - } - - /** - * Creates a new session. - * - * @param tag A short name for debugging purposes - * @return a {@link Session} for the new session - */ - public Session createSession(String tag) { - return createSessionAsUser(tag, UserHandle.myUserId()); - } - - /** - * Creates a new session as the specified user. To create a session as a - * user other than your own you must hold the - * {@link android.Manifest.permission#INTERACT_ACROSS_USERS_FULL} - * permission. - * - * @param tag A short name for debugging purposes - * @param userId The user id to create the session as. - * @return a {@link Session} for the new session - * @hide - */ - public Session createSessionAsUser(String tag, int userId) { - try { - Session.CallbackStub cbStub = new Session.CallbackStub(); - Session session = new Session(mService - .createSession(mContext.getPackageName(), cbStub, tag, userId), cbStub); - cbStub.setMediaSession(session); - - return session; - } catch (RemoteException e) { - Log.e(TAG, "Failed to create session: ", e); - return null; - } - } - - /** - * Get a list of controllers for all ongoing sessions. This requires the - * android.Manifest.permission.MEDIA_CONTENT_CONTROL permission be held by - * the calling app. You may also retrieve this list if your app is an - * enabled notification listener using the - * {@link NotificationListenerService} APIs, in which case you must pass the - * {@link ComponentName} of your enabled listener. - * - * @param notificationListener The enabled notification listener component. - * May be null. - * @return A list of controllers for ongoing sessions - */ - public List getActiveSessions(ComponentName notificationListener) { - return getActiveSessionsForUser(notificationListener, UserHandle.myUserId()); - } - - /** - * Get active sessions for a specific user. To retrieve actions for a user - * other than your own you must hold the - * {@link android.Manifest.permission#INTERACT_ACROSS_USERS_FULL} permission - * in addition to any other requirements. If you are an enabled notification - * listener you may only get sessions for the users you are enabled for. - * - * @param notificationListener The enabled notification listener component. - * May be null. - * @param userId The user id to fetch sessions for. - * @return A list of controllers for ongoing sessions. - * @hide - */ - public List getActiveSessionsForUser(ComponentName notificationListener, - int userId) { - ArrayList controllers = new ArrayList(); - try { - List binders = mService.getSessions(notificationListener, userId); - for (int i = binders.size() - 1; i >= 0; i--) { - SessionController controller = SessionController.fromBinder(ISessionController.Stub - .asInterface(binders.get(i))); - controllers.add(controller); - } - } catch (RemoteException e) { - Log.e(TAG, "Failed to get active sessions: ", e); - } - return controllers; - } - - /** - * Send a media key event. The receiver will be selected automatically. - * - * @param keyEvent The KeyEvent to send. - * @hide - */ - public void dispatchMediaKeyEvent(KeyEvent keyEvent) { - dispatchMediaKeyEvent(keyEvent, false); - } - - /** - * Send a media key event. The receiver will be selected automatically. - * - * @param keyEvent The KeyEvent to send - * @param needWakeLock true if a wake lock should be held while sending the - * key - * @hide - */ - public void dispatchMediaKeyEvent(KeyEvent keyEvent, boolean needWakeLock) { - try { - mService.dispatchMediaKeyEvent(keyEvent, needWakeLock); - } catch (RemoteException e) { - Log.e(TAG, "Failed to send key event.", e); - } - } -} diff --git a/media/java/android/media/session/SessionToken.aidl b/media/java/android/media/session/SessionToken.aidl deleted file mode 100644 index db35f85..0000000 --- a/media/java/android/media/session/SessionToken.aidl +++ /dev/null @@ -1,18 +0,0 @@ -/* Copyright 2014, The Android Open Source Project -** -** Licensed under the Apache License, Version 2.0 (the "License"); -** you may not use this file except in compliance with the License. -** You may obtain a copy of the License at -** -** http://www.apache.org/licenses/LICENSE-2.0 -** -** Unless required by applicable law or agreed to in writing, software -** distributed under the License is distributed on an "AS IS" BASIS, -** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -** See the License for the specific language governing permissions and -** limitations under the License. -*/ - -package android.media.session; - -parcelable SessionToken; diff --git a/media/java/android/media/session/SessionToken.java b/media/java/android/media/session/SessionToken.java deleted file mode 100644 index 59486f6..0000000 --- a/media/java/android/media/session/SessionToken.java +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Copyright (C) 2014 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package android.media.session; - -import android.media.session.ISessionController; -import android.os.Parcel; -import android.os.Parcelable; - -public class SessionToken implements Parcelable { - private ISessionController mBinder; - - /** - * @hide - */ - SessionToken(ISessionController binder) { - mBinder = binder; - } - - private SessionToken(Parcel in) { - mBinder = ISessionController.Stub.asInterface(in.readStrongBinder()); - } - - /** - * @hide - */ - ISessionController getBinder() { - return mBinder; - } - - @Override - public int describeContents() { - return 0; - } - - @Override - public void writeToParcel(Parcel dest, int flags) { - dest.writeStrongBinder(mBinder.asBinder()); - } - - public static final Parcelable.Creator CREATOR - = new Parcelable.Creator() { - @Override - public SessionToken createFromParcel(Parcel in) { - return new SessionToken(in); - } - - @Override - public SessionToken[] newArray(int size) { - return new SessionToken[size]; - } - }; -} diff --git a/media/java/android/media/session/TransportController.java b/media/java/android/media/session/TransportController.java index 9574df6..090489b 100644 --- a/media/java/android/media/session/TransportController.java +++ b/media/java/android/media/session/TransportController.java @@ -15,6 +15,7 @@ */ package android.media.session; +import android.media.MediaMetadata; import android.media.Rating; import android.os.Handler; import android.os.Looper; diff --git a/media/java/android/media/session/TransportPerformer.java b/media/java/android/media/session/TransportPerformer.java index 187f48d..1588d8f 100644 --- a/media/java/android/media/session/TransportPerformer.java +++ b/media/java/android/media/session/TransportPerformer.java @@ -16,6 +16,7 @@ package android.media.session; import android.media.AudioManager; +import android.media.MediaMetadata; import android.media.Rating; import android.os.Handler; import android.os.Looper; -- cgit v1.1