Merge changes I6bcc87c6,If633e677

* changes:
  Revert "audio policy: refactor audio record APIs"
  Revert "audio flinger: move record permission checks to audio policy service"
diff --git a/include/media/MicrophoneInfo.h b/include/media/MicrophoneInfo.h
new file mode 100644
index 0000000..b0199d4
--- /dev/null
+++ b/include/media/MicrophoneInfo.h
@@ -0,0 +1,249 @@
+/*
+ * Copyright (C) 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.
+ */
+
+#ifndef ANDROID_MICROPHONE_INFO_H
+#define ANDROID_MICROPHONE_INFO_H
+
+#include <binder/Parcel.h>
+#include <binder/Parcelable.h>
+#include <system/audio.h>
+#include <utils/String16.h>
+#include <utils/Vector.h>
+
+namespace android {
+namespace media {
+
+#define RETURN_IF_FAILED(calledOnce)                                     \
+    {                                                                    \
+        status_t returnStatus = calledOnce;                              \
+        if (returnStatus) {                                              \
+            ALOGE("Failed at %s:%d (%s)", __FILE__, __LINE__, __func__); \
+            return returnStatus;                                         \
+         }                                                               \
+    }
+
+class MicrophoneInfo : public Parcelable {
+public:
+    MicrophoneInfo() = default;
+    MicrophoneInfo(const MicrophoneInfo& microphoneInfo) = default;
+    MicrophoneInfo(audio_microphone_characteristic_t& characteristic) {
+        mDeviceId = String16(&characteristic.device_id[0]);
+        mPortId = characteristic.id;
+        mType = characteristic.type;
+        mAddress = String16(&characteristic.address[0]);
+        mDeviceLocation = characteristic.location;
+        mDeviceGroup = characteristic.group;
+        mIndexInTheGroup = characteristic.index_in_the_group;
+        mGeometricLocation.push_back(characteristic.geometric_location.x);
+        mGeometricLocation.push_back(characteristic.geometric_location.y);
+        mGeometricLocation.push_back(characteristic.geometric_location.z);
+        mOrientation.push_back(characteristic.orientation.x);
+        mOrientation.push_back(characteristic.orientation.y);
+        mOrientation.push_back(characteristic.orientation.z);
+        Vector<float> frequencies;
+        Vector<float> responses;
+        for (size_t i = 0; i < characteristic.num_frequency_responses; i++) {
+            frequencies.push_back(characteristic.frequency_responses[0][i]);
+            responses.push_back(characteristic.frequency_responses[1][i]);
+        }
+        mFrequencyResponses.push_back(frequencies);
+        mFrequencyResponses.push_back(responses);
+        for (size_t i = 0; i < AUDIO_CHANNEL_COUNT_MAX; i++) {
+            mChannelMapping.push_back(characteristic.channel_mapping[i]);
+        }
+        mSensitivity = characteristic.sensitivity;
+        mMaxSpl = characteristic.max_spl;
+        mMinSpl = characteristic.min_spl;
+        mDirectionality = characteristic.directionality;
+    }
+
+    virtual ~MicrophoneInfo() = default;
+
+    virtual status_t writeToParcel(Parcel* parcel) const {
+        RETURN_IF_FAILED(parcel->writeString16(mDeviceId));
+        RETURN_IF_FAILED(parcel->writeInt32(mPortId));
+        RETURN_IF_FAILED(parcel->writeUint32(mType));
+        RETURN_IF_FAILED(parcel->writeString16(mAddress));
+        RETURN_IF_FAILED(parcel->writeInt32(mDeviceLocation));
+        RETURN_IF_FAILED(parcel->writeInt32(mDeviceGroup));
+        RETURN_IF_FAILED(parcel->writeInt32(mIndexInTheGroup));
+        RETURN_IF_FAILED(writeFloatVector(parcel, mGeometricLocation));
+        RETURN_IF_FAILED(writeFloatVector(parcel, mOrientation));
+        if (mFrequencyResponses.size() != 2) {
+            return BAD_VALUE;
+        }
+        for (size_t i = 0; i < mFrequencyResponses.size(); i++) {
+            RETURN_IF_FAILED(parcel->writeInt32(mFrequencyResponses[i].size()));
+            RETURN_IF_FAILED(writeFloatVector(parcel, mFrequencyResponses[i]));
+        }
+        std::vector<int> channelMapping;
+        for (size_t i = 0; i < mChannelMapping.size(); ++i) {
+            channelMapping.push_back(mChannelMapping[i]);
+        }
+        RETURN_IF_FAILED(parcel->writeInt32Vector(channelMapping));
+        RETURN_IF_FAILED(parcel->writeFloat(mSensitivity));
+        RETURN_IF_FAILED(parcel->writeFloat(mMaxSpl));
+        RETURN_IF_FAILED(parcel->writeFloat(mMinSpl));
+        RETURN_IF_FAILED(parcel->writeInt32(mDirectionality));
+        return OK;
+    }
+
+    virtual status_t readFromParcel(const Parcel* parcel) {
+        RETURN_IF_FAILED(parcel->readString16(&mDeviceId));
+        RETURN_IF_FAILED(parcel->readInt32(&mPortId));
+        RETURN_IF_FAILED(parcel->readUint32(&mType));
+        RETURN_IF_FAILED(parcel->readString16(&mAddress));
+        RETURN_IF_FAILED(parcel->readInt32(&mDeviceLocation));
+        RETURN_IF_FAILED(parcel->readInt32(&mDeviceGroup));
+        RETURN_IF_FAILED(parcel->readInt32(&mIndexInTheGroup));
+        RETURN_IF_FAILED(readFloatVector(parcel, &mGeometricLocation, 3));
+        RETURN_IF_FAILED(readFloatVector(parcel, &mOrientation, 3));
+        int32_t frequenciesNum;
+        RETURN_IF_FAILED(parcel->readInt32(&frequenciesNum));
+        Vector<float> frequencies;
+        RETURN_IF_FAILED(readFloatVector(parcel, &frequencies, frequenciesNum));
+        int32_t responsesNum;
+        RETURN_IF_FAILED(parcel->readInt32(&responsesNum));
+        Vector<float> responses;
+        RETURN_IF_FAILED(readFloatVector(parcel, &responses, responsesNum));
+        if (frequencies.size() != responses.size()) {
+            return BAD_VALUE;
+        }
+        mFrequencyResponses.push_back(frequencies);
+        mFrequencyResponses.push_back(responses);
+        std::vector<int> channelMapping;
+        status_t result = parcel->readInt32Vector(&channelMapping);
+        if (result != OK) {
+            return result;
+        }
+        if (channelMapping.size() != AUDIO_CHANNEL_COUNT_MAX) {
+            return BAD_VALUE;
+        }
+        for (size_t i = 0; i < channelMapping.size(); i++) {
+            mChannelMapping.push_back(channelMapping[i]);
+        }
+        RETURN_IF_FAILED(parcel->readFloat(&mSensitivity));
+        RETURN_IF_FAILED(parcel->readFloat(&mMaxSpl));
+        RETURN_IF_FAILED(parcel->readFloat(&mMinSpl));
+        RETURN_IF_FAILED(parcel->readInt32(&mDirectionality));
+        return OK;
+    }
+
+    String16 getDeviceId() const {
+        return mDeviceId;
+    }
+
+    int getPortId() const {
+        return mPortId;
+    }
+
+    unsigned int getType() const {
+        return mType;
+    }
+
+    String16 getAddress() const {
+        return mAddress;
+    }
+
+    int getDeviceLocation() const {
+        return mDeviceLocation;
+    }
+
+    int getDeviceGroup() const {
+        return mDeviceGroup;
+    }
+
+    int getIndexInTheGroup() const {
+        return mIndexInTheGroup;
+    }
+
+    const Vector<float>& getGeometricLocation() const {
+        return mGeometricLocation;
+    }
+
+    const Vector<float>& getOrientation() const {
+        return mOrientation;
+    }
+
+    const Vector<Vector<float>>& getFrequencyResponses() const {
+        return mFrequencyResponses;
+    }
+
+    const Vector<int>& getChannelMapping() const {
+        return mChannelMapping;
+    }
+
+    float getSensitivity() const {
+        return mSensitivity;
+    }
+
+    float getMaxSpl() const {
+        return mMaxSpl;
+    }
+
+    float getMinSpl() const {
+        return mMinSpl;
+    }
+
+    int getDirectionality() const {
+        return mDirectionality;
+    }
+
+private:
+    status_t readFloatVector(
+            const Parcel* parcel, Vector<float> *vectorPtr, size_t defaultLength) {
+        std::unique_ptr<std::vector<float>> v;
+        status_t result = parcel->readFloatVector(&v);
+        if (result != OK) return result;
+        vectorPtr->clear();
+        if (v.get() != nullptr) {
+            for (const auto& iter : *v) {
+                vectorPtr->push_back(iter);
+            }
+        } else {
+            vectorPtr->resize(defaultLength);
+        }
+        return OK;
+    }
+    status_t writeFloatVector(Parcel* parcel, const Vector<float>& vector) const {
+        std::vector<float> v;
+        for (size_t i = 0; i < vector.size(); i++) {
+            v.push_back(vector[i]);
+        }
+        return parcel->writeFloatVector(v);
+    }
+
+    String16 mDeviceId;
+    int32_t mPortId;
+    uint32_t mType;
+    String16 mAddress;
+    int32_t mDeviceLocation;
+    int32_t mDeviceGroup;
+    int32_t mIndexInTheGroup;
+    Vector<float> mGeometricLocation;
+    Vector<float> mOrientation;
+    Vector<Vector<float>> mFrequencyResponses;
+    Vector<int> mChannelMapping;
+    float mSensitivity;
+    float mMaxSpl;
+    float mMinSpl;
+    int32_t mDirectionality;
+};
+
+} // namespace media
+} // namespace android
+
+#endif
diff --git a/media/extractors/mp4/MPEG4Extractor.h b/media/extractors/mp4/MPEG4Extractor.h
index 8bfecaa..35c5321 100644
--- a/media/extractors/mp4/MPEG4Extractor.h
+++ b/media/extractors/mp4/MPEG4Extractor.h
@@ -22,6 +22,7 @@
 
 #include <media/DataSource.h>
 #include <media/MediaExtractor.h>
+#include <media/stagefright/foundation/AString.h>
 #include <utils/List.h>
 #include <utils/Vector.h>
 #include <utils/String8.h>
diff --git a/media/extractors/mpeg2/MPEG2TSExtractor.cpp b/media/extractors/mpeg2/MPEG2TSExtractor.cpp
index fef4d30..5beaeda 100644
--- a/media/extractors/mpeg2/MPEG2TSExtractor.cpp
+++ b/media/extractors/mpeg2/MPEG2TSExtractor.cpp
@@ -202,9 +202,7 @@
             break;
         }
         if (!haveVideo) {
-            sp<AnotherPacketSource> impl =
-                (AnotherPacketSource *)mParser->getSource(
-                        ATSParser::VIDEO).get();
+            sp<AnotherPacketSource> impl = mParser->getSource(ATSParser::VIDEO);
 
             if (impl != NULL) {
                 sp<MetaData> format = impl->getFormat();
@@ -220,9 +218,7 @@
         }
 
         if (!haveAudio) {
-            sp<AnotherPacketSource> impl =
-                (AnotherPacketSource *)mParser->getSource(
-                        ATSParser::AUDIO).get();
+            sp<AnotherPacketSource> impl = mParser->getSource(ATSParser::AUDIO);
 
             if (impl != NULL) {
                 sp<MetaData> format = impl->getFormat();
@@ -261,10 +257,8 @@
     off64_t size;
     if (mDataSource->getSize(&size) == OK && (haveAudio || haveVideo)) {
         sp<AnotherPacketSource> impl = haveVideo
-                ? (AnotherPacketSource *)mParser->getSource(
-                        ATSParser::VIDEO).get()
-                : (AnotherPacketSource *)mParser->getSource(
-                        ATSParser::AUDIO).get();
+                ? mParser->getSource(ATSParser::VIDEO)
+                : mParser->getSource(ATSParser::AUDIO);
         size_t prevSyncSize = 1;
         int64_t durationUs = -1;
         List<int64_t> durations;
@@ -420,8 +414,7 @@
                 ev.reset();
 
                 int64_t firstTimeUs;
-                sp<AnotherPacketSource> src =
-                    (AnotherPacketSource *)mParser->getSource(type).get();
+                sp<AnotherPacketSource> src = mParser->getSource(type);
                 if (src == NULL || src->nextBufferTime(&firstTimeUs) != OK) {
                     continue;
                 }
@@ -449,7 +442,7 @@
         if (!allDurationsFound) {
             allDurationsFound = true;
             for (auto t: {ATSParser::VIDEO, ATSParser::AUDIO}) {
-                sp<AnotherPacketSource> src = (AnotherPacketSource *)mParser->getSource(t).get();
+                sp<AnotherPacketSource> src = mParser->getSource(t);
                 if (src == NULL) {
                     continue;
                 }
diff --git a/media/libaudioclient/AudioSystem.cpp b/media/libaudioclient/AudioSystem.cpp
index 3bb09d2..9dfb514 100644
--- a/media/libaudioclient/AudioSystem.cpp
+++ b/media/libaudioclient/AudioSystem.cpp
@@ -1278,6 +1278,13 @@
     return aps->getStreamVolumeDB(stream, index, device);
 }
 
+status_t AudioSystem::getMicrophones(std::vector<media::MicrophoneInfo> *microphones)
+{
+    const sp<IAudioFlinger>& af = AudioSystem::get_audio_flinger();
+    if (af == 0) return PERMISSION_DENIED;
+    return af->getMicrophones(microphones);
+}
+
 // ---------------------------------------------------------------------------
 
 int AudioSystem::AudioPolicyServiceClient::addAudioPortCallback(
diff --git a/media/libaudioclient/IAudioFlinger.cpp b/media/libaudioclient/IAudioFlinger.cpp
index ae9c96f..b6c98cc 100644
--- a/media/libaudioclient/IAudioFlinger.cpp
+++ b/media/libaudioclient/IAudioFlinger.cpp
@@ -85,6 +85,7 @@
     GET_AUDIO_HW_SYNC_FOR_SESSION,
     SYSTEM_READY,
     FRAME_COUNT_HAL,
+    LIST_MICROPHONES,
 };
 
 #define MAX_ITEMS_PER_LIST 1024
@@ -842,6 +843,18 @@
         }
         return reply.readInt64();
     }
+    virtual status_t getMicrophones(std::vector<media::MicrophoneInfo> *microphones)
+    {
+        Parcel data, reply;
+        data.writeInterfaceToken(IAudioFlinger::getInterfaceDescriptor());
+        status_t status = remote()->transact(LIST_MICROPHONES, data, &reply);
+        if (status != NO_ERROR ||
+                (status = (status_t)reply.readInt32()) != NO_ERROR) {
+            return status;
+        }
+        status = reply.readParcelableVector(microphones);
+        return status;
+    }
 };
 
 IMPLEMENT_META_INTERFACE(AudioFlinger, "android.media.IAudioFlinger");
@@ -1407,6 +1420,16 @@
             reply->writeInt64( frameCountHAL((audio_io_handle_t) data.readInt32()) );
             return NO_ERROR;
         } break;
+        case LIST_MICROPHONES: {
+            CHECK_INTERFACE(IAudioFlinger, data, reply);
+            std::vector<media::MicrophoneInfo> microphones;
+            status_t status = getMicrophones(&microphones);
+            reply->writeInt32(status);
+            if (status == NO_ERROR) {
+                reply->writeParcelableVector(microphones);
+            }
+            return NO_ERROR;
+        }
         default:
             return BBinder::onTransact(code, data, reply, flags);
     }
diff --git a/media/libaudioclient/aidl/android/media/MicrophoneInfo.aidl b/media/libaudioclient/aidl/android/media/MicrophoneInfo.aidl
new file mode 100644
index 0000000..d6e46cb
--- /dev/null
+++ b/media/libaudioclient/aidl/android/media/MicrophoneInfo.aidl
@@ -0,0 +1,19 @@
+/*
+ * 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;
+
+parcelable MicrophoneInfo cpp_header "media/MicrophoneInfo.h";
diff --git a/media/libaudioclient/include/media/AudioSystem.h b/media/libaudioclient/include/media/AudioSystem.h
index f7c3d03..3c8e7bc 100644
--- a/media/libaudioclient/include/media/AudioSystem.h
+++ b/media/libaudioclient/include/media/AudioSystem.h
@@ -23,11 +23,13 @@
 #include <media/AudioIoDescriptor.h>
 #include <media/IAudioFlingerClient.h>
 #include <media/IAudioPolicyServiceClient.h>
+#include <media/MicrophoneInfo.h>
 #include <system/audio.h>
 #include <system/audio_effect.h>
 #include <system/audio_policy.h>
 #include <utils/Errors.h>
 #include <utils/Mutex.h>
+#include <vector>
 
 namespace android {
 
@@ -340,6 +342,8 @@
     static float    getStreamVolumeDB(
             audio_stream_type_t stream, int index, audio_devices_t device);
 
+    static status_t getMicrophones(std::vector<media::MicrophoneInfo> *microphones);
+
     // ----------------------------------------------------------------------------
 
     class AudioPortCallback : public RefBase
diff --git a/media/libaudioclient/include/media/IAudioFlinger.h b/media/libaudioclient/include/media/IAudioFlinger.h
index e8d405b..e6bf72f 100644
--- a/media/libaudioclient/include/media/IAudioFlinger.h
+++ b/media/libaudioclient/include/media/IAudioFlinger.h
@@ -35,6 +35,8 @@
 #include <media/IEffect.h>
 #include <media/IEffectClient.h>
 #include <utils/String8.h>
+#include <media/MicrophoneInfo.h>
+#include <vector>
 
 #include "android/media/IAudioRecord.h"
 
@@ -486,6 +488,9 @@
 
     // Returns the number of frames per audio HAL buffer.
     virtual size_t frameCountHAL(audio_io_handle_t ioHandle) const = 0;
+
+    /* List available microphones and their characteristics */
+    virtual status_t getMicrophones(std::vector<media::MicrophoneInfo> *microphones) = 0;
 };
 
 
diff --git a/media/libheif/HeifDecoderImpl.cpp b/media/libheif/HeifDecoderImpl.cpp
index a63a2df..2dfbdca 100644
--- a/media/libheif/HeifDecoderImpl.cpp
+++ b/media/libheif/HeifDecoderImpl.cpp
@@ -22,6 +22,7 @@
 #include <stdio.h>
 
 #include <binder/IMemory.h>
+#include <binder/MemoryDealer.h>
 #include <drm/drm_framework_common.h>
 #include <media/IDataSource.h>
 #include <media/mediametadataretriever.h>
diff --git a/media/libmedia/MediaPlayer2Factory.cpp b/media/libmedia/MediaPlayer2Factory.cpp
index df567ce..ac115f6 100644
--- a/media/libmedia/MediaPlayer2Factory.cpp
+++ b/media/libmedia/MediaPlayer2Factory.cpp
@@ -34,9 +34,17 @@
 namespace android {
 
 Mutex MediaPlayer2Factory::sLock;
-MediaPlayer2Factory::tFactoryMap MediaPlayer2Factory::sFactoryMap;
+MediaPlayer2Factory::tFactoryMap *MediaPlayer2Factory::sFactoryMap;
 bool MediaPlayer2Factory::sInitComplete = false;
 
+// static
+bool MediaPlayer2Factory::ensureInit_l() {
+    if (sFactoryMap == NULL) {
+        sFactoryMap = new (std::nothrow) tFactoryMap();
+    }
+    return (sFactoryMap != NULL);
+}
+
 status_t MediaPlayer2Factory::registerFactory_l(IFactory* factory,
                                                 player2_type type) {
     if (NULL == factory) {
@@ -45,13 +53,17 @@
         return BAD_VALUE;
     }
 
-    if (sFactoryMap.indexOfKey(type) >= 0) {
+    if (!ensureInit_l()) {
+        return NO_INIT;
+    }
+
+    if (sFactoryMap->indexOfKey(type) >= 0) {
         ALOGE("Failed to register MediaPlayer2Factory of type %d, type is"
               " already registered.", type);
         return ALREADY_EXISTS;
     }
 
-    if (sFactoryMap.add(type, factory) < 0) {
+    if (sFactoryMap->add(type, factory) < 0) {
         ALOGE("Failed to register MediaPlayer2Factory of type %d, failed to add"
               " to map.", type);
         return UNKNOWN_ERROR;
@@ -64,31 +76,24 @@
     return PLAYER2_NU_PLAYER2;
 }
 
-status_t MediaPlayer2Factory::registerFactory(IFactory* factory,
-                                              player2_type type) {
-    Mutex::Autolock lock_(&sLock);
-    return registerFactory_l(factory, type);
-}
-
-void MediaPlayer2Factory::unregisterFactory(player2_type type) {
-    Mutex::Autolock lock_(&sLock);
-    sFactoryMap.removeItem(type);
-}
-
 #define GET_PLAYER_TYPE_IMPL(a...)                      \
     Mutex::Autolock lock_(&sLock);                      \
                                                         \
     player2_type ret = PLAYER2_STAGEFRIGHT_PLAYER;      \
     float bestScore = 0.0;                              \
                                                         \
-    for (size_t i = 0; i < sFactoryMap.size(); ++i) {   \
+    if (!ensureInit_l()) {                              \
+        return ret;                                     \
+    }                                                   \
                                                         \
-        IFactory* v = sFactoryMap.valueAt(i);           \
+    for (size_t i = 0; i < sFactoryMap->size(); ++i) {  \
+                                                        \
+        IFactory* v = sFactoryMap->valueAt(i);          \
         float thisScore;                                \
         CHECK(v != NULL);                               \
         thisScore = v->scoreFactory(a, bestScore);      \
         if (thisScore > bestScore) {                    \
-            ret = sFactoryMap.keyAt(i);                 \
+            ret = sFactoryMap->keyAt(i);                \
             bestScore = thisScore;                      \
         }                                               \
     }                                                   \
@@ -133,13 +138,17 @@
     status_t init_result;
     Mutex::Autolock lock_(&sLock);
 
-    if (sFactoryMap.indexOfKey(playerType) < 0) {
+    if (!ensureInit_l()) {
+        return NULL;
+    }
+
+    if (sFactoryMap->indexOfKey(playerType) < 0) {
         ALOGE("Failed to create player object of type %d, no registered"
               " factory", playerType);
         return p;
     }
 
-    factory = sFactoryMap.valueFor(playerType);
+    factory = sFactoryMap->valueFor(playerType);
     CHECK(NULL != factory);
     p = factory->createPlayer(pid);
 
diff --git a/media/libmedia/MediaPlayer2Factory.h b/media/libmedia/MediaPlayer2Factory.h
index 799b5f3..416d241 100644
--- a/media/libmedia/MediaPlayer2Factory.h
+++ b/media/libmedia/MediaPlayer2Factory.h
@@ -50,9 +50,6 @@
         virtual sp<MediaPlayer2Base> createPlayer(pid_t pid) = 0;
     };
 
-    static status_t registerFactory(IFactory* factory,
-                                    player2_type type);
-    static void unregisterFactory(player2_type type);
     static player2_type getPlayerType(const sp<MediaPlayer2Engine>& client,
                                       const char* url);
     static player2_type getPlayerType(const sp<MediaPlayer2Engine>& client,
@@ -76,11 +73,13 @@
 
     MediaPlayer2Factory() { }
 
+    static bool ensureInit_l();
+
     static status_t registerFactory_l(IFactory* factory,
                                       player2_type type);
 
     static Mutex       sLock;
-    static tFactoryMap sFactoryMap;
+    static tFactoryMap *sFactoryMap;
     static bool        sInitComplete;
 
     DISALLOW_EVIL_CONSTRUCTORS(MediaPlayer2Factory);
diff --git a/media/libmedia/nuplayer2/GenericSource.cpp b/media/libmedia/nuplayer2/GenericSource.cpp
index 6d5b14d..094af7e 100644
--- a/media/libmedia/nuplayer2/GenericSource.cpp
+++ b/media/libmedia/nuplayer2/GenericSource.cpp
@@ -26,6 +26,7 @@
 #include <media/DataSource.h>
 #include <media/MediaBufferHolder.h>
 #include <media/IMediaExtractorService.h>
+#include <media/IMediaSource.h>
 #include <media/MediaHTTPService.h>
 #include <media/MediaExtractor.h>
 #include <media/MediaSource.h>
diff --git a/media/libmedia/nuplayer2/GenericSource.h b/media/libmedia/nuplayer2/GenericSource.h
index 0666d27..5a71edb 100644
--- a/media/libmedia/nuplayer2/GenericSource.h
+++ b/media/libmedia/nuplayer2/GenericSource.h
@@ -33,6 +33,7 @@
 struct ARTSPController;
 class DataSource;
 class IDataSource;
+class IMediaSource;
 struct MediaHTTPService;
 struct MediaSource;
 class MediaBuffer;
diff --git a/media/libmediaextractor/include/media/DataSource.h b/media/libmediaextractor/include/media/DataSource.h
index 44f94a0..9925a21 100644
--- a/media/libmediaextractor/include/media/DataSource.h
+++ b/media/libmediaextractor/include/media/DataSource.h
@@ -19,23 +19,15 @@
 #define DATA_SOURCE_H_
 
 #include <sys/types.h>
-#include <media/stagefright/foundation/ADebug.h>
 #include <media/stagefright/MediaErrors.h>
 #include <utils/Errors.h>
-#include <utils/KeyedVector.h>
-#include <utils/List.h>
 #include <utils/RefBase.h>
 #include <utils/threads.h>
 #include <drm/DrmManagerClient.h>
 
 namespace android {
 
-struct AMessage;
-struct AString;
-class  IDataSource;
-struct IMediaHTTPService;
 class String8;
-struct HTTPBase;
 
 class DataSource : public RefBase {
 public:
diff --git a/media/libmediaextractor/include/media/MediaExtractor.h b/media/libmediaextractor/include/media/MediaExtractor.h
index f197b5e..276cbed 100644
--- a/media/libmediaextractor/include/media/MediaExtractor.h
+++ b/media/libmediaextractor/include/media/MediaExtractor.h
@@ -27,10 +27,7 @@
 namespace android {
 
 class DataSource;
-class IMediaSource;
-class MediaExtractorFactory;
 class MetaData;
-class Parcel;
 class String8;
 struct AMessage;
 struct MediaSource;
diff --git a/media/libmediaextractor/include/media/MediaSource.h b/media/libmediaextractor/include/media/MediaSource.h
index 25d691d..3df9a0d 100644
--- a/media/libmediaextractor/include/media/MediaSource.h
+++ b/media/libmediaextractor/include/media/MediaSource.h
@@ -20,17 +20,13 @@
 
 #include <sys/types.h>
 
-#include <binder/IMemory.h>
-#include <binder/MemoryDealer.h>
 #include <media/stagefright/MediaErrors.h>
 #include <media/stagefright/MetaData.h>
 #include <utils/RefBase.h>
-#include <utils/Vector.h>
 
 namespace android {
 
 class MediaBuffer;
-class IMediaSource;
 
 struct MediaSource : public virtual RefBase {
     MediaSource();
diff --git a/media/libmediaextractor/include/media/stagefright/MediaBuffer.h b/media/libmediaextractor/include/media/stagefright/MediaBuffer.h
index 2b51081..a8f8375 100644
--- a/media/libmediaextractor/include/media/stagefright/MediaBuffer.h
+++ b/media/libmediaextractor/include/media/stagefright/MediaBuffer.h
@@ -30,7 +30,6 @@
 namespace android {
 
 struct ABuffer;
-class GraphicBuffer;
 class MediaBuffer;
 class MediaBufferObserver;
 class MetaData;
@@ -143,7 +142,7 @@
         return mObserver != nullptr;
     }
 
-    ~MediaBuffer();
+    virtual ~MediaBuffer();
 
     sp<IMemory> mMemory;
 
diff --git a/media/libmediaplayerservice/nuplayer/GenericSource.h b/media/libmediaplayerservice/nuplayer/GenericSource.h
index 856f03b..2406665 100644
--- a/media/libmediaplayerservice/nuplayer/GenericSource.h
+++ b/media/libmediaplayerservice/nuplayer/GenericSource.h
@@ -35,6 +35,7 @@
 class IDataSource;
 struct IMediaHTTPService;
 struct MediaSource;
+class IMediaSource;
 class MediaBuffer;
 struct MediaClock;
 struct NuCachedSource2;
diff --git a/media/libstagefright/MediaExtractorFactory.cpp b/media/libstagefright/MediaExtractorFactory.cpp
index 472c137..e79696c 100644
--- a/media/libstagefright/MediaExtractorFactory.cpp
+++ b/media/libstagefright/MediaExtractorFactory.cpp
@@ -358,12 +358,16 @@
     Mutex::Autolock autoLock(gPluginMutex);
     String8 out;
     out.append("Available extractors:\n");
-    for (auto it = gPlugins->begin(); it != gPlugins->end(); ++it) {
-        out.appendFormat("  %25s: uuid(%s), version(%u), path(%s)\n",
-                (*it)->def.extractor_name,
-                (*it)->uuidString.c_str(),
-                (*it)->def.extractor_version,
-                (*it)->libPath.c_str());
+    if (gPluginsRegistered) {
+        for (auto it = gPlugins->begin(); it != gPlugins->end(); ++it) {
+            out.appendFormat("  %25s: uuid(%s), version(%u), path(%s)\n",
+                    (*it)->def.extractor_name,
+                    (*it)->uuidString.c_str(),
+                    (*it)->def.extractor_version,
+                    (*it)->libPath.c_str());
+        }
+    } else {
+        out.append("  (no plugins registered)\n");
     }
     write(fd, out.string(), out.size());
     return OK;
diff --git a/media/libstagefright/include/FrameDecoder.h b/media/libstagefright/include/FrameDecoder.h
index d7c074c..6f07838 100644
--- a/media/libstagefright/include/FrameDecoder.h
+++ b/media/libstagefright/include/FrameDecoder.h
@@ -27,6 +27,7 @@
 
 struct AMessage;
 class MediaCodecBuffer;
+class IMediaSource;
 class VideoFrame;
 
 struct FrameDecoder {
diff --git a/media/libstagefright/include/HTTPBase.h b/media/libstagefright/include/HTTPBase.h
index 26d7e8a..a924197 100644
--- a/media/libstagefright/include/HTTPBase.h
+++ b/media/libstagefright/include/HTTPBase.h
@@ -21,6 +21,7 @@
 #include <media/DataSource.h>
 #include <media/stagefright/foundation/ABase.h>
 #include <media/stagefright/MediaErrors.h>
+#include <utils/List.h>
 #include <utils/threads.h>
 
 namespace android {
diff --git a/media/libstagefright/include/media/stagefright/CallbackMediaSource.h b/media/libstagefright/include/media/stagefright/CallbackMediaSource.h
index 3459de1..944d951 100644
--- a/media/libstagefright/include/media/stagefright/CallbackMediaSource.h
+++ b/media/libstagefright/include/media/stagefright/CallbackMediaSource.h
@@ -22,6 +22,8 @@
 
 namespace android {
 
+class IMediaSource;
+
 // A stagefright MediaSource that wraps a binder IMediaSource.
 class CallbackMediaSource : public MediaSource {
 public:
diff --git a/media/libstagefright/include/media/stagefright/InterfaceUtils.h b/media/libstagefright/include/media/stagefright/InterfaceUtils.h
index 224c1f1..d449aec 100644
--- a/media/libstagefright/include/media/stagefright/InterfaceUtils.h
+++ b/media/libstagefright/include/media/stagefright/InterfaceUtils.h
@@ -17,13 +17,17 @@
 #ifndef INTERFACE_UTILS_H_
 #define INTERFACE_UTILS_H_
 
-#include <media/MediaExtractor.h>
-#include <media/MediaSource.h>
-#include <media/IMediaExtractor.h>
-#include <media/IMediaSource.h>
+#include <utils/RefBase.h>
 
 namespace android {
 
+class DataSource;
+class MediaExtractor;
+struct MediaSource;
+class IDataSource;
+class IMediaExtractor;
+class IMediaSource;
+
 // Creates a DataSource which wraps the given IDataSource object.
 sp<DataSource> CreateDataSourceFromIDataSource(const sp<IDataSource> &source);
 
diff --git a/media/libstagefright/include/media/stagefright/MediaExtractorFactory.h b/media/libstagefright/include/media/stagefright/MediaExtractorFactory.h
index e5c67e1..4610359 100644
--- a/media/libstagefright/include/media/stagefright/MediaExtractorFactory.h
+++ b/media/libstagefright/include/media/stagefright/MediaExtractorFactory.h
@@ -22,6 +22,7 @@
 
 #include <media/IMediaExtractor.h>
 #include <media/MediaExtractor.h>
+#include <utils/List.h>
 
 namespace android {
 
diff --git a/media/libstagefright/include/media/stagefright/RemoteMediaExtractor.h b/media/libstagefright/include/media/stagefright/RemoteMediaExtractor.h
index 2bd71ee..bdbad7a 100644
--- a/media/libstagefright/include/media/stagefright/RemoteMediaExtractor.h
+++ b/media/libstagefright/include/media/stagefright/RemoteMediaExtractor.h
@@ -19,6 +19,7 @@
 
 #include <media/IMediaExtractor.h>
 #include <media/MediaExtractor.h>
+#include <media/stagefright/foundation/ABase.h>
 
 namespace android {
 
diff --git a/media/libstagefright/mpeg2ts/ATSParser.cpp b/media/libstagefright/mpeg2ts/ATSParser.cpp
index 464ee90..9b12b2d 100644
--- a/media/libstagefright/mpeg2ts/ATSParser.cpp
+++ b/media/libstagefright/mpeg2ts/ATSParser.cpp
@@ -23,6 +23,8 @@
 #include "ESQueue.h"
 
 #include <android/hardware/cas/native/1.0/IDescrambler.h>
+#include <binder/IMemory.h>
+#include <binder/MemoryDealer.h>
 #include <cutils/native_handle.h>
 #include <media/stagefright/foundation/ABitReader.h>
 #include <media/stagefright/foundation/ABuffer.h>
@@ -77,7 +79,7 @@
 
     void signalEOS(status_t finalResult);
 
-    sp<MediaSource> getSource(SourceType type);
+    sp<AnotherPacketSource> getSource(SourceType type);
     bool hasSource(SourceType type) const;
 
     int64_t convertPTSToTimestamp(uint64_t PTS);
@@ -170,7 +172,7 @@
     void signalEOS(status_t finalResult);
 
     SourceType getSourceType();
-    sp<MediaSource> getSource(SourceType type);
+    sp<AnotherPacketSource> getSource(SourceType type);
 
     bool isAudio() const;
     bool isVideo() const;
@@ -274,7 +276,7 @@
 ATSParser::SyncEvent::SyncEvent(off64_t offset)
     : mHasReturnedData(false), mOffset(offset), mTimeUs(0) {}
 
-void ATSParser::SyncEvent::init(off64_t offset, const sp<MediaSource> &source,
+void ATSParser::SyncEvent::init(off64_t offset, const sp<AnotherPacketSource> &source,
         int64_t timeUs, SourceType type) {
     mHasReturnedData = true;
     mOffset = offset;
@@ -641,9 +643,9 @@
     return mLastRecoveredPTS;
 }
 
-sp<MediaSource> ATSParser::Program::getSource(SourceType type) {
+sp<AnotherPacketSource> ATSParser::Program::getSource(SourceType type) {
     for (size_t i = 0; i < mStreams.size(); ++i) {
-        sp<MediaSource> source = mStreams.editValueAt(i)->getSource(type);
+        sp<AnotherPacketSource> source = mStreams.editValueAt(i)->getSource(type);
         if (source != NULL) {
             return source;
         }
@@ -1607,7 +1609,7 @@
     return NUM_SOURCE_TYPES;
 }
 
-sp<MediaSource> ATSParser::Stream::getSource(SourceType type) {
+sp<AnotherPacketSource> ATSParser::Stream::getSource(SourceType type) {
     switch (type) {
         case VIDEO:
         {
@@ -2042,11 +2044,11 @@
     return err;
 }
 
-sp<MediaSource> ATSParser::getSource(SourceType type) {
-    sp<MediaSource> firstSourceFound;
+sp<AnotherPacketSource> ATSParser::getSource(SourceType type) {
+    sp<AnotherPacketSource> firstSourceFound;
     for (size_t i = 0; i < mPrograms.size(); ++i) {
         const sp<Program> &program = mPrograms.editItemAt(i);
-        sp<MediaSource> source = program->getSource(type);
+        sp<AnotherPacketSource> source = program->getSource(type);
         if (source == NULL) {
             continue;
         }
diff --git a/media/libstagefright/mpeg2ts/ATSParser.h b/media/libstagefright/mpeg2ts/ATSParser.h
index 6079afc..45ca06b 100644
--- a/media/libstagefright/mpeg2ts/ATSParser.h
+++ b/media/libstagefright/mpeg2ts/ATSParser.h
@@ -81,13 +81,13 @@
     struct SyncEvent {
         explicit SyncEvent(off64_t offset);
 
-        void init(off64_t offset, const sp<MediaSource> &source,
+        void init(off64_t offset, const sp<AnotherPacketSource> &source,
                 int64_t timeUs, SourceType type);
 
         bool hasReturnedData() const { return mHasReturnedData; }
         void reset();
         off64_t getOffset() const { return mOffset; }
-        const sp<MediaSource> &getMediaSource() const { return mMediaSource; }
+        const sp<AnotherPacketSource> &getMediaSource() const { return mMediaSource; }
         int64_t getTimeUs() const { return mTimeUs; }
         SourceType getType() const { return mType; }
 
@@ -100,7 +100,7 @@
          */
         off64_t mOffset;
         /* The media source object for this event. */
-        sp<MediaSource> mMediaSource;
+        sp<AnotherPacketSource> mMediaSource;
         /* The timestamp of the sync frame. */
         int64_t mTimeUs;
         SourceType mType;
@@ -126,7 +126,7 @@
 
     void signalEOS(status_t finalResult);
 
-    sp<MediaSource> getSource(SourceType type);
+    sp<AnotherPacketSource> getSource(SourceType type);
     bool hasSource(SourceType type) const;
 
     bool PTSTimeDeltaEstablished();
diff --git a/media/ndk/NdkMediaExtractor.cpp b/media/ndk/NdkMediaExtractor.cpp
index 5dee8b0..ea43d2e 100644
--- a/media/ndk/NdkMediaExtractor.cpp
+++ b/media/ndk/NdkMediaExtractor.cpp
@@ -51,7 +51,6 @@
 struct AMediaExtractor {
     sp<NuMediaExtractor> mImpl;
     sp<ABuffer> mPsshBuf;
-
 };
 
 extern "C" {
@@ -127,6 +126,13 @@
 }
 
 EXPORT
+AMediaFormat* AMediaExtractor_getFileFormat(AMediaExtractor *mData) {
+    sp<AMessage> format;
+    mData->mImpl->getFileFormat(&format);
+    return AMediaFormat_fromMsg(&format);
+}
+
+EXPORT
 size_t AMediaExtractor_getTrackCount(AMediaExtractor *mData) {
     return mData->mImpl->countTracks();
 }
@@ -188,6 +194,16 @@
 }
 
 EXPORT
+ssize_t AMediaExtractor_getSampleSize(AMediaExtractor *mData) {
+    size_t sampleSize;
+    status_t err = mData->mImpl->getSampleSize(&sampleSize);
+    if (err != OK) {
+        return -1;
+    }
+    return sampleSize;
+}
+
+EXPORT
 uint32_t AMediaExtractor_getSampleFlags(AMediaExtractor *mData) {
     int sampleFlags = 0;
     sp<MetaData> meta;
@@ -385,6 +401,15 @@
             (size_t*) crypteddata);
 }
 
+EXPORT
+int64_t AMediaExtractor_getCachedDuration(AMediaExtractor *ex) {
+    bool eos;
+    int64_t durationUs;
+    if (ex->mImpl->getCachedDuration(&durationUs, &eos)) {
+        return durationUs;
+    }
+    return -1;
+}
 
 } // extern "C"
 
diff --git a/media/ndk/include/media/NdkMediaExtractor.h b/media/ndk/include/media/NdkMediaExtractor.h
index 820e9f5..3c9e23d 100644
--- a/media/ndk/include/media/NdkMediaExtractor.h
+++ b/media/ndk/include/media/NdkMediaExtractor.h
@@ -162,12 +162,49 @@
 
 AMediaCodecCryptoInfo *AMediaExtractor_getSampleCryptoInfo(AMediaExtractor *);
 
-
 enum {
     AMEDIAEXTRACTOR_SAMPLE_FLAG_SYNC = 1,
     AMEDIAEXTRACTOR_SAMPLE_FLAG_ENCRYPTED = 2,
 };
 
+#if __ANDROID_API__ >= 28
+
+/**
+ * Returns the format of the extractor. The caller must free the returned format
+ * using AMediaFormat_delete(format).
+ *
+ * This function will always return a format; however, the format could be empty
+ * (no key-value pairs) if the media container does not provide format information.
+ */
+AMediaFormat* AMediaExtractor_getFileFormat(AMediaExtractor*);
+
+/**
+ * Returns the size of the current sample in bytes, or -1 when no samples are
+ * available (end of stream). This API can be used in in conjunction with
+ * AMediaExtractor_readSampleData:
+ *
+ * ssize_t sampleSize = AMediaExtractor_getSampleSize(ex);
+ * uint8_t *buf = new uint8_t[sampleSize];
+ * AMediaExtractor_readSampleData(ex, buf, sampleSize);
+ *
+ */
+ssize_t AMediaExtractor_getSampleSize(AMediaExtractor*);
+
+/**
+ * Returns the duration of cached media samples downloaded from a network data source
+ * (AMediaExtractor_setDataSource with a "http(s)" URI) in microseconds.
+ *
+ * This information is calculated using total bitrate; if total bitrate is not in the
+ * media container it is calculated using total duration and file size.
+ *
+ * Returns -1 when the extractor is not reading from a network data source, or when the
+ * cached duration cannot be calculated (bitrate, duration, and file size information
+ * not available).
+ */
+int64_t AMediaExtractor_getCachedDuration(AMediaExtractor *);
+
+#endif /* __ANDROID_API__ >= 28 */
+
 #endif /* __ANDROID_API__ >= 21 */
 
 __END_DECLS
diff --git a/media/ndk/libmediandk.map.txt b/media/ndk/libmediandk.map.txt
index 613cc63..37c557a 100644
--- a/media/ndk/libmediandk.map.txt
+++ b/media/ndk/libmediandk.map.txt
@@ -153,9 +153,12 @@
     AMediaDrm_verify;
     AMediaExtractor_advance;
     AMediaExtractor_delete;
+    AMediaExtractor_getCachedDuration; # introduced=28
+    AMediaExtractor_getFileFormat;     # introduced=28
     AMediaExtractor_getPsshInfo;
     AMediaExtractor_getSampleCryptoInfo;
     AMediaExtractor_getSampleFlags;
+    AMediaExtractor_getSampleSize;     # introduced=28
     AMediaExtractor_getSampleTime;
     AMediaExtractor_getSampleTrackIndex;
     AMediaExtractor_getTrackCount;
diff --git a/packages/MediaComponents/Android.mk b/packages/MediaComponents/Android.mk
index 61d73a0..7c7718e 100644
--- a/packages/MediaComponents/Android.mk
+++ b/packages/MediaComponents/Android.mk
@@ -27,7 +27,10 @@
 # TODO: Use System SDK once public APIs are approved
 # LOCAL_SDK_VERSION := system_current
 
-LOCAL_SRC_FILES := $(call all-java-files-under, src)
+LOCAL_SRC_FILES := \
+    $(call all-java-files-under, src) \
+    $(call all-Iaidl-files-under, src)
+
 LOCAL_PROGUARD_FLAG_FILES := proguard.cfg
 
 LOCAL_MULTILIB := first
diff --git a/packages/MediaComponents/res/layout/media_controller.xml b/packages/MediaComponents/res/layout/media_controller.xml
index 4d05546..74a8306 100644
--- a/packages/MediaComponents/res/layout/media_controller.xml
+++ b/packages/MediaComponents/res/layout/media_controller.xml
@@ -136,7 +136,13 @@
             android:layout_height="wrap_content"
             android:layout_centerVertical="true"
             android:visibility="gone"
-            android:orientation="horizontal" >
+            android:orientation="horizontal"
+            android:gravity="center">
+
+            <LinearLayout
+                android:id="@+id/custom_buttons"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content" />
 
             <ImageButton
                 android:id="@+id/mute"
diff --git a/packages/MediaComponents/src/com/android/media/IMediaSession2.aidl b/packages/MediaComponents/src/com/android/media/IMediaSession2.aidl
new file mode 100644
index 0000000..fc62a68
--- /dev/null
+++ b/packages/MediaComponents/src/com/android/media/IMediaSession2.aidl
@@ -0,0 +1,55 @@
+/*
+ * 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.media;
+
+import android.os.Bundle;
+import android.os.ResultReceiver;
+
+import com.android.media.IMediaSession2Callback;
+
+/**
+ * Interface to MediaSession2.
+ * <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.
+ */
+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.
+    void connect(String callingPackage, IMediaSession2Callback callback);
+    void release(IMediaSession2Callback caller);
+
+    //////////////////////////////////////////////////////////////////////////////////////////////
+    // send command
+    //////////////////////////////////////////////////////////////////////////////////////////////
+    void sendCommand(IMediaSession2Callback caller, in Bundle command, in Bundle args);
+    void sendTransportControlCommand(IMediaSession2Callback caller,
+            int commandCode, in Bundle args);
+    void sendCustomCommand(IMediaSession2Callback caller, in Bundle command, in Bundle args,
+            in ResultReceiver receiver);
+
+    //////////////////////////////////////////////////////////////////////////////////////////////
+    // Get library service specific
+    //////////////////////////////////////////////////////////////////////////////////////////////
+    void getBrowserRoot(IMediaSession2Callback callback, in Bundle rootHints);
+}
diff --git a/packages/MediaComponents/src/com/android/media/IMediaSession2Callback.aidl b/packages/MediaComponents/src/com/android/media/IMediaSession2Callback.aidl
new file mode 100644
index 0000000..aabbc69
--- /dev/null
+++ b/packages/MediaComponents/src/com/android/media/IMediaSession2Callback.aidl
@@ -0,0 +1,57 @@
+/*
+ * 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.media;
+
+import android.os.Bundle;
+import android.os.ResultReceiver;
+
+import com.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.
+ */
+oneway interface IMediaSession2Callback {
+    void onPlaybackStateChanged(in Bundle state);
+    void onPlaylistChanged(in List<Bundle> playlist);
+    void onPlaylistParamsChanged(in Bundle params);
+
+    /**
+     * 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, in Bundle commandGroup);
+
+    void onCustomLayoutChanged(in List<Bundle> commandButtonlist);
+
+    void sendCustomCommand(in Bundle command, in Bundle args, in ResultReceiver receiver);
+
+    //////////////////////////////////////////////////////////////////////////////////////////////
+    // Browser sepcific
+    //////////////////////////////////////////////////////////////////////////////////////////////
+    void onGetRootResult(in Bundle rootHints, String rootMediaId, in Bundle rootExtra);
+}
diff --git a/packages/MediaComponents/src/com/android/media/MediaBrowser2Impl.java b/packages/MediaComponents/src/com/android/media/MediaBrowser2Impl.java
index 511eed1..d21edae 100644
--- a/packages/MediaComponents/src/com/android/media/MediaBrowser2Impl.java
+++ b/packages/MediaComponents/src/com/android/media/MediaBrowser2Impl.java
@@ -17,7 +17,6 @@
 package com.android.media;
 
 import android.content.Context;
-import android.media.IMediaSession2;
 import android.media.MediaBrowser2;
 import android.media.MediaBrowser2.BrowserCallback;
 import android.media.MediaSession2.CommandButton;
diff --git a/packages/MediaComponents/src/com/android/media/MediaController2Impl.java b/packages/MediaComponents/src/com/android/media/MediaController2Impl.java
index 43f8473..30e32ec 100644
--- a/packages/MediaComponents/src/com/android/media/MediaController2Impl.java
+++ b/packages/MediaComponents/src/com/android/media/MediaController2Impl.java
@@ -21,8 +21,6 @@
 import android.content.Context;
 import android.content.Intent;
 import android.content.ServiceConnection;
-import android.media.IMediaSession2;
-import android.media.IMediaSession2Callback;
 import android.media.MediaController2.PlaybackInfo;
 import android.media.MediaItem2;
 import android.media.MediaSession2;
@@ -56,13 +54,6 @@
 
     private final MediaController2 mInstance;
 
-    /**
-     * Flag used by MediaController2Record to filter playback callback.
-     */
-    static final int CALLBACK_FLAG_PLAYBACK = 0x1;
-
-    static final int REQUEST_CODE_ALL = 0;
-
     private final Object mLock = new Object();
 
     private final Context mContext;
@@ -73,11 +64,15 @@
     private final IBinder.DeathRecipient mDeathRecipient;
 
     @GuardedBy("mLock")
-    private final List<PlaybackListenerHolder> mPlaybackListeners = new ArrayList<>();
-    @GuardedBy("mLock")
     private SessionServiceConnection mServiceConnection;
     @GuardedBy("mLock")
     private boolean mIsReleased;
+    @GuardedBy("mLock")
+    private PlaybackState2 mPlaybackState;
+    @GuardedBy("mLock")
+    private List<MediaItem2> mPlaylist;
+    @GuardedBy("mLock")
+    private PlaylistParams mPlaylistParams;
 
     // Assignment should be used with the lock hold, but should be used without a lock to prevent
     // potential deadlock.
@@ -92,7 +87,6 @@
     public MediaController2Impl(Context context, MediaController2 instance, SessionToken2 token,
             Executor executor, ControllerCallback callback) {
         mInstance = instance;
-
         if (context == null) {
             throw new IllegalArgumentException("context shouldn't be null");
         }
@@ -115,21 +109,28 @@
         };
 
         mSessionBinder = null;
+    }
 
-        if (token.getSessionBinder() == null) {
+    @Override
+    public void initialize() {
+        SessionToken2Impl impl = SessionToken2Impl.from(mToken);
+        // TODO(jaewan): More sanity checks.
+        if (impl.getSessionBinder() == null) {
+            // Session service
             mServiceConnection = new SessionServiceConnection();
             connectToService();
         } else {
+            // Session
             mServiceConnection = null;
-            connectToSession(token.getSessionBinder());
+            connectToSession(impl.getSessionBinder());
         }
     }
 
-    // Should be only called by constructor.
     private void connectToService() {
         // Service. Needs to get fresh binder whenever connection is needed.
+        SessionToken2Impl impl = SessionToken2Impl.from(mToken);
         final Intent intent = new Intent(MediaSessionService2.SERVICE_INTERFACE);
-        intent.setClassName(mToken.getPackageName(), mToken.getServiceName());
+        intent.setClassName(mToken.getPackageName(), impl.getServiceName());
 
         // Use bindService() instead of startForegroundService() to start session service for three
         // reasons.
@@ -166,7 +167,7 @@
     @Override
     public void close_impl() {
         if (DEBUG) {
-            Log.d(TAG, "relese from " + mToken);
+            Log.d(TAG, "release from " + mToken);
         }
         final IMediaSession2 binder;
         synchronized (mLock) {
@@ -179,7 +180,6 @@
                 mContext.unbindService(mServiceConnection);
                 mServiceConnection = null;
             }
-            mPlaybackListeners.clear();
             binder = mSessionBinder;
             mSessionBinder = null;
             mSessionCallbackStub.destroy();
@@ -222,38 +222,38 @@
 
     @Override
     public void play_impl() {
-        sendCommand(MediaSession2.COMMAND_CODE_PLAYBACK_START);
+        sendTransportControlCommand(MediaSession2.COMMAND_CODE_PLAYBACK_START);
     }
 
     @Override
     public void pause_impl() {
-        sendCommand(MediaSession2.COMMAND_CODE_PLAYBACK_PAUSE);
+        sendTransportControlCommand(MediaSession2.COMMAND_CODE_PLAYBACK_PAUSE);
     }
 
     @Override
     public void stop_impl() {
-        sendCommand(MediaSession2.COMMAND_CODE_PLAYBACK_STOP);
+        sendTransportControlCommand(MediaSession2.COMMAND_CODE_PLAYBACK_STOP);
     }
 
     @Override
     public void skipToPrevious_impl() {
-        sendCommand(MediaSession2.COMMAND_CODE_PLAYBACK_SKIP_PREV_ITEM);
+        sendTransportControlCommand(MediaSession2.COMMAND_CODE_PLAYBACK_SKIP_PREV_ITEM);
     }
 
     @Override
     public void skipToNext_impl() {
-        sendCommand(MediaSession2.COMMAND_CODE_PLAYBACK_SKIP_NEXT_ITEM);
+        sendTransportControlCommand(MediaSession2.COMMAND_CODE_PLAYBACK_SKIP_NEXT_ITEM);
     }
 
-    private void sendCommand(int code) {
-        // TODO(jaewan): optimization) Cache Command objects?
-        Command command = new Command(code);
-        // TODO(jaewan): Check if the command is in the allowed group.
+    private void sendTransportControlCommand(int commandCode) {
+        sendTransportControlCommand(commandCode, null);
+    }
 
+    private void sendTransportControlCommand(int commandCode, Bundle args) {
         final IMediaSession2 binder = mSessionBinder;
         if (binder != null) {
             try {
-                binder.sendCommand(mSessionCallbackStub, command.toBundle(), null);
+                binder.sendTransportControlCommand(mSessionCallbackStub, commandCode, args);
             } catch (RemoteException e) {
                 Log.w(TAG, "Cannot connect to the service or the session is gone", e);
             }
@@ -330,44 +330,64 @@
 
     @Override
     public void sendCustomCommand_impl(Command command, Bundle args, ResultReceiver cb) {
-        // TODO(jaewan): Implement
+        if (command == null) {
+            throw new IllegalArgumentException("command shouldn't be null");
+        }
+        // TODO(jaewan): Also check if the command is allowed.
+        final IMediaSession2 binder = mSessionBinder;
+        if (binder != null) {
+            try {
+                binder.sendCustomCommand(mSessionCallbackStub, command.toBundle(), args, cb);
+            } catch (RemoteException e) {
+                Log.w(TAG, "Cannot connect to the service or the session is gone", e);
+            }
+        } else {
+            Log.w(TAG, "Session isn't active", new IllegalStateException());
+        }
     }
 
     @Override
     public List<MediaItem2> getPlaylist_impl() {
-        // TODO(jaewan): Implement
-        return null;
+        synchronized (mLock) {
+            return mPlaylist;
+        }
     }
 
     @Override
     public void prepare_impl() {
-        // TODO(jaewan): Implement
+        sendTransportControlCommand(MediaSession2.COMMAND_CODE_PLAYBACK_PREPARE);
     }
 
     @Override
     public void fastForward_impl() {
-        // TODO(jaewan): Implement
+        sendTransportControlCommand(MediaSession2.COMMAND_CODE_PLAYBACK_FAST_FORWARD);
     }
 
     @Override
     public void rewind_impl() {
-        // TODO(jaewan): Implement
+        sendTransportControlCommand(MediaSession2.COMMAND_CODE_PLAYBACK_REWIND);
     }
 
     @Override
     public void seekTo_impl(long pos) {
-        // TODO(jaewan): Implement
+        Bundle args = new Bundle();
+        args.putLong(MediaSession2Stub.ARGUMENT_KEY_POSITION, pos);
+        sendTransportControlCommand(MediaSession2.COMMAND_CODE_PLAYBACK_SEEK_TO, args);
     }
 
     @Override
     public void setCurrentPlaylistItem_impl(int index) {
-        // TODO(jaewan): Implement
+        Bundle args = new Bundle();
+        args.putInt(MediaSession2Stub.ARGUMENT_KEY_ITEM_INDEX, index);
+        sendTransportControlCommand(
+                MediaSession2.COMMAND_CODE_PLAYBACK_SET_CURRENT_PLAYLIST_ITEM, args);
     }
 
     @Override
     public PlaybackState2 getPlaybackState_impl() {
-        // TODO(jaewan): Implement
-        return null;
+        synchronized (mLock) {
+            return mPlaybackState;
+        }
     }
 
     @Override
@@ -381,32 +401,66 @@
     }
 
     @Override
-    public PlaylistParams getPlaylistParam_impl() {
-        // TODO(jaewan): Implement
-        return null;
+    public PlaylistParams getPlaylistParams_impl() {
+        synchronized (mLock) {
+            return mPlaylistParams;
+        }
+    }
+
+    @Override
+    public void setPlaylistParams_impl(PlaylistParams params) {
+        if (params == null) {
+            throw new IllegalArgumentException("PlaylistParams should not be null!");
+        }
+        Bundle args = new Bundle();
+        args.putBundle(MediaSession2Stub.ARGUMENT_KEY_PLAYLIST_PARAMS, params.toBundle());
+        sendTransportControlCommand(MediaSession2.COMMAND_CODE_PLAYBACK_SET_PLAYLIST_PARAMS, args);
     }
 
     ///////////////////////////////////////////////////
     // Protected or private methods
     ///////////////////////////////////////////////////
-    // Should be used without a lock to prevent potential deadlock.
-    private void registerCallbackForPlaybackNotLocked() {
-        final IMediaSession2 binder = mSessionBinder;
-        if (binder != null) {
-            try {
-                binder.registerCallback(mSessionCallbackStub,
-                        CALLBACK_FLAG_PLAYBACK, REQUEST_CODE_ALL);
-            } catch (RemoteException e) {
-                Log.e(TAG, "Cannot connect to the service or the session is gone", e);
-            }
-        }
-    }
-
     private void pushPlaybackStateChanges(final PlaybackState2 state) {
         synchronized (mLock) {
-            for (int i = 0; i < mPlaybackListeners.size(); i++) {
-                mPlaybackListeners.get(i).postPlaybackChange(state);
+            mPlaybackState = state;
+        }
+        mCallbackExecutor.execute(() -> {
+            if (!mInstance.isConnected()) {
+                return;
             }
+            mCallback.onPlaybackStateChanged(state);
+        });
+    }
+
+    private void pushPlaylistParamsChanges(final PlaylistParams params) {
+        synchronized (mLock) {
+            mPlaylistParams = params;
+        }
+        mCallbackExecutor.execute(() -> {
+            if (!mInstance.isConnected()) {
+                return;
+            }
+            mCallback.onPlaylistParamsChanged(params);
+        });
+    }
+
+    private void pushPlaylistChanges(final List<Bundle> list) {
+        final List<MediaItem2> playlist = new ArrayList<>();
+        for (int i = 0; i < list.size(); i++) {
+            MediaItem2 item = MediaItem2.fromBundle(mContext, list.get(i));
+            if (item != null) {
+                playlist.add(item);
+            }
+        }
+
+        synchronized (mLock) {
+            mPlaylist = playlist;
+            mCallbackExecutor.execute(() -> {
+                if (!mInstance.isConnected()) {
+                    return;
+                }
+                mCallback.onPlaylistChanged(playlist);
+            });
         }
     }
 
@@ -425,7 +479,6 @@
                 release = true;
                 return;
             }
-            boolean registerCallbackForPlaybackNeeded;
             synchronized (mLock) {
                 if (mIsReleased) {
                     return;
@@ -448,15 +501,11 @@
                     release = true;
                     return;
                 }
-                registerCallbackForPlaybackNeeded = !mPlaybackListeners.isEmpty();
             }
             // TODO(jaewan): Keep commands to prevents illegal API calls.
             mCallbackExecutor.execute(() -> {
                 mCallback.onConnected(commandGroup);
             });
-            if (registerCallbackForPlaybackNeeded) {
-                registerCallbackForPlaybackNotLocked();
-            }
         } finally {
             if (release) {
                 // Trick to call release() without holding the lock, to prevent potential deadlock
@@ -466,6 +515,17 @@
         }
     }
 
+    private void onCustomCommand(final Command command, final Bundle args,
+            final ResultReceiver receiver) {
+        if (DEBUG) {
+            Log.d(TAG, "onCustomCommand cmd=" + command);
+        }
+        mCallbackExecutor.execute(() -> {
+            // TODO(jaewan): Double check if the controller exists.
+            mCallback.onCustomCommand(command, args, receiver);
+        });
+    }
+
     // TODO(jaewan): Pull out this from the controller2, and rename it to the MediaController2Stub
     //               or MediaBrowser2Stub.
     static class MediaSession2CallbackStub extends IMediaSession2Callback.Stub {
@@ -498,11 +558,44 @@
 
         @Override
         public void onPlaybackStateChanged(Bundle state) throws RuntimeException {
-            final MediaController2Impl controller = getController();
+            final MediaController2Impl controller;
+            try {
+                controller = getController();
+            } catch (IllegalStateException e) {
+                Log.w(TAG, "Don't fail silently here. Highly likely a bug");
+                return;
+            }
             controller.pushPlaybackStateChanges(PlaybackState2.fromBundle(state));
         }
 
         @Override
+        public void onPlaylistChanged(List<Bundle> playlist) throws RuntimeException {
+            final MediaController2Impl controller;
+            try {
+                controller = getController();
+            } catch (IllegalStateException e) {
+                Log.w(TAG, "Don't fail silently here. Highly likely a bug");
+                return;
+            }
+            if (playlist == null) {
+                return;
+            }
+            controller.pushPlaylistChanges(playlist);
+        }
+
+        @Override
+        public void onPlaylistParamsChanged(Bundle params) throws RuntimeException {
+            final MediaController2Impl controller;
+            try {
+                controller = getController();
+            } catch (IllegalStateException e) {
+                Log.w(TAG, "Don't fail silently here. Highly likely a bug");
+                return;
+            }
+            controller.pushPlaylistParamsChanges(PlaylistParams.fromBundle(params));
+        }
+
+        @Override
         public void onConnectionChanged(IMediaSession2 sessionBinder, Bundle commandGroup)
                 throws RuntimeException {
             final MediaController2Impl controller;
@@ -559,6 +652,22 @@
             }
             browser.onCustomLayoutChanged(layout);
         }
+
+        @Override
+        public void sendCustomCommand(Bundle commandBundle, Bundle args, ResultReceiver receiver) {
+            final MediaController2Impl controller;
+            try {
+                controller = getController();
+            } catch (IllegalStateException e) {
+                Log.w(TAG, "Don't fail silently here. Highly likely a bug");
+                return;
+            }
+            Command command = Command.fromBundle(commandBundle);
+            if (command == null) {
+                return;
+            }
+            controller.onCustomCommand(command, args, receiver);
+        }
     }
 
     // This will be called on the main thread.
diff --git a/packages/MediaComponents/src/com/android/media/MediaItem2Impl.java b/packages/MediaComponents/src/com/android/media/MediaItem2Impl.java
new file mode 100644
index 0000000..5639346
--- /dev/null
+++ b/packages/MediaComponents/src/com/android/media/MediaItem2Impl.java
@@ -0,0 +1,163 @@
+/*
+ * 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.media;
+
+import static android.media.MediaItem2.FLAG_BROWSABLE;
+import static android.media.MediaItem2.FLAG_PLAYABLE;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.Context;
+import android.media.DataSourceDesc;
+import android.media.MediaItem2;
+import android.media.MediaItem2.Flags;
+import android.media.MediaMetadata2;
+import android.media.update.MediaItem2Provider;
+import android.os.Bundle;
+import android.text.TextUtils;
+
+public class MediaItem2Impl implements MediaItem2Provider {
+    private static final String KEY_ID = "android.media.mediaitem2.id";
+    private static final String KEY_FLAGS = "android.media.mediaitem2.flags";
+    private static final String KEY_METADATA = "android.media.mediaitem2.metadata";
+
+    private final Context mContext;
+    private final MediaItem2 mInstance;
+    private final String mId;
+    private final int mFlags;
+    private MediaMetadata2 mMetadata;
+    private DataSourceDesc mDataSourceDesc;
+
+    // From the public API
+    public MediaItem2Impl(Context context, MediaItem2 instance, String mediaId,
+            DataSourceDesc dsd, MediaMetadata2 metadata, @Flags int flags) {
+        if (mediaId == null) {
+            throw new IllegalArgumentException("mediaId shouldn't be null");
+        }
+        if (dsd == null) {
+            throw new IllegalArgumentException("dsd shouldn't be null");
+        }
+        if (metadata != null && !TextUtils.equals(mediaId, metadata.getMediaId())) {
+            throw new IllegalArgumentException("metadata's id should be match with the mediaid");
+        }
+
+        mContext = context;
+        mInstance = instance;
+
+        mId = mediaId;
+        mDataSourceDesc = dsd;
+        mMetadata = metadata;
+        mFlags = flags;
+    }
+
+    // Create anonymized version
+    public MediaItem2Impl(Context context, String mediaId, MediaMetadata2 metadata,
+            @Flags int flags) {
+        if (mediaId == null) {
+            throw new IllegalArgumentException("mediaId shouldn't be null");
+        }
+        if (metadata != null && !TextUtils.equals(mediaId, metadata.getMediaId())) {
+            throw new IllegalArgumentException("metadata's id should be match with the mediaid");
+        }
+        mContext =context;
+        mId = mediaId;
+        mMetadata = metadata;
+        mFlags = flags;
+        mInstance = new MediaItem2(this);
+    }
+
+    /**
+     * Return this object as a bundle to share between processes.
+     *
+     * @return a new bundle instance
+     */
+    public Bundle toBundle_impl() {
+        Bundle bundle = new Bundle();
+        bundle.putString(KEY_ID, mId);
+        bundle.putInt(KEY_FLAGS, mFlags);
+        if (mMetadata != null) {
+            bundle.putBundle(KEY_METADATA, mMetadata.getBundle());
+        }
+        return bundle;
+    }
+
+    public static MediaItem2 fromBundle(Context context, Bundle bundle) {
+        if (bundle == null) {
+            return null;
+        }
+        final String id = bundle.getString(KEY_ID);
+        final Bundle metadataBundle = bundle.getBundle(KEY_METADATA);
+        final MediaMetadata2 metadata = metadataBundle != null
+                ? new MediaMetadata2(metadataBundle) : null;
+        final int flags = bundle.getInt(KEY_FLAGS);
+        return new MediaItem2Impl(context, id, metadata, flags).getInstance();
+    }
+
+    private MediaItem2 getInstance() {
+        return mInstance;
+    }
+
+    @Override
+    public String toString_impl() {
+        final StringBuilder sb = new StringBuilder("MediaItem2{");
+        sb.append("mFlags=").append(mFlags);
+        sb.append(", mMetadata=").append(mMetadata);
+        sb.append('}');
+        return sb.toString();
+    }
+
+    @Override
+    public @Flags int getFlags_impl() {
+        return mFlags;
+    }
+
+    @Override
+    public boolean isBrowsable_impl() {
+        return (mFlags & FLAG_BROWSABLE) != 0;
+    }
+
+    @Override
+    public boolean isPlayable_impl() {
+        return (mFlags & FLAG_PLAYABLE) != 0;
+    }
+
+    @Override
+    public void setMetadata_impl(@NonNull MediaMetadata2 metadata) {
+        if (metadata == null) {
+            throw new IllegalArgumentException("metadata shouldn't be null");
+        }
+        if (TextUtils.isEmpty(metadata.getMediaId())) {
+            throw new IllegalArgumentException("metadata must have a non-empty media id");
+        }
+        mMetadata = metadata;
+    }
+
+    @Override
+    public MediaMetadata2 getMetadata_impl() {
+        return mMetadata;
+    }
+
+    @Override
+    public @Nullable String getMediaId_impl() {
+        return mMetadata.getMediaId();
+    }
+
+    @Override
+    public @Nullable DataSourceDesc getDataSourceDesc_impl() {
+        return mDataSourceDesc;
+    }
+}
diff --git a/packages/MediaComponents/src/com/android/media/MediaLibraryService2Impl.java b/packages/MediaComponents/src/com/android/media/MediaLibraryService2Impl.java
index bbb3411..4b1b9de 100644
--- a/packages/MediaComponents/src/com/android/media/MediaLibraryService2Impl.java
+++ b/packages/MediaComponents/src/com/android/media/MediaLibraryService2Impl.java
@@ -18,15 +18,14 @@
 
 import android.app.PendingIntent;
 import android.content.Context;
-import android.content.Intent;
 import android.media.MediaLibraryService2;
 import android.media.MediaLibraryService2.MediaLibrarySession;
 import android.media.MediaLibraryService2.MediaLibrarySessionCallback;
-import android.media.MediaPlayerBase;
+import android.media.MediaPlayerInterface;
 import android.media.MediaSession2;
 import android.media.MediaSession2.ControllerInfo;
-import android.media.MediaSession2.SessionCallback;
 import android.media.MediaSessionService2;
+import android.media.SessionToken2;
 import android.media.VolumeProvider;
 import android.media.update.MediaLibraryService2Provider;
 import android.os.Bundle;
@@ -56,10 +55,8 @@
     }
 
     @Override
-    Intent createServiceIntent() {
-        Intent serviceIntent = new Intent(mInstance, mInstance.getClass());
-        serviceIntent.setAction(MediaLibraryService2.SERVICE_INTERFACE);
-        return serviceIntent;
+    int getSessionType() {
+        return SessionToken2.TYPE_LIBRARY_SERVICE;
     }
 
     public static class MediaLibrarySessionImpl extends MediaSession2Impl
@@ -68,8 +65,8 @@
         private final MediaLibrarySessionCallback mCallback;
 
         public MediaLibrarySessionImpl(Context context, MediaLibrarySession instance,
-                MediaPlayerBase player, String id, VolumeProvider volumeProvider, int ratingType,
-                PendingIntent sessionActivity, Executor callbackExecutor,
+                MediaPlayerInterface player, String id, VolumeProvider volumeProvider,
+                int ratingType, PendingIntent sessionActivity, Executor callbackExecutor,
                 MediaLibrarySessionCallback callback)  {
             super(context, instance, player, id, volumeProvider, ratingType, sessionActivity,
                     callbackExecutor, callback);
diff --git a/packages/MediaComponents/src/com/android/media/MediaSession2Impl.java b/packages/MediaComponents/src/com/android/media/MediaSession2Impl.java
index 22a3187..7c36739 100644
--- a/packages/MediaComponents/src/com/android/media/MediaSession2Impl.java
+++ b/packages/MediaComponents/src/com/android/media/MediaSession2Impl.java
@@ -16,15 +16,21 @@
 
 package com.android.media;
 
+import static android.media.SessionToken2.TYPE_LIBRARY_SERVICE;
+import static android.media.SessionToken2.TYPE_SESSION;
+import static android.media.SessionToken2.TYPE_SESSION_SERVICE;
+
 import android.Manifest.permission;
 import android.app.PendingIntent;
 import android.content.Context;
+import android.content.Intent;
 import android.content.pm.PackageManager;
 import android.content.pm.PackageManager.NameNotFoundException;
-import android.media.AudioAttributes;
-import android.media.IMediaSession2Callback;
+import android.content.pm.ResolveInfo;
 import android.media.MediaItem2;
-import android.media.MediaPlayerBase;
+import android.media.MediaLibraryService2;
+import android.media.MediaPlayerInterface;
+import android.media.MediaPlayerInterface.PlaybackListener;
 import android.media.MediaSession2;
 import android.media.MediaSession2.Builder;
 import android.media.MediaSession2.Command;
@@ -33,17 +39,20 @@
 import android.media.MediaSession2.ControllerInfo;
 import android.media.MediaSession2.PlaylistParams;
 import android.media.MediaSession2.SessionCallback;
+import android.media.MediaSessionService2;
 import android.media.PlaybackState2;
 import android.media.SessionToken2;
 import android.media.VolumeProvider;
 import android.media.session.MediaSessionManager;
 import android.media.update.MediaSession2Provider;
 import android.os.Bundle;
-import android.os.Handler;
+import android.os.Process;
 import android.os.IBinder;
-import android.os.Looper;
 import android.os.ResultReceiver;
+import android.support.annotation.GuardedBy;
+import android.text.TextUtils;
 import android.util.Log;
+
 import java.lang.ref.WeakReference;
 import java.util.ArrayList;
 import java.util.List;
@@ -53,24 +62,29 @@
     private static final String TAG = "MediaSession2";
     private static final boolean DEBUG = true;//Log.isLoggable(TAG, Log.DEBUG);
 
-    private final MediaSession2 mInstance;
+    private final Object mLock = new Object();
 
+    private final MediaSession2 mInstance;
     private final Context mContext;
     private final String mId;
-    private final Handler mHandler;
     private final Executor mCallbackExecutor;
+    private final SessionCallback mCallback;
     private final MediaSession2Stub mSessionStub;
     private final SessionToken2 mSessionToken;
-
-    private MediaPlayerBase mPlayer;
-
     private final List<PlaybackListenerHolder> mListeners = new ArrayList<>();
+
+    @GuardedBy("mLock")
+    private MediaPlayerInterface mPlayer;
+    @GuardedBy("mLock")
     private MyPlaybackListener mListener;
-    private MediaSession2 instance;
+    @GuardedBy("mLock")
+    private PlaylistParams mPlaylistParams;
+    @GuardedBy("mLock")
+    private List<MediaItem2> mPlaylist;
 
     /**
      * Can be only called by the {@link Builder#build()}.
-     * 
+     *
      * @param instance
      * @param context
      * @param player
@@ -80,7 +94,7 @@
      * @param ratingType
      * @param sessionActivity
      */
-    public MediaSession2Impl(Context context, MediaSession2 instance, MediaPlayerBase player,
+    public MediaSession2Impl(Context context, MediaSession2 instance, MediaPlayerInterface player,
             String id, VolumeProvider volumeProvider, int ratingType, PendingIntent sessionActivity,
             Executor callbackExecutor, SessionCallback callback) {
         mInstance = instance;
@@ -90,63 +104,109 @@
         // Initialize finals first.
         mContext = context;
         mId = id;
-        mHandler = new Handler(Looper.myLooper());
+        mCallback = callback;
         mCallbackExecutor = callbackExecutor;
-        mSessionStub = new MediaSession2Stub(this, callback);
-        // Ask server to create session token for following reasons.
-        //   1. Make session ID unique per package.
-        //      Server can only know if the package has another process and has another session
-        //      with the same id. Let server check this.
-        //      Note that 'ID is unique per package' is important for controller to distinguish
-        //      a session in another package.
-        //   2. Easier to know the type of session.
-        //      Session created here can be the session service token. In order distinguish,
-        //      we need to iterate AndroidManifest.xml but it's already done by the server.
-        //      Let server to create token with the type.
+        mSessionStub = new MediaSession2Stub(this);
+
+        // Infer type from the id and package name.
+        String libraryService = getServiceName(context, MediaLibraryService2.SERVICE_INTERFACE, id);
+        String sessionService = getServiceName(context, MediaSessionService2.SERVICE_INTERFACE, id);
+        if (sessionService != null && libraryService != null) {
+            throw new IllegalArgumentException("Ambiguous session type. Multiple"
+                    + " session services define the same id=" + id);
+        } else if (libraryService != null) {
+            mSessionToken = new SessionToken2Impl(context, Process.myUid(), TYPE_LIBRARY_SERVICE,
+                    mContext.getPackageName(), libraryService, id, mSessionStub).getInstance();
+        } else if (sessionService != null) {
+            mSessionToken = new SessionToken2Impl(context, Process.myUid(), TYPE_SESSION_SERVICE,
+                    mContext.getPackageName(), sessionService, id, mSessionStub).getInstance();
+        } else {
+            mSessionToken = new SessionToken2Impl(context, Process.myUid(), TYPE_SESSION,
+                    mContext.getPackageName(), null, id, mSessionStub).getInstance();
+        }
+
+        // Only remember player. Actual settings will be done in the initialize().
+        mPlayer = player;
+    }
+
+    private static String getServiceName(Context context, String serviceAction, String id) {
+        PackageManager manager = context.getPackageManager();
+        Intent serviceIntent = new Intent(serviceAction);
+        serviceIntent.setPackage(context.getPackageName());
+        List<ResolveInfo> services = manager.queryIntentServices(serviceIntent,
+                PackageManager.GET_META_DATA);
+        String serviceName = null;
+        if (services != null) {
+            for (int i = 0; i < services.size(); i++) {
+                String serviceId = SessionToken2Impl.getSessionId(services.get(i));
+                if (serviceId != null && TextUtils.equals(id, serviceId)) {
+                    if (services.get(i).serviceInfo == null) {
+                        continue;
+                    }
+                    if (serviceName != null) {
+                        throw new IllegalArgumentException("Ambiguous session type. Multiple"
+                                + " session services define the same id=" + id);
+                    }
+                    serviceName = services.get(i).serviceInfo.name;
+                }
+            }
+        }
+        return serviceName;
+    }
+
+    @Override
+    public void initialize() {
+        synchronized (mLock) {
+            setPlayerLocked(mPlayer);
+        }
+        // Ask server for the sanity check, and starts
+        // Sanity check for making session ID unique 'per package' cannot be done in here.
+        // Server can only know if the package has another process and has another session with the
+        // same id. Note that 'ID is unique per package' is important for controller to distinguish
+        // a session in another package.
         MediaSessionManager manager =
                 (MediaSessionManager) mContext.getSystemService(Context.MEDIA_SESSION_SERVICE);
-        mSessionToken = manager.createSessionToken(mContext.getPackageName(), mId, mSessionStub);
-        if (mSessionToken == null) {
+        if (!manager.onSessionCreated(mSessionToken)) {
             throw new IllegalStateException("Session with the same id is already used by"
                     + " another process. Use MediaController2 instead.");
         }
-
-        setPlayerInternal(player);
     }
 
     // TODO(jaewan): Add explicit release() and do not remove session object with the
     //               setPlayer(null). Token can be available when player is null, and
     //               controller can also attach to session.
     @Override
-    public void setPlayer_impl(MediaPlayerBase player, VolumeProvider volumeProvider) throws IllegalArgumentException {
+    public void setPlayer_impl(MediaPlayerInterface player, VolumeProvider volumeProvider)
+            throws IllegalArgumentException {
         ensureCallingThread();
         if (player == null) {
             throw new IllegalArgumentException("player shouldn't be null");
         }
-        setPlayerInternal(player);
-    }
-
-    private void setPlayerInternal(MediaPlayerBase player) {
-        if (mPlayer == player) {
-            // Player didn't changed. No-op.
+        if (player == mPlayer) {
             return;
         }
-        // TODO(jaewan): Find equivalent for the executor
-        //mHandler.removeCallbacksAndMessages(null);
+        synchronized (mLock) {
+            setPlayerLocked(player);
+        }
+    }
+
+    private void setPlayerLocked(MediaPlayerInterface player) {
         if (mPlayer != null && mListener != null) {
             // This might not work for a poorly implemented player.
             mPlayer.removePlaybackListener(mListener);
         }
+        mPlayer = player;
         mListener = new MyPlaybackListener(this, player);
         player.addPlaybackListener(mCallbackExecutor, mListener);
-        notifyPlaybackStateChanged(player.getPlaybackState());
-        mPlayer = player;
     }
 
     @Override
     public void close_impl() {
-        // Flush any pending messages.
-        mHandler.removeCallbacksAndMessages(null);
+        // Stop system service from listening this session first.
+        MediaSessionManager manager =
+                (MediaSessionManager) mContext.getSystemService(Context.MEDIA_SESSION_SERVICE);
+        manager.onSessionDestroyed(mSessionToken);
+
         if (mSessionStub != null) {
             if (DEBUG) {
                 Log.d(TAG, "session is now unavailable, id=" + mId);
@@ -154,10 +214,17 @@
             // Invalidate previously published session stub.
             mSessionStub.destroyNotLocked();
         }
+        synchronized (mLock) {
+            if (mPlayer != null) {
+                // close can be called multiple times
+                mPlayer.removePlaybackListener(mListener);
+                mPlayer = null;
+            }
+        }
     }
 
     @Override
-    public MediaPlayerBase getPlayer_impl() {
+    public MediaPlayerInterface getPlayer_impl() {
         return getPlayer();
     }
 
@@ -173,11 +240,6 @@
     }
 
     @Override
-    public void setAudioAttributes_impl(AudioAttributes attributes) {
-        // implement
-    }
-
-    @Override
     public void setAudioFocusRequest_impl(int focusGain) {
         // implement
     }
@@ -229,11 +291,31 @@
         mSessionStub.notifyCustomLayoutNotLocked(controller, layout);
     }
 
+    @Override
+    public void setPlaylistParams_impl(PlaylistParams params) {
+        if (params == null) {
+            throw new IllegalArgumentException("PlaylistParams should not be null!");
+        }
+        ensureCallingThread();
+        ensurePlayer();
+        synchronized (mLock) {
+            mPlaylistParams = params;
+        }
+        mPlayer.setPlaylistParams(params);
+        mSessionStub.notifyPlaylistParamsChanged(params);
+    }
+
+    @Override
+    public PlaylistParams getPlaylistParams_impl() {
+        // TODO: Do we need to synchronize here for preparing Controller2.setPlaybackParams?
+        return mPlaylistParams;
+    }
+
     //////////////////////////////////////////////////////////////////////////////////////
     // TODO(jaewan): Implement follows
     //////////////////////////////////////////////////////////////////////////////////////
     @Override
-    public void setPlayer_impl(MediaPlayerBase player) {
+    public void setPlayer_impl(MediaPlayerInterface player) {
         // TODO(jaewan): Implement
     }
 
@@ -250,42 +332,106 @@
     @Override
     public void sendCustomCommand_impl(ControllerInfo controller, Command command, Bundle args,
             ResultReceiver receiver) {
-        // TODO(jaewan): Implement
+        mSessionStub.sendCustomCommand(controller, command, args, receiver);
     }
 
     @Override
     public void sendCustomCommand_impl(Command command, Bundle args) {
-        // TODO(jaewan): Implement
+        mSessionStub.sendCustomCommand(command, args);
     }
 
     @Override
-    public void setPlaylist_impl(List<MediaItem2> playlist, PlaylistParams param) {
-        // TODO(jaewan): Implement
+    public void setPlaylist_impl(List<MediaItem2> playlist) {
+        if (playlist == null) {
+            throw new IllegalArgumentException("Playlist should not be null!");
+        }
+        ensureCallingThread();
+        ensurePlayer();
+        synchronized (mLock) {
+            mPlaylist = playlist;
+        }
+        mPlayer.setPlaylist(playlist);
+        mSessionStub.notifyPlaylistChanged(playlist);
+    }
+
+    @Override
+    public List<MediaItem2> getPlaylist_impl() {
+        synchronized (mLock) {
+            return mPlaylist;
+        }
     }
 
     @Override
     public void prepare_impl() {
-        // TODO(jaewan): Implement
+        ensureCallingThread();
+        ensurePlayer();
+        mPlayer.prepare();
     }
 
     @Override
     public void fastForward_impl() {
-        // TODO(jaewan): Implement
+        ensureCallingThread();
+        ensurePlayer();
+        mPlayer.fastForward();
     }
 
     @Override
     public void rewind_impl() {
-        // TODO(jaewan): Implement
+        ensureCallingThread();
+        ensurePlayer();
+        mPlayer.rewind();
     }
 
     @Override
     public void seekTo_impl(long pos) {
-        // TODO(jaewan): Implement
+        ensureCallingThread();
+        ensurePlayer();
+        mPlayer.seekTo(pos);
     }
 
     @Override
     public void setCurrentPlaylistItem_impl(int index) {
-        // TODO(jaewan): Implement
+        ensureCallingThread();
+        ensurePlayer();
+        mPlayer.setCurrentPlaylistItem(index);
+    }
+
+    @Override
+    public void addPlaybackListener_impl(Executor executor, PlaybackListener listener) {
+        if (executor == null) {
+            throw new IllegalArgumentException("executor shouldn't be null");
+        }
+        if (listener == null) {
+            throw new IllegalArgumentException("listener shouldn't be null");
+        }
+        ensureCallingThread();
+        if (PlaybackListenerHolder.contains(mListeners, listener)) {
+            Log.w(TAG, "listener is already added. Ignoring.");
+            return;
+        }
+        mListeners.add(new PlaybackListenerHolder(executor, listener));
+        executor.execute(() -> listener.onPlaybackChanged(getInstance().getPlaybackState()));
+    }
+
+    @Override
+    public void removePlaybackListener_impl(PlaybackListener listener) {
+        if (listener == null) {
+            throw new IllegalArgumentException("listener shouldn't be null");
+        }
+        ensureCallingThread();
+        int idx = PlaybackListenerHolder.indexOf(mListeners, listener);
+        if (idx >= 0) {
+            mListeners.remove(idx);
+        }
+    }
+
+    @Override
+    public PlaybackState2 getPlaybackState_impl() {
+        ensureCallingThread();
+        ensurePlayer();
+        // TODO(jaewan): Is it safe to be called on any thread?
+        //               Otherwise we should cache the result from listener.
+        return mPlayer.getPlaybackState();
     }
 
     ///////////////////////////////////////////////////
@@ -312,7 +458,6 @@
         }*/
     }
 
-
     private void ensurePlayer() {
         // TODO(jaewan): Should we pend command instead? Follow the decision from MP2.
         //               Alternatively we can add a API like setAcceptsPendingCommands(boolean).
@@ -321,14 +466,14 @@
         }
     }
 
-    Handler getHandler() {
-        return mHandler;
-    }
-
-    private void notifyPlaybackStateChanged(PlaybackState2 state) {
+    private void notifyPlaybackStateChangedNotLocked(PlaybackState2 state) {
+        List<PlaybackListenerHolder> listeners = new ArrayList<>();
+        synchronized (mLock) {
+            listeners.addAll(mListeners);
+        }
         // Notify to listeners added directly to this session
-        for (int i = 0; i < mListeners.size(); i++) {
-            mListeners.get(i).postPlaybackChange(state);
+        for (int i = 0; i < listeners.size(); i++) {
+            listeners.get(i).postPlaybackChange(state);
         }
         // Notify to controllers as well.
         mSessionStub.notifyPlaybackStateChangedNotLocked(state);
@@ -342,15 +487,23 @@
         return mInstance;
     }
 
-    MediaPlayerBase getPlayer() {
+    MediaPlayerInterface getPlayer() {
         return mPlayer;
     }
 
-    private static class MyPlaybackListener implements MediaPlayerBase.PlaybackListener {
-        private final WeakReference<MediaSession2Impl> mSession;
-        private final MediaPlayerBase mPlayer;
+    Executor getCallbackExecutor() {
+        return mCallbackExecutor;
+    }
 
-        private MyPlaybackListener(MediaSession2Impl session, MediaPlayerBase player) {
+    SessionCallback getCallback() {
+        return mCallback;
+    }
+
+    private static class MyPlaybackListener implements MediaPlayerInterface.PlaybackListener {
+        private final WeakReference<MediaSession2Impl> mSession;
+        private final MediaPlayerInterface mPlayer;
+
+        private MyPlaybackListener(MediaSession2Impl session, MediaPlayerInterface player) {
             mSession = new WeakReference<>(session);
             mPlayer = player;
         }
@@ -363,7 +516,7 @@
                         new IllegalStateException());
                 return;
             }
-            session.notifyPlaybackStateChanged(state);
+            session.notifyPlaybackStateChangedNotLocked(state);
         }
     }
 
@@ -374,11 +527,6 @@
         private final boolean mIsTrusted;
         private final IMediaSession2Callback mControllerBinder;
 
-        // Flag to indicate which callbacks should be returned for the controller binder.
-        // Either 0 or combination of {@link #CALLBACK_FLAG_PLAYBACK},
-        // {@link #CALLBACK_FLAG_SESSION_ACTIVENESS}
-        private int mFlag;
-
         public ControllerInfoImpl(Context context, ControllerInfo instance, int uid,
                 int pid, String packageName, IMediaSession2Callback callback) {
             mInstance = instance;
@@ -462,18 +610,6 @@
             return mControllerBinder;
         }
 
-        public boolean containsFlag(int flag) {
-            return (mFlag & flag) != 0;
-        }
-
-        public void addFlag(int flag) {
-            mFlag |= flag;
-        }
-
-        public void removeFlag(int flag) {
-            mFlag &= ~flag;
-        }
-
         public static ControllerInfoImpl from(ControllerInfo controller) {
             return (ControllerInfoImpl) controller.getProvider();
         }
diff --git a/packages/MediaComponents/src/com/android/media/MediaSession2Stub.java b/packages/MediaComponents/src/com/android/media/MediaSession2Stub.java
index 2f75dfa..4fc69b9 100644
--- a/packages/MediaComponents/src/com/android/media/MediaSession2Stub.java
+++ b/packages/MediaComponents/src/com/android/media/MediaSession2Stub.java
@@ -16,11 +16,7 @@
 
 package com.android.media;
 
-import static com.android.media.MediaController2Impl.CALLBACK_FLAG_PLAYBACK;
-
-import android.content.Context;
-import android.media.IMediaSession2;
-import android.media.IMediaSession2Callback;
+import android.media.MediaItem2;
 import android.media.MediaLibraryService2.BrowserRoot;
 import android.media.MediaLibraryService2.MediaLibrarySessionCallback;
 import android.media.MediaSession2;
@@ -28,53 +24,47 @@
 import android.media.MediaSession2.CommandButton;
 import android.media.MediaSession2.CommandGroup;
 import android.media.MediaSession2.ControllerInfo;
-import android.media.MediaSession2.SessionCallback;
+import android.media.MediaSession2.PlaylistParams;
 import android.media.PlaybackState2;
-import android.media.session.PlaybackState;
 import android.os.Binder;
 import android.os.Bundle;
-import android.os.Handler;
 import android.os.IBinder;
-import android.os.Looper;
-import android.os.Message;
 import android.os.RemoteException;
+import android.os.ResultReceiver;
 import android.support.annotation.GuardedBy;
 import android.util.ArrayMap;
 import android.util.Log;
+
 import com.android.media.MediaSession2Impl.ControllerInfoImpl;
+
 import java.lang.ref.WeakReference;
 import java.util.ArrayList;
 import java.util.List;
 
 public class MediaSession2Stub extends IMediaSession2.Stub {
+
+    static final String ARGUMENT_KEY_POSITION = "android.media.media_session2.key_position";
+    static final String ARGUMENT_KEY_ITEM_INDEX = "android.media.media_session2.key_item_index";
+    static final String ARGUMENT_KEY_PLAYLIST_PARAMS =
+            "android.media.media_session2.key_playlist_params";
+
     private static final String TAG = "MediaSession2Stub";
     private static final boolean DEBUG = true; // TODO(jaewan): Rename.
 
     private final Object mLock = new Object();
-    private final CommandHandler mCommandHandler;
     private final WeakReference<MediaSession2Impl> mSession;
-    private final Context mContext;
-    private final SessionCallback mSessionCallback;
-    private final MediaLibrarySessionCallback mLibraryCallback;
 
     @GuardedBy("mLock")
     private final ArrayMap<IBinder, ControllerInfo> mControllers = new ArrayMap<>();
 
-    public MediaSession2Stub(MediaSession2Impl session, SessionCallback callback) {
+    public MediaSession2Stub(MediaSession2Impl session) {
         mSession = new WeakReference<>(session);
-        mContext = session.getContext();
-        // TODO(jaewan): Should be executor from the session builder
-        mCommandHandler = new CommandHandler(session.getHandler().getLooper());
-        mSessionCallback = callback;
-        mLibraryCallback = (callback instanceof MediaLibrarySessionCallback)
-                ? (MediaLibrarySessionCallback) callback : null;
     }
 
     public void destroyNotLocked() {
         final List<ControllerInfo> list;
         synchronized (mLock) {
             mSession.clear();
-            mCommandHandler.removeCallbacksAndMessages(null);
             list = getControllers();
             mControllers.clear();
         }
@@ -99,14 +89,60 @@
     }
 
     @Override
-    public void connect(String callingPackage, IMediaSession2Callback callback) {
-        if (callback == null) {
-            // Requesting connect without callback to receive result.
-            return;
-        }
-        ControllerInfo request = new ControllerInfo(mContext,
+    public void connect(String callingPackage, final IMediaSession2Callback callback)
+            throws RuntimeException {
+        final MediaSession2Impl sessionImpl = getSession();
+        final ControllerInfo request = new ControllerInfo(sessionImpl.getContext(),
                 Binder.getCallingUid(), Binder.getCallingPid(), callingPackage, callback);
-        mCommandHandler.postConnect(request);
+        sessionImpl.getCallbackExecutor().execute(() -> {
+            final MediaSession2Impl session = mSession.get();
+            if (session == null) {
+                return;
+            }
+            CommandGroup allowedCommands = session.getCallback().onConnect(request);
+            // Don't reject connection for the request from trusted app.
+            // Otherwise server will fail to retrieve session's information to dispatch
+            // media keys to.
+            boolean accept = allowedCommands != null || request.isTrusted();
+            ControllerInfoImpl impl = ControllerInfoImpl.from(request);
+            if (accept) {
+                synchronized (mLock) {
+                    mControllers.put(impl.getId(), request);
+                }
+                if (allowedCommands == null) {
+                    // For trusted apps, send non-null allowed commands to keep connection.
+                    allowedCommands = new CommandGroup();
+                }
+            }
+            if (DEBUG) {
+                Log.d(TAG, "onConnectResult, request=" + request
+                        + " accept=" + accept);
+            }
+            try {
+                callback.onConnectionChanged(
+                        accept ? MediaSession2Stub.this : null,
+                        allowedCommands == null ? null : allowedCommands.toBundle());
+            } catch (RemoteException e) {
+                // Controller may be died prematurely.
+            }
+            if (accept) {
+                // If connection is accepted, notify the current state to the controller.
+                // It's needed because we cannot call synchronous calls between session/controller.
+                // Note: We're doing this after the onConnectionChanged(), but there's no guarantee
+                //       that events here are notified after the onConnected() because
+                //       IMediaSession2Callback is oneway (i.e. async call) and CallbackStub will
+                //       use thread poll for incoming calls.
+                // TODO(jaewan): Should we protect getting playback state?
+                final PlaybackState2 state = session.getInstance().getPlaybackState();
+                final Bundle bundle = state != null ? state.toBundle() : null;
+                try {
+                    callback.onPlaybackStateChanged(bundle);
+                } catch (RemoteException e) {
+                    // TODO(jaewan): Handle this.
+                    // Controller may be died prematurely.
+                }
+            }
+        });
     }
 
     @Override
@@ -122,20 +158,106 @@
     @Override
     public void sendCommand(IMediaSession2Callback caller, Bundle command, Bundle args)
             throws RuntimeException {
-        ControllerInfo controller = getController(caller);
+        // TODO(jaewan): Generic command
+    }
+
+    @Override
+    public void sendTransportControlCommand(IMediaSession2Callback caller,
+            int commandCode, Bundle args) throws RuntimeException {
+        final MediaSession2Impl sessionImpl = getSession();
+        final ControllerInfo controller = getController(caller);
         if (controller == null) {
             if (DEBUG) {
                 Log.d(TAG, "Command from a controller that hasn't connected. Ignore");
             }
             return;
         }
-        mCommandHandler.postCommand(controller, Command.fromBundle(command), args);
+        sessionImpl.getCallbackExecutor().execute(() -> {
+            final MediaSession2Impl session = mSession.get();
+            if (session == null) {
+                return;
+            }
+            // TODO(jaewan): Sanity check.
+            Command command = new Command(commandCode);
+            boolean accepted = session.getCallback().onCommandRequest(controller, command);
+            if (!accepted) {
+                // Don't run rejected command.
+                if (DEBUG) {
+                    Log.d(TAG, "Command " + commandCode + " from "
+                            + controller + " was rejected by " + session);
+                }
+                return;
+            }
+
+            switch (commandCode) {
+                case MediaSession2.COMMAND_CODE_PLAYBACK_START:
+                    session.getInstance().play();
+                    break;
+                case MediaSession2.COMMAND_CODE_PLAYBACK_PAUSE:
+                    session.getInstance().pause();
+                    break;
+                case MediaSession2.COMMAND_CODE_PLAYBACK_STOP:
+                    session.getInstance().stop();
+                    break;
+                case MediaSession2.COMMAND_CODE_PLAYBACK_SKIP_PREV_ITEM:
+                    session.getInstance().skipToPrevious();
+                    break;
+                case MediaSession2.COMMAND_CODE_PLAYBACK_SKIP_NEXT_ITEM:
+                    session.getInstance().skipToNext();
+                    break;
+                case MediaSession2.COMMAND_CODE_PLAYBACK_PREPARE:
+                    session.getInstance().prepare();
+                    break;
+                case MediaSession2.COMMAND_CODE_PLAYBACK_FAST_FORWARD:
+                    session.getInstance().fastForward();
+                    break;
+                case MediaSession2.COMMAND_CODE_PLAYBACK_REWIND:
+                    session.getInstance().rewind();
+                    break;
+                case MediaSession2.COMMAND_CODE_PLAYBACK_SEEK_TO:
+                    session.getInstance().seekTo(args.getLong(ARGUMENT_KEY_POSITION));
+                    break;
+                case MediaSession2.COMMAND_CODE_PLAYBACK_SET_CURRENT_PLAYLIST_ITEM:
+                    session.getInstance().setCurrentPlaylistItem(
+                            args.getInt(ARGUMENT_KEY_ITEM_INDEX));
+                    break;
+                case MediaSession2.COMMAND_CODE_PLAYBACK_SET_PLAYLIST_PARAMS:
+                    session.getInstance().setPlaylistParams(
+                            PlaylistParams.fromBundle(
+                                    args.getBundle(ARGUMENT_KEY_PLAYLIST_PARAMS)));
+                    break;
+                default:
+                    // TODO(jaewan): Resend unknown (new) commands through the custom command.
+            }
+        });
+    }
+
+    @Override
+    public void sendCustomCommand(final IMediaSession2Callback caller, final Bundle commandBundle,
+            final Bundle args, final ResultReceiver receiver) {
+        final MediaSession2Impl sessionImpl = getSession();
+        final ControllerInfo controller = getController(caller);
+        if (controller == null) {
+            if (DEBUG) {
+                Log.d(TAG, "Command from a controller that hasn't connected. Ignore");
+            }
+            return;
+        }
+        sessionImpl.getCallbackExecutor().execute(() -> {
+            final MediaSession2Impl session = mSession.get();
+            if (session == null) {
+                return;
+            }
+            final Command command = Command.fromBundle(commandBundle);
+            session.getCallback().onCustomCommand(controller, command, args, receiver);
+        });
     }
 
     @Override
     public void getBrowserRoot(IMediaSession2Callback caller, Bundle rootHints)
             throws RuntimeException {
-        if (mLibraryCallback == null) {
+        final MediaSession2Impl sessionImpl = getSession();
+        if (!(sessionImpl.getCallback() instanceof MediaLibrarySessionCallback)) {
             if (DEBUG) {
                 Log.d(TAG, "Session cannot hand getBrowserRoot()");
             }
@@ -148,51 +270,36 @@
             }
             return;
         }
-        mCommandHandler.postOnGetRoot(controller, rootHints);
-    }
-
-    @Deprecated
-    @Override
-    public Bundle getPlaybackState() throws RemoteException {
-        MediaSession2Impl session = getSession();
-        // TODO(jaewan): Check if mPlayer.getPlaybackState() is safe here.
-        return session.getInstance().getPlayer().getPlaybackState().toBundle();
-    }
-
-    @Deprecated
-    @Override
-    public void registerCallback(final IMediaSession2Callback callbackBinder,
-            final int callbackFlag, final int requestCode) throws RemoteException {
-        // TODO(jaewan): Call onCommand() here. To do so, you should pend message.
-        synchronized (mLock) {
-            ControllerInfo controllerInfo = getController(callbackBinder);
-            if (controllerInfo == null) {
+        sessionImpl.getCallbackExecutor().execute(() -> {
+            final MediaSession2Impl session = mSession.get();
+            if (session == null) {
                 return;
             }
-            ControllerInfoImpl.from(controllerInfo).addFlag(callbackFlag);
-        }
-    }
-
-    @Deprecated
-    @Override
-    public void unregisterCallback(IMediaSession2Callback callbackBinder, int callbackFlag)
-            throws RemoteException {
-        // TODO(jaewan): Call onCommand() here. To do so, you should pend message.
-        synchronized (mLock) {
-            ControllerInfo controllerInfo = getController(callbackBinder);
-            if (controllerInfo == null) {
-                return;
+            final MediaLibrarySessionCallback libraryCallback =
+                    (MediaLibrarySessionCallback) session.getCallback();
+            final ControllerInfoImpl controllerImpl = ControllerInfoImpl.from(controller);
+            BrowserRoot root = libraryCallback.onGetRoot(controller, rootHints);
+            try {
+                controllerImpl.getControllerBinder().onGetRootResult(rootHints,
+                        root == null ? null : root.getRootId(),
+                        root == null ? null : root.getExtras());
+            } catch (RemoteException e) {
+                // Controller may be died prematurely.
+                // TODO(jaewan): Handle this.
             }
-            ControllerInfoImpl.from(controllerInfo).removeFlag(callbackFlag);
-        }
+        });
     }
 
     private ControllerInfo getController(IMediaSession2Callback caller) {
+        // TODO(jaewan): Device a way to return connection-in-progress-controller
+        //               to be included here, because session owner may want to send some datas
+        //               while onConnected() hasn't returned.
         synchronized (mLock) {
             return mControllers.get(caller.asBinder());
         }
     }
 
+    // TODO(jaewan): Need a way to get controller with permissions
     public List<ControllerInfo> getControllers() {
         ArrayList<ControllerInfo> controllers = new ArrayList<>();
         synchronized (mLock) {
@@ -203,27 +310,15 @@
         return controllers;
     }
 
-    public List<ControllerInfo> getControllersWithFlag(int flag) {
-        ArrayList<ControllerInfo> controllers = new ArrayList<>();
-        synchronized (mLock) {
-            for (int i = 0; i < mControllers.size(); i++) {
-                ControllerInfo controllerInfo = mControllers.valueAt(i);
-                if (ControllerInfoImpl.from(controllerInfo).containsFlag(flag)) {
-                    controllers.add(controllerInfo);
-                }
-            }
-        }
-        return controllers;
-    }
-
     // Should be used without a lock to prevent potential deadlock.
     public void notifyPlaybackStateChangedNotLocked(PlaybackState2 state) {
-        final List<ControllerInfo> list = getControllersWithFlag(CALLBACK_FLAG_PLAYBACK);
+        final List<ControllerInfo> list = getControllers();
         for (int i = 0; i < list.size(); i++) {
             IMediaSession2Callback callbackBinder =
                     ControllerInfoImpl.from(list.get(i)).getControllerBinder();
             try {
-                callbackBinder.onPlaybackStateChanged(state.toBundle());
+                final Bundle bundle = state != null ? state.toBundle() : null;
+                callbackBinder.onPlaybackStateChanged(bundle);
             } catch (RemoteException e) {
                 Log.w(TAG, "Controller is gone", e);
                 // TODO(jaewan): What to do when the controller is gone?
@@ -251,130 +346,83 @@
         }
     }
 
-    // TODO(jaewan): Remove this. We should use Executor given by the session builder.
-    private class CommandHandler extends Handler {
-        public static final int MSG_CONNECT = 1000;
-        public static final int MSG_COMMAND = 1001;
-        public static final int MSG_ON_GET_ROOT = 2000;
-
-        public CommandHandler(Looper looper) {
-            super(looper);
+    public void notifyPlaylistChanged(List<MediaItem2> playlist) {
+        if (playlist == null) {
+            return;
         }
-
-        @Override
-        public void handleMessage(Message msg) {
-            final MediaSession2Impl session = MediaSession2Stub.this.mSession.get();
-            if (session == null || session.getPlayer() == null) {
-                return;
-            }
-
-            switch (msg.what) {
-                case MSG_CONNECT: {
-                    ControllerInfo request = (ControllerInfo) msg.obj;
-                    CommandGroup allowedCommands = mSessionCallback.onConnect(request);
-                    // Don't reject connection for the request from trusted app.
-                    // Otherwise server will fail to retrieve session's information to dispatch
-                    // media keys to.
-                    boolean accept = allowedCommands != null || request.isTrusted();
-                    ControllerInfoImpl impl = ControllerInfoImpl.from(request);
-                    if (accept) {
-                        synchronized (mLock) {
-                            mControllers.put(impl.getId(), request);
-                        }
-                        if (allowedCommands == null) {
-                            // For trusted apps, send non-null allowed commands to keep connection.
-                            allowedCommands = new CommandGroup();
-                        }
-                    }
-                    if (DEBUG) {
-                        Log.d(TAG, "onConnectResult, request=" + request
-                                + " accept=" + accept);
-                    }
-                    try {
-                        impl.getControllerBinder().onConnectionChanged(
-                                accept ? MediaSession2Stub.this : null,
-                                allowedCommands == null ? null : allowedCommands.toBundle());
-                    } catch (RemoteException e) {
-                        // Controller may be died prematurely.
-                    }
-                    break;
-                }
-                case MSG_COMMAND: {
-                    CommandParam param = (CommandParam) msg.obj;
-                    Command command = param.command;
-                    boolean accepted = mSessionCallback.onCommandRequest(
-                            param.controller, command);
-                    if (!accepted) {
-                        // Don't run rejected command.
-                        if (DEBUG) {
-                            Log.d(TAG, "Command " + command + " from "
-                                    + param.controller + " was rejected by " + session);
-                        }
-                        return;
-                    }
-
-                    switch (param.command.getCommandCode()) {
-                        case MediaSession2.COMMAND_CODE_PLAYBACK_START:
-                            session.getInstance().play();
-                            break;
-                        case MediaSession2.COMMAND_CODE_PLAYBACK_PAUSE:
-                            session.getInstance().pause();
-                            break;
-                        case MediaSession2.COMMAND_CODE_PLAYBACK_STOP:
-                            session.getInstance().stop();
-                            break;
-                        case MediaSession2.COMMAND_CODE_PLAYBACK_SKIP_PREV_ITEM:
-                            session.getInstance().skipToPrevious();
-                            break;
-                        case MediaSession2.COMMAND_CODE_PLAYBACK_SKIP_NEXT_ITEM:
-                            session.getInstance().skipToNext();
-                            break;
-                        default:
-                            // TODO(jaewan): Handle custom command.
-                    }
-                    break;
-                }
-                case MSG_ON_GET_ROOT: {
-                    final CommandParam param = (CommandParam) msg.obj;
-                    final ControllerInfoImpl controller = ControllerInfoImpl.from(param.controller);
-                    BrowserRoot root = mLibraryCallback.onGetRoot(param.controller, param.args);
-                    try {
-                        controller.getControllerBinder().onGetRootResult(param.args,
-                                root == null ? null : root.getRootId(),
-                                root == null ? null : root.getExtras());
-                    } catch (RemoteException e) {
-                        // Controller may be died prematurely.
-                        // TODO(jaewan): Handle this.
-                    }
-                    break;
+        final List<Bundle> bundleList = new ArrayList<>();
+        for (int i = 0; i < playlist.size(); i++) {
+            if (playlist.get(i) != null) {
+                Bundle bundle = playlist.get(i).toBundle();
+                if (bundle != null) {
+                    bundleList.add(bundle);
                 }
             }
         }
-
-        public void postConnect(ControllerInfo request) {
-            obtainMessage(MSG_CONNECT, request).sendToTarget();
-        }
-
-        public void postCommand(ControllerInfo controller, Command command, Bundle args) {
-            CommandParam param = new CommandParam(controller, command, args);
-            obtainMessage(MSG_COMMAND, param).sendToTarget();
-        }
-
-        public void postOnGetRoot(ControllerInfo controller, Bundle rootHints) {
-            CommandParam param = new CommandParam(controller, null, rootHints);
-            obtainMessage(MSG_ON_GET_ROOT, param).sendToTarget();
+        final List<ControllerInfo> list = getControllers();
+        for (int i = 0; i < list.size(); i++) {
+            IMediaSession2Callback callbackBinder =
+                    ControllerInfoImpl.from(list.get(i)).getControllerBinder();
+            try {
+                callbackBinder.onPlaylistChanged(bundleList);
+            } catch (RemoteException e) {
+                Log.w(TAG, "Controller is gone", e);
+                // TODO(jaewan): What to do when the controller is gone?
+            }
         }
     }
 
-    private static class CommandParam {
-        public final ControllerInfo controller;
-        public final Command command;
-        public final Bundle args;
+    public void notifyPlaylistParamsChanged(MediaSession2.PlaylistParams params) {
+        final List<ControllerInfo> list = getControllers();
+        for (int i = 0; i < list.size(); i++) {
+            IMediaSession2Callback callbackBinder =
+                    ControllerInfoImpl.from(list.get(i)).getControllerBinder();
+            try {
+                callbackBinder.onPlaylistParamsChanged(params.toBundle());
+            } catch (RemoteException e) {
+                Log.w(TAG, "Controller is gone", e);
+                // TODO(jaewan): What to do when the controller is gone?
+            }
+        }
+    }
 
-        private CommandParam(ControllerInfo controller, Command command, Bundle args) {
-            this.controller = controller;
-            this.command = command;
-            this.args = args;
+    public void sendCustomCommand(ControllerInfo controller, Command command, Bundle args,
+            ResultReceiver receiver) {
+        if (receiver != null && controller == null) {
+            throw new IllegalArgumentException("Controller shouldn't be null if result receiver is"
+                    + " specified");
+        }
+        if (command == null) {
+            throw new IllegalArgumentException("command shouldn't be null");
+        }
+        final IMediaSession2Callback callbackBinder =
+                ControllerInfoImpl.from(controller).getControllerBinder();
+        if (getController(callbackBinder) == null) {
+            throw new IllegalArgumentException("Controller is gone");
+        }
+        sendCustomCommandInternal(controller, command, args, receiver);
+    }
+
+    public void sendCustomCommand(Command command, Bundle args) {
+        if (command == null) {
+            throw new IllegalArgumentException("command shouldn't be null");
+        }
+        final List<ControllerInfo> controllers = getControllers();
+        for (int i = 0; i < controllers.size(); i++) {
+            sendCustomCommand(controllers.get(i), command, args, null);
+        }
+    }
+
+    private void sendCustomCommandInternal(ControllerInfo controller, Command command, Bundle args,
+            ResultReceiver receiver) {
+        final IMediaSession2Callback callbackBinder =
+                ControllerInfoImpl.from(controller).getControllerBinder();
+        try {
+            Bundle commandBundle = command.toBundle();
+            callbackBinder.sendCustomCommand(commandBundle, args, receiver);
+        } catch (RemoteException e) {
+            Log.w(TAG, "Controller is gone", e);
+            // TODO(jaewan): What to do when the controller is gone?
         }
     }
 }
diff --git a/packages/MediaComponents/src/com/android/media/MediaSessionService2Impl.java b/packages/MediaComponents/src/com/android/media/MediaSessionService2Impl.java
index 9d24082..7dce109 100644
--- a/packages/MediaComponents/src/com/android/media/MediaSessionService2Impl.java
+++ b/packages/MediaComponents/src/com/android/media/MediaSessionService2Impl.java
@@ -20,17 +20,16 @@
 
 import android.app.NotificationManager;
 import android.content.Intent;
-import android.content.pm.PackageManager;
-import android.content.pm.ResolveInfo;
-import android.media.MediaPlayerBase.PlaybackListener;
+import android.media.MediaPlayerInterface.PlaybackListener;
 import android.media.MediaSession2;
 import android.media.MediaSessionService2;
 import android.media.MediaSessionService2.MediaNotification;
 import android.media.PlaybackState2;
+import android.media.SessionToken2;
+import android.media.SessionToken2.TokenType;
 import android.media.session.PlaybackState;
 import android.media.update.MediaSessionService2Provider;
 import android.os.IBinder;
-import android.os.Looper;
 import android.support.annotation.GuardedBy;
 import android.util.Log;
 
@@ -82,39 +81,27 @@
                 NOTIFICATION_SERVICE);
         mStartSelfIntent = new Intent(mInstance, mInstance.getClass());
 
-        Intent serviceIntent = createServiceIntent();
-        ResolveInfo resolveInfo = mInstance.getPackageManager()
-                .resolveService(serviceIntent, PackageManager.GET_META_DATA);
-        String id;
-        if (resolveInfo == null || resolveInfo.serviceInfo == null) {
-            throw new IllegalArgumentException("service " + mInstance + " doesn't implement"
-                    + serviceIntent.getAction());
-        } else if (resolveInfo.serviceInfo.metaData == null) {
-            if (DEBUG) {
-                Log.d(TAG, "Failed to resolve ID for " + mInstance + ". Using empty id");
-            }
-            id = "";
-        } else {
-            id = resolveInfo.serviceInfo.metaData.getString(
-                    MediaSessionService2.SERVICE_META_DATA, "");
+        SessionToken2 token = new SessionToken2(mInstance, mInstance.getPackageName(),
+                mInstance.getClass().getName());
+        if (token.getType() != getSessionType()) {
+            throw new RuntimeException("Expected session service, but was " + token.getType());
         }
-        mSession = mInstance.onCreateSession(id);
-        if (mSession == null || !id.equals(mSession.getToken().getId())) {
-            throw new RuntimeException("Expected session with id " + id + ", but got " + mSession);
+        mSession = mInstance.onCreateSession(token.getId());
+        if (mSession == null || !token.getId().equals(mSession.getToken().getId())) {
+            throw new RuntimeException("Expected session with id " + token.getId()
+                    + ", but got " + mSession);
         }
         // TODO(jaewan): Uncomment here.
         // mSession.addPlaybackListener(mListener, mSession.getExecutor());
     }
 
-    Intent createServiceIntent() {
-        Intent serviceIntent = new Intent(mInstance, mInstance.getClass());
-        serviceIntent.setAction(MediaSessionService2.SERVICE_INTERFACE);
-        return serviceIntent;
+    @TokenType int getSessionType() {
+        return SessionToken2.TYPE_SESSION_SERVICE;
     }
 
     public IBinder onBind_impl(Intent intent) {
         if (MediaSessionService2.SERVICE_INTERFACE.equals(intent.getAction())) {
-            return mSession.getToken().getSessionBinder().asBinder();
+            return SessionToken2Impl.from(mSession.getToken()).getSessionBinder().asBinder();
         }
         return null;
     }
@@ -152,11 +139,6 @@
                 return;
             }
             MediaSession2Impl impl = (MediaSession2Impl) mSession.getProvider();
-            if (impl.getHandler().getLooper() != Looper.myLooper()) {
-                Log.w(TAG, "Ignoring " + state + ". Expected " + impl.getHandler().getLooper()
-                        + " but " + Looper.myLooper());
-                return;
-            }
             updateNotification(state);
         }
     }
diff --git a/packages/MediaComponents/src/com/android/media/PlaybackListenerHolder.java b/packages/MediaComponents/src/com/android/media/PlaybackListenerHolder.java
index 7b336c4..4241f85 100644
--- a/packages/MediaComponents/src/com/android/media/PlaybackListenerHolder.java
+++ b/packages/MediaComponents/src/com/android/media/PlaybackListenerHolder.java
@@ -16,11 +16,9 @@
 
 package com.android.media;
 
-import android.media.MediaPlayerBase.PlaybackListener;
+import android.media.MediaPlayerInterface.PlaybackListener;
 import android.media.PlaybackState2;
-import android.media.session.PlaybackState;
 import android.os.Handler;
-import android.os.Message;
 import android.support.annotation.NonNull;
 
 import java.util.List;
diff --git a/packages/MediaComponents/src/com/android/media/SessionToken2Impl.java b/packages/MediaComponents/src/com/android/media/SessionToken2Impl.java
new file mode 100644
index 0000000..c91a89c
--- /dev/null
+++ b/packages/MediaComponents/src/com/android/media/SessionToken2Impl.java
@@ -0,0 +1,248 @@
+/*
+ * 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.media;
+
+import static android.media.SessionToken2.TYPE_SESSION;
+import static android.media.SessionToken2.TYPE_SESSION_SERVICE;
+import static android.media.SessionToken2.TYPE_LIBRARY_SERVICE;
+
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.content.pm.ResolveInfo;
+import android.media.MediaLibraryService2;
+import android.media.MediaSessionService2;
+import android.media.SessionToken2;
+import android.media.SessionToken2.TokenType;
+import android.media.update.SessionToken2Provider;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.text.TextUtils;
+
+public class SessionToken2Impl implements SessionToken2Provider {
+    private static final String KEY_UID = "android.media.token.uid";
+    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 SessionToken2 mInstance;
+    private final int mUid;
+    private final @TokenType int mType;
+    private final String mPackageName;
+    private final String mServiceName;
+    private final String mId;
+    private final IMediaSession2 mSessionBinder;
+
+    /**
+     * Public constructor for the legacy support (i.e. browser can try connecting to any browser service
+     * if it knows the service name)
+     */
+    public SessionToken2Impl(Context context, SessionToken2 instance,
+            String packageName, String serviceName, int uid) {
+        if (TextUtils.isEmpty(packageName)) {
+            throw new IllegalArgumentException("package name shouldn't be null");
+        }
+        if (TextUtils.isEmpty(serviceName)) {
+            throw new IllegalArgumentException("service name shouldn't be null");
+        }
+        mInstance = instance;
+        // Calculate uid if it's not specified.
+        final PackageManager manager = context.getPackageManager();
+        if (uid < 0) {
+            try {
+                uid = manager.getPackageUid(packageName, 0);
+            } catch (NameNotFoundException e) {
+                throw new IllegalArgumentException("Cannot find package " + packageName);
+            }
+        }
+        mUid = uid;
+        // calculate id and type
+        Intent serviceIntent = new Intent(MediaLibraryService2.SERVICE_INTERFACE);
+        serviceIntent.setClassName(packageName, serviceName);
+        String id = getSessionId(manager.resolveService(serviceIntent,
+                PackageManager.GET_META_DATA));
+        int type = TYPE_LIBRARY_SERVICE;
+        if (id == null) {
+            // retry with session service
+            serviceIntent.setClassName(packageName, serviceName);
+            id = getSessionId(manager.resolveService(serviceIntent,
+                    PackageManager.GET_META_DATA));
+            type = TYPE_SESSION_SERVICE;
+        }
+        if (id == null) {
+            throw new IllegalArgumentException("service " + serviceName + " doesn't implement"
+                    + " session service nor library service");
+        }
+        mId = id;
+        mType = type;
+        mPackageName = packageName;
+        mServiceName = serviceName;
+        mSessionBinder = null;
+    }
+
+    public SessionToken2Impl(Context context, int uid, int type,
+            String packageName, String serviceName, String id, IMediaSession2 sessionBinder) {
+        // TODO(jaewan): Add sanity check
+        mUid = uid;
+        mType = type;
+        mPackageName = packageName;
+        mServiceName = serviceName;
+        mId = id;
+        mSessionBinder = sessionBinder;
+        mInstance = new SessionToken2(this);
+    }
+
+    public static String getSessionId(ResolveInfo resolveInfo) {
+        if (resolveInfo == null || resolveInfo.serviceInfo == null) {
+            return null;
+        } else if (resolveInfo.serviceInfo.metaData == null) {
+            return "";
+        } else {
+            return resolveInfo.serviceInfo.metaData.getString(
+                    MediaSessionService2.SERVICE_META_DATA, "");
+        }
+    }
+
+    public SessionToken2 getInstance() {
+        return mInstance;
+    }
+
+    @Override
+    public String getPackageName_impl() {
+        return mPackageName;
+    }
+
+    @Override
+    public int getUid_impl() {
+        return mUid;
+    }
+
+    @Override
+    public String getId_imp() {
+        return mId;
+    }
+
+    @Override
+    public int getType_impl() {
+        return mType;
+    }
+
+    public String getServiceName() {
+        return mServiceName;
+    }
+
+    public IMediaSession2 getSessionBinder() {
+        return mSessionBinder;
+    }
+
+    public static SessionToken2 fromBundle(Context context, Bundle bundle) {
+        if (bundle == null) {
+            return null;
+        }
+        final int uid = bundle.getInt(KEY_UID);
+        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 == null) {
+                    throw new IllegalArgumentException("Unexpected sessionBinder for session,"
+                            + " binder=" + sessionBinder);
+                }
+                break;
+            case TYPE_SESSION_SERVICE:
+            case TYPE_LIBRARY_SERVICE:
+                if (TextUtils.isEmpty(serviceName)) {
+                    throw new IllegalArgumentException("Session service needs service name");
+                }
+                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 SessionToken2Impl(context, uid, type, packageName, serviceName, id,
+                sessionBinder != null ? IMediaSession2.Stub.asInterface(sessionBinder) : null)
+                .getInstance();
+    }
+
+    @Override
+    public Bundle toBundle_impl() {
+        Bundle bundle = new Bundle();
+        bundle.putInt(KEY_UID, mUid);
+        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;
+    }
+
+    @Override
+    public int hashCode_impl() {
+        final int prime = 31;
+        return mType
+                + prime * (mUid
+                + prime * (mPackageName.hashCode()
+                + prime * (mId.hashCode()
+                + prime * ((mServiceName != null ? mServiceName.hashCode() : 0)
+                + prime * (mSessionBinder != null ? mSessionBinder.asBinder().hashCode() : 0)))));
+    }
+
+    @Override
+    public boolean equals_impl(Object obj) {
+        if (!(obj instanceof SessionToken2)) {
+            return false;
+        }
+        SessionToken2Impl other = from((SessionToken2) obj);
+        if (mUid != other.mUid
+                || !TextUtils.equals(mPackageName, other.mPackageName)
+                || !TextUtils.equals(mServiceName, other.mServiceName)
+                || !TextUtils.equals(mId, other.mId)
+                || mType != other.mType) {
+            return false;
+        }
+        if (mSessionBinder == other.mSessionBinder) {
+            return true;
+        } else if (mSessionBinder == null || other.mSessionBinder == null) {
+            return false;
+        }
+        return mSessionBinder.asBinder().equals(other.mSessionBinder.asBinder());
+    }
+
+    @Override
+    public String toString_impl() {
+        return "SessionToken {pkg=" + mPackageName + " id=" + mId + " type=" + mType
+                + " service=" + mServiceName + " binder=" + mSessionBinder + "}";
+    }
+
+    public static SessionToken2Impl from(SessionToken2 token) {
+        return ((SessionToken2Impl) token.getProvider());
+    }
+}
diff --git a/packages/MediaComponents/src/com/android/media/update/ApiFactory.java b/packages/MediaComponents/src/com/android/media/update/ApiFactory.java
index 8ce7bef..b9d7612 100644
--- a/packages/MediaComponents/src/com/android/media/update/ApiFactory.java
+++ b/packages/MediaComponents/src/com/android/media/update/ApiFactory.java
@@ -20,42 +20,53 @@
 import android.content.Context;
 import android.content.res.Resources;
 import android.content.res.Resources.Theme;
+import android.media.DataSourceDesc;
 import android.media.MediaBrowser2;
 import android.media.MediaBrowser2.BrowserCallback;
 import android.media.MediaController2;
 import android.media.MediaController2.ControllerCallback;
+import android.media.MediaItem2;
 import android.media.MediaLibraryService2;
 import android.media.MediaLibraryService2.MediaLibrarySession;
 import android.media.MediaLibraryService2.MediaLibrarySessionCallback;
-import android.media.MediaPlayerBase;
+import android.media.MediaMetadata2;
+import android.media.MediaPlayerInterface;
 import android.media.MediaSession2;
 import android.media.MediaSession2.ControllerInfo;
 import android.media.MediaSession2.SessionCallback;
 import android.media.MediaSessionService2;
-import android.media.IMediaSession2Callback;
+import android.media.SessionPlayer2;
 import android.media.SessionToken2;
 import android.media.VolumeProvider;
 import android.media.update.MediaBrowser2Provider;
 import android.media.update.MediaControlView2Provider;
 import android.media.update.MediaController2Provider;
+import android.media.update.MediaItem2Provider;
 import android.media.update.MediaLibraryService2Provider.MediaLibrarySessionProvider;
 import android.media.update.MediaSession2Provider;
 import android.media.update.MediaSessionService2Provider;
+import android.media.update.SessionPlayer2Provider;
+import android.media.update.SessionToken2Provider;
 import android.media.update.VideoView2Provider;
 import android.media.update.StaticProvider;
 import android.media.update.ViewProvider;
+import android.os.Bundle;
 import android.os.IInterface;
 import android.support.annotation.Nullable;
 import android.util.AttributeSet;
 import android.widget.MediaControlView2;
 import android.widget.VideoView2;
 
+import com.android.media.IMediaSession2;
+import com.android.media.IMediaSession2Callback;
 import com.android.media.MediaBrowser2Impl;
 import com.android.media.MediaController2Impl;
+import com.android.media.MediaItem2Impl;
 import com.android.media.MediaLibraryService2Impl;
 import com.android.media.MediaLibraryService2Impl.MediaLibrarySessionImpl;
 import com.android.media.MediaSession2Impl;
 import com.android.media.MediaSessionService2Impl;
+import com.android.media.SessionToken2Impl;
 import com.android.widget.MediaControlView2Impl;
 import com.android.widget.VideoView2Impl;
 
@@ -83,7 +94,7 @@
 
     @Override
     public MediaSession2Provider createMediaSession2(Context context, MediaSession2 instance,
-            MediaPlayerBase player, String id, VolumeProvider volumeProvider,
+            MediaPlayerInterface player, String id, VolumeProvider volumeProvider,
             int ratingType, PendingIntent sessionActivity, Executor callbackExecutor,
             SessionCallback callback) {
         return new MediaSession2Impl(context, instance, player, id, volumeProvider, ratingType,
@@ -112,7 +123,7 @@
 
     @Override
     public MediaLibrarySessionProvider createMediaLibraryService2MediaLibrarySession(
-            Context context, MediaLibrarySession instance, MediaPlayerBase player,
+            Context context, MediaLibrarySession instance, MediaPlayerInterface player,
             String id, VolumeProvider volumeProvider, int ratingType, PendingIntent sessionActivity,
             Executor callbackExecutor, MediaLibrarySessionCallback callback) {
         return new MediaLibrarySessionImpl(context, instance, player, id, volumeProvider,
@@ -131,4 +142,32 @@
             @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
         return new VideoView2Impl(instance, superProvider, attrs, defStyleAttr, defStyleRes);
     }
+
+    @Override
+    public SessionToken2Provider createSessionToken2(Context context, SessionToken2 instance,
+            String packageName, String serviceName, int uid) {
+        return new SessionToken2Impl(context, instance, packageName, serviceName, uid);
+    }
+
+    @Override
+    public SessionToken2 SessionToken2_fromBundle(Context context, Bundle bundle) {
+        return SessionToken2Impl.fromBundle(context, bundle);
+    }
+
+    @Override
+    public SessionPlayer2Provider createSessionPlayer2(Context context, SessionPlayer2 instance) {
+        // TODO(jaewan): Implement this
+        return null;
+    }
+
+    @Override
+    public MediaItem2Provider createMediaItem2Provider(Context context, MediaItem2 instance,
+            String mediaId, DataSourceDesc dsd, MediaMetadata2 metadata, int flags) {
+        return new MediaItem2Impl(context, instance, mediaId, dsd, metadata, flags);
+    }
+
+    @Override
+    public MediaItem2 fromBundle_MediaItem2(Context context, Bundle bundle) {
+        return MediaItem2Impl.fromBundle(context, bundle);
+    }
 }
diff --git a/packages/MediaComponents/src/com/android/widget/MediaControlView2Impl.java b/packages/MediaComponents/src/com/android/widget/MediaControlView2Impl.java
index bc370d8..688aadc 100644
--- a/packages/MediaComponents/src/com/android/widget/MediaControlView2Impl.java
+++ b/packages/MediaComponents/src/com/android/widget/MediaControlView2Impl.java
@@ -24,11 +24,11 @@
 import android.media.update.ViewProvider;
 import android.os.Bundle;
 import android.util.Log;
-import android.view.KeyEvent;
 import android.view.MotionEvent;
 import android.view.View;
 import android.view.ViewGroup;
 import android.view.accessibility.AccessibilityManager;
+import android.widget.Button;
 import android.widget.ImageButton;
 import android.widget.MediaControlView2;
 import android.widget.ProgressBar;
@@ -40,6 +40,7 @@
 import com.android.media.update.R;
 
 import java.util.Formatter;
+import java.util.List;
 import java.util.Locale;
 
 public class MediaControlView2Impl implements MediaControlView2Provider {
@@ -48,10 +49,6 @@
     private final MediaControlView2 mInstance;
     private final ViewProvider mSuperProvider;
 
-    static final String COMMAND_SHOW_SUBTITLE = "showSubtitle";
-    static final String COMMAND_HIDE_SUBTITLE = "hideSubtitle";
-    static final String COMMAND_SET_FULLSCREEN = "setFullscreen";
-
     static final String ARGUMENT_KEY_FULLSCREEN = "fullScreen";
 
     static final String KEY_STATE_CONTAINS_SUBTITLE = "StateContainsSubtitle";
@@ -84,7 +81,6 @@
     private boolean mSubtitleIsEnabled;
     private boolean mContainsSubtitle;
     private boolean mSeekAvailable;
-    private View.OnClickListener mNextListener, mPrevListener;
     private ImageButton mPlayPauseButton;
     private ImageButton mFfwdButton;
     private ImageButton mRewButton;
@@ -97,6 +93,7 @@
     private ImageButton mOverflowButtonRight;
 
     private ViewGroup mExtraControls;
+    private ViewGroup mCustomButtons;
     private ImageButton mOverflowButtonLeft;
     private ImageButton mMuteButton;
     private ImageButton mAspectRationButton;
@@ -142,7 +139,7 @@
     }
 
     @Override
-    public void show_impl(int timeout) {
+    public void show_impl(long timeout) {
         if (!mShowing) {
             setProgress();
             if (mPlayPauseButton != null) {
@@ -184,92 +181,65 @@
     }
 
     @Override
-    public void showSubtitle_impl() {
-        mController.sendCommand(COMMAND_SHOW_SUBTITLE, null, null);
-    }
-
-    @Override
-    public void hideSubtitle_impl() {
-        mController.sendCommand(COMMAND_HIDE_SUBTITLE, null, null);
-    }
-
-    @Override
-    public void setPrevNextListeners_impl(View.OnClickListener next, View.OnClickListener prev) {
-        mNextListener = next;
-        mPrevListener = prev;
-
-        if (mNextButton != null) {
-            mNextButton.setOnClickListener(mNextListener);
-            mNextButton.setEnabled(mNextListener != null);
-            mNextButton.setVisibility(View.VISIBLE);
-        }
-        if (mPrevButton != null) {
-            mPrevButton.setOnClickListener(mPrevListener);
-            mPrevButton.setEnabled(mPrevListener != null);
-            mPrevButton.setVisibility(View.VISIBLE);
-        }
-    }
-
-    @Override
-    public void setButtonVisibility_impl(int button, boolean visible) {
+    public void setButtonVisibility_impl(int button, int visibility) {
         switch (button) {
             case MediaControlView2.BUTTON_PLAY_PAUSE:
                 if (mPlayPauseButton != null && canPause()) {
-                    mPlayPauseButton.setVisibility((visible) ? View.VISIBLE : View.GONE);
+                    mPlayPauseButton.setVisibility(visibility);
                 }
                 break;
             case MediaControlView2.BUTTON_FFWD:
                 if (mFfwdButton != null && canSeekForward()) {
-                    mFfwdButton.setVisibility((visible) ? View.VISIBLE : View.GONE);
+                    mFfwdButton.setVisibility(visibility);
                 }
                 break;
             case MediaControlView2.BUTTON_REW:
                 if (mRewButton != null && canSeekBackward()) {
-                    mRewButton.setVisibility((visible) ? View.VISIBLE : View.GONE);
+                    mRewButton.setVisibility(visibility);
                 }
                 break;
             case MediaControlView2.BUTTON_NEXT:
                 // TODO: this button is not visible unless its listener is manually set. Should this
                 // function still be provided?
                 if (mNextButton != null) {
-                    mNextButton.setVisibility((visible) ? View.VISIBLE : View.GONE);
+                    mNextButton.setVisibility(visibility);
                 }
                 break;
             case MediaControlView2.BUTTON_PREV:
                 // TODO: this button is not visible unless its listener is manually set. Should this
                 // function still be provided?
                 if (mPrevButton != null) {
-                    mPrevButton.setVisibility((visible) ? View.VISIBLE : View.GONE);
+                    mPrevButton.setVisibility(visibility);
                 }
                 break;
             case MediaControlView2.BUTTON_SUBTITLE:
                 if (mSubtitleButton != null && mContainsSubtitle) {
-                    mSubtitleButton.setVisibility((visible) ? View.VISIBLE : View.GONE);
+                    mSubtitleButton.setVisibility(visibility);
                 }
                 break;
             case MediaControlView2.BUTTON_FULL_SCREEN:
                 if (mFullScreenButton != null) {
-                    mFullScreenButton.setVisibility((visible) ? View.VISIBLE : View.GONE);
+                    mFullScreenButton.setVisibility(visibility);
                 }
                 break;
             case MediaControlView2.BUTTON_OVERFLOW:
                 if (mOverflowButtonRight != null) {
-                    mOverflowButtonRight.setVisibility((visible) ? View.VISIBLE : View.GONE);
+                    mOverflowButtonRight.setVisibility(visibility);
                 }
                 break;
             case MediaControlView2.BUTTON_MUTE:
                 if (mMuteButton != null) {
-                    mMuteButton.setVisibility((visible) ? View.VISIBLE : View.GONE);
+                    mMuteButton.setVisibility(visibility);
                 }
                 break;
             case MediaControlView2.BUTTON_ASPECT_RATIO:
                 if (mAspectRationButton != null) {
-                    mAspectRationButton.setVisibility((visible) ? View.VISIBLE : View.GONE);
+                    mAspectRationButton.setVisibility(visibility);
                 }
                 break;
             case MediaControlView2.BUTTON_SETTINGS:
                 if (mSettingsButton != null) {
-                    mSettingsButton.setVisibility((visible) ? View.VISIBLE : View.GONE);
+                    mSettingsButton.setVisibility(visibility);
                 }
                 break;
             default:
@@ -305,62 +275,11 @@
     }
 
     @Override
-    public boolean onKeyDown_impl(int keyCode, KeyEvent event) {
-        return mSuperProvider.onKeyDown_impl(keyCode, event);
-    }
-
-    @Override
     public void onFinishInflate_impl() {
         mSuperProvider.onFinishInflate_impl();
     }
 
     @Override
-    public boolean dispatchKeyEvent_impl(KeyEvent event) {
-        int keyCode = event.getKeyCode();
-        final boolean uniqueDown = event.getRepeatCount() == 0
-                && event.getAction() == KeyEvent.ACTION_DOWN;
-        if (keyCode == KeyEvent.KEYCODE_HEADSETHOOK
-                || keyCode == KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE
-                || keyCode == KeyEvent.KEYCODE_SPACE) {
-            if (uniqueDown) {
-                togglePausePlayState();
-                mInstance.show(DEFAULT_TIMEOUT_MS);
-                if (mPlayPauseButton != null) {
-                    mPlayPauseButton.requestFocus();
-                }
-            }
-            return true;
-        } else if (keyCode == KeyEvent.KEYCODE_MEDIA_PLAY) {
-            if (uniqueDown && !isPlaying()) {
-                togglePausePlayState();
-                mInstance.show(DEFAULT_TIMEOUT_MS);
-            }
-            return true;
-        } else if (keyCode == KeyEvent.KEYCODE_MEDIA_STOP
-                || keyCode == KeyEvent.KEYCODE_MEDIA_PAUSE) {
-            if (uniqueDown && isPlaying()) {
-                togglePausePlayState();
-                mInstance.show(DEFAULT_TIMEOUT_MS);
-            }
-            return true;
-        } else if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN
-                || keyCode == KeyEvent.KEYCODE_VOLUME_UP
-                || keyCode == KeyEvent.KEYCODE_VOLUME_MUTE
-                || keyCode == KeyEvent.KEYCODE_CAMERA) {
-            // don't show the controls for volume adjustment
-            return mSuperProvider.dispatchKeyEvent_impl(event);
-        } else if (keyCode == KeyEvent.KEYCODE_BACK || keyCode == KeyEvent.KEYCODE_MENU) {
-            if (uniqueDown) {
-                mInstance.hide();
-            }
-            return true;
-        }
-
-        mInstance.show(DEFAULT_TIMEOUT_MS);
-        return mSuperProvider.dispatchKeyEvent_impl(event);
-    }
-
-    @Override
     public void setEnabled_impl(boolean enabled) {
         if (mPlayPauseButton != null) {
             mPlayPauseButton.setEnabled(enabled);
@@ -475,11 +394,11 @@
         }
         mNextButton = v.findViewById(R.id.next);
         if (mNextButton != null) {
-            mNextButton.setVisibility(View.GONE);
+            mNextButton.setOnClickListener(mNextListener);
         }
         mPrevButton = v.findViewById(R.id.prev);
         if (mPrevButton != null) {
-            mPrevButton.setVisibility(View.GONE);
+            mPrevButton.setOnClickListener(mPrevListener);
         }
 
         mBasicControls = v.findViewById(R.id.basic_controls);
@@ -501,6 +420,7 @@
 
         // TODO: should these buttons be shown as default?
         mExtraControls = v.findViewById(R.id.extra_controls);
+        mCustomButtons = v.findViewById(R.id.custom_buttons);
         mOverflowButtonLeft = v.findViewById(R.id.overflow_left);
         if (mOverflowButtonLeft != null) {
             mOverflowButtonLeft.setOnClickListener(mOverflowLeftListener);
@@ -741,6 +661,22 @@
         }
     };
 
+    private final View.OnClickListener mNextListener = new View.OnClickListener() {
+        @Override
+        public void onClick(View v) {
+            mControls.skipToNext();
+            mInstance.show(DEFAULT_TIMEOUT_MS);
+        }
+    };
+
+    private final View.OnClickListener mPrevListener = new View.OnClickListener() {
+        @Override
+        public void onClick(View v) {
+            mControls.skipToPrevious();
+            mInstance.show(DEFAULT_TIMEOUT_MS);
+        }
+    };
+
     private final View.OnClickListener mSubtitleListener = new View.OnClickListener() {
         @Override
         public void onClick(View v) {
@@ -748,13 +684,13 @@
                 mSubtitleButton.setImageDrawable(
                         ApiHelper.getLibResources().getDrawable(
                                 R.drawable.ic_media_subtitle_enabled, null));
-                mInstance.showSubtitle();
+                mController.sendCommand(MediaControlView2.COMMAND_SHOW_SUBTITLE, null, null);
                 mSubtitleIsEnabled = true;
             } else {
                 mSubtitleButton.setImageDrawable(
                         ApiHelper.getLibResources().getDrawable(
                                 R.drawable.ic_media_subtitle_disabled, null));
-                mInstance.hideSubtitle();
+                mController.sendCommand(MediaControlView2.COMMAND_HIDE_SUBTITLE, null, null);
                 mSubtitleIsEnabled = false;
             }
             mInstance.show(DEFAULT_TIMEOUT_MS);
@@ -776,7 +712,7 @@
             }
             Bundle args = new Bundle();
             args.putBoolean(ARGUMENT_KEY_FULLSCREEN, isEnteringFullScreen);
-            mController.sendCommand(COMMAND_SET_FULLSCREEN, args, null);
+            mController.sendCommand(MediaControlView2.COMMAND_SET_FULLSCREEN, args, null);
 
             mIsFullScreen = isEnteringFullScreen;
             mInstance.show(DEFAULT_TIMEOUT_MS);
@@ -875,6 +811,31 @@
                 }
                 mPlaybackActions = newActions;
             }
+
+            // Add buttons if custom actions are present.
+            List<PlaybackState.CustomAction> customActions = mPlaybackState.getCustomActions();
+            mCustomButtons.removeAllViews();
+            if (customActions.size() > 0) {
+                for (PlaybackState.CustomAction action : customActions) {
+                    ImageButton button = new ImageButton(mInstance.getContext(),
+                            null /* AttributeSet */, 0 /* Style */);
+                    // TODO: Apply R.style.BottomBarButton to this button using library context.
+                    // Refer Constructor with argument (int defStyleRes) of View.java
+                    button.setImageResource(action.getIcon());
+                    button.setTooltipText(action.getName());
+                    final String actionString = action.getAction().toString();
+                    button.setOnClickListener(new View.OnClickListener() {
+                        @Override
+                        public void onClick(View v) {
+                            // TODO: Currently, we are just sending extras that came from session.
+                            // Is it the right behavior?
+                            mControls.sendCustomAction(actionString, action.getExtras());
+                            mInstance.show(DEFAULT_TIMEOUT_MS);
+                        }
+                    });
+                    mCustomButtons.addView(button);
+                }
+            }
         }
 
         @Override
diff --git a/packages/MediaComponents/src/com/android/widget/VideoView2Impl.java b/packages/MediaComponents/src/com/android/widget/VideoView2Impl.java
index f3aaec8..38ade41 100644
--- a/packages/MediaComponents/src/com/android/widget/VideoView2Impl.java
+++ b/packages/MediaComponents/src/com/android/widget/VideoView2Impl.java
@@ -16,6 +16,7 @@
 
 package com.android.widget;
 
+import android.annotation.NonNull;
 import android.app.AlertDialog;
 import android.content.Context;
 import android.content.DialogInterface;
@@ -25,11 +26,9 @@
 import android.media.AudioManager;
 import android.media.MediaMetadata;
 import android.media.MediaPlayer;
-import android.media.MediaPlayerBase;
+import android.media.MediaPlayerInterface;
 import android.media.Cea708CaptionRenderer;
 import android.media.ClosedCaptionRenderer;
-import android.media.MediaMetadata;
-import android.media.MediaPlayer;
 import android.media.Metadata;
 import android.media.PlaybackParams;
 import android.media.SubtitleController;
@@ -47,7 +46,6 @@
 import android.util.AttributeSet;
 import android.util.Log;
 import android.view.Gravity;
-import android.view.KeyEvent;
 import android.view.MotionEvent;
 import android.view.View;
 import android.widget.FrameLayout.LayoutParams;
@@ -56,11 +54,17 @@
 
 import com.android.media.update.ApiHelper;
 import com.android.media.update.R;
+import com.android.support.mediarouter.media.MediaRouter;
+import com.android.support.mediarouter.media.MediaRouteSelector;
+import com.android.support.mediarouter.media.RemotePlaybackClient;
+import com.android.support.mediarouter.media.MediaItemStatus;
+import com.android.support.mediarouter.media.MediaSessionStatus;
 
 import java.io.IOException;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
+import java.util.concurrent.Executor;
 
 public class VideoView2Impl implements VideoView2Provider, VideoViewInterface.SurfaceListener {
     private static final String TAG = "VideoView2";
@@ -80,14 +84,14 @@
     private final AudioManager mAudioManager;
     private AudioAttributes mAudioAttributes;
     private int mAudioFocusType = AudioManager.AUDIOFOCUS_GAIN; // legacy focus gain
-    private int mAudioSession;
 
+    private VideoView2.OnCustomActionListener mOnCustomActionListener;
     private VideoView2.OnPreparedListener mOnPreparedListener;
     private VideoView2.OnCompletionListener mOnCompletionListener;
     private VideoView2.OnErrorListener mOnErrorListener;
     private VideoView2.OnInfoListener mOnInfoListener;
     private VideoView2.OnViewTypeChangedListener mOnViewTypeChangedListener;
-    private VideoView2.OnFullScreenChangedListener mOnFullScreenChangedListener;
+    private VideoView2.OnFullScreenRequestListener mOnFullScreenRequestListener;
 
     private VideoViewInterface mCurrentView;
     private VideoTextureView mTextureView;
@@ -97,10 +101,14 @@
     private MediaControlView2 mMediaControlView;
     private MediaSession mMediaSession;
     private MediaController mMediaController;
+    private MediaSession.Callback mRouteSessionCallback = new RouteSessionCallback();
+    private MediaRouter mMediaRouter;
+    private MediaRouteSelector mMediaRouteSelector;
     private Metadata mMetadata;
     private String mTitle;
 
     private PlaybackState.Builder mStateBuilder;
+    private List<PlaybackState.CustomAction> mCustomActionList;
     private int mTargetState = STATE_IDLE;
     private int mCurrentState = STATE_IDLE;
     private int mCurrentBufferPercentage;
@@ -182,6 +190,9 @@
 
     @Override
     public MediaController getMediaController_impl() {
+        if (mMediaSession == null) {
+            throw new IllegalStateException("MediaSession instance is not available.");
+        }
         return mMediaController;
     }
 
@@ -191,16 +202,6 @@
     }
 
     @Override
-    public int getAudioSessionId_impl() {
-        if (mAudioSession == 0) {
-            MediaPlayer foo = new MediaPlayer();
-            mAudioSession = foo.getAudioSessionId();
-            foo.release();
-        }
-        return mAudioSession;
-    }
-
-    @Override
     public void showSubtitle_impl() {
         // Retrieve all tracks that belong to the current video.
         MediaPlayer.TrackInfo[] trackInfos = mMediaPlayer.getTrackInfo();
@@ -228,13 +229,6 @@
         }
     }
 
-    @Override
-    public void setFullScreen_impl(boolean fullScreen) {
-        if (mOnFullScreenChangedListener != null) {
-            mOnFullScreenChangedListener.onFullScreenChanged(fullScreen);
-        }
-    }
-
     // TODO: remove setSpeed_impl once MediaController2 is ready.
     @Override
     public void setSpeed_impl(float speed) {
@@ -270,22 +264,37 @@
     }
 
     @Override
-    public void setRouteAttributes_impl(List<String> routeCategories, MediaPlayerBase player) {
+    public void setRouteAttributes_impl(List<String> routeCategories, MediaPlayerInterface player) {
         // TODO: implement this.
     }
 
     @Override
+    public void setRouteAttributes_impl(@NonNull List<String> routeCategories,
+            MediaSession.Callback sessionPlayer) {
+        MediaRouteSelector.Builder builder = new MediaRouteSelector.Builder();
+        for (String category : routeCategories) {
+            builder.addControlCategory(category);
+        }
+        mMediaRouteSelector = builder.build();
+        mMediaRouter = MediaRouter.getInstance(mInstance.getContext());
+        mRouteSessionCallback = sessionPlayer;
+        if (mMediaSession != null) {
+            mMediaRouter.setMediaSession(mMediaSession);
+        }
+    }
+
+    @Override
     public void setVideoPath_impl(String path) {
-        mInstance.setVideoURI(Uri.parse(path));
+        mInstance.setVideoUri(Uri.parse(path));
     }
 
     @Override
-    public void setVideoURI_impl(Uri uri) {
-        mInstance.setVideoURI(uri, null);
+    public void setVideoUri_impl(Uri uri) {
+        mInstance.setVideoUri(uri, null);
     }
 
     @Override
-    public void setVideoURI_impl(Uri uri, Map<String, String> headers) {
+    public void setVideoUri_impl(Uri uri, Map<String, String> headers) {
         mSeekWhenPrepared = 0;
         openVideo(uri, headers);
     }
@@ -315,34 +324,49 @@
         return mCurrentView.getViewType();
     }
 
+    // TODO: Handle executor properly for all the set listener methods.
     @Override
-    public void setOnPreparedListener_impl(VideoView2.OnPreparedListener l) {
+    public void setCustomActions_impl(
+            List<PlaybackState.CustomAction> actionList,
+            Executor executor, VideoView2.OnCustomActionListener listener) {
+        mCustomActionList = actionList;
+        mOnCustomActionListener = listener;
+
+        // Create a new playback builder in order to clear existing the custom actions.
+        mStateBuilder = null;
+        updatePlaybackState();
+    }
+
+    @Override
+    public void setOnPreparedListener_impl(Executor executor, VideoView2.OnPreparedListener l) {
         mOnPreparedListener = l;
     }
 
     @Override
-    public void setOnCompletionListener_impl(VideoView2.OnCompletionListener l) {
+    public void setOnCompletionListener_impl(Executor executor, VideoView2.OnCompletionListener l) {
         mOnCompletionListener = l;
     }
 
     @Override
-    public void setOnErrorListener_impl(VideoView2.OnErrorListener l) {
+    public void setOnErrorListener_impl(Executor executor, VideoView2.OnErrorListener l) {
         mOnErrorListener = l;
     }
 
     @Override
-    public void setOnInfoListener_impl(VideoView2.OnInfoListener l) {
+    public void setOnInfoListener_impl(Executor executor, VideoView2.OnInfoListener l) {
         mOnInfoListener = l;
     }
 
     @Override
-    public void setOnViewTypeChangedListener_impl(VideoView2.OnViewTypeChangedListener l) {
+    public void setOnViewTypeChangedListener_impl(Executor executor,
+            VideoView2.OnViewTypeChangedListener l) {
         mOnViewTypeChangedListener = l;
     }
 
     @Override
-    public void setFullScreenChangedListener_impl(VideoView2.OnFullScreenChangedListener l) {
-        mOnFullScreenChangedListener = l;
+    public void setFullScreenRequestListener_impl(Executor executor,
+            VideoView2.OnFullScreenRequestListener l) {
+        mOnFullScreenRequestListener = l;
     }
 
     @Override
@@ -353,6 +377,9 @@
         mMediaSession = new MediaSession(mInstance.getContext(), "VideoView2MediaSession");
         mMediaSession.setCallback(new MediaSessionCallback());
         mMediaController = mMediaSession.getController();
+        if (mMediaRouter != null) {
+            mMediaRouter.setMediaSession(mMediaSession);
+        }
 
         attachMediaControlView();
     }
@@ -362,6 +389,7 @@
         mSuperProvider.onDetachedFromWindow_impl();
         mMediaSession.release();
         mMediaSession = null;
+        mMediaController = null;
     }
 
     @Override
@@ -392,58 +420,11 @@
     }
 
     @Override
-    public boolean onKeyDown_impl(int keyCode, KeyEvent event) {
-        Log.v(TAG, "onKeyDown_impl: " + keyCode);
-        boolean isKeyCodeSupported = keyCode != KeyEvent.KEYCODE_BACK
-                && keyCode != KeyEvent.KEYCODE_VOLUME_UP
-                && keyCode != KeyEvent.KEYCODE_VOLUME_DOWN
-                && keyCode != KeyEvent.KEYCODE_VOLUME_MUTE
-                && keyCode != KeyEvent.KEYCODE_MENU
-                && keyCode != KeyEvent.KEYCODE_CALL
-                && keyCode != KeyEvent.KEYCODE_ENDCALL;
-        if (isInPlaybackState() && isKeyCodeSupported && mMediaControlView != null) {
-            if (keyCode == KeyEvent.KEYCODE_HEADSETHOOK
-                    || keyCode == KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE) {
-                if (mMediaPlayer.isPlaying()) {
-                    mMediaController.getTransportControls().pause();
-                    mMediaControlView.show();
-                } else {
-                    mMediaController.getTransportControls().play();
-                    mMediaControlView.hide();
-                }
-                return true;
-            } else if (keyCode == KeyEvent.KEYCODE_MEDIA_PLAY) {
-                if (!mMediaPlayer.isPlaying()) {
-                    mMediaController.getTransportControls().play();
-                    mMediaControlView.hide();
-                }
-                return true;
-            } else if (keyCode == KeyEvent.KEYCODE_MEDIA_STOP
-                    || keyCode == KeyEvent.KEYCODE_MEDIA_PAUSE) {
-                if (mMediaPlayer.isPlaying()) {
-                    mMediaController.getTransportControls().pause();
-                    mMediaControlView.show();
-                }
-                return true;
-            } else {
-                toggleMediaControlViewVisibility();
-            }
-        }
-
-        return mSuperProvider.onKeyDown_impl(keyCode, event);
-    }
-
-    @Override
     public void onFinishInflate_impl() {
         mSuperProvider.onFinishInflate_impl();
     }
 
     @Override
-    public boolean dispatchKeyEvent_impl(KeyEvent event) {
-        return mSuperProvider.dispatchKeyEvent_impl(event);
-    }
-
-    @Override
     public void setEnabled_impl(boolean enabled) {
         mSuperProvider.setEnabled_impl(enabled);
     }
@@ -491,7 +472,7 @@
         }
         mCurrentView = view;
         if (mOnViewTypeChangedListener != null) {
-            mOnViewTypeChangedListener.onViewTypeChanged(view.getViewType());
+            mOnViewTypeChangedListener.onViewTypeChanged(mInstance, view.getViewType());
         }
         if (needToStart()) {
             mMediaController.getTransportControls().play();
@@ -553,12 +534,6 @@
             controller.registerRenderer(new Cea708CaptionRenderer(context));
             controller.registerRenderer(new ClosedCaptionRenderer(context));
             mMediaPlayer.setSubtitleAnchor(controller, (SubtitleController.Anchor) mSubtitleView);
-
-            if (mAudioSession != 0) {
-                mMediaPlayer.setAudioSessionId(mAudioSession);
-            } else {
-                mAudioSession = mMediaPlayer.getAudioSessionId();
-            }
             mMediaPlayer.setOnPreparedListener(mPreparedListener);
             mMediaPlayer.setOnVideoSizeChangedListener(mSizeChangedListener);
             mMediaPlayer.setOnCompletionListener(mCompletionListener);
@@ -657,8 +632,12 @@
             }
             mStateBuilder = new PlaybackState.Builder();
             mStateBuilder.setActions(playbackActions);
-            mStateBuilder.addCustomAction(MediaControlView2Impl.COMMAND_SHOW_SUBTITLE, null, -1);
-            mStateBuilder.addCustomAction(MediaControlView2Impl.COMMAND_HIDE_SUBTITLE, null, -1);
+
+            if (mCustomActionList != null) {
+                for (PlaybackState.CustomAction action : mCustomActionList) {
+                    mStateBuilder.addCustomAction(action);
+                }
+            }
         }
         mStateBuilder.setState(getCorrespondingPlaybackState(),
                 mMediaPlayer.getCurrentPosition(), mSpeed);
@@ -751,7 +730,7 @@
             }
             mCurrentState = STATE_PREPARED;
             if (mOnPreparedListener != null) {
-                mOnPreparedListener.onPrepared();
+                mOnPreparedListener.onPrepared(mInstance);
             }
             if (mMediaControlView != null) {
                 mMediaControlView.setEnabled(true);
@@ -826,7 +805,7 @@
                     updatePlaybackState();
 
                     if (mOnCompletionListener != null) {
-                        mOnCompletionListener.onCompletion();
+                        mOnCompletionListener.onCompletion(mInstance);
                     }
                     if (mAudioFocusType != AudioManager.AUDIOFOCUS_NONE) {
                         mAudioManager.abandonAudioFocus(null);
@@ -838,7 +817,7 @@
             new MediaPlayer.OnInfoListener() {
                 public boolean onInfo(MediaPlayer mp, int what, int extra) {
                     if (mOnInfoListener != null) {
-                        mOnInfoListener.onInfo(what, extra);
+                        mOnInfoListener.onInfo(mInstance, what, extra);
                     }
                     return true;
                 }
@@ -860,7 +839,7 @@
 
                     /* If an error handler has been supplied, use it and finish. */
                     if (mOnErrorListener != null) {
-                        if (mOnErrorListener.onError(frameworkErr, implErr)) {
+                        if (mOnErrorListener.onError(mInstance, frameworkErr, implErr)) {
                             return true;
                         }
                     }
@@ -891,7 +870,7 @@
                                                 * at least inform them that the video is over.
                                                 */
                                                 if (mOnCompletionListener != null) {
-                                                    mOnCompletionListener.onCompletion();
+                                                    mOnCompletionListener.onCompletion(mInstance);
                                                 }
                                             }
                                         })
@@ -913,65 +892,161 @@
     private class MediaSessionCallback extends MediaSession.Callback {
         @Override
         public void onCommand(String command, Bundle args, ResultReceiver receiver) {
-            switch (command) {
-                case MediaControlView2Impl.COMMAND_SHOW_SUBTITLE:
-                    mInstance.showSubtitle();
-                    break;
-                case MediaControlView2Impl.COMMAND_HIDE_SUBTITLE:
-                    mInstance.hideSubtitle();
-                    break;
-                case MediaControlView2Impl.COMMAND_SET_FULLSCREEN:
-                    mInstance.setFullScreen(
-                            args.getBoolean(MediaControlView2Impl.ARGUMENT_KEY_FULLSCREEN));
-                    break;
+            if (mMediaController.getPlaybackInfo().getPlaybackType()
+                    == MediaController.PlaybackInfo.PLAYBACK_TYPE_REMOTE) {
+                mRouteSessionCallback.onCommand(command, args, receiver);
+            } else {
+                switch (command) {
+                    case MediaControlView2.COMMAND_SHOW_SUBTITLE:
+                        mInstance.showSubtitle();
+                        break;
+                    case MediaControlView2.COMMAND_HIDE_SUBTITLE:
+                        mInstance.hideSubtitle();
+                        break;
+                    case MediaControlView2.COMMAND_SET_FULLSCREEN:
+                        if (mOnFullScreenRequestListener != null) {
+                            mOnFullScreenRequestListener.onFullScreenRequest(
+                                    mInstance,
+                                    args.getBoolean(MediaControlView2Impl.ARGUMENT_KEY_FULLSCREEN));
+                        }
+                        break;
+                }
             }
         }
 
         @Override
+        public void onCustomAction(String action, Bundle extras) {
+            mOnCustomActionListener.onCustomAction(action, extras);
+        }
+
+        @Override
         public void onPlay() {
-            if (isInPlaybackState() && mCurrentView.hasAvailableSurface()) {
-                applySpeed();
-                mMediaPlayer.start();
-                mCurrentState = STATE_PLAYING;
-                updatePlaybackState();
-            }
-            mTargetState = STATE_PLAYING;
-            if (DEBUG) {
-                Log.d(TAG, "onPlay(). mCurrentState=" + mCurrentState
-                        + ", mTargetState=" + mTargetState);
+            if (mMediaController.getPlaybackInfo().getPlaybackType()
+                    == MediaController.PlaybackInfo.PLAYBACK_TYPE_REMOTE) {
+                mRouteSessionCallback.onPlay();
+            } else {
+                if (isInPlaybackState() && mCurrentView.hasAvailableSurface()) {
+                    applySpeed();
+                    mMediaPlayer.start();
+                    mCurrentState = STATE_PLAYING;
+                    updatePlaybackState();
+                }
+                mTargetState = STATE_PLAYING;
+                if (DEBUG) {
+                    Log.d(TAG, "onPlay(). mCurrentState=" + mCurrentState
+                            + ", mTargetState=" + mTargetState);
+                }
             }
         }
 
         @Override
         public void onPause() {
-            if (isInPlaybackState()) {
-                if (mMediaPlayer.isPlaying()) {
-                    mMediaPlayer.pause();
-                    mCurrentState = STATE_PAUSED;
-                    updatePlaybackState();
+            if (mMediaController.getPlaybackInfo().getPlaybackType()
+                    == MediaController.PlaybackInfo.PLAYBACK_TYPE_REMOTE) {
+                mRouteSessionCallback.onPause();
+            } else {
+                if (isInPlaybackState()) {
+                    if (mMediaPlayer.isPlaying()) {
+                        mMediaPlayer.pause();
+                        mCurrentState = STATE_PAUSED;
+                        updatePlaybackState();
+                    }
                 }
-            }
-            mTargetState = STATE_PAUSED;
-            if (DEBUG) {
-                Log.d(TAG, "onPause(). mCurrentState=" + mCurrentState
-                        + ", mTargetState=" + mTargetState);
+                mTargetState = STATE_PAUSED;
+                if (DEBUG) {
+                    Log.d(TAG, "onPause(). mCurrentState=" + mCurrentState
+                            + ", mTargetState=" + mTargetState);
+                }
             }
         }
 
         @Override
         public void onSeekTo(long pos) {
-            if (isInPlaybackState()) {
-                mMediaPlayer.seekTo(pos, MediaPlayer.SEEK_PREVIOUS_SYNC);
-                mSeekWhenPrepared = 0;
-                updatePlaybackState();
+            if (mMediaController.getPlaybackInfo().getPlaybackType()
+                    == MediaController.PlaybackInfo.PLAYBACK_TYPE_REMOTE) {
+                mRouteSessionCallback.onSeekTo(pos);
             } else {
-                mSeekWhenPrepared = pos;
+                if (isInPlaybackState()) {
+                    mMediaPlayer.seekTo(pos, MediaPlayer.SEEK_PREVIOUS_SYNC);
+                    mSeekWhenPrepared = 0;
+                    updatePlaybackState();
+                } else {
+                    mSeekWhenPrepared = pos;
+                }
             }
         }
 
         @Override
         public void onStop() {
-            resetPlayer();
+            if (mMediaController.getPlaybackInfo().getPlaybackType()
+                    == MediaController.PlaybackInfo.PLAYBACK_TYPE_REMOTE) {
+                mRouteSessionCallback.onStop();
+            } else {
+                resetPlayer();
+            }
+        }
+    }
+
+    private class RouteSessionCallback extends MediaSession.Callback {
+        RemotePlaybackClient mClient;
+
+        RemotePlaybackClient.StatusCallback mStatusCallback =
+                new RemotePlaybackClient.StatusCallback() {
+            @Override
+            public void onItemStatusChanged(Bundle data,
+                    String sessionId, MediaSessionStatus sessionStatus,
+                    String itemId, MediaItemStatus itemStatus) {
+                // TODO: implement this
+            }
+
+            @Override
+            public void onSessionStatusChanged(Bundle data,
+                    String sessionId, MediaSessionStatus sessionStatus) {
+                // TODO: implement this
+            }
+
+            @Override
+            public void onSessionChanged(String sessionId) {
+                // TODO: implement this
+            }
+        };
+
+        @Override
+        public void onCommand(String command, Bundle args, ResultReceiver receiver) {
+            ensureClient();
+            // TODO: implement this
+        }
+
+        @Override
+        public void onPlay() {
+            ensureClient();
+            // TODO: implement this
+        }
+
+        @Override
+        public void onPause() {
+            ensureClient();
+            // TODO: implement this
+        }
+
+        @Override
+        public void onSeekTo(long pos) {
+            ensureClient();
+            // TODO: implement this
+        }
+
+        @Override
+        public void onStop() {
+            ensureClient();
+            // TODO: implement this
+        }
+
+        private void ensureClient() {
+            if (mClient == null) {
+                mClient = new RemotePlaybackClient(
+                        mInstance.getContext(), mMediaRouter.getSelectedRoute());
+                mClient.setStatusCallback(mStatusCallback);
+            }
         }
     }
 }
diff --git a/packages/MediaComponents/test/runtest.sh b/packages/MediaComponents/test/runtest.sh
index 5c0ef51..920fa96 100644
--- a/packages/MediaComponents/test/runtest.sh
+++ b/packages/MediaComponents/test/runtest.sh
@@ -129,11 +129,12 @@
       ${adb} root
       ${adb} remount
       ${adb} shell stop
+      ${adb} shell setprop log.tag.MediaSessionService DEBUG
       ${adb} sync
       ${adb} shell start
       ${adb} wait-for-device || break
       # Ensure package manager is loaded.
-      sleep 5
+      sleep 15
 
       # Install apks
       local install_failed="false"
diff --git a/packages/MediaComponents/test/src/android/media/MediaBrowser2Test.java b/packages/MediaComponents/test/src/android/media/MediaBrowser2Test.java
index ef75060..50e59b8 100644
--- a/packages/MediaComponents/test/src/android/media/MediaBrowser2Test.java
+++ b/packages/MediaComponents/test/src/android/media/MediaBrowser2Test.java
@@ -20,10 +20,14 @@
 import static junit.framework.Assert.assertFalse;
 import static junit.framework.Assert.assertTrue;
 
+import android.annotation.Nullable;
 import android.content.Context;
 import android.media.MediaBrowser2.BrowserCallback;
+import android.media.MediaSession2.Command;
 import android.media.MediaSession2.CommandGroup;
+import android.media.MediaSession2.PlaylistParams;
 import android.os.Bundle;
+import android.os.ResultReceiver;
 import android.support.annotation.CallSuper;
 import android.support.annotation.NonNull;
 import android.support.test.filters.SmallTest;
@@ -49,17 +53,25 @@
 
     @Override
     TestControllerInterface onCreateController(@NonNull SessionToken2 token,
-            @NonNull TestControllerCallbackInterface callback) {
+            @Nullable TestControllerCallbackInterface callback) {
+        if (callback == null) {
+            callback = new TestBrowserCallbackInterface() {};
+        }
         return new TestMediaBrowser(mContext, token, new TestBrowserCallback(callback));
     }
 
+    interface TestBrowserCallbackInterface extends TestControllerCallbackInterface {
+        // Browser specific callbacks
+        default void onGetRootResult(Bundle rootHints, String rootMediaId, Bundle rootExtra) {}
+    }
+
     @Test
     public void testGetBrowserRoot() throws InterruptedException {
         final Bundle param = new Bundle();
         param.putString(TAG, TAG);
 
         final CountDownLatch latch = new CountDownLatch(1);
-        final TestControllerCallbackInterface callback = new TestControllerCallbackInterface() {
+        final TestControllerCallbackInterface callback = new TestBrowserCallbackInterface() {
             @Override
             public void onGetRootResult(Bundle rootHints, String rootMediaId, Bundle rootExtra) {
                 assertTrue(TestUtils.equals(param, rootHints));
@@ -83,6 +95,9 @@
         public final CountDownLatch disconnectLatch = new CountDownLatch(1);
 
         TestBrowserCallback(TestControllerCallbackInterface callbackProxy) {
+            if (callbackProxy == null) {
+                throw new IllegalArgumentException("Callback proxy shouldn't be null. Test bug");
+            }
             mCallbackProxy = callbackProxy;
         }
 
@@ -101,8 +116,30 @@
         }
 
         @Override
+        public void onPlaybackStateChanged(PlaybackState2 state) {
+            super.onPlaybackStateChanged(state);
+            mCallbackProxy.onPlaybackStateChanged(state);
+        }
+
+        @Override
+        public void onPlaylistParamsChanged(PlaylistParams params) {
+            super.onPlaylistParamsChanged(params);
+            mCallbackProxy.onPlaylistParamsChanged(params);
+        }
+
+        @Override
+        public void onCustomCommand(Command command, Bundle args, ResultReceiver receiver) {
+            super.onCustomCommand(command, args, receiver);
+            mCallbackProxy.onCustomCommand(command, args, receiver);
+        }
+
+        @Override
         public void onGetRootResult(Bundle rootHints, String rootMediaId, Bundle rootExtra) {
-            mCallbackProxy.onGetRootResult(rootHints, rootMediaId, rootExtra);
+            super.onGetRootResult(rootHints, rootMediaId, rootExtra);
+            if (mCallbackProxy instanceof TestBrowserCallbackInterface) {
+                ((TestBrowserCallbackInterface) mCallbackProxy)
+                        .onGetRootResult(rootHints, rootMediaId, rootExtra);
+            }
         }
 
         @Override
diff --git a/packages/MediaComponents/test/src/android/media/MediaController2Test.java b/packages/MediaComponents/test/src/android/media/MediaController2Test.java
index ae67a95..9c5aa21 100644
--- a/packages/MediaComponents/test/src/android/media/MediaController2Test.java
+++ b/packages/MediaComponents/test/src/android/media/MediaController2Test.java
@@ -16,14 +16,19 @@
 
 package android.media;
 
-import android.media.MediaPlayerBase.PlaybackListener;
+import android.media.MediaController2.ControllerCallback;
+import android.media.MediaPlayerInterface.PlaybackListener;
+import android.media.MediaSession2.Command;
 import android.media.MediaSession2.ControllerInfo;
+import android.media.MediaSession2.PlaylistParams;
 import android.media.MediaSession2.SessionCallback;
 import android.media.TestUtils.SyncHandler;
 import android.media.session.PlaybackState;
+import android.os.Bundle;
 import android.os.Handler;
 import android.os.HandlerThread;
 import android.os.Process;
+import android.os.ResultReceiver;
 import android.support.test.filters.FlakyTest;
 import android.support.test.filters.SmallTest;
 import android.support.test.runner.AndroidJUnit4;
@@ -45,6 +50,7 @@
  */
 // TODO(jaewan): Implement host-side test so controller and session can run in different processes.
 // TODO(jaewan): Fix flaky failure -- see MediaController2Impl.getController()
+// TODO(jaeawn): Revisit create/close session in the sHandler. It's no longer necessary.
 @RunWith(AndroidJUnit4.class)
 @SmallTest
 @FlakyTest
@@ -60,11 +66,10 @@
     public void setUp() throws Exception {
         super.setUp();
         // Create this test specific MediaSession2 to use our own Handler.
-        sHandler.postAndSync(()->{
-            mPlayer = new MockPlayer(1);
-            mSession = new MediaSession2.Builder(mContext, mPlayer).setId(TAG).build();
-        });
-
+        mPlayer = new MockPlayer(1);
+        mSession = new MediaSession2.Builder(mContext, mPlayer)
+                .setSessionCallback(sHandlerExecutor, new SessionCallback())
+                .setId(TAG).build();
         mController = createController(mSession.getToken());
         TestServiceRegistry.getInstance().setHandler(sHandler);
     }
@@ -73,11 +78,9 @@
     @Override
     public void cleanUp() throws Exception {
         super.cleanUp();
-        sHandler.postAndSync(() -> {
-            if (mSession != null) {
-                mSession.close();
-            }
-        });
+        if (mSession != null) {
+            mSession.close();
+        }
         TestServiceRegistry.getInstance().cleanUp();
     }
 
@@ -103,7 +106,6 @@
         assertTrue(mPlayer.mPauseCalled);
     }
 
-
     @Test
     public void testSkipToPrevious() throws InterruptedException {
         mController.skipToPrevious();
@@ -138,66 +140,146 @@
     }
 
     @Test
+    public void testPrepare() throws InterruptedException {
+        mController.prepare();
+        try {
+            assertTrue(mPlayer.mCountDownLatch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+        } catch (InterruptedException e) {
+            fail(e.getMessage());
+        }
+        assertTrue(mPlayer.mPrepareCalled);
+    }
+
+    @Test
+    public void testFastForward() throws InterruptedException {
+        mController.fastForward();
+        try {
+            assertTrue(mPlayer.mCountDownLatch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+        } catch (InterruptedException e) {
+            fail(e.getMessage());
+        }
+        assertTrue(mPlayer.mFastForwardCalled);
+    }
+
+    @Test
+    public void testRewind() throws InterruptedException {
+        mController.rewind();
+        try {
+            assertTrue(mPlayer.mCountDownLatch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+        } catch (InterruptedException e) {
+            fail(e.getMessage());
+        }
+        assertTrue(mPlayer.mRewindCalled);
+    }
+
+    @Test
+    public void testSeekTo() throws InterruptedException {
+        final long seekPosition = 12125L;
+        mController.seekTo(seekPosition);
+        try {
+            assertTrue(mPlayer.mCountDownLatch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+        } catch (InterruptedException e) {
+            fail(e.getMessage());
+        }
+        assertTrue(mPlayer.mSeekToCalled);
+        assertEquals(seekPosition, mPlayer.mSeekPosition);
+    }
+
+    @Test
+    public void testSetCurrentPlaylistItem() throws InterruptedException {
+        final int itemIndex = 9;
+        mController.setCurrentPlaylistItem(itemIndex);
+        try {
+            assertTrue(mPlayer.mCountDownLatch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+        } catch (InterruptedException e) {
+            fail(e.getMessage());
+        }
+        assertTrue(mPlayer.mSetCurrentPlaylistItemCalled);
+        assertEquals(itemIndex, mPlayer.mItemIndex);
+    }
+
+    @Test
+    public void testGetSetPlaylistParams() throws Exception {
+        final PlaylistParams params = new PlaylistParams(
+                PlaylistParams.REPEAT_MODE_ALL,
+                PlaylistParams.SHUFFLE_MODE_ALL,
+                null /* PlaylistMetadata */);
+
+        final CountDownLatch latch = new CountDownLatch(1);
+        final TestControllerCallbackInterface callback = new TestControllerCallbackInterface() {
+            @Override
+            public void onPlaylistParamsChanged(PlaylistParams givenParams) {
+                TestUtils.equals(params.toBundle(), givenParams.toBundle());
+                latch.countDown();
+            }
+        };
+
+        final MediaController2 controller = createController(mSession.getToken(), true, callback);
+        controller.setPlaylistParams(params);
+
+        assertTrue(latch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+        TestUtils.equals(params.toBundle(), mSession.getPlaylistParams().toBundle());
+        TestUtils.equals(params.toBundle(), controller.getPlaylistParams().toBundle());
+    }
+
+    @Test
     public void testGetPackageName() {
         assertEquals(mContext.getPackageName(), mController.getSessionToken().getPackageName());
     }
 
+    // This also tests testGetPlaybackState().
     @Test
-    public void testGetPlaybackState() throws InterruptedException {
-        // TODO(jaewan): add equivalent test later
-        /*
-        final CountDownLatch latch = new CountDownLatch(1);
-        final MediaPlayerBase.PlaybackListener listener = (state) -> {
-            assertEquals(PlaybackState.STATE_BUFFERING, state.getState());
-            latch.countDown();
-        };
-        assertNull(mController.getPlaybackState());
-        mController.addPlaybackListener(listener, sHandler);
-
-        mPlayer.notifyPlaybackState(createPlaybackState(PlaybackState.STATE_BUFFERING));
-        assertTrue(latch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
-        assertEquals(PlaybackState.STATE_BUFFERING, mController.getPlaybackState().getState());
-        */
-    }
-
-    // TODO(jaewan): add equivalent test later
-    /*
-    @Test
-    public void testAddPlaybackListener() throws InterruptedException {
+    public void testControllerCallback_onPlaybackStateChanged() throws InterruptedException {
         final CountDownLatch latch = new CountDownLatch(2);
-        final MediaPlayerBase.PlaybackListener listener = (state) -> {
-            switch ((int) latch.getCount()) {
-                case 2:
-                    assertEquals(PlaybackState.STATE_PLAYING, state.getState());
-                    break;
-                case 1:
-                    assertEquals(PlaybackState.STATE_PAUSED, state.getState());
-                    break;
+        final TestControllerCallbackInterface callback = new TestControllerCallbackInterface() {
+            @Override
+            public void onPlaybackStateChanged(PlaybackState2 state) {
+                switch ((int) latch.getCount()) {
+                    case 2:
+                        assertEquals(PlaybackState.STATE_PLAYING, state.getState());
+                        break;
+                    case 1:
+                        assertEquals(PlaybackState.STATE_PAUSED, state.getState());
+                        break;
+                }
+                latch.countDown();
             }
-            latch.countDown();
         };
 
-        mController.addPlaybackListener(listener, sHandler);
-        sHandler.postAndSync(()->{
-            mPlayer.notifyPlaybackState(createPlaybackState(PlaybackState.STATE_PLAYING));
-            mPlayer.notifyPlaybackState(createPlaybackState(PlaybackState.STATE_PAUSED));
-        });
+        mPlayer.notifyPlaybackState(createPlaybackState(PlaybackState.STATE_PLAYING));
+        mController = createController(mSession.getToken(), true, callback);
+        mPlayer.notifyPlaybackState(createPlaybackState(PlaybackState.STATE_PAUSED));
         assertTrue(latch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+        assertEquals(PlaybackState.STATE_PAUSED, mController.getPlaybackState().getState());
     }
 
     @Test
-    public void testRemovePlaybackListener() throws InterruptedException {
+    public void testSendCustomCommand() throws InterruptedException {
+        // TODO(jaewan): Need to revisit with the permission.
+        final Command testCommand = new Command(MediaSession2.COMMAND_CODE_PLAYBACK_PREPARE);
+        final Bundle testArgs = new Bundle();
+        testArgs.putString("args", "testSendCustomAction");
+
         final CountDownLatch latch = new CountDownLatch(1);
-        final MediaPlayerBase.PlaybackListener listener = (state) -> {
-            fail();
-            latch.countDown();
+        final SessionCallback callback = new SessionCallback() {
+            @Override
+            public void onCustomCommand(ControllerInfo controller, Command customCommand,
+                    Bundle args, ResultReceiver cb) {
+                super.onCustomCommand(controller, customCommand, args, cb);
+                assertEquals(mContext.getPackageName(), controller.getPackageName());
+                assertEquals(testCommand, customCommand);
+                assertTrue(TestUtils.equals(testArgs, args));
+                assertNull(cb);
+                latch.countDown();
+            }
         };
-        mController.addPlaybackListener(listener, sHandler);
-        mController.removePlaybackListener(listener);
-        mPlayer.notifyPlaybackState(createPlaybackState(PlaybackState.STATE_PLAYING));
-        assertFalse(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+        mSession.close();
+        mSession = new MediaSession2.Builder(mContext, mPlayer)
+                .setSessionCallback(sHandlerExecutor, callback).setId(TAG).build();
+        final MediaController2 controller = createController(mSession.getToken());
+        controller.sendCustomCommand(testCommand, testArgs, null);
+        assertTrue(latch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
     }
-    */
 
     @Test
     public void testControllerCallback_onConnected() throws InterruptedException {
@@ -275,6 +357,7 @@
             final MockPlayer player = new MockPlayer(0);
             sessionHandler.postAndSync(() -> {
                 mSession = new MediaSession2.Builder(mContext, mPlayer)
+                        .setSessionCallback(sHandlerExecutor, new SessionCallback())
                         .setId("testDeadlock").build();
             });
             final MediaController2 controller = createController(mSession.getToken());
@@ -324,7 +407,6 @@
         assertNotNull(token);
         assertEquals(mContext.getPackageName(), token.getPackageName());
         assertEquals(MockMediaSessionService2.ID, token.getId());
-        assertNull(token.getSessionBinder());
         assertEquals(SessionToken2.TYPE_SESSION_SERVICE, token.getType());
     }
 
@@ -462,7 +544,9 @@
         sHandler.postAndSync(() -> {
             // Recreated session has different session stub, so previously created controller
             // shouldn't be available.
-            mSession = new MediaSession2.Builder(mContext, mPlayer).setId(id).build();
+            mSession = new MediaSession2.Builder(mContext, mPlayer)
+                    .setSessionCallback(sHandlerExecutor, new SessionCallback())
+                    .setId(id).build();
         });
         testNoInteraction();
     }
diff --git a/packages/MediaComponents/test/src/android/media/MediaSession2Test.java b/packages/MediaComponents/test/src/android/media/MediaSession2Test.java
index 045dcd5..6b10ccc 100644
--- a/packages/MediaComponents/test/src/android/media/MediaSession2Test.java
+++ b/packages/MediaComponents/test/src/android/media/MediaSession2Test.java
@@ -16,27 +16,37 @@
 
 package android.media;
 
-import android.media.MediaPlayerBase.PlaybackListener;
+import static junit.framework.Assert.assertEquals;
+import static junit.framework.Assert.assertTrue;
+
+import static android.media.TestUtils.createPlaybackState;
+
+import android.media.MediaPlayerInterface.PlaybackListener;
 import android.media.MediaSession2.Builder;
+import android.media.MediaSession2.Command;
 import android.media.MediaSession2.ControllerInfo;
+import android.media.MediaSession2.PlaylistParams;
 import android.media.MediaSession2.SessionCallback;
-import android.media.session.PlaybackState;
-import android.os.Process;
+import android.os.Bundle;
 import android.os.Looper;
+import android.os.Process;
+import android.os.ResultReceiver;
 import android.support.annotation.NonNull;
 import android.support.test.filters.SmallTest;
 import android.support.test.runner.AndroidJUnit4;
+import android.text.TextUtils;
 
 import java.util.ArrayList;
 import org.junit.After;
 import org.junit.Before;
+import org.junit.Ignore;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
+import java.util.List;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.TimeUnit;
 
-import static android.media.TestUtils.createPlaybackState;
 import static org.junit.Assert.*;
 
 /**
@@ -54,19 +64,16 @@
     @Override
     public void setUp() throws Exception {
         super.setUp();
-        sHandler.postAndSync(() -> {
-            mPlayer = new MockPlayer(0);
-            mSession = new MediaSession2.Builder(mContext, mPlayer).build();
-        });
+        mPlayer = new MockPlayer(0);
+        mSession = new MediaSession2.Builder(mContext, mPlayer)
+                .setSessionCallback(sHandlerExecutor, new SessionCallback()).build();
     }
 
     @After
     @Override
     public void cleanUp() throws Exception {
         super.cleanUp();
-        sHandler.postAndSync(() -> {
-            mSession.close();
-        });
+        mSession.close();
     }
 
     @Test
@@ -138,9 +145,57 @@
     }
 
     @Test
+    public void testSetPlaylist() throws Exception {
+        final List<MediaItem2> playlist = new ArrayList<>();
+
+        final CountDownLatch latch = new CountDownLatch(1);
+        final TestControllerCallbackInterface callback = new TestControllerCallbackInterface() {
+            @Override
+            public void onPlaylistChanged(List<MediaItem2> givenList) {
+                assertMediaItemListEquals(playlist, givenList);
+                latch.countDown();
+            }
+        };
+
+        final MediaController2 controller = createController(mSession.getToken(), true, callback);
+        mSession.setPlaylist(playlist);
+
+        assertTrue(mPlayer.mSetPlaylistCalled);
+        assertMediaItemListEquals(playlist, mPlayer.mPlaylist);
+        assertMediaItemListEquals(playlist, mSession.getPlaylist());
+
+        assertTrue(latch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+        assertMediaItemListEquals(playlist, controller.getPlaylist());
+    }
+
+    @Test
+    public void testSetPlaylistParams() throws Exception {
+        final PlaylistParams params = new PlaylistParams(
+                PlaylistParams.REPEAT_MODE_ALL,
+                PlaylistParams.SHUFFLE_MODE_ALL,
+                null /* PlaylistMetadata */);
+
+        final CountDownLatch latch = new CountDownLatch(1);
+        final TestControllerCallbackInterface callback = new TestControllerCallbackInterface() {
+            @Override
+            public void onPlaylistParamsChanged(PlaylistParams givenParams) {
+                TestUtils.equals(params.toBundle(), givenParams.toBundle());
+                latch.countDown();
+            }
+        };
+
+        final MediaController2 controller = createController(mSession.getToken(), true, callback);
+        mSession.setPlaylistParams(params);
+        assertTrue(mPlayer.mSetPlaylistParamsCalled);
+        TestUtils.equals(params.toBundle(), mPlayer.mPlaylistParams.toBundle());
+        TestUtils.equals(params.toBundle(), mSession.getPlaylistParams().toBundle());
+        assertTrue(latch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+    }
+
+    // TODO(jaewan): Re-enable test..
+    @Ignore
+    @Test
     public void testPlaybackStateChangedListener() throws InterruptedException {
-        // TODO(jaewan): Add equivalent tests again
-        /*
         final CountDownLatch latch = new CountDownLatch(2);
         final MockPlayer player = new MockPlayer(0);
         final PlaybackListener listener = (state) -> {
@@ -148,45 +203,42 @@
             assertNotNull(state);
             switch ((int) latch.getCount()) {
                 case 2:
-                    assertEquals(PlaybackState.STATE_PLAYING, state.getState());
+                    assertEquals(PlaybackState2.STATE_PLAYING, state.getState());
                     break;
                 case 1:
-                    assertEquals(PlaybackState.STATE_PAUSED, state.getState());
+                    assertEquals(PlaybackState2.STATE_PAUSED, state.getState());
                     break;
                 case 0:
                     fail();
             }
             latch.countDown();
         };
-        player.notifyPlaybackState(createPlaybackState(PlaybackState.STATE_PLAYING));
+        player.notifyPlaybackState(createPlaybackState(PlaybackState2.STATE_PLAYING));
         sHandler.postAndSync(() -> {
-            mSession.addPlaybackListener(listener, sHandler);
+            mSession.addPlaybackListener(sHandlerExecutor, listener);
             // When the player is set, listeners will be notified about the player's current state.
             mSession.setPlayer(player);
         });
-        player.notifyPlaybackState(createPlaybackState(PlaybackState.STATE_PAUSED));
+        player.notifyPlaybackState(createPlaybackState(PlaybackState2.STATE_PAUSED));
         assertTrue(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
-        */
     }
 
     @Test
     public void testBadPlayer() throws InterruptedException {
         // TODO(jaewan): Add equivalent tests again
-        /*
         final CountDownLatch latch = new CountDownLatch(3); // expected call + 1
         final BadPlayer player = new BadPlayer(0);
         sHandler.postAndSync(() -> {
-            mSession.addPlaybackListener((state) -> {
+            mSession.addPlaybackListener(sHandlerExecutor, (state) -> {
                 // This will be called for every setPlayer() calls, but no more.
                 assertNull(state);
                 latch.countDown();
-            }, sHandler);
+            });
             mSession.setPlayer(player);
             mSession.setPlayer(mPlayer);
         });
-        player.notifyPlaybackState(createPlaybackState(PlaybackState.STATE_PAUSED));
+        player.notifyPlaybackState(createPlaybackState(PlaybackState2.STATE_PAUSED));
         assertFalse(latch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
-        */
     }
 
     private static class BadPlayer extends MockPlayer {
@@ -241,6 +293,47 @@
         waitForDisconnect(controller, true);
     }
 
+    @Test
+    public void testSendCustomAction() throws InterruptedException {
+        final Command testCommand = new Command(MediaSession2.COMMAND_CODE_PLAYBACK_PREPARE);
+        final Bundle testArgs = new Bundle();
+        testArgs.putString("args", "testSendCustomAction");
+
+        final CountDownLatch latch = new CountDownLatch(2);
+        final TestControllerCallbackInterface callback = new TestControllerCallbackInterface() {
+            @Override
+            public void onCustomCommand(Command command, Bundle args, ResultReceiver receiver) {
+                assertEquals(testCommand, command);
+                assertTrue(TestUtils.equals(testArgs, args));
+                assertNull(receiver);
+                latch.countDown();
+            }
+        };
+        final MediaController2 controller =
+                createController(mSession.getToken(), true, callback);
+        // TODO(jaewan): Test with multiple controllers
+        mSession.sendCustomCommand(testCommand, testArgs);
+
+        ControllerInfo controllerInfo = getTestControllerInfo();
+        assertNotNull(controllerInfo);
+        // TODO(jaewan): Test receivers as well.
+        mSession.sendCustomCommand(controllerInfo, testCommand, testArgs, null);
+        assertTrue(latch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+    }
+
+    private ControllerInfo getTestControllerInfo() {
+        List<ControllerInfo> controllers = mSession.getConnectedControllers();
+        assertNotNull(controllers);
+        final String packageName = mContext.getPackageName();
+        for (int i = 0; i < controllers.size(); i++) {
+            if (TextUtils.equals(packageName, controllers.get(i).getPackageName())) {
+                return controllers.get(i);
+            }
+        }
+        fail("Fails to get custom command");
+        return null;
+    }
+
     public class MockOnConnectCallback extends SessionCallback {
         @Override
         public MediaSession2.CommandGroup onConnect(ControllerInfo controllerInfo) {
@@ -270,4 +363,28 @@
             return true;
         }
     }
+
+    private static void assertMediaItemListEquals(List<MediaItem2> a, List<MediaItem2> b) {
+        if (a == null || b == null) {
+            assertEquals(a, b);
+        }
+        assertEquals(a.size(), b.size());
+
+        for (int i = 0; i < a.size(); i++) {
+            MediaItem2 aItem = a.get(i);
+            MediaItem2 bItem = b.get(i);
+
+            if (aItem == null || bItem == null) {
+                assertEquals(aItem, bItem);
+                continue;
+            }
+
+            assertEquals(aItem.getMediaId(), bItem.getMediaId());
+            assertEquals(aItem.getFlags(), bItem.getFlags());
+            TestUtils.equals(aItem.getMetadata().getBundle(), bItem.getMetadata().getBundle());
+
+            // Note: Here it does not check whether DataSourceDesc are equal,
+            // since there DataSourceDec is not comparable.
+        }
+    }
 }
diff --git a/packages/MediaComponents/test/src/android/media/MediaSession2TestBase.java b/packages/MediaComponents/test/src/android/media/MediaSession2TestBase.java
index 7834a42..8e1c782 100644
--- a/packages/MediaComponents/test/src/android/media/MediaSession2TestBase.java
+++ b/packages/MediaComponents/test/src/android/media/MediaSession2TestBase.java
@@ -21,13 +21,16 @@
 
 import android.content.Context;
 import android.media.MediaController2.ControllerCallback;
+import android.media.MediaSession2.Command;
 import android.media.MediaSession2.CommandGroup;
 import android.os.Bundle;
 import android.os.HandlerThread;
+import android.os.ResultReceiver;
 import android.support.annotation.CallSuper;
 import android.support.annotation.NonNull;
 import android.support.annotation.Nullable;
 import android.support.test.InstrumentationRegistry;
+
 import java.util.ArrayList;
 import java.util.List;
 import java.util.concurrent.CountDownLatch;
@@ -57,10 +60,14 @@
     }
 
     interface TestControllerCallbackInterface {
-        // Currently empty. Add methods in ControllerCallback/BrowserCallback that you want to test.
+        // Add methods in ControllerCallback/BrowserCallback that you want to test.
+        default void onPlaylistChanged(List<MediaItem2> playlist) {}
+        default void onPlaylistParamsChanged(MediaSession2.PlaylistParams params) {}
 
-        // Browser specific callbacks
-        default void onGetRootResult(Bundle rootHints, String rootMediaId, Bundle rootExtra) {}
+        // Currently empty. Add methods in ControllerCallback/BrowserCallback that you want to test.
+        default void onPlaybackStateChanged(PlaybackState2 state) { }
+
+        default void onCustomCommand(Command command, Bundle args, ResultReceiver receiver) {}
     }
 
     interface WaitForConnectionInterface {
@@ -146,7 +153,10 @@
     }
 
     TestControllerInterface onCreateController(@NonNull SessionToken2 token,
-            @NonNull TestControllerCallbackInterface callback) {
+            @Nullable TestControllerCallbackInterface callback) {
+        if (callback == null) {
+            callback = new TestControllerCallbackInterface() {};
+        }
         return new TestMediaController(mContext, token, new TestControllerCallback(callback));
     }
 
@@ -156,7 +166,10 @@
         public final CountDownLatch connectLatch = new CountDownLatch(1);
         public final CountDownLatch disconnectLatch = new CountDownLatch(1);
 
-        TestControllerCallback(TestControllerCallbackInterface callbackProxy) {
+        TestControllerCallback(@NonNull TestControllerCallbackInterface callbackProxy) {
+            if (callbackProxy == null) {
+                throw new IllegalArgumentException("Callback proxy shouldn't be null. Test bug");
+            }
             mCallbackProxy = callbackProxy;
         }
 
@@ -175,6 +188,18 @@
         }
 
         @Override
+        public void onPlaybackStateChanged(PlaybackState2 state) {
+            super.onPlaybackStateChanged(state);
+            mCallbackProxy.onPlaybackStateChanged(state);
+        }
+
+        @Override
+        public void onCustomCommand(Command command, Bundle args, ResultReceiver receiver) {
+            super.onCustomCommand(command, args, receiver);
+            mCallbackProxy.onCustomCommand(command, args, receiver);
+        }
+
+        @Override
         public void waitForConnect(boolean expect) throws InterruptedException {
             if (expect) {
                 assertTrue(connectLatch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
@@ -191,6 +216,20 @@
                 assertFalse(disconnectLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
             }
         }
+
+        @Override
+        public void onPlaylistChanged(List<MediaItem2> params) {
+            if (mCallbackProxy != null) {
+                mCallbackProxy.onPlaylistChanged(params);
+            }
+        }
+
+        @Override
+        public void onPlaylistParamsChanged(MediaSession2.PlaylistParams params) {
+            if (mCallbackProxy != null) {
+                mCallbackProxy.onPlaylistParamsChanged(params);
+            }
+        }
     }
 
     public class TestMediaController extends MediaController2 implements TestControllerInterface {
diff --git a/packages/MediaComponents/test/src/android/media/MediaSessionManager_MediaSession2.java b/packages/MediaComponents/test/src/android/media/MediaSessionManager_MediaSession2.java
index 192cbc2..d0106fa 100644
--- a/packages/MediaComponents/test/src/android/media/MediaSessionManager_MediaSession2.java
+++ b/packages/MediaComponents/test/src/android/media/MediaSessionManager_MediaSession2.java
@@ -59,9 +59,8 @@
         // Specify TAG here so {@link MediaSession2.getInstance()} doesn't complaint about
         // per test thread differs across the {@link MediaSession2} with the same TAG.
         final MockPlayer player = new MockPlayer(1);
-        sHandler.postAndSync(() -> {
-            mSession = new MediaSession2.Builder(mContext, player).setId(TAG).build();
-        });
+        mSession = new MediaSession2.Builder(mContext, player)
+                .setSessionCallback(sHandlerExecutor, new SessionCallback()).setId(TAG).build();
         ensureChangeInSession();
     }
 
@@ -70,9 +69,7 @@
     public void cleanUp() throws Exception {
         super.cleanUp();
         sHandler.removeCallbacksAndMessages(null);
-        sHandler.postAndSync(() -> {
-            mSession.close();
-        });
+        mSession.close();
     }
 
     // TODO(jaewan): Make this host-side test to see per-user behavior.
@@ -88,7 +85,6 @@
             SessionToken2 token = tokens.get(i);
             if (mContext.getPackageName().equals(token.getPackageName())
                     && TAG.equals(token.getId())) {
-                assertNotNull(token.getSessionBinder());
                 assertNull(controller);
                 controller = createController(token);
             }
@@ -166,13 +162,11 @@
                     && MockMediaSessionService2.ID.equals(token.getId())) {
                 assertFalse(foundTestSessionService);
                 assertEquals(SessionToken2.TYPE_SESSION_SERVICE, token.getType());
-                assertNull(token.getSessionBinder());
                 foundTestSessionService = true;
             } else if (mContext.getPackageName().equals(token.getPackageName())
                     && MockMediaLibraryService2.ID.equals(token.getId())) {
                 assertFalse(foundTestLibraryService);
                 assertEquals(SessionToken2.TYPE_LIBRARY_SERVICE, token.getType());
-                assertNull(token.getSessionBinder());
                 foundTestLibraryService = true;
             }
         }
diff --git a/packages/MediaComponents/test/src/android/media/MockMediaLibraryService2.java b/packages/MediaComponents/test/src/android/media/MockMediaLibraryService2.java
index 14cf257..c1187c2 100644
--- a/packages/MediaComponents/test/src/android/media/MockMediaLibraryService2.java
+++ b/packages/MediaComponents/test/src/android/media/MockMediaLibraryService2.java
@@ -18,6 +18,8 @@
 
 import static junit.framework.Assert.fail;
 
+import static org.junit.Assert.assertEquals;
+
 import android.content.Context;
 import android.media.MediaSession2.CommandGroup;
 import android.media.MediaSession2.ControllerInfo;
@@ -70,9 +72,9 @@
     public static SessionToken2 getToken(Context context) {
         synchronized (MockMediaLibraryService2.class) {
             if (sToken == null) {
-                sToken = new SessionToken2(SessionToken2.TYPE_LIBRARY_SERVICE,
-                        context.getPackageName(), ID,
-                        MockMediaLibraryService2.class.getName(), null);
+                sToken = new SessionToken2(context, context.getPackageName(),
+                        MockMediaLibraryService2.class.getName());
+                assertEquals(SessionToken2.TYPE_LIBRARY_SERVICE, sToken.getType());
             }
             return sToken;
         }
diff --git a/packages/MediaComponents/test/src/android/media/MockMediaSessionService2.java b/packages/MediaComponents/test/src/android/media/MockMediaSessionService2.java
index b058117..5c5c7d2 100644
--- a/packages/MediaComponents/test/src/android/media/MockMediaSessionService2.java
+++ b/packages/MediaComponents/test/src/android/media/MockMediaSessionService2.java
@@ -28,6 +28,8 @@
 import android.media.session.PlaybackState;
 import android.os.Process;
 
+import java.util.concurrent.Executor;
+
 /**
  * Mock implementation of {@link android.media.MediaSessionService2} for testing.
  */
@@ -46,11 +48,12 @@
     public MediaSession2 onCreateSession(String sessionId) {
         final MockPlayer player = new MockPlayer(1);
         final SyncHandler handler = (SyncHandler) TestServiceRegistry.getInstance().getHandler();
+        final Executor executor = (runnable) -> handler.post(runnable);
         try {
             handler.postAndSync(() -> {
                 mSession = new MediaSession2.Builder(MockMediaSessionService2.this, player)
-                        .setId(sessionId).setSessionCallback((runnable)->handler.post(runnable),
-                                new MySessionCallback()).build();
+                        .setSessionCallback(executor, new MySessionCallback())
+                        .setId(sessionId).build();
             });
         } catch (InterruptedException e) {
             fail(e.toString());
diff --git a/packages/MediaComponents/test/src/android/media/MockPlayer.java b/packages/MediaComponents/test/src/android/media/MockPlayer.java
index ad7ba2f..1faf0f4 100644
--- a/packages/MediaComponents/test/src/android/media/MockPlayer.java
+++ b/packages/MediaComponents/test/src/android/media/MockPlayer.java
@@ -26,9 +26,9 @@
 import java.util.concurrent.Executor;
 
 /**
- * A mock implementation of {@link MediaPlayerBase} for testing.
+ * A mock implementation of {@link MediaPlayerInterface} for testing.
  */
-public class MockPlayer extends MediaPlayerBase {
+public class MockPlayer implements MediaPlayerInterface {
     public final CountDownLatch mCountDownLatch;
 
     public boolean mPlayCalled;
@@ -36,8 +36,22 @@
     public boolean mStopCalled;
     public boolean mSkipToPreviousCalled;
     public boolean mSkipToNextCalled;
+    public boolean mPrepareCalled;
+    public boolean mFastForwardCalled;
+    public boolean mRewindCalled;
+    public boolean mSeekToCalled;
+    public long mSeekPosition;
+    public boolean mSetCurrentPlaylistItemCalled;
+    public int mItemIndex;
+    public boolean mSetPlaylistCalled;
+    public boolean mSetPlaylistParamsCalled;
+
     public List<PlaybackListenerHolder> mListeners = new ArrayList<>();
+    public List<MediaItem2> mPlaylist;
+    public PlaylistParams mPlaylistParams;
+
     private PlaybackState2 mLastPlaybackState;
+    private AudioAttributes mAudioAttributes;
 
     public MockPlayer(int count) {
         mCountDownLatch = (count > 0) ? new CountDownLatch(count) : null;
@@ -83,7 +97,47 @@
         }
     }
 
+    @Override
+    public void prepare() {
+        mPrepareCalled = true;
+        if (mCountDownLatch != null) {
+            mCountDownLatch.countDown();
+        }
+    }
 
+    @Override
+    public void fastForward() {
+        mFastForwardCalled = true;
+        if (mCountDownLatch != null) {
+            mCountDownLatch.countDown();
+        }
+    }
+
+    @Override
+    public void rewind() {
+        mRewindCalled = true;
+        if (mCountDownLatch != null) {
+            mCountDownLatch.countDown();
+        }
+    }
+
+    @Override
+    public void seekTo(long pos) {
+        mSeekToCalled = true;
+        mSeekPosition = pos;
+        if (mCountDownLatch != null) {
+            mCountDownLatch.countDown();
+        }
+    }
+
+    @Override
+    public void setCurrentPlaylistItem(int index) {
+        mSetCurrentPlaylistItemCalled = true;
+        mItemIndex = index;
+        if (mCountDownLatch != null) {
+            mCountDownLatch.countDown();
+        }
+    }
 
     @Nullable
     @Override
@@ -112,33 +166,43 @@
         }
     }
 
-    // No-op. Should be added for test later.
     @Override
-    public void prepare() {
+    public void setPlaylistParams(PlaylistParams params) {
+        mSetPlaylistParamsCalled = true;
+        mPlaylistParams = params;
     }
 
     @Override
-    public void seekTo(long pos) {
+    public void addPlaylistItem(int index, MediaItem2 item) {
     }
 
     @Override
-    public void fastFoward() {
+    public void removePlaylistItem(MediaItem2 item) {
     }
 
     @Override
-    public void rewind() {
+    public PlaylistParams getPlaylistParams() {
+        return mPlaylistParams;
+    }
+
+    @Override
+    public void setAudioAttributes(AudioAttributes attributes) {
+        mAudioAttributes = attributes;
     }
 
     @Override
     public AudioAttributes getAudioAttributes() {
-        return null;
+        return mAudioAttributes;
     }
 
     @Override
-    public void setPlaylist(List<MediaItem2> item, PlaylistParams param) {
+    public void setPlaylist(List<MediaItem2> playlist) {
+        mSetPlaylistCalled = true;
+        mPlaylist = playlist;
     }
 
     @Override
-    public void setCurrentPlaylistItem(int index) {
+    public List<MediaItem2> getPlaylist() {
+        return mPlaylist;
     }
 }
diff --git a/packages/MediaComponents/test/src/android/media/PlaybackListenerHolder.java b/packages/MediaComponents/test/src/android/media/PlaybackListenerHolder.java
index 4e19d4d..0f1644c 100644
--- a/packages/MediaComponents/test/src/android/media/PlaybackListenerHolder.java
+++ b/packages/MediaComponents/test/src/android/media/PlaybackListenerHolder.java
@@ -16,10 +16,8 @@
 
 package android.media;
 
-import android.media.MediaPlayerBase.PlaybackListener;
-import android.media.session.PlaybackState;
+import android.media.MediaPlayerInterface.PlaybackListener;
 import android.os.Handler;
-import android.os.Message;
 import android.support.annotation.NonNull;
 
 import java.util.List;
diff --git a/services/audioflinger/AudioFlinger.cpp b/services/audioflinger/AudioFlinger.cpp
index e362530..592273e 100644
--- a/services/audioflinger/AudioFlinger.cpp
+++ b/services/audioflinger/AudioFlinger.cpp
@@ -1976,6 +1976,43 @@
     return NO_ERROR;
 }
 
+status_t AudioFlinger::getMicrophones(std::vector<media::MicrophoneInfo> *microphones)
+{
+    // Fake data
+    size_t fakeNum = 2;
+    audio_devices_t fakeTypes[] = { AUDIO_DEVICE_IN_BUILTIN_MIC, AUDIO_DEVICE_IN_BACK_MIC };
+    for (size_t i = 0; i < fakeNum; i++) {
+        struct audio_microphone_characteristic_t characteristics;
+        sprintf(characteristics.device_id, "microphone:%zu", i);
+        characteristics.type = fakeTypes[i];
+        sprintf(characteristics.address, "");
+        characteristics.location = AUDIO_MICROPHONE_LOCATION_MAINBODY;
+        characteristics.group = 0;
+        characteristics.index_in_the_group = i;
+        characteristics.sensitivity = 1.0f;
+        characteristics.max_spl = 100.0f;
+        characteristics.min_spl = 0.0f;
+        characteristics.directionality = AUDIO_MICROPHONE_DIRECTIONALITY_OMNI;
+        characteristics.num_frequency_responses = 5 - i;
+        for (size_t j = 0; j < characteristics.num_frequency_responses; j++) {
+            characteristics.frequency_responses[0][j] = 100.0f - j;
+            characteristics.frequency_responses[1][j] = 100.0f + j;
+        }
+        for (size_t j = 0; j < AUDIO_CHANNEL_COUNT_MAX; j++) {
+            characteristics.channel_mapping[j] = AUDIO_MICROPHONE_CHANNEL_MAPPING_UNUSED;
+        }
+        characteristics.geometric_location.x = 0.1f;
+        characteristics.geometric_location.y = 0.2f;
+        characteristics.geometric_location.z = 0.3f;
+        characteristics.orientation.x = 0.0f;
+        characteristics.orientation.y = 1.0f;
+        characteristics.orientation.z = 0.0f;
+        media::MicrophoneInfo microphoneInfo = media::MicrophoneInfo(characteristics);
+        microphones->push_back(microphoneInfo);
+    }
+    return NO_ERROR;
+}
+
 // setAudioHwSyncForSession_l() must be called with AudioFlinger::mLock held
 void AudioFlinger::setAudioHwSyncForSession_l(PlaybackThread *thread, audio_session_t sessionId)
 {
diff --git a/services/audioflinger/AudioFlinger.h b/services/audioflinger/AudioFlinger.h
index 296f092..83caca7 100644
--- a/services/audioflinger/AudioFlinger.h
+++ b/services/audioflinger/AudioFlinger.h
@@ -250,6 +250,8 @@
     /* Indicate JAVA services are ready (scheduling, power management ...) */
     virtual status_t systemReady();
 
+    virtual status_t getMicrophones(std::vector<media::MicrophoneInfo> *microphones);
+
     virtual     status_t    onTransact(
                                 uint32_t code,
                                 const Parcel& data,
diff --git a/services/camera/libcameraservice/CameraService.cpp b/services/camera/libcameraservice/CameraService.cpp
index 536b54d..e7609ed 100644
--- a/services/camera/libcameraservice/CameraService.cpp
+++ b/services/camera/libcameraservice/CameraService.cpp
@@ -2054,7 +2054,8 @@
         mp->prepare();
     } else {
         ALOGE("Failed to load CameraService sounds: %s", file);
-        return NULL;
+        delete mp;
+        return nullptr;
     }
     return mp;
 }
@@ -2066,9 +2067,19 @@
     LOG1("CameraService::loadSound ref=%d", mSoundRef);
     if (mSoundRef++) return;
 
-    mSoundPlayer[SOUND_SHUTTER] = newMediaPlayer("/system/media/audio/ui/camera_click.ogg");
-    mSoundPlayer[SOUND_RECORDING_START] = newMediaPlayer("/system/media/audio/ui/VideoRecord.ogg");
-    mSoundPlayer[SOUND_RECORDING_STOP] = newMediaPlayer("/system/media/audio/ui/VideoStop.ogg");
+    mSoundPlayer[SOUND_SHUTTER] = newMediaPlayer("/product/media/audio/ui/camera_click.ogg");
+    if (mSoundPlayer[SOUND_SHUTTER] == nullptr) {
+        mSoundPlayer[SOUND_SHUTTER] = newMediaPlayer("/system/media/audio/ui/camera_click.ogg");
+    }
+    mSoundPlayer[SOUND_RECORDING_START] = newMediaPlayer("/product/media/audio/ui/VideoRecord.ogg");
+    if (mSoundPlayer[SOUND_RECORDING_START] == nullptr) {
+        mSoundPlayer[SOUND_RECORDING_START] =
+                newMediaPlayer("/system/media/audio/ui/VideoRecord.ogg");
+    }
+    mSoundPlayer[SOUND_RECORDING_STOP] = newMediaPlayer("/product/media/audio/ui/VideoStop.ogg");
+    if (mSoundPlayer[SOUND_RECORDING_STOP] == nullptr) {
+        mSoundPlayer[SOUND_RECORDING_STOP] = newMediaPlayer("/system/media/audio/ui/VideoStop.ogg");
+    }
 }
 
 void CameraService::releaseSound() {