diff --git a/app/src/androidTest/java/org/xbmc/kore/testhelpers/EspressoTestUtils.java b/app/src/androidTest/java/org/xbmc/kore/testhelpers/EspressoTestUtils.java index dde1e4c..93032a9 100644 --- a/app/src/androidTest/java/org/xbmc/kore/testhelpers/EspressoTestUtils.java +++ b/app/src/androidTest/java/org/xbmc/kore/testhelpers/EspressoTestUtils.java @@ -177,10 +177,26 @@ public class EspressoTestUtils { * Checks that the list contains item(s) matching search query * @param query text each element must contain * @param listSize amount of elements expected in list + * @param resourceId resource identifier or list view */ public static void checkListMatchesSearchQuery(String query, int listSize, int resourceId) { + onView(isRoot()).perform(ViewActions.waitForView(resourceId, new ViewActions.CheckStatus() { + @Override + public boolean check(View v) { + return v.isShown(); + } + }, 10000)); + onView(allOf(withId(resourceId), isDisplayed())) .check(matches(Matchers.withOnlyMatchingDataItems(hasDescendant(withText(containsString(query)))))); + checkRecyclerViewListsize(listSize, resourceId); + } + + /** + * Checks that the list size matches the given list size + * @param listSize amount of elements expected in list + */ + public static void checkRecyclerViewListsize(int listSize, int resourceId) { onView(allOf(withId(resourceId), isDisplayed())) .check(matches(Matchers.withRecyclerViewSize(listSize))); } @@ -294,7 +310,7 @@ public class EspressoTestUtils { */ public static void selectListItemRotateDeviceAndCheckActionbarTitle(String itemText, int listResourceId, - String actionbarTitle, + final String actionbarTitle, Activity activity) { EspressoTestUtils.clickRecyclerViewItem(itemText, listResourceId); EspressoTestUtils.rotateDevice(activity); diff --git a/app/src/androidTest/java/org/xbmc/kore/tests/ui/AbstractTestClass.java b/app/src/androidTest/java/org/xbmc/kore/tests/ui/AbstractTestClass.java index cc84b2c..43acae2 100644 --- a/app/src/androidTest/java/org/xbmc/kore/tests/ui/AbstractTestClass.java +++ b/app/src/androidTest/java/org/xbmc/kore/tests/ui/AbstractTestClass.java @@ -43,6 +43,7 @@ import org.xbmc.kore.testutils.tcpserver.handlers.InputHandler; import org.xbmc.kore.testutils.tcpserver.handlers.JSONConnectionHandlerManager; import org.xbmc.kore.testutils.tcpserver.handlers.JSONRPCHandler; import org.xbmc.kore.testutils.tcpserver.handlers.PlayerHandler; +import org.xbmc.kore.testutils.tcpserver.handlers.PlaylistHandler; import org.xbmc.kore.ui.sections.hosts.HostFragmentManualConfiguration; import org.xbmc.kore.utils.LogUtils; @@ -69,6 +70,7 @@ abstract public class AbstractTestClass { private static PlayerHandler playerHandler; private static ApplicationHandler applicationHandler; private static InputHandler inputHandler; + private static PlaylistHandler playlistHandler; private int kodiMajorVersion = HostInfo.DEFAULT_KODI_VERSION_MAJOR; private HostInfo hostInfo; @@ -77,11 +79,13 @@ abstract public class AbstractTestClass { playerHandler = new PlayerHandler(); applicationHandler = new ApplicationHandler(); inputHandler = new InputHandler(); + playlistHandler = new PlaylistHandler(); manager = new JSONConnectionHandlerManager(); manager.addHandler(playerHandler); manager.addHandler(applicationHandler); manager.addHandler(inputHandler); manager.addHandler(new AddonsHandler()); + manager.addHandler(playlistHandler); manager.addHandler(new JSONRPCHandler()); server = new MockTcpServer(manager); server.start(); @@ -134,7 +138,7 @@ abstract public class AbstractTestClass { playerHandler.reset(); Context context = activityTestRule.getActivity(); - Database.flush(context.getContentResolver(), hostInfo); + Database.flush(context.getContentResolver()); Utils.enableAnimations(context); } @@ -161,6 +165,10 @@ abstract public class AbstractTestClass { this.kodiMajorVersion = kodiMajorVersion; } + public static JSONConnectionHandlerManager getConnectionHandlerManager() { + return manager; + } + public static PlayerHandler getPlayerHandler() { return playerHandler; } @@ -172,4 +180,8 @@ abstract public class AbstractTestClass { public static InputHandler getInputHandler() { return inputHandler; } + + public static PlaylistHandler getPlaylistHandler() { + return playlistHandler; + } } diff --git a/app/src/androidTest/java/org/xbmc/kore/tests/ui/addons/AddonsActivityTests.java b/app/src/androidTest/java/org/xbmc/kore/tests/ui/addons/AddonsActivityTests.java index 3aa34e0..df97107 100644 --- a/app/src/androidTest/java/org/xbmc/kore/tests/ui/addons/AddonsActivityTests.java +++ b/app/src/androidTest/java/org/xbmc/kore/tests/ui/addons/AddonsActivityTests.java @@ -26,7 +26,6 @@ import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.xbmc.kore.R; -import org.xbmc.kore.host.HostInfo; import org.xbmc.kore.testhelpers.EspressoTestUtils; import org.xbmc.kore.testhelpers.action.ViewActions; import org.xbmc.kore.tests.ui.AbstractTestClass; @@ -65,7 +64,6 @@ import static org.xbmc.kore.testhelpers.EspressoTestUtils.selectListItemPressBac * {@link org.xbmc.kore.ui.sections.addon.AddonsActivity} to become idle. */ public class AddonsActivityTests extends AbstractTestClass { - @Rule public ActivityTestRule mActivityRule = new ActivityTestRule<>(AddonsActivity.class); diff --git a/app/src/androidTest/java/org/xbmc/kore/tests/ui/music/SlideUpPanelTests.java b/app/src/androidTest/java/org/xbmc/kore/tests/ui/music/SlideUpPanelTests.java index 2f871ce..a3bc81a 100644 --- a/app/src/androidTest/java/org/xbmc/kore/tests/ui/music/SlideUpPanelTests.java +++ b/app/src/androidTest/java/org/xbmc/kore/tests/ui/music/SlideUpPanelTests.java @@ -36,11 +36,15 @@ import org.xbmc.kore.testhelpers.Utils; import org.xbmc.kore.testhelpers.action.ViewActions; import org.xbmc.kore.tests.ui.AbstractTestClass; import org.xbmc.kore.testutils.tcpserver.handlers.PlayerHandler; +import org.xbmc.kore.testutils.tcpserver.handlers.jsonrpc.response.methods.Application; import org.xbmc.kore.testutils.tcpserver.handlers.jsonrpc.response.methods.Player; +import org.xbmc.kore.testutils.tcpserver.handlers.jsonrpc.response.methods.Playlist; import org.xbmc.kore.ui.sections.audio.MusicActivity; import org.xbmc.kore.ui.widgets.HighlightButton; import org.xbmc.kore.ui.widgets.RepeatModeButton; +import java.util.concurrent.TimeoutException; + import static android.support.test.espresso.Espresso.onView; import static android.support.test.espresso.Espresso.pressBack; import static android.support.test.espresso.action.ViewActions.click; @@ -50,13 +54,17 @@ import static android.support.test.espresso.matcher.ViewMatchers.isRoot; import static android.support.test.espresso.matcher.ViewMatchers.withId; import static android.support.test.espresso.matcher.ViewMatchers.withText; import static org.hamcrest.Matchers.not; -import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertSame; import static org.junit.Assert.assertTrue; import static org.xbmc.kore.testhelpers.EspressoTestUtils.clickAdapterViewItem; import static org.xbmc.kore.testhelpers.EspressoTestUtils.rotateDevice; import static org.xbmc.kore.testhelpers.EspressoTestUtils.waitForPanelState; import static org.xbmc.kore.testhelpers.Matchers.withHighlightState; import static org.xbmc.kore.testhelpers.Matchers.withProgress; +import static org.xbmc.kore.testutils.TestUtils.createMusicItem; +import static org.xbmc.kore.testutils.TestUtils.createMusicVideoItem; +import static org.xbmc.kore.testutils.TestUtils.createVideoItem; public class SlideUpPanelTests extends AbstractTestClass { @@ -78,8 +86,14 @@ public class SlideUpPanelTests extends AbstractTestClass { public void setUp() throws Throwable { super.setUp(); + getPlaylistHandler().reset(); + getPlaylistHandler().addItemToPlaylist(Playlist.playlistID.AUDIO, createMusicItem(0, 0)); + getPlaylistHandler().addItemToPlaylist(Playlist.playlistID.VIDEO, createVideoItem(0, 1)); + getPlaylistHandler().addItemToPlaylist(Playlist.playlistID.VIDEO, createMusicVideoItem(0, 2)); + getPlayerHandler().reset(); - getPlayerHandler().startPlay(); + getPlayerHandler().setPlaylists(getPlaylistHandler().getPlaylists()); + getPlayerHandler().startPlay(Playlist.playlistID.AUDIO, 0); waitForPanelState(SlidingUpPanelLayout.PanelState.COLLAPSED); } @@ -120,8 +134,7 @@ public class SlideUpPanelTests extends AbstractTestClass { */ @Test public void panelButtonsMoviesTest() { - getPlayerHandler().setMediaType(PlayerHandler.TYPE.MOVIE); - getPlayerHandler().startPlay(); + getPlayerHandler().startPlay(Playlist.playlistID.VIDEO, 0); Player.GetItem item = getPlayerHandler().getMediaItem(); final String title = item.getTitle(); onView(isRoot()).perform(ViewActions.waitForView( @@ -146,8 +159,7 @@ public class SlideUpPanelTests extends AbstractTestClass { */ @Test public void panelButtonsMusicVideoTest() { - getPlayerHandler().setMediaType(PlayerHandler.TYPE.MUSICVIDEO); - getPlayerHandler().startPlay(); + getPlayerHandler().startPlay(Playlist.playlistID.VIDEO, 1); Player.GetItem item = getPlayerHandler().getMediaItem(); final String title = item.getTitle(); onView(isRoot()).perform(ViewActions.waitForView( @@ -374,7 +386,7 @@ public class SlideUpPanelTests extends AbstractTestClass { * 4. Result: Volume indicator should show volume level and server should be set to new volume level */ @Test - public void changeVolume() { + public void changeVolume() throws TimeoutException { final int volume = 16; expandPanel(); @@ -382,7 +394,10 @@ public class SlideUpPanelTests extends AbstractTestClass { onView(withId(R.id.vli_seek_bar)).check(matches(withProgress(volume))); onView(withId(R.id.vli_volume_text)).check(matches(withText(String.valueOf(volume)))); - assertTrue(getApplicationHandler().getVolume() == volume); + + getConnectionHandlerManager().waitForMethodHandled(Application.SetVolume.METHOD_NAME, 10000); + assertTrue("applicationHandler volume: "+ getApplicationHandler().getVolume() + + " != " + volume, getApplicationHandler().getVolume() == volume); } /** @@ -396,13 +411,15 @@ public class SlideUpPanelTests extends AbstractTestClass { * 4. Result: Volume indicator should show volume level and server should be set to new volume level */ @Test - public void restoreVolumeIndicatorOnRotate() { + public void restoreVolumeIndicatorOnRotate() throws TimeoutException { final int volume = 16; expandPanel(); onView(withId(R.id.vli_seek_bar)).perform(ViewActions.slideSeekBar(volume)); rotateDevice(getActivity()); + assertTrue("applicationHandler volume: "+ getApplicationHandler().getVolume() + + " != " + volume, getApplicationHandler().getVolume() == volume); onView(isRoot()).perform(ViewActions.waitForView(R.id.vli_seek_bar, new ViewActions.CheckStatus() { @Override public boolean check(View v) { @@ -410,7 +427,6 @@ public class SlideUpPanelTests extends AbstractTestClass { } }, 10000)); onView(withId(R.id.vli_volume_text)).check(matches(withText(String.valueOf(volume)))); - assertTrue(getApplicationHandler().getVolume() == volume); } /** @@ -434,7 +450,7 @@ public class SlideUpPanelTests extends AbstractTestClass { onView(withId(R.id.mpi_seek_bar)).perform(ViewActions.slideSeekBar(progress)); onView(withId(R.id.mpi_progress)).check(matches(withText(progressText))); - assertTrue(getPlayerHandler().getPosition() == progress); + assertTrue(getPlayerHandler().getTimeElapsed() == progress); } /** @@ -458,7 +474,7 @@ public class SlideUpPanelTests extends AbstractTestClass { onView(withId(R.id.mpi_seek_bar)).perform(ViewActions.slideSeekBar(progress)); rotateDevice(getActivity()); - assertTrue(getPlayerHandler().getPosition() == progress); + assertEquals(getPlayerHandler().getTimeElapsed(), progress); onView(withId(R.id.mpi_progress)).check(matches(withProgress(progressText))); onView(withId(R.id.mpi_seek_bar)).check(matches(withProgress(progress))); } @@ -560,7 +576,7 @@ public class SlideUpPanelTests extends AbstractTestClass { public void pausePlayback() { onView(withId(R.id.npp_play)).perform(click()); - assertFalse(getPlayerHandler().isPlaying()); + assertSame(getPlayerHandler().getPlayState(), PlayerHandler.PLAY_STATE.PAUSED); expandPanel(); final int progress = ((SeekBar) getActivity().findViewById(R.id.mpi_seek_bar)).getProgress(); diff --git a/app/src/androidTest/java/org/xbmc/kore/tests/ui/remote/controlpad/eventserver/KodiPreV17Tests.java b/app/src/androidTest/java/org/xbmc/kore/tests/ui/remote/controlpad/eventserver/KodiPreV17Tests.java index 9cb27d4..c525227 100644 --- a/app/src/androidTest/java/org/xbmc/kore/tests/ui/remote/controlpad/eventserver/KodiPreV17Tests.java +++ b/app/src/androidTest/java/org/xbmc/kore/tests/ui/remote/controlpad/eventserver/KodiPreV17Tests.java @@ -26,7 +26,6 @@ import org.junit.Rule; import org.junit.Test; import org.xbmc.kore.R; import org.xbmc.kore.host.HostInfo; -import org.xbmc.kore.host.HostManager; import org.xbmc.kore.jsonrpc.method.Input; import org.xbmc.kore.testhelpers.Utils; import org.xbmc.kore.tests.ui.AbstractTestClass; diff --git a/app/src/androidTest/java/org/xbmc/kore/tests/ui/remote/controlpad/http/ButtonTests.java b/app/src/androidTest/java/org/xbmc/kore/tests/ui/remote/controlpad/http/ButtonTests.java index d91c0c5..ad5430a 100644 --- a/app/src/androidTest/java/org/xbmc/kore/tests/ui/remote/controlpad/http/ButtonTests.java +++ b/app/src/androidTest/java/org/xbmc/kore/tests/ui/remote/controlpad/http/ButtonTests.java @@ -57,49 +57,49 @@ public class ButtonTests extends AbstractTestClass { } @Test - public void leftControlPadButtonTest() throws InterruptedException { + public void leftControlPadButtonTest() { onView(withId(R.id.left)).perform(click()); TestUtils.testHTTPEvent(Input.Left.METHOD_NAME, null); } @Test - public void rightControlPadButtonTest() throws InterruptedException { + public void rightControlPadButtonTest() { onView(withId(R.id.right)).perform(click()); TestUtils.testHTTPEvent(Input.Right.METHOD_NAME, null); } @Test - public void upControlPadButtonTest() throws InterruptedException { + public void upControlPadButtonTest() { onView(withId(R.id.up)).perform(click()); TestUtils.testHTTPEvent(Input.Up.METHOD_NAME, null); } @Test - public void downControlPadButtonTest() throws InterruptedException { + public void downControlPadButtonTest() { onView(withId(R.id.down)).perform(click()); TestUtils.testHTTPEvent(Input.Down.METHOD_NAME, null); } @Test - public void selectPadButtonTest() throws InterruptedException { + public void selectPadButtonTest() { onView(withId(R.id.select)).perform(click()); TestUtils.testHTTPEvent(Input.Select.METHOD_NAME, null); } @Test - public void contextControlPadButtonTest() throws InterruptedException { + public void contextControlPadButtonTest() { onView(withId(R.id.context)).perform(click()); TestUtils.testHTTPEvent(Input.ExecuteAction.METHOD_NAME, Input.ExecuteAction.CONTEXTMENU); } @Test - public void infoControlPadButtonTest() throws InterruptedException { + public void infoControlPadButtonTest() { HostManager.getInstance(getActivity()).getHostInfo().setKodiVersionMajor(17); onView(withId(R.id.info)).perform(click()); @@ -108,21 +108,21 @@ public class ButtonTests extends AbstractTestClass { } @Test - public void infoControlPadButtonLongClickTest() throws InterruptedException { + public void infoControlPadButtonLongClickTest() { onView(withId(R.id.info)).perform(longClick()); TestUtils.testHTTPEvent(Input.ExecuteAction.METHOD_NAME, Input.ExecuteAction.PLAYERPROCESSINFO); } @Test - public void osdControlPadButtonTest() throws InterruptedException { + public void osdControlPadButtonTest() { onView(withId(R.id.osd)).perform(click()); TestUtils.testHTTPEvent(Input.ExecuteAction.METHOD_NAME, Input.ExecuteAction.OSD); } @Test - public void backControlPadButtonTest() throws InterruptedException { + public void backControlPadButtonTest() { onView(withId(R.id.back)).perform(click()); TestUtils.testHTTPEvent(Input.Back.METHOD_NAME, null); diff --git a/app/src/androidTest/java/org/xbmc/kore/tests/ui/remote/controlpad/http/KodiPreV17Tests.java b/app/src/androidTest/java/org/xbmc/kore/tests/ui/remote/controlpad/http/KodiPreV17Tests.java index 1b10b3f..4d635e9 100644 --- a/app/src/androidTest/java/org/xbmc/kore/tests/ui/remote/controlpad/http/KodiPreV17Tests.java +++ b/app/src/androidTest/java/org/xbmc/kore/tests/ui/remote/controlpad/http/KodiPreV17Tests.java @@ -55,7 +55,7 @@ public class KodiPreV17Tests extends AbstractTestClass { } @Test - public void infoControlPadButtonLongClickTest() throws InterruptedException { + public void infoControlPadButtonLongClickTest() { onView(withId(R.id.info)).perform(longClick()); TestUtils.testHTTPEvent(Input.ExecuteAction.METHOD_NAME, Input.ExecuteAction.CODECINFO); diff --git a/app/src/debug/java/org/xbmc/kore/testutils/Database.java b/app/src/debug/java/org/xbmc/kore/testutils/Database.java index e3be1e3..a9aabe9 100644 --- a/app/src/debug/java/org/xbmc/kore/testutils/Database.java +++ b/app/src/debug/java/org/xbmc/kore/testutils/Database.java @@ -60,8 +60,8 @@ public class Database { return hostInfo; } - public static void flush(ContentResolver contentResolver, HostInfo hostInfo) { - contentResolver.delete(MediaContract.Hosts.buildHostUri(hostInfo.getId()), null, null); + public static void flush(ContentResolver contentResolver) { + contentResolver.delete(MediaContract.Hosts.CONTENT_URI, null, null); } public static HostInfo addHost(Context context) { diff --git a/app/src/debug/java/org/xbmc/kore/testutils/TestUtils.java b/app/src/debug/java/org/xbmc/kore/testutils/TestUtils.java index 9e7c1ab..dfd8498 100644 --- a/app/src/debug/java/org/xbmc/kore/testutils/TestUtils.java +++ b/app/src/debug/java/org/xbmc/kore/testutils/TestUtils.java @@ -18,6 +18,8 @@ package org.xbmc.kore.testutils; import android.database.Cursor; +import org.xbmc.kore.testutils.tcpserver.handlers.jsonrpc.response.methods.Player; + import java.util.HashMap; import java.util.Map; @@ -71,4 +73,72 @@ public class TestUtils { assertTrue("Id " + key + " not found", entry.getValue()); } } + + public static Player.GetItem createMusicItem(int i, int libraryId) { + Player.GetItem getItem = new Player.GetItem(); + getItem.addTrack(i); + getItem.addLibraryId(libraryId); + getItem.addAlbum("Album 1"); + getItem.addArtist("Artist 1"); + getItem.addDisplayartist("Artist 1"); + getItem.addAlbumArtist("Album Artist 1"); + getItem.addFanart("image://http%3a%2f%2fmedia.theaudiodb.com%2fimages%2fmedia%2fartist%2ffanart%2fxpptss1381301172.jpg/"); + getItem.addDuration(240); + getItem.addFile("/Users/martijn/Projects/dummymediafiles/media/music/Artist 1/Album 1/Track " + i + ".mp3"); + getItem.addLabel("Label " + i); + getItem.addThumbnail(""); + getItem.addTitle("Music "+ i); + getItem.addType(Player.GetItem.TYPE.song); + + return getItem; + } + + public static Player.GetItem createVideoItem(int i, int libraryId) { + Player.GetItem getItem = new Player.GetItem(0); + getItem.addTrack(i); + getItem.addLibraryId(libraryId); + getItem.addDirector("Director 1"); + getItem.addDescription("Description of video " + i); + getItem.addGenre("Drama"); + getItem.addFanart("image://http%3a%2f%2fmedia.theaudiodb.com%2fimages%2fmedia%2fartist%2ffanart%2fxpptss1381301172.jpg/"); + getItem.addDuration(25); + getItem.addFile("/Users/martijn/Projects/dummymediafiles/media/music/Artist 1/Album 1/Track " + i + ".mp3"); + getItem.addLabel("Label " + i); + getItem.addThumbnail(""); + getItem.addTitle("Video "+ i); + getItem.addPlot("Plot " + i); + getItem.addYear(2009); + getItem.addType(Player.GetItem.TYPE.movie); + + return getItem; + } + + public static Player.GetItem createMusicVideoItem(int i, int libraryId) { + Player.GetItem getItem = new Player.GetItem(0); + getItem.addTrack(i); + getItem.addLibraryId(libraryId); + getItem.addType(Player.GetItem.TYPE.musicvideo); + getItem.addAlbum("...Baby One More Time"); + getItem.addDirector("Nigel Dick"); + getItem.addThumbnail("image://http%3a%2f%2fwww.theaudiodb.com%2fimages%2fmedia%2falbum%2fthumb%2fbaby-one-more-time-4dcff7453745a.jpg/"); + getItem.addYear(1999); + getItem.addTitle("(You Drive Me) Crazy"); + getItem.addLabel("(You Drive Me) Crazy"); + getItem.addDuration(201); + getItem.addGenre("Pop"); + getItem.addPremiered("1999-01-01"); + + return getItem; + } + + public static Player.GetItem createPictureItem(int i, int libraryId) { + Player.GetItem getItem = new Player.GetItem(0); + getItem.addLibraryId(libraryId); + getItem.addDescription("Description of picture " + i); + getItem.addFile("/Users/martijn/Projects/dummymediafiles/media/music/Artist 1/Album 1/Track " + i + ".mp3"); + getItem.addYear(2008); + getItem.addType(Player.GetItem.TYPE.picture); + + return getItem; + } } diff --git a/app/src/debug/java/org/xbmc/kore/testutils/tcpserver/MockTcpServer.java b/app/src/debug/java/org/xbmc/kore/testutils/tcpserver/MockTcpServer.java index 369d3f2..d636003 100644 --- a/app/src/debug/java/org/xbmc/kore/testutils/tcpserver/MockTcpServer.java +++ b/app/src/debug/java/org/xbmc/kore/testutils/tcpserver/MockTcpServer.java @@ -21,13 +21,13 @@ import com.squareup.okhttp.internal.Util; import org.xbmc.kore.utils.LogUtils; import java.io.IOException; + import java.io.InputStreamReader; import java.io.PrintWriter; import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.ServerSocket; import java.net.Socket; -import java.net.SocketException; import java.util.Collections; import java.util.Iterator; import java.util.Set; @@ -45,7 +45,7 @@ public class MockTcpServer { private ServerSocketFactory serverSocketFactory = ServerSocketFactory.getDefault(); private ServerSocket serverSocket; - private boolean started; + private boolean running; private ExecutorService executor; private int port = -1; private InetSocketAddress inetSocketAddress; @@ -62,10 +62,10 @@ public class MockTcpServer { public interface TcpServerConnectionHandler { /** * Processes received input - * @param c character received + * @param socket * @return id of associated response if any, -1 if more input is needed. */ - void processInput(char c); + void processInput(Socket socket); /** * Gets the answer for this handler that should be returned to the server after input has been @@ -93,8 +93,8 @@ public class MockTcpServer { * @throws IOException */ public void start(InetSocketAddress inetSocketAddress) throws IOException { - if (started) throw new IllegalStateException("start() already called"); - started = true; + if (running) throw new IllegalStateException("start() already called"); + running = true; this.inetSocketAddress = inetSocketAddress; serverSocket = serverSocketFactory.createServerSocket(); @@ -106,46 +106,50 @@ public class MockTcpServer { port = serverSocket.getLocalPort(); + LogUtils.LOGD(TAG, "start: server started on " + serverSocket.getInetAddress() + ":" + port); + executor.execute(new Runnable() { @Override public void run() { - try { - acceptConnection(); - } catch (Throwable e) { - LogUtils.LOGE(TAG, " failed unexpectedly: " + e); + while (running) { + try { + Socket socket = acceptConnection(); + serveConnection(socket); + } catch(IOException e){ + //Socket closed + LogUtils.LOGD(TAG, "acceptConnection: " + e.getMessage()); + } } - - // Release all sockets and all threads, even if any close fails. - Util.closeQuietly(serverSocket); - for (Iterator s = openClientSockets.iterator(); s.hasNext(); ) { - Util.closeQuietly(s.next()); - s.remove(); - } - - executor.shutdown(); } - private void acceptConnection() throws Exception { - while (true) { - Socket socket; - try { - socket = serverSocket.accept(); - } catch (SocketException e) { - //Socket closed - return; - } + private Socket acceptConnection() throws IOException { + Socket socket = serverSocket.accept(); + + synchronized (openClientSockets) { openClientSockets.add(socket); - serveConnection(socket); } + + return socket; } }); } public synchronized void shutdown() throws IOException { - if (!started) return; + if (!running) return; + if (serverSocket == null) throw new IllegalStateException("shutdown() before start()"); - serverSocket.close(); + running = false; + + // Release all sockets and all threads, even if any close fails. + for (Iterator s = openClientSockets.iterator(); s.hasNext(); ) { + Socket socket = s.next(); + Util.closeQuietly(socket); + s.remove(); + } + Util.closeQuietly(serverSocket); + + executor.shutdown(); // Await shutdown. try { @@ -153,7 +157,7 @@ public class MockTcpServer { throw new IOException("Gave up waiting for executor to shut down"); } } catch (InterruptedException e) { - throw new AssertionError(); + LogUtils.LOGD(TAG, "shutdown: " + e.getMessage()); } } @@ -178,34 +182,28 @@ public class MockTcpServer { @Override public void run() { try { - handleInput(); + LogUtils.LOGD(TAG, "serveConnection: handling client " + socket.getInetAddress() + + ":" + socket.getLocalPort()); + + connectionHandler.processInput(socket); + socket.close(); + + synchronized (openClientSockets) { + openClientSockets.remove(socket); + } } catch (IOException e) { LogUtils.LOGW(TAG, "processing input from " + socket.getInetAddress() + " failed: " + e); } } - - private void handleInput() throws IOException { - InputStreamReader in = new InputStreamReader(socket.getInputStream()); - - int i; - while ((i = in.read()) != -1) { - connectionHandler.processInput((char) i); - } - - socket.close(); - openClientSockets.remove(socket); - } }); executor.execute(new Runnable() { @Override public void run() { try { - while (true) { + while ( ! (serverSocket.isClosed() || socket.isClosed()) ) { sendResponse(); Thread.sleep(100); - if ( serverSocket.isClosed() ) - return; } } catch (IOException e) { LogUtils.LOGW(TAG, " sending response from " + socket.getInetAddress() + " failed: " + e); @@ -216,9 +214,10 @@ public class MockTcpServer { private void sendResponse() throws IOException { PrintWriter out = - new PrintWriter(socket.getOutputStream(), true); + new PrintWriter(socket.getOutputStream(), false); String answer = connectionHandler.getResponse(); if (answer != null) { + LogUtils.LOGD(TAG, "serveConnection: sendResponse: " +answer); out.print(answer); out.flush(); } diff --git a/app/src/debug/java/org/xbmc/kore/testutils/tcpserver/handlers/AddonsHandler.java b/app/src/debug/java/org/xbmc/kore/testutils/tcpserver/handlers/AddonsHandler.java index c6074af..95fad1b 100644 --- a/app/src/debug/java/org/xbmc/kore/testutils/tcpserver/handlers/AddonsHandler.java +++ b/app/src/debug/java/org/xbmc/kore/testutils/tcpserver/handlers/AddonsHandler.java @@ -28,29 +28,20 @@ import java.util.ArrayList; /** * Simulates Addons JSON-RPC API */ -public class AddonsHandler implements JSONConnectionHandlerManager.ConnectionHandler { +public class AddonsHandler extends ConnectionHandler { private static final String TAG = LogUtils.makeLogTag(AddonsHandler.class); private static final String ID_NODE = "id"; public AddonsHandler() { } - @Override - public ArrayList getNotifications() { - return null; - } - - @Override - public void reset() { - } - @Override public String[] getType() { return new String[]{Addons.GetAddons.METHOD_NAME}; } @Override - public ArrayList getResponse(String method, ObjectNode jsonRequest) { + public ArrayList createResponse(String method, ObjectNode jsonRequest) { ArrayList jsonResponses = new ArrayList<>(); int methodId = jsonRequest.get(ID_NODE).asInt(-1); diff --git a/app/src/debug/java/org/xbmc/kore/testutils/tcpserver/handlers/ApplicationHandler.java b/app/src/debug/java/org/xbmc/kore/testutils/tcpserver/handlers/ApplicationHandler.java index 8fb2124..ecf7431 100644 --- a/app/src/debug/java/org/xbmc/kore/testutils/tcpserver/handlers/ApplicationHandler.java +++ b/app/src/debug/java/org/xbmc/kore/testutils/tcpserver/handlers/ApplicationHandler.java @@ -31,7 +31,7 @@ import static org.xbmc.kore.testutils.tcpserver.handlers.jsonrpc.response.notifi /** * Simulates Application JSON-RPC API */ -public class ApplicationHandler implements JSONConnectionHandlerManager.ConnectionHandler { +public class ApplicationHandler extends ConnectionHandler { private static final String TAG = LogUtils.makeLogTag(ApplicationHandler.class); private boolean muted; @@ -40,8 +40,6 @@ public class ApplicationHandler implements JSONConnectionHandlerManager.Connecti private static final String PARAMS_NODE = "params"; private static final String PROPERTIES_NODE = "properties"; - private ArrayList jsonNotifications = new ArrayList<>(); - /** * Sets the muted state and sends a notification * @param muted @@ -51,7 +49,7 @@ public class ApplicationHandler implements JSONConnectionHandlerManager.Connecti this.muted = muted; if (notify) - jsonNotifications.add(new OnVolumeChanged(muted, volume)); + addNotification(new OnVolumeChanged(muted, volume)); } /** @@ -63,22 +61,16 @@ public class ApplicationHandler implements JSONConnectionHandlerManager.Connecti this.volume = volume; if (notify) - jsonNotifications.add(new OnVolumeChanged(muted, volume)); + addNotification(new OnVolumeChanged(muted, volume)); } public int getVolume() { return volume; } - @Override - public ArrayList getNotifications() { - ArrayList jsonResponses = new ArrayList<>(jsonNotifications); - jsonNotifications.clear(); - return jsonResponses; - } - @Override public void reset() { + super.reset(); this.volume = 0; this.muted = false; } @@ -91,7 +83,7 @@ public class ApplicationHandler implements JSONConnectionHandlerManager.Connecti } @Override - public ArrayList getResponse(String method, ObjectNode jsonRequest) { + public ArrayList createResponse(String method, ObjectNode jsonRequest) { ArrayList jsonResponses = new ArrayList<>(); int methodId = jsonRequest.get(ID_NODE).asInt(-1); diff --git a/app/src/debug/java/org/xbmc/kore/testutils/tcpserver/handlers/ConnectionHandler.java b/app/src/debug/java/org/xbmc/kore/testutils/tcpserver/handlers/ConnectionHandler.java new file mode 100644 index 0000000..8bb40e6 --- /dev/null +++ b/app/src/debug/java/org/xbmc/kore/testutils/tcpserver/handlers/ConnectionHandler.java @@ -0,0 +1,102 @@ +/* + * Copyright 2018 Martijn Brekhof. All rights reserved. + * + * 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 org.xbmc.kore.testutils.tcpserver.handlers; + + +import com.fasterxml.jackson.databind.node.ObjectNode; + +import org.xbmc.kore.testutils.tcpserver.handlers.jsonrpc.JsonResponse; +import org.xbmc.kore.utils.LogUtils; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.concurrent.TimeoutException; + +abstract class ConnectionHandler { + private static final String TAG = LogUtils.makeLogTag(ConnectionHandler.class); + + private ArrayList notifications = new ArrayList<>(); + private HashSet methodsHandled = new HashSet<>(); + + /** + * Used to determine which methods the handler implements + * @return list of JSON method names + */ + abstract String[] getType(); + + abstract ArrayList createResponse(String method, ObjectNode jsonRequest); + + /** + * Used to get any notifications from the handler. + * @return {@link JsonResponse} that should be sent to the client or null if there are no notifications + */ + public ArrayList getNotifications() { + ArrayList list = new ArrayList<>(notifications); + notifications.clear(); + return list; + } + + /** + * Returns the response for the requested method. + * @param method requested method + * @param jsonRequest json node containing the original request + * @return {@link JsonResponse} that should be sent to the client + */ + public ArrayList getResponse(String method, ObjectNode jsonRequest) { + ArrayList responses = createResponse(method, jsonRequest); + methodsHandled.add(method); + return responses; + } + + /** + * Sets the state of the handler to its initial state + */ + public void reset() { + methodsHandled.clear(); + } + + /** + * Waits for given method to be handled by this handler before returning. + * @param method + * @param timeOutMillis + */ + public void waitForMethodHandled(String method, long timeOutMillis) throws TimeoutException { + while ((!methodsHandled.contains(method)) && timeOutMillis > 0) { + try { + Thread.sleep(100); + timeOutMillis -= 100; + } catch (InterruptedException e) { + LogUtils.LOGE(TAG, "Thread.sleep interrupted"); + return; + } + } + if (timeOutMillis <= 0) + throw new TimeoutException(); + } + + /** + * Clears the list of methods handled by the connection handler. + */ + public void clearMethodsHandled() { + methodsHandled.clear(); + } + + void addNotification(JsonResponse notification) { + notifications.add(notification); + } +} diff --git a/app/src/debug/java/org/xbmc/kore/testutils/tcpserver/handlers/InputHandler.java b/app/src/debug/java/org/xbmc/kore/testutils/tcpserver/handlers/InputHandler.java index c2976d8..59171ec 100644 --- a/app/src/debug/java/org/xbmc/kore/testutils/tcpserver/handlers/InputHandler.java +++ b/app/src/debug/java/org/xbmc/kore/testutils/tcpserver/handlers/InputHandler.java @@ -27,7 +27,7 @@ import java.util.ArrayList; /** * Simulates Input JSON-RPC API */ -public class InputHandler implements JSONConnectionHandlerManager.ConnectionHandler { +public class InputHandler extends ConnectionHandler { private static final String TAG = LogUtils.makeLogTag(InputHandler.class); private static final String ACTION = "action"; @@ -36,15 +36,6 @@ public class InputHandler implements JSONConnectionHandlerManager.ConnectionHand private String action; private String methodName; - @Override - public ArrayList getNotifications() { - return null; - } - - @Override - public void reset() { - } - @Override public String[] getType() { return new String[]{Input.ExecuteAction.METHOD_NAME, @@ -58,7 +49,7 @@ public class InputHandler implements JSONConnectionHandlerManager.ConnectionHand } @Override - public ArrayList getResponse(String method, ObjectNode jsonRequest) { + public ArrayList createResponse(String method, ObjectNode jsonRequest) { ArrayList jsonResponses = new ArrayList<>(); methodName = method; diff --git a/app/src/debug/java/org/xbmc/kore/testutils/tcpserver/handlers/JSONConnectionHandlerManager.java b/app/src/debug/java/org/xbmc/kore/testutils/tcpserver/handlers/JSONConnectionHandlerManager.java index e910bbc..8a8ed92 100644 --- a/app/src/debug/java/org/xbmc/kore/testutils/tcpserver/handlers/JSONConnectionHandlerManager.java +++ b/app/src/debug/java/org/xbmc/kore/testutils/tcpserver/handlers/JSONConnectionHandlerManager.java @@ -25,10 +25,13 @@ import org.xbmc.kore.testutils.tcpserver.handlers.jsonrpc.JsonResponse; import org.xbmc.kore.utils.LogUtils; import java.io.IOException; +import java.io.InputStreamReader; +import java.net.Socket; +import java.net.SocketException; import java.util.ArrayList; -import java.util.Collection; import java.util.HashMap; import java.util.HashSet; +import java.util.Map; import java.util.concurrent.TimeoutException; import static org.xbmc.kore.jsonrpc.ApiMethod.ID_NODE; @@ -37,143 +40,218 @@ import static org.xbmc.kore.jsonrpc.ApiMethod.METHOD_NODE; public class JSONConnectionHandlerManager implements MockTcpServer.TcpServerConnectionHandler { public static final String TAG = LogUtils.makeLogTag(JSONConnectionHandlerManager.class); - private HashMap handlersByType = new HashMap<>(); - private HashSet handlers = new HashSet<>(); - private StringBuffer buffer = new StringBuffer(); + private final HashMap handlersByType = new HashMap<>(); private int amountOfOpenBrackets = 0; private final ObjectMapper objectMapper = new ObjectMapper(); - private int responseCount; + //HashMap used to prevent adding duplicate responses for the same methodId when invoking + //a handler multiple times. + private final HashMap> clientResponses = new HashMap<>(); - private HashMap> clientResponses = new HashMap<>(); + private final HashMap methodIdsHandled = new HashMap<>(); + private final HashSet notificationsHandled = new HashSet<>(); - public interface ConnectionHandler { - /** - * Used to determine which methods the handler implements - * @return list of JSON method names - */ - String[] getType(); - - /** - * Used to get the response from a handler implementing the requested - * method. - * @param method requested method - * @param jsonRequest json node containing the original request - * @return {@link JsonResponse} that should be sent to the client - */ - ArrayList getResponse(String method, ObjectNode jsonRequest); - - /** - * Used to get any notifications from the handler. - * @return {@link JsonResponse} that should be sent to the client or null if there are no notifications - */ - ArrayList getNotifications(); - - /** - * Should set the state of the handler to its initial state - */ - void reset(); - } - - public void addHandler(ConnectionHandler handler) throws Exception { - for(String type : handler.getType()) { - handlersByType.put(type, handler); + public void addHandler(ConnectionHandler handler) { + synchronized (handlersByType) { + for (String type : handler.getType()) { + handlersByType.put(type, handler); + } } - handlers.add(handler); } @Override - public void processInput(char c) { - buffer.append(c); + public void processInput(Socket socket) { + StringBuilder stringBuffer = new StringBuilder(); + + try { + InputStreamReader in = new InputStreamReader(socket.getInputStream()); + int i; + while (!socket.isClosed() && (i = in.read()) != -1) { + stringBuffer.append((char) i); + if (isEndOfJSONStringReached((char) i)) { + processJSONInput(stringBuffer.toString()); + stringBuffer = new StringBuilder(); + } + } + } catch (SocketException e) { + // Socket closed + } catch (IOException e) { + LogUtils.LOGD(TAG, "processInput: error reading from socket: " + socket + + ", buffer holds: " + stringBuffer); + LogUtils.LOGE(TAG, e.getMessage()); + } + } + + /** + * Processes JSON input on individual characters. + * Each iteration should start with an opening accolade { and + * end with a closing accolade to indicate a complete JSON string has been + * fully processed. + * @param c + * @return true if a JSON string was fully processed, false otherwise + */ + private boolean isEndOfJSONStringReached(char c) { + //We simply assume well formed JSON input so it should always start with + //a {. If we need to filter out other input we need to add an additional check + //to detect the first opening accolade. if ( c == '{' ) { amountOfOpenBrackets++; } else if ( c == '}' ) { amountOfOpenBrackets--; } - if ( amountOfOpenBrackets == 0 ) { - String input = buffer.toString(); - buffer = new StringBuffer(); - processJSONInput(input); - } + return amountOfOpenBrackets == 0; } private void processJSONInput(String input) { try { - JsonParser parser = objectMapper.getFactory().createParser(input); - ObjectNode jsonRequest = objectMapper.readTree(parser); + synchronized (clientResponses) { + LogUtils.LOGD(TAG, "processJSONInput: " + input); + JsonParser parser = objectMapper.getFactory().createParser(input); + ObjectNode jsonRequest = objectMapper.readTree(parser); - int methodId = jsonRequest.get(ID_NODE).asInt(); - String method = jsonRequest.get(METHOD_NODE).asText(); - ConnectionHandler connectionHandler = handlersByType.get(method); - if ( connectionHandler != null ) { - ArrayList responses = connectionHandler.getResponse(method, jsonRequest); - if (responses != null) { - addResponse(methodId, responses); + int methodId = jsonRequest.get(ID_NODE).asInt(); + String method = jsonRequest.get(METHOD_NODE).asText(); + + methodIdsHandled.put(String.valueOf(methodId), new MethodPendingState(method)); + + if (clientResponses.get(String.valueOf(methodId)) != null) + return; + + ConnectionHandler connectionHandler = handlersByType.get(method); + if (connectionHandler != null) { + ArrayList responses = connectionHandler.getResponse(method, jsonRequest); + if (responses != null) { + clientResponses.put(String.valueOf(methodId), responses); + } } + + parser.close(); } } catch (IOException e) { - LogUtils.LOGE(getClass().getSimpleName(), e.getMessage()); + LogUtils.LOGD(TAG, "processJSONInput: error parsing: " + input); + LogUtils.LOGE(TAG, e.getMessage()); } } @Override public String getResponse() { - StringBuffer stringBuffer = new StringBuffer(); + StringBuilder stringBuilder = new StringBuilder(); + //Handle client responses synchronized (clientResponses) { - //Handle responses - Collection> jsonResponses = clientResponses.values(); - for (ArrayList arrayList : jsonResponses) { - for (JsonResponse response : arrayList) { - stringBuffer.append(response.toJsonString() + "\n"); + for(Map.Entry> clientResponse : clientResponses.entrySet()) { + for (JsonResponse jsonResponse : clientResponse.getValue()) { + LogUtils.LOGD(TAG, "sending response: " + jsonResponse.toJsonString()); + try { + MethodPendingState methodPending = methodIdsHandled.get(jsonResponse.getId()); + methodPending.handled = true; + stringBuilder.append(jsonResponse.toJsonString()).append("\n"); + } catch (Exception e) { + LogUtils.LOGD(TAG, "getResponse: Error handling response: " + jsonResponse.toJsonString()); + LogUtils.LOGW(TAG, "getResponse: " + e); + } } } clientResponses.clear(); } - //Handle notifications - for(ConnectionHandler handler : handlers) { - ArrayList jsonNotifications = handler.getNotifications(); - if (jsonNotifications != null) { + synchronized (handlersByType) { + //Build a new set to make sure we only handle each handler once, even if it handles + //multiple types. + HashSet uniqueHandlers = new HashSet<>(handlersByType.values()); + + //Handle notifications + for (ConnectionHandler handler : uniqueHandlers) { + ArrayList jsonNotifications = handler.getNotifications(); for (JsonResponse jsonResponse : jsonNotifications) { - stringBuffer.append(jsonResponse.toJsonString() +"\n"); + try { + notificationsHandled.add(jsonResponse.getMethod()); + stringBuilder.append(jsonResponse.toJsonString()).append("\n"); + } catch (Exception e) { + LogUtils.LOGD(TAG, "getResponse: Error handling notification: " + jsonResponse.toJsonString()); + } } } } - responseCount++; - if (stringBuffer.length() > 0) { - return stringBuffer.toString(); + if (stringBuilder.length() > 0) { + return stringBuilder.toString(); } else { return null; } } + public void reset() { + synchronized (clientResponses) { + clearNotificationsHandled(); + clearMethodsHandled(); + clientResponses.clear(); + } + } + + public void clearMethodsHandled() { + methodIdsHandled.clear(); + } + /** * Waits until at least one response has been processed before returning */ - public void waitForNextResponse(long timeOutMillis) throws TimeoutException { - responseCount = 0; - while ((responseCount < 2) && (timeOutMillis > 0)) { + public void waitForMethodHandled(String methodName, long timeOutMillis) throws TimeoutException { + while (! isMethodHandled(methodName) && (timeOutMillis > 0)) { try { Thread.sleep(500); timeOutMillis -= 500; } catch (InterruptedException e) { - + LogUtils.LOGW(TAG, "waitForNextResponse got interrupted"); } } - if (timeOutMillis < 0) + if (timeOutMillis <= 0) + throw new TimeoutException(); + } + + public void clearNotificationsHandled() { + notificationsHandled.clear(); + } + + /** + * Waits until at least one response has been processed before returning + */ + public void waitForNotification(String methodName, long timeOutMillis) throws TimeoutException { + while (! notificationsHandled.contains(methodName) && (timeOutMillis > 0)) { + try { + Thread.sleep(500); + timeOutMillis -= 500; + } catch (InterruptedException e) { + LogUtils.LOGW(TAG, "waitForNextResponse got interrupted"); + } + } + if (timeOutMillis <= 0) throw new TimeoutException(); } private void addResponse(int id, ArrayList jsonResponses) { - ArrayList responses = clientResponses.get(String.valueOf(id)); - if (responses == null) { - responses = new ArrayList<>(); - synchronized (clientResponses) { - clientResponses.put(String.valueOf(id), responses); + + } + + private boolean isMethodHandled(String methodName) { + for(MethodPendingState methodPending : methodIdsHandled.values()) { + if (methodName.contentEquals(methodPending.name)) { + return methodPending.handled; } } - responses.addAll(jsonResponses); + return false; + } + + private void setMethodHandled(String methodId) { + + } + + private static class MethodPendingState { + boolean handled; + String name; + + MethodPendingState(String name) { + this.name = name; + } } } diff --git a/app/src/debug/java/org/xbmc/kore/testutils/tcpserver/handlers/JSONRPCHandler.java b/app/src/debug/java/org/xbmc/kore/testutils/tcpserver/handlers/JSONRPCHandler.java index 9023d6d..c8de164 100644 --- a/app/src/debug/java/org/xbmc/kore/testutils/tcpserver/handlers/JSONRPCHandler.java +++ b/app/src/debug/java/org/xbmc/kore/testutils/tcpserver/handlers/JSONRPCHandler.java @@ -27,7 +27,7 @@ import java.util.ArrayList; /** * Simulates JSON RPC JSON-RPC API */ -public class JSONRPCHandler implements JSONConnectionHandlerManager.ConnectionHandler { +public class JSONRPCHandler extends ConnectionHandler { @Override public String[] getType() { @@ -35,19 +35,9 @@ public class JSONRPCHandler implements JSONConnectionHandlerManager.ConnectionHa } @Override - public ArrayList getResponse(String method, ObjectNode jsonRequest) { + public ArrayList createResponse(String method, ObjectNode jsonRequest) { ArrayList jsonResponses = new ArrayList<>(); jsonResponses.add(new Ping(jsonRequest.get("id").asInt())); return jsonResponses; } - - @Override - public ArrayList getNotifications() { - return null; - } - - @Override - public void reset() { - - } } diff --git a/app/src/debug/java/org/xbmc/kore/testutils/tcpserver/handlers/PlayerHandler.java b/app/src/debug/java/org/xbmc/kore/testutils/tcpserver/handlers/PlayerHandler.java index 5217974..cb4e031 100644 --- a/app/src/debug/java/org/xbmc/kore/testutils/tcpserver/handlers/PlayerHandler.java +++ b/app/src/debug/java/org/xbmc/kore/testutils/tcpserver/handlers/PlayerHandler.java @@ -16,185 +16,189 @@ package org.xbmc.kore.testutils.tcpserver.handlers; -import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ObjectNode; import org.xbmc.kore.jsonrpc.type.GlobalType; import org.xbmc.kore.jsonrpc.type.PlayerType; import org.xbmc.kore.testutils.tcpserver.handlers.jsonrpc.JsonResponse; import org.xbmc.kore.testutils.tcpserver.handlers.jsonrpc.response.methods.Player; +import org.xbmc.kore.testutils.tcpserver.handlers.jsonrpc.response.methods.Playlist; +import org.xbmc.kore.testutils.tcpserver.handlers.jsonrpc.response.notifications.Player.OnAVStart; import org.xbmc.kore.testutils.tcpserver.handlers.jsonrpc.response.notifications.Player.OnPause; import org.xbmc.kore.testutils.tcpserver.handlers.jsonrpc.response.notifications.Player.OnPlay; import org.xbmc.kore.testutils.tcpserver.handlers.jsonrpc.response.notifications.Player.OnPropertyChanged; import org.xbmc.kore.testutils.tcpserver.handlers.jsonrpc.response.notifications.Player.OnSeek; import org.xbmc.kore.testutils.tcpserver.handlers.jsonrpc.response.notifications.Player.OnSpeedChanged; +import org.xbmc.kore.testutils.tcpserver.handlers.jsonrpc.response.notifications.Player.OnStop; import org.xbmc.kore.utils.LogUtils; +import java.io.IOException; import java.util.ArrayList; +import java.util.List; -import static org.xbmc.kore.testutils.tcpserver.handlers.PlayerHandler.TYPE.MUSIC; +import static org.xbmc.kore.testutils.tcpserver.handlers.PlayerHandler.PLAY_STATE.PAUSED; +import static org.xbmc.kore.testutils.tcpserver.handlers.PlayerHandler.PLAY_STATE.PLAYING; +import static org.xbmc.kore.testutils.tcpserver.handlers.PlayerHandler.PLAY_STATE.STOPPED; /** * Simulates Player JSON-RPC API */ -public class PlayerHandler implements JSONConnectionHandlerManager.ConnectionHandler { +public class PlayerHandler extends ConnectionHandler { private static final String TAG = LogUtils.makeLogTag(PlayerHandler.class); - public enum TYPE { - MUSIC, - MOVIE, - EPISODE, - MUSICVIDEO, - UNKNOWN, - PICTURE, - CHANNEL - } - public static String[] repeatModes = { - "off", - "one", - "all" + "off", + "one", + "all" }; + public enum PLAY_STATE {PLAYING, STOPPED, PAUSED} + private PLAY_STATE playState = STOPPED; private int currentRepeatMode; private boolean shuffled; - private boolean playing; - private int position; - private long totalTimeSec = 240; // default value + private int elapsedTime; - private TYPE mediaType = MUSIC; - - private Player.GetItem mediaItem = createSongItem(); + private Player.GetItem mediaItem; + private List playlists = new ArrayList<>(); + private Playlist.playlistID activePlaylistId = Playlist.playlistID.AUDIO; private String playerType = PlayerType.GetActivePlayersReturnType.AUDIO; - private ArrayList notifications = new ArrayList<>(); - - @Override - public ArrayList getNotifications() { - ArrayList list = new ArrayList<>(notifications); - notifications.clear(); - return list; - } - @Override public void reset() { + super.reset(); this.shuffled = false; this.currentRepeatMode = 0; - this.position = 0; - this.playing = false; - setMediaType(MUSIC); + this.elapsedTime = 0; + this.playState = STOPPED; + playerType = PlayerType.GetActivePlayersReturnType.AUDIO; + playlists.clear(); + setMediaType(Player.GetItem.TYPE.unknown); } @Override public String[] getType() { return new String[] {Player.GetActivePlayers.METHOD_NAME, - Player.GetProperties.METHOD_NAME, - Player.GetItem.METHOD_NAME, - Player.SetRepeat.METHOD_NAME, - Player.SetShuffle.METHOD_NAME, - Player.Seek.METHOD_NAME, - Player.PlayPause.METHOD_NAME}; + Player.GetProperties.METHOD_NAME, + Player.GetItem.METHOD_NAME, + Player.SetRepeat.METHOD_NAME, + Player.SetShuffle.METHOD_NAME, + Player.Seek.METHOD_NAME, + Player.PlayPause.METHOD_NAME, + Player.Stop.METHOD_NAME, + Player.Open.METHOD_NAME}; } @Override - public ArrayList getResponse(String method, ObjectNode jsonRequest) { - LogUtils.LOGD(TAG, "getResponse: method="+method); - + public ArrayList createResponse(String method, ObjectNode jsonRequest) { ArrayList jsonResponses = new ArrayList<>(); - JsonNode node = jsonRequest.get("id"); JsonResponse response = null; - int playerId; + + int methodId = jsonRequest.get("id").asInt(); + switch (method) { case Player.GetActivePlayers.METHOD_NAME: - response = new Player.GetActivePlayers(node.asInt(), 0, playerType); + response = handleGetActivePlayers(methodId); break; case Player.GetProperties.METHOD_NAME: - response = updatePlayerProperties(createPlayerProperties(node.asInt())); + response = updatePlayerProperties(createPlayerProperties(methodId)); break; case Player.GetItem.METHOD_NAME: - mediaItem.setMethodId(node.asInt()); - response = mediaItem; + response = handleGetItem(methodId); break; case Player.SetRepeat.METHOD_NAME: - response = new Player.SetRepeat(node.asInt(), "OK"); - playerId = jsonRequest.get("params").get("playerid").asInt(); - currentRepeatMode = ++currentRepeatMode % 3; - notifications.add(new OnPropertyChanged(repeatModes[currentRepeatMode], null, playerId)); + response = handleSetRepeat(methodId, jsonRequest); break; case Player.SetShuffle.METHOD_NAME: - response = new Player.SetShuffle(node.asInt(), "OK"); - playerId = jsonRequest.get("params").get("playerid").asInt(); - shuffled = !shuffled; - notifications.add(new OnPropertyChanged(null, shuffled, playerId)); + response = handleSetShuffle(methodId, jsonRequest); + break; + case Player.Open.METHOD_NAME: + response = handleOpen(methodId, jsonRequest); break; case Player.PlayPause.METHOD_NAME: - playing = !playing; - int speed = playing ? 1 : 0; - response = new Player.PlayPause(node.asInt(), speed); - playerId = jsonRequest.get("params").get("playerid").asInt(); - if (playing) - notifications.add(new OnPlay(1580, getMediaItemType(), playerId, speed)); - else - notifications.add(new OnPause(1580, getMediaItemType(), playerId, speed)); - notifications.add(new OnSpeedChanged(1580, getMediaItemType(), playerId, speed)); + response = handlePlayPause(methodId, jsonRequest); break; case Player.Seek.METHOD_NAME: - position = new GlobalType.Time(jsonRequest.get("params").get("value")).ToSeconds(); - response = new Player.Seek(node.asInt(), (100 * position) / (double) totalTimeSec, position, - totalTimeSec); - playerId = jsonRequest.get("params").get("playerid").asInt(); - - notifications.add(new OnSeek(node.asInt(), getMediaItemType(), playerId, - playing ? 1 : 0, 0, position)); + response = handleSeek(methodId, jsonRequest); break; + case Player.Stop.METHOD_NAME: + handleStop(); + break; + default: + LogUtils.LOGD(TAG, "getResponse: unknown method received: "+method); } - jsonResponses.add(response); + if (response != null) + jsonResponses.add(response); return jsonResponses; } - /** - * Sets the returned media type - * @param mediaType - */ - public void setMediaType(TYPE mediaType) { + private void setMediaType(Player.GetItem.TYPE mediaType) { switch (mediaType) { - case MOVIE: - mediaItem = createMovieItem(); + case movie: playerType = PlayerType.GetActivePlayersReturnType.VIDEO; break; - case MUSIC: - mediaItem = createSongItem(); + case song: playerType = PlayerType.GetActivePlayersReturnType.AUDIO; break; - case UNKNOWN: - mediaItem = createUnknownItem(); + case unknown: playerType = PlayerType.GetActivePlayersReturnType.AUDIO; break; - case MUSICVIDEO: - mediaItem = createMusicVideoItem(); + case musicvideo: playerType = PlayerType.GetActivePlayersReturnType.VIDEO; break; - case PICTURE: - mediaItem = createPictureItem(); + case picture: playerType = PlayerType.GetActivePlayersReturnType.PICTURE; break; - case CHANNEL: - mediaItem = createChannelItem(); + case channel: playerType = PlayerType.GetActivePlayersReturnType.VIDEO; break; } } + /** + * Starts playing current item in the playlist + */ public void startPlay() { - OnPlay onPlay = new OnPlay(1580, getMediaItemType(), 0, 1); - notifications.add(onPlay); - playing = true; + if (playlists.size() > 0 && activePlaylistId != null) { + mediaItem = playlists.get(activePlaylistId.ordinal()).getCurrentItem(); + + if (mediaItem != null) { + setMediaType(Player.GetItem.TYPE.valueOf(getMediaItemType())); + + addNotification(new OnPlay(mediaItem.getLibraryId(), getMediaItemType(), getPlayerId(), 1)); + addNotification(new OnAVStart(mediaItem.getLibraryId(), getMediaItemType(), getPlayerId(), 1)); + if (playState == PAUSED) { + addNotification(new OnSpeedChanged(mediaItem.getLibraryId(), getMediaItemType(), getPlayerId(), 1)); + } + + playState = PLAYING; + } + } + } + + public void startPlay(Playlist.playlistID playlistId, int playlistPosition) { + activePlaylistId = playlistId; + + PlaylistHolder playlistHolder = playlists.get(playlistId.ordinal()); + playlistHolder.setPlaylistIndex(playlistPosition); + + startPlay(); + } + + public void stopPlay() { + handleStop(); + addNotification(new OnStop(mediaItem.getLibraryId(), getMediaItemType(), false)); + this.playState = STOPPED; + mediaItem = null; + } + + public void setPlaylists(List playlists) { + this.playlists = playlists; } /** - * Returns the current media item for the media type set through {@link #setMediaType(TYPE)} + * Returns the current media item for the media type set through {@link #setMediaType(Player.GetItem.TYPE)} * @return */ public Player.GetItem getMediaItem() { @@ -205,168 +209,151 @@ public class PlayerHandler implements JSONConnectionHandlerManager.ConnectionHan * Returns the play position of the current media item * @return the time elapsed in seconds */ - public long getPosition() { - return position; + public long getTimeElapsed() { + return elapsedTime; } - public boolean isPlaying() { - return playing; - } - - public void setTotalTimeSec(long totalTimeSec) { - this.totalTimeSec = totalTimeSec; + public PLAY_STATE getPlayState() { + return playState; } private String getMediaItemType() { - switch (mediaType) { - case MOVIE: - return OnPlay.TYPE_MOVIE; - case MUSIC: - return OnPlay.TYPE_SONG; - case UNKNOWN: - return OnPlay.TYPE_UNKNOWN; - case MUSICVIDEO: - return OnPlay.TYPE_MUSICVIDEO; - case PICTURE: - return OnPlay.TYPE_PICTURE; - case CHANNEL: - return OnPlay.TYPE_MOVIE; + return mediaItem.getType(); + } + + private int getPlayerId() { + switch (playerType) { + case PlayerType.GetActivePlayersReturnType.VIDEO: + return 0; + case PlayerType.GetActivePlayersReturnType.AUDIO: + return 1; + case PlayerType.GetActivePlayersReturnType.PICTURE: + return 2; default: - return OnPlay.TYPE_SONG; + return 1; } } private Player.GetProperties updatePlayerProperties(Player.GetProperties playerProperties) { - if (playing) - position++; + if (playState == PLAYING) + elapsedTime++; - if ( ( position > totalTimeSec ) && currentRepeatMode != 0 ) - position = 0; + if ( mediaItem != null ) { + if ( elapsedTime > mediaItem.getDuration() && currentRepeatMode != 0 ) { + elapsedTime = 0; + } - playerProperties.addPosition(position); - playerProperties.addPercentage((int) ((position * 100 ) / totalTimeSec)); - playerProperties.addTime(0, 0, position, 767); + playerProperties.addPercentage((elapsedTime * 100 ) / mediaItem.getDuration()); + } + + playerProperties.addPosition(elapsedTime); + playerProperties.addTime(0, 0, elapsedTime, 767); playerProperties.addShuffled(shuffled); playerProperties.addRepeat(repeatModes[currentRepeatMode]); + playerProperties.addPlaylistId(activePlaylistId.ordinal()); + return playerProperties; } private Player.GetProperties createPlayerProperties(int id) { Player.GetProperties properties = new Player.GetProperties(id); - properties.addPlaylistId(0); + properties.addPlaylistId(activePlaylistId.ordinal()); properties.addRepeat(repeatModes[currentRepeatMode]); properties.addShuffled(false); - properties.addSpeed(playing ? 1 : 0); - properties.addTotaltime(0,0,240,41); + properties.addSpeed(playState == PLAYING ? 1 : 0); + + int duration = mediaItem != null ? mediaItem.getDuration() : 0; + int hours = duration / 3600; + int remainder = (duration - (hours * 3600)); + int minutes = remainder / 60; + int seconds = remainder - (minutes * 60); + properties.addTotaltime(hours,minutes, seconds,0); + return properties; } - private Player.GetItem createSongItem() { - Player.GetItem item = new Player.GetItem(); - item.addAlbum("My Time Is The Right Time"); - item.addAlbumArtist("Alton Ellis"); - item.addArtist("Alton Ellis"); - item.addDisplayartist("Alton Ellis"); - item.addDuration(240); - item.addFile("/Users/martijn/Projects/dummymediafiles/media/music/Alton Ellis/My Time Is The Right Time/11-I Can't Stand It.mp3"); - item.addGenre("Reggae"); - item.addLabel("I Can't Stand It"); - item.addRating(0); - item.addTitle("I Can't Stand It"); - item.addTrack(11); - item.addType(Player.GetItem.TYPE.SONG); - item.addYear(2000); + private JsonResponse handleGetItem(int methodId) { + if (playlists.size() > 0) { + mediaItem = playlists.get(activePlaylistId.ordinal()).getCurrentItem(); + } - return item; + try { + mediaItem = new Player.GetItem(methodId, mediaItem.toJsonString()); + } catch (IOException e) { + LogUtils.LOGE(TAG, "handleGetItem: Error creating new Player.GetItem object"); + } + return mediaItem; } - private Player.GetItem createMovieItem() { - Player.GetItem item = new Player.GetItem(); - item.addTitle("Elephants Dream"); - item.addCast("", "Cas Jansen", "Emo"); - item.addCast("", "Tygo Gernandt", "Proog"); - item.addDuration(660); - item.addFile("/Users/martijn/Projects/dummymediafiles/media/movies/Elephants Dream (2006).mp4"); - item.addGenre("Animation"); - item.addRating(0); - item.addType(Player.GetItem.TYPE.MOVIE); - item.addYear(2006); - - return item; + private JsonResponse handleGetActivePlayers(int methodId) { + if (playState == STOPPED) { + return new Player.GetActivePlayers(methodId); + } else { + return new Player.GetActivePlayers(methodId, getPlayerId(), playerType); + } } - private Player.GetItem createEpisodeItem() { - Player.GetItem item = new Player.GetItem(); - item.addShowtitle("According to Jim"); - item.addCast("image://http%3a%2f%2fthetvdb.com%2fbanners%2factors%2f41995.jpg/", "James Belushi", "Jim"); - item.addCast("image://http%3a%2f%2fthetvdb.com%2fbanners%2factors%2f41994.jpg/", "Courtney Thorne-Smith", "Cheryl"); - item.addDuration(1800); - item.addFile("/Users/martijn/Projects/dummymediafiles/media/movies/Elephants Dream (2006).mp4"); - item.addGenre("Comedy"); - item.addRating(7); - item.addType(Player.GetItem.TYPE.EPISODE); - item.addFirstaired("2001-10-03"); - item.addEpisode(1); - item.addSeason(1); - item.addDirector("Andy Cadiff"); - item.addTitle("Pilot"); - return item; + private JsonResponse handleSetRepeat(int methodId, ObjectNode jsonRequest) { + int playerId = getPlayerIdFromJsonRequest(jsonRequest); + currentRepeatMode = ++currentRepeatMode % 3; + addNotification(new OnPropertyChanged(repeatModes[currentRepeatMode], null, playerId)); + return new Player.SetRepeat(methodId, "OK"); } - private Player.GetItem createMusicVideoItem() { - Player.GetItem item = new Player.GetItem(); - item.addType(Player.GetItem.TYPE.MUSICVIDEO); - item.addAlbum("...Baby One More Time"); - item.addDirector("Nigel Dick"); - item.addThumbnail("image://http%3a%2f%2fwww.theaudiodb.com%2fimages%2fmedia%2falbum%2fthumb%2fbaby-one-more-time-4dcff7453745a.jpg/"); - item.addYear(1999); - item.addTitle("(You Drive Me) Crazy"); - item.addLabel("(You Drive Me) Crazy"); - item.addRuntime(12); - item.addGenre("Pop"); - item.addPremiered("1999-01-01"); - return item; + private JsonResponse handleSetShuffle(int methodId, ObjectNode jsonRequest) { + int playerId = getPlayerIdFromJsonRequest(jsonRequest); + shuffled = !shuffled; + addNotification(new OnPropertyChanged(null, shuffled, playerId)); + return new Player.SetShuffle(methodId, "OK"); } - private Player.GetItem createChannelItem() { - Player.GetItem item = new Player.GetItem(); - item.addShowtitle("According to Jim"); - item.addCast("image://http%3a%2f%2fthetvdb.com%2fbanners%2factors%2f41995.jpg/", "James Belushi", "Jim"); - item.addCast("image://http%3a%2f%2fthetvdb.com%2fbanners%2factors%2f41994.jpg/", "Courtney Thorne-Smith", "Cheryl"); - item.addDuration(1800); - item.addFile("/Users/martijn/Projects/dummymediafiles/media/movies/Elephants Dream (2006).mp4"); - item.addGenre("Comedy"); - item.addRating(7); - item.addType(Player.GetItem.TYPE.EPISODE); - item.addFirstaired("2001-10-03"); - item.addEpisode(1); - item.addSeason(1); - item.addDirector("Andy Cadiff"); - item.addTitle("Pilot"); - item.addType(Player.GetItem.TYPE.CHANNEL); + private JsonResponse handleOpen(int methodId, ObjectNode jsonRequest) { + int playlistId = jsonRequest.get("params").get("item").get("playlistid").asInt(); + int playlistIndex = jsonRequest.get("params").get("item").get("position").asInt(); - return item; + startPlay(Playlist.playlistID.values()[playlistId], playlistIndex); + + return new Player.Open(methodId); } - private Player.GetItem createUnknownItem() { - Player.GetItem item = new Player.GetItem(); - item.addTitle("Dumpert"); - item.addCast("", "Martijn Kaiser", "himself"); - item.addCast("", "", "Skipmode A1"); - item.addCast("", "", "Sparkline"); - item.addGenre("Addon"); - item.addType(Player.GetItem.TYPE.UNKNOWN); + private JsonResponse handlePlayPause(int methodId, ObjectNode jsonRequest) { + playState = playState == PLAYING ? PAUSED : PLAYING; //toggle playstate - return item; + int speed = playState == PLAYING ? 1 : 0; + int itemId = mediaItem.getLibraryId(); + int playerId = getPlayerIdFromJsonRequest(jsonRequest); + + if (playState == PLAYING) + addNotification(new OnPlay(itemId, getMediaItemType(), playerId, speed)); + else + addNotification(new OnPause(itemId, getMediaItemType(), playerId, speed)); + + addNotification(new OnSpeedChanged(itemId, getMediaItemType(), playerId, speed)); + + return new Player.PlayPause(methodId, speed); } - private Player.GetItem createPictureItem() { - Player.GetItem item = new Player.GetItem(); - item.addTitle("Kore Artwork"); - item.addFile("/Users/martijn/Projects/Kore/art/screenshots/Kore_Artwork_Concept_2.png"); - item.addType(Player.GetItem.TYPE.PICTURE); - return item; + private JsonResponse handleSeek(int methodId, ObjectNode jsonRequest) { + if (mediaItem == null) + return new Player.Seek(methodId, 0, 0, 0); + + elapsedTime = new GlobalType.Time(jsonRequest.get("params").get("value")).ToSeconds(); + int playerId = getPlayerIdFromJsonRequest(jsonRequest); + + addNotification(new OnSeek(methodId, getMediaItemType(), playerId, + playState == PLAYING ? 1 : 0, 0, elapsedTime)); + return new Player.Seek(methodId, (100 * elapsedTime) / (double) mediaItem.getDuration(), + elapsedTime, mediaItem.getDuration()); + } + + private void handleStop() { + addNotification(new OnStop(mediaItem.getLibraryId(), getMediaItemType(), false)); + playState = STOPPED; + } + + private int getPlayerIdFromJsonRequest(ObjectNode jsonRequest) { + return jsonRequest.get("params").get("playerid").asInt(); } } diff --git a/app/src/debug/java/org/xbmc/kore/testutils/tcpserver/handlers/PlaylistHandler.java b/app/src/debug/java/org/xbmc/kore/testutils/tcpserver/handlers/PlaylistHandler.java new file mode 100644 index 0000000..3beb723 --- /dev/null +++ b/app/src/debug/java/org/xbmc/kore/testutils/tcpserver/handlers/PlaylistHandler.java @@ -0,0 +1,131 @@ +/* + * Copyright 2018 Martijn Brekhof. All rights reserved. + * + * 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 org.xbmc.kore.testutils.tcpserver.handlers; + +import com.fasterxml.jackson.databind.node.ObjectNode; + +import org.xbmc.kore.testutils.tcpserver.handlers.jsonrpc.JsonResponse; +import org.xbmc.kore.testutils.tcpserver.handlers.jsonrpc.response.methods.Player; +import org.xbmc.kore.testutils.tcpserver.handlers.jsonrpc.response.methods.Playlist; +import org.xbmc.kore.testutils.tcpserver.handlers.jsonrpc.response.notifications.Playlist.OnAdd; +import org.xbmc.kore.testutils.tcpserver.handlers.jsonrpc.response.notifications.Playlist.OnClear; +import org.xbmc.kore.utils.LogUtils; + +import java.util.ArrayList; +import java.util.List; + +/** + * Simulates Playlist JSON-RPC API + */ +public class PlaylistHandler extends ConnectionHandler { + private static final String TAG = LogUtils.makeLogTag(PlaylistHandler.class); + + private static final String ID_NODE = "id"; + private static final String PARAMS_NODE = "params"; + private static final String PLAYLISTID_NODE = "playlistid"; + + private ArrayList playlists = new ArrayList<>(); + + @Override + public void reset() { + playlists.clear(); + } + + @Override + public String[] getType() { + return new String[]{Playlist.GetItems.METHOD_NAME, Playlist.GetPlaylists.METHOD_NAME}; + } + + @Override + public ArrayList createResponse(String method, ObjectNode jsonRequest) { + ArrayList jsonResponses = new ArrayList<>(); + + + int methodId = jsonRequest.get(ID_NODE).asInt(-1); + + switch (method) { + case Playlist.GetItems.METHOD_NAME: + int playlistId = jsonRequest.get(PARAMS_NODE).get(PLAYLISTID_NODE).asInt(-1); + jsonResponses.add(createPlaylist(methodId, playlistId)); + break; + case Playlist.GetPlaylists.METHOD_NAME: + jsonResponses.add(new Playlist.GetPlaylists(methodId)); + break; + default: + LogUtils.LOGD(TAG, "method: " + method + ", not implemented"); + } + return jsonResponses; + } + + private Playlist.GetItems createPlaylist(int methodId, int playlistId) { + Playlist.GetItems playlistGetItems = new Playlist.GetItems(methodId); + + if (playlists.size() > playlistId) { + for (Player.GetItem getItem : playlists.get(playlistId).getItems()) { + playlistGetItems.addItem(getItem); + } + } + + return playlistGetItems; + } + + public ArrayList getPlaylists() { + return playlists; + } + + public List getPlaylist(Playlist.playlistID id) { + int playlistId = id.ordinal(); + + if (playlistId < playlists.size()) + return playlists.get(playlistId).getItems(); + else + return null; + } + + /** + * Clears the playlist and sends the OnClear notification + */ + public void clearPlaylist(Playlist.playlistID id) { + int playlistId = id.ordinal(); + + if (playlistId >= playlists.size()) + return; + + OnClear onClearNotification = new OnClear(playlistId); + addNotification(onClearNotification); + + playlists.get(playlistId).clear(); + } + + public void addItemToPlaylist(Playlist.playlistID id, Player.GetItem item) { + int playlistId = id.ordinal(); + + while (playlists.size() <= playlistId) { + playlists.add(null); + } + + PlaylistHolder playlist = playlists.get(playlistId); + if (playlist == null) { + playlist = new PlaylistHolder(playlistId); + playlists.set(playlistId, playlist); + } + playlist.add(item); + + OnAdd onAddNotification = new OnAdd(item.getLibraryId(), item.getType(), playlistId, playlist.getIndexOf(item)); + addNotification(onAddNotification); + } +} diff --git a/app/src/debug/java/org/xbmc/kore/testutils/tcpserver/handlers/PlaylistHolder.java b/app/src/debug/java/org/xbmc/kore/testutils/tcpserver/handlers/PlaylistHolder.java new file mode 100644 index 0000000..679c74d --- /dev/null +++ b/app/src/debug/java/org/xbmc/kore/testutils/tcpserver/handlers/PlaylistHolder.java @@ -0,0 +1,55 @@ +package org.xbmc.kore.testutils.tcpserver.handlers; + +import org.xbmc.kore.testutils.tcpserver.handlers.jsonrpc.response.methods.Player; + +import java.util.ArrayList; +import java.util.List; + +public class PlaylistHolder { + private int id; + private List items = new ArrayList<>(); + private int currentIndex; + + PlaylistHolder(int id) { + this.id = id; + } + + public int getId() { + return id; + } + + public void clear() { + id = 0; + currentIndex = 0; + items.clear(); + } + + public void add(Player.GetItem item) { + items.add(item); + } + + public List getItems() { + return items; + } + + public int getIndexOf(Player.GetItem item) { + return items.indexOf(item); + } + + public Player.GetItem getCurrentItem() { + return items.get(currentIndex); + } + + public int getPlaylistSize() { + return items.size(); + } + + public void setPlaylistIndex(int index) { + currentIndex = index; + + if (currentIndex < 0) + currentIndex = 0; + else if (currentIndex >= items.size()) + currentIndex = getPlaylistSize() - 1; + } +} diff --git a/app/src/debug/java/org/xbmc/kore/testutils/tcpserver/handlers/jsonrpc/JsonResponse.java b/app/src/debug/java/org/xbmc/kore/testutils/tcpserver/handlers/jsonrpc/JsonResponse.java index f07fbc8..bc5e0e5 100644 --- a/app/src/debug/java/org/xbmc/kore/testutils/tcpserver/handlers/jsonrpc/JsonResponse.java +++ b/app/src/debug/java/org/xbmc/kore/testutils/tcpserver/handlers/jsonrpc/JsonResponse.java @@ -133,6 +133,10 @@ public abstract class JsonResponse { return data; } + protected void setResultToResponse(JsonNode value) { + jsonResponse.set(RESULT_NODE, value); + } + protected void setResultToResponse(boolean value) { jsonResponse.put(RESULT_NODE, value); } @@ -145,6 +149,15 @@ public abstract class JsonResponse { jsonResponse.put(RESULT_NODE, value); } + protected void setLimits(int start, int end, int total) { + ObjectNode limits = createObjectNode(); + limits.put("start", start); + limits.put("end", end); + limits.put("total", total); + + ((ObjectNode) getResultNode(TYPE.OBJECT)).set("limits", limits); + } + /** * Adds the value to the array in node with the given key. * If the array does not exist it will be created @@ -189,6 +202,20 @@ public abstract class JsonResponse { } } + protected void addToArrayNode(ObjectNode node, String key, JsonNode value) { + JsonNode jsonNode = node.get(key); + if (jsonNode == null) { + jsonNode = objectMapper.createArrayNode(); + node.set(key, jsonNode); + } + + if (jsonNode.isArray()) { + ((ArrayNode) jsonNode).add(value); + } else { + LogUtils.LOGE(TAG, "JsonNode at key: " + key + " not of type ArrayNode." ); + } + } + protected void addDataToResponse(String parameter, boolean value) { getDataNode().put(parameter, value); } @@ -213,6 +240,18 @@ public abstract class JsonResponse { return jsonResponse; } + public JsonNode getResultNode() { + return jsonResponse.get(RESULT_NODE); + } + + public String getId() { + return jsonResponse.get(ID_NODE).asText(); + } + + public String getMethod() { + return jsonResponse.get(METHOD_NODE).asText(); + } + public String toJsonString() { return jsonResponse.toString(); } diff --git a/app/src/debug/java/org/xbmc/kore/testutils/tcpserver/handlers/jsonrpc/response/methods/Player.java b/app/src/debug/java/org/xbmc/kore/testutils/tcpserver/handlers/jsonrpc/response/methods/Player.java index 724ef0e..e45e197 100644 --- a/app/src/debug/java/org/xbmc/kore/testutils/tcpserver/handlers/jsonrpc/response/methods/Player.java +++ b/app/src/debug/java/org/xbmc/kore/testutils/tcpserver/handlers/jsonrpc/response/methods/Player.java @@ -20,7 +20,6 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.ObjectNode; -import org.xbmc.kore.jsonrpc.type.GlobalType; import org.xbmc.kore.jsonrpc.type.PlayerType; import org.xbmc.kore.testutils.tcpserver.handlers.jsonrpc.JsonResponse; import org.xbmc.kore.testutils.tcpserver.handlers.jsonrpc.JsonUtils; @@ -29,11 +28,31 @@ import org.xbmc.kore.testutils.tcpserver.handlers.jsonrpc.nodes.SubtitleDetailsN import org.xbmc.kore.testutils.tcpserver.handlers.jsonrpc.nodes.VideoDetailsNode; import org.xbmc.kore.utils.LogUtils; +import java.io.IOException; + /** * Serverside JSON RPC responses in Methods.Player.* */ public class Player { + /** + * JSON response for Player.Open request + * + * Example: + * Query: {"jsonrpc":"2.0","method":"Player.Open","id":77,"params":{"item":{"playlistid":0,"position":2}}} + * Answer: {"id":77,"jsonrpc":"2.0","result":"OK"} + * + * @return JSON string + */ + public static class Open extends JsonResponse { + public final static String METHOD_NAME = "Player.Open"; + + public Open(int methodId) { + super(methodId); + setResultToResponse("OK"); + } + } + /** * JSON response for Player.Seek request * @@ -82,9 +101,18 @@ public class Player { } } + public static class Stop extends JsonResponse { + public final static String METHOD_NAME = "Player.Stop"; + } + public static class GetActivePlayers extends JsonResponse { public final static String METHOD_NAME = "Player.GetActivePlayers"; + public GetActivePlayers(int methodId) { + super(methodId); + getResultNode(TYPE.ARRAY); + } + public GetActivePlayers(int methodId, int playerId, String type) { super(methodId); ObjectNode objectNode = createObjectNode(); @@ -94,6 +122,7 @@ public class Player { } } + public static class GetProperties extends JsonResponse { public final static String METHOD_NAME = "Player.GetProperties"; @@ -199,6 +228,11 @@ public class Player { } } + /** + * Example: + * query: {"jsonrpc":"2.0","method":"Player.GetItem","id":4119,"params":{"playerid":0,"properties":["art","artist","albumartist","album","cast","director","displayartist","duration","episode","fanart","file","firstaired","genre","imdbnumber","plot","premiered","rating","resume","runtime","season","showtitle","streamdetails","studio","tagline","thumbnail","title","top250","track","votes","writer","year","description"]}} + * answer: {"id":4119,"jsonrpc":"2.0","result":{"item":{"album":"My Time Is the Right Time","albumartist":["Alton Ellis"],"art":{"artist.fanart":"image://http%3a%2f%2fmedia.theaudiodb.com%2fimages%2fmedia%2fartist%2ffanart%2fxpptss1381301172.jpg/"},"artist":["Alton Ellis"],"displayartist":"Alton Ellis","duration":5,"fanart":"image://http%3a%2f%2fmedia.theaudiodb.com%2fimages%2fmedia%2fartist%2ffanart%2fxpptss1381301172.jpg/","file":"/Users/martijn/Projects/dummymediafiles/media/music/Alton Ellis/My Time Is The Right Time/06-Rock Steady.mp3","genre":["Reggae"],"id":14769,"label":"Rock Steady","rating":0,"thumbnail":"","title":"Rock Steady","track":6,"type":"song","votes":0,"year":2000}}} + */ public static class GetItem extends JsonResponse { public final static String METHOD_NAME = "Player.GetItem"; @@ -238,62 +272,64 @@ public class Player { final static String DESCRIPTION = "description"; final static String LABEL = "label"; - public enum TYPE { UNKNOWN, - MOVIE, - EPISODE, - MUSICVIDEO, - SONG, - PICTURE, - CHANNEL + public enum TYPE { unknown, + movie, + episode, + musicvideo, + song, + picture, + channel } private ObjectNode itemNode; public GetItem() { super(); - ObjectNode resultNode = ((ObjectNode) getResultNode(JsonResponse.TYPE.OBJECT)); - itemNode = createObjectNode(); - resultNode.set(ITEM, itemNode); + setupItemNode(); } public GetItem(int methodId) { super(methodId); + setupItemNode(); + } + + public GetItem(int methodId, String jsonString) throws IOException { + super(methodId, jsonString); + ObjectNode resultNode = ((ObjectNode) getResultNode(JsonResponse.TYPE.OBJECT)); + if (resultNode.has(ITEM)) { + itemNode = (ObjectNode) resultNode.get(ITEM); + } else { + setupItemNode(); + } + } + + private void setupItemNode() { ObjectNode resultNode = ((ObjectNode) getResultNode(JsonResponse.TYPE.OBJECT)); itemNode = createObjectNode(); resultNode.set(ITEM, itemNode); } - public void setMethodId(int methodId) { - getResponseNode().put(ID_NODE, methodId); + public void addLibraryId(int id) { + itemNode.put(ID_NODE, id); + } + + /** + * @return library identifier or -1 if not set + */ + public int getLibraryId() { + JsonNode idNode = itemNode.get(ID_NODE); + if (idNode != null) + return idNode.asInt(); + else + return -1; } public void addType(TYPE type) { - String strType; - switch (type) { - case MOVIE: - strType = "movie"; - break; - case EPISODE: - strType = "episode"; - break; - case MUSICVIDEO: - strType = "musicvideo"; - break; - case SONG: - strType = "song"; - break; - case PICTURE: - strType = "picture"; - break; - case CHANNEL: - strType = "channel"; - break; - case UNKNOWN: - default: - strType = "unknown"; - break; - } - itemNode.put(TYPE, strType); + itemNode.put(TYPE, type.name()); + } + + public String getType() { + return itemNode.get(TYPE).textValue(); } public void addArt(String banner, String poster, String fanart, String thumbnail) { @@ -329,6 +365,10 @@ public class Player { itemNode.put(DURATION, duration); } + public int getDuration() { + return itemNode.get(DURATION).asInt(); + } + public void addEpisode(int episode) { itemNode.put(EPISODE, episode); } @@ -369,6 +409,10 @@ public class Player { itemNode.putObject(RESUME).setAll(createResumeNode(position, total)); } + public int getRuntime() { + return itemNode.get(RUNTIME).asInt(); + } + public void addRuntime(int runtime) { itemNode.put(RUNTIME, runtime); } diff --git a/app/src/debug/java/org/xbmc/kore/testutils/tcpserver/handlers/jsonrpc/response/methods/Playlist.java b/app/src/debug/java/org/xbmc/kore/testutils/tcpserver/handlers/jsonrpc/response/methods/Playlist.java new file mode 100644 index 0000000..3d4ff81 --- /dev/null +++ b/app/src/debug/java/org/xbmc/kore/testutils/tcpserver/handlers/jsonrpc/response/methods/Playlist.java @@ -0,0 +1,120 @@ +/* + * Copyright 2018 Martijn Brekhof. All rights reserved. + * + * 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 org.xbmc.kore.testutils.tcpserver.handlers.jsonrpc.response.methods; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; + +import org.xbmc.kore.testutils.tcpserver.handlers.jsonrpc.JsonResponse; + +/** + * Serverside JSON RPC responses in Playlist.* + */ +public class Playlist { + + public enum playlistID { + AUDIO, VIDEO, PICTURE + } + + /** + * JSON response for Playlist.GetItems request + * + * * Example: + * Query: {"jsonrpc":"2.0","method":"Playlist.GetItems","id":48,"params": + * {"playlistid":0,"properties":["art","artist","albumartist","album", + * "displayartist","episode","fanart","file","season", + * "showtitle","studio","tagline","thumbnail","title", + * "track","duration","runtime"] + * } + * } + * Answer: {"id":1,"jsonrpc":"2.0","result":{"items": + * [ + * {"album":"My Time Is the Right Time", + * "albumartist":[], + * "art":{"artist.fanart":"image://http%3a%2f%2fmedia.theaudiodb.com%2fimages%2fmedia%2fartist%2ffanart%2fxpptss1381301172.jpg/"}, + * "artist":["Alton Ellis"], + * "displayartist":"Alton Ellis", + * "duration":5, + * "fanart":"image://http%3a%2f%2fmedia.theaudiodb.com%2fimages%2fmedia%2fartist%2ffanart%2fxpptss1381301172.jpg/", + * "file":"/Users/martijn/Projects/dummymediafiles/media/music/Alton Ellis/My Time Is The Right Time/17-Black Man's Word.mp3", + * "id":41, + * "label":"Black Man's Word", + * "thumbnail":"", + * "title":"Black Man's Word", + * "track":17, + * "type":"song"} + * ], + * "limits":{"end":1,"start":0,"total":1}}} + * + * Playlist empty answer : {"id":48,"jsonrpc":"2.0","result":{"limits":{"end":0,"start":0,"total":0}}} + * + * @return JSON string + */ + public static class GetItems extends JsonResponse { + public final static String METHOD_NAME = "Playlist.GetItems"; + + int limitsEnd; + + public GetItems(int id) { + super(id); + } + + @Override + public String toJsonString() { + setLimits(0, limitsEnd, limitsEnd); + return super.toJsonString(); + } + + public void addItem(Player.GetItem playerItem) { + ObjectNode resultNode = (ObjectNode) getResultNode(TYPE.OBJECT); + JsonNode item = playerItem.getResultNode().get(Player.GetItem.ITEM); + addToArrayNode(resultNode, "items", item); + + limitsEnd++; + } + } + + /** + * JSON response for Playlist.GetPlaylists response + * + * Example: + * Query: {"jsonrpc":"2.0","method":"Playlist.GetPlaylists","id":31} + * Response: {"id":31,"jsonrpc":"2.0","result":[{"playlistid":0,"type":"audio"},{"playlistid":1,"type":"video"},{"playlistid":2,"type":"picture"}]} + */ + public static class GetPlaylists extends JsonResponse { + public final static String METHOD_NAME = "Playlist.GetPlaylists"; + + public GetPlaylists(int id) { + super(id); + + ArrayNode playlists = createArrayNode(); + playlists.add(createPlaylistNode(playlistID.AUDIO.ordinal(), "audio")); + playlists.add(createPlaylistNode(playlistID.VIDEO.ordinal(), "video")); + playlists.add(createPlaylistNode(playlistID.PICTURE.ordinal(), "picture")); + + setResultToResponse(playlists); + } + + private ObjectNode createPlaylistNode(int id, String type) { + ObjectNode playlistNode = createObjectNode(); + playlistNode.put("playlistid", id); + playlistNode.put("type", type); + return playlistNode; + } + } +} diff --git a/app/src/debug/java/org/xbmc/kore/testutils/tcpserver/handlers/jsonrpc/response/notifications/Player.java b/app/src/debug/java/org/xbmc/kore/testutils/tcpserver/handlers/jsonrpc/response/notifications/Player.java index f641e09..349eafa 100644 --- a/app/src/debug/java/org/xbmc/kore/testutils/tcpserver/handlers/jsonrpc/response/notifications/Player.java +++ b/app/src/debug/java/org/xbmc/kore/testutils/tcpserver/handlers/jsonrpc/response/notifications/Player.java @@ -24,15 +24,6 @@ import org.xbmc.kore.testutils.tcpserver.handlers.jsonrpc.JsonUtils; public class Player { abstract public static class PlayPause extends JsonResponse { - public static String TYPE_SONG = "song"; - public static String TYPE_EPISODE = "episode"; - public static String TYPE_MOVIE = "movie"; - public static String TYPE_MUSICVIDEO = "musicvideo"; - public static String TYPE_VIDEO = "video"; - public static String TYPE_UNKNOWN = "unknown"; - public static String TYPE_PICTURE = "picture"; - public static String TYPE_CHANNEL = "channel"; - private PlayPause(String methodName, int itemId, String itemType, int playerId, int speed) { addMethodToResponse(methodName); @@ -93,6 +84,30 @@ public class Player { } } + /** + * JSON response for Player.OnStop notification + * + * Example: + * {"jsonrpc":"2.0","method":"Player.OnStop","params":{"data":{"end":false,"item":{"id":14765,"type":"song"}},"sender":"xbmc"}} + */ + public static class OnStop extends JsonResponse { + public final static String METHOD_NAME = "Player.OnStop"; + + public OnStop(int itemId, String itemType, boolean ended) { + super(); + addMethodToResponse(METHOD_NAME); + + addDataToResponse("end", false); + + ObjectNode itemNode = createObjectNode(); + itemNode.put("id", itemId); + itemNode.put("type", itemType); + addDataToResponse("item", itemNode); + + addParameterToResponse("sender", "xbmc"); + } + } + /** * JSON response for Player.OnPropertyChanged notification * @@ -127,7 +142,7 @@ public class Player { } /** - * JSON response for Player.OnPropertyChanged notification + * JSON response for Player.OnSeek notification * * Example: * {"jsonrpc":"2.0","method":"Player.OnSeek", "params":{ "data":{"item":{ "id":127,"type":"episode" },"player":{ "playerid":1,"seekoffset":{ "hours":0,"milliseconds":0, "minutes":0,"seconds":-14 },"speed":0, "time":{"hours":0, "milliseconds":0,"minutes":0, "seconds":2} }},"sender":"xbmc" }} @@ -154,4 +169,22 @@ public class Player { addParameterToResponse("sender", "xbmc"); } } + + /** + * JSON response for Player.OnAVStart notification + * + * Example: + * {"jsonrpc":"2.0","method":"Player.OnAVStart", + * "params":{"data":{ + * "item":{"id":1502,"type":"song"}, + * "player":{"playerid":0,"speed":1}}, + * "sender":"xbmc"}} + */ + public static class OnAVStart extends PlayPause { + public final static String METHOD_NAME = "Player.OnAVStart"; + + public OnAVStart(int itemId, String itemType, int playerId, int speed) { + super(METHOD_NAME, itemId, itemType, playerId, speed); + } + } } diff --git a/app/src/debug/java/org/xbmc/kore/testutils/tcpserver/handlers/jsonrpc/response/notifications/Playlist.java b/app/src/debug/java/org/xbmc/kore/testutils/tcpserver/handlers/jsonrpc/response/notifications/Playlist.java new file mode 100644 index 0000000..852ff71 --- /dev/null +++ b/app/src/debug/java/org/xbmc/kore/testutils/tcpserver/handlers/jsonrpc/response/notifications/Playlist.java @@ -0,0 +1,67 @@ +/* + * Copyright 2018 Martijn Brekhof. All rights reserved. + * + * 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 org.xbmc.kore.testutils.tcpserver.handlers.jsonrpc.response.notifications; + +import com.fasterxml.jackson.databind.node.ObjectNode; + +import org.xbmc.kore.testutils.tcpserver.handlers.jsonrpc.JsonResponse; + +public class Playlist { + + /** + * JSON response for Playlist.OnClear notification + * + * Example: + * {"jsonrpc":"2.0","method":"Playlist.OnClear","params":{"data":{"playlistid":0},"sender":"xbmc"}} + */ + public static class OnClear extends JsonResponse { + public final static String METHOD_NAME = "Playlist.OnClear"; + + public OnClear(int playlistId) { + super(); + addMethodToResponse(METHOD_NAME); + + addDataToResponse("playlistid", playlistId); + + addParameterToResponse("sender", "xbmc"); + } + } + + /** + * JSON response for Playlist.OnAdd notification + * + * Example: + * {"jsonrpc":"2.0","method":"Playlist.OnAdd","params":{"data":{"item":{"id":1502,"type":"song"},"playlistid":0,"position":0},"sender":"xbmc"}} + */ + public static class OnAdd extends JsonResponse { + public final static String METHOD_NAME = "Playlist.OnAdd"; + + public OnAdd(int itemId, String type, int playlistId, int playlistPosition) { + addMethodToResponse(METHOD_NAME); + + ObjectNode item = createObjectNode(); + item.put("id", itemId); + item.put("type", type); + addDataToResponse("item", item); + + addDataToResponse("playlistid", playlistId); + addDataToResponse("position", playlistPosition); + + addParameterToResponse("sender", "xbmc"); + } + } +} diff --git a/app/src/main/java/org/xbmc/kore/host/HostConnectionObserver.java b/app/src/main/java/org/xbmc/kore/host/HostConnectionObserver.java index 2f25b31..ec396d0 100644 --- a/app/src/main/java/org/xbmc/kore/host/HostConnectionObserver.java +++ b/app/src/main/java/org/xbmc/kore/host/HostConnectionObserver.java @@ -876,6 +876,7 @@ public class HostConnectionObserver checkingWhatsPlaying = false; int currentCallResult = (getPropertiesResult.speed == 0) ? PlayerEventsObserver.PLAYER_IS_PAUSED : PlayerEventsObserver.PLAYER_IS_PLAYING; + if (forceReply || (hostState.lastCallResult != currentCallResult) || getPropertiesResultChanged(getPropertiesResult) || @@ -887,6 +888,7 @@ public class HostConnectionObserver forceReply = false; // Copy list to prevent ConcurrentModificationExceptions List allObservers = new ArrayList<>(observers); + for (final PlayerEventsObserver observer : allObservers) { notifySomethingIsPlaying(getActivePlayersResult, getPropertiesResult, getItemResult, observer); } diff --git a/app/src/main/res/layout/grid_item_playlist.xml b/app/src/main/res/layout/grid_item_playlist.xml index 126066c..a1b6b8d 100644 --- a/app/src/main/res/layout/grid_item_playlist.xml +++ b/app/src/main/res/layout/grid_item_playlist.xml @@ -55,7 +55,7 @@ android:contentDescription="@string/action_options"/>