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));
+ }
+ }
+ }
+}