diff --git a/media/java/android/media/IMediaSession2.aidl b/media/java/android/media/IMediaSession2.aidl index bc8121ef80433..078b611bae4cf 100644 --- a/media/java/android/media/IMediaSession2.aidl +++ b/media/java/android/media/IMediaSession2.aidl @@ -46,6 +46,11 @@ interface IMediaSession2 { PlaybackState getPlaybackState(); + ////////////////////////////////////////////////////////////////////////////////////////////// + // Get library service specific + ////////////////////////////////////////////////////////////////////////////////////////////// + oneway void getBrowserRoot(IMediaSession2Callback callback, in Bundle rootHints); + ////////////////////////////////////////////////////////////////////////////////////////////// // Callbacks -- remove them ////////////////////////////////////////////////////////////////////////////////////////////// diff --git a/media/java/android/media/IMediaSession2Callback.aidl b/media/java/android/media/IMediaSession2Callback.aidl index 0058e79eab157..a63b6bf4fcab4 100644 --- a/media/java/android/media/IMediaSession2Callback.aidl +++ b/media/java/android/media/IMediaSession2Callback.aidl @@ -44,4 +44,9 @@ oneway interface IMediaSession2Callback { // Follow-up TODO: Add similar functions to the session. // TODO(jaewan): Is term 'accepted/rejected' correct? For permission, 'grant' is used. void onConnectionChanged(IMediaSession2 sessionBinder, in Bundle commandGroup); + + ////////////////////////////////////////////////////////////////////////////////////////////// + // Browser sepcific + ////////////////////////////////////////////////////////////////////////////////////////////// + void onGetRootResult(in Bundle rootHints, String rootMediaId, in Bundle rootExtra); } diff --git a/media/java/android/media/MediaBrowser2.java b/media/java/android/media/MediaBrowser2.java new file mode 100644 index 0000000000000..fa00902077491 --- /dev/null +++ b/media/java/android/media/MediaBrowser2.java @@ -0,0 +1,69 @@ +/* + * Copyright 2018 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.annotation.Nullable; +import android.content.Context; +import android.media.update.ApiLoader; +import android.media.update.MediaBrowser2Provider; +import android.os.Bundle; + +import java.util.concurrent.Executor; + +/** + * Browses media content offered by a {@link MediaLibraryService2}. + * @hide + */ +public class MediaBrowser2 extends MediaController2 { + // Equals to the ((MediaBrowser2Provider) getProvider()) + private final MediaBrowser2Provider mProvider; + + /** + * Callback to listen events from {@link MediaLibraryService2}. + */ + public abstract static class BrowserCallback extends MediaController2.ControllerCallback { + /** + * Called with the result of {@link #getBrowserRoot(Bundle)}. + *
+ * {@code rootMediaId} and {@code rootExtra} can be {@code null} if the browser root isn't + * available. + * + * @param rootHints rootHints that you previously requested. + * @param rootMediaId media id of the browser root. Can be {@code null} + * @param rootExtra extra of the browser root. Can be {@code null} + */ + public abstract void onGetRootResult(Bundle rootHints, @Nullable String rootMediaId, + @Nullable Bundle rootExtra); + } + + public MediaBrowser2(Context context, SessionToken token, BrowserCallback callback, + Executor executor) { + super(context, token, callback, executor); + mProvider = (MediaBrowser2Provider) getProvider(); + } + + @Override + MediaBrowser2Provider createProvider(Context context, SessionToken token, + ControllerCallback callback, Executor executor) { + return ApiLoader.getProvider(context) + .createMediaBrowser2(this, context, token, (BrowserCallback) callback, executor); + } + + public void getBrowserRoot(Bundle rootHints) { + mProvider.getBrowserRoot_impl(rootHints); + } +} diff --git a/media/java/android/media/MediaController2.java b/media/java/android/media/MediaController2.java index 550eee2fd47c0..911245043e0b7 100644 --- a/media/java/android/media/MediaController2.java +++ b/media/java/android/media/MediaController2.java @@ -104,7 +104,13 @@ public class MediaController2 extends MediaPlayerBase { // session whose session binder is only valid while it's active. // prevent a controller from reusable after the // session is released and recreated. - mProvider = ApiLoader.getProvider(context) + mProvider = createProvider(context, token, callback, executor); + } + + MediaController2Provider createProvider(@NonNull Context context, + @NonNull SessionToken token, @NonNull ControllerCallback callback, + @NonNull Executor executor) { + return ApiLoader.getProvider(context) .createMediaController2(this, context, token, callback, executor); } diff --git a/media/java/android/media/MediaLibraryService2.java b/media/java/android/media/MediaLibraryService2.java new file mode 100644 index 0000000000000..bbc940799926e --- /dev/null +++ b/media/java/android/media/MediaLibraryService2.java @@ -0,0 +1,144 @@ +/* + * Copyright 2018 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.annotation.NonNull; +import android.annotation.Nullable; +import android.content.Context; +import android.media.MediaSession2.BuilderBase; +import android.media.MediaSession2.ControllerInfo; +import android.media.update.ApiLoader; +import android.media.update.MediaSessionService2Provider; +import android.os.Bundle; +import android.service.media.MediaBrowserService.BrowserRoot; + +/** + * Base class for media library services. + *
+ * Media library services enable applications to browse media content provided by an application + * and ask the application to start playing it. They may also be used to control content that + * is already playing by way of a {@link MediaSession2}. + *
+ * To extend this class, adding followings directly to your {@code AndroidManifest.xml}. + *
+ * <service android:name="component_name_of_your_implementation" > + * <intent-filter> + * <action android:name="android.media.MediaLibraryService2" /> + * </intent-filter> + * </service>+ *
+ * A {@link MediaLibraryService2} is extension of {@link MediaSessionService2}. IDs shouldn't + * be shared between the {@link MediaSessionService2} and {@link MediaSession2}. By + * default, an empty string will be used for ID of the service. If you want to specify an ID, + * declare metadata in the manifest as follows. + * @hide + */ +// TODO(jaewan): Unhide +public abstract class MediaLibraryService2 extends MediaSessionService2 { + /** + * This is the interface name that a service implementing a session service should say that it + * support -- that is, this is the action it uses for its intent filter. + */ + public static final String SERVICE_INTERFACE = "android.media.MediaLibraryService2"; + + /** + * Session for the media library service. + */ + public class MediaLibrarySession extends MediaSession2 { + MediaLibrarySession(Context context, MediaPlayerBase player, String id, + SessionCallback callback) { + super(context, player, id, callback); + } + // TODO(jaewan): Place public methods here. + } + + public static abstract class MediaLibrarySessionCallback extends MediaSession2.SessionCallback { + /** + * Called to get the root information for browsing by a particular client. + *
+ * The implementation should verify that the client package has permission
+ * to access browse media information before returning the root id; it
+ * should return null if the client is not allowed to access this
+ * information.
+ *
+ * @param controllerInfo information of the controller requesting access to browse media.
+ * @param rootHints An optional bundle of service-specific arguments to send
+ * to the media browser service when connecting and retrieving the
+ * root id for browsing, or null if none. The contents of this
+ * bundle may affect the information returned when browsing.
+ * @return The {@link BrowserRoot} for accessing this app's content or null.
+ * @see BrowserRoot#EXTRA_RECENT
+ * @see BrowserRoot#EXTRA_OFFLINE
+ * @see BrowserRoot#EXTRA_SUGGESTED
+ */
+ public abstract @Nullable BrowserRoot onGetRoot(
+ @NonNull ControllerInfo controllerInfo, @Nullable Bundle rootHints);
+ }
+
+ /**
+ * Builder for {@link MediaLibrarySession}.
+ */
+ // TODO(jaewan): Move this to updatable.
+ public class MediaLibrarySessionBuilder
+ extends BuilderBase
+ * Library service will accept or reject the connection with the
+ * {@link MediaLibrarySessionCallback} in the created session.
+ *
+ * Service wouldn't run if {@code null} is returned or session's ID doesn't match with the
+ * expected ID that you've specified through the AndroidManifest.xml.
+ *
+ * This method will be called on the main thread.
+ *
+ * @param sessionId session id written in the AndroidManifest.xml.
+ * @return a new browser session
+ * @see MediaLibrarySessionBuilder
+ * @see #getSession()
+ * @throws RuntimeException if returned session is invalid
+ */
+ @Override
+ public @NonNull abstract MediaLibrarySession onCreateSession(String sessionId);
+}
diff --git a/media/java/android/media/MediaSession2.java b/media/java/android/media/MediaSession2.java
index 807535bed967a..33a5807d13137 100644
--- a/media/java/android/media/MediaSession2.java
+++ b/media/java/android/media/MediaSession2.java
@@ -16,7 +16,6 @@
package android.media;
-import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.Context;
@@ -29,12 +28,9 @@ import android.media.update.MediaSession2Provider.ControllerInfoProvider;
import android.os.Bundle;
import android.os.Handler;
import android.os.Parcelable;
-import android.os.Process;
import android.text.TextUtils;
import android.util.ArraySet;
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.List;
@@ -55,20 +51,17 @@ import java.util.List;
* instead. With it, your playback can be revived even after you've finished playback. See
* {@link MediaSessionService2} for details.
*
- * A session can be obtained by {@link #getInstance(Context, Handler)}. The owner of the session may
- * pass its session token to other processes to allow them to create a {@link MediaController2}
- * to interact with the session.
+ * A session can be obtained by {@link Builder}. The owner of the session may pass its session token
+ * to other processes to allow them to create a {@link MediaController2} to interact with the
+ * session.
*
- * To receive transport control commands, an underlying media player must be set with
- * {@link #setPlayer(MediaPlayerBase)}. Commands will be sent to the underlying player directly
- * on the thread that had been specified by {@link #getInstance(Context, Handler)}.
+ * When a session receive transport control commands, the session sends the commands directly to
+ * the the underlying media player set by {@link Builder} or {@link #setPlayer(MediaPlayerBase)}.
*
- * When an app is finished performing playback it must call
- * {@link #setPlayer(MediaPlayerBase)} with {@code null} to clean up the session and notify any
- * controllers. It's developers responsibility of cleaning the session and releasing resources.
+ * When an app is finished performing playback it must call {@link #close()} to clean up the session
+ * and notify any controllers.
*
- * MediaSession2 objects should be used on the handler's thread that is initially given by
- * {@link #getInstance(Context, Handler)}.
+ * {@link MediaSession2} objects should be used on the thread on the looper.
*
* @see MediaSessionService2
* @hide
@@ -81,7 +74,7 @@ import java.util.List;
// TODO(jaewan): Should we make APIs for MediaSessionService2 public? It's helpful for
// developers that doesn't want to override from Browser, but user may not use this
// correctly.
-public final class MediaSession2 extends MediaPlayerBase {
+public class MediaSession2 extends MediaPlayerBase {
private final MediaSession2Provider mProvider;
// Note: Do not define IntDef because subclass can add more command code on top of these.
@@ -321,19 +314,16 @@ public final class MediaSession2 extends MediaPlayerBase {
};
/**
- * Builder for {@link MediaSession2}.
- *
- * Any incoming event from the {@link MediaController2} will be handled on the thread
- * that created session with the {@link Builder#build()}.
+ * Base builder class for MediaSession2 and its subclass.
+ *
+ * @hide
*/
- // TODO(jaewan): Move this to updatable
- // TODO(jaewan): Add setRatingType()
- // TODO(jaewan): Add setSessionActivity()
- public final static class Builder {
- private final Context mContext;
- private final MediaPlayerBase mPlayer;
- private String mId;
- private SessionCallback mCallback;
+ static abstract class BuilderBase
+
+ * Any incoming event from the {@link MediaController2} will be handled on the thread
+ * that created session with the {@link Builder#build()}.
+ */
+ // TODO(jaewan): Move this to updatable
+ // TODO(jaewan): Add setRatingType()
+ // TODO(jaewan): Add setSessionActivity()
+ public static final class Builder extends BuilderBase
* It's highly recommended for an app to use this instead of {@link MediaSession2} if it wants
* to keep media playback in the background.
@@ -43,11 +43,11 @@ import android.os.IBinder;
*
* For example, user's voice command can start playback of your app even when it's not running.
*
- * To use this class, adding followings directly to your {@code AndroidManifest.xml}.
+ * To extend this class, adding followings directly to your {@code AndroidManifest.xml}.
*
@@ -58,7 +58,7 @@ import android.os.IBinder;
*
- * This method will be call on the main thread.
+ * This method will be called on the main thread.
*
* @param sessionId session id written in the AndroidManifest.xml.
* @return a new session
* @see MediaSession2.Builder
* @see #getSession()
*/
- // TODO(jaewan): Replace this with onCreateSession(). Its sesssion callback will replace
- // this abstract method.
- // TODO(jaewan): Should we also include/documents notification listener access?
- // TODO(jaewan): Is term accepted/rejected correct? For permission, granted is more common.
- // TODO(jaewan): Return ConnectResult() that encapsulate supported action and player.
public @NonNull abstract MediaSession2 onCreateSession(String sessionId);
/**
* Called when the playback state of this session is changed, and notification needs update.
- *
- * Default media style notification will be shown if you don't override this or return
- * {@code null}. Override this method to show your own notification UI.
+ * Override this method to show your own notification UI.
*
* With the notification returned here, the service become foreground service when the playback
* is started. It becomes background service after the playback is stopped.
*
* @param state playback state
- * @return a {@link MediaNotification}. If it's {@code null}, default notification will be shown
- * instead.
+ * @return a {@link MediaNotification}. If it's {@code null}, notification wouldn't be shown.
*/
// TODO(jaewan): Also add metadata
public MediaNotification onUpdateNotification(PlaybackState state) {
diff --git a/media/java/android/media/SessionToken.java b/media/java/android/media/SessionToken.java
index b470dea4fee38..53fc24afea2ce 100644
--- a/media/java/android/media/SessionToken.java
+++ b/media/java/android/media/SessionToken.java
@@ -42,12 +42,13 @@ import java.lang.annotation.RetentionPolicy;
// TODO(jaewan): Find better name for this (SessionToken or Session2Token)
public final class SessionToken {
@Retention(RetentionPolicy.SOURCE)
- @IntDef(value = {TYPE_SESSION, TYPE_SESSION_SERVICE})
+ @IntDef(value = {TYPE_SESSION, TYPE_SESSION_SERVICE, TYPE_LIBRARY_SERVICE})
public @interface TokenType {
}
public static final int TYPE_SESSION = 0;
public static final int TYPE_SESSION_SERVICE = 1;
+ public static final int TYPE_LIBRARY_SERVICE = 2;
private static final String KEY_TYPE = "android.media.token.type";
private static final String KEY_PACKAGE_NAME = "android.media.token.package_name";
@@ -73,6 +74,7 @@ public final class SessionToken {
* @hide
*/
// TODO(jaewan): UID is also needed.
+ // TODO(jaewan): Unhide
public SessionToken(@TokenType int type, @NonNull String packageName, @NonNull String id,
@Nullable String serviceName, @Nullable IMediaSession2 sessionBinder) {
// TODO(jaewan): Add sanity check.
diff --git a/media/java/android/media/update/MediaBrowser2Provider.java b/media/java/android/media/update/MediaBrowser2Provider.java
new file mode 100644
index 0000000000000..355dbc9f4aefc
--- /dev/null
+++ b/media/java/android/media/update/MediaBrowser2Provider.java
@@ -0,0 +1,26 @@
+/*
+ * Copyright 2018 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.update;
+
+import android.os.Bundle;
+
+/**
+ * @hide
+ */
+public interface MediaBrowser2Provider extends MediaController2Provider {
+ void getBrowserRoot_impl(Bundle rootHints);
+}
diff --git a/media/java/android/media/update/MediaLibraryService2Provider.java b/media/java/android/media/update/MediaLibraryService2Provider.java
new file mode 100644
index 0000000000000..7e3444f4d9efd
--- /dev/null
+++ b/media/java/android/media/update/MediaLibraryService2Provider.java
@@ -0,0 +1,24 @@
+/*
+ * Copyright 2018 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.update;
+
+/**
+ * @hide
+ */
+public interface MediaLibraryService2Provider extends MediaSessionService2Provider {
+ // Nothing new for now
+}
diff --git a/media/java/android/media/update/StaticProvider.java b/media/java/android/media/update/StaticProvider.java
index 91c9c660876ba..fef2cbb432817 100644
--- a/media/java/android/media/update/StaticProvider.java
+++ b/media/java/android/media/update/StaticProvider.java
@@ -19,7 +19,11 @@ package android.media.update;
import android.annotation.Nullable;
import android.content.Context;
import android.media.IMediaSession2Callback;
+import android.media.MediaBrowser2;
+import android.media.MediaBrowser2.BrowserCallback;
import android.media.MediaController2;
+import android.media.MediaController2.ControllerCallback;
+import android.media.MediaLibraryService2;
import android.media.MediaPlayerBase;
import android.media.MediaSession2;
import android.media.MediaSessionService2;
@@ -53,7 +57,12 @@ public interface StaticProvider {
String packageName, IMediaSession2Callback callback);
MediaController2Provider createMediaController2(
MediaController2 instance, Context context, SessionToken token,
- MediaController2.ControllerCallback callback, Executor executor);
+ ControllerCallback callback, Executor executor);
+ MediaBrowser2Provider createMediaBrowser2(
+ MediaBrowser2 instance, Context context, SessionToken token,
+ BrowserCallback callback, Executor executor);
MediaSessionService2Provider createMediaSessionService2(
MediaSessionService2 instance);
+ MediaSessionService2Provider createMediaLibraryService2(
+ MediaLibraryService2 instance);
}
diff --git a/services/core/java/com/android/server/media/MediaSessionService.java b/services/core/java/com/android/server/media/MediaSessionService.java
index 6812778ee0953..c9c7d04e6e141 100644
--- a/services/core/java/com/android/server/media/MediaSessionService.java
+++ b/services/core/java/com/android/server/media/MediaSessionService.java
@@ -38,6 +38,7 @@ import android.media.AudioSystem;
import android.media.IAudioService;
import android.media.IMediaSession2;
import android.media.IRemoteVolumeController;
+import android.media.MediaLibraryService2;
import android.media.MediaSessionService2;
import android.media.SessionToken;
import android.media.session.IActiveSessionsListener;
@@ -445,9 +446,18 @@ public class MediaSessionService extends SystemService implements Monitor {
}
// TODO(jaewan): Query per users.
- List
* <service android:name="component_name_of_your_implementation" >
* <intent-filter>
- * <action android:name="android.media.session.MediaSessionService2" />
+ * <action android:name="android.media.MediaSessionService2" />
* </intent-filter>
* </service>
*
* <service android:name="component_name_of_your_implementation" >
* <intent-filter>
- * <action android:name="android.media.session.MediaSessionService2" />
+ * <action android:name="android.media.MediaSessionService2" />
* </intent-filter>
* <meta-data android:name="android.media.session"
* android:value="session_id"/>
@@ -120,8 +120,7 @@ public abstract class MediaSessionService2 extends Service {
* This is the interface name that a service implementing a session service should say that it
* support -- that is, this is the action it uses for its intent filter.
*/
- public static final String SERVICE_INTERFACE =
- "android.media.session.MediaSessionService2";
+ public static final String SERVICE_INTERFACE = "android.media.MediaSessionService2";
/**
* Name under which a MediaSessionService2 component publishes information about itself.
@@ -129,21 +128,13 @@ public abstract class MediaSessionService2 extends Service {
*/
public static final String SERVICE_META_DATA = "android.media.session";
- /**
- * Default notification channel ID used by {@link #onUpdateNotification(PlaybackState)}.
- *
- */
- public static final String DEFAULT_MEDIA_NOTIFICATION_CHANNEL_ID = "media_session_service";
-
- /**
- * Default notification channel ID used by {@link #onUpdateNotification(PlaybackState)}.
- *
- */
- public static final int DEFAULT_MEDIA_NOTIFICATION_ID = 1001;
-
public MediaSessionService2() {
super();
- mProvider = ApiLoader.getProvider(this).createMediaSessionService2(this);
+ mProvider = createProvider();
+ }
+
+ MediaSessionService2Provider createProvider() {
+ return ApiLoader.getProvider(this).createMediaSessionService2(this);
}
/**
@@ -168,32 +159,24 @@ public abstract class MediaSessionService2 extends Service {
* Service wouldn't run if {@code null} is returned or session's ID doesn't match with the
* expected ID that you've specified through the AndroidManifest.xml.
*