Implemented support for handling multiple playlists in tests (#657)

Added support for handling multiple playlists in the Mock TCP server.
Refactored SlideUpPanelTests to support the new playlist handler.

Some code cleanup and enhancements
- Removed throw exception declarations from methods that never throw the exception.
- Changed deleting the test database on the test device to delete all databases.
  The former method of only deleting the test database resulted in many old databases
  that were not removed when the test run crashed.
- Enhanced robustness of testing async code by explicitly waiting for a view
  to reach a certain state.
This commit is contained in:
Martijn Brekhof 2019-07-03 19:54:51 +02:00 committed by Synced Synapse
parent 1020b8b7de
commit 3b2447607b
26 changed files with 1209 additions and 477 deletions

View File

@ -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);

View File

@ -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<T extends AppCompatActivity> {
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<T extends AppCompatActivity> {
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<T extends AppCompatActivity> {
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<T extends AppCompatActivity> {
this.kodiMajorVersion = kodiMajorVersion;
}
public static JSONConnectionHandlerManager getConnectionHandlerManager() {
return manager;
}
public static PlayerHandler getPlayerHandler() {
return playerHandler;
}
@ -172,4 +180,8 @@ abstract public class AbstractTestClass<T extends AppCompatActivity> {
public static InputHandler getInputHandler() {
return inputHandler;
}
public static PlaylistHandler getPlaylistHandler() {
return playlistHandler;
}
}

View File

@ -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<AddonsActivity> {
@Rule
public ActivityTestRule<AddonsActivity> mActivityRule = new ActivityTestRule<>(AddonsActivity.class);

View File

@ -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<MusicActivity> {
@ -78,8 +86,14 @@ public class SlideUpPanelTests extends AbstractTestClass<MusicActivity> {
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<MusicActivity> {
*/
@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<MusicActivity> {
*/
@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<MusicActivity> {
* 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<MusicActivity> {
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<MusicActivity> {
* 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<MusicActivity> {
}
}, 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<MusicActivity> {
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<MusicActivity> {
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<MusicActivity> {
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();

View File

@ -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;

View File

@ -57,49 +57,49 @@ public class ButtonTests extends AbstractTestClass<RemoteActivity> {
}
@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<RemoteActivity> {
}
@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);

View File

@ -55,7 +55,7 @@ public class KodiPreV17Tests extends AbstractTestClass<RemoteActivity> {
}
@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);

View File

@ -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) {

View File

@ -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;
}
}

View File

@ -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<Socket> 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<Socket> 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();
}

View File

@ -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<JsonResponse> getNotifications() {
return null;
}
@Override
public void reset() {
}
@Override
public String[] getType() {
return new String[]{Addons.GetAddons.METHOD_NAME};
}
@Override
public ArrayList<JsonResponse> getResponse(String method, ObjectNode jsonRequest) {
public ArrayList<JsonResponse> createResponse(String method, ObjectNode jsonRequest) {
ArrayList<JsonResponse> jsonResponses = new ArrayList<>();
int methodId = jsonRequest.get(ID_NODE).asInt(-1);

View File

@ -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<JsonResponse> 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<JsonResponse> getNotifications() {
ArrayList<JsonResponse> 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<JsonResponse> getResponse(String method, ObjectNode jsonRequest) {
public ArrayList<JsonResponse> createResponse(String method, ObjectNode jsonRequest) {
ArrayList<JsonResponse> jsonResponses = new ArrayList<>();
int methodId = jsonRequest.get(ID_NODE).asInt(-1);

View File

@ -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<JsonResponse> notifications = new ArrayList<>();
private HashSet<String> methodsHandled = new HashSet<>();
/**
* Used to determine which methods the handler implements
* @return list of JSON method names
*/
abstract String[] getType();
abstract ArrayList<JsonResponse> 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<JsonResponse> getNotifications() {
ArrayList<JsonResponse> 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<JsonResponse> getResponse(String method, ObjectNode jsonRequest) {
ArrayList<JsonResponse> 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);
}
}

View File

@ -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<JsonResponse> 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<JsonResponse> getResponse(String method, ObjectNode jsonRequest) {
public ArrayList<JsonResponse> createResponse(String method, ObjectNode jsonRequest) {
ArrayList<JsonResponse> jsonResponses = new ArrayList<>();
methodName = method;

View File

@ -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<String, ConnectionHandler> handlersByType = new HashMap<>();
private HashSet<ConnectionHandler> handlers = new HashSet<>();
private StringBuffer buffer = new StringBuffer();
private final HashMap<String, ConnectionHandler> 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<String, ArrayList<JsonResponse>> clientResponses = new HashMap<>();
private HashMap<String, ArrayList<JsonResponse>> clientResponses = new HashMap<>();
private final HashMap<String, MethodPendingState> methodIdsHandled = new HashMap<>();
private final HashSet<String> 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<JsonResponse> 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<JsonResponse> 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<JsonResponse> 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<JsonResponse> 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<ArrayList<JsonResponse>> jsonResponses = clientResponses.values();
for (ArrayList<JsonResponse> arrayList : jsonResponses) {
for (JsonResponse response : arrayList) {
stringBuffer.append(response.toJsonString() + "\n");
for(Map.Entry<String, ArrayList <JsonResponse>> 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<JsonResponse> 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<ConnectionHandler> uniqueHandlers = new HashSet<>(handlersByType.values());
//Handle notifications
for (ConnectionHandler handler : uniqueHandlers) {
ArrayList<JsonResponse> 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<JsonResponse> jsonResponses) {
ArrayList<JsonResponse> 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;
}
}
}

View File

@ -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<JsonResponse> getResponse(String method, ObjectNode jsonRequest) {
public ArrayList<JsonResponse> createResponse(String method, ObjectNode jsonRequest) {
ArrayList<JsonResponse> jsonResponses = new ArrayList<>();
jsonResponses.add(new Ping(jsonRequest.get("id").asInt()));
return jsonResponses;
}
@Override
public ArrayList<JsonResponse> getNotifications() {
return null;
}
@Override
public void reset() {
}
}

View File

@ -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<PlaylistHolder> playlists = new ArrayList<>();
private Playlist.playlistID activePlaylistId = Playlist.playlistID.AUDIO;
private String playerType = PlayerType.GetActivePlayersReturnType.AUDIO;
private ArrayList<JsonResponse> notifications = new ArrayList<>();
@Override
public ArrayList<JsonResponse> getNotifications() {
ArrayList<JsonResponse> 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<JsonResponse> getResponse(String method, ObjectNode jsonRequest) {
LogUtils.LOGD(TAG, "getResponse: method="+method);
public ArrayList<JsonResponse> createResponse(String method, ObjectNode jsonRequest) {
ArrayList<JsonResponse> 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<PlaylistHolder> 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();
}
}

View File

@ -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<PlaylistHolder> 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<JsonResponse> createResponse(String method, ObjectNode jsonRequest) {
ArrayList<JsonResponse> 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<PlaylistHolder> getPlaylists() {
return playlists;
}
public List<Player.GetItem> 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);
}
}

View File

@ -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<Player.GetItem> 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<Player.GetItem> 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;
}
}

View File

@ -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();
}

View File

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

View File

@ -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;
}
}
}

View File

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

View File

@ -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");
}
}
}

View File

@ -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<PlayerEventsObserver> allObservers = new ArrayList<>(observers);
for (final PlayerEventsObserver observer : allObservers) {
notifySomethingIsPlaying(getActivePlayersResult, getPropertiesResult, getItemResult, observer);
}

View File

@ -55,7 +55,7 @@
android:contentDescription="@string/action_options"/>
<TextView
android:id="@+id/title"
android:id="@+id/playlist_item_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_toRightOf="@id/art"
@ -68,9 +68,9 @@
android:id="@+id/details"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignLeft="@id/title"
android:layout_alignStart="@id/title"
android:layout_below="@id/title"
android:layout_alignLeft="@id/playlist_item_title"
android:layout_alignStart="@id/playlist_item_title"
android:layout_below="@id/playlist_item_title"
android:layout_toRightOf="@id/art"
android:layout_toEndOf="@id/art"
android:layout_toLeftOf="@id/list_context_menu"