[go: nahoru, domu]

blob: e4a873460f1b531fd15cd86dd85e4434cdb969c4 [file] [log] [blame]
/*
* 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 androidx.media2;
import static androidx.media2.SessionResult.RESULT_SUCCESS;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.media.AudioManager;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Process;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.media.AudioAttributesCompat;
import androidx.media.VolumeProviderCompat;
import androidx.media2.MediaController.ControllerCallback;
import androidx.media2.MediaController.PlaybackInfo;
import androidx.media2.MediaLibraryService.MediaLibrarySession.MediaLibrarySessionCallback;
import androidx.media2.MediaSession.ControllerInfo;
import androidx.media2.MediaSession.SessionCallback;
import androidx.media2.TestServiceRegistry.SessionServiceCallback;
import androidx.media2.TestUtils.SyncHandler;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.FlakyTest;
import androidx.test.filters.LargeTest;
import androidx.test.filters.SdkSuppress;
import androidx.testutils.PollingCheck;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.lang.reflect.Method;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
/**
* Tests {@link MediaController}.
*/
// TODO(jaewan): Implement host-side test so controller and session can run in different processes.
// TODO(jaewan): Fix flaky failure -- see MediaControllerImpl.getController()
// TODO(jaeawn): Revisit create/close session in the sHandler. It's no longer necessary.
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.JELLY_BEAN)
@RunWith(AndroidJUnit4.class)
@LargeTest
@FlakyTest
public class MediaControllerTest extends MediaSessionTestBase {
private static final String TAG = "MediaControllerTest";
PendingIntent mIntent;
MediaSession mSession;
MediaController mController;
MockPlayer mPlayer;
AudioManager mAudioManager;
@Before
@Override
public void setUp() throws Exception {
super.setUp();
final Intent sessionActivity = new Intent(mContext, MockActivity.class);
// Create this test specific MediaSession to use our own Handler.
mIntent = PendingIntent.getActivity(mContext, 0, sessionActivity, 0);
mPlayer = new MockPlayer(1);
mSession = new MediaSession.Builder(mContext, mPlayer)
.setSessionCallback(sHandlerExecutor, new SessionCallback() {
@Override
public SessionCommandGroup onConnect(MediaSession session,
ControllerInfo controller) {
if (Process.myUid() == controller.getUid()) {
return super.onConnect(session, controller);
}
return null;
}
@Override
public MediaItem onCreateMediaItem(MediaSession session,
ControllerInfo controller, String mediaId) {
return TestUtils.createMediaItem(mediaId);
}
})
.setSessionActivity(mIntent)
.setId(TAG).build();
mController = createController(mSession.getToken());
mAudioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
TestServiceRegistry.getInstance().setHandler(sHandler);
}
@After
@Override
public void cleanUp() throws Exception {
super.cleanUp();
if (mSession != null) {
mSession.close();
mSession = null;
}
if (mController != null) {
mController.close();
mController = null;
}
TestServiceRegistry.getInstance().cleanUp();
}
/**
* Test if the {@link MockControllerCallback} wraps the callback proxy
* without missing any method.
*/
@Test
public void testTestControllerCallback() {
prepareLooper();
Method[] methods = MockControllerCallback.class.getMethods();
assertNotNull(methods);
for (int i = 0; i < methods.length; i++) {
// For any methods in the controller callback, TestControllerCallback should have
// overriden the method and call matching API in the callback proxy.
assertNotEquals("TestControllerCallback should override " + methods[i]
+ " and call callback proxy",
ControllerCallback.class, methods[i].getDeclaringClass());
}
}
@Test
public void testPlay() {
prepareLooper();
mController.play();
try {
assertTrue(mPlayer.mCountDownLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
} catch (InterruptedException e) {
fail(e.getMessage());
}
assertTrue(mPlayer.mPlayCalled);
}
@Test
public void testPlay_autoPrepare() throws Exception {
prepareLooper();
final MockPlayer player = new MockPlayer(2);
player.mLastPlayerState = SessionPlayer.PLAYER_STATE_IDLE;
mSession.updatePlayer(player);
mController.play();
assertTrue(player.mCountDownLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
assertTrue(player.mPlayCalled);
assertTrue(player.mPrepareCalled);
}
@Test
public void testPause() {
prepareLooper();
mController.pause();
try {
assertTrue(mPlayer.mCountDownLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
} catch (InterruptedException e) {
fail(e.getMessage());
}
assertTrue(mPlayer.mPauseCalled);
}
@Test
public void testPrepare() {
prepareLooper();
mController.prepare();
try {
assertTrue(mPlayer.mCountDownLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
} catch (InterruptedException e) {
fail(e.getMessage());
}
assertTrue(mPlayer.mPrepareCalled);
}
@Test
public void testSeekTo() {
prepareLooper();
final long seekPosition = 12125L;
mController.seekTo(seekPosition);
try {
assertTrue(mPlayer.mCountDownLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
} catch (InterruptedException e) {
fail(e.getMessage());
}
assertTrue(mPlayer.mSeekToCalled);
assertEquals(seekPosition, mPlayer.mSeekPosition);
}
@Test
public void testGettersAfterConnected() throws InterruptedException {
prepareLooper();
final int state = SessionPlayer.PLAYER_STATE_PLAYING;
final int bufferingState = SessionPlayer.BUFFERING_STATE_COMPLETE;
final long position = 150000;
final long bufferedPosition = 900000;
final float speed = 0.5f;
final long timeDiff = 102;
final MediaItem currentMediaItem = TestUtils.createMediaItemWithMetadata();
mPlayer.mLastPlayerState = state;
mPlayer.mLastBufferingState = bufferingState;
mPlayer.mCurrentPosition = position;
mPlayer.mBufferedPosition = bufferedPosition;
mPlayer.mPlaybackSpeed = speed;
mPlayer.mCurrentMediaItem = currentMediaItem;
MediaController controller = createController(mSession.getToken());
controller.setTimeDiff(timeDiff);
assertEquals(state, controller.getPlayerState());
assertEquals(bufferedPosition, controller.getBufferedPosition());
assertEquals(speed, controller.getPlaybackSpeed(), 0.0f);
assertEquals(position + (long) (speed * timeDiff), controller.getCurrentPosition());
assertEquals(currentMediaItem, controller.getCurrentMediaItem());
}
@Test
public void testUpdatePlayer() throws InterruptedException {
prepareLooper();
final int testState = SessionPlayer.PLAYER_STATE_PLAYING;
final List<MediaItem> testPlaylist = TestUtils.createMediaItems(3);
final AudioAttributesCompat testAudioAttributes = new AudioAttributesCompat.Builder()
.setLegacyStreamType(AudioManager.STREAM_RING).build();
final CountDownLatch latch = new CountDownLatch(3);
mController = createController(mSession.getToken(), true, new ControllerCallback() {
@Override
public void onPlayerStateChanged(MediaController controller, int state) {
assertEquals(mController, controller);
assertEquals(testState, state);
latch.countDown();
}
@Override
public void onPlaylistChanged(MediaController controller, List<MediaItem> list,
MediaMetadata metadata) {
assertEquals(mController, controller);
assertEquals(testPlaylist, list);
assertNull(metadata);
latch.countDown();
}
@Override
public void onPlaybackInfoChanged(MediaController controller, PlaybackInfo info) {
assertEquals(mController, controller);
assertEquals(testAudioAttributes, info.getAudioAttributes());
latch.countDown();
}
});
MockPlayer player = new MockPlayer(0);
player.mLastPlayerState = testState;
player.setAudioAttributes(testAudioAttributes);
player.mPlaylist = testPlaylist;
mSession.updatePlayer(player);
assertTrue(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
}
@Test
public void testGetSessionActivity() {
prepareLooper();
PendingIntent sessionActivity = mController.getSessionActivity();
assertNotNull(sessionActivity);
if (Build.VERSION.SDK_INT >= 17) {
// PendingIntent#getCreatorPackage() is added in API 17.
assertEquals(mContext.getPackageName(), sessionActivity.getCreatorPackage());
assertEquals(Process.myUid(), sessionActivity.getCreatorUid());
}
}
@Test
public void testSetPlaylist() throws InterruptedException {
prepareLooper();
final List<String> list = TestUtils.createMediaIds(2);
mController.setPlaylist(list, null /* Metadata */);
assertTrue(mPlayer.mCountDownLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
assertTrue(mPlayer.mSetPlaylistCalled);
assertNull(mPlayer.mMetadata);
assertNotNull(mPlayer.mPlaylist);
assertEquals(list.size(), mPlayer.mPlaylist.size());
for (int i = 0; i < list.size(); i++) {
// MediaController.setPlaylist does not ensure the equality of the items.
assertEquals(list.get(i), mPlayer.mPlaylist.get(i).getMediaId());
}
}
@Test
public void testSetMediaItem() throws InterruptedException {
prepareLooper();
final MediaItem item = TestUtils.createMediaItemWithMetadata();
mController.setMediaItem(item.getMetadata()
.getString(MediaMetadata.METADATA_KEY_MEDIA_ID));
assertTrue(mPlayer.mCountDownLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
assertNull(mPlayer.mMetadata);
assertEquals(item.getMediaId(), mPlayer.mItem.getMediaId());
}
/**
* This also tests {@link ControllerCallback#onPlaylistChanged(
* MediaController, List, MediaMetadata)}.
*/
@Test
public void testGetPlaylist() throws InterruptedException {
prepareLooper();
final List<MediaItem> testList = TestUtils.createMediaItems(2);
final MediaMetadata testMetadata = TestUtils.createMetadata();
final AtomicReference<List<MediaItem>> listFromCallback = new AtomicReference<>();
final CountDownLatch latch = new CountDownLatch(1);
final ControllerCallback callback = new ControllerCallback() {
@Override
public void onPlaylistChanged(MediaController controller,
List<MediaItem> playlist, MediaMetadata metadata) {
assertNotNull(playlist);
TestUtils.assertMediaItemListEquals(testList, playlist);
TestUtils.assertMetadataEquals(testMetadata, metadata);
listFromCallback.set(playlist);
latch.countDown();
}
};
MediaController controller = createController(mSession.getToken(), true, callback);
mPlayer.mPlaylist = testList;
mPlayer.mMetadata = testMetadata;
mPlayer.notifyPlaylistChanged();
assertTrue(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
// Ensures object equality
assertEquals(listFromCallback.get(), controller.getPlaylist());
TestUtils.assertMetadataEquals(testMetadata, controller.getPlaylistMetadata());
}
/**
* This also tests {@link ControllerCallback#onPlaylistChanged(
* MediaController, List, MediaMetadata)}.
*/
@Test
@LargeTest
public void testGetPlaylist_withLongPlaylist() throws InterruptedException {
prepareLooper();
final List<MediaItem> testList = TestUtils.createMediaItems(5000);
final MediaMetadata testMetadata = TestUtils.createMetadata();
final AtomicReference<List<MediaItem>> listFromCallback = new AtomicReference<>();
final CountDownLatch latch = new CountDownLatch(1);
final ControllerCallback callback = new ControllerCallback() {
@Override
public void onPlaylistChanged(MediaController controller,
List<MediaItem> playlist, MediaMetadata metadata) {
assertNotNull(playlist);
TestUtils.assertMediaItemListEquals(testList, playlist);
TestUtils.assertMetadataEquals(testMetadata, metadata);
listFromCallback.set(playlist);
latch.countDown();
}
};
MediaController controller = createController(mSession.getToken(), true, callback);
mPlayer.mPlaylist = testList;
mPlayer.mMetadata = testMetadata;
mPlayer.notifyPlaylistChanged();
assertTrue(latch.await(3, TimeUnit.SECONDS));
// Ensures object equality
assertEquals(listFromCallback.get(), controller.getPlaylist());
TestUtils.assertMetadataEquals(testMetadata, controller.getPlaylistMetadata());
}
@Test
public void testUpdatePlaylistMetadata() throws InterruptedException {
prepareLooper();
final MediaMetadata testMetadata = TestUtils.createMetadata();
mController.updatePlaylistMetadata(testMetadata);
assertTrue(mPlayer.mCountDownLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
assertTrue(mPlayer.mUpdatePlaylistMetadataCalled);
assertNotNull(mPlayer.mMetadata);
assertEquals(testMetadata.getMediaId(), mPlayer.mMetadata.getMediaId());
}
@Test
public void testGetPlaylistMetadata() throws InterruptedException {
prepareLooper();
final MediaMetadata testMetadata = TestUtils.createMetadata();
final AtomicReference<MediaMetadata> metadataFromCallback = new AtomicReference<>();
final CountDownLatch latch = new CountDownLatch(1);
final ControllerCallback callback = new ControllerCallback() {
@Override
public void onPlaylistMetadataChanged(MediaController controller,
MediaMetadata metadata) {
assertNotNull(testMetadata);
assertEquals(testMetadata.getMediaId(), metadata.getMediaId());
metadataFromCallback.set(metadata);
latch.countDown();
}
};
mPlayer.mMetadata = testMetadata;
try (MediaSession session = new MediaSession.Builder(mContext, mPlayer)
.setId("testGetPlaylistMetadata")
.setSessionCallback(sHandlerExecutor, new SessionCallback() {})
.build()) {
MediaController controller = createController(session.getToken(), true, callback);
mPlayer.notifyPlaylistMetadataChanged();
assertTrue(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
assertEquals(metadataFromCallback.get().getMediaId(),
controller.getPlaylistMetadata().getMediaId());
}
}
@Test
public void testSetPlaybackSpeed() throws Exception {
prepareLooper();
final float speed = 1.5f;
mController.setPlaybackSpeed(speed);
assertTrue(mPlayer.mCountDownLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
assertEquals(speed, mPlayer.mPlaybackSpeed, 0.0f);
}
/**
* This also tests {@link MediaController.ControllerCallback#onPlaybackSpeedChanged(
* MediaController, float)}.
*
* @throws InterruptedException
*/
@Test
public void testGetPlaybackSpeed() throws InterruptedException {
prepareLooper();
final float speed = 1.5f;
mPlayer.setPlaybackSpeed(speed);
final CountDownLatch latch = new CountDownLatch(1);
final MediaController controller =
createController(mSession.getToken(), true, new ControllerCallback() {
@Override
public void onPlaybackSpeedChanged(MediaController controller,
float speedOut) {
assertEquals(speed, speedOut, 0.0f);
latch.countDown();
}
});
mPlayer.notifyPlaybackSpeedChanged(speed);
assertTrue(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
assertEquals(speed, controller.getPlaybackSpeed(), 0.0f);
}
/**
* Test whether {@link SessionPlayer#setPlaylist(List, MediaMetadata)} is notified
* through the
* {@link ControllerCallback#onPlaylistMetadataChanged(MediaController, MediaMetadata)}
* if the controller doesn't have {@link SessionCommand#COMMAND_CODE_PLAYER_GET_PLAYLIST} but
* {@link SessionCommand#COMMAND_CODE_PLAYER_GET_PLAYLIST_METADATA}.
*/
@Test
public void testControllerCallback_onPlaylistMetadataChanged() throws InterruptedException {
prepareLooper();
final MediaItem item = TestUtils.createMediaItemWithMetadata();
final List<MediaItem> list = TestUtils.createMediaItems(2);
final CountDownLatch latch = new CountDownLatch(1);
final ControllerCallback callback = new ControllerCallback() {
@Override
public void onPlaylistMetadataChanged(MediaController controller,
MediaMetadata metadata) {
assertNotNull(metadata);
assertEquals(item.getMediaId(), metadata.getMediaId());
latch.countDown();
}
};
final SessionCallback sessionCallback = new SessionCallback() {
@Override
public SessionCommandGroup onConnect(MediaSession session,
ControllerInfo controller) {
if (Process.myUid() == controller.getUid()) {
SessionCommandGroup commands = new SessionCommandGroup.Builder()
.addCommand(new SessionCommand(
SessionCommand.COMMAND_CODE_PLAYER_GET_PLAYLIST_METADATA))
.build();
return commands;
}
return super.onConnect(session, controller);
}
};
mPlayer.mMetadata = item.getMetadata();
mPlayer.mPlaylist = list;
try (MediaSession session = new MediaSession.Builder(mContext, mPlayer)
.setId("testControllerCallback_onPlaylistMetadataChanged")
.setSessionCallback(sHandlerExecutor, sessionCallback)
.build()) {
MediaController controller = createController(session.getToken(), true, callback);
mPlayer.notifyPlaylistChanged();
assertTrue(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
}
}
@Test
public void testControllerCallback_onSeekCompleted() throws InterruptedException {
prepareLooper();
final long testSeekPosition = 400;
final long testPosition = 500;
final CountDownLatch latch = new CountDownLatch(1);
final ControllerCallback callback = new ControllerCallback() {
@Override
public void onSeekCompleted(MediaController controller, long position) {
controller.setTimeDiff(Long.valueOf(0));
assertEquals(testSeekPosition, position);
assertEquals(testPosition, controller.getCurrentPosition());
latch.countDown();
}
};
final MediaController controller = createController(mSession.getToken(), true, callback);
mPlayer.mCurrentPosition = testPosition;
mPlayer.mLastPlayerState = SessionPlayer.PLAYER_STATE_PAUSED;
mPlayer.notifySeekCompleted(testSeekPosition);
assertTrue(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
}
/**
* This also tests {@link MediaController#getBufferedPosition()}, and
* {@link MediaController#getBufferingState()}.
*
* @throws InterruptedException
*/
@Test
public void testControllerCallback_onBufferingStateChanged() throws InterruptedException {
prepareLooper();
final List<MediaItem> testPlaylist = TestUtils.createMediaItems(3);
final MediaItem testItem = testPlaylist.get(0);
final int testBufferingState = SessionPlayer.BUFFERING_STATE_BUFFERING_AND_PLAYABLE;
final long testBufferingPosition = 500;
final CountDownLatch latch = new CountDownLatch(1);
final ControllerCallback callback = new ControllerCallback() {
@Override
public void onBufferingStateChanged(MediaController controller, MediaItem item,
int state) {
controller.setTimeDiff(Long.valueOf(0));
assertEquals(testItem, item);
assertEquals(testBufferingState, state);
assertEquals(testBufferingState, controller.getBufferingState());
assertEquals(testBufferingPosition, controller.getBufferedPosition());
latch.countDown();
}
};
final MediaController controller = createController(mSession.getToken(), true, callback);
mSession.getPlayer().setPlaylist(testPlaylist, null);
mPlayer.mBufferedPosition = testBufferingPosition;
mPlayer.notifyBufferingStateChanged(testItem, testBufferingState);
assertTrue(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
}
/**
* This also tests {@link MediaController#getPlayerState()}.
*
* @throws InterruptedException
*/
@Test
public void testControllerCallback_onPlayerStateChanged() throws InterruptedException {
prepareLooper();
final int testPlayerState = SessionPlayer.PLAYER_STATE_PLAYING;
final long testPosition = 500;
final CountDownLatch latch = new CountDownLatch(1);
final ControllerCallback callback = new ControllerCallback() {
@Override
public void onPlayerStateChanged(MediaController controller, int state) {
controller.setTimeDiff(Long.valueOf(0));
assertEquals(testPlayerState, state);
assertEquals(testPlayerState, controller.getPlayerState());
assertEquals(testPosition, controller.getCurrentPosition());
latch.countDown();
}
};
final MediaController controller = createController(mSession.getToken(), true, callback);
mPlayer.mCurrentPosition = testPosition;
mPlayer.notifyPlayerStateChanged(testPlayerState);
assertTrue(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
}
/**
* This also tests {@link MediaController#getCurrentMediaItem()}.
*
* @throws InterruptedException
*/
@Test
public void testControllerCallback_onCurrentMediaItemChanged() throws InterruptedException {
prepareLooper();
final int listSize = 5;
final List<MediaItem> list = TestUtils.createMediaItems(listSize);
mPlayer.setPlaylist(list, null);
final int index = 3;
final MediaItem currentItem = list.get(index);
final MediaItem unknownItem = TestUtils.createMediaItemWithMetadata();
final CountDownLatch latch = new CountDownLatch(3);
final MediaController controller =
createController(mSession.getToken(), true, new ControllerCallback() {
@Override
public void onCurrentMediaItemChanged(MediaController controller,
MediaItem item) {
switch ((int) latch.getCount()) {
case 3:
assertEquals(-1, controller.getCurrentMediaItemIndex());
assertEquals(unknownItem, item);
break;
case 2:
assertEquals(index, controller.getCurrentMediaItemIndex());
assertEquals(currentItem, item);
break;
case 1:
assertEquals(-1, controller.getCurrentMediaItemIndex());
assertNull(item);
}
latch.countDown();
}
});
// Player notifies with the unknown item. It's still OK.
mPlayer.notifyCurrentMediaItemChanged(unknownItem);
assertFalse(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
// Known DSD should be notified through the onCurrentMediaItemChanged.
mPlayer.skipToPlaylistItem(index);
mPlayer.notifyCurrentMediaItemChanged(currentItem);
assertFalse(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
// Null DSD becomes null MediaItem.
mPlayer.setMediaItem(null);
mPlayer.notifyCurrentMediaItemChanged(null);
assertTrue(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
}
@Test
public void testAddPlaylistItem() throws InterruptedException {
prepareLooper();
final int testIndex = 12;
final String testId = "testAddPlaylistItem";
mController.addPlaylistItem(testIndex, testId);
assertTrue(mPlayer.mCountDownLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
assertTrue(mPlayer.mAddPlaylistItemCalled);
assertEquals(testIndex, mPlayer.mIndex);
assertEquals(testId, mPlayer.mItem.getMediaId());
}
@Test
public void testRemovePlaylistItem() throws InterruptedException {
prepareLooper();
mPlayer.mPlaylist = TestUtils.createMediaItems(2);
// Recreate controller for sending removePlaylistItem.
// It's easier to ensure that MediaController.getPlaylist() returns the playlist from the
// player.
MediaController controller = createController(mSession.getToken());
int targetIndex = 0;
controller.removePlaylistItem(targetIndex);
assertTrue(mPlayer.mCountDownLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
assertTrue(mPlayer.mRemovePlaylistItemCalled);
assertEquals(targetIndex, mPlayer.mIndex);
}
@Test
public void testReplacePlaylistItem() throws InterruptedException {
prepareLooper();
final int testIndex = 12;
final String testId = "testAddPlaylistItem";
mController.replacePlaylistItem(testIndex, testId);
assertTrue(mPlayer.mCountDownLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
assertTrue(mPlayer.mReplacePlaylistItemCalled);
// MediaController.replacePlaylistItem does not ensure the equality of the items.
assertEquals(testId, mPlayer.mItem.getMediaId());
}
@Test
public void testSkipToPreviousItem() throws InterruptedException {
prepareLooper();
mController.skipToPreviousPlaylistItem();
assertTrue(mPlayer.mCountDownLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
assertTrue(mPlayer.mSkipToPreviousItemCalled);
}
@Test
public void testSkipToNextItem() throws InterruptedException {
prepareLooper();
mController.skipToNextPlaylistItem();
assertTrue(mPlayer.mCountDownLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
assertTrue(mPlayer.mSkipToNextItemCalled);
}
@Test
public void testSkipToPlaylistItem() throws InterruptedException {
prepareLooper();
List<MediaItem> playlist = TestUtils.createMediaItems(2);
int targetIndex = 1;
mPlayer.mPlaylist = playlist;
MediaController controller = createController(mSession.getToken());
controller.skipToPlaylistItem(targetIndex);
assertTrue(mPlayer.mCountDownLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
assertTrue(mPlayer.mSkipToPlaylistItemCalled);
assertEquals(targetIndex, mPlayer.mIndex);
}
/**
* This also tests {@link ControllerCallback#onShuffleModeChanged(MediaController, int)}.
*/
@Test
public void testGetShuffleMode() throws InterruptedException {
prepareLooper();
final int testShuffleMode = SessionPlayer.SHUFFLE_MODE_GROUP;
mPlayer.mShuffleMode = testShuffleMode;
final CountDownLatch latch = new CountDownLatch(1);
final ControllerCallback callback = new ControllerCallback() {
@Override
public void onShuffleModeChanged(MediaController controller, int shuffleMode) {
assertEquals(testShuffleMode, shuffleMode);
latch.countDown();
}
};
MediaController controller = createController(mSession.getToken(), true, callback);
mPlayer.notifyShuffleModeChanged();
assertTrue(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
assertEquals(testShuffleMode, controller.getShuffleMode());
}
@Test
public void testSetShuffleMode() throws InterruptedException {
prepareLooper();
final int testShuffleMode = SessionPlayer.SHUFFLE_MODE_GROUP;
mController.setShuffleMode(testShuffleMode);
assertTrue(mPlayer.mCountDownLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
assertTrue(mPlayer.mSetShuffleModeCalled);
assertEquals(testShuffleMode, mPlayer.mShuffleMode);
}
/**
* This also tests {@link ControllerCallback#onRepeatModeChanged(MediaController, int)}.
*/
@Test
public void testGetRepeatMode() throws InterruptedException {
prepareLooper();
final int testRepeatMode = SessionPlayer.REPEAT_MODE_GROUP;
mPlayer.mRepeatMode = testRepeatMode;
final CountDownLatch latch = new CountDownLatch(1);
final ControllerCallback callback = new ControllerCallback() {
@Override
public void onRepeatModeChanged(MediaController controller, int repeatMode) {
assertEquals(testRepeatMode, repeatMode);
latch.countDown();
}
};
MediaController controller = createController(mSession.getToken(), true, callback);
mPlayer.notifyRepeatModeChanged();
assertTrue(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
assertEquals(testRepeatMode, controller.getRepeatMode());
}
@Test
public void testSetRepeatMode() throws InterruptedException {
prepareLooper();
final int testRepeatMode = SessionPlayer.REPEAT_MODE_GROUP;
mController.setRepeatMode(testRepeatMode);
assertTrue(mPlayer.mCountDownLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
assertTrue(mPlayer.mSetRepeatModeCalled);
assertEquals(testRepeatMode, mPlayer.mRepeatMode);
}
@Test
public void testUpdatedIndicesInRepeatMode() throws InterruptedException {
prepareLooper();
final int noneRepeatMode = SessionPlayer.REPEAT_MODE_NONE;
final int groupRepeatMode = SessionPlayer.REPEAT_MODE_GROUP;
final int currentIndex = -1;
final int targetIndex = 2;
final CountDownLatch latch = new CountDownLatch(2);
final ControllerCallback callback = new ControllerCallback() {
@Override
public void onRepeatModeChanged(MediaController controller, int repeatMode) {
switch ((int) latch.getCount()) {
case 2:
assertEquals(noneRepeatMode, repeatMode);
break;
case 1:
assertEquals(groupRepeatMode, repeatMode);
}
latch.countDown();
}
};
MediaController controller = createController(mSession.getToken(), true, callback);
mPlayer.mPrevMediaItemIndex = currentIndex;
mPlayer.mRepeatMode = noneRepeatMode;
// Need to call this in order to update previous media item index.
mPlayer.notifyRepeatModeChanged();
assertFalse(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
assertEquals(currentIndex, controller.getPreviousMediaItemIndex());
mPlayer.mPrevMediaItemIndex = targetIndex;
mPlayer.mRepeatMode = groupRepeatMode;
mPlayer.notifyRepeatModeChanged();
assertTrue(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
assertEquals(targetIndex, controller.getPreviousMediaItemIndex());
}
@Test
public void testUpdatedIndicesInShuffleMode() throws InterruptedException {
prepareLooper();
final int noneShuffleMode = SessionPlayer.SHUFFLE_MODE_NONE;
final int groupShuffleMode = SessionPlayer.SHUFFLE_MODE_GROUP;
final int currentIndex = -1;
final int targetIndex = 2;
final CountDownLatch latch = new CountDownLatch(2);
final ControllerCallback callback = new ControllerCallback() {
@Override
public void onShuffleModeChanged(@NonNull MediaController controller, int shuffleMode) {
switch ((int) latch.getCount()) {
case 2:
assertEquals(noneShuffleMode, shuffleMode);
break;
case 1:
assertEquals(groupShuffleMode, shuffleMode);
}
latch.countDown();
}
};
MediaController controller = createController(mSession.getToken(), true, callback);
mPlayer.mPrevMediaItemIndex = currentIndex;
mPlayer.mShuffleMode = noneShuffleMode;
mPlayer.notifyShuffleModeChanged();
assertFalse(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
assertEquals(currentIndex, controller.getPreviousMediaItemIndex());
mPlayer.mPrevMediaItemIndex = targetIndex;
mPlayer.mShuffleMode = groupShuffleMode;
mPlayer.notifyShuffleModeChanged();
assertTrue(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
assertEquals(targetIndex, controller.getPreviousMediaItemIndex());
}
@Test
public void testSetVolumeTo() throws Exception {
prepareLooper();
final int maxVolume = 100;
final int currentVolume = 23;
final int volumeControlType = VolumeProviderCompat.VOLUME_CONTROL_ABSOLUTE;
MockRemotePlayer remotePlayer =
new MockRemotePlayer(volumeControlType, maxVolume, currentVolume);
mSession.updatePlayer(remotePlayer);
final MediaController controller = createController(mSession.getToken(), true, null);
final int targetVolume = 50;
controller.setVolumeTo(targetVolume, 0 /* flags */);
assertTrue(remotePlayer.mLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
assertTrue(remotePlayer.mSetVolumeToCalled);
assertEquals(targetVolume, (int) remotePlayer.mCurrentVolume);
}
@Test
public void testAdjustVolume() throws Exception {
prepareLooper();
final int maxVolume = 100;
final int currentVolume = 23;
final int volumeControlType = VolumeProviderCompat.VOLUME_CONTROL_ABSOLUTE;
MockRemotePlayer remotePlayer =
new MockRemotePlayer(volumeControlType, maxVolume, currentVolume);
mSession.updatePlayer(remotePlayer);
final MediaController controller = createController(mSession.getToken(), true, null);
final int direction = AudioManager.ADJUST_RAISE;
controller.adjustVolume(direction, 0 /* flags */);
assertTrue(remotePlayer.mLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
assertTrue(remotePlayer.mAdjustVolumeCalled);
assertEquals(direction, remotePlayer.mDirection);
}
@Test
public void testSetVolumeWithLocalVolume() throws Exception {
prepareLooper();
if (Build.VERSION.SDK_INT >= 21 && mAudioManager.isVolumeFixed()) {
// This test is not eligible for this device.
return;
}
// Here, we intentionally choose STREAM_ALARM in order not to consider
// 'Do Not Disturb' or 'Volume limit'.
final int stream = AudioManager.STREAM_ALARM;
final int maxVolume = mAudioManager.getStreamMaxVolume(stream);
final int minVolume =
Build.VERSION.SDK_INT >= 28 ? mAudioManager.getStreamMinVolume(stream) : 0;
Log.d(TAG, "maxVolume=" + maxVolume + ", minVolume=" + minVolume);
if (maxVolume <= minVolume) {
return;
}
// Set stream of the session.
AudioAttributesCompat attrs = new AudioAttributesCompat.Builder()
.setLegacyStreamType(stream)
.build();
mPlayer.setAudioAttributes(attrs);
mSession.updatePlayer(mPlayer);
final int originalVolume = mAudioManager.getStreamVolume(stream);
final int targetVolume = originalVolume == minVolume
? originalVolume + 1 : originalVolume - 1;
Log.d(TAG, "originalVolume=" + originalVolume + ", targetVolume=" + targetVolume);
mController.setVolumeTo(targetVolume, AudioManager.FLAG_SHOW_UI);
new PollingCheck(TIMEOUT_MS) {
@Override
protected boolean check() {
return targetVolume == mAudioManager.getStreamVolume(stream);
}
}.run();
// Set back to original volume.
mAudioManager.setStreamVolume(stream, originalVolume, 0 /* flags */);
}
@Test
public void testAdjustVolumeWithLocalVolume() throws Exception {
prepareLooper();
if (Build.VERSION.SDK_INT >= 21 && mAudioManager.isVolumeFixed()) {
// This test is not eligible for this device.
return;
}
// Here, we intentionally choose STREAM_ALARM in order not to consider
// 'Do Not Disturb' or 'Volume limit'.
final int stream = AudioManager.STREAM_ALARM;
final int maxVolume = mAudioManager.getStreamMaxVolume(stream);
final int minVolume =
Build.VERSION.SDK_INT >= 28 ? mAudioManager.getStreamMinVolume(stream) : 0;
Log.d(TAG, "maxVolume=" + maxVolume + ", minVolume=" + minVolume);
if (maxVolume <= minVolume) {
return;
}
// Set stream of the session.
AudioAttributesCompat attrs = new AudioAttributesCompat.Builder()
.setLegacyStreamType(stream)
.build();
mPlayer.setAudioAttributes(attrs);
mSession.updatePlayer(mPlayer);
final int originalVolume = mAudioManager.getStreamVolume(stream);
final int direction = originalVolume == minVolume
? AudioManager.ADJUST_RAISE : AudioManager.ADJUST_LOWER;
final int targetVolume = originalVolume + direction;
Log.d(TAG, "originalVolume=" + originalVolume + ", targetVolume=" + targetVolume);
mController.adjustVolume(direction, AudioManager.FLAG_SHOW_UI);
new PollingCheck(TIMEOUT_MS) {
@Override
protected boolean check() {
return targetVolume == mAudioManager.getStreamVolume(stream);
}
}.run();
// Set back to original volume.
mAudioManager.setStreamVolume(stream, originalVolume, 0 /* flags */);
}
@Test
public void testGetPackageName() {
prepareLooper();
assertEquals(mContext.getPackageName(),
mController.getConnectedSessionToken().getPackageName());
}
@Test
public void testSendCustomCommand() throws InterruptedException {
prepareLooper();
// TODO(jaewan): Need to revisit with the permission.
final String command = "test_custom_command";
final Bundle testArgs = new Bundle();
testArgs.putString("args", "test_args");
final SessionCommand testCommand = new SessionCommand(command, null);
final CountDownLatch latch = new CountDownLatch(1);
final SessionCallback callback = new SessionCallback() {
@Override
public SessionCommandGroup onConnect(@NonNull MediaSession session,
@NonNull ControllerInfo controller) {
SessionCommandGroup commands =
new SessionCommandGroup.Builder(super.onConnect(session, controller))
.addCommand(testCommand)
.build();
return commands;
}
@Override
public SessionResult onCustomCommand(MediaSession session,
ControllerInfo controller, SessionCommand customCommand, Bundle args) {
assertEquals(mContext.getPackageName(), controller.getPackageName());
assertEquals(command, customCommand.getCustomCommand());
assertTrue(TestUtils.equals(testArgs, args));
latch.countDown();
return new SessionResult(RESULT_SUCCESS, null);
}
};
mSession.close();
mSession = new MediaSession.Builder(mContext, mPlayer)
.setSessionCallback(sHandlerExecutor, callback).setId(TAG).build();
final MediaController controller = createController(mSession.getToken());
controller.sendCustomCommand(testCommand, testArgs);
assertTrue(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
}
@Test
public void testControllerCallback_onConnected() throws InterruptedException {
prepareLooper();
// createController() uses controller callback to wait until the controller becomes
// available.
MediaController controller = createController(mSession.getToken());
assertNotNull(controller);
}
@Test
public void testControllerCallback_sessionRejects() throws InterruptedException {
prepareLooper();
final MediaSession.SessionCallback sessionCallback = new SessionCallback() {
@Override
public SessionCommandGroup onConnect(MediaSession session,
ControllerInfo controller) {
return null;
}
};
sHandler.postAndSync(new Runnable() {
@Override
public void run() {
mSession.close();
mSession = new MediaSession.Builder(mContext, mPlayer)
.setSessionCallback(sHandlerExecutor, sessionCallback).build();
}
});
MediaController controller =
createController(mSession.getToken(), false, null);
assertNotNull(controller);
waitForConnect(controller, false);
waitForDisconnect(controller, true);
}
@Test
public void testControllerCallback_releaseSession() throws InterruptedException {
prepareLooper();
mSession.close();
waitForDisconnect(mController, true);
}
@Test
public void testControllerCallback_close() throws InterruptedException {
prepareLooper();
mController.close();
waitForDisconnect(mController, true);
}
@Test
public void testFastForward() throws InterruptedException {
prepareLooper();
final CountDownLatch latch = new CountDownLatch(1);
final SessionCallback callback = new SessionCallback() {
@Override
public int onFastForward(MediaSession session, ControllerInfo controller) {
assertEquals(mContext.getPackageName(), controller.getPackageName());
latch.countDown();
return RESULT_SUCCESS;
}
};
try (MediaSession session = new MediaSession.Builder(mContext, mPlayer)
.setSessionCallback(sHandlerExecutor, callback)
.setId("testFastForward").build()) {
MediaController controller = createController(session.getToken());
controller.fastForward();
assertTrue(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
}
}
@Test
public void testRewind() throws InterruptedException {
prepareLooper();
final CountDownLatch latch = new CountDownLatch(1);
final SessionCallback callback = new SessionCallback() {
@Override
public int onRewind(MediaSession session, ControllerInfo controller) {
assertEquals(mContext.getPackageName(), controller.getPackageName());
latch.countDown();
return RESULT_SUCCESS;
}
};
try (MediaSession session = new MediaSession.Builder(mContext, mPlayer)
.setSessionCallback(sHandlerExecutor, callback)
.setId("testRewind").build()) {
MediaController controller = createController(session.getToken());
controller.rewind();
assertTrue(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
}
}
@Test
public void testPlayFromSearch() throws InterruptedException {
prepareLooper();
final String request = "random query";
final Bundle bundle = new Bundle();
bundle.putString("key", "value");
final CountDownLatch latch = new CountDownLatch(1);
final SessionCallback callback = new SessionCallback() {
@Override
public int onPlayFromSearch(MediaSession session, ControllerInfo controller,
String query, Bundle extras) {
super.onPlayFromSearch(session, controller, query, extras);
assertEquals(mContext.getPackageName(), controller.getPackageName());
assertEquals(request, query);
assertTrue(TestUtils.equals(bundle, extras));
latch.countDown();
return RESULT_SUCCESS;
}
};
try (MediaSession session = new MediaSession.Builder(mContext, mPlayer)
.setSessionCallback(sHandlerExecutor, callback)
.setId("testPlayFromSearch").build()) {
MediaController controller = createController(session.getToken());
controller.playFromSearch(request, bundle);
assertTrue(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
}
}
@Test
public void testPlayFromUri() throws InterruptedException {
prepareLooper();
final Uri request = Uri.parse("foo://boo");
final Bundle bundle = new Bundle();
bundle.putString("key", "value");
final CountDownLatch latch = new CountDownLatch(1);
final SessionCallback callback = new SessionCallback() {
@Override
public int onPlayFromUri(MediaSession session, ControllerInfo controller, Uri uri,
Bundle extras) {
assertEquals(mContext.getPackageName(), controller.getPackageName());
assertEquals(request, uri);
assertTrue(TestUtils.equals(bundle, extras));
latch.countDown();
return RESULT_SUCCESS;
}
};
try (MediaSession session = new MediaSession.Builder(mContext, mPlayer)
.setSessionCallback(sHandlerExecutor, callback)
.setId("testPlayFromUri").build()) {
MediaController controller = createController(session.getToken());
controller.playFromUri(request, bundle);
assertTrue(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
}
}
@Test
public void testPlayFromMediaId() throws InterruptedException {
prepareLooper();
final String request = "media_id";
final Bundle bundle = new Bundle();
bundle.putString("key", "value");
final CountDownLatch latch = new CountDownLatch(1);
final SessionCallback callback = new SessionCallback() {
@Override
public int onPlayFromMediaId(MediaSession session, ControllerInfo controller,
String mediaId, Bundle extras) {
assertEquals(mContext.getPackageName(), controller.getPackageName());
assertEquals(request, mediaId);
assertTrue(TestUtils.equals(bundle, extras));
latch.countDown();
return RESULT_SUCCESS;
}
};
try (MediaSession session = new MediaSession.Builder(mContext, mPlayer)
.setSessionCallback(sHandlerExecutor, callback)
.setId("testPlayFromMediaId").build()) {
MediaController controller = createController(session.getToken());
controller.playFromMediaId(request, bundle);
assertTrue(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
}
}
@Test
public void testPrepareFromSearch() throws InterruptedException {
prepareLooper();
final String request = "random query";
final Bundle bundle = new Bundle();
bundle.putString("key", "value");
final CountDownLatch latch = new CountDownLatch(1);
final SessionCallback callback = new SessionCallback() {
@Override
public int onPrepareFromSearch(MediaSession session, ControllerInfo controller,
String query, Bundle extras) {
assertEquals(mContext.getPackageName(), controller.getPackageName());
assertEquals(request, query);
assertTrue(TestUtils.equals(bundle, extras));
latch.countDown();
return RESULT_SUCCESS;
}
};
try (MediaSession session = new MediaSession.Builder(mContext, mPlayer)
.setSessionCallback(sHandlerExecutor, callback)
.setId("testPrepareFromSearch").build()) {
MediaController controller = createController(session.getToken());
controller.prepareFromSearch(request, bundle);
assertTrue(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
}
}
@Test
public void testPrepareFromUri() throws InterruptedException {
prepareLooper();
final Uri request = Uri.parse("foo://boo");
final Bundle bundle = new Bundle();
bundle.putString("key", "value");
final CountDownLatch latch = new CountDownLatch(1);
final SessionCallback callback = new SessionCallback() {
@Override
public int onPrepareFromUri(MediaSession session, ControllerInfo controller, Uri uri,
Bundle extras) {
assertEquals(mContext.getPackageName(), controller.getPackageName());
assertEquals(request, uri);
assertTrue(TestUtils.equals(bundle, extras));
latch.countDown();
return RESULT_SUCCESS;
}
};
try (MediaSession session = new MediaSession.Builder(mContext, mPlayer)
.setSessionCallback(sHandlerExecutor, callback)
.setId("testPrepareFromUri").build()) {
MediaController controller = createController(session.getToken());
controller.prepareFromUri(request, bundle);
assertTrue(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
}
}
@Test
public void testPrepareFromMediaId() throws InterruptedException {
prepareLooper();
final String request = "media_id";
final Bundle bundle = new Bundle();
bundle.putString("key", "value");
final CountDownLatch latch = new CountDownLatch(1);
final SessionCallback callback = new SessionCallback() {
@Override
public int onPrepareFromMediaId(MediaSession session, ControllerInfo controller,
String mediaId, Bundle extras) {
assertEquals(mContext.getPackageName(), controller.getPackageName());
assertEquals(request, mediaId);
assertTrue(TestUtils.equals(bundle, extras));
latch.countDown();
return RESULT_SUCCESS;
}
};
try (MediaSession session = new MediaSession.Builder(mContext, mPlayer)
.setSessionCallback(sHandlerExecutor, callback)
.setId("testPrepareFromMediaId").build()) {
MediaController controller = createController(session.getToken());
controller.prepareFromMediaId(request, bundle);
assertTrue(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
}
}
@Test
public void testSetRating() throws InterruptedException {
prepareLooper();
final float ratingValue = 3.5f;
final Rating rating = new StarRating(5, ratingValue);
final String mediaId = "media_id";
final CountDownLatch latch = new CountDownLatch(1);
final SessionCallback callback = new SessionCallback() {
@Override
public int onSetRating(MediaSession session, ControllerInfo controller,
String mediaIdOut, Rating ratingOut) {
assertEquals(mContext.getPackageName(), controller.getPackageName());
assertEquals(mediaId, mediaIdOut);
assertEquals(rating, ratingOut);
latch.countDown();
return RESULT_SUCCESS;
}
};
try (MediaSession session = new MediaSession.Builder(mContext, mPlayer)
.setSessionCallback(sHandlerExecutor, callback)
.setId("testSetRating").build()) {
MediaController controller = createController(session.getToken());
controller.setRating(mediaId, rating);
assertTrue(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
}
}
@Test
public void testIsConnected() throws InterruptedException {
prepareLooper();
assertTrue(mController.isConnected());
sHandler.postAndSync(new Runnable() {
@Override
public void run() {
mSession.close();
}
});
waitForDisconnect(mController, true);
assertFalse(mController.isConnected());
}
/**
* Test potential deadlock for calls between controller and session.
*/
@Test
public void testDeadlock() throws InterruptedException {
prepareLooper();
sHandler.postAndSync(new Runnable() {
@Override
public void run() {
mSession.close();
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(new Runnable() {
@Override
public void run() {
mSession = new MediaSession.Builder(mContext, mPlayer)
.setSessionCallback(sHandlerExecutor, new SessionCallback() {})
.setId("testDeadlock").build();
}
});
final MediaController controller = createController(mSession.getToken());
testHandler.post(new Runnable() {
@Override
public void run() {
final int state = SessionPlayer.PLAYER_STATE_ERROR;
for (int i = 0; i < 100; i++) {
// triggers call from session to controller.
player.notifyPlayerStateChanged(state);
// triggers call from controller to session.
controller.play();
// Repeat above
player.notifyPlayerStateChanged(state);
controller.pause();
player.notifyPlayerStateChanged(state);
controller.seekTo(0);
player.notifyPlayerStateChanged(state);
controller.skipToNextPlaylistItem();
player.notifyPlayerStateChanged(state);
controller.skipToPreviousPlaylistItem();
}
// This may hang if deadlock happens.
latch.countDown();
}
});
assertTrue(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
} finally {
if (mSession != null) {
sessionHandler.postAndSync(new Runnable() {
@Override
public void run() {
// Clean up here because sessionHandler will be removed afterwards.
mSession.close();
mSession = null;
}
});
}
if (Build.VERSION.SDK_INT >= 18) {
sessionThread.quitSafely();
testThread.quitSafely();
} else {
sessionThread.quit();
testThread.quit();
}
}
}
@Test
public void testGetServiceToken() {
prepareLooper();
SessionToken token = TestUtils.getServiceToken(mContext, MockMediaSessionService.ID);
assertNotNull(token);
assertEquals(mContext.getPackageName(), token.getPackageName());
assertEquals(SessionToken.TYPE_SESSION_SERVICE, token.getType());
}
@Test
public void testConnectToService_sessionService() throws InterruptedException {
prepareLooper();
testConnectToService(MockMediaSessionService.ID);
}
@Test
public void testConnectToService_libraryService() throws InterruptedException {
prepareLooper();
testConnectToService(MockMediaLibraryService.ID);
}
public void testConnectToService(String id) throws InterruptedException {
prepareLooper();
final CountDownLatch latch = new CountDownLatch(1);
final MediaLibrarySessionCallback sessionCallback = new MediaLibrarySessionCallback() {
@Override
public SessionCommandGroup onConnect(@NonNull MediaSession session,
@NonNull ControllerInfo controller) {
if (Process.myUid() == controller.getUid()) {
if (mSession != null) {
mSession.close();
}
mSession = session;
mPlayer = (MockPlayer) session.getPlayer();
assertEquals(mContext.getPackageName(), controller.getPackageName());
assertFalse(controller.isTrusted());
latch.countDown();
}
return super.onConnect(session, controller);
}
};
TestServiceRegistry.getInstance().setSessionCallback(sessionCallback);
final SessionCommand testCommand = new SessionCommand("testConnectToService", null);
final CountDownLatch controllerLatch = new CountDownLatch(1);
mController = createController(TestUtils.getServiceToken(mContext, id), true,
new ControllerCallback() {
@Override
public SessionResult onCustomCommand(
MediaController controller, SessionCommand command, Bundle args) {
if (testCommand.equals(command)) {
controllerLatch.countDown();
}
return new SessionResult(RESULT_SUCCESS);
}
}
);
assertTrue(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
// 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.
mSession.broadcastCustomCommand(testCommand, null);
assertTrue(controllerLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
}
@LargeTest
@Test
public void testControllerAfterSessionIsGone_session() throws InterruptedException {
prepareLooper();
testControllerAfterSessionIsClosed(TAG);
}
@LargeTest
@Test
public void testControllerAfterSessionIsClosed_sessionService() throws InterruptedException {
prepareLooper();
testConnectToService(MockMediaSessionService.ID);
testControllerAfterSessionIsClosed(MockMediaSessionService.ID);
}
@Test
public void testClose_beforeConnected() throws InterruptedException {
prepareLooper();
MediaController controller =
createController(mSession.getToken(), false, null);
controller.close();
}
@Test
public void testClose_twice() {
prepareLooper();
mController.close();
mController.close();
}
@LargeTest
@Test
public void testClose_session() throws InterruptedException {
prepareLooper();
mController.close();
// close is done immediately for session.
testNoInteraction();
// Test whether the controller is notified about later close of the session or
// re-creation.
testControllerAfterSessionIsClosed(TAG);
}
@LargeTest
@Test
public void testClose_sessionService() throws InterruptedException {
prepareLooper();
testCloseFromService(MockMediaSessionService.ID);
}
@LargeTest
@Test
public void testClose_libraryService() throws InterruptedException {
prepareLooper();
testCloseFromService(MockMediaLibraryService.ID);
}
@Test
public void testGetCurrentPosition() throws InterruptedException {
prepareLooper();
final int pausedState = SessionPlayer.PLAYER_STATE_PAUSED;
final int playingState = SessionPlayer.PLAYER_STATE_PLAYING;
final long timeDiff = 5000L;
final long position = 0L;
final CountDownLatch latch = new CountDownLatch(2);
final ControllerCallback callback = new ControllerCallback() {
@Override
public void onPlayerStateChanged(MediaController controller, int state) {
switch ((int) latch.getCount()) {
case 2:
assertEquals(state, pausedState);
assertEquals(position, controller.getCurrentPosition());
mPlayer.notifyPlayerStateChanged(playingState);
break;
case 1:
assertEquals(state, playingState);
assertEquals(position + timeDiff, controller.getCurrentPosition());
}
latch.countDown();
}
};
MediaController controller = createController(mSession.getToken(), true, callback);
controller.setTimeDiff(timeDiff);
mPlayer.notifyPlayerStateChanged(pausedState);
assertTrue(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
}
@Test
public void testSetMetadataForCurrentMediaItem() throws InterruptedException {
final CountDownLatch latch = new CountDownLatch(2);
final long duration = 1000L;
final MediaItem item = TestUtils.createMediaItemWithMetadata();
final ControllerCallback callback = new ControllerCallback() {
@Override
public void onCurrentMediaItemChanged(@NonNull MediaController controller,
@Nullable MediaItem item) {
MediaMetadata metadata = item.getMetadata();
if (metadata != null) {
switch ((int) latch.getCount()) {
case 2:
assertEquals(-1, controller.getCurrentMediaItemIndex());
assertFalse(metadata.containsKey(
MediaMetadata.METADATA_KEY_DURATION));
break;
case 1:
assertEquals(-1, controller.getCurrentMediaItemIndex());
assertTrue(metadata.containsKey(
MediaMetadata.METADATA_KEY_DURATION));
assertEquals(duration,
metadata.getLong(MediaMetadata.METADATA_KEY_DURATION));
}
}
latch.countDown();
}
};
MediaController controller = createController(mSession.getToken(), true, callback);
mPlayer.setMediaItem(item);
mPlayer.notifyCurrentMediaItemChanged(item);
assertFalse(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
item.setMetadata(TestUtils.createMetadata(item.getMediaId(), duration));
assertTrue(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
}
@Test
public void testSetMetadataForMediaItemInPlaylist() throws InterruptedException {
final CountDownLatch latch = new CountDownLatch(2);
final long duration = 1000L;
final int currentItemIdx = 0;
final List<MediaItem> list = TestUtils.createMediaItems(2);
final MediaMetadata oldMetadata = list.get(1).getMetadata();
final MediaMetadata newMetadata = TestUtils.createMetadata(oldMetadata.getMediaId(),
duration);
final ControllerCallback callback = new ControllerCallback() {
@Override
public void onPlaylistChanged(@NonNull MediaController controller,
@NonNull List<MediaItem> list, @Nullable MediaMetadata metadata) {
switch ((int) latch.getCount()) {
case 2:
assertEquals(currentItemIdx, controller.getCurrentMediaItemIndex());
assertFalse(oldMetadata.containsKey(MediaMetadata.METADATA_KEY_DURATION));
break;
case 1:
assertEquals(currentItemIdx, controller.getCurrentMediaItemIndex());
assertTrue(list.get(1).getMetadata().containsKey(
MediaMetadata.METADATA_KEY_DURATION));
assertEquals(duration, list.get(1).getMetadata().getLong(
MediaMetadata.METADATA_KEY_DURATION));
}
latch.countDown();
}
};
MediaController controller = createController(mSession.getToken(), true, callback);
mPlayer.setPlaylist(list, null);
mPlayer.skipToPlaylistItem(currentItemIdx);
mPlayer.notifyPlaylistChanged();
assertFalse(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
list.get(1).setMetadata(newMetadata);
assertTrue(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
}
private void testCloseFromService(String id) throws InterruptedException {
final CountDownLatch latch = new CountDownLatch(1);
TestServiceRegistry.getInstance().setSessionServiceCallback(new SessionServiceCallback() {
@Override
public void onCreated() {
// Do nothing.
}
@Override
public void onDestroyed() {
latch.countDown();
}
});
mController = createController(TestUtils.getServiceToken(mContext, id));
mController.close();
// Wait until close triggers onDestroy() of the session service.
assertTrue(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
assertNull(TestServiceRegistry.getInstance().getServiceInstance());
testNoInteraction();
// Test whether the controller is notified about later close of the session or
// re-creation.
testControllerAfterSessionIsClosed(id);
}
private void testControllerAfterSessionIsClosed(final String id) throws InterruptedException {
// This cause session service to be died.
mSession.close();
waitForDisconnect(mController, true);
testNoInteraction();
// Ensure that the controller cannot use newly create session with the same ID.
// Recreated session has different session stub, so previously created controller
// shouldn't be available.
mSession = new MediaSession.Builder(mContext, mPlayer)
.setSessionCallback(sHandlerExecutor, new SessionCallback() {})
.setId(id).build();
testNoInteraction();
}
// Test that mSession and mController doesn't interact.
// Note that this method can be called after the mSession is died, so mSession may not have
// valid player.
private void testNoInteraction() throws InterruptedException {
// TODO: check that calls from the controller to session shouldn't be delivered.
// Calls from the session to controller shouldn't be delivered.
final CountDownLatch latch = new CountDownLatch(1);
setRunnableForOnCustomCommand(mController, new Runnable() {
@Override
public void run() {
latch.countDown();
}
});
SessionCommand customCommand = new SessionCommand("testNoInteraction", null);
mSession.broadcastCustomCommand(customCommand, null);
assertFalse(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
setRunnableForOnCustomCommand(mController, null);
}
// TODO(jaewan): Add test for service connect rejection, when we differentiate session
// active/inactive and connection accept/refuse
class TestSessionCallback extends SessionCallback {
CountDownLatch mLatch;
void resetLatchCount(int count) {
mLatch = new CountDownLatch(count);
}
}
}