blob: ae67a952c99ed29815f3f120bde8b598d9671e3f [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 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";
MediaSession2 mSession;
MediaController2 mController;
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.close();
}
});
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 {
// TODO(jaewan): add equivalent test later
/*
final CountDownLatch latch = new CountDownLatch(1);
final MediaPlayerBase.PlaybackListener listener = (state) -> {
assertEquals(PlaybackState.STATE_BUFFERING, state.getState());
latch.countDown();
};
assertNull(mController.getPlaybackState());
mController.addPlaybackListener(listener, sHandler);
mPlayer.notifyPlaybackState(createPlaybackState(PlaybackState.STATE_BUFFERING));
assertTrue(latch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
assertEquals(PlaybackState.STATE_BUFFERING, mController.getPlaybackState().getState());
*/
}
// TODO(jaewan): add equivalent test later
/*
@Test
public void testAddPlaybackListener() throws InterruptedException {
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 MediaSession2.CommandGroup onConnect(ControllerInfo controller) {
return null;
}
};
sHandler.postAndSync(() -> {
mSession.close();
mSession = new MediaSession2.Builder(mContext, mPlayer)
.setSessionCallback(sHandlerExecutor, sessionCallback).build();
});
MediaController2 controller =
createController(mSession.getToken(), false, null);
assertNotNull(controller);
waitForConnect(controller, false);
waitForDisconnect(controller, true);
}
@Test
public void testControllerCallback_releaseSession() throws InterruptedException {
sHandler.postAndSync(() -> {
mSession.close();
});
waitForDisconnect(mController, true);
}
@Test
public void testControllerCallback_release() throws InterruptedException {
mController.close();
waitForDisconnect(mController, true);
}
@Test
public void testIsConnected() throws InterruptedException {
assertTrue(mController.isConnected());
sHandler.postAndSync(()->{
mSession.close();
});
// 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.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(() -> {
mSession = new MediaSession2.Builder(mContext, mPlayer)
.setId("testDeadlock").build();
});
final MediaController2 controller = createController(mSession.getToken());
testHandler.post(() -> {
final PlaybackState2 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.close();
mSession = null;
});
}
if (sessionThread != null) {
sessionThread.quitSafely();
}
if (testThread != null) {
testThread.quitSafely();
}
}
}
@Ignore
@Test
public void testGetServiceToken() {
SessionToken2 token = TestUtils.getServiceToken(mContext, MockMediaSessionService2.ID);
assertNotNull(token);
assertEquals(mContext.getPackageName(), token.getPackageName());
assertEquals(MockMediaSessionService2.ID, token.getId());
assertNull(token.getSessionBinder());
assertEquals(SessionToken2.TYPE_SESSION_SERVICE, token.getType());
}
private void connectToService(SessionToken2 token) throws InterruptedException {
mController = createController(token);
mSession = TestServiceRegistry.getInstance().getServiceInstance().getSession();
mPlayer = (MockPlayer) mSession.getPlayer();
}
// TODO(jaewan): Reenable when session manager detects app installs
@Ignore
@Test
public void testConnectToService_sessionService() throws InterruptedException {
connectToService(TestUtils.getServiceToken(mContext, MockMediaSessionService2.ID));
testConnectToService();
}
// TODO(jaewan): Reenable when session manager detects app installs
@Ignore
@Test
public void testConnectToService_libraryService() throws InterruptedException {
connectToService(TestUtils.getServiceToken(mContext, MockMediaLibraryService2.ID));
testConnectToService();
}
public void testConnectToService() throws InterruptedException {
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
// TODO(jaewan): Add equivalent tests again
/*
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 testClose_beforeConnected() throws InterruptedException {
MediaController2 controller =
createController(mSession.getToken(), false, null);
controller.close();
}
@Test
public void testClose_twice() throws InterruptedException {
mController.close();
mController.close();
}
@Test
public void testClose_session() throws InterruptedException {
final String id = mSession.getToken().getId();
mController.close();
// close is done immediately for session.
testNoInteraction();
// Test whether the controller is notified about later close of the session or
// re-creation.
testControllerAfterSessionIsGone(id);
}
// TODO(jaewan): Reenable when session manager detects app installs
@Ignore
@Test
public void testClose_sessionService() throws InterruptedException {
connectToService(TestUtils.getServiceToken(mContext, MockMediaSessionService2.ID));
testCloseFromService();
}
// TODO(jaewan): Reenable when session manager detects app installs
@Ignore
@Test
public void testClose_libraryService() throws InterruptedException {
connectToService(TestUtils.getServiceToken(mContext, MockMediaSessionService2.ID));
testCloseFromService();
}
private void testCloseFromService() throws InterruptedException {
final String id = mController.getSessionToken().getId();
final CountDownLatch latch = new CountDownLatch(1);
TestServiceRegistry.getInstance().setServiceInstanceChangedCallback((service) -> {
if (service == null) {
// Destroying..
latch.countDown();
}
});
mController.close();
// Wait until close 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 close of the session or
// re-creation.
testControllerAfterSessionIsGone(id);
}
private void testControllerAfterSessionIsGone(final String id) throws InterruptedException {
sHandler.postAndSync(() -> {
// TODO(jaewan): Use Session.close later when we add the API.
mSession.close();
});
waitForDisconnect(mController, 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 close.");
latch.countDown();
};
// TODO(jaewan): Add equivalent tests again
/*
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
}