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>
+ * &lt;service android:name="component_name_of_your_implementation" &gt;
+ *   &lt;intent-filter&gt;
+ *     &lt;action android:name="android.media.session.MediaSessionService2" /&gt;
+ *   &lt;/intent-filter&gt;
+ * &lt;/service&gt;</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>
+ * &lt;service android:name="component_name_of_your_implementation" &gt;
+ *   &lt;intent-filter&gt;
+ *     &lt;action android:name="android.media.session.MediaSessionService2" /&gt;
+ *   &lt;/intent-filter&gt;
+ *   &lt;meta-data android:name="android.media.session"
+ *       android:value="session_id"/&gt;
+ * &lt;/service&gt;</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;
+    }
+}