From dbd08c07aad93189ad8e659db862bcbe9988a07a Mon Sep 17 00:00:00 2001 From: Martijn Brekhof Date: Mon, 17 Jul 2017 20:07:11 +0200 Subject: [PATCH] Implemented unit/integration tests for the now playing panel (#420) * Fixed issue with opening drawer and Espresso not waiting for when drawer was actually opened. * Implemented new handlers for MockTcpServer to test media control functions. * Decreased delay for sending reponses in MockTcpServer from 1 sec to 100 ms. This was needed to prevent race conditions with the progress bar that increases automatically once per second. --- .../kore/testhelpers/EspressoTestUtils.java | 28 +- .../org/xbmc/kore/testhelpers/Matchers.java | 89 +++ .../java/org/xbmc/kore/testhelpers/Utils.java | 8 + .../kore/testhelpers/action/ViewActions.java | 110 +++ .../xbmc/kore/tests/ui/AbstractTestClass.java | 29 + .../kore/tests/ui/BaseMediaActivityTests.java | 17 +- .../tests/ui/music/SlideUpPanelTests.java | 629 ++++++++++++++++++ .../testutils/tcpserver/MockTcpServer.java | 2 +- .../tcpserver/handlers/AddonsHandler.java | 2 +- .../handlers/ApplicationHandler.java | 6 +- .../JSONConnectionHandlerManager.java | 10 +- .../tcpserver/handlers/JSONRPCHandler.java | 53 ++ .../tcpserver/handlers/PlayerHandler.java | 372 +++++++++++ .../handlers/jsonrpc/JsonResponse.java | 14 +- .../tcpserver/handlers/jsonrpc/JsonUtils.java | 39 ++ .../jsonrpc/nodes/AudioDetailsNode.java | 33 + .../jsonrpc/nodes/SubtitleDetailsNode.java | 31 + .../jsonrpc/nodes/VideoDetailsNode.java | 35 + .../jsonrpc/response/methods/JSONRPC.java | 34 + .../jsonrpc/response/methods/Player.java | 482 ++++++++++++++ .../response/notifications/Player.java | 157 +++++ 21 files changed, 2160 insertions(+), 20 deletions(-) create mode 100644 app/src/androidTest/java/org/xbmc/kore/tests/ui/music/SlideUpPanelTests.java create mode 100644 app/src/testUtils/java/org/xbmc/kore/testutils/tcpserver/handlers/JSONRPCHandler.java create mode 100644 app/src/testUtils/java/org/xbmc/kore/testutils/tcpserver/handlers/PlayerHandler.java create mode 100644 app/src/testUtils/java/org/xbmc/kore/testutils/tcpserver/handlers/jsonrpc/JsonUtils.java create mode 100644 app/src/testUtils/java/org/xbmc/kore/testutils/tcpserver/handlers/jsonrpc/nodes/AudioDetailsNode.java create mode 100644 app/src/testUtils/java/org/xbmc/kore/testutils/tcpserver/handlers/jsonrpc/nodes/SubtitleDetailsNode.java create mode 100644 app/src/testUtils/java/org/xbmc/kore/testutils/tcpserver/handlers/jsonrpc/nodes/VideoDetailsNode.java create mode 100644 app/src/testUtils/java/org/xbmc/kore/testutils/tcpserver/handlers/jsonrpc/response/methods/JSONRPC.java create mode 100644 app/src/testUtils/java/org/xbmc/kore/testutils/tcpserver/handlers/jsonrpc/response/methods/Player.java create mode 100644 app/src/testUtils/java/org/xbmc/kore/testutils/tcpserver/handlers/jsonrpc/response/notifications/Player.java 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 71e3b30..2159e4e 100644 --- a/app/src/androidTest/java/org/xbmc/kore/testhelpers/EspressoTestUtils.java +++ b/app/src/androidTest/java/org/xbmc/kore/testhelpers/EspressoTestUtils.java @@ -27,8 +27,11 @@ import android.view.View; import android.widget.AutoCompleteTextView; import android.widget.TextView; +import com.sothree.slidinguppanel.SlidingUpPanelLayout; + import org.hamcrest.Matcher; import org.xbmc.kore.R; +import org.xbmc.kore.testhelpers.action.ViewActions; import static android.support.test.espresso.Espresso.onData; import static android.support.test.espresso.Espresso.onView; @@ -43,6 +46,7 @@ import static android.support.test.espresso.assertion.ViewAssertions.doesNotExis import static android.support.test.espresso.assertion.ViewAssertions.matches; import static android.support.test.espresso.matcher.ViewMatchers.isAssignableFrom; import static android.support.test.espresso.matcher.ViewMatchers.isDisplayed; +import static android.support.test.espresso.matcher.ViewMatchers.isRoot; import static android.support.test.espresso.matcher.ViewMatchers.withContentDescription; import static android.support.test.espresso.matcher.ViewMatchers.withId; import static android.support.test.espresso.matcher.ViewMatchers.withParent; @@ -60,8 +64,8 @@ public class EspressoTestUtils { = activity.getResources().getConfiguration().orientation; activity.setRequestedOrientation( (orientation == Configuration.ORIENTATION_PORTRAIT) ? - ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE : - ActivityInfo.SCREEN_ORIENTATION_PORTRAIT); + ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE : + ActivityInfo.SCREEN_ORIENTATION_PORTRAIT); } /** @@ -141,11 +145,11 @@ public class EspressoTestUtils { /** * Performs a click on an item in an adapter view, such as GridView or ListView * @param position - * @param resourceId + * @param resourceId of adapter view holding the item that should be clicked */ public static void clickAdapterViewItem(int position, int resourceId) { onData(anything()).inAdapterView(allOf(withId(resourceId), isDisplayed())) - .atPosition(position).perform(click()); + .atPosition(position).perform(click()); } /** @@ -153,7 +157,7 @@ public class EspressoTestUtils { * @param query text that SearchView should contain */ public static void checkTextInSearchQuery(String query) { - onView(isAssignableFrom(AutoCompleteTextView.class)).check(matches(withText(query))); + onView(isAssignableFrom(AutoCompleteTextView.class)).check(matches(withText(query))); } /** @@ -280,4 +284,18 @@ public class EspressoTestUtils { onView(allOf(instanceOf(TextView.class), withParent(withId(R.id.default_toolbar)))) .check(matches(withText(actionbarTitle))); } + + /** + * Waits for 10 seconds till panel has given state. + * + * @param panelState desired state of panel + */ + public static void waitForPanelState(final SlidingUpPanelLayout.PanelState panelState) { + onView(isRoot()).perform(ViewActions.waitForView(R.id.now_playing_panel, new ViewActions.CheckStatus() { + @Override + public boolean check(View v) { + return ((SlidingUpPanelLayout) v).getPanelState() == panelState; + } + }, 10000)); + } } diff --git a/app/src/androidTest/java/org/xbmc/kore/testhelpers/Matchers.java b/app/src/androidTest/java/org/xbmc/kore/testhelpers/Matchers.java index 559b2ba..249b38e 100644 --- a/app/src/androidTest/java/org/xbmc/kore/testhelpers/Matchers.java +++ b/app/src/androidTest/java/org/xbmc/kore/testhelpers/Matchers.java @@ -24,11 +24,16 @@ import android.view.View; import android.view.ViewGroup; import android.widget.Adapter; import android.widget.AdapterView; +import android.widget.SeekBar; +import android.widget.TextView; import org.hamcrest.BaseMatcher; import org.hamcrest.Description; import org.hamcrest.Matcher; import org.hamcrest.TypeSafeMatcher; +import org.xbmc.kore.ui.widgets.RepeatModeButton; +import org.xbmc.kore.ui.widgets.HighlightButton; +import org.xbmc.kore.utils.UIUtils; public class Matchers { public static MenuItemTitleMatcher withMenuTitle(String title) { @@ -128,4 +133,88 @@ public class Matchers { } }; } + + public static Matcher withProgress(final int progress) { + return new BoundedMatcher(SeekBar.class) { + @Override + protected boolean matchesSafely(SeekBar item) { + return item.getProgress() == progress; + } + + @Override + public void describeTo(Description description) { + description.appendText("expected: " + progress); + } + }; + } + + public static Matcher withProgress(final String progress) { + return new BoundedMatcher(TextView.class) { + @Override + protected boolean matchesSafely(TextView item) { + return progress.contentEquals(item.getText()); + } + + @Override + public void describeTo(Description description) { + description.appendText("expected: " + progress); + } + }; + } + + public static Matcher withProgressGreaterThanOrEqual(final String time) { + return new BoundedMatcher(SeekBar.class) { + @Override + protected boolean matchesSafely(SeekBar item) { + return item.getProgress() >= UIUtils.timeToSeconds(time); + } + + @Override + public void describeTo(Description description) { + description.appendText("expected progress greater than " + time); + } + }; + } + + public static Matcher withProgressGreaterThan(final int progress) { + return new BoundedMatcher(SeekBar.class) { + @Override + protected boolean matchesSafely(SeekBar item) { + return item.getProgress() > progress; + } + + @Override + public void describeTo(Description description) { + description.appendText("expected progress greater than " + progress); + } + }; + } + + public static Matcher withHighlightState(final boolean highlight) { + return new BoundedMatcher(HighlightButton.class) { + @Override + protected boolean matchesSafely(HighlightButton item) { + return item.isHighlighted(); + } + + @Override + public void describeTo(Description description) { + description.appendText("expected: " + highlight); + } + }; + } + + public static Matcher withRepeatMode(final RepeatModeButton.MODE mode) { + return new BoundedMatcher(RepeatModeButton.class) { + @Override + protected boolean matchesSafely(RepeatModeButton item) { + return item.getMode() == mode; + } + + @Override + public void describeTo(Description description) { + description.appendText("expected: " + mode.name()); + } + }; + } } diff --git a/app/src/androidTest/java/org/xbmc/kore/testhelpers/Utils.java b/app/src/androidTest/java/org/xbmc/kore/testhelpers/Utils.java index 1ec9a41..69ebe09 100644 --- a/app/src/androidTest/java/org/xbmc/kore/testhelpers/Utils.java +++ b/app/src/androidTest/java/org/xbmc/kore/testhelpers/Utils.java @@ -19,11 +19,14 @@ package org.xbmc.kore.testhelpers; import android.content.Context; import android.content.pm.PackageManager; import android.os.IBinder; +import android.support.annotation.IntDef; import android.support.test.rule.ActivityTestRule; import android.support.v4.widget.DrawerLayout; import android.util.Log; import android.view.Gravity; +import android.view.View; +import org.junit.internal.runners.statements.RunAfters; import org.xbmc.kore.R; import org.xbmc.kore.host.HostInfo; import org.xbmc.kore.host.HostManager; @@ -63,6 +66,11 @@ public class Utils { drawerLayout.openDrawer(Gravity.LEFT); } }); + DrawerLayout drawerLayout = (DrawerLayout) activityTestRule.getActivity().findViewById(R.id.drawer_layout); + while(true) { + if (drawerLayout.isDrawerOpen(Gravity.LEFT)) + return; + } } public static void initialize(ActivityTestRule activityTestRule, HostInfo info) throws Throwable { diff --git a/app/src/androidTest/java/org/xbmc/kore/testhelpers/action/ViewActions.java b/app/src/androidTest/java/org/xbmc/kore/testhelpers/action/ViewActions.java index 957e54b..2eedad7 100644 --- a/app/src/androidTest/java/org/xbmc/kore/testhelpers/action/ViewActions.java +++ b/app/src/androidTest/java/org/xbmc/kore/testhelpers/action/ViewActions.java @@ -17,10 +17,27 @@ package org.xbmc.kore.testhelpers.action; +import android.support.test.espresso.PerformException; +import android.support.test.espresso.UiController; import android.support.test.espresso.ViewAction; +import android.support.test.espresso.action.MotionEvents; +import android.support.test.espresso.action.Press; +import android.support.test.espresso.util.HumanReadables; +import android.support.test.espresso.util.TreeIterables; +import android.view.View; +import android.widget.SeekBar; + +import org.hamcrest.Description; +import org.hamcrest.Matcher; +import org.hamcrest.TypeSafeMatcher; + +import java.util.concurrent.TimeoutException; + import static android.support.test.espresso.action.ViewActions.actionWithAssertions; +import static android.support.test.espresso.matcher.ViewMatchers.isRoot; public final class ViewActions { + /** * Returns an action that clears the focus on the view. *
@@ -33,4 +50,97 @@ public final class ViewActions { return actionWithAssertions(new ClearFocus()); } + public interface CheckStatus { + boolean check(View v); + } + + /** + * ViewAction that waits until view with viewId becomes visible + * @param viewId Resource identifier of view item that must be checked + * @param checkStatus called when viewId has been found to check its status. If return value + * is true waitForView will stop, false it will continue until timeout is exceeded + * @param millis amount of time to wait for view to become visible + * @return + */ + public static ViewAction waitForView(final int viewId, final CheckStatus checkStatus, final long millis) { + return new ViewAction() { + @Override + public Matcher getConstraints() { + return isRoot(); + } + + @Override + public String getDescription() { + return "Searches for view with id: " + viewId + " and tests its status using CheckStatus, using timeout " + millis + " ms."; + } + + @Override + public void perform(UiController uiController, View view) { + uiController.loopMainThreadUntilIdle(); + final long endTime = System.currentTimeMillis() + millis; + do { + for (View child : TreeIterables.breadthFirstViewTraversal(view)) { + if (child.getId() == viewId) { + if (checkStatus.check(child)) { + return; + } + } + } + + uiController.loopMainThreadForAtLeast(50); + } while (System.currentTimeMillis() < endTime); + + throw new PerformException.Builder() + .withActionDescription(this.getDescription()) + .withViewDescription(HumanReadables.describe(view)) + .withCause(new TimeoutException()) + .build(); + } + }; + } + + public static ViewAction slideSeekBar(final int progress) { + return new ViewAction() { + @Override + public Matcher getConstraints() { + return new TypeSafeMatcher() { + @Override + protected boolean matchesSafely(View item) { + return item instanceof SeekBar; + } + + @Override + public void describeTo(Description description) { + description.appendText("is a SeekBar."); + } + }; + } + + @Override + public String getDescription() { + return "Slides seekbar to progress position " + progress; + } + + @Override + public void perform(UiController uiController, View view) { + SeekBar seekBar = (SeekBar) view; + + int[] seekBarPos = {0,0}; + view.getLocationOnScreen(seekBarPos); + float[] startPos = {seekBarPos[0], seekBarPos[1]}; + + MotionEvents.DownResultHolder downResultHolder = + MotionEvents.sendDown(uiController, startPos, + Press.PINPOINT.describePrecision()); + + while(seekBar.getProgress() < progress) { + startPos[0]++; + MotionEvents.sendMovement(uiController, downResultHolder.down, startPos); + uiController.loopMainThreadForAtLeast(10); + } + + MotionEvents.sendUp(uiController, downResultHolder.down, startPos); + } + }; + } } 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 aefc0ad..33e8501 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 @@ -17,10 +17,12 @@ package org.xbmc.kore.tests.ui; import android.content.Intent; +import android.content.SharedPreferences; import android.support.test.espresso.Espresso; import android.support.test.rule.ActivityTestRule; import android.support.test.runner.AndroidJUnit4; import android.support.v7.app.AppCompatActivity; +import android.support.v7.preference.PreferenceManager; import org.junit.After; import org.junit.AfterClass; @@ -35,7 +37,10 @@ import org.xbmc.kore.testhelpers.Utils; import org.xbmc.kore.testutils.Database; import org.xbmc.kore.testutils.tcpserver.MockTcpServer; import org.xbmc.kore.testutils.tcpserver.handlers.AddonsHandler; +import org.xbmc.kore.testutils.tcpserver.handlers.ApplicationHandler; 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 java.io.IOException; @@ -50,10 +55,17 @@ abstract public class AbstractTestClass { private static MockTcpServer server; private static JSONConnectionHandlerManager manager; private AddonsHandler addonsHandler; + private static PlayerHandler playerHandler; + private static ApplicationHandler applicationHandler; @BeforeClass public static void setupMockTCPServer() throws Throwable { + playerHandler = new PlayerHandler(); + applicationHandler = new ApplicationHandler(); manager = new JSONConnectionHandlerManager(); + manager.addHandler(playerHandler); + manager.addHandler(applicationHandler); + manager.addHandler(new JSONRPCHandler()); server = new MockTcpServer(manager); server.start(); } @@ -79,6 +91,12 @@ abstract public class AbstractTestClass { loaderIdlingResource = new LoaderIdlingResource(activityTestRule.getActivity().getSupportLoaderManager()); Espresso.registerIdlingResources(loaderIdlingResource); + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(activityTestRule.getActivity()); + SharedPreferences.Editor editor = prefs.edit(); + editor.clear(); + editor.commit(); + + //Relaunch the activity for the changes (Host selections, preference reset) to take effect activityTestRule.launchActivity(new Intent()); Utils.closeDrawer(activityTestRule); @@ -89,6 +107,9 @@ abstract public class AbstractTestClass { if ( loaderIdlingResource != null ) Espresso.unregisterIdlingResources(loaderIdlingResource); + applicationHandler.reset(); + playerHandler.reset(); + Utils.cleanup(); } @@ -103,4 +124,12 @@ abstract public class AbstractTestClass { } return null; } + + public static PlayerHandler getPlayerHandler() { + return playerHandler; + } + + public static ApplicationHandler getApplicationHandler() { + return applicationHandler; + } } diff --git a/app/src/androidTest/java/org/xbmc/kore/tests/ui/BaseMediaActivityTests.java b/app/src/androidTest/java/org/xbmc/kore/tests/ui/BaseMediaActivityTests.java index f651afa..1e731e3 100644 --- a/app/src/androidTest/java/org/xbmc/kore/tests/ui/BaseMediaActivityTests.java +++ b/app/src/androidTest/java/org/xbmc/kore/tests/ui/BaseMediaActivityTests.java @@ -17,17 +17,18 @@ package org.xbmc.kore.tests.ui; import android.support.test.espresso.Espresso; -import android.support.test.runner.AndroidJUnit4; import org.junit.Ignore; import org.junit.Test; -import org.junit.runner.RunWith; import org.xbmc.kore.R; import org.xbmc.kore.testhelpers.EspressoTestUtils; +import org.xbmc.kore.testhelpers.Utils; import org.xbmc.kore.ui.BaseMediaActivity; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; +import static org.xbmc.kore.testhelpers.EspressoTestUtils.clickAdapterViewItem; +import static org.xbmc.kore.testhelpers.EspressoTestUtils.rotateDevice; /** * Contains generic tests for all activities extending BaseMediaActivity @@ -53,7 +54,7 @@ abstract public class BaseMediaActivityTests extend */ @Test public void showArrowWhenSelectingListItem() { - EspressoTestUtils.clickAdapterViewItem(0, R.id.list); + clickAdapterViewItem(0, R.id.list); assertTrue(((T) EspressoTestUtils.getActivity()).getDrawerIndicatorIsArrow()); } @@ -68,7 +69,7 @@ abstract public class BaseMediaActivityTests extend */ @Test public void showHamburgerWhenSelectingListItemAndReturn() { - EspressoTestUtils.clickAdapterViewItem(0, R.id.list); + clickAdapterViewItem(0, R.id.list); Espresso.pressBack(); assertFalse(((T) EspressoTestUtils.getActivity()).getDrawerIndicatorIsArrow()); @@ -85,8 +86,8 @@ abstract public class BaseMediaActivityTests extend */ @Test public void restoreArrowOnConfigurationChange() { - EspressoTestUtils.clickAdapterViewItem(0, R.id.list); - EspressoTestUtils.rotateDevice(getActivity()); + clickAdapterViewItem(0, R.id.list); + rotateDevice(getActivity()); assertTrue(((T) EspressoTestUtils.getActivity()).getDrawerIndicatorIsArrow()); } @@ -103,8 +104,8 @@ abstract public class BaseMediaActivityTests extend */ @Test public void restoreHamburgerOnConfigurationChangeOnReturn() { - EspressoTestUtils.clickAdapterViewItem(0, R.id.list); - EspressoTestUtils.rotateDevice(getActivity()); + clickAdapterViewItem(0, R.id.list); + rotateDevice(getActivity()); Espresso.pressBack(); assertFalse(((T) EspressoTestUtils.getActivity()).getDrawerIndicatorIsArrow()); 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 new file mode 100644 index 0000000..9f0299b --- /dev/null +++ b/app/src/androidTest/java/org/xbmc/kore/tests/ui/music/SlideUpPanelTests.java @@ -0,0 +1,629 @@ +/* + * Copyright 2017 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.tests.ui.music; + + +import android.content.SharedPreferences; +import android.os.SystemClock; +import android.support.test.rule.ActivityTestRule; +import android.support.v7.preference.PreferenceManager; +import android.view.View; +import android.widget.SeekBar; +import android.widget.TextView; + +import com.sothree.slidinguppanel.SlidingUpPanelLayout; + +import org.junit.Rule; +import org.junit.Test; +import org.xbmc.kore.R; +import org.xbmc.kore.Settings; +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.Player; +import org.xbmc.kore.ui.sections.audio.MusicActivity; +import org.xbmc.kore.ui.widgets.HighlightButton; +import org.xbmc.kore.ui.widgets.RepeatModeButton; + +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; +import static android.support.test.espresso.assertion.ViewAssertions.matches; +import static android.support.test.espresso.matcher.ViewMatchers.isDisplayed; +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.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; + +public class SlideUpPanelTests extends AbstractTestClass { + + @Rule + public ActivityTestRule musicActivityActivityTestRule = + new ActivityTestRule<>(MusicActivity.class); + + @Override + protected ActivityTestRule getActivityTestRule() { + return musicActivityActivityTestRule; + } + + @Override + public void setUp() throws Throwable { + super.setUp(); + + getPlayerHandler().reset(); + getPlayerHandler().startPlay(); + + waitForPanelState(SlidingUpPanelLayout.PanelState.COLLAPSED); + } + + /** + * Test if panel title is correctly set + * + * UI interaction flow tested: + * 1. Start playing a music item + * 2. Result: panel title should show current playing media item + */ + @Test + public void panelTitleTest() { + Player.GetItem item = getPlayerHandler().getMediaItem(); + onView(withId(R.id.npp_title)).check(matches(withText(item.getTitle()))); + } + + /** + * Test if panel buttons are correctly set for music items + * + * UI interaction flow tested: + * 1. Start playing a music item + * 2. Result: panel should show next, play, and previous buttons + */ + @Test + public void panelButtonsMusicTest() { + onView(withId(R.id.npp_next)).check(matches(isDisplayed())); + onView(withId(R.id.npp_previous)).check(matches(isDisplayed())); + onView(withId(R.id.npp_play)).check(matches(isDisplayed())); + } + + /** + * Test if panel buttons are correctly set for movie items + * + * UI interaction flow tested: + * 1. Start playing a movie item + * 2. Result: panel should show play button + */ + @Test + public void panelButtonsMoviesTest() { + getPlayerHandler().setMediaType(PlayerHandler.TYPE.MOVIE); + getPlayerHandler().startPlay(); + Player.GetItem item = getPlayerHandler().getMediaItem(); + final String title = item.getTitle(); + onView(isRoot()).perform(ViewActions.waitForView( + R.id.npp_title, new ViewActions.CheckStatus() { + @Override + public boolean check(View v) { + return title.contentEquals(((TextView) v).getText()); + } + }, 10000)); + + onView(withId(R.id.npp_next)).check(matches(not(isDisplayed()))); + onView(withId(R.id.npp_previous)).check(matches(not(isDisplayed()))); + onView(withId(R.id.npp_play)).check(matches(isDisplayed())); + } + + /** + * Test if panel buttons are correctly set for music video items + * + * UI interaction flow tested: + * 1. Start playing a music video item + * 2. Result: panel should show next, play, and previous buttons + */ + @Test + public void panelButtonsMusicVideoTest() { + getPlayerHandler().setMediaType(PlayerHandler.TYPE.MUSICVIDEO); + getPlayerHandler().startPlay(); + Player.GetItem item = getPlayerHandler().getMediaItem(); + final String title = item.getTitle(); + onView(isRoot()).perform(ViewActions.waitForView( + R.id.npp_title, new ViewActions.CheckStatus() { + @Override + public boolean check(View v) { + return title.contentEquals(((TextView) v).getText()); + } + }, 10000)); + + onView(withId(R.id.npp_next)).check(matches(isDisplayed())); + onView(withId(R.id.npp_previous)).check(matches(isDisplayed())); + onView(withId(R.id.npp_play)).check(matches(isDisplayed())); + } + + /** + * Test if shuffle button state is correctly set + * + * UI interaction flow tested: + * 1. Start playing a music item + * 2. Expand panel + * 3. Click on shuffle button + * 4. Result: shuffle button should be highlighted + */ + @Test + public void panelButtonsShuffleTest() { + expandPanel(); + + onView(withId(R.id.npp_shuffle)).perform(click()); + + onView(isRoot()).perform(ViewActions.waitForView(R.id.npp_shuffle, new ViewActions.CheckStatus() { + @Override + public boolean check(View v) { + return ((HighlightButton) v).isHighlighted(); + } + }, 10000)); + } + + /** + * Test if repeat button state is correctly set + * + * UI interaction flow tested: + * 1. Start playing a music item + * 2. Expand panel + * 3. Click on repeat button + * 4. Result: repeat button should be highlighted and show single item repeat mode + * 5. Click on repeat button + * 6. Result: repeat button should be highlighted and show repeat playlist mode + * 7. Click on repeat button + * 8. Result: repeat button should not be highlighted + */ + @Test + public void panelButtonsRepeatModes() { + expandPanel(); + + //Initial state should be OFF + onView(isRoot()).perform(ViewActions.waitForView(R.id.npp_repeat, new ViewActions.CheckStatus() { + @Override + public boolean check(View v) { + return ((RepeatModeButton) v).getMode() == RepeatModeButton.MODE.OFF; + } + }, 10000)); + + // Test if repeat mode is set to ONE after first click + onView(withId(R.id.npp_repeat)).perform(click()); + onView(isRoot()).perform(ViewActions.waitForView(R.id.npp_repeat, new ViewActions.CheckStatus() { + @Override + public boolean check(View v) { + return ((RepeatModeButton) v).getMode() == RepeatModeButton.MODE.ONE; + } + }, 10000)); + + // Test if repeat mode is set to ALL after second click + onView(withId(R.id.npp_repeat)).perform(click()); + onView(isRoot()).perform(ViewActions.waitForView(R.id.npp_repeat, new ViewActions.CheckStatus() { + @Override + public boolean check(View v) { + return ((RepeatModeButton) v).getMode() == RepeatModeButton.MODE.ALL; + } + }, 10000)); + + + // Test if repeat mode is set to OFF after third click + onView(withId(R.id.npp_repeat)).perform(click()); + onView(isRoot()).perform(ViewActions.waitForView(R.id.npp_repeat, new ViewActions.CheckStatus() { + @Override + public boolean check(View v) { + return ((RepeatModeButton) v).getMode() == RepeatModeButton.MODE.OFF; + } + }, 10000)); + } + + /** + * Test if panel collapsed state is restored on configuration changes + * + * UI interaction flow tested: + * 1. Start playing a music item + * 2. Rotate device + * 3. Result: panel state should be collapsed + */ + @Test + public void keepCollapsedOnRotate() { + rotateDevice(getActivity()); + + waitForPanelState(SlidingUpPanelLayout.PanelState.COLLAPSED); + } + + /** + * Test if panel expanded state is restored on configuration changes + * + * UI interaction flow tested: + * 1. Start playing a music item + * 2. Expand panel + * 3. Rotate device + * 4. Result: panel state should be expanded + */ + @Test + public void keepExpandedOnRotate() { + expandPanel(); + + rotateDevice(getActivity()); + + waitForPanelState(SlidingUpPanelLayout.PanelState.EXPANDED); + } + + /** + * Test if repeat button state is restored on configuration changes + * + * UI interaction flow tested: + * 1. Start playing a music item + * 2. Expand panel + * 3. Click on repeat button + * 4. Rotate device + * 5. Result: repeat button state should be restored to state in step 2 + */ + @Test + public void restoreRepeatButtonStateOnRotate() { + expandPanel(); + onView(withId(R.id.npp_repeat)).perform(click()); + + rotateDevice(getActivity()); + + onView(isRoot()).perform(ViewActions.waitForView(R.id.npp_repeat, new ViewActions.CheckStatus() { + @Override + public boolean check(View v) { + return ((RepeatModeButton) v).getMode() == RepeatModeButton.MODE.ONE; + } + }, 10000)); + } + + /** + * Test if shuffle button state is correctly set + * + * UI interaction flow tested: + * 1. Start playing a music item + * 2. Expand panel + * 3. Click on shuffle button + * 4. Result: shuffle button state should be set to shuffle + */ + @Test + public void setShuffleButtonState() { + expandPanel(); + + onView(withId(R.id.npp_shuffle)).perform(click()); //Set state to shuffled + + onView(withId(R.id.npp_shuffle)).check(matches(withHighlightState(true))); + } + + /** + * Test if shuffle button state is restored on configuration changes + * + * UI interaction flow tested: + * 1. Start playing a music item + * 2. Expand panel + * 3. Click on shuffle button + * 4. Rotate device + * 5. Result: shuffle button state should be restored to state in step 2 + */ + @Test + public void restoreShuffleButtonStateOnRotate() { + expandPanel(); + onView(withId(R.id.npp_shuffle)).perform(click()); //Set state to shuffled + + rotateDevice(getActivityTestRule().getActivity()); + + //Using waitForView as we need to wait for the rotate to finish + onView(isRoot()).perform(ViewActions.waitForView(R.id.npp_shuffle, new ViewActions.CheckStatus() { + @Override + public boolean check(View v) { + return ((HighlightButton) v).isHighlighted(); + } + }, 10000)); + } + + /** + * Test if volume is correctly set at start + * + * UI interaction flow tested: + * 1. Start playing a music item + * 2. Set volume at server + * 3. Expand panel + * 4. Result: Volume indicator should show the same volume level as set at the server + */ + @Test + public void setVolume() { + final int volume = 16; + + getApplicationHandler().setVolume(volume, true); + + assertTrue(getApplicationHandler().getVolume() == volume); + expandPanel(); + onView(withId(R.id.vli_seek_bar)).check(matches(withProgress(volume))); + onView(withId(R.id.vli_volume_text)).check(matches(withText(String.valueOf(volume)))); + } + + /** + * Test if changing volume through the volume slider, updates the volume indicator correctly + * and sends the volume change to the server + * + * UI interaction flow tested: + * 1. Start playing a music item + * 2. Expand panel + * 3. Set volume using slider + * 4. Result: Volume indicator should show volume level and server should be set to new volume level + */ + @Test + public void changeVolume() { + final int volume = 16; + expandPanel(); + + onView(withId(R.id.vli_seek_bar)).perform(ViewActions.slideSeekBar(volume)); + + 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); + } + + /** + * Test if changing volume through the volume slider, updates the volume indicator correctly + * and sends the volume change to the server + * + * UI interaction flow tested: + * 1. Start playing a music item + * 2. Expand panel + * 3. Set volume using slider + * 4. Result: Volume indicator should show volume level and server should be set to new volume level + */ + @Test + public void restoreVolumeIndicatorOnRotate() { + final int volume = 16; + expandPanel(); + onView(withId(R.id.vli_seek_bar)).perform(ViewActions.slideSeekBar(volume)); + + rotateDevice(getActivity()); + + 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); + } + + /** + * Test if setting progression correctly updates the media progress indicator + * + * UI interaction flow tested: + * 1. Start playing a music item + * 2. Pause playback + * 3. Expand panel + * 4. Set progression + * 5. Result: Media progression indicator should be correctly updated and progression change + * should be sent to the server. + */ + @Test + public void setProgression() { + final int progress = 16; + final String progressText = "0:16"; + expandPanel(); + onView(withId(R.id.npp_play)).perform(click()); //Pause playback + + 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); + } + + /** + * Test if progression is correctly restored after device configuration change + * + * UI interaction flow tested: + * 1. Start playing a music item + * 2. Pause playback + * 3. Expand panel + * 4. Set progression + * 5. Rotate device + * 6. Result: Progression should be correctly same as before rotating the device. + */ + @Test + public void restoreProgressOnRotate() { + final int progress = 16; + final String progressText = "0:16"; + expandPanel(); + onView(withId(R.id.npp_play)).perform(click()); //Pause playback + + onView(withId(R.id.mpi_seek_bar)).perform(ViewActions.slideSeekBar(progress)); + rotateDevice(getActivity()); + + assertTrue(getPlayerHandler().getPosition() == progress); + onView(withId(R.id.mpi_progress)).check(matches(withProgress(progressText))); + onView(withId(R.id.mpi_seek_bar)).check(matches(withProgress(progress))); + } + + /** + * Kodi resumes playback when progression changes. + * Test if changing progression when player is paused caused + * progression to start updating again + * + * UI interaction flow tested: + * 1. Start playing a music item + * 2. Expand panel + * 3. Pause playback + * 4. Set progression + * 5. Start playback at server (that's what Kodi does) + * 6. Result: Playback should start at paused position + */ + @Test + public void pauseSetProgressionPlay() { + expandPanel(); + + onView(withId(R.id.npp_play)).perform(click()); //Pause playback + onView(withId(R.id.mpi_seek_bar)).perform(ViewActions.slideSeekBar(16)); + getPlayerHandler().startPlay(); + + SeekBar seekBar = (SeekBar) getActivity().findViewById(R.id.mpi_seek_bar); + int progress = seekBar.getProgress(); + SystemClock.sleep(1000); //wait one second to check if progression has indeed progressed + assertTrue(seekBar.getProgress() > progress); + } + + /** + * Test if panel's progressionbar progresses when playing media + * + * UI interaction flow tested: + * 1. Start playing a music item + * 2. Result: Progression should be progressing + */ + @Test + public void progressionUpdaterStartedAfterPlay() { + expandPanel(); + SeekBar seekBar = (SeekBar) getActivity().findViewById(R.id.mpi_seek_bar); + int progress = seekBar.getProgress(); + + SystemClock.sleep(1000); //wait one second to check if progression has indeed progressed + + assertTrue(seekBar.getProgress() > progress); + } + + /** + * Test if panel's progression is maintained when starting a new activity + * + * UI interaction flow tested: + * 1. Start playing a music item + * 2. Expand panel + * 3. Set progression + * 4. Switch to movies (new activity) + * 5. Result: Progression should continue from step 3 + */ + @Test + public void continueProgressionAfterSwitchingActivity() throws Throwable { + final int progress = 24; + expandPanel(); + onView(withId(R.id.mpi_seek_bar)).perform(ViewActions.slideSeekBar(progress)); + + Utils.openDrawer(getActivityTestRule()); + clickAdapterViewItem(2, R.id.navigation_drawer); //select movie activity + + waitForPanelState(SlidingUpPanelLayout.PanelState.COLLAPSED); + expandPanel(); + + onView(isRoot()).perform(ViewActions.waitForView(R.id.mpi_seek_bar, new ViewActions.CheckStatus() { + @Override + public boolean check(View v) { + int seekBarProgress = ((SeekBar) v).getProgress(); + return (seekBarProgress > progress) && (seekBarProgress < (progress + 4)); + } + }, 10000)); + } + + /** + * Test if pause button pauses playback + * + * UI interaction flow tested: + * 1. Start playing a music item + * 2. Pause playback + * 3. Result: Server should stop playing and progressbar should pause + */ + @Test + public void pausePlayback() { + onView(withId(R.id.npp_play)).perform(click()); + + assertFalse(getPlayerHandler().isPlaying()); + + expandPanel(); + final int progress = ((SeekBar) getActivity().findViewById(R.id.mpi_seek_bar)).getProgress(); + SystemClock.sleep(1000); //wait one second to check if progression has indeed paused + onView(isRoot()).perform(ViewActions.waitForView(R.id.mpi_seek_bar, new ViewActions.CheckStatus() { + @Override + public boolean check(View v) { + int seekBarProgress = ((SeekBar) v).getProgress(); + return seekBarProgress == progress; + } + }, 10000)); + } + + /** + * Test if panel is not displayed when user disables the panel + * through the preference screen + * + * UI interaction flow tested: + * 1. Start playing a music item + * 2. Disable showing panel in settings + * 3. Result: Panel should not show + */ + @Test + public void disableShowingPanelInPreferences() throws Throwable { + Utils.openDrawer(getActivityTestRule()); + clickAdapterViewItem(10, R.id.navigation_drawer); //Show preference screen + + SharedPreferences.Editor edit = PreferenceManager.getDefaultSharedPreferences(getActivity()).edit(); + edit.putBoolean(Settings.KEY_PREF_SHOW_NOW_PLAYING_PANEL, false); + edit.apply(); + pressBack(); + + waitForPanelState(SlidingUpPanelLayout.PanelState.HIDDEN); + } + + /** + * Test if panel is displayed when user enables the panel + * through the preference screen + * + * UI interaction flow tested: + * 1. Start playing a music item + * 2. Disable showing panel in settings + * 3. Show Music screen + * 4. Enable showing panel in settings + * 4. Return to Music screen + * 5. Result: Panel should show + */ + @Test + public void showPanelWhenUserEnablesPanel() throws Throwable { + Utils.openDrawer(getActivityTestRule()); + clickAdapterViewItem(10, R.id.navigation_drawer); //Show preference screen + SharedPreferences.Editor edit = PreferenceManager.getDefaultSharedPreferences(getActivity()).edit(); + edit.putBoolean(Settings.KEY_PREF_SHOW_NOW_PLAYING_PANEL, false); + edit.apply(); + pressBack(); + + Utils.openDrawer(getActivityTestRule()); + clickAdapterViewItem(10, R.id.navigation_drawer); //Show preference screen + edit.putBoolean(Settings.KEY_PREF_SHOW_NOW_PLAYING_PANEL, true); + edit.apply(); + pressBack(); + + waitForPanelState(SlidingUpPanelLayout.PanelState.COLLAPSED); + } + + private void expandPanel() { + int tries = 10; + while (tries-- > 0) { + try { + onView(withId(R.id.npp_title)).perform(click()); + + onView(isRoot()).perform(ViewActions.waitForView(R.id.now_playing_panel, new ViewActions.CheckStatus() { + @Override + public boolean check(View v) { + return ((SlidingUpPanelLayout) v).getPanelState() == SlidingUpPanelLayout.PanelState.EXPANDED; + } + }, 1000)); + + return; + } catch (Exception e) { + //Either the click event did not work or the panel did not expand. + //Let's try again. + } + } + } +} diff --git a/app/src/testUtils/java/org/xbmc/kore/testutils/tcpserver/MockTcpServer.java b/app/src/testUtils/java/org/xbmc/kore/testutils/tcpserver/MockTcpServer.java index 7b86c95..369d3f2 100644 --- a/app/src/testUtils/java/org/xbmc/kore/testutils/tcpserver/MockTcpServer.java +++ b/app/src/testUtils/java/org/xbmc/kore/testutils/tcpserver/MockTcpServer.java @@ -203,7 +203,7 @@ public class MockTcpServer { try { while (true) { sendResponse(); - Thread.sleep(1000); + Thread.sleep(100); if ( serverSocket.isClosed() ) return; } diff --git a/app/src/testUtils/java/org/xbmc/kore/testutils/tcpserver/handlers/AddonsHandler.java b/app/src/testUtils/java/org/xbmc/kore/testutils/tcpserver/handlers/AddonsHandler.java index 2a42070..4d587c5 100644 --- a/app/src/testUtils/java/org/xbmc/kore/testutils/tcpserver/handlers/AddonsHandler.java +++ b/app/src/testUtils/java/org/xbmc/kore/testutils/tcpserver/handlers/AddonsHandler.java @@ -43,7 +43,7 @@ public class AddonsHandler implements JSONConnectionHandlerManager.ConnectionHan } @Override - public ArrayList getNotification() { + public ArrayList getNotifications() { return null; } diff --git a/app/src/testUtils/java/org/xbmc/kore/testutils/tcpserver/handlers/ApplicationHandler.java b/app/src/testUtils/java/org/xbmc/kore/testutils/tcpserver/handlers/ApplicationHandler.java index 7641098..8fb2124 100644 --- a/app/src/testUtils/java/org/xbmc/kore/testutils/tcpserver/handlers/ApplicationHandler.java +++ b/app/src/testUtils/java/org/xbmc/kore/testutils/tcpserver/handlers/ApplicationHandler.java @@ -66,8 +66,12 @@ public class ApplicationHandler implements JSONConnectionHandlerManager.Connecti jsonNotifications.add(new OnVolumeChanged(muted, volume)); } + public int getVolume() { + return volume; + } + @Override - public ArrayList getNotification() { + public ArrayList getNotifications() { ArrayList jsonResponses = new ArrayList<>(jsonNotifications); jsonNotifications.clear(); return jsonResponses; diff --git a/app/src/testUtils/java/org/xbmc/kore/testutils/tcpserver/handlers/JSONConnectionHandlerManager.java b/app/src/testUtils/java/org/xbmc/kore/testutils/tcpserver/handlers/JSONConnectionHandlerManager.java index eb0794a..2c80cfc 100644 --- a/app/src/testUtils/java/org/xbmc/kore/testutils/tcpserver/handlers/JSONConnectionHandlerManager.java +++ b/app/src/testUtils/java/org/xbmc/kore/testutils/tcpserver/handlers/JSONConnectionHandlerManager.java @@ -67,7 +67,7 @@ public class JSONConnectionHandlerManager implements MockTcpServer.TcpServerConn * Used to get any notifications from the handler. * @return {@link JsonResponse} that should be sent to the client */ - ArrayList getNotification(); + ArrayList getNotifications(); /** * Should set the state of the handler to its initial state @@ -132,7 +132,7 @@ public class JSONConnectionHandlerManager implements MockTcpServer.TcpServerConn //Handle notifications for(ConnectionHandler handler : handlers) { - ArrayList jsonNotifications = handler.getNotification(); + ArrayList jsonNotifications = handler.getNotifications(); if (jsonNotifications != null) { for (JsonResponse jsonResponse : jsonNotifications) { stringBuffer.append(jsonResponse.toJsonString() +"\n"); @@ -140,7 +140,11 @@ public class JSONConnectionHandlerManager implements MockTcpServer.TcpServerConn } } responseCount++; - return stringBuffer.toString(); + if (stringBuffer.length() > 0) { + return stringBuffer.toString(); + } else { + return null; + } } /** diff --git a/app/src/testUtils/java/org/xbmc/kore/testutils/tcpserver/handlers/JSONRPCHandler.java b/app/src/testUtils/java/org/xbmc/kore/testutils/tcpserver/handlers/JSONRPCHandler.java new file mode 100644 index 0000000..9023d6d --- /dev/null +++ b/app/src/testUtils/java/org/xbmc/kore/testutils/tcpserver/handlers/JSONRPCHandler.java @@ -0,0 +1,53 @@ +/* + * Copyright 2016 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.jsonrpc.method.JSONRPC; +import org.xbmc.kore.testutils.tcpserver.handlers.jsonrpc.JsonResponse; +import org.xbmc.kore.testutils.tcpserver.handlers.jsonrpc.response.methods.JSONRPC.Ping; + +import java.util.ArrayList; + +/** + * Simulates JSON RPC JSON-RPC API + */ +public class JSONRPCHandler implements JSONConnectionHandlerManager.ConnectionHandler { + + @Override + public String[] getType() { + return new String[] {JSONRPC.Ping.METHOD_NAME}; + } + + @Override + public ArrayList getResponse(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/testUtils/java/org/xbmc/kore/testutils/tcpserver/handlers/PlayerHandler.java b/app/src/testUtils/java/org/xbmc/kore/testutils/tcpserver/handlers/PlayerHandler.java new file mode 100644 index 0000000..5217974 --- /dev/null +++ b/app/src/testUtils/java/org/xbmc/kore/testutils/tcpserver/handlers/PlayerHandler.java @@ -0,0 +1,372 @@ +/* + * Copyright 2016 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.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.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.utils.LogUtils; + +import java.util.ArrayList; + +import static org.xbmc.kore.testutils.tcpserver.handlers.PlayerHandler.TYPE.MUSIC; + +/** + * Simulates Player JSON-RPC API + */ +public class PlayerHandler implements JSONConnectionHandlerManager.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" + }; + + private int currentRepeatMode; + private boolean shuffled; + private boolean playing; + private int position; + private long totalTimeSec = 240; // default value + + private TYPE mediaType = MUSIC; + + private Player.GetItem mediaItem = createSongItem(); + 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() { + this.shuffled = false; + this.currentRepeatMode = 0; + this.position = 0; + this.playing = false; + setMediaType(MUSIC); + } + + @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}; + } + + @Override + public ArrayList getResponse(String method, ObjectNode jsonRequest) { + LogUtils.LOGD(TAG, "getResponse: method="+method); + + ArrayList jsonResponses = new ArrayList<>(); + JsonNode node = jsonRequest.get("id"); + JsonResponse response = null; + int playerId; + switch (method) { + case Player.GetActivePlayers.METHOD_NAME: + response = new Player.GetActivePlayers(node.asInt(), 0, playerType); + break; + case Player.GetProperties.METHOD_NAME: + response = updatePlayerProperties(createPlayerProperties(node.asInt())); + break; + case Player.GetItem.METHOD_NAME: + mediaItem.setMethodId(node.asInt()); + response = mediaItem; + 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)); + 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)); + 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)); + 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)); + break; + } + + jsonResponses.add(response); + + return jsonResponses; + } + + /** + * Sets the returned media type + * @param mediaType + */ + public void setMediaType(TYPE mediaType) { + switch (mediaType) { + case MOVIE: + mediaItem = createMovieItem(); + playerType = PlayerType.GetActivePlayersReturnType.VIDEO; + break; + case MUSIC: + mediaItem = createSongItem(); + playerType = PlayerType.GetActivePlayersReturnType.AUDIO; + break; + case UNKNOWN: + mediaItem = createUnknownItem(); + playerType = PlayerType.GetActivePlayersReturnType.AUDIO; + break; + case MUSICVIDEO: + mediaItem = createMusicVideoItem(); + playerType = PlayerType.GetActivePlayersReturnType.VIDEO; + break; + case PICTURE: + mediaItem = createPictureItem(); + playerType = PlayerType.GetActivePlayersReturnType.PICTURE; + break; + case CHANNEL: + mediaItem = createChannelItem(); + playerType = PlayerType.GetActivePlayersReturnType.VIDEO; + break; + } + } + + public void startPlay() { + OnPlay onPlay = new OnPlay(1580, getMediaItemType(), 0, 1); + notifications.add(onPlay); + playing = true; + } + + /** + * Returns the current media item for the media type set through {@link #setMediaType(TYPE)} + * @return + */ + public Player.GetItem getMediaItem() { + return mediaItem; + } + + /** + * Returns the play position of the current media item + * @return the time elapsed in seconds + */ + public long getPosition() { + return position; + } + + public boolean isPlaying() { + return playing; + } + + public void setTotalTimeSec(long totalTimeSec) { + this.totalTimeSec = totalTimeSec; + } + + 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; + default: + return OnPlay.TYPE_SONG; + } + } + + private Player.GetProperties updatePlayerProperties(Player.GetProperties playerProperties) { + if (playing) + position++; + + if ( ( position > totalTimeSec ) && currentRepeatMode != 0 ) + position = 0; + + playerProperties.addPosition(position); + playerProperties.addPercentage((int) ((position * 100 ) / totalTimeSec)); + playerProperties.addTime(0, 0, position, 767); + + playerProperties.addShuffled(shuffled); + playerProperties.addRepeat(repeatModes[currentRepeatMode]); + + return playerProperties; + } + + private Player.GetProperties createPlayerProperties(int id) { + Player.GetProperties properties = new Player.GetProperties(id); + properties.addPlaylistId(0); + properties.addRepeat(repeatModes[currentRepeatMode]); + properties.addShuffled(false); + properties.addSpeed(playing ? 1 : 0); + properties.addTotaltime(0,0,240,41); + 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); + + return item; + } + + 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 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 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 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); + + return item; + } + + 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); + + return item; + } + + 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; + } +} diff --git a/app/src/testUtils/java/org/xbmc/kore/testutils/tcpserver/handlers/jsonrpc/JsonResponse.java b/app/src/testUtils/java/org/xbmc/kore/testutils/tcpserver/handlers/jsonrpc/JsonResponse.java index 055383d..f07fbc8 100644 --- a/app/src/testUtils/java/org/xbmc/kore/testutils/tcpserver/handlers/jsonrpc/JsonResponse.java +++ b/app/src/testUtils/java/org/xbmc/kore/testutils/tcpserver/handlers/jsonrpc/JsonResponse.java @@ -36,7 +36,7 @@ public abstract class JsonResponse { private static final String PARAMS_NODE = "params"; private static final String METHOD_NODE = "method"; private static final String DATA_NODE = "data"; - private static final String ID_NODE = "id"; + protected static final String ID_NODE = "id"; private static final String JSONRPC_NODE = "jsonrpc"; public enum TYPE { @@ -167,6 +167,14 @@ public abstract class JsonResponse { } } + /** + * Adds the value to the array in node with the given key. + * If the array does not exist it will be created + * and added. + * @param node ObjectNode that should contain an entry with key with an array as value + * @param key the key of the item in ObjectNode that should hold the array + * @param value the value to be added to the array + */ protected void addToArrayNode(ObjectNode node, String key, ObjectNode value) { JsonNode jsonNode = node.get(key); if (jsonNode == null) { @@ -189,6 +197,10 @@ public abstract class JsonResponse { getDataNode().put(parameter, value); } + protected void addDataToResponse(String parameter, ObjectNode node) { + getDataNode().set(parameter, node); + } + protected void addParameterToResponse(String parameter, String value) { getParametersNode().put(parameter, value); } diff --git a/app/src/testUtils/java/org/xbmc/kore/testutils/tcpserver/handlers/jsonrpc/JsonUtils.java b/app/src/testUtils/java/org/xbmc/kore/testutils/tcpserver/handlers/jsonrpc/JsonUtils.java new file mode 100644 index 0000000..1c64420 --- /dev/null +++ b/app/src/testUtils/java/org/xbmc/kore/testutils/tcpserver/handlers/jsonrpc/JsonUtils.java @@ -0,0 +1,39 @@ +package org.xbmc.kore.testutils.tcpserver.handlers.jsonrpc; + + +import com.fasterxml.jackson.databind.node.ObjectNode; + +import org.xbmc.kore.jsonrpc.type.GlobalType; +import org.xbmc.kore.utils.LogUtils; + +public class JsonUtils { + /** + * Fills objectNode with time values + * @param objectNode + * @param timeSec + * @return objectNode for chaining + */ + public static ObjectNode createTimeNode(ObjectNode objectNode, long timeSec) { + int hours = (int) timeSec / 3600; + int minutes = (int) ( timeSec / 60 ) % 60; + int seconds = (int) timeSec % 60 ; + return createTimeNode(objectNode, hours, minutes, seconds, 0); + } + + /** + * Fills objectNode with time values + * @param objectNode + * @param hours + * @param minutes + * @param seconds + * @param milliseconds + * @return objectNode for chaining + */ + public static ObjectNode createTimeNode(ObjectNode objectNode, int hours, int minutes, int seconds, int milliseconds) { + objectNode.put(GlobalType.Time.HOURS, hours); + objectNode.put(GlobalType.Time.MINUTES, minutes); + objectNode.put(GlobalType.Time.SECONDS, seconds); + objectNode.put(GlobalType.Time.MILLISECONDS, milliseconds); + return objectNode; + } +} diff --git a/app/src/testUtils/java/org/xbmc/kore/testutils/tcpserver/handlers/jsonrpc/nodes/AudioDetailsNode.java b/app/src/testUtils/java/org/xbmc/kore/testutils/tcpserver/handlers/jsonrpc/nodes/AudioDetailsNode.java new file mode 100644 index 0000000..3ffd300 --- /dev/null +++ b/app/src/testUtils/java/org/xbmc/kore/testutils/tcpserver/handlers/jsonrpc/nodes/AudioDetailsNode.java @@ -0,0 +1,33 @@ +/* + * Copyright 2016 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.nodes; + +import com.fasterxml.jackson.databind.node.ObjectNode; + +import org.xbmc.kore.testutils.tcpserver.handlers.jsonrpc.JsonResponse; + +public class AudioDetailsNode extends JsonResponse { + + private AudioDetailsNode() {}; + + public AudioDetailsNode(int channels, String codec, String language) { + ObjectNode node = (ObjectNode) getResultNode(TYPE.OBJECT); + node.put("channels", channels); + node.put("codec", codec); + node.put("language", language); + } +} diff --git a/app/src/testUtils/java/org/xbmc/kore/testutils/tcpserver/handlers/jsonrpc/nodes/SubtitleDetailsNode.java b/app/src/testUtils/java/org/xbmc/kore/testutils/tcpserver/handlers/jsonrpc/nodes/SubtitleDetailsNode.java new file mode 100644 index 0000000..762be2b --- /dev/null +++ b/app/src/testUtils/java/org/xbmc/kore/testutils/tcpserver/handlers/jsonrpc/nodes/SubtitleDetailsNode.java @@ -0,0 +1,31 @@ +/* + * Copyright 2016 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.nodes; + +import com.fasterxml.jackson.databind.node.ObjectNode; + +import org.xbmc.kore.testutils.tcpserver.handlers.jsonrpc.JsonResponse; + +public class SubtitleDetailsNode extends JsonResponse { + + private SubtitleDetailsNode() {}; + + public SubtitleDetailsNode(String language) { + ObjectNode node = (ObjectNode) getResultNode(TYPE.OBJECT); + node.put("language", language); + } +} diff --git a/app/src/testUtils/java/org/xbmc/kore/testutils/tcpserver/handlers/jsonrpc/nodes/VideoDetailsNode.java b/app/src/testUtils/java/org/xbmc/kore/testutils/tcpserver/handlers/jsonrpc/nodes/VideoDetailsNode.java new file mode 100644 index 0000000..666da02 --- /dev/null +++ b/app/src/testUtils/java/org/xbmc/kore/testutils/tcpserver/handlers/jsonrpc/nodes/VideoDetailsNode.java @@ -0,0 +1,35 @@ +/* + * Copyright 2016 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.nodes; + +import com.fasterxml.jackson.databind.node.ObjectNode; + +import org.xbmc.kore.testutils.tcpserver.handlers.jsonrpc.JsonResponse; + +public class VideoDetailsNode extends JsonResponse { + + private VideoDetailsNode() {}; + + public VideoDetailsNode(int width, int height, float aspect, String code, int duration) { + ObjectNode node = (ObjectNode) getResultNode(TYPE.OBJECT); + node.put("width", width); + node.put("height", height); + node.put("aspect", aspect); + node.put("code", code); + node.put("duration", duration); + } +} diff --git a/app/src/testUtils/java/org/xbmc/kore/testutils/tcpserver/handlers/jsonrpc/response/methods/JSONRPC.java b/app/src/testUtils/java/org/xbmc/kore/testutils/tcpserver/handlers/jsonrpc/response/methods/JSONRPC.java new file mode 100644 index 0000000..a88a2fe --- /dev/null +++ b/app/src/testUtils/java/org/xbmc/kore/testutils/tcpserver/handlers/jsonrpc/response/methods/JSONRPC.java @@ -0,0 +1,34 @@ +/* + * Copyright 2016 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 org.xbmc.kore.testutils.tcpserver.handlers.jsonrpc.JsonResponse; + +/** + * Serverside JSON RPC responses in Application.* + */ +public class JSONRPC { + + public static class Ping extends JsonResponse { + public final static String METHOD_NAME = "JSONRPC.Ping"; + + public Ping(int id) { + super(id); + setResultToResponse("pong"); + } + } +} diff --git a/app/src/testUtils/java/org/xbmc/kore/testutils/tcpserver/handlers/jsonrpc/response/methods/Player.java b/app/src/testUtils/java/org/xbmc/kore/testutils/tcpserver/handlers/jsonrpc/response/methods/Player.java new file mode 100644 index 0000000..724ef0e --- /dev/null +++ b/app/src/testUtils/java/org/xbmc/kore/testutils/tcpserver/handlers/jsonrpc/response/methods/Player.java @@ -0,0 +1,482 @@ +/* + * Copyright 2016 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.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; +import org.xbmc.kore.testutils.tcpserver.handlers.jsonrpc.nodes.AudioDetailsNode; +import org.xbmc.kore.testutils.tcpserver.handlers.jsonrpc.nodes.SubtitleDetailsNode; +import org.xbmc.kore.testutils.tcpserver.handlers.jsonrpc.nodes.VideoDetailsNode; +import org.xbmc.kore.utils.LogUtils; + +/** + * Serverside JSON RPC responses in Methods.Player.* + */ +public class Player { + + /** + * JSON response for Player.Seek request + * + * Example: + * Query: {"jsonrpc":"2.0","method":"Player.Seek","id":41,"params":{"playerid":0,"value":{"hours":0,"milliseconds":0,"minutes":0,"seconds":2}}} + * Answer: {"id":41,"jsonrpc":"2.0","result":{"percentage":16.570009231567382812,"time":{"hours":0,"milliseconds":0,"minutes":0,"seconds":2},"totaltime":{"hours":0,"milliseconds":70,"minutes":0,"seconds":12}}} + * + * @return JSON string + */ + public static class Seek extends JsonResponse { + public final static String METHOD_NAME = "Player.Seek"; + + public Seek(int methodId, double percentage, long timeSec, long totalTime) { + super(methodId); + ObjectNode resultNode = (ObjectNode) getResultNode(TYPE.OBJECT); + resultNode.put("percentage", percentage); + resultNode.set("time", JsonUtils.createTimeNode(createObjectNode(), timeSec)); + resultNode.set("totalTime", JsonUtils.createTimeNode(createObjectNode(), totalTime)); + } + } + + public static class SetShuffle extends JsonResponse { + public final static String METHOD_NAME = "Player.SetShuffle"; + + public SetShuffle(int methodId, String result) { + super(methodId); + setResultToResponse(result); + } + } + + public static class SetRepeat extends JsonResponse { + public final static String METHOD_NAME = "Player.SetRepeat"; + + public SetRepeat(int methodId, String result) { + super(methodId); + setResultToResponse(result); + } + } + + public static class PlayPause extends JsonResponse { + public final static String METHOD_NAME = "Player.PlayPause"; + + public PlayPause(int methodId, int speed) { + super(methodId); + ((ObjectNode) getResultNode(TYPE.OBJECT)).put("speed", speed); + } + } + + public static class GetActivePlayers extends JsonResponse { + public final static String METHOD_NAME = "Player.GetActivePlayers"; + + public GetActivePlayers(int methodId, int playerId, String type) { + super(methodId); + ObjectNode objectNode = createObjectNode(); + objectNode.put("playerid", playerId); + objectNode.put("type", type); + ((ArrayNode) getResultNode(TYPE.ARRAY)).add(objectNode); + } + } + + public static class GetProperties extends JsonResponse { + public final static String METHOD_NAME = "Player.GetProperties"; + + final static String SPEED = PlayerType.PropertyName.SPEED; + final static String PERCENTAGE = PlayerType.PropertyName.PERCENTAGE; + final static String POSITION = PlayerType.PropertyName.POSITION; + final static String TIME = PlayerType.PropertyName.TIME; + final static String TOTALTIME = PlayerType.PropertyName.TOTALTIME; + final static String REPEAT = PlayerType.PropertyName.REPEAT; + final static String SHUFFLED = PlayerType.PropertyName.SHUFFLED; + final static String CURRENTAUDIOSTREAM = PlayerType.PropertyName.CURRENTAUDIOSTREAM; + final static String CURRENTSUBTITLE = PlayerType.PropertyName.CURRENTSUBTITLE; + final static String AUDIOSTREAMS = PlayerType.PropertyName.AUDIOSTREAMS; + final static String SUBTITLES = PlayerType.PropertyName.SUBTITLES; + final static String PLAYLISTID = PlayerType.PropertyName.PLAYLISTID; + + public GetProperties(int methodId) { + super(methodId); + } + + public void addSpeed(int value) { + ((ObjectNode) getResultNode(TYPE.OBJECT)).put(SPEED, value); + } + + public void addPercentage(int value) { + ((ObjectNode) getResultNode(TYPE.OBJECT)).put(PERCENTAGE, value); + } + + public void addPosition(int value) { + ((ObjectNode) getResultNode(TYPE.OBJECT)).put(POSITION, value); + } + + public void addTime(int hours, int minutes, int seconds, int milliseconds) { + ObjectNode timeNode = JsonUtils.createTimeNode(createObjectNode(), hours, minutes, seconds, milliseconds); + ((ObjectNode) getResultNode(TYPE.OBJECT)).putObject(TIME).setAll(timeNode); + } + + public void addTotaltime(int hours, int minutes, int seconds, int milliseconds) { + ObjectNode timeNode = JsonUtils.createTimeNode(createObjectNode(), hours, minutes, seconds, milliseconds); + ((ObjectNode) getResultNode(TYPE.OBJECT)).putObject(TOTALTIME).setAll(timeNode); + } + + public void addRepeat(String value) { + ((ObjectNode) getResultNode(TYPE.OBJECT)).put(REPEAT, value); + } + + public void addShuffled(boolean value) { + ((ObjectNode) getResultNode(TYPE.OBJECT)).put(SHUFFLED, value); + } + + public void addCurrentAudioStream(int channels, String codec, int bitrate) { + ObjectNode objectNode = createAudioStreamNode(channels, codec, bitrate); + ((ObjectNode) getResultNode(TYPE.OBJECT)).putObject(CURRENTAUDIOSTREAM).setAll(objectNode); + } + + public void addCurrentSubtitle(int index, String language, String name) { + ObjectNode objectNode = createSubtitleNode(index, language, name); + ((ObjectNode) getResultNode(TYPE.OBJECT)).putObject(CURRENTSUBTITLE).setAll(objectNode); + } + + public void addAudioStream(int channels, String codec, int bitrate) { + ObjectNode objectNode = createAudioStreamNode(channels, codec, bitrate); + addObjectToArray(AUDIOSTREAMS, objectNode); + } + + public void addSubtitle(int index, String language, String name) { + ObjectNode objectNode = createSubtitleNode(index, language, name); + addObjectToArray(SUBTITLES, objectNode); + } + + public void addPlaylistId(int value) { + ((ObjectNode) getResultNode(TYPE.OBJECT)).put(PLAYLISTID, value); + } + + private ObjectNode createAudioStreamNode(int channels, String codec, int bitrate) { + ObjectNode audioNode = createObjectNode(); + audioNode.put("channels", channels); + audioNode.put("codec", codec); + audioNode.put("bitrate", bitrate); + return audioNode; + } + + private ObjectNode createSubtitleNode(int index, String language, String name) { + ObjectNode subtitleNode = createObjectNode(); + subtitleNode.put("index", index); + subtitleNode.put("language", language); + subtitleNode.put("name", name); + return subtitleNode; + } + + private void addObjectToArray(String key, ObjectNode objectNode) { + ObjectNode resultNode = (ObjectNode) getResultNode(TYPE.OBJECT); + JsonNode jsonNode = resultNode.get(key); + + if(jsonNode == null) { + ArrayNode arrayNode = createArrayNode().add(objectNode); + resultNode.set(key, arrayNode); + } else if(jsonNode.isArray()) { + ((ArrayNode) jsonNode).add(objectNode); + } else { + LogUtils.LOGW("Player", "JsonNode at " + key + " is not of type ArrayNode"); + } + } + } + + public static class GetItem extends JsonResponse { + public final static String METHOD_NAME = "Player.GetItem"; + + final static String ITEM = "item"; + final static String TYPE = "type"; + final static String ART = "art"; + final static String ARTIST = "artist"; + final static String ALBUMARTIST = "albumartist"; + final static String ALBUM = "album"; + final static String CAST = "cast"; + final static String DIRECTOR = "director"; + final static String DISPLAYARTIST = "displayartist"; + final static String DURATION = "duration"; + final static String EPISODE = "episode"; + final static String FANART = "fanart"; + final static String FILE = "file"; + final static String FIRSTAIRED = "firstaired"; + final static String GENRE = "genre"; + final static String IMDBNUMBER = "imdbnumber"; + final static String PLOT = "plot"; + final static String PREMIERED = "premiered"; + final static String RATING = "rating"; + final static String RESUME = "resume"; + final static String RUNTIME = "runtime"; + final static String SEASON = "season"; + final static String SHOWTITLE = "showtitle"; + final static String STREAMDETAILS = "streamdetails"; + final static String STUDIO = "studio"; + final static String TAGLINE = "tagline"; + final static String THUMBNAIL = "thumbnail"; + final static String TITLE = "title"; + final static String TOP250 = "top250"; + final static String TRACK = "track"; + final static String VOTES = "votes"; + final static String WRITER = "writer"; + final static String YEAR = "year"; + final static String DESCRIPTION = "description"; + final static String LABEL = "label"; + + 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); + } + + public GetItem(int methodId) { + super(methodId); + 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 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); + } + + public void addArt(String banner, String poster, String fanart, String thumbnail) { + ObjectNode objectNode = createArtNode(banner, poster, fanart, thumbnail); + itemNode.putObject(ART).setAll(objectNode); + } + + public void addArtist(String artist) { + addToArrayNode(itemNode, ARTIST, artist); + } + + public void addAlbumArtist(String artist) { + addToArrayNode(itemNode, ALBUMARTIST, artist); + } + + public void addAlbum(String album) { + itemNode.put(ALBUM, album); + } + + public void addCast(String thumbnail, String name, String role) { + addToArrayNode(itemNode, CAST, createCastNode(thumbnail, name, role)); + } + + public void addDirector(String director) { + addToArrayNode(itemNode, DIRECTOR, director); + } + + public void addDisplayartist(String displayartist) { + itemNode.put(DISPLAYARTIST, displayartist); + } + + public void addDuration(int duration) { + itemNode.put(DURATION, duration); + } + + public void addEpisode(int episode) { + itemNode.put(EPISODE, episode); + } + + public void addFanart(String fanart) { + itemNode.put(FANART, fanart); + } + + public void addFile(String file) { + itemNode.put(FILE, file); + } + + public void addFirstaired(String firstaired) { + itemNode.put(FIRSTAIRED, firstaired); + } + + public void addGenre(String genre) { + itemNode.put(GENRE, genre); + } + + public void addImdbnumber(String imdbnumber) { + itemNode.put(IMDBNUMBER, imdbnumber); + } + + public void addPlot(String plot) { + itemNode.put(PLOT, plot); + } + + public void addPremiered(String premiered) { + itemNode.put(PREMIERED, premiered); + } + + public void addRating(int rating) { + itemNode.put(RATING, rating); + } + + public void addResume(int position, int total) { + itemNode.putObject(RESUME).setAll(createResumeNode(position, total)); + } + + public void addRuntime(int runtime) { + itemNode.put(RUNTIME, runtime); + } + + public void addSeason(int season) { + itemNode.put(SEASON, season); + } + + public void addShowtitle(String showtitle) { + itemNode.put(SHOWTITLE, showtitle); + } + + public void addStreamdetails(AudioDetailsNode audioDetailsNode, + VideoDetailsNode videoDetailsNode, + SubtitleDetailsNode subtitleDetailsNode) { + ObjectNode objectNode = createObjectNode(); + objectNode.putObject("audio").setAll(audioDetailsNode.getResponseNode()); + objectNode.putObject("video").setAll(videoDetailsNode.getResponseNode()); + objectNode.putObject("subtitle").setAll(subtitleDetailsNode.getResponseNode()); + + itemNode.set(STREAMDETAILS, objectNode); + } + + public void addStudio(String studio) { + addToArrayNode(itemNode, STUDIO, studio); + } + + public void addTagline(String tagline) { + itemNode.put(TAGLINE, tagline); + } + + public void addThumbnail(String thumbnail) { + itemNode.put(THUMBNAIL, thumbnail); + } + + public void addTitle(String title) { + itemNode.put(TITLE, title); + } + + public String getTitle() { + JsonNode jsonNode = itemNode.get(TITLE); + if (jsonNode != null) + return jsonNode.asText(); + else + return null; + } + + public void addTop250(int top250) { + itemNode.put(TOP250, top250); + } + + public void addTrack(int track) { + itemNode.put(TRACK, track); + } + + public void addVotes(String votes) { + itemNode.put(VOTES, votes); + } + + public void addWriter(String writer) { + addToArrayNode(itemNode, WRITER, writer); + } + + public void addYear(int year) { + itemNode.put(YEAR, year); + } + + public void addDescription(String description) { + itemNode.put(DESCRIPTION, description); + } + + public void addLabel(String label) { + itemNode.put(LABEL, label); + } + + private ObjectNode createArtNode(String banner, + String poster, + String fanart, + String thumbnail) { + ObjectNode objectNode = createObjectNode(); + objectNode.put("poster", poster); + objectNode.put("fanart", fanart); + objectNode.put("thumbnail", thumbnail); + objectNode.put("banner", banner); + return objectNode; + } + + private ObjectNode createArtworkNode(String banner, String poster, String fanart, String thumbnail) { + ObjectNode objectNode = createObjectNode(); + objectNode.put("poster", poster); + objectNode.put("fanart", fanart); + objectNode.put("thumbnail", thumbnail); + return objectNode; + } + + private ObjectNode createCastNode(String thumbnail, String name, String role) { + ObjectNode objectNode = createObjectNode(); + objectNode.put("thumbnail", thumbnail); + objectNode.put("name", name); + objectNode.put("role", role); + return objectNode; + } + + private ObjectNode createResumeNode(int position, int total) { + ObjectNode objectNode = createObjectNode(); + objectNode.put("position", position); + objectNode.put("total", total); + return objectNode; + } + } +} diff --git a/app/src/testUtils/java/org/xbmc/kore/testutils/tcpserver/handlers/jsonrpc/response/notifications/Player.java b/app/src/testUtils/java/org/xbmc/kore/testutils/tcpserver/handlers/jsonrpc/response/notifications/Player.java new file mode 100644 index 0000000..f641e09 --- /dev/null +++ b/app/src/testUtils/java/org/xbmc/kore/testutils/tcpserver/handlers/jsonrpc/response/notifications/Player.java @@ -0,0 +1,157 @@ +/* + * Copyright 2016 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; +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); + + ObjectNode itemNode = createObjectNode(); + itemNode.put("id", itemId); + if (itemType != null) + itemNode.put("type", itemType); + addDataToResponse("item", itemNode); + + itemNode = createObjectNode(); + itemNode.put("playerid", playerId); + itemNode.put("speed", speed); + addDataToResponse("player", itemNode); + + addParameterToResponse("sender", "xbmc"); + } + } + + /** + * JSON response for Player.OnSpeedChanged notification + * + * Example: + * Answer: {"jsonrpc":"2.0","method":"Player.OnSpeedChanged","params":{"data":{"item":{"id":94,"type":"song"},"player":{"playerid":0,"speed":0}},"sender":"xbmc"}} + */ + public static class OnSpeedChanged extends PlayPause { + public final static String METHOD_NAME = "Player.OnSpeedChanged"; + + public OnSpeedChanged(int itemId, String itemType, int playerId, int speed) { + super(METHOD_NAME, itemId, itemType, playerId, speed); + } + } + + /** + * JSON response for Player.OnPause notification + * + * Example: + * Answer: {"jsonrpc":"2.0","method":"Player.OnPause","params":{"data":{"item":{"id":94,"type":"song"},"player":{"playerid":0,"speed":0}},"sender":"xbmc"}} + */ + public static class OnPause extends PlayPause { + public final static String METHOD_NAME = "Player.OnPause"; + + public OnPause(int itemId, String itemType, int playerId, int speed) { + super(METHOD_NAME, itemId, itemType, playerId, speed); + } + } + + /** + * JSON response for Player.OnPlay notification + * + * Example: + * Answer: {"jsonrpc":"2.0","method":"Player.OnPlay","params":{"data":{"item":{"id":1580,"type":"song"},"player":{"playerid":0,"speed":1}},"sender":"xbmc"}} + */ + public static class OnPlay extends PlayPause { + public final static String METHOD_NAME = "Player.OnPlay"; + + public OnPlay(int itemId, String itemType, int playerId, int speed) { + super(METHOD_NAME, itemId, itemType, playerId, speed); + } + } + + /** + * JSON response for Player.OnPropertyChanged notification + * + * Example: + * {"jsonrpc":"2.0","method":"Player.OnPropertyChanged","params":{"data":{"player":{"playerid":0},"property":{"repeat":"all"}},"sender":"xbmc"}} + */ + public static class OnPropertyChanged extends JsonResponse { + public final static String METHOD_NAME = "Player.OnPropertyChanged"; + + public OnPropertyChanged(String repeatType, Boolean shuffled, int playerId) { + super(); + addMethodToResponse(METHOD_NAME); + + ObjectNode playerIdNode = createObjectNode(); + playerIdNode.put("playerid", playerId); + addDataToResponse("player", playerIdNode); + + if (repeatType != null) { + ObjectNode repeatNode = createObjectNode(); + repeatNode.put("repeat", repeatType); + addDataToResponse("property", repeatNode); + } + + if (shuffled != null) { + ObjectNode repeatNode = createObjectNode(); + repeatNode.put("shuffled", shuffled); + addDataToResponse("property", repeatNode); + } + + addParameterToResponse("sender", "xbmc"); + } + } + + /** + * JSON response for Player.OnPropertyChanged 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" }} + */ + public static class OnSeek extends JsonResponse { + public final static String METHOD_NAME = "Player.OnSeek"; + + public OnSeek(int itemId, String type, int playerId, int speed, long seekOffsetSecs, long timeSecs) { + super(); + addMethodToResponse(METHOD_NAME); + + ObjectNode itemNode = createObjectNode(); + itemNode.put("id", itemId); + itemNode.put("type", type); + addDataToResponse("item", itemNode); + + ObjectNode playerNode = createObjectNode(); + playerNode.put("playerid", playerId); + playerNode.set("seekoffset", JsonUtils.createTimeNode(createObjectNode(), seekOffsetSecs)); + playerNode.set("time", JsonUtils.createTimeNode(createObjectNode(), timeSecs)); + playerNode.put("speed", speed); + addDataToResponse("player", playerNode); + + addParameterToResponse("sender", "xbmc"); + } + } +}