MediaSession2: Move MediaSession2/MediaController2 from experimental
APIs will be unhidden later
Test: Run MediaComponentsTest
Change-Id: I2d9fcd98232016281fad128e9e674885b41e20d9
diff --git a/Android.bp b/Android.bp
index 19c0580..65ececb 100644
--- a/Android.bp
+++ b/Android.bp
@@ -416,6 +416,8 @@
"media/java/android/media/IMediaRouterService.aidl",
"media/java/android/media/IMediaScannerListener.aidl",
"media/java/android/media/IMediaScannerService.aidl",
+ "media/java/android/media/IMediaSession2.aidl",
+ "media/java/android/media/IMediaSession2Callback.aidl",
"media/java/android/media/IPlaybackConfigDispatcher.aidl",
":libaudioclient_aidl",
"media/java/android/media/IRecordingConfigDispatcher.aidl",
diff --git a/media/java/android/media/IMediaSession2.aidl b/media/java/android/media/IMediaSession2.aidl
new file mode 100644
index 0000000..11cf302
--- /dev/null
+++ b/media/java/android/media/IMediaSession2.aidl
@@ -0,0 +1,68 @@
+/*
+ * 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.media.session.PlaybackState;
+import android.media.IMediaSession2Callback;
+
+/**
+ * Interface to MediaSession2. Framework MUST only call oneway APIs.
+ *
+ * @hide
+ */
+// TODO(jaewan): Make this oneway interface.
+// Malicious app can fake session binder and holds commands from controller.
+interface IMediaSession2 {
+ // TODO(jaewan): add onCommand() to send private command
+ // TODO(jaewan): Due to the nature of oneway calls, APIs can be called in out of order
+ // Add id for individual calls to address this.
+
+ // TODO(jaewan): We may consider to add another binder just for the connection
+ // not to expose other methods to the controller whose connection wasn't accepted.
+ // But this would be enough for now because it's the same as existing
+ // MediaBrowser and MediaBrowserService.
+ oneway void connect(String callingPackage, IMediaSession2Callback callback);
+ oneway void release(IMediaSession2Callback caller);
+
+ //////////////////////////////////////////////////////////////////////////////////////////////
+ // Playback controls.
+ //////////////////////////////////////////////////////////////////////////////////////////////
+ oneway void play(IMediaSession2Callback caller);
+ oneway void pause(IMediaSession2Callback caller);
+ oneway void stop(IMediaSession2Callback caller);
+ oneway void skipToPrevious(IMediaSession2Callback caller);
+ oneway void skipToNext(IMediaSession2Callback caller);
+
+ PlaybackState getPlaybackState();
+
+ //////////////////////////////////////////////////////////////////////////////////////////////
+ // Callbacks -- remove them
+ //////////////////////////////////////////////////////////////////////////////////////////////
+ /**
+ * @param callbackBinder binder to be used to notify changes.
+ * @param callbackFlag one of {@link MediaController2#FLAG_CALLBACK_PLAYBACK} or
+ * {@link MediaController2#FLAG_CALLBACK_SESSION_ACTIVENESS}
+ * @param requestCode If >= 0, this code will be called back by the callback after the callback
+ * is registered.
+ */
+ // TODO(jaewan): Due to the nature of the binder, calls can be called out of order.
+ // Need a way to ensure calling of unregisterCallback unregisters later
+ // registerCallback.
+ oneway void registerCallback(IMediaSession2Callback callbackBinder,
+ int callbackFlag, int requestCode);
+ oneway void unregisterCallback(IMediaSession2Callback callbackBinder, int callbackFlag);
+}
diff --git a/media/java/android/media/IMediaSession2Callback.aidl b/media/java/android/media/IMediaSession2Callback.aidl
new file mode 100644
index 0000000..5259e6c
--- /dev/null
+++ b/media/java/android/media/IMediaSession2Callback.aidl
@@ -0,0 +1,46 @@
+/*
+ * 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.media.session.PlaybackState;
+import android.media.IMediaSession2;
+
+/**
+ * Interface from MediaSession2 to MediaSession2Record.
+ * <p>
+ * Keep this interface oneway. Otherwise a malicious app may implement fake version of this,
+ * and holds calls from session to make session owner(s) frozen.
+ *
+ * @hide
+ */
+oneway interface IMediaSession2Callback {
+ void onPlaybackStateChanged(in PlaybackState state);
+
+ /**
+ * Called only when the controller is created with service's token.
+ *
+ * @param sessionBinder {@code null} if the connect is rejected or is disconnected. a session
+ * binder if the connect is accepted.
+ * @param commands initially allowed commands.
+ */
+ // TODO(jaewan): Also need to pass flags for allowed actions for permission check.
+ // For example, a media can allow setRating only for whitelisted apps
+ // it's better for controller to know such information in advance.
+ // 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, long commands);
+}
diff --git a/media/java/android/media/MediaController2.java b/media/java/android/media/MediaController2.java
new file mode 100644
index 0000000..c28c2fa
--- /dev/null
+++ b/media/java/android/media/MediaController2.java
@@ -0,0 +1,196 @@
+/*
+ * 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.CommandFlags;
+import android.media.MediaSession2.ControllerInfo;
+import android.media.session.PlaybackState;
+import android.media.update.ApiLoader;
+import android.media.update.MediaController2Provider;
+import android.os.Handler;
+import java.util.concurrent.Executor;
+
+/**
+ * Allows an app to interact with an active {@link MediaSession2} or a
+ * {@link MediaSessionService2} in any status. Media buttons and other commands can be sent to
+ * the session.
+ * <p>
+ * When you're done, use {@link #release()} to clean up resources. This also helps session service
+ * to be destroyed when there's no controller associated with it.
+ * <p>
+ * When controlling {@link MediaSession2}, the controller will be available immediately after
+ * the creation.
+ * <p>
+ * When controlling {@link MediaSessionService2}, the {@link MediaController2} would be
+ * available only if the session service allows this controller by
+ * {@link MediaSession2.SessionCallback#onConnect(ControllerInfo)} for the service. Wait
+ * {@link ControllerCallback#onConnected(long)} or {@link ControllerCallback#onDisconnected()} for
+ * the result.
+ * <p>
+ * A controller can be created through {@link MediaPlayerSessionManager} if you hold the
+ * signature|privileged permission "android.permission.MEDIA_CONTENT_CONTROL" permission or are
+ * an enabled notification listener or by getting a {@link SessionToken} directly the
+ * the session owner.
+ * <p>
+ * MediaController2 objects are thread-safe.
+ * <p>
+ * @see MediaSession2
+ * @see MediaSessionService2
+ * @hide
+ */
+// TODO(jaewan): Unhide
+// TODO(jaewan): Revisit comments. Currently MediaBrowser case is missing.
+public class MediaController2 extends MediaPlayerBase {
+ /**
+ * Interface for listening to change in activeness of the {@link MediaSession2}. It's
+ * active if and only if it has set a player.
+ */
+ public abstract static class ControllerCallback {
+ /**
+ * Called when the controller is successfully connected to the session. The controller
+ * becomes available afterwards.
+ *
+ * @param commands commands that's allowed by the session.
+ */
+ public void onConnected(@CommandFlags long commands) { }
+
+ /**
+ * Called when the session refuses the controller or the controller is disconnected from
+ * the session. The controller becomes unavailable afterwards and the callback wouldn't
+ * be called.
+ * <p>
+ * It will be also called after the {@link #release()}, so you can put clean up code here.
+ * You don't need to call {@link #release()} after this.
+ */
+ public void onDisconnected() { }
+ }
+
+ private final MediaController2Provider mProvider;
+
+ /**
+ * Create a {@link MediaController2} from the {@link SessionToken}. This connects to the session
+ * and may wake up the service if it's not available.
+ *
+ * @param context Context
+ * @param token token to connect to
+ * @param callback controller callback to receive changes in
+ * @param executor executor to run callbacks on.
+ */
+ // TODO(jaewan): Put @CallbackExecutor to the constructor.
+ public MediaController2(@NonNull Context context, @NonNull SessionToken token,
+ @NonNull ControllerCallback callback, @NonNull Executor executor) {
+ super();
+
+ // This also connects to the token.
+ // Explicit connect() isn't added on purpose because retrying connect() is impossible with
+ // 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)
+ .createMediaController2(this, context, token, callback, executor);
+ }
+
+ /**
+ * Release this object, and disconnect from the session. After this, callbacks wouldn't be
+ * received.
+ */
+ public void release() {
+ mProvider.release_impl();
+ }
+
+ /**
+ * @hide
+ */
+ public MediaController2Provider getProvider() {
+ return mProvider;
+ }
+
+ /**
+ * @return token
+ */
+ public @NonNull
+ SessionToken getSessionToken() {
+ return mProvider.getSessionToken_impl();
+ }
+
+ /**
+ * Returns whether this class is connected to active {@link MediaSession2} or not.
+ */
+ public boolean isConnected() {
+ return mProvider.isConnected_impl();
+ }
+
+ @Override
+ public void play() {
+ mProvider.play_impl();
+ }
+
+ @Override
+ public void pause() {
+ mProvider.pause_impl();
+ }
+
+ @Override
+ public void stop() {
+ mProvider.stop_impl();
+ }
+
+ @Override
+ public void skipToPrevious() {
+ mProvider.skipToPrevious_impl();
+ }
+
+ @Override
+ public void skipToNext() {
+ mProvider.skipToNext_impl();
+ }
+
+ @Override
+ public @Nullable PlaybackState getPlaybackState() {
+ return mProvider.getPlaybackState_impl();
+ }
+
+ /**
+ * Add a {@link PlaybackListener} to listen changes in the
+ * {@link MediaSession2}.
+ *
+ * @param listener the listener that will be run
+ * @param handler the Handler that will receive the listener
+ * @throws IllegalArgumentException Called when either the listener or handler is {@code null}.
+ */
+ // TODO(jaewan): Match with the addSessionAvailabilityListener() that tells the current state
+ // through the listener.
+ // TODO(jaewan): Can handler be null? Follow the API guideline after it's finalized.
+ @Override
+ public void addPlaybackListener(@NonNull PlaybackListener listener, @NonNull Handler handler) {
+ mProvider.addPlaybackListener_impl(listener, handler);
+ }
+
+ /**
+ * Remove previously added {@link PlaybackListener}.
+ *
+ * @param listener the listener to be removed
+ * @throws IllegalArgumentException if the listener is {@code null}.
+ */
+ @Override
+ public void removePlaybackListener(@NonNull PlaybackListener listener) {
+ mProvider.removePlaybackListener_impl(listener);
+ }
+}
diff --git a/media/java/android/media/MediaPlayerBase.java b/media/java/android/media/MediaPlayerBase.java
new file mode 100644
index 0000000..980c70f
--- /dev/null
+++ b/media/java/android/media/MediaPlayerBase.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.media.session.PlaybackState;
+import android.os.Handler;
+
+/**
+ * Tentative interface for all media players that want media session.
+ * APIs are named to avoid conflicts with MediaPlayer APIs.
+ * All calls should be asynchrounous.
+ *
+ * @hide
+ */
+// TODO(wjia) Finalize the list of MediaPlayer2, which MediaPlayerBase's APIs will be come from.
+public abstract class MediaPlayerBase {
+ /**
+ * Listens change in {@link PlaybackState}.
+ */
+ public interface PlaybackListener {
+ /**
+ * Called when {@link PlaybackState} for this player is changed.
+ */
+ void onPlaybackChanged(PlaybackState state);
+ }
+
+ // TODO(jaewan): setDataSources()?
+ // TODO(jaewan): Add release() or do that in stop()?
+
+ // TODO(jaewan): Add set/getSupportedActions().
+ public abstract void play();
+ public abstract void pause();
+ public abstract void stop();
+ public abstract void skipToPrevious();
+ public abstract void skipToNext();
+
+ // Currently PlaybackState's error message is the content title (for testing only)
+ // TODO(jaewan): Add metadata support
+ public abstract PlaybackState getPlaybackState();
+
+ /**
+ * Add a {@link PlaybackListener} to be invoked when the playback state is changed.
+ *
+ * @param listener the listener that will be run
+ * @param handler the Handler that will receive the listener
+ */
+ public abstract void addPlaybackListener(PlaybackListener listener, Handler handler);
+
+ /**
+ * Remove previously added {@link PlaybackListener}.
+ *
+ * @param listener the listener to be removed
+ */
+ public abstract void removePlaybackListener(PlaybackListener listener);
+}
diff --git a/media/java/android/media/MediaSession2.java b/media/java/android/media/MediaSession2.java
new file mode 100644
index 0000000..8daa833
--- /dev/null
+++ b/media/java/android/media/MediaSession2.java
@@ -0,0 +1,443 @@
+/*
+ * 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.LongDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.Context;
+import android.media.session.MediaSession;
+import android.media.session.MediaSession.Callback;
+import android.media.session.PlaybackState;
+import android.media.update.ApiLoader;
+import android.media.update.MediaSession2Provider;
+import android.media.update.MediaSession2Provider.ControllerInfoProvider;
+import android.os.Handler;
+import android.os.Process;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.List;
+
+/**
+ * Allows a media app to expose its transport controls and playback information in a process to
+ * other processes including the Android framework and other apps. Common use cases are as follows.
+ * <ul>
+ * <li>Bluetooth/wired headset key events support</li>
+ * <li>Android Auto/Wearable support</li>
+ * <li>Separating UI process and playback process</li>
+ * </ul>
+ * <p>
+ * A MediaSession2 should be created when an app wants to publish media playback information or
+ * handle media keys. In general an app only needs one session for all playback, though multiple
+ * sessions can be created to provide finer grain controls of media.
+ * <p>
+ * If you want to support background playback, {@link MediaSessionService2} is preferred
+ * instead. With it, your playback can be revived even after you've finished playback. See
+ * {@link MediaSessionService2} for details.
+ * <p>
+ * 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.
+ * <p>
+ * 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)}.
+ * <p>
+ * 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.
+ * <p>
+ * MediaSession2 objects should be used on the handler's thread that is initially given by
+ * {@link #getInstance(Context, Handler)}.
+ *
+ * @see MediaSessionService2
+ * @hide
+ */
+// TODO(jaewan): Unhide
+// TODO(jaewan): Revisit comments. Currently it's borrowed from the MediaSession.
+// TODO(jaewan): Add explicit release(), and make token @NonNull. Session will be active while the
+// session exists, and controllers will be invalidated when session becomes inactive.
+// TODO(jaewan): Should we support thread safe? It may cause tricky issue such as b/63797089
+// 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 {
+ private final MediaSession2Provider mProvider;
+
+ // These are intentionally public to allow apps to hook for every incoming command.
+ // Type is long (64 bits) to have enough buffer to keep all commands from MediaControllers (29)
+ // and future extensions.
+ // Sync with the MediaSession2Impl.java
+ // TODO(jaewan): Add a way to log every incoming calls outside of the app with the calling
+ // package.
+ // Keep these sync with IMediaSession2RecordCallback.
+ // TODO(jaewan): Should we move this to updatable as well?
+ public static final long COMMAND_FLAG_PLAYBACK_START = 1 << 0;
+ public static final long COMMAND_FLAG_PLAYBACK_PAUSE = 1 << 1;
+ public static final long COMMAND_FLAG_PLAYBACK_STOP = 1 << 2;
+ public static final long COMMAND_FLAG_PLAYBACK_SKIP_NEXT_ITEM = 1 << 3;
+ public static final long COMMAND_FLAG_PLAYBACK_SKIP_PREV_ITEM = 1 << 4;
+
+ /**
+ * Command flag for adding/removing playback listener to get playback state.
+ */
+ public static final long COMMAND_FLAG_GET_PLAYBACK_STATE = 1 << 5;
+
+ @Retention(RetentionPolicy.SOURCE)
+ @LongDef(flag = true, value = {COMMAND_FLAG_PLAYBACK_START, COMMAND_FLAG_PLAYBACK_PAUSE,
+ COMMAND_FLAG_PLAYBACK_STOP, COMMAND_FLAG_PLAYBACK_SKIP_NEXT_ITEM,
+ COMMAND_FLAG_PLAYBACK_SKIP_PREV_ITEM, COMMAND_FLAG_GET_PLAYBACK_STATE})
+ public @interface CommandFlags {
+ }
+
+ /**
+ * Callback to be called for all incoming commands from {@link MediaController2}s.
+ * <p>
+ * If it's not set, the session will accept all controllers and all incoming commands by
+ * default.
+ */
+ // TODO(jaewan): Add UID with multi-user support.
+ // TODO(jaewan): Can we move this inside of the updatable for default implementation.
+ // TODO(jaewan): Add onConnected() to return permitted action.
+ // TODO(jaewan): Cache the result? Will it be persistent?
+ public static class SessionCallback {
+ /**
+ * Called when a controller is created for this session. Return allowed commands for
+ * controller. By default it allows system apps and self.
+ * <p>
+ * You can reject the connection at all by return {@code 0}.
+ *
+ * @param controller controller information.
+ * @return
+ */
+ // TODO(jaewan): Change return type. Once we do, null is for reject.
+ public @CommandFlags long onConnect(ControllerInfo controller) {
+ // TODO(jaewan): Move this to updatable.
+ if (controller.isTrusted() || controller.getUid() == Process.myUid()) {
+ // TODO(jaewan): Change default.
+ return (1 << 6) - 1;
+ }
+ // Reject others
+ return 0;
+ }
+
+ /**
+ * Called when a controller sent a command to the session. You can also reject the request
+ * by return {@code false} for apps without system permission. You cannot reject commands
+ * from apps with system permission.
+ * <p>
+ * This method will be called on the session handler.
+ *
+ * @param controller controller information.
+ * @param command one of the {@link CommandFlags}. This method will be called for every
+ * single command.
+ * @return {@code true} if you want to accept incoming command. {@code false} otherwise.
+ * It will be ignored for apps with the system permission.
+ * @see {@link CommandFlags}
+ */
+ // TODO(jaewan): Get confirmation from devrel/auto that it's OK to return void here.
+ public boolean onCommand(ControllerInfo controller, @CommandFlags long command) {
+ return true;
+ }
+ };
+
+ /**
+ * Builder for {@link MediaSession2}.
+ * <p>
+ * 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 final static class Builder {
+ private final Context mContext;
+ private final MediaPlayerBase mPlayer;
+ private String mId;
+ private SessionCallback mCallback;
+
+ /**
+ * Constructor.
+ *
+ * @param context a context
+ * @param player a player to handle incoming command from any controller.
+ * @throws IllegalArgumentException if any parameter is null, or the player is a
+ * {@link MediaSession2} or {@link MediaController2}.
+ */
+ public Builder(@NonNull Context context, @NonNull MediaPlayerBase player) {
+ if (context == null) {
+ throw new IllegalArgumentException("context shouldn't be null");
+ }
+ if (player == null) {
+ throw new IllegalArgumentException("player shouldn't be null");
+ }
+ if (player instanceof MediaSession2 || player instanceof MediaController2) {
+ throw new IllegalArgumentException("player doesn't accept MediaSession2 nor"
+ + " MediaController2");
+ }
+ mContext = context;
+ mPlayer = player;
+ // Ensure non-null
+ mId = "";
+ }
+
+ /**
+ * Set ID of the session. If it's not set, an empty string with used to create a session.
+ * <p>
+ * Use this if and only if your app supports multiple playback at the same time and also
+ * wants to provide external apps to have finer controls of them.
+ *
+ * @param id id of the session. Must be unique per package.
+ * @throws IllegalArgumentException if id is {@code null}
+ * @return
+ */
+ public Builder setId(@NonNull String id) {
+ if (id == null) {
+ throw new IllegalArgumentException("id shouldn't be null");
+ }
+ mId = id;
+ return this;
+ }
+
+ /**
+ * Set {@link SessionCallback}.
+ *
+ * @param callback session callback.
+ * @return
+ */
+ public Builder setSessionCallback(@Nullable SessionCallback callback) {
+ mCallback = callback;
+ return this;
+ }
+
+ /**
+ * Build {@link MediaSession2}.
+ *
+ * @return a new session
+ * @throws IllegalStateException if the session with the same id is already exists for the
+ * package.
+ */
+ public MediaSession2 build() throws IllegalStateException {
+ if (mCallback == null) {
+ mCallback = new SessionCallback();
+ }
+ return new MediaSession2(mContext, mPlayer, mId, mCallback);
+ }
+ }
+
+ /**
+ * Information of a controller.
+ */
+ // TODO(jaewan): Move implementation to the updatable.
+ public static final class ControllerInfo {
+ private final ControllerInfoProvider mProvider;
+
+ /**
+ * @hide
+ */
+ // TODO(jaewan): SystemApi
+ // TODO(jaewan): Also accept componentName to check notificaiton listener.
+ public ControllerInfo(Context context, int uid, int pid, String packageName,
+ IMediaSession2Callback callback) {
+ mProvider = ApiLoader.getProvider(context)
+ .createMediaSession2ControllerInfoProvider(
+ this, context, uid, pid, packageName, callback);
+ }
+
+ /**
+ * @return package name of the controller
+ */
+ public String getPackageName() {
+ return mProvider.getPackageName_impl();
+ }
+
+ /**
+ * @return uid of the controller
+ */
+ public int getUid() {
+ return mProvider.getUid_impl();
+ }
+
+ /**
+ * Return if the controller has granted {@code android.permission.MEDIA_CONTENT_CONTROL} or
+ * has a enabled notification listener so can be trusted to accept connection and incoming
+ * command request.
+ *
+ * @return {@code true} if the controller is trusted.
+ */
+ public boolean isTrusted() {
+ return mProvider.isTrusted_impl();
+ }
+
+ /**
+ * @hide
+ * @return
+ */
+ // TODO(jaewan): SystemApi
+ public ControllerInfoProvider getProvider() {
+ return mProvider;
+ }
+
+ @Override
+ public int hashCode() {
+ return mProvider.hashCode_impl();
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (!(obj instanceof ControllerInfo)) {
+ return false;
+ }
+ ControllerInfo other = (ControllerInfo) obj;
+ return mProvider.equals_impl(other.mProvider);
+ }
+
+ @Override
+ public String toString() {
+ return "ControllerInfo {pkg=" + getPackageName() + ", uid=" + getUid() + ", trusted="
+ + isTrusted() + "}";
+ }
+ }
+
+ /**
+ * Constructor is hidden and apps can only instantiate indirectly through {@link Builder}.
+ * <p>
+ * This intended behavior and here's the reasons.
+ * 1. Prevent multiple sessions with the same tag in a media app.
+ * Whenever it happens only one session was properly setup and others were all dummies.
+ * Android framework couldn't find the right session to dispatch media key event.
+ * 2. Simplify session's lifecycle.
+ * {@link MediaSession} can be available after all of {@link MediaSession#setFlags(int)},
+ * {@link MediaSession#setCallback(Callback)}, and
+ * {@link MediaSession#setActive(boolean)}. It was common for an app to omit one, so
+ * framework had to add heuristics to figure out if an app is
+ * @hide
+ */
+ private MediaSession2(Context context, MediaPlayerBase player, String id,
+ SessionCallback callback) {
+ super();
+ mProvider = ApiLoader.getProvider(context)
+ .createMediaSession2(this, context, player, id, callback);
+ }
+
+ /**
+ * @hide
+ */
+ // TODO(jaewan): SystemApi
+ public MediaSession2Provider getProvider() {
+ return mProvider;
+ }
+
+ /**
+ * Set the underlying {@link MediaPlayerBase} for this session to dispatch incoming event to.
+ * Events from the {@link MediaController2} will be sent directly to the underlying
+ * player on the {@link Handler} where the session is created on.
+ * <p>
+ * If the new player is successfully set, {@link PlaybackListener}
+ * will be called to tell the current playback state of the new player.
+ * <p>
+ * Calling this method with {@code null} will disconnect binding connection between the
+ * controllers and also release this object.
+ *
+ * @param player a {@link MediaPlayerBase} that handles actual media playback in your app.
+ * It shouldn't be {@link MediaSession2} nor {@link MediaController2}.
+ * @throws IllegalArgumentException if the player is either {@link MediaSession2}
+ * or {@link MediaController2}.
+ */
+ // TODO(jaewan): Add release instead of setPlayer(null).
+ public void setPlayer(MediaPlayerBase player) throws IllegalArgumentException {
+ mProvider.setPlayer_impl(player);
+ }
+
+ /**
+ * @return player
+ */
+ public @Nullable MediaPlayerBase getPlayer() {
+ return mProvider.getPlayer_impl();
+ }
+
+ /**
+ * Returns the {@link SessionToken} for creating {@link MediaController2}.
+ */
+ public @NonNull
+ SessionToken getToken() {
+ return mProvider.getToken_impl();
+ }
+
+ public @NonNull List<ControllerInfo> getConnectedControllers() {
+ return mProvider.getConnectedControllers_impl();
+ }
+
+ @Override
+ public void play() {
+ mProvider.play_impl();
+ }
+
+ @Override
+ public void pause() {
+ mProvider.pause_impl();
+ }
+
+ @Override
+ public void stop() {
+ mProvider.stop_impl();
+ }
+
+ @Override
+ public void skipToPrevious() {
+ mProvider.skipToPrevious_impl();
+ }
+
+ @Override
+ public void skipToNext() {
+ mProvider.skipToNext_impl();
+ }
+
+ @Override
+ public @NonNull PlaybackState getPlaybackState() {
+ return mProvider.getPlaybackState_impl();
+ }
+
+ /**
+ * Add a {@link PlaybackListener} to listen changes in the
+ * underlying {@link MediaPlayerBase} which is previously set by
+ * {@link #setPlayer(MediaPlayerBase)}.
+ * <p>
+ * Added listeners will be also called when the underlying player is changed.
+ *
+ * @param listener the listener that will be run
+ * @param handler the Handler that will receive the listener
+ * @throws IllegalArgumentException when either the listener or handler is {@code null}.
+ */
+ // TODO(jaewan): Can handler be null? Follow API guideline after it's finalized.
+ @Override
+ public void addPlaybackListener(@NonNull PlaybackListener listener, @NonNull Handler handler) {
+ mProvider.addPlaybackListener_impl(listener, handler);
+ }
+
+ /**
+ * Remove previously added {@link PlaybackListener}.
+ *
+ * @param listener the listener to be removed
+ * @throws IllegalArgumentException if the listener is {@code null}.
+ */
+ @Override
+ public void removePlaybackListener(PlaybackListener listener) {
+ mProvider.removePlaybackListener_impl(listener);
+ }
+}
diff --git a/media/java/android/media/MediaSessionService2.java b/media/java/android/media/MediaSessionService2.java
new file mode 100644
index 0000000..f1f5467
--- /dev/null
+++ b/media/java/android/media/MediaSessionService2.java
@@ -0,0 +1,264 @@
+/*
+ * 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.CallSuper;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.Notification;
+import android.app.Service;
+import android.content.Intent;
+import android.media.MediaSession2.ControllerInfo;
+import android.media.session.PlaybackState;
+import android.media.update.ApiLoader;
+import android.media.update.MediaSessionService2Provider;
+import android.os.IBinder;
+
+/**
+ * Service version of the {@link MediaSession2}.
+ * <p>
+ * It's highly recommended for an app to use this instead of {@link MediaSession2} if it wants
+ * to keep media playback in the background.
+ * <p>
+ * Here's the benefits of using {@link MediaSessionService2} instead of
+ * {@link MediaSession2}.
+ * <ul>
+ * <li>Another app can know that your app supports {@link MediaSession2} even when your app
+ * isn't running.
+ * <li>Another app can start playback of your app even when your app isn't running.
+ * </ul>
+ * For example, user's voice command can start playback of your app even when it's not running.
+ * <p>
+ * To use this class, adding followings directly to your {@code AndroidManifest.xml}.
+ * <pre>
+ * <service android:name="component_name_of_your_implementation" >
+ * <intent-filter>
+ * <action android:name="android.media.session.MediaSessionService2" />
+ * </intent-filter>
+ * </service></pre>
+ * <p>
+ * A {@link MediaSessionService2} is another form of {@link MediaSession2}. 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.
+ * <pre>
+ * <service android:name="component_name_of_your_implementation" >
+ * <intent-filter>
+ * <action android:name="android.media.session.MediaSessionService2" />
+ * </intent-filter>
+ * <meta-data android:name="android.media.session"
+ * android:value="session_id"/>
+ * </service></pre>
+ * <p>
+ * It's recommended for an app to have a single {@link MediaSessionService2} declared in the
+ * manifest. Otherwise, your app might be shown twice in the list of the Auto/Wearable, or another
+ * app fails to pick the right session service when it wants to start the playback this app.
+ * <p>
+ * If there's conflicts with the session ID among the services, services wouldn't be available for
+ * any controllers.
+ * <p>
+ * Topic covered here:
+ * <ol>
+ * <li><a href="#ServiceLifecycle">Service Lifecycle</a>
+ * <li><a href="#Permissions">Permissions</a>
+ * </ol>
+ * <div class="special reference">
+ * <a name="ServiceLifecycle"></a>
+ * <h3>Service Lifecycle</h3>
+ * <p>
+ * Session service is bounded service. When a {@link MediaController2} is created for the
+ * session service, the controller binds to the session service. {@link #onCreateSession(String)}
+ * may be called after the {@link #onCreate} if the service hasn't created yet.
+ * <p>
+ * After the binding, session's {@link MediaSession2.SessionCallback#onConnect(ControllerInfo)}
+ * will be called to accept or reject connection request from a controller. If the connection is
+ * rejected, the controller will unbind. If it's accepted, the controller will be available to use
+ * and keep binding.
+ * <p>
+ * When playback is started for this session service, {@link #onUpdateNotification(PlaybackState)}
+ * is called and service would become a foreground service. It's needed to keep playback after the
+ * controller is destroyed. The session service becomes background service when the playback is
+ * stopped.
+ * <a name="Permissions"></a>
+ * <h3>Permissions</h3>
+ * <p>
+ * Any app can bind to the session service with controller, but the controller can be used only if
+ * the session service accepted the connection request through
+ * {@link MediaSession2.SessionCallback#onConnect(ControllerInfo)}.
+ *
+ * @hide
+ */
+// TODO(jaewan): Unhide
+// TODO(jaewan): Can we clean up sessions in onDestroy() automatically instead?
+// What about currently running SessionCallback when the onDestroy() is called?
+// TODO(jaewan): Protect this with system|privilleged permission - Q.
+// TODO(jaewan): Add permission check for the service to know incoming connection request.
+// Follow-up questions: What about asking a XML for list of white/black packages for
+// allowing enumeration?
+// We can read the information even when the service is started,
+// so SessionManager.getXXXXService() can only return apps
+// TODO(jaewan): Will be the black/white listing persistent?
+// In other words, can we cache the rejection?
+public abstract class MediaSessionService2 extends Service {
+ private final MediaSessionService2Provider mProvider;
+
+ /**
+ * 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";
+
+ /**
+ * Name under which a MediaSessionService2 component publishes information about itself.
+ * This meta-data must provide a string value for the ID.
+ */
+ 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);
+ }
+
+ /**
+ * Default implementation for {@link MediaSessionService2} to initialize session service.
+ * <p>
+ * Override this method if you need your own initialization. Derived classes MUST call through
+ * to the super class's implementation of this method.
+ */
+ @CallSuper
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ mProvider.onCreate_impl();
+ }
+
+ /**
+ * Called when another app requested to start this service to get {@link MediaSession2}.
+ * <p>
+ * Session service will accept or reject the connection with the
+ * {@link MediaSession2.SessionCallback} in the created session.
+ * <p>
+ * 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.
+ * <p>
+ * This method will be call 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.
+ * <p>
+ * 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.
+ * <p>
+ * 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.
+ */
+ // TODO(jaewan): Also add metadata
+ public MediaNotification onUpdateNotification(PlaybackState state) {
+ return mProvider.onUpdateNotification_impl(state);
+ }
+
+ /**
+ * Get instance of the {@link MediaSession2} that you've previously created with the
+ * {@link #onCreateSession} for this service.
+ *
+ * @return created session
+ */
+ public final MediaSession2 getSession() {
+ return mProvider.getSession_impl();
+ }
+
+ /**
+ * Default implementation for {@link MediaSessionService2} to handle incoming binding
+ * request. If the request is for getting the session, the intent will have action
+ * {@link #SERVICE_INTERFACE}.
+ * <p>
+ * Override this method if this service also needs to handle binder requests other than
+ * {@link #SERVICE_INTERFACE}. Derived classes MUST call through to the super class's
+ * implementation of this method.
+ *
+ * @param intent
+ * @return Binder
+ */
+ @CallSuper
+ @Nullable
+ @Override
+ public IBinder onBind(Intent intent) {
+ return mProvider.onBind_impl(intent);
+ }
+
+ /**
+ * Returned by {@link #onUpdateNotification(PlaybackState)} for making session service
+ * foreground service to keep playback running in the background. It's highly recommended to
+ * show media style notification here.
+ */
+ // TODO(jaewan): Should we also move this to updatable?
+ public static class MediaNotification {
+ public final int id;
+ public final Notification notification;
+
+ private MediaNotification(int id, @NonNull Notification notification) {
+ this.id = id;
+ this.notification = notification;
+ }
+
+ /**
+ * Create a {@link MediaNotification}.
+ *
+ * @param notificationId notification id to be used for
+ * {@link android.app.NotificationManager#notify(int, Notification)}.
+ * @param notification a notification to make session service foreground service. Media
+ * style notification is recommended here.
+ * @return
+ */
+ public static MediaNotification create(int notificationId,
+ @NonNull Notification notification) {
+ if (notification == null) {
+ throw new IllegalArgumentException("Notification cannot be null");
+ }
+ return new MediaNotification(notificationId, notification);
+ }
+ }
+}
diff --git a/media/java/android/media/SessionToken.java b/media/java/android/media/SessionToken.java
new file mode 100644
index 0000000..b470dea
--- /dev/null
+++ b/media/java/android/media/SessionToken.java
@@ -0,0 +1,223 @@
+/*
+ * 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.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.media.session.MediaSessionManager;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.text.TextUtils;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Represents an ongoing {@link MediaSession2} or a {@link MediaSessionService2}.
+ * If it's representing a session service, it may not be ongoing.
+ * <p>
+ * This may be passed to apps by the session owner to allow them to create a
+ * {@link MediaController2} to communicate with the session.
+ * <p>
+ * It can be also obtained by {@link MediaSessionManager}.
+ * @hide
+ */
+// TODO(jaewan): Unhide. SessionToken2?
+// TODO(jaewan): Move Token to updatable!
+// TODO(jaewan): Find better name for this (SessionToken or Session2Token)
+public final class SessionToken {
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(value = {TYPE_SESSION, TYPE_SESSION_SERVICE})
+ public @interface TokenType {
+ }
+
+ public static final int TYPE_SESSION = 0;
+ public static final int TYPE_SESSION_SERVICE = 1;
+
+ private static final String KEY_TYPE = "android.media.token.type";
+ private static final String KEY_PACKAGE_NAME = "android.media.token.package_name";
+ private static final String KEY_SERVICE_NAME = "android.media.token.service_name";
+ private static final String KEY_ID = "android.media.token.id";
+ private static final String KEY_SESSION_BINDER = "android.media.token.session_binder";
+
+ private final @TokenType int mType;
+ private final String mPackageName;
+ private final String mServiceName;
+ private final String mId;
+ private final IMediaSession2 mSessionBinder;
+
+ /**
+ * Constructor for the token.
+ *
+ * @hide
+ * @param type type
+ * @param packageName package name
+ * @param id id
+ * @param serviceName name of service. Can be {@code null} if it's not an service.
+ * @param sessionBinder binder for this session. Can be {@code null} if it's service.
+ * @hide
+ */
+ // TODO(jaewan): UID is also needed.
+ public SessionToken(@TokenType int type, @NonNull String packageName, @NonNull String id,
+ @Nullable String serviceName, @Nullable IMediaSession2 sessionBinder) {
+ // TODO(jaewan): Add sanity check.
+ mType = type;
+ mPackageName = packageName;
+ mId = id;
+ mServiceName = serviceName;
+ mSessionBinder = sessionBinder;
+ }
+
+ public int hashCode() {
+ final int prime = 31;
+ return mType
+ + prime * (mPackageName.hashCode()
+ + prime * (mId.hashCode()
+ + prime * ((mServiceName != null ? mServiceName.hashCode() : 0)
+ + prime * (mSessionBinder != null ? mSessionBinder.asBinder().hashCode() : 0))));
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj)
+ return true;
+ if (obj == null)
+ return false;
+ if (getClass() != obj.getClass())
+ return false;
+ SessionToken other = (SessionToken) obj;
+ if (!mPackageName.equals(other.getPackageName())
+ || !mServiceName.equals(other.getServiceName())
+ || !mId.equals(other.getId())
+ || mType != other.getType()) {
+ return false;
+ }
+ if (mSessionBinder == other.getSessionBinder()) {
+ return true;
+ } else if (mSessionBinder == null || other.getSessionBinder() == null) {
+ return false;
+ }
+ return mSessionBinder.asBinder().equals(other.getSessionBinder().asBinder());
+ }
+
+ @Override
+ public String toString() {
+ return "SessionToken {pkg=" + mPackageName + " id=" + mId + " type=" + mType
+ + " service=" + mServiceName + " binder=" + mSessionBinder + "}";
+ }
+
+ /**
+ * @return package name
+ */
+ public String getPackageName() {
+ return mPackageName;
+ }
+
+ /**
+ * @return id
+ */
+ public String getId() {
+ return mId;
+ }
+
+ /**
+ * @return type of the token
+ * @see #TYPE_SESSION
+ * @see #TYPE_SESSION_SERVICE
+ */
+ public @TokenType int getType() {
+ return mType;
+ }
+
+ /**
+ * @return session binder.
+ * @hide
+ */
+ public @Nullable IMediaSession2 getSessionBinder() {
+ return mSessionBinder;
+ }
+
+ /**
+ * @return service name if it's session service.
+ * @hide
+ */
+ public @Nullable String getServiceName() {
+ return mServiceName;
+ }
+
+ /**
+ * Create a token from the bundle, exported by {@link #toBundle()}.
+ *
+ * @param bundle
+ * @return
+ */
+ public static SessionToken fromBundle(@NonNull Bundle bundle) {
+ if (bundle == null) {
+ return null;
+ }
+ final @TokenType int type = bundle.getInt(KEY_TYPE, -1);
+ final String packageName = bundle.getString(KEY_PACKAGE_NAME);
+ final String serviceName = bundle.getString(KEY_SERVICE_NAME);
+ final String id = bundle.getString(KEY_ID);
+ final IBinder sessionBinder = bundle.getBinder(KEY_SESSION_BINDER);
+
+ // Sanity check.
+ switch (type) {
+ case TYPE_SESSION:
+ if (!(sessionBinder instanceof IMediaSession2)) {
+ throw new IllegalArgumentException("Session needs sessionBinder");
+ }
+ break;
+ case TYPE_SESSION_SERVICE:
+ if (TextUtils.isEmpty(serviceName)) {
+ throw new IllegalArgumentException("Session service needs service name");
+ }
+ if (sessionBinder != null && !(sessionBinder instanceof IMediaSession2)) {
+ throw new IllegalArgumentException("Invalid session binder");
+ }
+ break;
+ default:
+ throw new IllegalArgumentException("Invalid type");
+ }
+ if (TextUtils.isEmpty(packageName) || id == null) {
+ throw new IllegalArgumentException("Package name nor ID cannot be null.");
+ }
+ // TODO(jaewan): Revisit here when we add connection callback to the session for individual
+ // controller's permission check. With it, sessionBinder should be available
+ // if and only if for session, not session service.
+ return new SessionToken(type, packageName, id, serviceName,
+ sessionBinder != null ? IMediaSession2.Stub.asInterface(sessionBinder) : null);
+ }
+
+ /**
+ * Create a {@link Bundle} from this token to share it across processes.
+ *
+ * @return Bundle
+ * @hide
+ */
+ public Bundle toBundle() {
+ Bundle bundle = new Bundle();
+ bundle.putString(KEY_PACKAGE_NAME, mPackageName);
+ bundle.putString(KEY_SERVICE_NAME, mServiceName);
+ bundle.putString(KEY_ID, mId);
+ bundle.putInt(KEY_TYPE, mType);
+ bundle.putBinder(KEY_SESSION_BINDER,
+ mSessionBinder != null ? mSessionBinder.asBinder() : null);
+ return bundle;
+ }
+}
diff --git a/media/java/android/media/session/ISessionManager.aidl b/media/java/android/media/session/ISessionManager.aidl
index 5fcb430..b8463dd 100644
--- a/media/java/android/media/session/ISessionManager.aidl
+++ b/media/java/android/media/session/ISessionManager.aidl
@@ -17,6 +17,7 @@
import android.content.ComponentName;
import android.media.IRemoteVolumeController;
+import android.media.IMediaSession2;
import android.media.session.IActiveSessionsListener;
import android.media.session.ICallback;
import android.media.session.IOnMediaKeyListener;
@@ -49,4 +50,8 @@
void setCallback(in ICallback callback);
void setOnVolumeKeyLongPressListener(in IOnVolumeKeyLongPressListener listener);
void setOnMediaKeyListener(in IOnMediaKeyListener listener);
+
+ // MediaSession2
+ Bundle createSessionToken(String callingPackage, String id, IMediaSession2 binder);
+ List<Bundle> getSessionTokens(boolean activeSessionOnly, boolean sessionServiceOnly);
}
diff --git a/media/java/android/media/session/MediaSessionManager.java b/media/java/android/media/session/MediaSessionManager.java
index b215825..6a9f04a 100644
--- a/media/java/android/media/session/MediaSessionManager.java
+++ b/media/java/android/media/session/MediaSessionManager.java
@@ -24,8 +24,13 @@
import android.content.ComponentName;
import android.content.Context;
import android.media.AudioManager;
+import android.media.IMediaSession2;
import android.media.IRemoteVolumeController;
+import android.media.MediaSession2;
+import android.media.MediaSessionService2;
+import android.media.SessionToken;
import android.media.session.ISessionManager;
+import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.os.RemoteException;
@@ -38,6 +43,7 @@
import android.view.KeyEvent;
import java.util.ArrayList;
+import java.util.Collections;
import java.util.List;
/**
@@ -331,6 +337,101 @@
}
/**
+ * Called when a {@link MediaSession2} is created.
+ *
+ * @hide
+ */
+ // TODO(jaewan): System API
+ public SessionToken createSessionToken(@NonNull String callingPackage, @NonNull String id,
+ @NonNull IMediaSession2 binder) {
+ try {
+ Bundle bundle = mService.createSessionToken(callingPackage, id, binder);
+ return SessionToken.fromBundle(bundle);
+ } catch (RemoteException e) {
+ Log.wtf(TAG, "Cannot communicate with the service.", e);
+ }
+ return null;
+ }
+
+ /**
+ * Get {@link List} of {@link SessionToken} whose sessions are active now. This list represents
+ * active sessions regardless of whether they're {@link MediaSession2} or
+ * {@link MediaSessionService2}.
+ *
+ * @return list of Tokens
+ * @hide
+ */
+ // TODO(jaewan): Unhide
+ // TODO(jaewan): Protect this with permission.
+ // TODO(jaewna): Add listener for change in lists.
+ public List<SessionToken> getActiveSessionTokens() {
+ try {
+ List<Bundle> bundles = mService.getSessionTokens(
+ /* activeSessionOnly */ true, /* sessionServiceOnly */ false);
+ return toTokenList(bundles);
+ } catch (RemoteException e) {
+ Log.wtf(TAG, "Cannot communicate with the service.", e);
+ return Collections.emptyList();
+ }
+ }
+
+ /**
+ * Get {@link List} of {@link SessionToken} for {@link MediaSessionService2} regardless of their
+ * activeness. This list represents media apps that support background playback.
+ *
+ * @return list of Tokens
+ * @hide
+ */
+ // TODO(jaewan): Unhide
+ // TODO(jaewna): Add listener for change in lists.
+ public List<SessionToken> getSessionServiceTokens() {
+ try {
+ List<Bundle> bundles = mService.getSessionTokens(
+ /* activeSessionOnly */ false, /* sessionServiceOnly */ true);
+ return toTokenList(bundles);
+ } catch (RemoteException e) {
+ Log.wtf(TAG, "Cannot communicate with the service.", e);
+ return Collections.emptyList();
+ }
+ }
+
+ /**
+ * Get all {@link SessionToken}s. This is the combined list of {@link #getActiveSessionTokens()}
+ * and {@link #getSessionServiceTokens}.
+ *
+ * @return list of Tokens
+ * @see #getActiveSessionTokens
+ * @see #getSessionServiceTokens
+ * @hide
+ */
+ // TODO(jaewan): Unhide
+ // TODO(jaewan): Protect this with permission.
+ // TODO(jaewna): Add listener for change in lists.
+ public List<SessionToken> getAllSessionTokens() {
+ try {
+ List<Bundle> bundles = mService.getSessionTokens(
+ /* activeSessionOnly */ false, /* sessionServiceOnly */ false);
+ return toTokenList(bundles);
+ } catch (RemoteException e) {
+ Log.wtf(TAG, "Cannot communicate with the service.", e);
+ return Collections.emptyList();
+ }
+ }
+
+ private static List<SessionToken> toTokenList(List<Bundle> bundles) {
+ List<SessionToken> tokens = new ArrayList<>();
+ if (bundles != null) {
+ for (int i = 0; i < bundles.size(); i++) {
+ SessionToken token = SessionToken.fromBundle(bundles.get(i));
+ if (token != null) {
+ tokens.add(token);
+ }
+ }
+ }
+ return tokens;
+ }
+
+ /**
* Check if the global priority session is currently active. This can be
* used to decide if media keys should be sent to the session or to the app.
*
diff --git a/media/java/android/media/update/MediaController2Provider.java b/media/java/android/media/update/MediaController2Provider.java
new file mode 100644
index 0000000..b15d6db
--- /dev/null
+++ b/media/java/android/media/update/MediaController2Provider.java
@@ -0,0 +1,28 @@
+/*
+ * 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.media.SessionToken;
+
+/**
+ * @hide
+ */
+public interface MediaController2Provider extends MediaPlayerBaseProvider {
+ void release_impl();
+ SessionToken getSessionToken_impl();
+ boolean isConnected_impl();
+}
diff --git a/media/java/android/media/update/MediaPlayerBaseProvider.java b/media/java/android/media/update/MediaPlayerBaseProvider.java
new file mode 100644
index 0000000..5b13e74
--- /dev/null
+++ b/media/java/android/media/update/MediaPlayerBaseProvider.java
@@ -0,0 +1,36 @@
+/*
+ * 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.media.MediaPlayerBase;
+import android.media.session.PlaybackState;
+import android.os.Handler;
+
+/**
+ * @hide
+ */
+public interface MediaPlayerBaseProvider {
+ void play_impl();
+ void pause_impl();
+ void stop_impl();
+ void skipToPrevious_impl();
+ void skipToNext_impl();
+
+ PlaybackState getPlaybackState_impl();
+ void addPlaybackListener_impl(MediaPlayerBase.PlaybackListener listener, Handler handler);
+ void removePlaybackListener_impl(MediaPlayerBase.PlaybackListener listener);
+}
diff --git a/media/java/android/media/update/MediaSession2Provider.java b/media/java/android/media/update/MediaSession2Provider.java
new file mode 100644
index 0000000..36fd182
--- /dev/null
+++ b/media/java/android/media/update/MediaSession2Provider.java
@@ -0,0 +1,44 @@
+/*
+ * 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.media.MediaPlayerBase;
+import android.media.MediaSession2.ControllerInfo;
+import android.media.SessionToken;
+
+import java.util.List;
+
+/**
+ * @hide
+ */
+public interface MediaSession2Provider extends MediaPlayerBaseProvider {
+ void setPlayer_impl(MediaPlayerBase player) throws IllegalArgumentException;
+ MediaPlayerBase getPlayer_impl();
+ SessionToken getToken_impl();
+ List<ControllerInfo> getConnectedControllers_impl();
+
+ /**
+ * @hide
+ */
+ interface ControllerInfoProvider {
+ String getPackageName_impl();
+ int getUid_impl();
+ boolean isTrusted_impl();
+ int hashCode_impl();
+ boolean equals_impl(ControllerInfoProvider obj);
+ }
+}
diff --git a/media/java/android/media/update/MediaSessionService2Provider.java b/media/java/android/media/update/MediaSessionService2Provider.java
new file mode 100644
index 0000000..1174915
--- /dev/null
+++ b/media/java/android/media/update/MediaSessionService2Provider.java
@@ -0,0 +1,36 @@
+/*
+ * 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.content.Intent;
+import android.media.MediaSession2;
+import android.media.MediaSessionService2.MediaNotification;
+import android.media.session.PlaybackState;
+import android.os.Handler;
+import android.os.IBinder;
+
+/**
+ * @hide
+ */
+public interface MediaSessionService2Provider {
+ MediaSession2 getSession_impl();
+ MediaNotification onUpdateNotification_impl(PlaybackState state);
+
+ // Service
+ void onCreate_impl();
+ IBinder onBind_impl(Intent intent);
+}
diff --git a/media/java/android/media/update/StaticProvider.java b/media/java/android/media/update/StaticProvider.java
index 1a0df52..91c9c66 100644
--- a/media/java/android/media/update/StaticProvider.java
+++ b/media/java/android/media/update/StaticProvider.java
@@ -17,11 +17,19 @@
package android.media.update;
import android.annotation.Nullable;
-import android.annotation.SystemApi;
+import android.content.Context;
+import android.media.IMediaSession2Callback;
+import android.media.MediaController2;
+import android.media.MediaPlayerBase;
+import android.media.MediaSession2;
+import android.media.MediaSessionService2;
+import android.media.SessionToken;
import android.util.AttributeSet;
import android.widget.MediaControlView2;
import android.widget.VideoView2;
+import java.util.concurrent.Executor;
+
/**
* Interface for connecting the public API to an updatable implementation.
*
@@ -37,4 +45,15 @@
VideoView2Provider createVideoView2(
VideoView2 instance, ViewProvider superProvider,
@Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes);
+
+ MediaSession2Provider createMediaSession2(MediaSession2 mediaSession2, Context context,
+ MediaPlayerBase player, String id, MediaSession2.SessionCallback callback);
+ MediaSession2Provider.ControllerInfoProvider createMediaSession2ControllerInfoProvider(
+ MediaSession2.ControllerInfo instance, Context context, int uid, int pid,
+ String packageName, IMediaSession2Callback callback);
+ MediaController2Provider createMediaController2(
+ MediaController2 instance, Context context, SessionToken token,
+ MediaController2.ControllerCallback callback, Executor executor);
+ MediaSessionService2Provider createMediaSessionService2(
+ MediaSessionService2 instance);
}
diff --git a/services/core/java/com/android/server/media/MediaSession2Record.java b/services/core/java/com/android/server/media/MediaSession2Record.java
new file mode 100644
index 0000000..b25eaa7
--- /dev/null
+++ b/services/core/java/com/android/server/media/MediaSession2Record.java
@@ -0,0 +1,181 @@
+/*
+ * 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 com.android.server.media;
+
+import android.annotation.CallSuper;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.Context;
+import android.media.IMediaSession2;
+import android.media.MediaController2;
+import android.media.MediaSession2;
+import android.media.SessionToken;
+import android.os.Handler;
+import android.os.Looper;
+import android.util.Log;
+import java.util.concurrent.Executor;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+/**
+ * Records a {@link MediaSession2} and holds {@link MediaController2}.
+ * <p>
+ * Owner of this object should handle synchronization.
+ */
+class MediaSession2Record {
+ interface SessionDestroyedListener {
+ void onSessionDestroyed(MediaSession2Record record);
+ }
+
+ private static final String TAG = "Session2Record";
+ private static final boolean DEBUG = true; // TODO(jaewan): Change
+
+ private final Context mContext;
+ private final SessionDestroyedListener mSessionDestroyedListener;
+
+ // TODO(jaewan): Replace these with the mContext.getMainExecutor()
+ private final Handler mMainHandler;
+ private final Executor mMainExecutor;
+
+ private MediaController2 mController;
+ private ControllerCallback mControllerCallback;
+
+ private int mSessionPid;
+
+ /**
+ * Constructor
+ */
+ public MediaSession2Record(@NonNull Context context,
+ @NonNull SessionDestroyedListener listener) {
+ mContext = context;
+ mSessionDestroyedListener = listener;
+
+ mMainHandler = new Handler(Looper.getMainLooper());
+ mMainExecutor = (runnable) -> {
+ mMainHandler.post(runnable);
+ };
+ }
+
+ public int getSessionPid() {
+ return mSessionPid;
+ }
+
+ public Context getContext() {
+ return mContext;
+ }
+
+ @CallSuper
+ public void onSessionDestroyed() {
+ if (mController != null) {
+ mControllerCallback.destroy();
+ mController.release();
+ mController = null;
+ }
+ mSessionPid = 0;
+ }
+
+ /**
+ * Create session token and tell server that session is now active.
+ *
+ * @param sessionPid session's pid
+ * @return a token if successfully set, {@code null} if sanity check fails.
+ */
+ // TODO(jaewan): also add uid for multiuser support
+ @CallSuper
+ public @Nullable
+ SessionToken createSessionToken(int sessionPid, String packageName, String id,
+ IMediaSession2 sessionBinder) {
+ if (mController != null) {
+ if (mSessionPid != sessionPid) {
+ // A package uses the same id for session across the different process.
+ return null;
+ }
+ // If a session becomes inactive and then active again very quickly, previous 'inactive'
+ // may not have delivered yet. Check if it's the case and destroy controller before
+ // creating its session record to prevents getXXTokens() API from returning duplicated
+ // tokens.
+ // TODO(jaewan): Change this. If developer is really creating two sessions with the same
+ // id, this will silently invalidate previous session and no way for
+ // developers to know that.
+ // Instead, keep the list of static session ids from our APIs.
+ // Also change Controller2Impl.onConnectionChanged / getController.
+ // Also clean up ControllerCallback#destroy().
+ if (DEBUG) {
+ Log.d(TAG, "Session is recreated almost immediately. " + this);
+ }
+ onSessionDestroyed();
+ }
+ mController = onCreateMediaController(packageName, id, sessionBinder);
+ mSessionPid = sessionPid;
+ return mController.getSessionToken();
+ }
+
+ /**
+ * Called when session becomes active and needs controller to listen session's activeness.
+ * <p>
+ * Should be overridden by subclasses to create token with its own extra information.
+ */
+ MediaController2 onCreateMediaController(
+ String packageName, String id, IMediaSession2 sessionBinder) {
+ SessionToken token = new SessionToken(
+ SessionToken.TYPE_SESSION, packageName, id, null, sessionBinder);
+ return createMediaController(token);
+ }
+
+ final MediaController2 createMediaController(SessionToken token) {
+ mControllerCallback = new ControllerCallback();
+ return new MediaController2(mContext, token, mControllerCallback, mMainExecutor);
+ }
+
+ /**
+ * @return controller. Note that framework can only call oneway calls.
+ */
+ public SessionToken getToken() {
+ return mController == null ? null : mController.getSessionToken();
+ }
+
+ @Override
+ public String toString() {
+ return getToken() == null
+ ? "Token {null}"
+ : "SessionRecord {pid=" + mSessionPid + ", " + getToken().toString() + "}";
+ }
+
+ private class ControllerCallback extends MediaController2.ControllerCallback {
+ private final AtomicBoolean mIsActive = new AtomicBoolean(true);
+
+ // This is called on the main thread with no lock. So place ensure followings.
+ // 1. Don't touch anything in the parent class that needs synchronization.
+ // All other APIs in the MediaSession2Record assumes that server would use them with
+ // the lock hold.
+ // 2. This can be called after the controller registered is released.
+ @Override
+ public void onDisconnected() {
+ if (!mIsActive.get()) {
+ return;
+ }
+ if (DEBUG) {
+ Log.d(TAG, "onDisconnected, token=" + getToken());
+ }
+ mSessionDestroyedListener.onSessionDestroyed(MediaSession2Record.this);
+ }
+
+ // TODO(jaewan): Remove this API when we revisit createSessionToken()
+ public void destroy() {
+ mIsActive.set(false);
+ }
+ };
+}
diff --git a/services/core/java/com/android/server/media/MediaSessionService.java b/services/core/java/com/android/server/media/MediaSessionService.java
index 06f4f5e..6812778 100644
--- a/services/core/java/com/android/server/media/MediaSessionService.java
+++ b/services/core/java/com/android/server/media/MediaSessionService.java
@@ -28,13 +28,18 @@
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.content.pm.ServiceInfo;
import android.content.pm.UserInfo;
import android.database.ContentObserver;
import android.media.AudioManager;
import android.media.AudioPlaybackConfiguration;
import android.media.AudioSystem;
import android.media.IAudioService;
+import android.media.IMediaSession2;
import android.media.IRemoteVolumeController;
+import android.media.MediaSessionService2;
+import android.media.SessionToken;
import android.media.session.IActiveSessionsListener;
import android.media.session.ICallback;
import android.media.session.IOnMediaKeyListener;
@@ -118,6 +123,24 @@
// better way to handle this.
private IRemoteVolumeController mRvc;
+ // MediaSession2 support
+ // TODO(jaewan): Support multi-user and managed profile.
+ // TODO(jaewan): Make it priority list for handling volume/media key.
+ private final List<MediaSession2Record> mSessions = new ArrayList<>();
+
+ private final MediaSession2Record.SessionDestroyedListener mSessionDestroyedListener =
+ (MediaSession2Record record) -> {
+ synchronized (mLock) {
+ if (DEBUG) {
+ Log.d(TAG, record.toString() + " becomes inactive");
+ }
+ record.onSessionDestroyed();
+ if (!(record instanceof MediaSessionService2Record)) {
+ mSessions.remove(record);
+ }
+ }
+ };
+
public MediaSessionService(Context context) {
super(context);
mSessionManagerImpl = new SessionManagerImpl();
@@ -158,6 +181,11 @@
PackageManager.FEATURE_LEANBACK);
updateUser();
+
+ // TODO(jaewan): Query per users
+ // TODO(jaewan): Add listener to know changes in list of services.
+ // Refer TvInputManagerService.registerBroadcastReceivers()
+ buildMediaSessionService2List();
}
private IAudioService getAudioService() {
@@ -411,6 +439,64 @@
mHandler.postSessionsChanged(session.getUserId());
}
+ private void buildMediaSessionService2List() {
+ if (DEBUG) {
+ Log.d(TAG, "buildMediaSessionService2List");
+ }
+
+ // TODO(jaewan): Query per users.
+ List<ResolveInfo> services = getContext().getPackageManager().queryIntentServices(
+ new Intent(MediaSessionService2.SERVICE_INTERFACE),
+ PackageManager.GET_META_DATA);
+ synchronized (mLock) {
+ mSessions.clear();
+ if (services == null) {
+ return;
+ }
+ for (int i = 0; i < services.size(); i++) {
+ if (services.get(i) == null || services.get(i).serviceInfo == null) {
+ continue;
+ }
+ ServiceInfo serviceInfo = services.get(i).serviceInfo;
+ String id = (serviceInfo.metaData != null) ? serviceInfo.metaData.getString(
+ MediaSessionService2.SERVICE_META_DATA) : null;
+ // Do basic sanity check
+ // TODO(jaewan): also santity check if it's protected with the system|privileged
+ // permission
+ boolean conflict = (getSessionRecordLocked(serviceInfo.name, id) != null);
+ if (conflict) {
+ Log.w(TAG, serviceInfo.packageName + " contains multiple"
+ + " MediaSessionService2s declared in the manifest with"
+ + " the same ID=" + id + ". Ignoring "
+ + serviceInfo.packageName + "/" + serviceInfo.name);
+ } else {
+ MediaSessionService2Record record =
+ new MediaSessionService2Record(getContext(), mSessionDestroyedListener,
+ SessionToken.TYPE_SESSION_SERVICE,
+ serviceInfo.packageName, serviceInfo.name, id);
+ mSessions.add(record);
+ }
+ }
+ }
+ if (DEBUG) {
+ Log.d(TAG, "Found " + mSessions.size() + " session services");
+ for (int i = 0; i < mSessions.size(); i++) {
+ Log.d(TAG, " " + mSessions.get(i).getToken());
+ }
+ }
+ }
+
+ MediaSession2Record getSessionRecordLocked(String packageName, String id) {
+ for (int i = 0; i < mSessions.size(); i++) {
+ MediaSession2Record record = mSessions.get(i);
+ if (record.getToken().getPackageName().equals(packageName)
+ && record.getToken().getId().equals(id)) {
+ return record;
+ }
+ }
+ return null;
+ }
+
private void enforcePackageName(String packageName, int uid) {
if (TextUtils.isEmpty(packageName)) {
throw new IllegalArgumentException("packageName may not be empty");
@@ -1312,6 +1398,57 @@
}
}
+ @Override
+ public Bundle createSessionToken(String sessionPackage, String id,
+ IMediaSession2 sessionBinder) throws RemoteException {
+ int uid = Binder.getCallingUid();
+ int pid = Binder.getCallingPid();
+
+ MediaSession2Record record;
+ SessionToken token;
+ // TODO(jaewan): Add sanity check for the token if calling package is from uid.
+ synchronized (mLock) {
+ record = getSessionRecordLocked(sessionPackage, id);
+ if (record == null) {
+ record = new MediaSession2Record(getContext(), mSessionDestroyedListener);
+ mSessions.add(record);
+ }
+ token = record.createSessionToken(pid, sessionPackage, id, sessionBinder);
+ if (token == null) {
+ Log.d(TAG, "failed to create session token for " + sessionPackage
+ + " from pid=" + pid + ". Previously " + record);
+ } else {
+ Log.d(TAG, "session " + token + " is created");
+ }
+ }
+ return token == null ? null : token.toBundle();
+ }
+
+ // TODO(jaewan): Protect this API with permission
+ // TODO(jaewan): Add listeners for change in operations..
+ @Override
+ public List<Bundle> getSessionTokens(boolean activeSessionOnly,
+ boolean sessionServiceOnly) throws RemoteException {
+ List<Bundle> tokens = new ArrayList<>();
+ synchronized (mLock) {
+ for (int i = 0; i < mSessions.size(); i++) {
+ MediaSession2Record record = mSessions.get(i);
+ boolean isSessionService = (record instanceof MediaSessionService2Record);
+ boolean isActive = record.getSessionPid() != 0;
+ if ((!activeSessionOnly && isSessionService)
+ || (!sessionServiceOnly && isActive)) {
+ SessionToken token = record.getToken();
+ if (token != null) {
+ tokens.add(token.toBundle());
+ } else {
+ Log.wtf(TAG, "Null token for record=" + record);
+ }
+ }
+ }
+ }
+ return tokens;
+ }
+
private int verifySessionsRequest(ComponentName componentName, int userId, final int pid,
final int uid) {
String packageName = null;
diff --git a/services/core/java/com/android/server/media/MediaSessionService2Record.java b/services/core/java/com/android/server/media/MediaSessionService2Record.java
new file mode 100644
index 0000000..bd97dbc
--- /dev/null
+++ b/services/core/java/com/android/server/media/MediaSessionService2Record.java
@@ -0,0 +1,65 @@
+/*
+ * 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 com.android.server.media;
+
+import android.content.Context;
+import android.media.IMediaSession2;
+import android.media.MediaController2;
+import android.media.SessionToken;
+import android.media.MediaSessionService2;
+
+/**
+ * Records a {@link MediaSessionService2}.
+ * <p>
+ * Owner of this object should handle synchronization.
+ */
+class MediaSessionService2Record extends MediaSession2Record {
+ private static final boolean DEBUG = true; // TODO(jaewan): Modify
+ private static final String TAG = "SessionService2Record";
+
+ private final int mType;
+ private final String mServiceName;
+ private final SessionToken mToken;
+
+ public MediaSessionService2Record(Context context,
+ SessionDestroyedListener sessionDestroyedListener, int type,
+ String packageName, String serviceName, String id) {
+ super(context, sessionDestroyedListener);
+ mType = type;
+ mServiceName = serviceName;
+ mToken = new SessionToken(mType, packageName, id, mServiceName, null);
+ }
+
+ /**
+ * Overriden to change behavior of
+ * {@link #createSessionToken(int, String, String, IMediaSession2)}}.
+ */
+ @Override
+ MediaController2 onCreateMediaController(
+ String packageName, String id, IMediaSession2 sessionBinder) {
+ SessionToken token = new SessionToken(mType, packageName, id, mServiceName, sessionBinder);
+ return createMediaController(token);
+ }
+
+ /**
+ * @return token with no session binder information.
+ */
+ @Override
+ public SessionToken getToken() {
+ return mToken;
+ }
+}