MediaSession2: Move MediaSession2/MediaController2 from experimental

APIs will be unhidden later

Test: Run MediaComponentsTest
Change-Id: I4e6f5937baa7e09cf850929e534ac44b5278d744
diff --git a/packages/MediaComponents/test/Android.mk b/packages/MediaComponents/test/Android.mk
new file mode 100644
index 0000000..8703b9f
--- /dev/null
+++ b/packages/MediaComponents/test/Android.mk
@@ -0,0 +1,28 @@
+# Copyright 2018 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+LOCAL_PATH:= $(call my-dir)
+include $(CLEAR_VARS)
+
+# TODO(jaewan): Copy this to the CTS as well
+LOCAL_MODULE_TAGS := tests
+
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+LOCAL_STATIC_JAVA_LIBRARIES := \
+    android-support-test \
+    mockito-target-minus-junit4 \
+    compatibility-device-util
+
+LOCAL_PACKAGE_NAME := MediaComponentsTest
+include $(BUILD_PACKAGE)
diff --git a/packages/MediaComponents/test/AndroidManifest.xml b/packages/MediaComponents/test/AndroidManifest.xml
new file mode 100644
index 0000000..fe16583
--- /dev/null
+++ b/packages/MediaComponents/test/AndroidManifest.xml
@@ -0,0 +1,46 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          package="android.media.test">
+
+    <application android:label="Media API Test">
+        <uses-library android:name="android.test.runner" />
+
+        <activity android:name="android.widget2.VideoView2TestActivity"
+                  android:configChanges="keyboardHidden|orientation|screenSize"
+                  android:label="VideoView2TestActivity">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST" />
+            </intent-filter>
+          </activity>
+
+        <!-- Keep the test services synced together with the TestUtils.java -->
+        <service android:name="android.media.MockMediaSessionService2">
+            <intent-filter>
+                <action android:name="android.media.session.MediaSessionService2" />
+            </intent-filter>
+            <meta-data android:name="android.media.session" android:value="TestSession" />
+        </service>
+    </application>
+
+    <instrumentation
+        android:name="android.support.test.runner.AndroidJUnitRunner"
+        android:targetPackage="android.media.test"
+        android:label="Media API test" />
+
+</manifest>
diff --git a/packages/MediaComponents/test/runtest.sh b/packages/MediaComponents/test/runtest.sh
new file mode 100644
index 0000000..bda48b0
--- /dev/null
+++ b/packages/MediaComponents/test/runtest.sh
@@ -0,0 +1,189 @@
+#!/bin/bash
+# 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.
+
+# Usage '. runtest.sh'
+
+function _runtest_mediacomponent_usage() {
+  echo 'runtest-MediaComponents [option]: Run MediaComponents test'
+  echo '     -h|--help: This help'
+  echo '     --skip: Skip build. Just rerun-tests.'
+  echo '     --min: Only rebuild test apk and updatable library.'
+  echo '     -s [device_id]: Specify a device name to run test against.'
+  echo '                     You can define ${ADBHOST} instead.'
+  echo '     -r [count]: Repeat tests for given count. It will stop when fails.'
+  echo '     --ignore: Keep repeating tests even when it fails.'
+  echo '     -t [test]: Only run the specific test. Can be either a class or a method.'
+}
+
+function runtest-MediaComponents() {
+  # Edit here if you want to support other tests.
+  # List up libs and apks in the media_api needed for tests, and place test target at the last.
+  local TEST_PACKAGE_DIR=("frameworks/av/packages/MediaComponents/test")
+  local BUILD_TARGETS=("MediaComponents" "MediaComponentsTest")
+  local INSTALL_TARGETS=("MediaComponentsTest")
+  local TEST_RUNNER="android.support.test.runner.AndroidJUnitRunner"
+  local DEPENDENCIES=("mockito-target-minus-junit4" "android-support-test" "compatibility-device-util")
+
+  if [[ -z "${ANDROID_BUILD_TOP}" ]]; then
+    echo "Needs to lunch a target first"
+    return
+  fi
+
+  local old_path=${OLDPWD}
+  while true; do
+    local OPTION_SKIP="false"
+    local OPTION_MIN="false"
+    local OPTION_REPEAT_COUNT="1"
+    local OPTION_IGNORE="false"
+    local OPTION_TEST_TARGET=""
+    local adbhost_local
+    while (( "$#" )); do
+      case "${1}" in
+        -h|--help)
+          _runtest_mediacomponent_usage
+          return
+          ;;
+        --skip)
+          OPTION_SKIP="true"
+          ;;
+        --min)
+          OPTION_MIN="true"
+          ;;
+        -s)
+          shift
+          adbhost_local=${1}
+          ;;
+        -r)
+          shift
+          OPTION_REPEAT_COUNT="${1}"
+          ;;
+        --ignore)
+          OPTION_IGNORE="true"
+          ;;
+        -t)
+          shift
+          OPTION_TEST_TARGET="${1}"
+      esac
+      shift
+    done
+
+    # Build adb command.
+    local adb
+    if [[ -z "${adbhost_local}" ]]; then
+      adbhost_local=${ADBHOST}
+    fi
+    if [[ -z "${adbhost_local}" ]]; then
+      local device_count=$(adb devices | sed '/^[[:space:]]*$/d' | wc -l)
+      if [[ "${device_count}" != "2" ]]; then
+        echo "Too many devices. Specify a device." && break
+      fi
+      adb="adb"
+    else
+      adb="adb -s ${adbhost_local}"
+    fi
+
+    local target_dir="${ANDROID_BUILD_TOP}/${TEST_PACKAGE_DIR}"
+    local TEST_PACKAGE=$(sed -n 's/^.*\bpackage\b="\([a-z0-9\.]*\)".*$/\1/p' ${target_dir}/AndroidManifest.xml)
+
+    if [[ "${OPTION_SKIP}" != "true" ]]; then
+      # Build dependencies if needed.
+      local dependency
+      local build_dependency=""
+      for dependency in ${DEPENDENCIES[@]}; do
+        if [[ "${dependency}" == "out/"* ]]; then
+          if [[ ! -f ${ANDROID_BUILD_TOP}/${dependency} ]]; then
+            build_dependency="true"
+            break
+          fi
+        else
+          if [[ "$(find ${OUT} -name ${dependency}_intermediates | wc -l)" == "0" ]]; then
+            build_dependency="true"
+            break
+          fi
+        fi
+      done
+      if [[ "${build_dependency}" == "true" ]]; then
+        echo "Building dependencies. Will only print stderr."
+        m ${DEPENDENCIES[@]} -j > /dev/null
+      fi
+
+      # Build test apk and required apk.
+      local build_targets="${BUILD_TARGETS[@]}"
+      if [[ "${OPTION_MIN}" != "true" ]]; then
+        build_targets="${build_targets} droid"
+      fi
+      m ${build_targets} -j || (echo "Build failed. stop" ; break)
+
+      ${adb} root
+      ${adb} remount
+      ${adb} shell stop
+      ${adb} sync
+      ${adb} shell start
+      ${adb} wait-for-device || break
+      # Ensure package manager is loaded.
+      sleep 5
+
+      # Install apks
+      local install_failed="false"
+      for target in ${INSTALL_TARGETS[@]}; do
+        echo "${target}"
+        local target_dir=$(mgrep -l -e '^LOCAL_PACKAGE_NAME.*'"${target}$")
+        if [[ -z ${target_dir} ]]; then
+          continue
+        fi
+        target_dir=$(dirname ${target_dir})
+        local package=$(sed -n 's/^.*\bpackage\b="\([a-z0-9\._]*\)".*$/\1/p' ${target_dir}/AndroidManifest.xml)
+        local apk_path=$(find ${OUT} -name ${target}.apk)
+        if [[ -z "${apk_path}" ]]; then
+          echo "Cannot locate ${target}.apk" && break
+        fi
+        echo "Installing ${target}.apk. path=${apk_path}"
+        ${adb} install -r ${apk_path}
+        if [[ "${?}" != "0" ]]; then
+          install_failed="true"
+          break
+        fi
+      done
+      if [[ "${install_failed}" == "true" ]]; then
+        echo "Failed to install. Test wouldn't run."
+        break
+      fi
+    fi
+
+    local test_target=""
+    if [[ -n "${OPTION_TEST_TARGET}" ]]; then
+      test_target="-e class ${OPTION_TEST_TARGET}"
+    fi
+
+    local i
+    local tmpfile=$(tempfile)
+    for ((i=1; i <= ${OPTION_REPEAT_COUNT}; i++)); do
+      echo "Run test ${i}/${OPTION_REPEAT_COUNT}"
+      ${adb} shell am instrument ${test_target} -w ${TEST_PACKAGE}/${TEST_RUNNER} >& ${tmpfile}
+      cat ${tmpfile}
+      if [[ "${OPTION_IGNORE}" != "true" ]]; then
+        if [[ -n "$(grep ${tmpfile} -e 'FAILURE\|crashed')" ]]; then
+          # am instrument doesn't return error code so need to grep result message instead
+          break
+        fi
+      fi
+    done
+    rm ${tmpfile}
+    break
+  done
+}
+
+echo "Following functions are added to your environment:"
+_runtest_mediacomponent_usage
diff --git a/packages/MediaComponents/test/src/android/media/MediaController2Test.java b/packages/MediaComponents/test/src/android/media/MediaController2Test.java
new file mode 100644
index 0000000..161a463
--- /dev/null
+++ b/packages/MediaComponents/test/src/android/media/MediaController2Test.java
@@ -0,0 +1,452 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.media;
+
+import android.media.MediaPlayerBase.PlaybackListener;
+import android.media.MediaSession2.ControllerInfo;
+import android.media.MediaSession2.SessionCallback;
+import android.media.TestUtils.SyncHandler;
+import android.media.session.PlaybackState;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Process;
+import android.support.test.filters.FlakyTest;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Ignore;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+import static android.media.TestUtils.createPlaybackState;
+import static org.junit.Assert.*;
+
+/**
+ * Tests {@link MediaController2}.
+ */
+// TODO(jaewan): Implement host-side test so controller and session can run in different processes.
+// TODO(jaewan): Fix flaky failure -- see MediaController2Impl.getController()
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+@FlakyTest
+public class MediaController2Test extends MediaSession2TestBase {
+    private static final String TAG = "MediaController2Test";
+
+    private MediaSession2 mSession;
+    private MediaController2Wrapper mController;
+    private MockPlayer mPlayer;
+
+    @Before
+    @Override
+    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();
+        });
+
+        mController = createController(mSession.getToken());
+        TestServiceRegistry.getInstance().setHandler(sHandler);
+    }
+
+    @After
+    @Override
+    public void cleanUp() throws Exception {
+        super.cleanUp();
+        sHandler.postAndSync(() -> {
+            if (mSession != null) {
+                mSession.setPlayer(null);
+            }
+        });
+        TestServiceRegistry.getInstance().cleanUp();
+    }
+
+    @Test
+    public void testPlay() throws InterruptedException {
+        mController.play();
+        try {
+            assertTrue(mPlayer.mCountDownLatch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+        } catch (InterruptedException e) {
+            fail(e.getMessage());
+        }
+        assertTrue(mPlayer.mPlayCalled);
+    }
+
+    @Test
+    public void testPause() throws InterruptedException {
+        mController.pause();
+        try {
+            assertTrue(mPlayer.mCountDownLatch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+        } catch (InterruptedException e) {
+            fail(e.getMessage());
+        }
+        assertTrue(mPlayer.mPauseCalled);
+    }
+
+
+    @Test
+    public void testSkipToPrevious() throws InterruptedException {
+        mController.skipToPrevious();
+        try {
+            assertTrue(mPlayer.mCountDownLatch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+        } catch (InterruptedException e) {
+            fail(e.getMessage());
+        }
+        assertTrue(mPlayer.mSkipToPreviousCalled);
+    }
+
+    @Test
+    public void testSkipToNext() throws InterruptedException {
+        mController.skipToNext();
+        try {
+            assertTrue(mPlayer.mCountDownLatch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+        } catch (InterruptedException e) {
+            fail(e.getMessage());
+        }
+        assertTrue(mPlayer.mSkipToNextCalled);
+    }
+
+    @Test
+    public void testStop() throws InterruptedException {
+        mController.stop();
+        try {
+            assertTrue(mPlayer.mCountDownLatch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+        } catch (InterruptedException e) {
+            fail(e.getMessage());
+        }
+        assertTrue(mPlayer.mStopCalled);
+    }
+
+    @Test
+    public void testGetPackageName() {
+        assertEquals(mContext.getPackageName(), mController.getSessionToken().getPackageName());
+    }
+
+    @Test
+    public void testGetPlaybackState() throws InterruptedException {
+        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());
+    }
+
+    @Test
+    public void testAddPlaybackListener() 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;
+            }
+            latch.countDown();
+        };
+
+        mController.addPlaybackListener(listener, sHandler);
+        sHandler.postAndSync(()->{
+            mPlayer.notifyPlaybackState(createPlaybackState(PlaybackState.STATE_PLAYING));
+            mPlayer.notifyPlaybackState(createPlaybackState(PlaybackState.STATE_PAUSED));
+        });
+        assertTrue(latch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+    }
+
+    @Test
+    public void testRemovePlaybackListener() throws InterruptedException {
+        final CountDownLatch latch = new CountDownLatch(1);
+        final MediaPlayerBase.PlaybackListener listener = (state) -> {
+            fail();
+            latch.countDown();
+        };
+        mController.addPlaybackListener(listener, sHandler);
+        mController.removePlaybackListener(listener);
+        mPlayer.notifyPlaybackState(createPlaybackState(PlaybackState.STATE_PLAYING));
+        assertFalse(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+    }
+
+    @Test
+    public void testControllerCallback_onConnected() throws InterruptedException {
+        // createController() uses controller callback to wait until the controller becomes
+        // available.
+        MediaController2 controller = createController(mSession.getToken());
+        assertNotNull(controller);
+    }
+
+    @Test
+    public void testControllerCallback_sessionRejects() throws InterruptedException {
+        final MediaSession2.SessionCallback sessionCallback = new SessionCallback() {
+            @Override
+            public long onConnect(ControllerInfo controller) {
+                return 0;
+            }
+        };
+        sHandler.postAndSync(() -> {
+            mSession.setPlayer(null);
+            mSession = new MediaSession2.Builder(mContext, mPlayer)
+                    .setSessionCallback(sessionCallback).build();
+        });
+        MediaController2Wrapper controller = createController(mSession.getToken(), false, null);
+        assertNotNull(controller);
+        controller.waitForConnect(false);
+        controller.waitForDisconnect(true);
+    }
+
+    @Test
+    public void testControllerCallback_releaseSession() throws InterruptedException {
+        sHandler.postAndSync(() -> {
+            mSession.setPlayer(null);
+        });
+        mController.waitForDisconnect(true);
+    }
+
+    @Test
+    public void testControllerCallback_release() throws InterruptedException {
+        mController.release();
+        mController.waitForDisconnect(true);
+    }
+
+    @Test
+    public void testIsConnected() throws InterruptedException {
+        assertTrue(mController.isConnected());
+        sHandler.postAndSync(()->{
+            mSession.setPlayer(null);
+        });
+        // postAndSync() to wait until the disconnection is propagated.
+        sHandler.postAndSync(()->{
+            assertFalse(mController.isConnected());
+        });
+    }
+
+    /**
+     * Test potential deadlock for calls between controller and session.
+     */
+    @Test
+    public void testDeadlock() throws InterruptedException {
+        sHandler.postAndSync(() -> {
+            mSession.setPlayer(null);
+            mSession = null;
+        });
+
+        // Two more threads are needed not to block test thread nor test wide thread (sHandler).
+        final HandlerThread sessionThread = new HandlerThread("testDeadlock_session");
+        final HandlerThread testThread = new HandlerThread("testDeadlock_test");
+        sessionThread.start();
+        testThread.start();
+        final SyncHandler sessionHandler = new SyncHandler(sessionThread.getLooper());
+        final Handler testHandler = new Handler(testThread.getLooper());
+        final CountDownLatch latch = new CountDownLatch(1);
+        try {
+            final MockPlayer player = new MockPlayer(0);
+            sessionHandler.postAndSync(() -> {
+                mSession = new MediaSession2.Builder(mContext, mPlayer)
+                        .setId("testDeadlock").build();
+            });
+            final MediaController2 controller = createController(mSession.getToken());
+            testHandler.post(() -> {
+                controller.addPlaybackListener((state) -> {
+                    // no-op. Just to set a binder call path from session to controller.
+                }, sessionHandler);
+                final PlaybackState state = createPlaybackState(PlaybackState.STATE_ERROR);
+                for (int i = 0; i < 100; i++) {
+                    // triggers call from session to controller.
+                    player.notifyPlaybackState(state);
+                    // triggers call from controller to session.
+                    controller.play();
+
+                    // Repeat above
+                    player.notifyPlaybackState(state);
+                    controller.pause();
+                    player.notifyPlaybackState(state);
+                    controller.stop();
+                    player.notifyPlaybackState(state);
+                    controller.skipToNext();
+                    player.notifyPlaybackState(state);
+                    controller.skipToPrevious();
+                }
+                // This may hang if deadlock happens.
+                latch.countDown();
+            });
+            assertTrue(latch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+        } finally {
+            if (mSession != null) {
+                sessionHandler.postAndSync(() -> {
+                    // Clean up here because sessionHandler will be removed afterwards.
+                    mSession.setPlayer(null);
+                    mSession = null;
+                });
+            }
+            if (sessionThread != null) {
+                sessionThread.quitSafely();
+            }
+            if (testThread != null) {
+                testThread.quitSafely();
+            }
+        }
+    }
+
+    @Ignore
+    @Test
+    public void testGetServiceToken() {
+        SessionToken token = TestUtils.getServiceToken(mContext, MockMediaSessionService2.ID);
+        assertNotNull(token);
+        assertEquals(mContext.getPackageName(), token.getPackageName());
+        assertEquals(MockMediaSessionService2.ID, token.getId());
+        assertNull(token.getSessionBinder());
+        assertEquals(SessionToken.TYPE_SESSION_SERVICE, token.getType());
+    }
+
+    private void connectToService(SessionToken token) throws InterruptedException {
+        mController = createController(token);
+        mSession = TestServiceRegistry.getInstance().getServiceInstance().getSession();
+        mPlayer = (MockPlayer) mSession.getPlayer();
+    }
+
+    @Ignore
+    @Test
+    public void testConnectToService() throws InterruptedException {
+        connectToService(TestUtils.getServiceToken(mContext, MockMediaSessionService2.ID));
+
+        TestServiceRegistry serviceInfo = TestServiceRegistry.getInstance();
+        ControllerInfo info = serviceInfo.getOnConnectControllerInfo();
+        assertEquals(mContext.getPackageName(), info.getPackageName());
+        assertEquals(Process.myUid(), info.getUid());
+        assertFalse(info.isTrusted());
+
+        // Test command from controller to session service
+        mController.play();
+        assertTrue(mPlayer.mCountDownLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+        assertTrue(mPlayer.mPlayCalled);
+
+        // Test command from session service to controller
+        final CountDownLatch latch = new CountDownLatch(1);
+        mController.addPlaybackListener((state) -> {
+            assertNotNull(state);
+            assertEquals(PlaybackState.STATE_REWINDING, state.getState());
+            latch.countDown();
+        }, sHandler);
+        mPlayer.notifyPlaybackState(
+                TestUtils.createPlaybackState(PlaybackState.STATE_REWINDING));
+        assertTrue(latch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+    }
+
+    @Test
+    public void testControllerAfterSessionIsGone_session() throws InterruptedException {
+        testControllerAfterSessionIsGone(mSession.getToken().getId());
+    }
+
+    @Ignore
+    @Test
+    public void testControllerAfterSessionIsGone_sessionService() throws InterruptedException {
+        connectToService(TestUtils.getServiceToken(mContext, MockMediaSessionService2.ID));
+        testControllerAfterSessionIsGone(MockMediaSessionService2.ID);
+    }
+
+    @Test
+    public void testRelease_beforeConnected() throws InterruptedException {
+        MediaController2 controller =
+                createController(mSession.getToken(), false, null);
+        controller.release();
+    }
+
+    @Test
+    public void testRelease_twice() throws InterruptedException {
+        mController.release();
+        mController.release();
+    }
+
+    @Test
+    public void testRelease_session() throws InterruptedException {
+        final String id = mSession.getToken().getId();
+        mController.release();
+        // Release is done immediately for session.
+        testNoInteraction();
+
+        // Test whether the controller is notified about later release of the session or
+        // re-creation.
+        testControllerAfterSessionIsGone(id);
+    }
+
+    @Ignore
+    @Test
+    public void testRelease_sessionService() throws InterruptedException {
+        connectToService(TestUtils.getServiceToken(mContext, MockMediaSessionService2.ID));
+        final CountDownLatch latch = new CountDownLatch(1);
+        TestServiceRegistry.getInstance().setServiceInstanceChangedCallback((service) -> {
+            if (service == null) {
+                // Destroying..
+                latch.countDown();
+            }
+        });
+        mController.release();
+        // Wait until release triggers onDestroy() of the session service.
+        assertTrue(latch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+        assertNull(TestServiceRegistry.getInstance().getServiceInstance());
+        testNoInteraction();
+
+        // Test whether the controller is notified about later release of the session or
+        // re-creation.
+        testControllerAfterSessionIsGone(MockMediaSessionService2.ID);
+    }
+
+    private void testControllerAfterSessionIsGone(final String id) throws InterruptedException {
+        sHandler.postAndSync(() -> {
+            // TODO(jaewan): Use Session.release later when we add the API.
+            mSession.setPlayer(null);
+        });
+        mController.waitForDisconnect(true);
+        testNoInteraction();
+
+        // Test with the newly created session.
+        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();
+        });
+        testNoInteraction();
+    }
+
+
+    private void testNoInteraction() throws InterruptedException {
+        final CountDownLatch latch = new CountDownLatch(1);
+        final PlaybackListener playbackListener = (state) -> {
+            fail("Controller shouldn't be notified about change in session after the release.");
+            latch.countDown();
+        };
+        mController.addPlaybackListener(playbackListener, sHandler);
+        mPlayer.notifyPlaybackState(TestUtils.createPlaybackState(PlaybackState.STATE_BUFFERING));
+        assertFalse(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+        mController.removePlaybackListener(playbackListener);
+    }
+
+    // TODO(jaewan): Add  test for service connect rejection, when we differentiate session
+    //               active/inactive and connection accept/refuse
+}
diff --git a/packages/MediaComponents/test/src/android/media/MediaSession2Test.java b/packages/MediaComponents/test/src/android/media/MediaSession2Test.java
new file mode 100644
index 0000000..a77fd63
--- /dev/null
+++ b/packages/MediaComponents/test/src/android/media/MediaSession2Test.java
@@ -0,0 +1,266 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.media;
+
+import android.media.MediaPlayerBase.PlaybackListener;
+import android.media.MediaSession2.Builder;
+import android.media.MediaSession2.ControllerInfo;
+import android.media.MediaSession2.SessionCallback;
+import android.media.session.PlaybackState;
+import android.os.Process;
+import android.os.Looper;
+import android.support.annotation.NonNull;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import java.util.ArrayList;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+import static android.media.TestUtils.createPlaybackState;
+import static org.junit.Assert.*;
+
+/**
+ * Tests {@link MediaSession2}.
+ */
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class MediaSession2Test extends MediaSession2TestBase {
+    private static final String TAG = "MediaSession2Test";
+
+    private MediaSession2 mSession;
+    private MockPlayer mPlayer;
+
+    @Before
+    @Override
+    public void setUp() throws Exception {
+        super.setUp();
+        sHandler.postAndSync(() -> {
+            mPlayer = new MockPlayer(0);
+            mSession = new MediaSession2.Builder(mContext, mPlayer).build();
+        });
+    }
+
+    @After
+    @Override
+    public void cleanUp() throws Exception {
+        super.cleanUp();
+        sHandler.postAndSync(() -> {
+            mSession.setPlayer(null);
+        });
+    }
+
+    @Test
+    public void testBuilder() throws Exception {
+        try {
+            MediaSession2.Builder builder = new Builder(mContext, null);
+            fail("null player shouldn't be allowed");
+        } catch (IllegalArgumentException e) {
+            // expected. pass-through
+        }
+        MediaSession2.Builder builder = new Builder(mContext, mPlayer);
+        try {
+            builder.setId(null);
+            fail("null id shouldn't be allowed");
+        } catch (IllegalArgumentException e) {
+            // expected. pass-through
+        }
+    }
+
+    @Test
+    public void testSetPlayer() throws Exception {
+        sHandler.postAndSync(() -> {
+            MockPlayer player = new MockPlayer(0);
+            // Test if setPlayer doesn't crash with various situations.
+            mSession.setPlayer(mPlayer);
+            mSession.setPlayer(player);
+            mSession.setPlayer(null);
+        });
+    }
+
+    @Test
+    public void testPlay() throws Exception {
+        sHandler.postAndSync(() -> {
+            mSession.play();
+            assertTrue(mPlayer.mPlayCalled);
+        });
+    }
+
+    @Test
+    public void testPause() throws Exception {
+        sHandler.postAndSync(() -> {
+            mSession.pause();
+            assertTrue(mPlayer.mPauseCalled);
+        });
+    }
+
+    @Test
+    public void testStop() throws Exception {
+        sHandler.postAndSync(() -> {
+            mSession.stop();
+            assertTrue(mPlayer.mStopCalled);
+        });
+    }
+
+    @Test
+    public void testSkipToNext() throws Exception {
+        sHandler.postAndSync(() -> {
+            mSession.skipToNext();
+            assertTrue(mPlayer.mSkipToNextCalled);
+        });
+    }
+
+    @Test
+    public void testSkipToPrevious() throws Exception {
+        sHandler.postAndSync(() -> {
+            mSession.skipToPrevious();
+            assertTrue(mPlayer.mSkipToPreviousCalled);
+        });
+    }
+
+    @Test
+    public void testPlaybackStateChangedListener() throws InterruptedException {
+        final CountDownLatch latch = new CountDownLatch(2);
+        final MockPlayer player = new MockPlayer(0);
+        final PlaybackListener listener = (state) -> {
+            assertEquals(sHandler.getLooper(), Looper.myLooper());
+            assertNotNull(state);
+            switch ((int) latch.getCount()) {
+                case 2:
+                    assertEquals(PlaybackState.STATE_PLAYING, state.getState());
+                    break;
+                case 1:
+                    assertEquals(PlaybackState.STATE_PAUSED, state.getState());
+                    break;
+                case 0:
+                    fail();
+            }
+            latch.countDown();
+        };
+        player.notifyPlaybackState(createPlaybackState(PlaybackState.STATE_PLAYING));
+        sHandler.postAndSync(() -> {
+            mSession.addPlaybackListener(listener, sHandler);
+            // When the player is set, listeners will be notified about the player's current state.
+            mSession.setPlayer(player);
+        });
+        player.notifyPlaybackState(createPlaybackState(PlaybackState.STATE_PAUSED));
+        assertTrue(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+    }
+
+    @Test
+    public void testBadPlayer() throws InterruptedException {
+        final CountDownLatch latch = new CountDownLatch(3); // expected call + 1
+        final BadPlayer player = new BadPlayer(0);
+        sHandler.postAndSync(() -> {
+            mSession.addPlaybackListener((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));
+        assertFalse(latch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+    }
+
+    private static class BadPlayer extends MockPlayer {
+        public BadPlayer(int count) {
+            super(count);
+        }
+
+        @Override
+        public void removePlaybackListener(@NonNull PlaybackListener listener) {
+            // No-op. This bad player will keep push notification to the listener that is previously
+            // registered by session.setPlayer().
+        }
+    }
+
+    @Test
+    public void testOnCommandCallback() throws InterruptedException {
+        final MockOnCommandCallback callback = new MockOnCommandCallback();
+        sHandler.postAndSync(() -> {
+            mSession.setPlayer(null);
+            mPlayer = new MockPlayer(1);
+            mSession = new MediaSession2.Builder(mContext, mPlayer)
+                    .setSessionCallback(callback).build();
+        });
+        MediaController2 controller = createController(mSession.getToken());
+        controller.pause();
+        assertFalse(mPlayer.mCountDownLatch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+        assertFalse(mPlayer.mPauseCalled);
+        assertEquals(1, callback.commands.size());
+        assertEquals(MediaSession2.COMMAND_FLAG_PLAYBACK_PAUSE,
+                (long) callback.commands.get(0));
+        controller.skipToNext();
+        assertTrue(mPlayer.mCountDownLatch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+        assertTrue(mPlayer.mSkipToNextCalled);
+        assertFalse(mPlayer.mPauseCalled);
+        assertEquals(2, callback.commands.size());
+        assertEquals(MediaSession2.COMMAND_FLAG_PLAYBACK_SKIP_NEXT_ITEM,
+                (long) callback.commands.get(1));
+    }
+
+    @Test
+    public void testOnConnectCallback() throws InterruptedException {
+        final MockOnConnectCallback sessionCallback = new MockOnConnectCallback();
+        sHandler.postAndSync(() -> {
+            mSession.setPlayer(null);
+            mSession = new MediaSession2.Builder(mContext, mPlayer)
+                    .setSessionCallback(sessionCallback).build();
+        });
+        MediaController2Wrapper controller = createController(mSession.getToken(), false, null);
+        assertNotNull(controller);
+        controller.waitForConnect(false);
+        controller.waitForDisconnect(true);
+    }
+
+    public class MockOnConnectCallback extends SessionCallback {
+        @Override
+        public long onConnect(ControllerInfo controllerInfo) {
+            if (Process.myUid() != controllerInfo.getUid()) {
+                return 0;
+            }
+            assertEquals(mContext.getPackageName(), controllerInfo.getPackageName());
+            assertEquals(Process.myUid(), controllerInfo.getUid());
+            assertFalse(controllerInfo.isTrusted());
+            // Reject all
+            return 0;
+        }
+    }
+
+    public class MockOnCommandCallback extends SessionCallback {
+        public final ArrayList<Long> commands = new ArrayList<>();
+
+        @Override
+        public boolean onCommand(ControllerInfo controllerInfo, long command) {
+            assertEquals(mContext.getPackageName(), controllerInfo.getPackageName());
+            assertEquals(Process.myUid(), controllerInfo.getUid());
+            assertFalse(controllerInfo.isTrusted());
+            commands.add(command);
+            if (command == MediaSession2.COMMAND_FLAG_PLAYBACK_PAUSE) {
+                return false;
+            }
+            return true;
+        }
+    }
+}
diff --git a/packages/MediaComponents/test/src/android/media/MediaSession2TestBase.java b/packages/MediaComponents/test/src/android/media/MediaSession2TestBase.java
new file mode 100644
index 0000000..80b8b79
--- /dev/null
+++ b/packages/MediaComponents/test/src/android/media/MediaSession2TestBase.java
@@ -0,0 +1,147 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.media;
+
+import static junit.framework.Assert.assertFalse;
+import static junit.framework.Assert.assertTrue;
+
+import android.content.Context;
+import android.os.HandlerThread;
+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;
+import java.util.concurrent.Executor;
+import java.util.concurrent.TimeUnit;
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+
+/**
+ * Base class for session test.
+ */
+abstract class MediaSession2TestBase {
+    // Expected success
+    static final int WAIT_TIME_MS = 1000;
+
+    // Expected timeout
+    static final int TIMEOUT_MS = 500;
+
+    static TestUtils.SyncHandler sHandler;
+    static Executor sHandlerExecutor;
+
+    Context mContext;
+    private List<MediaController2> mControllers = new ArrayList<>();
+
+    @BeforeClass
+    public static void setUpThread() {
+        if (sHandler == null) {
+            HandlerThread handlerThread = new HandlerThread("MediaSession2TestBase");
+            handlerThread.start();
+            sHandler = new TestUtils.SyncHandler(handlerThread.getLooper());
+            sHandlerExecutor = (runnable) -> {
+                sHandler.post(runnable);
+            };
+        }
+    }
+
+    @AfterClass
+    public static void cleanUpThread() {
+        if (sHandler != null) {
+            sHandler.getLooper().quitSafely();
+            sHandler = null;
+            sHandlerExecutor = null;
+        }
+    }
+
+    @CallSuper
+    public void setUp() throws Exception {
+        mContext = InstrumentationRegistry.getTargetContext();
+    }
+
+    @CallSuper
+    public void cleanUp() throws Exception {
+        for (int i = 0; i < mControllers.size(); i++) {
+            mControllers.get(i).release();
+        }
+    }
+
+    MediaController2Wrapper createController(SessionToken token) throws InterruptedException {
+        return createController(token, true, null);
+    }
+
+    MediaController2Wrapper createController(@NonNull SessionToken token, boolean waitForConnect,
+            @Nullable TestControllerCallback callback)
+            throws InterruptedException {
+        if (callback == null) {
+            callback = new TestControllerCallback();
+        }
+        MediaController2Wrapper controller = new MediaController2Wrapper(mContext, token, callback);
+        mControllers.add(controller);
+        if (waitForConnect) {
+            controller.waitForConnect(true);
+        }
+        return controller;
+    }
+
+    public static class TestControllerCallback extends MediaController2.ControllerCallback {
+        public final CountDownLatch connectLatch = new CountDownLatch(1);
+        public final CountDownLatch disconnectLatch = new CountDownLatch(1);
+
+        @CallSuper
+        @Override
+        public void onConnected(long commands) {
+            super.onConnected(commands);
+            connectLatch.countDown();
+        }
+
+        @CallSuper
+        @Override
+        public void onDisconnected() {
+            super.onDisconnected();
+            disconnectLatch.countDown();
+        }
+    }
+
+    public class MediaController2Wrapper extends MediaController2 {
+        private final TestControllerCallback mCallback;
+
+        public MediaController2Wrapper(@NonNull Context context, @NonNull SessionToken token,
+                @NonNull TestControllerCallback callback) {
+            super(context, token, callback, sHandlerExecutor);
+            mCallback = callback;
+        }
+
+        public void waitForConnect(boolean expect) throws InterruptedException {
+            if (expect) {
+                assertTrue(mCallback.connectLatch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+            } else {
+                assertFalse(mCallback.connectLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+            }
+        }
+
+        public void waitForDisconnect(boolean expect) throws InterruptedException {
+            if (expect) {
+                assertTrue(mCallback.disconnectLatch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+            } else {
+                assertFalse(mCallback.disconnectLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+            }
+        }
+    }
+}
diff --git a/packages/MediaComponents/test/src/android/media/MediaSessionManager_MediaSession2.java b/packages/MediaComponents/test/src/android/media/MediaSessionManager_MediaSession2.java
new file mode 100644
index 0000000..552f0a6
--- /dev/null
+++ b/packages/MediaComponents/test/src/android/media/MediaSessionManager_MediaSession2.java
@@ -0,0 +1,208 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.media;
+
+import android.content.Context;
+import android.media.MediaSession2.ControllerInfo;
+import android.media.MediaSession2.SessionCallback;
+import android.media.session.MediaSessionManager;
+import android.media.session.PlaybackState;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+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.TimeUnit;
+
+import static android.media.TestUtils.createPlaybackState;
+import static org.junit.Assert.*;
+
+/**
+ * Tests {@link MediaSessionManager} with {@link MediaSession2} specific APIs.
+ */
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+@Ignore
+// TODO(jaewan): Reenable test when the media session service detects newly installed sesison
+//               service app.
+public class MediaSessionManager_MediaSession2 extends MediaSession2TestBase {
+    private static final String TAG = "MediaSessionManager_MediaSession2";
+
+    private MediaSessionManager mManager;
+    private MediaSession2 mSession;
+
+    @Before
+    @Override
+    public void setUp() throws Exception {
+        super.setUp();
+        mManager = (MediaSessionManager) mContext.getSystemService(Context.MEDIA_SESSION_SERVICE);
+
+        // 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();
+        });
+        ensureChangeInSession();
+    }
+
+    @After
+    @Override
+    public void cleanUp() throws Exception {
+        super.cleanUp();
+        sHandler.removeCallbacksAndMessages(null);
+        sHandler.postAndSync(() -> {
+            mSession.setPlayer(null);
+        });
+    }
+
+    // TODO(jaewan): Make this host-side test to see per-user behavior.
+    @Test
+    public void testGetMediaSession2Tokens_hasMediaController() throws InterruptedException {
+        final MockPlayer player = (MockPlayer) mSession.getPlayer();
+        player.notifyPlaybackState(createPlaybackState(PlaybackState.STATE_STOPPED));
+
+        MediaController2 controller = null;
+        List<SessionToken> tokens = mManager.getActiveSessionTokens();
+        assertNotNull(tokens);
+        for (int i = 0; i < tokens.size(); i++) {
+            SessionToken token = tokens.get(i);
+            if (mContext.getPackageName().equals(token.getPackageName())
+                    && TAG.equals(token.getId())) {
+                assertNotNull(token.getSessionBinder());
+                assertNull(controller);
+                controller = createController(token);
+            }
+        }
+        assertNotNull(controller);
+
+        // Test if the found controller is correct one.
+        assertEquals(PlaybackState.STATE_STOPPED, controller.getPlaybackState().getState());
+        controller.play();
+
+        assertTrue(player.mCountDownLatch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+        assertTrue(player.mPlayCalled);
+    }
+
+    /**
+     * Test if server recognizes session even if session refuses the connection from server.
+     *
+     * @throws InterruptedException
+     */
+    @Test
+    public void testGetSessionTokens_sessionRejected() throws InterruptedException {
+        sHandler.postAndSync(() -> {
+            mSession.setPlayer(null);
+            mSession = new MediaSession2.Builder(mContext, new MockPlayer(0)).setId(TAG)
+                    .setSessionCallback(new SessionCallback() {
+                        @Override
+                        public long onConnect(ControllerInfo controller) {
+                            // Reject all connection request.
+                            return 0;
+                        }
+                    }).build();
+        });
+        ensureChangeInSession();
+
+        boolean foundSession = false;
+        List<SessionToken> tokens = mManager.getActiveSessionTokens();
+        assertNotNull(tokens);
+        for (int i = 0; i < tokens.size(); i++) {
+            SessionToken token = tokens.get(i);
+            if (mContext.getPackageName().equals(token.getPackageName())
+                    && TAG.equals(token.getId())) {
+                assertFalse(foundSession);
+                foundSession = true;
+            }
+        }
+        assertTrue(foundSession);
+    }
+
+    @Test
+    public void testGetMediaSession2Tokens_playerRemoved() throws InterruptedException {
+        // Release
+        sHandler.postAndSync(() -> {
+            mSession.setPlayer(null);
+        });
+        ensureChangeInSession();
+
+        // When the mSession's player becomes null, it should lose binder connection between server.
+        // So server will forget the session.
+        List<SessionToken> tokens = mManager.getActiveSessionTokens();
+        for (int i = 0; i < tokens.size(); i++) {
+            SessionToken token = tokens.get(i);
+            assertFalse(mContext.getPackageName().equals(token.getPackageName())
+                    && TAG.equals(token.getId()));
+        }
+    }
+
+    @Test
+    public void testGetMediaSessionService2Token() throws InterruptedException {
+        boolean foundTestSessionService = false;
+        List<SessionToken> tokens = mManager.getSessionServiceTokens();
+        for (int i = 0; i < tokens.size(); i++) {
+            SessionToken token = tokens.get(i);
+            if (mContext.getPackageName().equals(token.getPackageName())
+                    && MockMediaSessionService2.ID.equals(token.getId())) {
+                assertFalse(foundTestSessionService);
+                assertEquals(SessionToken.TYPE_SESSION_SERVICE, token.getType());
+                assertNull(token.getSessionBinder());
+                foundTestSessionService = true;
+            }
+        }
+        assertTrue(foundTestSessionService);
+    }
+
+    @Test
+    public void testGetAllSessionTokens() throws InterruptedException {
+        boolean foundTestSession = false;
+        boolean foundTestSessionService = false;
+        List<SessionToken> tokens = mManager.getAllSessionTokens();
+        for (int i = 0; i < tokens.size(); i++) {
+            SessionToken token = tokens.get(i);
+            if (!mContext.getPackageName().equals(token.getPackageName())) {
+                continue;
+            }
+            switch (token.getId()) {
+                case TAG:
+                    assertFalse(foundTestSession);
+                    foundTestSession = true;
+                    break;
+                case MockMediaSessionService2.ID:
+                    assertFalse(foundTestSessionService);
+                    foundTestSessionService = true;
+                    assertEquals(SessionToken.TYPE_SESSION_SERVICE, token.getType());
+                    break;
+                default:
+                    fail("Unexpected session " + token + " exists in the package");
+            }
+        }
+        assertTrue(foundTestSession);
+        assertTrue(foundTestSessionService);
+    }
+
+    // Ensures if the session creation/release is notified to the server.
+    private void ensureChangeInSession() throws InterruptedException {
+        // TODO(jaewan): Wait by listener.
+        Thread.sleep(WAIT_TIME_MS);
+    }
+}
diff --git a/packages/MediaComponents/test/src/android/media/MockMediaSessionService2.java b/packages/MediaComponents/test/src/android/media/MockMediaSessionService2.java
new file mode 100644
index 0000000..0fa2f52
--- /dev/null
+++ b/packages/MediaComponents/test/src/android/media/MockMediaSessionService2.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.media;
+
+import static junit.framework.Assert.fail;
+
+import android.media.MediaSession2.ControllerInfo;
+import android.media.MediaSession2.SessionCallback;
+import android.media.TestUtils.SyncHandler;
+import android.os.Process;
+
+/**
+ * Mock implementation of {@link android.media.MediaSessionService2} for testing.
+ */
+public class MockMediaSessionService2 extends MediaSessionService2 {
+    // Keep in sync with the AndroidManifest.xml
+    public static final String ID = "TestSession";
+    public MediaSession2 mSession;
+
+    @Override
+    public MediaSession2 onCreateSession(String sessionId) {
+        final MockPlayer player = new MockPlayer(1);
+        SyncHandler handler = (SyncHandler) TestServiceRegistry.getInstance().getHandler();
+        try {
+            handler.postAndSync(() -> {
+                mSession = new MediaSession2.Builder(MockMediaSessionService2.this, player)
+                        .setId(sessionId).setSessionCallback(new MySessionCallback()).build();
+            });
+        } catch (InterruptedException e) {
+            fail(e.toString());
+        }
+        return mSession;
+    }
+
+    @Override
+    public void onCreate() {
+        super.onCreate();
+    }
+
+    @Override
+    public void onDestroy() {
+        TestServiceRegistry.getInstance().cleanUp();
+        super.onDestroy();
+    }
+
+    private class MySessionCallback extends SessionCallback {
+        @Override
+        public long onConnect(ControllerInfo controller) {
+            if (Process.myUid() != controller.getUid()) {
+                // It's system app wants to listen changes. Ignore.
+                return super.onConnect(controller);
+            }
+            TestServiceRegistry.getInstance().setServiceInstance(
+                    MockMediaSessionService2.this, controller);
+            return super.onConnect(controller);
+        }
+    }
+}
diff --git a/packages/MediaComponents/test/src/android/media/MockPlayer.java b/packages/MediaComponents/test/src/android/media/MockPlayer.java
new file mode 100644
index 0000000..b0d7a69
--- /dev/null
+++ b/packages/MediaComponents/test/src/android/media/MockPlayer.java
@@ -0,0 +1,112 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.media;
+
+import android.media.session.PlaybackState;
+import android.os.Handler;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+
+/**
+ * A mock implementation of {@link MediaPlayerBase} for testing.
+ */
+public class MockPlayer extends MediaPlayerBase {
+    public final CountDownLatch mCountDownLatch;
+
+    public boolean mPlayCalled;
+    public boolean mPauseCalled;
+    public boolean mStopCalled;
+    public boolean mSkipToPreviousCalled;
+    public boolean mSkipToNextCalled;
+    public List<PlaybackListenerHolder> mListeners = new ArrayList<>();
+    private PlaybackState mLastPlaybackState;
+
+    public MockPlayer(int count) {
+        mCountDownLatch = (count > 0) ? new CountDownLatch(count) : null;
+    }
+
+    @Override
+    public void play() {
+        mPlayCalled = true;
+        if (mCountDownLatch != null) {
+            mCountDownLatch.countDown();
+        }
+    }
+
+    @Override
+    public void pause() {
+        mPauseCalled = true;
+        if (mCountDownLatch != null) {
+            mCountDownLatch.countDown();
+        }
+    }
+
+    @Override
+    public void stop() {
+        mStopCalled = true;
+        if (mCountDownLatch != null) {
+            mCountDownLatch.countDown();
+        }
+    }
+
+    @Override
+    public void skipToPrevious() {
+        mSkipToPreviousCalled = true;
+        if (mCountDownLatch != null) {
+            mCountDownLatch.countDown();
+        }
+    }
+
+    @Override
+    public void skipToNext() {
+        mSkipToNextCalled = true;
+        if (mCountDownLatch != null) {
+            mCountDownLatch.countDown();
+        }
+    }
+
+    @Nullable
+    @Override
+    public PlaybackState getPlaybackState() {
+        return mLastPlaybackState;
+    }
+
+    @Override
+    public void addPlaybackListener(
+            @NonNull PlaybackListener listener, @NonNull Handler handler) {
+        mListeners.add(new PlaybackListenerHolder(listener, handler));
+    }
+
+    @Override
+    public void removePlaybackListener(@NonNull PlaybackListener listener) {
+        int index = PlaybackListenerHolder.indexOf(mListeners, listener);
+        if (index >= 0) {
+            mListeners.remove(index);
+        }
+    }
+
+    public void notifyPlaybackState(final PlaybackState state) {
+        mLastPlaybackState = state;
+        for (int i = 0; i < mListeners.size(); i++) {
+            mListeners.get(i).postPlaybackChange(state);
+        }
+    }
+}
diff --git a/packages/MediaComponents/test/src/android/media/PlaybackListenerHolder.java b/packages/MediaComponents/test/src/android/media/PlaybackListenerHolder.java
new file mode 100644
index 0000000..b0b87de
--- /dev/null
+++ b/packages/MediaComponents/test/src/android/media/PlaybackListenerHolder.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.media;
+
+import android.media.MediaPlayerBase.PlaybackListener;
+import android.media.session.PlaybackState;
+import android.os.Handler;
+import android.os.Message;
+import android.support.annotation.NonNull;
+
+import java.util.List;
+
+/**
+ * Holds {@link PlaybackListener} with the {@link Handler}.
+ */
+public class PlaybackListenerHolder extends Handler {
+    private static final int ON_PLAYBACK_CHANGED = 1;
+
+    public final PlaybackListener listener;
+
+    public PlaybackListenerHolder(
+            @NonNull PlaybackListener listener, @NonNull Handler handler) {
+        super(handler.getLooper());
+        this.listener = listener;
+    }
+
+    @Override
+    public void handleMessage(Message msg) {
+        switch (msg.what) {
+            case ON_PLAYBACK_CHANGED:
+                listener.onPlaybackChanged((PlaybackState) msg.obj);
+                break;
+        }
+    }
+
+    public void postPlaybackChange(PlaybackState state) {
+        obtainMessage(ON_PLAYBACK_CHANGED, state).sendToTarget();
+    }
+
+    /**
+     * Returns {@code true} if the given list contains a {@link PlaybackListenerHolder} that holds
+     * the given listener.
+     *
+     * @param list list to check
+     * @param listener listener to check
+     * @return {@code true} if the given list contains listener. {@code false} otherwise.
+     */
+    public static <Holder extends PlaybackListenerHolder> boolean contains(
+            @NonNull List<Holder> list, PlaybackListener listener) {
+        return indexOf(list, listener) >= 0;
+    }
+
+    /**
+     * Returns the index of the {@link PlaybackListenerHolder} that contains the given listener.
+     *
+     * @param list list to check
+     * @param listener listener to check
+     * @return {@code index} of item if the given list contains listener. {@code -1} otherwise.
+     */
+    public static <Holder extends PlaybackListenerHolder> int indexOf(
+            @NonNull List<Holder> list, PlaybackListener listener) {
+        for (int i = 0; i < list.size(); i++) {
+            if (list.get(i).listener == listener) {
+                return i;
+            }
+        }
+        return -1;
+    }
+}
diff --git a/packages/MediaComponents/test/src/android/media/TestServiceRegistry.java b/packages/MediaComponents/test/src/android/media/TestServiceRegistry.java
new file mode 100644
index 0000000..378a6c4
--- /dev/null
+++ b/packages/MediaComponents/test/src/android/media/TestServiceRegistry.java
@@ -0,0 +1,135 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.media;
+
+import static org.junit.Assert.fail;
+
+import android.media.MediaSession2.ControllerInfo;
+import android.media.TestUtils.SyncHandler;
+import android.os.Handler;
+import android.os.Looper;
+import android.support.annotation.GuardedBy;
+
+/**
+ * Keeps the instance of currently running {@link MockMediaSessionService2}. And also provides
+ * a way to control them in one place.
+ * <p>
+ * It only support only one service at a time.
+ */
+public class TestServiceRegistry {
+    public interface ServiceInstanceChangedCallback {
+        void OnServiceInstanceChanged(MediaSessionService2 service);
+    }
+
+    @GuardedBy("TestServiceRegistry.class")
+    private static TestServiceRegistry sInstance;
+    @GuardedBy("TestServiceRegistry.class")
+    private MediaSessionService2 mService;
+    @GuardedBy("TestServiceRegistry.class")
+    private SyncHandler mHandler;
+    @GuardedBy("TestServiceRegistry.class")
+    private ControllerInfo mOnConnectControllerInfo;
+    @GuardedBy("TestServiceRegistry.class")
+    private ServiceInstanceChangedCallback mCallback;
+
+    public static TestServiceRegistry getInstance() {
+        synchronized (TestServiceRegistry.class) {
+            if (sInstance == null) {
+                sInstance = new TestServiceRegistry();
+            }
+            return sInstance;
+        }
+    }
+
+    public void setHandler(Handler handler) {
+        synchronized (TestServiceRegistry.class) {
+            mHandler = new SyncHandler(handler.getLooper());
+        }
+    }
+
+    public void setServiceInstanceChangedCallback(ServiceInstanceChangedCallback callback) {
+        synchronized (TestServiceRegistry.class) {
+            mCallback = callback;
+        }
+    }
+
+    public Handler getHandler() {
+        synchronized (TestServiceRegistry.class) {
+            return mHandler;
+        }
+    }
+
+    public void setServiceInstance(MediaSessionService2 service, ControllerInfo controller) {
+        synchronized (TestServiceRegistry.class) {
+            if (mService != null) {
+                fail("Previous service instance is still running. Clean up manually to ensure"
+                        + " previoulsy running service doesn't break current test");
+            }
+            mService = service;
+            mOnConnectControllerInfo = controller;
+            if (mCallback != null) {
+                mCallback.OnServiceInstanceChanged(service);
+            }
+        }
+    }
+
+    public MediaSessionService2 getServiceInstance() {
+        synchronized (TestServiceRegistry.class) {
+            return mService;
+        }
+    }
+
+    public ControllerInfo getOnConnectControllerInfo() {
+        synchronized (TestServiceRegistry.class) {
+            return mOnConnectControllerInfo;
+        }
+    }
+
+
+    public void cleanUp() {
+        synchronized (TestServiceRegistry.class) {
+            final ServiceInstanceChangedCallback callback = mCallback;
+            if (mService != null) {
+                try {
+                    if (mHandler.getLooper() == Looper.myLooper()) {
+                        mService.getSession().setPlayer(null);
+                    } else {
+                        mHandler.postAndSync(() -> {
+                            mService.getSession().setPlayer(null);
+                        });
+                    }
+                } catch (InterruptedException e) {
+                    // No-op. Service containing session will die, but shouldn't be a huge issue.
+                }
+                // stopSelf() would not kill service while the binder connection established by
+                // bindService() exists, and setPlayer(null) above will do the job instead.
+                // So stopSelf() isn't really needed, but just for sure.
+                mService.stopSelf();
+                mService = null;
+            }
+            if (mHandler != null) {
+                mHandler.removeCallbacksAndMessages(null);
+            }
+            mCallback = null;
+            mOnConnectControllerInfo = null;
+
+            if (callback != null) {
+                callback.OnServiceInstanceChanged(null);
+            }
+        }
+    }
+}
diff --git a/packages/MediaComponents/test/src/android/media/TestUtils.java b/packages/MediaComponents/test/src/android/media/TestUtils.java
new file mode 100644
index 0000000..0cca12c
--- /dev/null
+++ b/packages/MediaComponents/test/src/android/media/TestUtils.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.media;
+
+import android.content.Context;
+import android.media.session.MediaSessionManager;
+import android.media.session.PlaybackState;
+import android.os.Handler;
+
+import android.os.Looper;
+
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+/**
+ * Utilities for tests.
+ */
+public final class TestUtils {
+    private static final int WAIT_TIME_MS = 1000;
+    private static final int WAIT_SERVICE_TIME_MS = 5000;
+
+    /**
+     * Creates a {@link android.media.session.PlaybackState} with the given state.
+     *
+     * @param state one of the PlaybackState.STATE_xxx.
+     * @return a PlaybackState
+     */
+    public static PlaybackState createPlaybackState(int state) {
+        return new PlaybackState.Builder().setState(state, 0, 1.0f).build();
+    }
+
+    public static SessionToken getServiceToken(Context context, String id) {
+        MediaSessionManager manager =
+                (MediaSessionManager) context.getSystemService(Context.MEDIA_SESSION_SERVICE);
+        List<SessionToken> tokens = manager.getSessionServiceTokens();
+        for (int i = 0; i < tokens.size(); i++) {
+            SessionToken token = tokens.get(i);
+            if (context.getPackageName().equals(token.getPackageName())
+                    && id.equals(token.getId())) {
+                return token;
+            }
+        }
+        fail("Failed to find service");
+        return null;
+    }
+
+    /**
+     * Handler that always waits until the Runnable finishes.
+     */
+    public static class SyncHandler extends Handler {
+        public SyncHandler(Looper looper) {
+            super(looper);
+        }
+
+        public void postAndSync(Runnable runnable) throws InterruptedException {
+            final CountDownLatch latch = new CountDownLatch(1);
+            if (getLooper() == Looper.myLooper()) {
+                runnable.run();
+            } else {
+                post(()->{
+                    runnable.run();
+                    latch.countDown();
+                });
+                assertTrue(latch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+            }
+        }
+    }
+}