Repo created
This commit is contained in:
parent
5b950caea0
commit
477d1afe74
805 changed files with 316919 additions and 2 deletions
1
app/.gitignore
vendored
Normal file
1
app/.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
/build
|
||||
173
app/build.gradle
Normal file
173
app/build.gradle
Normal file
|
|
@ -0,0 +1,173 @@
|
|||
apply plugin: 'com.android.application'
|
||||
|
||||
def getVersionName = { ->
|
||||
def stdout = new ByteArrayOutputStream()
|
||||
exec {
|
||||
commandLine 'git', 'describe', '--tags', '--always'
|
||||
standardOutput = stdout
|
||||
}
|
||||
return stdout.toString().trim()
|
||||
}
|
||||
|
||||
android {
|
||||
compileSdk 34
|
||||
|
||||
defaultConfig {
|
||||
applicationId "org.xbmc.kore"
|
||||
minSdkVersion 24
|
||||
targetSdkVersion 34
|
||||
versionCode 33
|
||||
versionName = getVersionName()
|
||||
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
|
||||
|
||||
def supportedLocales = ["en",
|
||||
"af-za", "ast", "be-by", "bg", "ca", "cs", "da-dk", "de",
|
||||
"es", "es-MX", "eu", "fi", "fr", "hr", "hu", "it", "iw", "ja",
|
||||
"ko", "lt", "nl", "pl", "pt", "pt-BR", "ru", "sk", "sl", "zh-CN"]
|
||||
buildConfigField "String[]", "SUPPORTED_LOCALES", "new String[]{\""+
|
||||
supportedLocales.join("\",\"")+"\"}"
|
||||
}
|
||||
|
||||
signingConfigs {
|
||||
release {
|
||||
if (System.getenv("KODI_ANDROID_STORE_FILE") != null) {
|
||||
keyAlias System.getenv("KODI_ANDROID_KEY_ALIAS")
|
||||
keyPassword System.getenv("KODI_ANDROID_KEY_PASSWORD")
|
||||
storeFile file(System.getenv("KODI_ANDROID_STORE_FILE"))
|
||||
storePassword System.getenv("KODI_ANDROID_STORE_PASSWORD")
|
||||
enableV1Signing true
|
||||
enableV2Signing true
|
||||
enableV3Signing true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
testOptions {
|
||||
execution 'ANDROIDX_TEST_ORCHESTRATOR'
|
||||
unitTests {
|
||||
includeAndroidResources = true
|
||||
}
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
if (System.getenv("KODI_ANDROID_STORE_FILE") != null) {
|
||||
signingConfig signingConfigs.release
|
||||
zipAlignEnabled true
|
||||
}
|
||||
minifyEnabled true
|
||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
}
|
||||
|
||||
buildFeatures {
|
||||
viewBinding true
|
||||
}
|
||||
|
||||
packagingOptions {
|
||||
resources {
|
||||
excludes += ['META-INF/DEPENDENCIES', 'META-INF/NOTICE', 'META-INF/LICENSE', 'META-INF/LICENSE.txt', 'META-INF/NOTICE.txt']
|
||||
}
|
||||
}
|
||||
|
||||
lint {
|
||||
// Too much trouble keeping all translations in sync
|
||||
disable 'MissingTranslation'
|
||||
}
|
||||
|
||||
bundle {
|
||||
language {
|
||||
enableSplit = false
|
||||
}
|
||||
}
|
||||
namespace 'org.xbmc.kore'
|
||||
|
||||
}
|
||||
|
||||
java {
|
||||
toolchain {
|
||||
languageVersion = JavaLanguageVersion.of(17)
|
||||
}
|
||||
}
|
||||
|
||||
ext {
|
||||
supportLibVersion = '28.0.0'
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation 'com.google.android.material:material:1.10.0'
|
||||
implementation 'androidx.preference:preference:1.2.1'
|
||||
implementation 'androidx.appcompat:appcompat:1.6.1'
|
||||
implementation 'androidx.recyclerview:recyclerview:1.3.2'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
|
||||
implementation('androidx.core:core-google-shortcuts:1.1.0') {
|
||||
exclude group:'com.google.android.gms'
|
||||
}
|
||||
implementation 'androidx.media:media:1.6.0'
|
||||
implementation "androidx.viewpager2:viewpager2:1.1.0-beta02"
|
||||
implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0"
|
||||
|
||||
implementation "androidx.fragment:fragment:1.6.2"
|
||||
implementation "androidx.fragment:fragment-ktx:1.6.2"
|
||||
|
||||
// Dependency resolution
|
||||
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.2"
|
||||
implementation "androidx.lifecycle:lifecycle-viewmodel:2.6.2"
|
||||
|
||||
// Jackson v2.13 kept. v2.14 and later require minSDK >= 26
|
||||
// https://github.com/FasterXML/jackson/wiki/Jackson-Releases
|
||||
implementation 'com.fasterxml.jackson.core:jackson-databind:2.13.5'
|
||||
implementation 'com.squareup.okhttp3:okhttp:4.12.0'
|
||||
// Ignore the new version warning as it refers to v2.71... which is older on maven
|
||||
implementation 'com.squareup.picasso:picasso:2.8'
|
||||
implementation 'org.greenrobot:eventbus:3.3.1'
|
||||
implementation 'org.jmdns:jmdns:3.5.8'
|
||||
implementation 'at.blogc:expandabletextview:1.0.5'
|
||||
implementation 'com.simplecityapps:recyclerview-fastscroll:2.0.1'
|
||||
implementation 'org.nanohttpd:nanohttpd:2.3.1'
|
||||
|
||||
implementation fileTree(dir: 'libs', include: ['*.jar'])
|
||||
|
||||
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
|
||||
androidTestImplementation 'androidx.test:rules:1.5.0'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-contrib:3.5.1'
|
||||
androidTestImplementation 'androidx.legacy:legacy-support-v13:1.0.0'
|
||||
androidTestImplementation 'org.hamcrest:hamcrest-library:2.2'
|
||||
androidTestImplementation 'junit:junit:4.13.2'
|
||||
androidTestUtil 'androidx.test:orchestrator:1.4.2'
|
||||
|
||||
testImplementation 'org.robolectric:robolectric:4.8.1'
|
||||
testImplementation 'androidx.test:core:1.5.0'
|
||||
testImplementation 'androidx.test.ext:junit:1.1.5'
|
||||
debugImplementation 'junit:junit:4.13.2'
|
||||
}
|
||||
|
||||
def adb = android.getAdbExecutable().toString()
|
||||
afterEvaluate {
|
||||
tasks.register('grantAnimationPermissionDev', Exec) {
|
||||
dependsOn installDebug
|
||||
doFirst {
|
||||
println("Executing: $adb shell pm grant $android.defaultConfig.applicationId android.permission.SET_ANIMATION_SCALE")
|
||||
commandLine "$adb shell pm grant $android.defaultConfig.applicationId android.permission.SET_ANIMATION_SCALE".split(' ')
|
||||
}
|
||||
}
|
||||
tasks.each { task ->
|
||||
if (task.name.startsWith('connectedDebugAndroidTest')) {
|
||||
task.dependsOn grantAnimationPermissionDev
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes sure assets are copied before running the unit tests
|
||||
*/
|
||||
tasks.configureEach { task ->
|
||||
if (task.name.contains("testDebugUnitTest")) {
|
||||
task.dependsOn assembleDebug
|
||||
}
|
||||
}
|
||||
|
||||
tasks.withType(JavaCompile).configureEach {
|
||||
options.compilerArgs << '-Xlint:unchecked' // << '-Xlint:deprecation'
|
||||
}
|
||||
44
app/proguard-rules.pro
vendored
Normal file
44
app/proguard-rules.pro
vendored
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
# Add project specific ProGuard rules here.
|
||||
# By default, the flags in this file are appended to flags specified
|
||||
# in /opt/android-sdk/tools/proguard/proguard-android.txt
|
||||
# You can edit the include path and order by changing the proguardFiles
|
||||
# directive in build.gradle.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# Add any project specific keep options here:
|
||||
|
||||
# Don't obfuscate for now. Obfuscation decreases apk size by about 300k
|
||||
-dontobfuscate
|
||||
|
||||
# Picasso
|
||||
-dontwarn com.squareup.okhttp.**
|
||||
|
||||
# okio via OkHttp
|
||||
-dontwarn okio.**
|
||||
|
||||
# Jackson
|
||||
-dontskipnonpubliclibraryclassmembers
|
||||
-keepattributes EnclosingMethod, Signature
|
||||
#-keep class org.codehaus.** { *; }
|
||||
-keepnames class com.fasterxml.jackson.** { *; }
|
||||
-dontwarn com.fasterxml.jackson.databind.**
|
||||
|
||||
# EventBus
|
||||
-keepclassmembers class ** {
|
||||
public void onEvent*(**);
|
||||
}
|
||||
|
||||
# SearchView
|
||||
-keep class androidx.appcompat.widget.SearchView { *; }
|
||||
|
||||
#JmDNS
|
||||
-dontwarn org.slf4j.*
|
||||
|
||||
# If your project uses WebView with JS, uncomment the following
|
||||
# and specify the fully qualified class name to the JavaScript interface
|
||||
# class:
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
19
app/src/androidTest/README.md
Normal file
19
app/src/androidTest/README.md
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
Integration tests that need to be executed on an Android device.
|
||||
|
||||
## Run tests
|
||||
|
||||
You can run the tests as follows:
|
||||
|
||||
### Android Studio
|
||||
|
||||
1. Select build variant "instrumentationTestDebug"
|
||||
2. Set the [Project view](https://developer.android.com/studio/projects/index.html) to Android
|
||||
3. Right-click on the directory "androidTest" and select "Run tests"
|
||||
|
||||
### Commandline
|
||||
|
||||
Run the following command from the top of the project:
|
||||
|
||||
./gradlew connectedInstrumentationTestDebugAndroidTest
|
||||
|
||||
This will run the tests on all connected devices in parallel
|
||||
|
|
@ -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.testhelpers;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.pm.ActivityInfo;
|
||||
import android.content.res.Configuration;
|
||||
import android.view.View;
|
||||
import android.widget.AutoCompleteTextView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.test.espresso.Espresso;
|
||||
import androidx.test.espresso.NoMatchingViewException;
|
||||
import androidx.test.espresso.UiController;
|
||||
import androidx.test.espresso.ViewAction;
|
||||
import androidx.test.espresso.ViewInteraction;
|
||||
import androidx.test.espresso.contrib.RecyclerViewActions;
|
||||
|
||||
import com.sothree.slidinguppanel.SlidingUpPanelLayout;
|
||||
|
||||
import org.hamcrest.Matcher;
|
||||
import org.xbmc.kore.R;
|
||||
import org.xbmc.kore.testhelpers.action.ViewActions;
|
||||
import org.xbmc.kore.ui.widgets.NowPlayingPanel;
|
||||
|
||||
import static androidx.test.espresso.Espresso.onData;
|
||||
import static androidx.test.espresso.Espresso.onView;
|
||||
import static androidx.test.espresso.Espresso.openActionBarOverflowOrOptionsMenu;
|
||||
import static androidx.test.espresso.Espresso.pressBack;
|
||||
import static androidx.test.espresso.action.ViewActions.clearText;
|
||||
import static androidx.test.espresso.action.ViewActions.click;
|
||||
import static androidx.test.espresso.action.ViewActions.closeSoftKeyboard;
|
||||
import static androidx.test.espresso.action.ViewActions.typeText;
|
||||
import static androidx.test.espresso.assertion.ViewAssertions.doesNotExist;
|
||||
import static androidx.test.espresso.assertion.ViewAssertions.matches;
|
||||
import static androidx.test.espresso.matcher.ViewMatchers.hasDescendant;
|
||||
import static androidx.test.espresso.matcher.ViewMatchers.isAssignableFrom;
|
||||
import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
|
||||
import static androidx.test.espresso.matcher.ViewMatchers.isRoot;
|
||||
import static androidx.test.espresso.matcher.ViewMatchers.withContentDescription;
|
||||
import static androidx.test.espresso.matcher.ViewMatchers.withId;
|
||||
import static androidx.test.espresso.matcher.ViewMatchers.withParent;
|
||||
import static androidx.test.espresso.matcher.ViewMatchers.withText;
|
||||
import static org.hamcrest.CoreMatchers.anything;
|
||||
import static org.hamcrest.Matchers.allOf;
|
||||
import static org.hamcrest.Matchers.containsString;
|
||||
import static org.hamcrest.Matchers.instanceOf;
|
||||
import static org.xbmc.kore.testhelpers.action.ViewActions.clearFocus;
|
||||
|
||||
public class EspressoTestUtils {
|
||||
|
||||
public static void rotateDevice(Activity activity) {
|
||||
int orientation
|
||||
= activity.getResources().getConfiguration().orientation;
|
||||
activity.setRequestedOrientation(
|
||||
(orientation == Configuration.ORIENTATION_PORTRAIT) ?
|
||||
ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE :
|
||||
ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clicks a menu item regardless if it is in the overflow menu or
|
||||
* visible as icon in the action bar
|
||||
* @param activity
|
||||
* @param name Name of the menu item in the overflow menu
|
||||
* @param resourceId Resource identifier of the menu item
|
||||
*/
|
||||
public static void clickMenuItem(Activity activity, String name, int resourceId) {
|
||||
try {
|
||||
onView(withId(resourceId)).perform(click());
|
||||
} catch (NoMatchingViewException e) {
|
||||
openActionBarOverflowOrOptionsMenu(activity);
|
||||
//Use onData as item might not be visible in the View without scrolling
|
||||
onData(allOf(
|
||||
Matchers.withMenuTitle(name)))
|
||||
.perform(click());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clicks the arrow button in the toolbar when its function is collapsing a view. For instance,
|
||||
* collapse the search view in the toolbar.
|
||||
*/
|
||||
public static void clickToolbarCollapseButton() {
|
||||
/**
|
||||
* The image button in the toolbar used as home/collapse/back button has no ID we can use.
|
||||
* In appcompat v7 the arrow button in the toolbar used to collapse a search view has a
|
||||
* description "Collapse". We use this to find the button in the view and perform the click
|
||||
* action.
|
||||
*/
|
||||
onView(withContentDescription("Collapse")).perform(click());
|
||||
}
|
||||
|
||||
/**
|
||||
* Clicks the button with given resourceId and checks if the
|
||||
* button is displayed. As we occasionally use the same identifiers
|
||||
* in multiple fragments, we need to check if it is visible as well
|
||||
* to prevent Espresso from finding multiple views that match the
|
||||
* resource identifier.
|
||||
* @param resourceId
|
||||
*/
|
||||
public static void clickButton(int resourceId) {
|
||||
onView(allOf(withId(resourceId), isDisplayed())).perform(click());
|
||||
}
|
||||
|
||||
/**
|
||||
* Clicks on the search menu item and enters the given search query
|
||||
* @param activity
|
||||
* @param query
|
||||
*/
|
||||
public static void enterSearchQuery(Activity activity, String query) {
|
||||
EspressoTestUtils.clickMenuItem(activity, activity.getString(R.string.action_search), R.id.action_search);
|
||||
|
||||
onView(isAssignableFrom(AutoCompleteTextView.class))
|
||||
.perform(click(), typeText(query), closeSoftKeyboard());
|
||||
onView(isRoot()).perform(clearFocus());
|
||||
}
|
||||
|
||||
/**
|
||||
* Clicks on the search menu item and clears the search query by entering the empty string
|
||||
* @param activity
|
||||
*/
|
||||
public static void clearSearchQuery(Activity activity) {
|
||||
EspressoTestUtils.clickMenuItem(activity, activity.getString(R.string.action_search), R.id.action_search);
|
||||
|
||||
onView(isAssignableFrom(AutoCompleteTextView.class))
|
||||
.perform(click(), clearText(), closeSoftKeyboard());
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the search query by pressing the X button
|
||||
* @param activity
|
||||
*/
|
||||
public static void clearSearchQueryXButton(Activity activity) {
|
||||
try {
|
||||
onView(withId(R.id.search_close_btn)).perform(click());
|
||||
} catch (NoMatchingViewException e) {
|
||||
EspressoTestUtils.clickMenuItem(activity, activity.getString(R.string.action_search), R.id.action_search);
|
||||
onView(withId(R.id.search_close_btn)).perform(click());
|
||||
}
|
||||
Espresso.closeSoftKeyboard();
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs a click on an item in an adapter view, such as GridView or ListView
|
||||
* @param position
|
||||
* @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());
|
||||
}
|
||||
|
||||
|
||||
public static void clickRecyclerViewItem(int position, int resourceId) {
|
||||
onView(withId(resourceId)).perform(RecyclerViewActions.actionOnItemAtPosition(position, click()));
|
||||
}
|
||||
|
||||
public static void clickRecyclerViewItem(String text, int resourceId) {
|
||||
ViewInteraction viewInteraction = onView(allOf(withId(resourceId),
|
||||
hasDescendant(withText(containsString(text))),
|
||||
isDisplayed()));
|
||||
viewInteraction.perform(RecyclerViewActions.scrollTo(hasDescendant(withText(containsString(text)))));
|
||||
viewInteraction.perform(RecyclerViewActions.actionOnItem(hasDescendant(withText(containsString(text))),
|
||||
click()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks that SearchView contains the given text
|
||||
* @param query text that SearchView should contain
|
||||
*/
|
||||
public static void checkTextInSearchQuery(String query) {
|
||||
onView(isAssignableFrom(AutoCompleteTextView.class)).check(matches(withText(query)));
|
||||
}
|
||||
|
||||
/**
|
||||
* 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)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks that the list size matches the given list size
|
||||
* @param listSize amount of elements expected in list
|
||||
*/
|
||||
public static void checkListViewSize(int listSize, int resourceId) {
|
||||
onView(allOf(withId(resourceId), isDisplayed()))
|
||||
.check(matches(Matchers.withListViewSize(listSize)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if search action view does not exist in the current view hierarchy
|
||||
*/
|
||||
public static void checkSearchMenuCollapsed() {
|
||||
onView(isAssignableFrom(AutoCompleteTextView.class)).check(doesNotExist());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current active activity. Use this when the originally started activity
|
||||
* started a new activity and you need the reference to the new activity.
|
||||
* @return reference to the current active activity
|
||||
*/
|
||||
public static Activity getActivity() {
|
||||
final Activity[] activity = new Activity[1];
|
||||
onView(allOf(withId(android.R.id.content), isDisplayed())).perform(new ViewAction() {
|
||||
@Override
|
||||
public Matcher<View> getConstraints() {
|
||||
return isAssignableFrom(View.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDescription() {
|
||||
return "getting current activity";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void perform(UiController uiController, View view) {
|
||||
if (view.getContext() instanceof Activity) {
|
||||
activity[0] = ((Activity)view.getContext());
|
||||
}
|
||||
}
|
||||
});
|
||||
return activity[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Clicks on tab that contains the text given by stringResourceId.
|
||||
* @param stringResourceId text displayed in Tab that should be clicked
|
||||
*/
|
||||
public static void clickTab(int stringResourceId) {
|
||||
onView(withId(R.id.tab_layout)).perform(ViewActions.setCurrentViewPagerItem(stringResourceId));
|
||||
}
|
||||
|
||||
/**
|
||||
* Clicks the album tab in the music activity
|
||||
*/
|
||||
public static void clickAlbumsTab() {
|
||||
clickTab(R.string.albums);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clicks the artists tab in the music activity
|
||||
*/
|
||||
public static void clickArtistsTab() {
|
||||
clickTab(R.string.artists);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clicks the genres tab in the music activity
|
||||
*/
|
||||
public static void clickGenresTab() {
|
||||
clickTab(R.string.genres);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clicks the music videos tab in the music activity
|
||||
*/
|
||||
public static void clickMusicVideosTab() {
|
||||
clickTab(R.string.videos);
|
||||
}
|
||||
|
||||
/**
|
||||
* Selects an item in the list, then presses back and checks the action bar title
|
||||
* @param item number (0 is first item) of the item that should be pressed
|
||||
* @param listResourceId Resource identifier of the AdapterView
|
||||
* @param actionbarTitle title that should be displayed in the action bar after pressing back
|
||||
*/
|
||||
public static void selectListItemPressBackAndCheckActionbarTitle(int item,
|
||||
int listResourceId,
|
||||
String actionbarTitle) {
|
||||
EspressoTestUtils.clickRecyclerViewItem(item, listResourceId);
|
||||
pressBack();
|
||||
onView(allOf(instanceOf(TextView.class), withParent(withId(R.id.default_toolbar))))
|
||||
.check(matches(withText(actionbarTitle)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Selects an item in the list, then presses back and checks the action bar title
|
||||
* @param itemText the text the item that must be pressed should contain
|
||||
* @param listResourceId Resource identifier of the AdapterView
|
||||
* @param actionbarTitle title that should be displayed in the action bar after pressing back
|
||||
*/
|
||||
public static void selectListItemPressBackAndCheckActionbarTitle(String itemText,
|
||||
int listResourceId,
|
||||
String actionbarTitle) {
|
||||
EspressoTestUtils.clickRecyclerViewItem(itemText, listResourceId);
|
||||
pressBack();
|
||||
onView(allOf(instanceOf(TextView.class), withParent(withId(R.id.default_toolbar))))
|
||||
.check(matches(withText(containsString(actionbarTitle))));
|
||||
}
|
||||
|
||||
/**
|
||||
* Selects an item in the list, then rotates the device and checks the action bar title
|
||||
* @param itemText the text the item that must be pressed should contain
|
||||
* @param listResourceId Resource identifier of the AdapterView
|
||||
* @param actionbarTitle title that should be displayed in the action bar after rotating
|
||||
*/
|
||||
public static void selectListItemRotateDeviceAndCheckActionbarTitle(String itemText,
|
||||
int listResourceId,
|
||||
final String actionbarTitle,
|
||||
Activity activity) {
|
||||
EspressoTestUtils.clickRecyclerViewItem(itemText, listResourceId);
|
||||
EspressoTestUtils.rotateDevice(activity);
|
||||
|
||||
onView(allOf(instanceOf(TextView.class), withParent(withId(R.id.default_toolbar))))
|
||||
.check(matches(withText(containsString(actionbarTitle))));
|
||||
}
|
||||
|
||||
/**
|
||||
* Selects an item in the list and then checks the action bar title
|
||||
* @param itemText the text the item that must be pressed should contain
|
||||
* @param listResourceId Resource identifier of the AdapterView
|
||||
* @param actionbarTitle title that should be displayed in the action bar after selecting item
|
||||
*/
|
||||
public static void selectListItemAndCheckActionbarTitle(String itemText,
|
||||
int listResourceId,
|
||||
String actionbarTitle) {
|
||||
EspressoTestUtils.clickRecyclerViewItem(itemText, listResourceId);
|
||||
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 int panelState) {
|
||||
onView(isRoot()).perform(ViewActions.waitForView(R.id.now_playing_panel, new ViewActions.CheckStatus() {
|
||||
@Override
|
||||
public boolean check(View v) {
|
||||
return ((NowPlayingPanel)v).getPanelState() == panelState;
|
||||
}
|
||||
}, 10000));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
/*
|
||||
* 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.testhelpers;
|
||||
|
||||
import androidx.loader.app.LoaderManager;
|
||||
import androidx.test.espresso.IdlingResource;
|
||||
|
||||
public class LoaderIdlingResource implements IdlingResource {
|
||||
|
||||
private ResourceCallback mResourceCallback;
|
||||
private LoaderManager loaderManager;
|
||||
|
||||
public LoaderIdlingResource(LoaderManager loaderManager) {
|
||||
this.loaderManager = loaderManager;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return LoaderIdlingResource.class.getName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isIdleNow() {
|
||||
boolean idle = !loaderManager.hasRunningLoaders();
|
||||
if (idle && mResourceCallback != null) {
|
||||
mResourceCallback.onTransitionToIdle();
|
||||
}
|
||||
return idle;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void registerIdleTransitionCallback(ResourceCallback resourceCallback) {
|
||||
mResourceCallback = resourceCallback;
|
||||
}
|
||||
}
|
||||
217
app/src/androidTest/java/org/xbmc/kore/testhelpers/Matchers.java
Normal file
217
app/src/androidTest/java/org/xbmc/kore/testhelpers/Matchers.java
Normal file
|
|
@ -0,0 +1,217 @@
|
|||
/*
|
||||
* 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.testhelpers;
|
||||
|
||||
import android.database.Cursor;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.widget.ListView;
|
||||
import android.widget.SeekBar;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import androidx.test.espresso.matcher.BoundedMatcher;
|
||||
import androidx.test.espresso.matcher.CursorMatchers;
|
||||
|
||||
import org.hamcrest.BaseMatcher;
|
||||
import org.hamcrest.Description;
|
||||
import org.hamcrest.Matcher;
|
||||
import org.hamcrest.TypeSafeMatcher;
|
||||
import org.xbmc.kore.ui.widgets.HighlightButton;
|
||||
import org.xbmc.kore.ui.widgets.RepeatModeButton;
|
||||
import org.xbmc.kore.utils.UIUtils;
|
||||
|
||||
public class Matchers {
|
||||
public static MenuItemTitleMatcher withMenuTitle(String title) {
|
||||
return new MenuItemTitleMatcher(title);
|
||||
}
|
||||
|
||||
public static class MenuItemTitleMatcher extends BaseMatcher<Object> {
|
||||
private final String title;
|
||||
public MenuItemTitleMatcher(String title) { this.title = title; }
|
||||
|
||||
@Override
|
||||
public boolean matches(Object o) {
|
||||
if (o instanceof MenuItem) {
|
||||
return ((MenuItem) o).getTitle().equals(title);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@Override
|
||||
public void describeTo(Description description) { }
|
||||
}
|
||||
|
||||
public static Matcher<View> withListViewSize(final int size) {
|
||||
return new TypeSafeMatcher<View>() {
|
||||
@Override public boolean matchesSafely(final View view) {
|
||||
return (view instanceof ListView) &&
|
||||
((ListView) view).getAdapter().getCount() == size;
|
||||
}
|
||||
|
||||
@Override public void describeTo(final Description description) {
|
||||
description.appendText("List should have " + size + " item(s)");
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public static Matcher<View> withRecyclerViewSize(final int size) {
|
||||
return new TypeSafeMatcher<View>() {
|
||||
@Override
|
||||
protected boolean matchesSafely(View view) {
|
||||
return (view instanceof RecyclerView) &&
|
||||
((RecyclerView) view).getAdapter().getItemCount() == size;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void describeTo(Description description) {
|
||||
description.appendText("RecyclerView should have " + size + " item(s)");
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public static Matcher<View> withOnlyMatchingDataItems(final Matcher<View> dataMatcher) {
|
||||
return new TypeSafeMatcher<View>() {
|
||||
@Override
|
||||
protected boolean matchesSafely(View view) {
|
||||
if (!(view instanceof RecyclerView))
|
||||
return false;
|
||||
|
||||
RecyclerView recyclerView = (RecyclerView) view;
|
||||
for (int i = 0; i < recyclerView.getChildCount(); i++) {
|
||||
RecyclerView.ViewHolder viewHolder = recyclerView.findViewHolderForAdapterPosition(i);
|
||||
if (! dataMatcher.matches(viewHolder.itemView)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void describeTo(Description description) {
|
||||
description.appendText("withOnlyMatchingDataItems: ");
|
||||
dataMatcher.describeTo(description);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public static Matcher<Object> withItemContent(final Matcher<String> textMatcher) {
|
||||
return new BoundedMatcher<Object, Cursor>(Cursor.class) {
|
||||
@Override
|
||||
protected boolean matchesSafely(Cursor item) {
|
||||
for (int i = 0; i < item.getColumnCount();i++) {
|
||||
switch (item.getType(i)) {
|
||||
case Cursor.FIELD_TYPE_STRING:
|
||||
if (CursorMatchers.withRowString(i, textMatcher).matches(item))
|
||||
return true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void describeTo(Description description) {
|
||||
description.appendText("withItemContent: ");
|
||||
textMatcher.describeTo(description);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public static Matcher<View> withProgress(final int progress) {
|
||||
return new BoundedMatcher<View, SeekBar>(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<View> withProgress(final String progress) {
|
||||
return new BoundedMatcher<View, TextView>(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<View> withProgressGreaterThanOrEqual(final String time) {
|
||||
return new BoundedMatcher<View, SeekBar>(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<View> withProgressGreaterThan(final int progress) {
|
||||
return new BoundedMatcher<View, SeekBar>(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<View> withHighlightState(final boolean highlight) {
|
||||
return new BoundedMatcher<View, HighlightButton>(HighlightButton.class) {
|
||||
@Override
|
||||
protected boolean matchesSafely(HighlightButton item) {
|
||||
return item.isHighlighted();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void describeTo(Description description) {
|
||||
description.appendText("expected: " + highlight);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public static Matcher<View> withRepeatMode(final RepeatModeButton.MODE mode) {
|
||||
return new BoundedMatcher<View, RepeatModeButton>(RepeatModeButton.class) {
|
||||
@Override
|
||||
protected boolean matchesSafely(RepeatModeButton item) {
|
||||
return item.getMode() == mode;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void describeTo(Description description) {
|
||||
description.appendText("expected: " + mode.name());
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
/*
|
||||
* 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.testhelpers;
|
||||
|
||||
import org.xbmc.kore.testutils.tcpserver.handlers.InputHandler;
|
||||
|
||||
import static junit.framework.Assert.assertTrue;
|
||||
import static org.xbmc.kore.tests.ui.AbstractTestClass.getInputHandler;
|
||||
|
||||
public class TestUtils {
|
||||
/**
|
||||
* Tests if the event received at the server matches the given
|
||||
* method name and action
|
||||
* @param methodName name of the method that should be received serverside.
|
||||
* @param executeAction name of the action that should be received serverside. May be null if the input does not specify an action.
|
||||
*/
|
||||
public static void testHTTPEvent(String methodName, String executeAction) {
|
||||
InputHandler inputHandler = getInputHandler();
|
||||
assertTrue(inputHandler != null);
|
||||
|
||||
String methodNameReceived = inputHandler.getMethodName();
|
||||
assertTrue(methodNameReceived != null);
|
||||
assertTrue(methodNameReceived.contentEquals(methodName));
|
||||
|
||||
if (executeAction != null) {
|
||||
String actionReceived = inputHandler.getAction();
|
||||
assertTrue(actionReceived != null);
|
||||
assertTrue(actionReceived.contentEquals(executeAction));
|
||||
}
|
||||
}
|
||||
}
|
||||
148
app/src/androidTest/java/org/xbmc/kore/testhelpers/Utils.java
Normal file
148
app/src/androidTest/java/org/xbmc/kore/testhelpers/Utils.java
Normal file
|
|
@ -0,0 +1,148 @@
|
|||
/*
|
||||
* 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.testhelpers;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.os.IBinder;
|
||||
import android.util.Log;
|
||||
import android.view.Gravity;
|
||||
|
||||
import androidx.drawerlayout.widget.DrawerLayout;
|
||||
import androidx.preference.PreferenceManager;
|
||||
import androidx.test.rule.ActivityTestRule;
|
||||
|
||||
import org.xbmc.kore.R;
|
||||
import org.xbmc.kore.host.HostInfo;
|
||||
import org.xbmc.kore.host.HostManager;
|
||||
import org.xbmc.kore.provider.MediaProvider;
|
||||
import org.xbmc.kore.ui.AbstractTabsFragment;
|
||||
import org.xbmc.kore.ui.sections.hosts.HostFragmentManualConfiguration;
|
||||
import org.xbmc.kore.utils.LogUtils;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
import static org.xbmc.kore.ui.generic.NavigationDrawerFragment.PREF_USER_LEARNED_DRAWER;
|
||||
|
||||
public class Utils {
|
||||
private static final String TAG = LogUtils.makeLogTag(Utils.class);
|
||||
|
||||
private static final String ANIMATION_PERMISSION = "android.permission.SET_ANIMATION_SCALE";
|
||||
private static final float DISABLED = 0.0f;
|
||||
private static final float DEFAULT = 1.0f;
|
||||
|
||||
public static void closeDrawer(final Activity activity) throws Throwable {
|
||||
activity.runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
DrawerLayout drawerLayout = (DrawerLayout) activity.findViewById(R.id.drawer_layout);
|
||||
drawerLayout.closeDrawers();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static void openDrawer(final ActivityTestRule<?> activityTestRule) throws Throwable {
|
||||
activityTestRule.runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
DrawerLayout drawerLayout = (DrawerLayout) activityTestRule.getActivity().findViewById(R.id.drawer_layout);
|
||||
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 switchHost(final Context context, Activity activity, final HostInfo hostInfo) {
|
||||
activity.runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
HostManager.getInstance(context).switchHost(hostInfo);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static void clearSharedPreferences(Context context) {
|
||||
PreferenceManager.getDefaultSharedPreferences(context)
|
||||
.edit().clear().commit();
|
||||
context.getSharedPreferences(AbstractTabsFragment.PREFERENCES_NAME, Context.MODE_PRIVATE)
|
||||
.edit().clear().commit();
|
||||
}
|
||||
|
||||
public static void setLearnedAboutDrawerPreference(Context context, boolean learned) {
|
||||
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
|
||||
SharedPreferences.Editor editor = prefs.edit();
|
||||
editor.putBoolean(PREF_USER_LEARNED_DRAWER, learned);
|
||||
editor.commit();
|
||||
}
|
||||
|
||||
public static void setUseEventServerPreference(Context context, boolean use) {
|
||||
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
|
||||
SharedPreferences.Editor editor = prefs.edit();
|
||||
editor.putBoolean(HostFragmentManualConfiguration.HOST_USE_EVENT_SERVER, use);
|
||||
editor.commit();
|
||||
}
|
||||
|
||||
public static void setupMediaProvider(Context context) {
|
||||
MediaProvider mediaProvider = new MediaProvider();
|
||||
mediaProvider.setContext(context);
|
||||
mediaProvider.onCreate();
|
||||
}
|
||||
|
||||
public static void disableAnimations(Context context) {
|
||||
int permStatus = context.checkCallingOrSelfPermission(ANIMATION_PERMISSION);
|
||||
if (permStatus == PackageManager.PERMISSION_GRANTED) {
|
||||
setSystemAnimationsScale(DISABLED);
|
||||
}
|
||||
}
|
||||
|
||||
public static void enableAnimations(Context context) {
|
||||
int permStatus = context.checkCallingOrSelfPermission(ANIMATION_PERMISSION);
|
||||
if (permStatus == PackageManager.PERMISSION_GRANTED) {
|
||||
setSystemAnimationsScale(DEFAULT);
|
||||
} else {
|
||||
LogUtils.LOGD(TAG, "disableAnimations: permission " + ANIMATION_PERMISSION + " not granted");
|
||||
}
|
||||
}
|
||||
|
||||
private static void setSystemAnimationsScale(float animationScale) {
|
||||
try {
|
||||
Class<?> windowManagerStubClazz = Class.forName("android.view.IWindowManager$Stub");
|
||||
Method asInterface = windowManagerStubClazz.getDeclaredMethod("asInterface", IBinder.class);
|
||||
Class<?> serviceManagerClazz = Class.forName("android.os.ServiceManager");
|
||||
Method getService = serviceManagerClazz.getDeclaredMethod("getService", String.class);
|
||||
Class<?> windowManagerClazz = Class.forName("android.view.IWindowManager");
|
||||
Method setAnimationScales = windowManagerClazz.getDeclaredMethod("setAnimationScales", float[].class);
|
||||
Method getAnimationScales = windowManagerClazz.getDeclaredMethod("getAnimationScales");
|
||||
|
||||
IBinder windowManagerBinder = (IBinder) getService.invoke(null, "window");
|
||||
Object windowManagerObj = asInterface.invoke(null, windowManagerBinder);
|
||||
float[] currentScales = (float[]) getAnimationScales.invoke(windowManagerObj);
|
||||
for (int i = 0; i < currentScales.length; i++) {
|
||||
currentScales[i] = animationScale;
|
||||
}
|
||||
setAnimationScales.invoke(windowManagerObj, new Object[]{currentScales});
|
||||
} catch (Exception e) {
|
||||
Log.e("SystemAnimations", "Could not change animation scale to " + animationScale + " :'(");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
/**
|
||||
* Copyright (C) 2014 Subito.it S.r.l (www.subito.it)
|
||||
*
|
||||
* 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.testhelpers.action;
|
||||
|
||||
import android.view.View;
|
||||
|
||||
import androidx.test.espresso.UiController;
|
||||
import androidx.test.espresso.ViewAction;
|
||||
|
||||
import org.hamcrest.Matcher;
|
||||
|
||||
import static androidx.test.espresso.matcher.ViewMatchers.isAssignableFrom;
|
||||
import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
|
||||
import static org.hamcrest.Matchers.allOf;
|
||||
|
||||
public class ClearFocus implements ViewAction {
|
||||
|
||||
@Override
|
||||
public Matcher<View> getConstraints() {
|
||||
|
||||
return allOf(isDisplayed(), isAssignableFrom(View.class));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDescription() {
|
||||
|
||||
return "Clear focus on the given view";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void perform(UiController uiController, View view) {
|
||||
|
||||
view.clearFocus();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,78 @@
|
|||
/*
|
||||
* 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.testhelpers.action;
|
||||
|
||||
import android.graphics.Rect;
|
||||
import androidx.core.widget.NestedScrollView;
|
||||
import androidx.test.espresso.PerformException;
|
||||
import androidx.test.espresso.UiController;
|
||||
import androidx.test.espresso.ViewAction;
|
||||
import androidx.test.espresso.matcher.ViewMatchers;
|
||||
import androidx.test.espresso.util.HumanReadables;
|
||||
|
||||
import android.view.View;
|
||||
|
||||
import org.hamcrest.Matcher;
|
||||
import org.xbmc.kore.utils.LogUtils;
|
||||
|
||||
import static androidx.test.espresso.matcher.ViewMatchers.isAssignableFrom;
|
||||
import static androidx.test.espresso.matcher.ViewMatchers.isDisplayingAtLeast;
|
||||
import static androidx.test.espresso.matcher.ViewMatchers.withEffectiveVisibility;
|
||||
import static org.hamcrest.CoreMatchers.allOf;
|
||||
import static org.hamcrest.CoreMatchers.anyOf;
|
||||
|
||||
/**
|
||||
* Modified version of {@link androidx.test.espresso.action.ScrollToAction} to support
|
||||
* NestedScrollView.
|
||||
* TODO Check future versions of {@link androidx.test.espresso.action.ScrollToAction} to see if support for NestedScrollView has been added
|
||||
*/
|
||||
public class NestedScrollTo implements ViewAction {
|
||||
private final static String TAG = LogUtils.makeLogTag(NestedScrollTo.class);
|
||||
|
||||
@Override
|
||||
public Matcher<View> getConstraints() {
|
||||
return allOf(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE), ViewMatchers.isDescendantOfA(anyOf(
|
||||
isAssignableFrom(NestedScrollView.class))));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDescription() {
|
||||
return "nested scroll to";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void perform(UiController uiController, View view) {
|
||||
if (isDisplayingAtLeast(90).matches(view)) {
|
||||
LogUtils.LOGI(TAG, "View is already displayed. Returning.");
|
||||
return;
|
||||
}
|
||||
Rect rect = new Rect();
|
||||
view.getDrawingRect(rect);
|
||||
if (!view.requestRectangleOnScreen(rect, true /* immediate */)) {
|
||||
LogUtils.LOGW(TAG, "Scrolling to view was requested, but none of the parents scrolled.");
|
||||
}
|
||||
uiController.loopMainThreadUntilIdle();
|
||||
if (!isDisplayingAtLeast(90).matches(view)) {
|
||||
throw new PerformException.Builder()
|
||||
.withActionDescription(this.getDescription())
|
||||
.withViewDescription(HumanReadables.describe(view))
|
||||
.withCause(new RuntimeException(
|
||||
"Scrolling to view was attempted, but the view is not displayed"))
|
||||
.build();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,199 @@
|
|||
/*
|
||||
* 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.testhelpers.action;
|
||||
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import androidx.test.espresso.PerformException;
|
||||
import androidx.test.espresso.UiController;
|
||||
import androidx.test.espresso.ViewAction;
|
||||
import androidx.test.espresso.action.MotionEvents;
|
||||
import androidx.test.espresso.action.Press;
|
||||
import androidx.test.espresso.util.HumanReadables;
|
||||
import androidx.test.espresso.util.TreeIterables;
|
||||
import androidx.viewpager2.widget.ViewPager2;
|
||||
|
||||
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 androidx.test.espresso.action.ViewActions.actionWithAssertions;
|
||||
import static androidx.test.espresso.matcher.ViewMatchers.isRoot;
|
||||
|
||||
import com.google.android.material.tabs.TabLayout;
|
||||
|
||||
public final class ViewActions {
|
||||
|
||||
/**
|
||||
* Returns an action that clears the focus on the view.
|
||||
* <br/>
|
||||
* View constraints:
|
||||
* <ul>
|
||||
* <li>must be displayed on screen</li>
|
||||
* </ul>
|
||||
*/
|
||||
public static ViewAction clearFocus() {
|
||||
return actionWithAssertions(new ClearFocus());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an action that scrolls to the view in a nested scroll view.<br>
|
||||
* <br>
|
||||
* View preconditions:
|
||||
* <ul>
|
||||
* <li>must be a descendant of NestedScrollView
|
||||
* <li>must have visibility set to View.VISIBLE
|
||||
* <ul></ul>
|
||||
*/
|
||||
public static ViewAction nestedScrollTo() {
|
||||
return actionWithAssertions(new NestedScrollTo());
|
||||
}
|
||||
|
||||
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<View> 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) {
|
||||
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<View> getConstraints() {
|
||||
return new TypeSafeMatcher<View>() {
|
||||
@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);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public static ViewAction setCurrentViewPagerItem(final int pageTitleResourceId) {
|
||||
return new ViewAction() {
|
||||
|
||||
@Override
|
||||
public Matcher<View> getConstraints() {
|
||||
return new TypeSafeMatcher<View>() {
|
||||
@Override
|
||||
protected boolean matchesSafely(View item) {
|
||||
return item instanceof TabLayout;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void describeTo(Description description) {
|
||||
description.appendText("is a SeekBar.");
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDescription() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void perform(UiController uiController, View view) {
|
||||
TabLayout tabLayout = (TabLayout) view;
|
||||
String expectedTitle = view.getResources().getString(pageTitleResourceId);
|
||||
for(int i = 0; i < tabLayout.getTabCount(); i++) {
|
||||
if (expectedTitle.contentEquals(tabLayout.getTabAt(i).getText())) {
|
||||
tabLayout.getTabAt(i).select();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,190 @@
|
|||
/*
|
||||
* 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;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.preference.PreferenceManager;
|
||||
import androidx.test.espresso.IdlingRegistry;
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
import androidx.test.rule.ActivityTestRule;
|
||||
|
||||
import org.junit.After;
|
||||
import org.junit.AfterClass;
|
||||
import org.junit.Before;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Ignore;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.xbmc.kore.host.HostInfo;
|
||||
import org.xbmc.kore.host.HostConnection;
|
||||
import org.xbmc.kore.testhelpers.LoaderIdlingResource;
|
||||
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.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;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
@SuppressLint("IgnoreWithoutReason")
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
@Ignore
|
||||
abstract public class AbstractTestClass<T extends AppCompatActivity> {
|
||||
private static final String TAG = LogUtils.makeLogTag(AbstractTestClass.class);
|
||||
|
||||
abstract protected ActivityTestRule<T> getActivityTestRule();
|
||||
|
||||
/**
|
||||
* Method that can be used to change the shared preferences.
|
||||
* This will be called before each test after clearing the settings
|
||||
* in {@link #setUp()}
|
||||
*/
|
||||
abstract protected void setSharedPreferences(Context context);
|
||||
|
||||
private LoaderIdlingResource loaderIdlingResource;
|
||||
private ActivityTestRule<T> activityTestRule;
|
||||
private static MockTcpServer server;
|
||||
private static JSONConnectionHandlerManager manager;
|
||||
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;
|
||||
|
||||
@BeforeClass
|
||||
public static void setupMockTCPServer() throws Throwable {
|
||||
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();
|
||||
}
|
||||
|
||||
@Before
|
||||
public void setUp() throws Throwable {
|
||||
|
||||
activityTestRule = getActivityTestRule();
|
||||
|
||||
final Context context = activityTestRule.getActivity();
|
||||
if (context == null)
|
||||
throw new RuntimeException("Could not get context. Maybe activity failed to start?");
|
||||
|
||||
Utils.clearSharedPreferences(context);
|
||||
//Prevent drawer from opening when we start a new activity
|
||||
Utils.setLearnedAboutDrawerPreference(context, true);
|
||||
//Allow each test to change the shared preferences
|
||||
setSharedPreferences(context);
|
||||
|
||||
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
|
||||
boolean useEventServer = prefs.getBoolean(HostFragmentManualConfiguration.HOST_USE_EVENT_SERVER, false);
|
||||
|
||||
hostInfo = Database.addHost(context, server.getHostName(),
|
||||
HostConnection.PROTOCOL_TCP, HostInfo.DEFAULT_HTTP_PORT,
|
||||
server.getPort(), useEventServer, kodiMajorVersion);
|
||||
|
||||
loaderIdlingResource = new LoaderIdlingResource(activityTestRule.getActivity().getSupportLoaderManager());
|
||||
IdlingRegistry.getInstance().register(loaderIdlingResource);
|
||||
|
||||
Utils.disableAnimations(context);
|
||||
|
||||
Utils.setupMediaProvider(context);
|
||||
|
||||
Database.fill(hostInfo, context, context.getContentResolver());
|
||||
|
||||
Utils.switchHost(context, activityTestRule.getActivity(), hostInfo);
|
||||
|
||||
//Relaunch the activity for the changes (Host selection, preference changes, and database fill) to take effect
|
||||
activityTestRule.finishActivity();
|
||||
activityTestRule.launchActivity(new Intent());
|
||||
}
|
||||
|
||||
@After
|
||||
public void tearDown() throws Exception {
|
||||
if ( loaderIdlingResource != null )
|
||||
IdlingRegistry.getInstance().unregister(loaderIdlingResource);
|
||||
|
||||
applicationHandler.reset();
|
||||
playerHandler.reset();
|
||||
|
||||
Context context = activityTestRule.getActivity();
|
||||
Database.flush(context.getContentResolver());
|
||||
Utils.enableAnimations(context);
|
||||
}
|
||||
|
||||
@AfterClass
|
||||
public static void cleanup() throws IOException {
|
||||
server.shutdown();
|
||||
}
|
||||
|
||||
protected T getActivity() {
|
||||
if (activityTestRule != null) {
|
||||
return activityTestRule.getActivity();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Use this to set the major version of Kodi.
|
||||
* <br/>
|
||||
* NOTE: be sure to call this before {@link #setUp()} is called to have the version correctly
|
||||
* set in the database.
|
||||
* @param kodiMajorVersion
|
||||
*/
|
||||
protected void setKodiMajorVersion(int kodiMajorVersion) {
|
||||
this.kodiMajorVersion = kodiMajorVersion;
|
||||
}
|
||||
|
||||
public static JSONConnectionHandlerManager getConnectionHandlerManager() {
|
||||
return manager;
|
||||
}
|
||||
|
||||
public static PlayerHandler getPlayerHandler() {
|
||||
return playerHandler;
|
||||
}
|
||||
|
||||
public static ApplicationHandler getApplicationHandler() {
|
||||
return applicationHandler;
|
||||
}
|
||||
|
||||
public static InputHandler getInputHandler() {
|
||||
return inputHandler;
|
||||
}
|
||||
|
||||
public static PlaylistHandler getPlaylistHandler() {
|
||||
return playlistHandler;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,212 @@
|
|||
/*
|
||||
* 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.addons;
|
||||
|
||||
import android.content.Context;
|
||||
import android.view.View;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.test.espresso.Espresso;
|
||||
import androidx.test.rule.ActivityTestRule;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.xbmc.kore.R;
|
||||
import org.xbmc.kore.testhelpers.EspressoTestUtils;
|
||||
import org.xbmc.kore.testhelpers.action.ViewActions;
|
||||
import org.xbmc.kore.tests.ui.AbstractTestClass;
|
||||
import org.xbmc.kore.ui.sections.addon.AddonsActivity;
|
||||
|
||||
import static androidx.test.espresso.Espresso.onView;
|
||||
import static androidx.test.espresso.assertion.ViewAssertions.matches;
|
||||
import static androidx.test.espresso.matcher.ViewMatchers.isRoot;
|
||||
import static androidx.test.espresso.matcher.ViewMatchers.withId;
|
||||
import static androidx.test.espresso.matcher.ViewMatchers.withParent;
|
||||
import static androidx.test.espresso.matcher.ViewMatchers.withText;
|
||||
import static org.hamcrest.Matchers.allOf;
|
||||
import static org.hamcrest.Matchers.instanceOf;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.xbmc.kore.testhelpers.EspressoTestUtils.clickRecyclerViewItem;
|
||||
import static org.xbmc.kore.testhelpers.EspressoTestUtils.rotateDevice;
|
||||
import static org.xbmc.kore.testhelpers.EspressoTestUtils.selectListItemPressBackAndCheckActionbarTitle;
|
||||
|
||||
/**
|
||||
* Note: we use MoviesActivity here instead of AddonsActivity. The reason is that we use @Rule
|
||||
* to start the activity which is done prior to executing @Before. This results in a deadlock
|
||||
* situation.
|
||||
*
|
||||
* Normal startup procedure would be as follows:
|
||||
*
|
||||
* 1. Start MockTCPServer {@link AbstractTestClass#setupMockTCPServer()}
|
||||
* 2. Start activity {mActivityRule}
|
||||
* 3. Espresso waits for activity to become idle before calling {@link AbstractTestClass#setUp()}
|
||||
* 4. Add AddonsHandler {@link AbstractTestClass#setUp()}
|
||||
*
|
||||
* At step 2 the AddonsActivity displays an animated progress indicator while it waits for the
|
||||
* MockTCPServer to send the list of addons.
|
||||
* This is never send as the {@link org.xbmc.kore.testutils.tcpserver.handlers.AddonsHandler} is
|
||||
* added in {@link super#setUp()} which is never started by Espresso as it waits for
|
||||
* {@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);
|
||||
|
||||
@Override
|
||||
protected ActivityTestRule<AddonsActivity> getActivityTestRule() {
|
||||
return mActivityRule;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void setSharedPreferences(Context context) {
|
||||
|
||||
}
|
||||
|
||||
@Before
|
||||
public void setUp() throws Throwable {
|
||||
super.setUp();
|
||||
onView(isRoot()).perform(ViewActions.waitForView(R.id.list, v -> v.isShown(),10000));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test if action bar title initially displays Addons
|
||||
*/
|
||||
@Test
|
||||
public void setActionBarTitleMain() {
|
||||
onView(allOf(instanceOf(TextView.class), withParent(withId(R.id.default_toolbar))))
|
||||
.check(matches(withText(R.string.addons)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test if action bar title is correctly set after selecting a list item
|
||||
*
|
||||
* UI interaction flow tested:
|
||||
* 1. Click on list item
|
||||
* 2. Result: action bar title should show list item title
|
||||
*/
|
||||
@Test
|
||||
public void setActionBarTitle() {
|
||||
EspressoTestUtils.selectListItemAndCheckActionbarTitle("Dumpert", R.id.list,
|
||||
"Dumpert");
|
||||
}
|
||||
|
||||
/**
|
||||
* Test if action bar title is correctly restored after a configuration change
|
||||
*
|
||||
* UI interaction flow tested:
|
||||
* 1. Click on list item
|
||||
* 2. Rotate device
|
||||
* 3. Result: action bar title should show list item title
|
||||
*/
|
||||
@Test
|
||||
public void restoreActionBarTitleOnConfigurationStateChanged() {
|
||||
EspressoTestUtils.selectListItemRotateDeviceAndCheckActionbarTitle("Dumpert", R.id.list,
|
||||
"Dumpert",
|
||||
getActivity());
|
||||
}
|
||||
|
||||
/**
|
||||
* Test if action bar title is correctly restored after returning from a movie selection
|
||||
*
|
||||
* UI interaction flow tested:
|
||||
* 1. Click on list item
|
||||
* 2. Press back
|
||||
* 3. Result: action bar title should show main title
|
||||
*/
|
||||
@Test
|
||||
public void restoreActionBarTitleOnReturningFromMovie() {
|
||||
selectListItemPressBackAndCheckActionbarTitle(0, R.id.list,
|
||||
getActivity().getString(R.string.addons));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test if the initial state shows the hamburger icon
|
||||
*/
|
||||
@Test
|
||||
public void showHamburgerInInitialState() {
|
||||
assertFalse(getActivity().getDrawerIndicatorIsArrow());
|
||||
}
|
||||
|
||||
/**
|
||||
* Test if navigation icon is changed to an arrow when selecting a list item
|
||||
*
|
||||
* UI interaction flow tested:
|
||||
* 1. Click on list item
|
||||
* 2. Result: navigation icon should be an arrow
|
||||
*/
|
||||
@Test
|
||||
public void showArrowWhenSelectingListItem() {
|
||||
clickRecyclerViewItem(0, R.id.list);
|
||||
|
||||
assertTrue(getActivity().getDrawerIndicatorIsArrow());
|
||||
}
|
||||
|
||||
/**
|
||||
* Test if navigation icon is changed to an arrow when selecting a list item
|
||||
*
|
||||
* UI interaction flow tested:
|
||||
* 1. Click on list item
|
||||
* 2. Press back
|
||||
* 3. Result: navigation icon should be a hamburger
|
||||
*/
|
||||
@Test
|
||||
public void showHamburgerWhenSelectingListItemAndReturn() {
|
||||
clickRecyclerViewItem(0, R.id.list);
|
||||
Espresso.pressBack();
|
||||
|
||||
assertFalse(getActivity().getDrawerIndicatorIsArrow());
|
||||
}
|
||||
|
||||
/**
|
||||
* Test if navigation icon is restored to an arrow when selecting a list item
|
||||
* and rotating the device
|
||||
*
|
||||
* UI interaction flow tested:
|
||||
* 1. Click on list item
|
||||
* 2. Rotate device
|
||||
* 3. Result: navigation icon should be an arrow
|
||||
*/
|
||||
@Test
|
||||
public void restoreArrowOnConfigurationChange() {
|
||||
clickRecyclerViewItem(0, R.id.list);
|
||||
rotateDevice(getActivity());
|
||||
|
||||
assertTrue(getActivity().getDrawerIndicatorIsArrow());
|
||||
}
|
||||
|
||||
/**
|
||||
* Test if navigation icon is restored to an hamburger when selecting a list item
|
||||
* and rotating the device and returning to the list
|
||||
*
|
||||
* UI interaction flow tested:
|
||||
* 1. Click on list item
|
||||
* 2. Rotate device
|
||||
* 3. Press back
|
||||
* 4. Result: navigation icon should be a hamburger
|
||||
*/
|
||||
@Test
|
||||
public void restoreHamburgerOnConfigurationChangeOnReturn() {
|
||||
clickRecyclerViewItem(0, R.id.list);
|
||||
rotateDevice(getActivity());
|
||||
Espresso.pressBack();
|
||||
|
||||
assertTrue(EspressoTestUtils.getActivity() instanceof AddonsActivity);
|
||||
assertFalse(((AddonsActivity) EspressoTestUtils.getActivity()).getDrawerIndicatorIsArrow());
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,186 @@
|
|||
/*
|
||||
* 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.movies;
|
||||
|
||||
import android.content.Context;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.test.espresso.Espresso;
|
||||
import androidx.test.rule.ActivityTestRule;
|
||||
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.xbmc.kore.R;
|
||||
import org.xbmc.kore.testhelpers.EspressoTestUtils;
|
||||
import org.xbmc.kore.tests.ui.AbstractTestClass;
|
||||
import org.xbmc.kore.ui.sections.video.MoviesActivity;
|
||||
|
||||
import static androidx.test.espresso.Espresso.onView;
|
||||
import static androidx.test.espresso.assertion.ViewAssertions.matches;
|
||||
import static androidx.test.espresso.matcher.ViewMatchers.withId;
|
||||
import static androidx.test.espresso.matcher.ViewMatchers.withParent;
|
||||
import static androidx.test.espresso.matcher.ViewMatchers.withText;
|
||||
import static org.hamcrest.Matchers.allOf;
|
||||
import static org.hamcrest.Matchers.instanceOf;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.xbmc.kore.testhelpers.EspressoTestUtils.clickRecyclerViewItem;
|
||||
import static org.xbmc.kore.testhelpers.EspressoTestUtils.rotateDevice;
|
||||
import static org.xbmc.kore.testhelpers.EspressoTestUtils.selectListItemPressBackAndCheckActionbarTitle;
|
||||
|
||||
public class MoviesActivityTests extends AbstractTestClass<MoviesActivity> {
|
||||
|
||||
@Rule
|
||||
public ActivityTestRule<MoviesActivity> mActivityRule = new ActivityTestRule<>(
|
||||
MoviesActivity.class);
|
||||
|
||||
@Override
|
||||
protected ActivityTestRule<MoviesActivity> getActivityTestRule() {
|
||||
return mActivityRule;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void setSharedPreferences(Context context) {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Test if action bar title initially displays Movies
|
||||
*/
|
||||
@Test
|
||||
public void setActionBarTitleMain() {
|
||||
onView(allOf(instanceOf(TextView.class), withParent(withId(R.id.default_toolbar))))
|
||||
.check(matches(withText(R.string.movies)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test if action bar title is correctly set after selecting a list item
|
||||
*
|
||||
* UI interaction flow tested:
|
||||
* 1. Click on list item
|
||||
* 2. Result: action bar title should show list item title
|
||||
*/
|
||||
@Test
|
||||
public void setActionBarTitle() {
|
||||
EspressoTestUtils.selectListItemAndCheckActionbarTitle("#Rookie93 Marc Marquez: Beyond the Smile", R.id.list,
|
||||
"#Rookie93 Marc Marquez: Beyond the Smile");
|
||||
}
|
||||
|
||||
/**
|
||||
* Test if action bar title is correctly restored after a configuration change
|
||||
*
|
||||
* UI interaction flow tested:
|
||||
* 1. Click on list item
|
||||
* 2. Rotate device
|
||||
* 3. Result: action bar title should show list item title
|
||||
*/
|
||||
@Test
|
||||
public void restoreActionBarTitleOnConfigurationStateChanged() {
|
||||
EspressoTestUtils.selectListItemRotateDeviceAndCheckActionbarTitle("#Rookie93 Marc Marquez: Beyond the Smile", R.id.list,
|
||||
"#Rookie93 Marc Marquez: Beyond the Smile",
|
||||
getActivity());
|
||||
}
|
||||
|
||||
/**
|
||||
* Test if action bar title is correctly restored after returning from a movie selection
|
||||
*
|
||||
* UI interaction flow tested:
|
||||
* 1. Click on list item
|
||||
* 2. Press back
|
||||
* 3. Result: action bar title should show main title
|
||||
*/
|
||||
@Test
|
||||
public void restoreActionBarTitleOnReturningFromMovie() {
|
||||
selectListItemPressBackAndCheckActionbarTitle(0, R.id.list,
|
||||
getActivity().getString(R.string.movies));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test if the initial state shows the hamburger icon
|
||||
*/
|
||||
@Test
|
||||
public void showHamburgerInInitialState() {
|
||||
assertFalse(getActivity().getDrawerIndicatorIsArrow());
|
||||
}
|
||||
|
||||
/**
|
||||
* Test if navigation icon is changed to an arrow when selecting a list item
|
||||
*
|
||||
* UI interaction flow tested:
|
||||
* 1. Click on list item
|
||||
* 2. Result: navigation icon should be an arrow
|
||||
*/
|
||||
@Test
|
||||
public void showArrowWhenSelectingListItem() {
|
||||
clickRecyclerViewItem(0, R.id.list);
|
||||
|
||||
assertTrue(getActivity().getDrawerIndicatorIsArrow());
|
||||
}
|
||||
|
||||
/**
|
||||
* Test if navigation icon is changed to an arrow when selecting a list item
|
||||
*
|
||||
* UI interaction flow tested:
|
||||
* 1. Click on list item
|
||||
* 2. Press back
|
||||
* 3. Result: navigation icon should be a hamburger
|
||||
*/
|
||||
@Test
|
||||
public void showHamburgerWhenSelectingListItemAndReturn() {
|
||||
clickRecyclerViewItem(0, R.id.list);
|
||||
Espresso.pressBack();
|
||||
|
||||
assertFalse(getActivity().getDrawerIndicatorIsArrow());
|
||||
}
|
||||
|
||||
/**
|
||||
* Test if navigation icon is restored to an arrow when selecting a list item
|
||||
* and rotating the device
|
||||
*
|
||||
* UI interaction flow tested:
|
||||
* 1. Click on list item
|
||||
* 2. Rotate device
|
||||
* 3. Result: navigation icon should be an arrow
|
||||
*/
|
||||
@Test
|
||||
public void restoreArrowOnConfigurationChange() {
|
||||
clickRecyclerViewItem(0, R.id.list);
|
||||
rotateDevice(getActivity());
|
||||
|
||||
assertTrue(getActivity().getDrawerIndicatorIsArrow());
|
||||
}
|
||||
|
||||
/**
|
||||
* Test if navigation icon is restored to an hamburger when selecting a list item
|
||||
* and rotating the device and returning to the list
|
||||
*
|
||||
* UI interaction flow tested:
|
||||
* 1. Click on list item
|
||||
* 2. Rotate device
|
||||
* 3. Press back
|
||||
* 4. Result: navigation icon should be a hamburger
|
||||
*/
|
||||
@Test
|
||||
public void restoreHamburgerOnConfigurationChangeOnReturn() {
|
||||
clickRecyclerViewItem(0, R.id.list);
|
||||
rotateDevice(getActivity());
|
||||
Espresso.pressBack();
|
||||
|
||||
assertTrue(EspressoTestUtils.getActivity() instanceof MoviesActivity);
|
||||
assertFalse(((MoviesActivity) EspressoTestUtils.getActivity()).getDrawerIndicatorIsArrow());
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,219 @@
|
|||
/*
|
||||
* 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.tests.ui.movies;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.test.espresso.Espresso;
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
import androidx.test.rule.ActivityTestRule;
|
||||
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.xbmc.kore.R;
|
||||
import org.xbmc.kore.testhelpers.EspressoTestUtils;
|
||||
import org.xbmc.kore.tests.ui.AbstractTestClass;
|
||||
import org.xbmc.kore.ui.sections.video.MoviesActivity;
|
||||
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public class RestoreSearchQueryListFragmentTest extends AbstractTestClass<MoviesActivity> {
|
||||
|
||||
private final String SEARCH_QUERY = "Room";
|
||||
private final int SEARCH_QUERY_LIST_SIZE = 2;
|
||||
private final int COMPLETE_LIST_SIZE = 300;
|
||||
|
||||
@Rule
|
||||
public ActivityTestRule<MoviesActivity> mActivityRule = new ActivityTestRule<>(
|
||||
MoviesActivity.class);
|
||||
|
||||
@Override
|
||||
protected ActivityTestRule<MoviesActivity> getActivityTestRule() {
|
||||
return mActivityRule;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void setSharedPreferences(Context context) {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple test that checks if search query results in expected item(s)
|
||||
*/
|
||||
@Test
|
||||
public void simpleSearchTest() {
|
||||
EspressoTestUtils.enterSearchQuery(mActivityRule.getActivity(), SEARCH_QUERY);
|
||||
|
||||
EspressoTestUtils.checkTextInSearchQuery(SEARCH_QUERY);
|
||||
EspressoTestUtils.checkListMatchesSearchQuery(SEARCH_QUERY, SEARCH_QUERY_LIST_SIZE, R.id.list);
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple test that checks if search query is restored after device rotate
|
||||
*/
|
||||
@Test
|
||||
public void simpleRotateTest() {
|
||||
EspressoTestUtils.enterSearchQuery(mActivityRule.getActivity(), SEARCH_QUERY);
|
||||
EspressoTestUtils.rotateDevice(mActivityRule.getActivity());
|
||||
|
||||
EspressoTestUtils.checkTextInSearchQuery(SEARCH_QUERY);
|
||||
EspressoTestUtils.checkListMatchesSearchQuery(SEARCH_QUERY, SEARCH_QUERY_LIST_SIZE, R.id.list);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test if search query is restored when user returns to list fragment from
|
||||
* detail fragment
|
||||
*
|
||||
* UI interaction flow tested:
|
||||
* 1. Enter search query
|
||||
* 2. Click on list item
|
||||
* 3. Press back
|
||||
* 4. Result: search query entered at 1. should be restored in search field
|
||||
*/
|
||||
@Test
|
||||
public void searchClickBackTest() {
|
||||
EspressoTestUtils.clearSearchQuery(mActivityRule.getActivity());
|
||||
EspressoTestUtils.enterSearchQuery(mActivityRule.getActivity(), SEARCH_QUERY);
|
||||
EspressoTestUtils.clickRecyclerViewItem(0, R.id.list);
|
||||
Espresso.pressBack();
|
||||
|
||||
EspressoTestUtils.checkTextInSearchQuery(SEARCH_QUERY);
|
||||
EspressoTestUtils.checkListMatchesSearchQuery(SEARCH_QUERY, SEARCH_QUERY_LIST_SIZE, R.id.list);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test if search query is restored when user returns to list fragment from
|
||||
* detail fragment when device is rotated while on detail fragment
|
||||
*
|
||||
* UI interaction flow tested:
|
||||
* 1. Enter search query
|
||||
* 2. Click on list item
|
||||
* 3. Rotate device
|
||||
* 4. Press back
|
||||
* 5. Result: search query entered at 1. should be restored in search field
|
||||
*/
|
||||
@Test
|
||||
public void searchClickRotateBackTest() {
|
||||
EspressoTestUtils.clearSearchQuery(mActivityRule.getActivity());
|
||||
EspressoTestUtils.enterSearchQuery(mActivityRule.getActivity(), SEARCH_QUERY);
|
||||
EspressoTestUtils.clickRecyclerViewItem(0, R.id.list);
|
||||
EspressoTestUtils.rotateDevice(mActivityRule.getActivity());
|
||||
Espresso.pressBack();
|
||||
|
||||
EspressoTestUtils.checkTextInSearchQuery(SEARCH_QUERY);
|
||||
EspressoTestUtils.checkListMatchesSearchQuery(SEARCH_QUERY, SEARCH_QUERY_LIST_SIZE, R.id.list);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test if saved search query is cleared when user clears the
|
||||
* search query view
|
||||
*
|
||||
* UI interaction flow tested
|
||||
* 1. Enter search query
|
||||
* 2. Click on list item
|
||||
* 3. Return to list
|
||||
* 4. Clear search query
|
||||
* 5. Click on list item
|
||||
* 6. Return to list
|
||||
* 7. Result: search query should be empty and collapsed
|
||||
*/
|
||||
@Test
|
||||
public void searchClickBackClearSearchClickBackTest() {
|
||||
EspressoTestUtils.enterSearchQuery(mActivityRule.getActivity(), SEARCH_QUERY);
|
||||
EspressoTestUtils.clickRecyclerViewItem(0, R.id.list);
|
||||
Espresso.pressBack();
|
||||
EspressoTestUtils.clearSearchQuery(mActivityRule.getActivity());
|
||||
EspressoTestUtils.clickRecyclerViewItem(0, R.id.list);
|
||||
Espresso.pressBack();
|
||||
|
||||
EspressoTestUtils.checkSearchMenuCollapsed();
|
||||
}
|
||||
|
||||
/**
|
||||
* Test if after restoring search query the search query is cleared
|
||||
* when user presses back again.
|
||||
*
|
||||
* UI interaction flow tested
|
||||
* 1. Enter search query
|
||||
* 2. Click on list item
|
||||
* 3. Return to list
|
||||
* 4. Press back
|
||||
* 7. Result: search query should be cleared, collapsed, and list should show everything
|
||||
*/
|
||||
@Test
|
||||
public void searchClickBackBackTest() {
|
||||
EspressoTestUtils.enterSearchQuery(mActivityRule.getActivity(), SEARCH_QUERY);
|
||||
EspressoTestUtils.clickRecyclerViewItem(0, R.id.list);
|
||||
Espresso.pressBack();
|
||||
Espresso.pressBack();
|
||||
|
||||
EspressoTestUtils.checkSearchMenuCollapsed();
|
||||
EspressoTestUtils.checkListMatchesSearchQuery("", COMPLETE_LIST_SIZE, R.id.list);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test if pressing back clears a previous search
|
||||
*
|
||||
* UI interaction flow tested
|
||||
* 1. Enter search query
|
||||
* 2. Press back
|
||||
* 3. Result: search query should be cleared, collapsed, and list should show everything
|
||||
*/
|
||||
@Test
|
||||
public void searchBackTest() {
|
||||
EspressoTestUtils.enterSearchQuery(mActivityRule.getActivity(), SEARCH_QUERY);
|
||||
Espresso.pressBack();
|
||||
EspressoTestUtils.checkSearchMenuCollapsed();
|
||||
EspressoTestUtils.checkListMatchesSearchQuery("", COMPLETE_LIST_SIZE, R.id.list);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test if after restoring the search query pressing home button up clears a previous search
|
||||
*
|
||||
* UI interaction flow tested
|
||||
* 1. Enter search query
|
||||
* 2. Click on list item
|
||||
* 3. Press back
|
||||
* 4. Press home button
|
||||
* 5. Result: search query should be cleared, collapsed, and list should show everything
|
||||
*/
|
||||
@Test
|
||||
public void searchClickBackUpTest() {
|
||||
EspressoTestUtils.enterSearchQuery(mActivityRule.getActivity(), SEARCH_QUERY);
|
||||
EspressoTestUtils.clickRecyclerViewItem(0, R.id.list);
|
||||
Espresso.pressBack();
|
||||
EspressoTestUtils.clickToolbarCollapseButton();
|
||||
EspressoTestUtils.checkSearchMenuCollapsed();
|
||||
EspressoTestUtils.checkListMatchesSearchQuery("", COMPLETE_LIST_SIZE, R.id.list);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test if pressing home button up clears a previous search
|
||||
*
|
||||
* UI interaction flow tested
|
||||
* 1. Enter search query
|
||||
* 2. Press home button
|
||||
* 3. Result: search query should be cleared, collapsed, and list should show everything
|
||||
*/
|
||||
@Test
|
||||
public void searchUpTest() {
|
||||
EspressoTestUtils.enterSearchQuery(mActivityRule.getActivity(), SEARCH_QUERY);
|
||||
EspressoTestUtils.clickToolbarCollapseButton();
|
||||
EspressoTestUtils.checkSearchMenuCollapsed();
|
||||
EspressoTestUtils.checkListMatchesSearchQuery("", COMPLETE_LIST_SIZE, R.id.list);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,366 @@
|
|||
/*
|
||||
* 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.Context;
|
||||
import android.os.SystemClock;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.test.espresso.Espresso;
|
||||
import androidx.test.rule.ActivityTestRule;
|
||||
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.xbmc.kore.R;
|
||||
import org.xbmc.kore.testhelpers.EspressoTestUtils;
|
||||
import org.xbmc.kore.tests.ui.AbstractTestClass;
|
||||
import org.xbmc.kore.ui.sections.audio.MusicActivity;
|
||||
|
||||
import static androidx.test.espresso.Espresso.onView;
|
||||
import static androidx.test.espresso.assertion.ViewAssertions.matches;
|
||||
import static androidx.test.espresso.matcher.ViewMatchers.withId;
|
||||
import static androidx.test.espresso.matcher.ViewMatchers.withParent;
|
||||
import static androidx.test.espresso.matcher.ViewMatchers.withText;
|
||||
import static org.hamcrest.Matchers.allOf;
|
||||
import static org.hamcrest.Matchers.instanceOf;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.xbmc.kore.testhelpers.EspressoTestUtils.clickAlbumsTab;
|
||||
import static org.xbmc.kore.testhelpers.EspressoTestUtils.clickGenresTab;
|
||||
import static org.xbmc.kore.testhelpers.EspressoTestUtils.clickMusicVideosTab;
|
||||
import static org.xbmc.kore.testhelpers.EspressoTestUtils.rotateDevice;
|
||||
import static org.xbmc.kore.testhelpers.EspressoTestUtils.selectListItemAndCheckActionbarTitle;
|
||||
import static org.xbmc.kore.testhelpers.EspressoTestUtils.selectListItemPressBackAndCheckActionbarTitle;
|
||||
import static org.xbmc.kore.testhelpers.EspressoTestUtils.selectListItemRotateDeviceAndCheckActionbarTitle;
|
||||
|
||||
public class MusicActivityTests extends AbstractTestClass<MusicActivity> {
|
||||
@Rule
|
||||
public ActivityTestRule<MusicActivity> musicActivityActivityTestRule =
|
||||
new ActivityTestRule<>(MusicActivity.class);
|
||||
|
||||
@Override
|
||||
protected ActivityTestRule<MusicActivity> getActivityTestRule() {
|
||||
return musicActivityActivityTestRule;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void setSharedPreferences(Context context) {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Test if action bar title initially displays Music
|
||||
*/
|
||||
@Test
|
||||
public void setActionBarTitleMain() {
|
||||
onView(allOf(instanceOf(TextView.class), withParent(withId(R.id.default_toolbar))))
|
||||
.check(matches(withText(R.string.music)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test if action bar title is correctly set after selecting an artist
|
||||
*
|
||||
* UI interaction flow tested:
|
||||
* 1. Click on list item
|
||||
* 2. Result: action bar title should show list item title
|
||||
*/
|
||||
@Test
|
||||
public void setActionBarTitleArtist() {
|
||||
selectListItemAndCheckActionbarTitle(ArtistTestData.title, R.id.list, ArtistTestData.title);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test if action bar title is correctly set after selecting an album
|
||||
*
|
||||
* UI interaction flow tested:
|
||||
* 1. Click on albums tab
|
||||
* 2. Click on list item
|
||||
* 3. Result: action bar title should show list item title
|
||||
*/
|
||||
@Test
|
||||
public void setActionBarTitleAlbum() {
|
||||
clickAlbumsTab();
|
||||
selectListItemAndCheckActionbarTitle(AlbumTestData.title, R.id.list, AlbumTestData.title);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test if action bar title is correctly set after selecting a genre
|
||||
*
|
||||
* UI interaction flow tested:
|
||||
* 1. Click on genres tab
|
||||
* 2. Click on list item
|
||||
* 3. Result: action bar title should show list item title
|
||||
*/
|
||||
@Test
|
||||
public void setActionBarTitleGenre() {
|
||||
clickGenresTab();
|
||||
selectListItemAndCheckActionbarTitle(GenreTestData.title, R.id.list, GenreTestData.title);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test if action bar title is correctly set after selecting a video
|
||||
*
|
||||
* UI interaction flow tested:
|
||||
* 1. Click on videos tab
|
||||
* 2. Click on list item
|
||||
* 3. Result: action bar title should show list item title
|
||||
*/
|
||||
@Test
|
||||
public void setActionBarTitleVideo() {
|
||||
clickMusicVideosTab();
|
||||
selectListItemAndCheckActionbarTitle(MusicVideoTestData.title, R.id.list, MusicVideoTestData.title);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test if action bar title is correctly restored after a configuration change when artist
|
||||
* is selected
|
||||
*
|
||||
* UI interaction flow tested:
|
||||
* 1. Click on list item
|
||||
* 2. Rotate device
|
||||
* 3. Result: action bar title should show list item title
|
||||
*/
|
||||
@Test
|
||||
public void restoreActionBarTitleArtistOnConfigurationStateChanged() {
|
||||
SystemClock.sleep(10000);
|
||||
selectListItemRotateDeviceAndCheckActionbarTitle(ArtistTestData.title, R.id.list,
|
||||
ArtistTestData.title, getActivity());
|
||||
}
|
||||
|
||||
/**
|
||||
* Test if action bar title is correctly restored after a configuration change when album
|
||||
* is selected
|
||||
*
|
||||
* UI interaction flow tested:
|
||||
* 1. Select albums tab
|
||||
* 2. Click on list item
|
||||
* 3. Rotate device
|
||||
* 4. Result: action bar title should show list item title
|
||||
*/
|
||||
@Test
|
||||
public void restoreActionBarTitleAlbumOnConfigurationStateChanged() {
|
||||
clickAlbumsTab();
|
||||
selectListItemRotateDeviceAndCheckActionbarTitle(AlbumTestData.title, R.id.list,
|
||||
AlbumTestData.title,
|
||||
getActivity());
|
||||
}
|
||||
|
||||
/**
|
||||
* Test if action bar title is correctly restored after a configuration change when genre
|
||||
* is selected
|
||||
*
|
||||
* UI interaction flow tested:
|
||||
* 1. Select genres tab
|
||||
* 2. Click on list item
|
||||
* 3. Rotate device
|
||||
* 4. Result: action bar title should show list item title
|
||||
*/
|
||||
@Test
|
||||
public void restoreActionBarTitleGenreOnConfigurationStateChanged() {
|
||||
clickGenresTab();
|
||||
selectListItemRotateDeviceAndCheckActionbarTitle(GenreTestData.title, R.id.list,
|
||||
GenreTestData.title, getActivity());
|
||||
}
|
||||
|
||||
/**
|
||||
* Test if action bar title is correctly restored after a configuration change when music video
|
||||
* is selected
|
||||
*
|
||||
* UI interaction flow tested:
|
||||
* 1. Select music videos tab
|
||||
* 2. Click on list item
|
||||
* 3. Rotate device
|
||||
* 4. Result: action bar title should show list item title
|
||||
*/
|
||||
@Test
|
||||
public void restoreActionBarTitleMusicVideoOnConfigurationStateChanged() {
|
||||
clickMusicVideosTab();
|
||||
selectListItemRotateDeviceAndCheckActionbarTitle(MusicVideoTestData.title, R.id.list,
|
||||
MusicVideoTestData.title,
|
||||
getActivity());
|
||||
}
|
||||
|
||||
/**
|
||||
* Test if action bar title is correctly restored after returning from artist selection
|
||||
*
|
||||
* UI interaction flow tested:
|
||||
* 1. Click on list item
|
||||
* 2. Press back
|
||||
* 3. Result: action bar title should show main title
|
||||
*/
|
||||
@Test
|
||||
public void restoreActionBarTitleOnReturningFromArtist() {
|
||||
selectListItemPressBackAndCheckActionbarTitle(ArtistTestData.title, R.id.list,
|
||||
getActivity().getString(R.string.music));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test if action bar title is correctly restored after returning from an album under
|
||||
* artist
|
||||
*
|
||||
* UI interaction flow tested:
|
||||
* 1. Click on list item
|
||||
* 2. Select albums tab
|
||||
* 3. Press back
|
||||
* 4. Result: action bar title should show artist title
|
||||
*/
|
||||
@Test
|
||||
public void restoreActionBarTitleOnArtistOnReturningFromAlbum() {
|
||||
EspressoTestUtils.clickRecyclerViewItem(ArtistTestData.title, R.id.list);
|
||||
clickAlbumsTab();
|
||||
selectListItemPressBackAndCheckActionbarTitle(ArtistTestData.album, R.id.list, ArtistTestData.title);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test if action bar title is correctly restored after returning from music video selection
|
||||
*
|
||||
* UI interaction flow tested:
|
||||
* 1. Click on list item
|
||||
* 2. Press back
|
||||
* 3. Result: action bar title should show main title
|
||||
*/
|
||||
@Test
|
||||
public void restoreActionBarTitleOnReturningFromMusicVideo() {
|
||||
clickMusicVideosTab();
|
||||
selectListItemPressBackAndCheckActionbarTitle(MusicVideoTestData.title, R.id.list,
|
||||
getActivity().getString(R.string.music));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test if action bar title is correctly restored after returning from genre selection
|
||||
*
|
||||
* UI interaction flow tested:
|
||||
* 1. Click on list item
|
||||
* 2. Press back
|
||||
* 3. Result: action bar title should show main title
|
||||
*/
|
||||
@Test
|
||||
public void restoreActionBarTitleOnReturningFromGenre() {
|
||||
clickGenresTab();
|
||||
selectListItemPressBackAndCheckActionbarTitle(GenreTestData.title, R.id.list,
|
||||
getActivity().getString(R.string.music));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test if action bar title is correctly restored after returning from album selection
|
||||
*
|
||||
* UI interaction flow tested:
|
||||
* 1. Click on list item
|
||||
* 2. Press back
|
||||
* 3. Result: action bar title should show main title
|
||||
*/
|
||||
@Test
|
||||
public void restoreActionBarTitleOnReturningFromAlbum() {
|
||||
clickAlbumsTab();
|
||||
selectListItemPressBackAndCheckActionbarTitle(AlbumTestData.title, R.id.list,
|
||||
getActivity().getString(R.string.music));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test if the initial state shows the hamburger icon
|
||||
*/
|
||||
@Test
|
||||
public void showHamburgerInInitialState() {
|
||||
assertFalse(getActivity().getDrawerIndicatorIsArrow());
|
||||
}
|
||||
|
||||
/**
|
||||
* Test if navigation icon is changed to an arrow when selecting a list item
|
||||
*
|
||||
* UI interaction flow tested:
|
||||
* 1. Click on list item
|
||||
* 2. Result: navigation icon should be an arrow
|
||||
*/
|
||||
@Test
|
||||
public void showArrowWhenSelectingListItem() {
|
||||
EspressoTestUtils.clickRecyclerViewItem(ArtistTestData.title, R.id.list);
|
||||
|
||||
assertTrue(getActivity().getDrawerIndicatorIsArrow());
|
||||
}
|
||||
|
||||
/**
|
||||
* Test if navigation icon is changed to an arrow when selecting a list item
|
||||
*
|
||||
* UI interaction flow tested:
|
||||
* 1. Click on list item
|
||||
* 2. Press back
|
||||
* 3. Result: navigation icon should be a hamburger
|
||||
*/
|
||||
@Test
|
||||
public void showHamburgerWhenSelectingListItemAndReturn() {
|
||||
EspressoTestUtils.clickRecyclerViewItem(ArtistTestData.title, R.id.list);
|
||||
|
||||
Espresso.pressBack();
|
||||
|
||||
assertFalse(getActivity().getDrawerIndicatorIsArrow());
|
||||
}
|
||||
|
||||
/**
|
||||
* Test if navigation icon is restored to an arrow when selecting a list item
|
||||
* and rotating the device
|
||||
*
|
||||
* UI interaction flow tested:
|
||||
* 1. Click on list item
|
||||
* 2. Rotate device
|
||||
* 3. Result: navigation icon should be an arrow
|
||||
*/
|
||||
@Test
|
||||
public void restoreArrowOnConfigurationChange() {
|
||||
EspressoTestUtils.clickRecyclerViewItem(ArtistTestData.title, R.id.list);
|
||||
|
||||
rotateDevice(getActivity());
|
||||
|
||||
assertTrue(getActivity().getDrawerIndicatorIsArrow());
|
||||
}
|
||||
|
||||
/**
|
||||
* Test if navigation icon is restored to an hamburger when selecting a list item
|
||||
* and rotating the device and returning to the list
|
||||
*
|
||||
* UI interaction flow tested:
|
||||
* 1. Click on list item
|
||||
* 2. Rotate device
|
||||
* 3. Press back
|
||||
* 4. Result: navigation icon should be a hamburger
|
||||
*/
|
||||
@Test
|
||||
public void restoreHamburgerOnConfigurationChangeOnReturn() {
|
||||
EspressoTestUtils.clickRecyclerViewItem(ArtistTestData.title, R.id.list);
|
||||
rotateDevice(getActivity());
|
||||
Espresso.pressBack();
|
||||
|
||||
assertTrue(EspressoTestUtils.getActivity() instanceof MusicActivity);
|
||||
assertFalse(((MusicActivity) EspressoTestUtils.getActivity()).getDrawerIndicatorIsArrow());
|
||||
}
|
||||
|
||||
private static class ArtistTestData {
|
||||
static String title = "ABC Orch Conducted by Herschel Burke Gilbert";
|
||||
static String album = "Songs Of The West";
|
||||
}
|
||||
|
||||
private static class AlbumTestData {
|
||||
static String title = "1958 - The Fabulous Johnny Cash";
|
||||
}
|
||||
|
||||
private static class GenreTestData {
|
||||
static String title = "Ambient";
|
||||
}
|
||||
|
||||
private static class MusicVideoTestData {
|
||||
static String title = "(You Drive Me) Crazy";
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,315 @@
|
|||
/*
|
||||
* 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.tests.ui.music;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.test.espresso.Espresso;
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
import androidx.test.rule.ActivityTestRule;
|
||||
|
||||
import org.junit.Rule;
|
||||
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.LoaderIdlingResource;
|
||||
import org.xbmc.kore.tests.ui.AbstractTestClass;
|
||||
import org.xbmc.kore.ui.sections.audio.MusicActivity;
|
||||
|
||||
import static org.xbmc.kore.testhelpers.EspressoTestUtils.clickAlbumsTab;
|
||||
import static org.xbmc.kore.testhelpers.EspressoTestUtils.clickArtistsTab;
|
||||
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public class RestoreSearchQueryViewPagerTest extends AbstractTestClass<MusicActivity> {
|
||||
|
||||
private final String ARTIST_SEARCH_QUERY = "Ben";
|
||||
private final int ARTIST_SEARCH_QUERY_LIST_SIZE = 2;
|
||||
private final String ARTIST_MATCHING_SEARCH_QUERY = "Ben E. King";
|
||||
private final String ALBUMS_SEARCH_QUERY = "tes";
|
||||
private final int ALBUM_SEARCH_QUERY_LIST_SIZE = 3;
|
||||
private final int ARTIST_COMPLETE_LIST_SIZE = 229;
|
||||
private final int ALBUM_COMPLETE_LIST_SIZE = 235;
|
||||
|
||||
private LoaderIdlingResource loaderIdlingResource;
|
||||
|
||||
@Rule
|
||||
public ActivityTestRule<MusicActivity> mActivityRule = new ActivityTestRule<>(
|
||||
MusicActivity.class);
|
||||
|
||||
@Override
|
||||
protected ActivityTestRule<MusicActivity> getActivityTestRule() {
|
||||
return mActivityRule;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void setSharedPreferences(Context context) {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple test that checks if search query results in expected item(s)
|
||||
*
|
||||
* UI interaction flow tested:
|
||||
* 1. Enter search query
|
||||
* 2. Result: search query entered at 1. should show in search field and list should match search query
|
||||
*/
|
||||
@Test
|
||||
public void simpleSearchTest() {
|
||||
EspressoTestUtils.enterSearchQuery(mActivityRule.getActivity(), ARTIST_SEARCH_QUERY);
|
||||
|
||||
EspressoTestUtils.checkTextInSearchQuery(ARTIST_SEARCH_QUERY);
|
||||
EspressoTestUtils.checkListMatchesSearchQuery(ARTIST_SEARCH_QUERY, ARTIST_SEARCH_QUERY_LIST_SIZE, R.id.list);
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple test that checks if search query is restored after device rotate
|
||||
* UI interaction flow tested:
|
||||
* 1. Enter search query
|
||||
* 2. Rotate device
|
||||
* 3. Result: search query entered at 1. should show in search field and list should match search query
|
||||
*/
|
||||
@Test
|
||||
public void simpleRotateTest() {
|
||||
EspressoTestUtils.enterSearchQuery(mActivityRule.getActivity(), ARTIST_SEARCH_QUERY);
|
||||
EspressoTestUtils.rotateDevice(mActivityRule.getActivity());
|
||||
|
||||
EspressoTestUtils.checkTextInSearchQuery(ARTIST_SEARCH_QUERY);
|
||||
EspressoTestUtils.checkListMatchesSearchQuery(ARTIST_SEARCH_QUERY, ARTIST_SEARCH_QUERY_LIST_SIZE, R.id.list);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test if search query is restored when user returns to list fragment from
|
||||
* detail fragment
|
||||
*
|
||||
* UI interaction flow tested:
|
||||
* 1. Enter search query
|
||||
* 2. Click on list item
|
||||
* 3. Press back
|
||||
* 4. Result: search query entered at 1. should be restored in search field
|
||||
*/
|
||||
@Test
|
||||
public void searchClickBackTest() {
|
||||
EspressoTestUtils.enterSearchQuery(mActivityRule.getActivity(), ARTIST_SEARCH_QUERY);
|
||||
EspressoTestUtils.clickRecyclerViewItem(ARTIST_MATCHING_SEARCH_QUERY, R.id.list);
|
||||
Espresso.pressBack();
|
||||
|
||||
EspressoTestUtils.checkTextInSearchQuery(ARTIST_SEARCH_QUERY);
|
||||
EspressoTestUtils.checkListMatchesSearchQuery(ARTIST_SEARCH_QUERY, ARTIST_SEARCH_QUERY_LIST_SIZE, R.id.list);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test if search query is restored when user returns to list fragment from
|
||||
* detail fragment when device is rotated while on detail fragment
|
||||
*
|
||||
* UI interaction flow tested:
|
||||
* 1. Enter search query
|
||||
* 2. Click on list item
|
||||
* 3. Rotate device
|
||||
* 4. Press back
|
||||
* 5. Result: search query entered at 1. should be restored in search field
|
||||
*/
|
||||
@Test
|
||||
public void searchClickRotateBackTest() {
|
||||
EspressoTestUtils.enterSearchQuery(mActivityRule.getActivity(), ARTIST_SEARCH_QUERY);
|
||||
EspressoTestUtils.clickRecyclerViewItem(ARTIST_MATCHING_SEARCH_QUERY, R.id.list);
|
||||
EspressoTestUtils.rotateDevice(mActivityRule.getActivity());
|
||||
Espresso.pressBack();
|
||||
|
||||
EspressoTestUtils.checkTextInSearchQuery(ARTIST_SEARCH_QUERY);
|
||||
EspressoTestUtils.checkListMatchesSearchQuery(ARTIST_SEARCH_QUERY, ARTIST_SEARCH_QUERY_LIST_SIZE, R.id.list);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test if search query is cleared when switching to
|
||||
* different tab in the TabAdapter
|
||||
*
|
||||
* UI interaction flow tested:
|
||||
* 1. Enter search query
|
||||
* 2. Switch to Albums tab
|
||||
* 3. Result: search query should be cleared
|
||||
*/
|
||||
@Test
|
||||
public void searchSwitchTabTest() {
|
||||
Activity activity = mActivityRule.getActivity();
|
||||
|
||||
EspressoTestUtils.enterSearchQuery(activity, ARTIST_SEARCH_QUERY);
|
||||
clickAlbumsTab();
|
||||
|
||||
EspressoTestUtils.clickMenuItem(activity, activity.getString(R.string.action_search), R.id.action_search);
|
||||
EspressoTestUtils.checkTextInSearchQuery("");
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests if search query is still cleared when
|
||||
* device is rotated after switching to a different tab
|
||||
*
|
||||
* UI interaction flow tested:
|
||||
* 1. Enter search query
|
||||
* 2. Switch to Albums tab
|
||||
* 3. Rotate device
|
||||
* 4. Open search menu item
|
||||
* 5. Result: search query should be cleared
|
||||
*/
|
||||
@Test
|
||||
public void searchSwitchTabRotateTest() {
|
||||
Activity activity = mActivityRule.getActivity();
|
||||
|
||||
EspressoTestUtils.enterSearchQuery(activity, ARTIST_SEARCH_QUERY);
|
||||
clickAlbumsTab();
|
||||
EspressoTestUtils.rotateDevice(activity);
|
||||
EspressoTestUtils.clickMenuItem(activity, activity.getString(R.string.action_search), R.id.action_search);
|
||||
Espresso.closeSoftKeyboard();
|
||||
|
||||
EspressoTestUtils.checkTextInSearchQuery("");
|
||||
EspressoTestUtils.checkListMatchesSearchQuery("", ALBUM_COMPLETE_LIST_SIZE, R.id.list);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests if search query is restored when returning
|
||||
* to the original tab
|
||||
*
|
||||
* UI interaction flow tested:
|
||||
* 1. Enter search query
|
||||
* 2. Switch to Albums tab
|
||||
* 3. Switch to Artists tab
|
||||
* 4. Result: search query entered at 1. should show in search field and list should match search query
|
||||
*/
|
||||
@Test
|
||||
public void searchSwitchTabReturnTest() {
|
||||
EspressoTestUtils.enterSearchQuery(mActivityRule.getActivity(), ARTIST_SEARCH_QUERY);
|
||||
clickAlbumsTab();
|
||||
clickArtistsTab();
|
||||
|
||||
EspressoTestUtils.checkTextInSearchQuery(ARTIST_SEARCH_QUERY);
|
||||
EspressoTestUtils.checkListMatchesSearchQuery(ARTIST_SEARCH_QUERY, ARTIST_SEARCH_QUERY_LIST_SIZE, R.id.list);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Tests if search query is restored when returning
|
||||
* to the original tab after switching to a different
|
||||
* tab and rotating the device
|
||||
*
|
||||
* UI interaction flow tested:
|
||||
* 1. Enter search query
|
||||
* 2. Switch to Albums tab
|
||||
* 3. Rotate device
|
||||
* 4. Switch to Artists tab
|
||||
* 5. Result: search query entered at 1. should show in search field and list should match search query
|
||||
*/
|
||||
@Test
|
||||
public void searchSwitchTabRotateReturnTest() {
|
||||
Activity activity = mActivityRule.getActivity();
|
||||
|
||||
EspressoTestUtils.enterSearchQuery(activity, ARTIST_SEARCH_QUERY);
|
||||
clickAlbumsTab();
|
||||
EspressoTestUtils.rotateDevice(activity);
|
||||
clickArtistsTab();
|
||||
|
||||
EspressoTestUtils.checkTextInSearchQuery(ARTIST_SEARCH_QUERY);
|
||||
EspressoTestUtils.checkListMatchesSearchQuery(ARTIST_SEARCH_QUERY, ARTIST_SEARCH_QUERY_LIST_SIZE, R.id.list);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests if search query is still cleared when user clears a previous
|
||||
* search query and switches to a different tab and returns to the
|
||||
* original tab
|
||||
*
|
||||
* UI interaction flow tested:
|
||||
* 1. Enter search query
|
||||
* 2. Clear search query
|
||||
* 3. Switch to Albums tab
|
||||
* 4. Switch to Artists tab
|
||||
* 5. Click search menu item
|
||||
* 6. Result: search query should be cleared and list should contain all items
|
||||
*/
|
||||
@Test
|
||||
public void searchClearSwitchTabSwitchBack() {
|
||||
Activity activity = mActivityRule.getActivity();
|
||||
|
||||
EspressoTestUtils.enterSearchQuery(activity, ARTIST_SEARCH_QUERY);
|
||||
EspressoTestUtils.checkTextInSearchQuery(ARTIST_SEARCH_QUERY);
|
||||
EspressoTestUtils.clearSearchQuery(activity);
|
||||
clickAlbumsTab();
|
||||
clickArtistsTab();
|
||||
EspressoTestUtils.clickMenuItem(activity, activity.getString(R.string.action_search), R.id.action_search);
|
||||
|
||||
EspressoTestUtils.checkTextInSearchQuery("");
|
||||
EspressoTestUtils.checkListMatchesSearchQuery("", ARTIST_COMPLETE_LIST_SIZE, R.id.list);
|
||||
}
|
||||
|
||||
/**
|
||||
* Same test as {@link #searchClearSwitchTabSwitchBack()} but this time clearing performed using X button
|
||||
*
|
||||
* UI interaction flow tested:
|
||||
* 1. Enter search query
|
||||
* 2. Clear search query
|
||||
* 3. Switch to Albums tab
|
||||
* 4. Switch to Artists tab
|
||||
* 5. Click search menu item using X button
|
||||
* 6. Result: search query should be cleared and list should contain all items
|
||||
*/
|
||||
@Test
|
||||
public void searchSwitchTabSwitchBackClearUsingXButtonSwitchTabSwitchBack() {
|
||||
Activity activity = mActivityRule.getActivity();
|
||||
|
||||
EspressoTestUtils.enterSearchQuery(activity, ARTIST_SEARCH_QUERY);
|
||||
EspressoTestUtils.checkTextInSearchQuery(ARTIST_SEARCH_QUERY);
|
||||
clickAlbumsTab();
|
||||
clickArtistsTab();
|
||||
EspressoTestUtils.clearSearchQueryXButton(activity);
|
||||
clickAlbumsTab();
|
||||
clickArtistsTab();
|
||||
|
||||
EspressoTestUtils.checkSearchMenuCollapsed();
|
||||
EspressoTestUtils.clickMenuItem(activity, activity.getString(R.string.action_search), R.id.action_search);
|
||||
EspressoTestUtils.checkTextInSearchQuery("");
|
||||
EspressoTestUtils.checkListMatchesSearchQuery("", ARTIST_COMPLETE_LIST_SIZE, R.id.list);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests if search queries for separate tabs are restored correctly
|
||||
*
|
||||
* UI interaction flow tested:
|
||||
* 1. Enter search query artists tab
|
||||
* 2. Enter search query albums tab
|
||||
* 3. Switch to Artists tab
|
||||
* 4. Result: search query entered at 1. should show in search field and list should match search query
|
||||
* 5. Switch to Albums tab
|
||||
* 6. Result: search query entered at 2. should show in search field and list should match search query
|
||||
*/
|
||||
@Test
|
||||
public void searchArtistsSearchAlbumsSwitchArtists() {
|
||||
Activity activity = mActivityRule.getActivity();
|
||||
|
||||
EspressoTestUtils.enterSearchQuery(activity, ARTIST_SEARCH_QUERY);
|
||||
clickAlbumsTab();
|
||||
EspressoTestUtils.enterSearchQuery(activity, ALBUMS_SEARCH_QUERY);
|
||||
clickArtistsTab();
|
||||
|
||||
EspressoTestUtils.checkTextInSearchQuery(ARTIST_SEARCH_QUERY);
|
||||
EspressoTestUtils.checkListMatchesSearchQuery(ARTIST_SEARCH_QUERY, ARTIST_SEARCH_QUERY_LIST_SIZE, R.id.list);
|
||||
|
||||
clickAlbumsTab();
|
||||
|
||||
EspressoTestUtils.checkTextInSearchQuery(ALBUMS_SEARCH_QUERY);
|
||||
EspressoTestUtils.checkListMatchesSearchQuery(ALBUMS_SEARCH_QUERY, ALBUM_SEARCH_QUERY_LIST_SIZE, R.id.list);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,666 @@
|
|||
/*
|
||||
* 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.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.os.SystemClock;
|
||||
import android.view.View;
|
||||
import android.widget.SeekBar;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.preference.PreferenceManager;
|
||||
import androidx.test.rule.ActivityTestRule;
|
||||
|
||||
import com.google.android.material.bottomsheet.BottomSheetBehavior;
|
||||
|
||||
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.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.NowPlayingPanel;
|
||||
import org.xbmc.kore.ui.widgets.RepeatModeButton;
|
||||
|
||||
import java.util.concurrent.TimeoutException;
|
||||
|
||||
import static androidx.test.espresso.Espresso.onView;
|
||||
import static androidx.test.espresso.Espresso.pressBack;
|
||||
import static androidx.test.espresso.action.ViewActions.click;
|
||||
import static androidx.test.espresso.assertion.ViewAssertions.matches;
|
||||
import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
|
||||
import static androidx.test.espresso.matcher.ViewMatchers.isRoot;
|
||||
import static androidx.test.espresso.matcher.ViewMatchers.withId;
|
||||
import static androidx.test.espresso.matcher.ViewMatchers.withText;
|
||||
import static org.hamcrest.Matchers.not;
|
||||
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> {
|
||||
|
||||
@Rule
|
||||
public ActivityTestRule<MusicActivity> musicActivityActivityTestRule =
|
||||
new ActivityTestRule<>(MusicActivity.class);
|
||||
|
||||
@Override
|
||||
protected ActivityTestRule<MusicActivity> getActivityTestRule() {
|
||||
return musicActivityActivityTestRule;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void setSharedPreferences(Context context) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setUp() throws Throwable {
|
||||
super.setUp();
|
||||
|
||||
getPlaylistHandler().reset();
|
||||
getPlaylistHandler().addItemToPlaylist(Playlist.playlistID.AUDIO, createMusicItem(0, 0), true);
|
||||
getPlaylistHandler().addItemToPlaylist(Playlist.playlistID.VIDEO, createVideoItem(0, 1), false);
|
||||
getPlaylistHandler().addItemToPlaylist(Playlist.playlistID.VIDEO, createMusicVideoItem(0, 2), false);
|
||||
|
||||
getPlayerHandler().reset();
|
||||
getPlayerHandler().setPlaylists(getPlaylistHandler().getPlaylists());
|
||||
getPlayerHandler().startPlay(Playlist.playlistID.AUDIO, 0);
|
||||
|
||||
waitForPanelState(BottomSheetBehavior.STATE_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.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.next)).check(matches(isDisplayed()));
|
||||
onView(withId(R.id.previous)).check(matches(isDisplayed()));
|
||||
onView(withId(R.id.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().startPlay(Playlist.playlistID.VIDEO, 0);
|
||||
Player.GetItem item = getPlayerHandler().getMediaItem();
|
||||
final String title = item.getTitle();
|
||||
onView(isRoot()).perform(ViewActions.waitForView(
|
||||
R.id.title, new ViewActions.CheckStatus() {
|
||||
@Override
|
||||
public boolean check(View v) {
|
||||
return title.contentEquals(((TextView) v).getText());
|
||||
}
|
||||
}, 10000));
|
||||
|
||||
onView(withId(R.id.next)).check(matches(not(isDisplayed())));
|
||||
onView(withId(R.id.previous)).check(matches(not(isDisplayed())));
|
||||
onView(withId(R.id.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().startPlay(Playlist.playlistID.VIDEO, 1);
|
||||
Player.GetItem item = getPlayerHandler().getMediaItem();
|
||||
final String title = item.getTitle();
|
||||
onView(isRoot()).perform(ViewActions.waitForView(
|
||||
R.id.title, new ViewActions.CheckStatus() {
|
||||
@Override
|
||||
public boolean check(View v) {
|
||||
return title.contentEquals(((TextView) v).getText());
|
||||
}
|
||||
}, 10000));
|
||||
|
||||
onView(withId(R.id.next)).check(matches(isDisplayed()));
|
||||
onView(withId(R.id.previous)).check(matches(isDisplayed()));
|
||||
onView(withId(R.id.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.shuffle)).perform(click());
|
||||
|
||||
onView(isRoot()).perform(ViewActions.waitForView(R.id.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.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.repeat)).perform(click());
|
||||
onView(isRoot()).perform(ViewActions.waitForView(R.id.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.repeat)).perform(click());
|
||||
onView(isRoot()).perform(ViewActions.waitForView(R.id.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.repeat)).perform(click());
|
||||
onView(isRoot()).perform(ViewActions.waitForView(R.id.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(BottomSheetBehavior.STATE_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(BottomSheetBehavior.STATE_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.repeat)).perform(click());
|
||||
|
||||
rotateDevice(getActivity());
|
||||
|
||||
onView(isRoot()).perform(ViewActions.waitForView(R.id.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.shuffle)).perform(click()); //Set state to shuffled
|
||||
|
||||
onView(withId(R.id.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.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.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() throws TimeoutException {
|
||||
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))));
|
||||
|
||||
getConnectionHandlerManager().waitForMethodHandled(Application.SetVolume.METHOD_NAME, 10000);
|
||||
assertTrue("applicationHandler volume: "+ getApplicationHandler().getVolume()
|
||||
+ " != " + volume, 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() 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) {
|
||||
return ((SeekBar) v).getProgress() == volume;
|
||||
}
|
||||
}, 10000));
|
||||
onView(withId(R.id.vli_volume_text)).check(matches(withText(String.valueOf(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.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().getTimeElapsed() == 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.play)).perform(click()); //Pause playback
|
||||
|
||||
onView(withId(R.id.mpi_seek_bar)).perform(ViewActions.slideSeekBar(progress));
|
||||
rotateDevice(getActivity());
|
||||
|
||||
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)));
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.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);
|
||||
final int progress = seekBar.getProgress();
|
||||
onView(isRoot()).perform(ViewActions.waitForView(
|
||||
R.id.mpi_seek_bar, new ViewActions.CheckStatus() {
|
||||
@Override
|
||||
public boolean check(View v) {
|
||||
return ((SeekBar) v).getProgress() > progress;
|
||||
}
|
||||
}, 10000));
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
final int progress = seekBar.getProgress();
|
||||
|
||||
onView(isRoot()).perform(ViewActions.waitForView(
|
||||
R.id.mpi_seek_bar, new ViewActions.CheckStatus() {
|
||||
@Override
|
||||
public boolean check(View v) {
|
||||
return ((SeekBar) v).getProgress() > progress;
|
||||
}
|
||||
}, 10000));
|
||||
}
|
||||
|
||||
/**
|
||||
* 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(BottomSheetBehavior.STATE_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.play)).perform(click());
|
||||
|
||||
assertSame(getPlayerHandler().getPlayState(), PlayerHandler.PLAY_STATE.PAUSED);
|
||||
|
||||
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(BottomSheetBehavior.STATE_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(BottomSheetBehavior.STATE_COLLAPSED);
|
||||
}
|
||||
|
||||
private void expandPanel() {
|
||||
int tries = 10;
|
||||
while (tries-- > 0) {
|
||||
try {
|
||||
onView(withId(R.id.title)).perform(click());
|
||||
|
||||
onView(isRoot()).perform(ViewActions.waitForView(R.id.now_playing_panel, new ViewActions.CheckStatus() {
|
||||
@Override
|
||||
public boolean check(View v) {
|
||||
return ((NowPlayingPanel) v).getPanelState() == BottomSheetBehavior.STATE_EXPANDED;
|
||||
}
|
||||
}, 1000));
|
||||
|
||||
return;
|
||||
} catch (Exception e) {
|
||||
//Either the click event did not work or the panel did not expand.
|
||||
//Let's try again.
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,168 @@
|
|||
/*
|
||||
* 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.remote.controlpad.eventserver;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.test.rule.ActivityTestRule;
|
||||
|
||||
import org.junit.After;
|
||||
import org.junit.AfterClass;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.xbmc.kore.R;
|
||||
import org.xbmc.kore.eventclient.ButtonCodes;
|
||||
import org.xbmc.kore.host.HostInfo;
|
||||
import org.xbmc.kore.host.HostManager;
|
||||
import org.xbmc.kore.jsonrpc.method.Input;
|
||||
import org.xbmc.kore.testhelpers.TestUtils;
|
||||
import org.xbmc.kore.testhelpers.Utils;
|
||||
import org.xbmc.kore.tests.ui.AbstractTestClass;
|
||||
import org.xbmc.kore.testutils.eventserver.EventPacket;
|
||||
import org.xbmc.kore.testutils.eventserver.EventPacketBUTTON;
|
||||
import org.xbmc.kore.testutils.eventserver.MockEventServer;
|
||||
import org.xbmc.kore.ui.sections.remote.RemoteActivity;
|
||||
|
||||
import static androidx.test.espresso.Espresso.onView;
|
||||
import static androidx.test.espresso.action.ViewActions.click;
|
||||
import static androidx.test.espresso.action.ViewActions.longClick;
|
||||
import static androidx.test.espresso.matcher.ViewMatchers.withId;
|
||||
import static junit.framework.Assert.assertTrue;
|
||||
|
||||
public class ButtonTests extends AbstractTestClass<RemoteActivity> {
|
||||
private static MockEventServer mockEventServer;
|
||||
|
||||
@Rule
|
||||
public ActivityTestRule<RemoteActivity> remoteActivityActivityTestRule =
|
||||
new ActivityTestRule<>(RemoteActivity.class);
|
||||
|
||||
@Override
|
||||
protected ActivityTestRule<RemoteActivity> getActivityTestRule() {
|
||||
return remoteActivityActivityTestRule;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void setSharedPreferences(Context context) {
|
||||
Utils.setUseEventServerPreference(context, true);
|
||||
}
|
||||
|
||||
@BeforeClass
|
||||
public static void setupEventServer() {
|
||||
mockEventServer = new MockEventServer();
|
||||
mockEventServer.setListenPort(HostInfo.DEFAULT_EVENT_SERVER_PORT);
|
||||
mockEventServer.start();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setUp() throws Throwable {
|
||||
setKodiMajorVersion(HostInfo.KODI_V17_KRYPTON);
|
||||
super.setUp();
|
||||
}
|
||||
|
||||
@After
|
||||
public void resetState() {
|
||||
mockEventServer.reset();
|
||||
}
|
||||
|
||||
@AfterClass
|
||||
public static void cleanup() {
|
||||
mockEventServer.shutdown();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void leftControlPadButtonTest() {
|
||||
onView(withId(R.id.left)).perform(click());
|
||||
|
||||
testRemoteButton(ButtonCodes.REMOTE_LEFT);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void rightControlPadButtonTest() {
|
||||
onView(withId(R.id.right)).perform(click());
|
||||
|
||||
testRemoteButton(ButtonCodes.REMOTE_RIGHT);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void upControlPadButtonTest() {
|
||||
onView(withId(R.id.up)).perform(click());
|
||||
|
||||
testRemoteButton(ButtonCodes.REMOTE_UP);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void downControlPadButtonTest() {
|
||||
onView(withId(R.id.down)).perform(click());
|
||||
|
||||
testRemoteButton(ButtonCodes.REMOTE_DOWN);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void selectPadButtonTest() {
|
||||
onView(withId(R.id.select)).perform(click());
|
||||
|
||||
testRemoteButton(ButtonCodes.REMOTE_SELECT);
|
||||
}
|
||||
|
||||
//The following tests do not use the event server. They're included here
|
||||
//to make sure they still work when the event server is enabled.
|
||||
@Test
|
||||
public void contextControlPadButtonTest() {
|
||||
onView(withId(R.id.context)).perform(click());
|
||||
|
||||
TestUtils.testHTTPEvent(Input.ExecuteAction.METHOD_NAME, Input.ExecuteAction.CONTEXTMENU);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void infoControlPadButtonTest() {
|
||||
HostManager.getInstance(getActivity()).getHostInfo().setKodiVersionMajor(17);
|
||||
|
||||
onView(withId(R.id.info)).perform(click());
|
||||
|
||||
TestUtils.testHTTPEvent(Input.ExecuteAction.METHOD_NAME, Input.ExecuteAction.INFO);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void infoControlPadButtonLongClickTest() {
|
||||
onView(withId(R.id.info)).perform(longClick());
|
||||
|
||||
TestUtils.testHTTPEvent(Input.ExecuteAction.METHOD_NAME, Input.ExecuteAction.PLAYERPROCESSINFO);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void osdControlPadButtonTest() {
|
||||
onView(withId(R.id.osd)).perform(click());
|
||||
|
||||
TestUtils.testHTTPEvent(Input.ExecuteAction.METHOD_NAME, Input.ExecuteAction.OSD);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void backControlPadButtonTest() {
|
||||
onView(withId(R.id.back)).perform(click());
|
||||
|
||||
TestUtils.testHTTPEvent(Input.Back.METHOD_NAME, null);
|
||||
}
|
||||
|
||||
private void testRemoteButton(String buttonName) {
|
||||
EventPacket packet = mockEventServer.getEventPacket();
|
||||
assertTrue(packet != null);
|
||||
assertTrue(packet.getPacketType() == EventPacket.PT_BUTTON);
|
||||
assertTrue(((EventPacketBUTTON) packet).getButtonName().contentEquals(buttonName));
|
||||
assertTrue(((EventPacketBUTTON) packet).getMapName().contentEquals(ButtonCodes.MAP_REMOTE));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,89 @@
|
|||
/*
|
||||
* 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.remote.controlpad.eventserver;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.test.rule.ActivityTestRule;
|
||||
|
||||
import org.junit.After;
|
||||
import org.junit.AfterClass;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.xbmc.kore.R;
|
||||
import org.xbmc.kore.host.HostInfo;
|
||||
import org.xbmc.kore.jsonrpc.method.Input;
|
||||
import org.xbmc.kore.testhelpers.Utils;
|
||||
import org.xbmc.kore.tests.ui.AbstractTestClass;
|
||||
import org.xbmc.kore.testutils.eventserver.MockEventServer;
|
||||
import org.xbmc.kore.ui.sections.remote.RemoteActivity;
|
||||
|
||||
import static androidx.test.espresso.Espresso.onView;
|
||||
import static androidx.test.espresso.action.ViewActions.longClick;
|
||||
import static androidx.test.espresso.matcher.ViewMatchers.withId;
|
||||
import static junit.framework.Assert.assertTrue;
|
||||
|
||||
public class KodiPreV17Tests extends AbstractTestClass<RemoteActivity> {
|
||||
private static MockEventServer mockEventServer;
|
||||
|
||||
@Rule
|
||||
public ActivityTestRule<RemoteActivity> remoteActivityActivityTestRule =
|
||||
new ActivityTestRule<>(RemoteActivity.class);
|
||||
|
||||
@Override
|
||||
protected ActivityTestRule<RemoteActivity> getActivityTestRule() {
|
||||
return remoteActivityActivityTestRule;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void setSharedPreferences(Context context) {
|
||||
Utils.setUseEventServerPreference(context, true);
|
||||
}
|
||||
|
||||
@BeforeClass
|
||||
public static void setupEventServer() {
|
||||
mockEventServer = new MockEventServer();
|
||||
mockEventServer.setListenPort(HostInfo.DEFAULT_EVENT_SERVER_PORT);
|
||||
mockEventServer.start();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setUp() throws Throwable {
|
||||
setKodiMajorVersion(HostInfo.KODI_V16_JARVIS);
|
||||
super.setUp();
|
||||
}
|
||||
|
||||
@After
|
||||
public void resetState() {
|
||||
mockEventServer.reset();
|
||||
}
|
||||
|
||||
@AfterClass
|
||||
public static void cleanup() {
|
||||
mockEventServer.shutdown();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void infoControlPadButtonLongClickTest() {
|
||||
onView(withId(R.id.info)).perform(longClick());
|
||||
|
||||
String actionReceived = getInputHandler().getAction();
|
||||
assertTrue(actionReceived != null);
|
||||
assertTrue(actionReceived.contentEquals(Input.ExecuteAction.CODECINFO));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,132 @@
|
|||
/*
|
||||
* 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.remote.controlpad.http;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.test.rule.ActivityTestRule;
|
||||
|
||||
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.TestUtils;
|
||||
import org.xbmc.kore.testhelpers.Utils;
|
||||
import org.xbmc.kore.tests.ui.AbstractTestClass;
|
||||
import org.xbmc.kore.ui.sections.remote.RemoteActivity;
|
||||
|
||||
import static androidx.test.espresso.Espresso.onView;
|
||||
import static androidx.test.espresso.action.ViewActions.click;
|
||||
import static androidx.test.espresso.action.ViewActions.longClick;
|
||||
import static androidx.test.espresso.matcher.ViewMatchers.withId;
|
||||
|
||||
|
||||
public class ButtonTests extends AbstractTestClass<RemoteActivity> {
|
||||
@Rule
|
||||
public ActivityTestRule<RemoteActivity> remoteActivityActivityTestRule =
|
||||
new ActivityTestRule<>(RemoteActivity.class);
|
||||
|
||||
@Override
|
||||
protected ActivityTestRule<RemoteActivity> getActivityTestRule() {
|
||||
return remoteActivityActivityTestRule;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void setSharedPreferences(Context context) {
|
||||
Utils.setUseEventServerPreference(context, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setUp() throws Throwable {
|
||||
setKodiMajorVersion(HostInfo.KODI_V17_KRYPTON);
|
||||
super.setUp();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void leftControlPadButtonTest() {
|
||||
onView(withId(R.id.left)).perform(click());
|
||||
|
||||
TestUtils.testHTTPEvent(Input.Left.METHOD_NAME, null);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void rightControlPadButtonTest() {
|
||||
onView(withId(R.id.right)).perform(click());
|
||||
|
||||
TestUtils.testHTTPEvent(Input.Right.METHOD_NAME, null);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void upControlPadButtonTest() {
|
||||
onView(withId(R.id.up)).perform(click());
|
||||
|
||||
TestUtils.testHTTPEvent(Input.Up.METHOD_NAME, null);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void downControlPadButtonTest() {
|
||||
onView(withId(R.id.down)).perform(click());
|
||||
|
||||
TestUtils.testHTTPEvent(Input.Down.METHOD_NAME, null);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void selectPadButtonTest() {
|
||||
onView(withId(R.id.select)).perform(click());
|
||||
|
||||
TestUtils.testHTTPEvent(Input.Select.METHOD_NAME, null);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void contextControlPadButtonTest() {
|
||||
onView(withId(R.id.context)).perform(click());
|
||||
|
||||
TestUtils.testHTTPEvent(Input.ExecuteAction.METHOD_NAME, Input.ExecuteAction.CONTEXTMENU);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void infoControlPadButtonTest() {
|
||||
HostManager.getInstance(getActivity()).getHostInfo().setKodiVersionMajor(17);
|
||||
|
||||
onView(withId(R.id.info)).perform(click());
|
||||
|
||||
TestUtils.testHTTPEvent(Input.ExecuteAction.METHOD_NAME, Input.ExecuteAction.INFO);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void infoControlPadButtonLongClickTest() {
|
||||
onView(withId(R.id.info)).perform(longClick());
|
||||
|
||||
TestUtils.testHTTPEvent(Input.ExecuteAction.METHOD_NAME, Input.ExecuteAction.PLAYERPROCESSINFO);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void osdControlPadButtonTest() {
|
||||
onView(withId(R.id.osd)).perform(click());
|
||||
|
||||
TestUtils.testHTTPEvent(Input.ExecuteAction.METHOD_NAME, Input.ExecuteAction.OSD);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void backControlPadButtonTest() {
|
||||
onView(withId(R.id.back)).perform(click());
|
||||
|
||||
TestUtils.testHTTPEvent(Input.Back.METHOD_NAME, null);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
/*
|
||||
* 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.remote.controlpad.http;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.test.rule.ActivityTestRule;
|
||||
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.xbmc.kore.R;
|
||||
import org.xbmc.kore.host.HostInfo;
|
||||
import org.xbmc.kore.jsonrpc.method.Input;
|
||||
import org.xbmc.kore.testhelpers.TestUtils;
|
||||
import org.xbmc.kore.testhelpers.Utils;
|
||||
import org.xbmc.kore.tests.ui.AbstractTestClass;
|
||||
import org.xbmc.kore.ui.sections.remote.RemoteActivity;
|
||||
|
||||
import static androidx.test.espresso.Espresso.onView;
|
||||
import static androidx.test.espresso.action.ViewActions.longClick;
|
||||
import static androidx.test.espresso.matcher.ViewMatchers.withId;
|
||||
|
||||
public class KodiPreV17Tests extends AbstractTestClass<RemoteActivity> {
|
||||
@Rule
|
||||
public ActivityTestRule<RemoteActivity> remoteActivityActivityTestRule =
|
||||
new ActivityTestRule<>(RemoteActivity.class);
|
||||
|
||||
@Override
|
||||
protected ActivityTestRule<RemoteActivity> getActivityTestRule() {
|
||||
return remoteActivityActivityTestRule;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void setSharedPreferences(Context context) {
|
||||
Utils.setUseEventServerPreference(context, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setUp() throws Throwable {
|
||||
setKodiMajorVersion(HostInfo.KODI_V16_JARVIS);
|
||||
super.setUp();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void infoControlPadButtonLongClickTest() {
|
||||
onView(withId(R.id.info)).perform(longClick());
|
||||
|
||||
TestUtils.testHTTPEvent(Input.ExecuteAction.METHOD_NAME, Input.ExecuteAction.CODECINFO);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,294 @@
|
|||
/*
|
||||
* 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.tests.ui.remote.playlistfragment.TCP;
|
||||
|
||||
import android.content.Context;
|
||||
import android.view.View;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.test.rule.ActivityTestRule;
|
||||
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.xbmc.kore.R;
|
||||
import org.xbmc.kore.testhelpers.EspressoTestUtils;
|
||||
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.testutils.tcpserver.handlers.jsonrpc.response.methods.Playlist;
|
||||
import org.xbmc.kore.ui.sections.remote.RemoteActivity;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
|
||||
import static androidx.test.espresso.Espresso.onView;
|
||||
import static androidx.test.espresso.action.ViewActions.swipeLeft;
|
||||
import static androidx.test.espresso.action.ViewActions.swipeRight;
|
||||
import static androidx.test.espresso.assertion.ViewAssertions.matches;
|
||||
import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
|
||||
import static androidx.test.espresso.matcher.ViewMatchers.isRoot;
|
||||
import static androidx.test.espresso.matcher.ViewMatchers.withId;
|
||||
import static androidx.test.espresso.matcher.ViewMatchers.withText;
|
||||
import static org.hamcrest.Matchers.allOf;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.xbmc.kore.testutils.TestUtils.createMusicItem;
|
||||
import static org.xbmc.kore.testutils.TestUtils.createPictureItem;
|
||||
import static org.xbmc.kore.testutils.TestUtils.createVideoItem;
|
||||
import static org.xbmc.kore.testutils.tcpserver.handlers.jsonrpc.response.notifications.Playlist.OnClear;
|
||||
|
||||
public class PlaylistTests extends AbstractTestClass<RemoteActivity> {
|
||||
|
||||
private static final int PLAYLIST_SIZE = 10;
|
||||
|
||||
@Rule
|
||||
public ActivityTestRule<RemoteActivity> remoteActivityActivityTestRule =
|
||||
new ActivityTestRule<>(RemoteActivity.class);
|
||||
|
||||
@Override
|
||||
protected ActivityTestRule<RemoteActivity> getActivityTestRule() {
|
||||
return remoteActivityActivityTestRule;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void setSharedPreferences(Context context) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setUp() throws Throwable {
|
||||
int itemId = 0;
|
||||
|
||||
getPlaylistHandler().reset();
|
||||
for (int i = 0; i < PLAYLIST_SIZE; i++) {
|
||||
getPlaylistHandler().addItemToPlaylist(Playlist.playlistID.AUDIO, createMusicItem(i, itemId++), false);
|
||||
getPlaylistHandler().addItemToPlaylist(Playlist.playlistID.VIDEO, createVideoItem(i, itemId++), false);
|
||||
getPlaylistHandler().addItemToPlaylist(Playlist.playlistID.PICTURE, createPictureItem(i, itemId++), false);
|
||||
}
|
||||
|
||||
getPlayerHandler().reset();
|
||||
getPlayerHandler().setPlaylists(getPlaylistHandler().getPlaylists());
|
||||
getPlayerHandler().startPlay(Playlist.playlistID.AUDIO, 0);
|
||||
|
||||
// Checking for available playlists is done in PlaylistFragment on startup
|
||||
// and every 10 seconds. To make sure PlaylistFragment can get the available
|
||||
// playlists at startup, the activity needs to be created after the backend
|
||||
// has been fully setup.
|
||||
super.setUp();
|
||||
|
||||
onView(isRoot()).perform(swipeLeft());
|
||||
waitForAudioPlaylistToShow();
|
||||
}
|
||||
|
||||
/**
|
||||
* Test if playlist is not cleared when playback is stopped
|
||||
*
|
||||
* UI interaction flow tested:
|
||||
* 1. Start playing multiple music items
|
||||
* 2. Stop playback
|
||||
* 3. Result: playlist should still be visible
|
||||
*/
|
||||
@Test
|
||||
public void keepPlaylistOnStop() {
|
||||
onView(isRoot()).perform(swipeRight());
|
||||
EspressoTestUtils.clickButton(R.id.stop);
|
||||
onView(isRoot()).perform(swipeLeft());
|
||||
|
||||
assertEquals(getPlaylistHandler().getPlaylist(Playlist.playlistID.AUDIO).size(), PLAYLIST_SIZE);
|
||||
EspressoTestUtils.checkListViewSize(PLAYLIST_SIZE, R.id.playlist);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test if playlist is not cleared when playback is paused
|
||||
*
|
||||
* UI interaction flow tested:
|
||||
* 1. Start playing multiple music items
|
||||
* 2. Pause playback
|
||||
* 3. Result: playlist should still be visible
|
||||
*/
|
||||
@Test
|
||||
public void keepPlaylistOnPause() {
|
||||
onView(isRoot()).perform(swipeRight());
|
||||
EspressoTestUtils.clickButton(R.id.play);
|
||||
onView(isRoot()).perform(swipeLeft());
|
||||
|
||||
assertEquals(getPlaylistHandler().getPlaylist(Playlist.playlistID.AUDIO).size(), PLAYLIST_SIZE);
|
||||
EspressoTestUtils.checkListViewSize(PLAYLIST_SIZE, R.id.playlist);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test if playlist is cleared when cleared on Kodi
|
||||
*
|
||||
* UI interaction flow tested:
|
||||
* 1. Start playing multiple music items
|
||||
* 2. Clear playlist on server (Kodi)
|
||||
* 3. Result: playlist should be empty
|
||||
*/
|
||||
@Test
|
||||
public void clearPlaylistWhenClearedOnKodi() throws Exception {
|
||||
getPlaylistHandler().clearPlaylist(Playlist.playlistID.AUDIO);
|
||||
getConnectionHandlerManager().waitForNotification(OnClear.METHOD_NAME, 10000);
|
||||
|
||||
assertEquals(0, getPlaylistHandler().getPlaylist(Playlist.playlistID.AUDIO).size());
|
||||
onView(allOf(withId(R.id.info_title), withText(R.string.playlist_empty)))
|
||||
.check(matches(isDisplayed()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test if playback of a playlist is resumed after stopping playback
|
||||
*
|
||||
* UI interaction flow tested:
|
||||
* 1. Start playing multiple music items
|
||||
* 2. Stop playback
|
||||
* 3. Click on playlist item
|
||||
* 4. Result: playback should resume from clicked playlist item
|
||||
*/
|
||||
@Test
|
||||
public void stopPlayingAndResumeNextItem() throws TimeoutException {
|
||||
int positionClicked = 3;
|
||||
onView(isRoot()).perform(swipeRight());
|
||||
EspressoTestUtils.clickButton(R.id.stop);
|
||||
onView(isRoot()).perform(swipeLeft());
|
||||
getConnectionHandlerManager().clearMethodsHandled();
|
||||
EspressoTestUtils.clickAdapterViewItem(positionClicked, R.id.playlist);
|
||||
getConnectionHandlerManager().waitForMethodHandled(Player.Open.METHOD_NAME, 10000);
|
||||
|
||||
List<Player.GetItem> playlistOnServer = getPlaylistHandler().getPlaylist(Playlist.playlistID.AUDIO);
|
||||
assertSame(getPlayerHandler().getPlayState(), PlayerHandler.PLAY_STATE.PLAYING);
|
||||
assertEquals("Playlist on server has size " + playlistOnServer.size() +
|
||||
" but should be " + PLAYLIST_SIZE, playlistOnServer.size(), PLAYLIST_SIZE);
|
||||
assertEquals("Current playing item ID is " + getPlayerHandler().getMediaItem().getLibraryId() +
|
||||
", but this should be " + playlistOnServer.get(positionClicked).getLibraryId(),
|
||||
getPlayerHandler().getMediaItem().getLibraryId(), playlistOnServer.get(positionClicked).getLibraryId());
|
||||
}
|
||||
|
||||
/**
|
||||
* Test if playlist is correctly restored after playback has stopped
|
||||
* and device configuration changed
|
||||
* UI interaction flow tested:
|
||||
* 1. Start playing multiple music items
|
||||
* 2. Rotate device
|
||||
* 3. Result: playlist should be the same as before rotation
|
||||
*/
|
||||
@Test
|
||||
public void restorePlaylistAfterConfigurationChange() {
|
||||
getConnectionHandlerManager().clearMethodsHandled();
|
||||
EspressoTestUtils.rotateDevice(getActivity());
|
||||
waitForAudioPlaylistToShow();
|
||||
|
||||
assertEquals(getPlaylistHandler().getPlaylist(Playlist.playlistID.AUDIO).size(), PLAYLIST_SIZE);
|
||||
EspressoTestUtils.checkListViewSize(PLAYLIST_SIZE, R.id.playlist);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test if playlist is correctly restored after playback has stopped
|
||||
* and device configuration changed
|
||||
* UI interaction flow tested:
|
||||
* 1. Start playing multiple music items
|
||||
* 2. Stop playback
|
||||
* 3. Rotate device
|
||||
* 4. Result: playlist should be the same as before rotation
|
||||
*/
|
||||
@Test
|
||||
public void restorePlaylistAfterStopAndConfigurationChange() {
|
||||
onView(isRoot()).perform(swipeRight());
|
||||
EspressoTestUtils.clickButton(R.id.stop);
|
||||
onView(isRoot()).perform(swipeLeft());
|
||||
|
||||
getConnectionHandlerManager().clearMethodsHandled();
|
||||
EspressoTestUtils.rotateDevice(getActivity());
|
||||
waitForAudioPlaylistToShow();
|
||||
|
||||
assertEquals(getPlaylistHandler().getPlaylist(Playlist.playlistID.AUDIO).size(), PLAYLIST_SIZE);
|
||||
EspressoTestUtils.checkListViewSize(PLAYLIST_SIZE, R.id.playlist);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test if playlist for currently playing item is shown even if other
|
||||
* playlists are available on server
|
||||
* UI interaction flow tested:
|
||||
* 1. Add audio and video playlists on server
|
||||
* 2. Start playing video item
|
||||
* 3. Result: playlist for video items should be shown
|
||||
*/
|
||||
@Test
|
||||
public void showCurrentlyPlayingPlaylist() {
|
||||
getPlayerHandler().startPlay(Playlist.playlistID.VIDEO, 0);
|
||||
onView(isRoot()).perform(ViewActions.waitForView(R.id.playlist_item_title, new ViewActions.CheckStatus() {
|
||||
@Override
|
||||
public boolean check(View v) {
|
||||
return ((TextView) v).getText().toString().contains("Video");
|
||||
}
|
||||
}, 10000));
|
||||
|
||||
assertEquals("Playlist on server has size "
|
||||
+ getPlaylistHandler().getPlaylist(Playlist.playlistID.VIDEO).size() +
|
||||
" but should be " + PLAYLIST_SIZE,
|
||||
getPlaylistHandler().getPlaylist(Playlist.playlistID.VIDEO).size(), PLAYLIST_SIZE);
|
||||
assertEquals("Got media type "
|
||||
+ getPlayerHandler().getMediaItem().getType() +
|
||||
", this should be " + Player.GetItem.TYPE.movie.name(),
|
||||
getPlayerHandler().getMediaItem().getType(), Player.GetItem.TYPE.movie.name());
|
||||
|
||||
onView(allOf(withText(getPlayerHandler().getMediaItem().getTitle()), isDisplayed())).check(matches(isDisplayed()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test if playlist for last played item is shown when playback has stopped
|
||||
* and other playlists are available on server
|
||||
* UI interaction flow tested:
|
||||
* 1. Add audio, picture, and video playlists on server
|
||||
* 2. Start playing video item
|
||||
* 3. Stop playback
|
||||
* 4. Result: playlist for video items should be shown
|
||||
*/
|
||||
@Test
|
||||
public void showLastActivePlaylist() {
|
||||
getPlayerHandler().startPlay(Playlist.playlistID.VIDEO, 0);
|
||||
onView(isRoot()).perform(swipeRight());
|
||||
EspressoTestUtils.clickButton(R.id.stop);
|
||||
|
||||
onView(isRoot()).perform(swipeLeft());
|
||||
onView(isRoot()).perform(ViewActions.waitForView(R.id.playlist_item_title, new ViewActions.CheckStatus() {
|
||||
@Override
|
||||
public boolean check(View v) {
|
||||
return ((TextView) v).getText().toString().contains("Video");
|
||||
}
|
||||
}, 10000));
|
||||
|
||||
assertEquals("Playlist on server has size "
|
||||
+ getPlaylistHandler().getPlaylist(Playlist.playlistID.VIDEO).size() +
|
||||
" but should be " + PLAYLIST_SIZE,
|
||||
getPlaylistHandler().getPlaylist(Playlist.playlistID.VIDEO).size(), PLAYLIST_SIZE);
|
||||
assertEquals("Got media type "
|
||||
+ getPlayerHandler().getMediaItem().getType() +
|
||||
", this should be " + Player.GetItem.TYPE.movie.name(),
|
||||
getPlayerHandler().getMediaItem().getType(), Player.GetItem.TYPE.movie.name());
|
||||
|
||||
onView(allOf(withText(getPlayerHandler().getMediaItem().getTitle()), isDisplayed())).check(matches(isDisplayed()));
|
||||
}
|
||||
|
||||
private void waitForAudioPlaylistToShow() {
|
||||
onView(isRoot()).perform(ViewActions.waitForView(R.id.playlist_item_title, new ViewActions.CheckStatus() {
|
||||
@Override
|
||||
public boolean check(View v) {
|
||||
return "Music 1".contentEquals(((TextView) v).getText());
|
||||
}
|
||||
}, 10000));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,282 @@
|
|||
/*
|
||||
* 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.tvshows;
|
||||
|
||||
import android.content.Context;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.test.espresso.Espresso;
|
||||
import androidx.test.rule.ActivityTestRule;
|
||||
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.xbmc.kore.R;
|
||||
import org.xbmc.kore.testhelpers.EspressoTestUtils;
|
||||
import org.xbmc.kore.tests.ui.AbstractTestClass;
|
||||
import org.xbmc.kore.ui.sections.video.TVShowsActivity;
|
||||
|
||||
import static androidx.test.espresso.Espresso.onView;
|
||||
import static androidx.test.espresso.action.ViewActions.click;
|
||||
import static androidx.test.espresso.assertion.ViewAssertions.matches;
|
||||
import static androidx.test.espresso.matcher.ViewMatchers.withId;
|
||||
import static androidx.test.espresso.matcher.ViewMatchers.withParent;
|
||||
import static androidx.test.espresso.matcher.ViewMatchers.withText;
|
||||
import static org.hamcrest.CoreMatchers.allOf;
|
||||
import static org.hamcrest.Matchers.instanceOf;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.xbmc.kore.testhelpers.EspressoTestUtils.clickRecyclerViewItem;
|
||||
import static org.xbmc.kore.testhelpers.EspressoTestUtils.rotateDevice;
|
||||
import static org.xbmc.kore.testhelpers.action.ViewActions.nestedScrollTo;
|
||||
|
||||
public class TVShowsActivityTests extends AbstractTestClass<TVShowsActivity> {
|
||||
private final String TV_SHOW_TITLE = "11.22.63";
|
||||
private final String EPISODE_TITLE = "The Rabbit Hole";
|
||||
|
||||
@Rule
|
||||
public ActivityTestRule<TVShowsActivity> mActivityRule = new ActivityTestRule<>(
|
||||
TVShowsActivity.class);
|
||||
|
||||
@Override
|
||||
protected ActivityTestRule<TVShowsActivity> getActivityTestRule() {
|
||||
return mActivityRule;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void setSharedPreferences(Context context) {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Test if action bar title initially displays TV Shows
|
||||
*/
|
||||
@Test
|
||||
public void setActionBarTitleMain() {
|
||||
onView(allOf(instanceOf(TextView.class), withParent(withId(R.id.default_toolbar))))
|
||||
.check(matches(withText(R.string.tv_shows)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test if action bar title is correctly set after selecting a list item
|
||||
*
|
||||
* UI interaction flow tested:
|
||||
* 1. Click on list item
|
||||
* 2. Result: action bar title should show list item title
|
||||
*/
|
||||
@Test
|
||||
public void setActionBarTitle() {
|
||||
EspressoTestUtils.selectListItemAndCheckActionbarTitle(TV_SHOW_TITLE, R.id.list, TV_SHOW_TITLE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test if action bar title is correctly set after selecting a season
|
||||
*
|
||||
* UI interaction flow tested:
|
||||
* 1. Click on TV Show item
|
||||
* 2. Click on next episode item
|
||||
* 3. Result: action bar title should show next episode title
|
||||
*/
|
||||
@Test
|
||||
public void setActionBarTitleOnNextEpisode() {
|
||||
clickRecyclerViewItem(1, R.id.list);
|
||||
onView( withId(R.id.next_episode_list)).perform( nestedScrollTo(), click());
|
||||
|
||||
onView(allOf(instanceOf(TextView.class), withParent(withId(R.id.default_toolbar))))
|
||||
.check(matches(withText("3")));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test if action bar title is correctly set after selecting a season
|
||||
*
|
||||
* UI interaction flow tested:
|
||||
* 1. Click on TV Show item
|
||||
* 2. Click on season item
|
||||
* 3. Result: action bar title should show season title
|
||||
*/
|
||||
@Test
|
||||
public void setActionBarTitleOnSeasonList() {
|
||||
clickRecyclerViewItem(0, R.id.list);
|
||||
onView( withId(R.id.seasons_list)).perform(nestedScrollTo(), click());
|
||||
|
||||
onView(allOf(instanceOf(TextView.class), withParent(withId(R.id.default_toolbar))))
|
||||
.check(matches(withText("Season 01")));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test if action bar title is correctly set after selecting an episode from the season list
|
||||
*
|
||||
* UI interaction flow tested:
|
||||
* 1. Click on TV Show item
|
||||
* 2. Click on season item
|
||||
* 3. Click on an episode
|
||||
* 4. Result: action bar title should show episode title
|
||||
*/
|
||||
@Test
|
||||
public void setActionBarTitleOnSeasonListEpisode() {
|
||||
clickRecyclerViewItem(0, R.id.list);
|
||||
onView( withId(R.id.seasons_list)).perform( nestedScrollTo(), click());
|
||||
EspressoTestUtils.selectListItemAndCheckActionbarTitle(EPISODE_TITLE, R.id.list, TV_SHOW_TITLE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test if action bar title is correctly restored after a configuration change
|
||||
*
|
||||
* UI interaction flow tested:
|
||||
* 1. Click on TV Show item
|
||||
* 2. Rotate device
|
||||
* 3. Result: action bar title should show TV show item title
|
||||
*/
|
||||
@Test
|
||||
public void restoreActionBarTitleOnConfigurationStateChanged() {
|
||||
EspressoTestUtils.selectListItemRotateDeviceAndCheckActionbarTitle(TV_SHOW_TITLE, R.id.list,
|
||||
TV_SHOW_TITLE,
|
||||
mActivityRule.getActivity());
|
||||
}
|
||||
|
||||
/**
|
||||
* Test if action bar title is correctly restored on season list after a configuration change
|
||||
*
|
||||
* UI interaction flow tested:
|
||||
* 1. Click on TV Show item
|
||||
* 2. Click on season item
|
||||
* 3. Rotate device
|
||||
* 4. Result: action bar title should show season title
|
||||
*/
|
||||
@Test
|
||||
public void restoreActionBarTitleSeasonListOnConfigurationStateChanged() {
|
||||
clickRecyclerViewItem(0, R.id.list);
|
||||
onView( withId(R.id.seasons_list)).perform( nestedScrollTo(), click());
|
||||
EspressoTestUtils.rotateDevice(mActivityRule.getActivity());
|
||||
|
||||
onView(allOf(instanceOf(TextView.class), withParent(withId(R.id.default_toolbar))))
|
||||
.check(matches(withText("Season 01")));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test if action bar title is correctly restored on episode item title after a configuration change
|
||||
*
|
||||
* UI interaction flow tested:
|
||||
* 1. Click on TV Show item
|
||||
* 2. Click on season item
|
||||
* 3. Click on episode item
|
||||
* 4. Rotate device
|
||||
* 5. Result: action bar title should TV show title
|
||||
*/
|
||||
@Test
|
||||
public void restoreActionBarTitleSeasonListEpisodeOnConfigurationStateChanged() {
|
||||
clickRecyclerViewItem(0, R.id.list);
|
||||
onView( withId(R.id.seasons_list)).perform( nestedScrollTo(), click());
|
||||
EspressoTestUtils.selectListItemRotateDeviceAndCheckActionbarTitle(EPISODE_TITLE, R.id.list,
|
||||
TV_SHOW_TITLE,
|
||||
mActivityRule.getActivity());
|
||||
}
|
||||
|
||||
/**
|
||||
* Test if action bar title is correctly restored on next episode item title after a configuration change
|
||||
*
|
||||
* UI interaction flow tested:
|
||||
* 1. Click on TV Show item
|
||||
* 2. Click on next episode item
|
||||
* 3. Rotate device
|
||||
* 4. Result: action bar title should show season title
|
||||
*/
|
||||
@Test
|
||||
public void restoreActionBarTitleNextEpisodeOnConfigurationStateChanged() {
|
||||
clickRecyclerViewItem(1, R.id.list);
|
||||
onView( withId(R.id.next_episode_list)).perform( nestedScrollTo() );
|
||||
onView( withText("You'll See the Sparkle")).perform( click() );
|
||||
EspressoTestUtils.rotateDevice(mActivityRule.getActivity());
|
||||
|
||||
onView(allOf(instanceOf(TextView.class), withParent(withId(R.id.default_toolbar))))
|
||||
.check(matches(withText("3")));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test if the initial state shows the hamburger icon
|
||||
*/
|
||||
@Test
|
||||
public void showHamburgerInInitialState() {
|
||||
assertFalse(getActivity().getDrawerIndicatorIsArrow());
|
||||
}
|
||||
|
||||
/**
|
||||
* Test if navigation icon is changed to an arrow when selecting a list item
|
||||
*
|
||||
* UI interaction flow tested:
|
||||
* 1. Click on list item
|
||||
* 2. Result: navigation icon should be an arrow
|
||||
*/
|
||||
@Test
|
||||
public void showArrowWhenSelectingListItem() {
|
||||
clickRecyclerViewItem(0, R.id.list);
|
||||
|
||||
assertTrue(getActivity().getDrawerIndicatorIsArrow());
|
||||
}
|
||||
|
||||
/**
|
||||
* Test if navigation icon is changed to an arrow when selecting a list item
|
||||
*
|
||||
* UI interaction flow tested:
|
||||
* 1. Click on list item
|
||||
* 2. Press back
|
||||
* 3. Result: navigation icon should be a hamburger
|
||||
*/
|
||||
@Test
|
||||
public void showHamburgerWhenSelectingListItemAndReturn() {
|
||||
clickRecyclerViewItem(0, R.id.list);
|
||||
Espresso.pressBack();
|
||||
|
||||
assertFalse(getActivity().getDrawerIndicatorIsArrow());
|
||||
}
|
||||
|
||||
/**
|
||||
* Test if navigation icon is restored to an arrow when selecting a list item
|
||||
* and rotating the device
|
||||
*
|
||||
* UI interaction flow tested:
|
||||
* 1. Click on list item
|
||||
* 2. Rotate device
|
||||
* 3. Result: navigation icon should be an arrow
|
||||
*/
|
||||
@Test
|
||||
public void restoreArrowOnConfigurationChange() {
|
||||
clickRecyclerViewItem(0, R.id.list);
|
||||
rotateDevice(getActivity());
|
||||
|
||||
assertTrue(getActivity().getDrawerIndicatorIsArrow());
|
||||
}
|
||||
|
||||
/**
|
||||
* Test if navigation icon is restored to an hamburger when selecting a list item
|
||||
* and rotating the device and returning to the list
|
||||
*
|
||||
* UI interaction flow tested:
|
||||
* 1. Click on list item
|
||||
* 2. Rotate device
|
||||
* 3. Press back
|
||||
* 4. Result: navigation icon should be a hamburger
|
||||
*/
|
||||
@Test
|
||||
public void restoreHamburgerOnConfigurationChangeOnReturn() {
|
||||
clickRecyclerViewItem(0, R.id.list);
|
||||
rotateDevice(getActivity());
|
||||
Espresso.pressBack();
|
||||
|
||||
assertTrue(EspressoTestUtils.getActivity() instanceof TVShowsActivity);
|
||||
assertFalse(((TVShowsActivity) EspressoTestUtils.getActivity()).getDrawerIndicatorIsArrow());
|
||||
}
|
||||
}
|
||||
11
app/src/debug/AndroidManifest.xml
Normal file
11
app/src/debug/AndroidManifest.xml
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<!-- For espresso testing purposes, this is removed in live builds, but not in dev builds -->
|
||||
<uses-permission android:name="android.permission.SET_ANIMATION_SCALE"
|
||||
tools:ignore="ProtectedPermissions" />
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||
|
||||
</manifest>
|
||||
5
app/src/debug/README.md
Normal file
5
app/src/debug/README.md
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
Resources required for both the local and instrumentation tests.
|
||||
|
||||
**Note**: do not put any tests here! Put local tests
|
||||
that DO NOT need to be executed on an android device in [test](../test).
|
||||
Put tests that DO need to run on an android device in [androidTest](../androidTest).
|
||||
1244
app/src/debug/assets/Addons.GetAddons.json
Normal file
1244
app/src/debug/assets/Addons.GetAddons.json
Normal file
File diff suppressed because it is too large
Load diff
7666
app/src/debug/assets/AudioLibrary.GetAlbums.json
Normal file
7666
app/src/debug/assets/AudioLibrary.GetAlbums.json
Normal file
File diff suppressed because it is too large
Load diff
4593
app/src/debug/assets/AudioLibrary.GetArtists.json
Normal file
4593
app/src/debug/assets/AudioLibrary.GetArtists.json
Normal file
File diff suppressed because it is too large
Load diff
247
app/src/debug/assets/AudioLibrary.GetGenres.json
Normal file
247
app/src/debug/assets/AudioLibrary.GetGenres.json
Normal file
|
|
@ -0,0 +1,247 @@
|
|||
{
|
||||
"id" : "libGenres",
|
||||
"result" : {
|
||||
"limits" : {
|
||||
"total" : 39,
|
||||
"start" : 0,
|
||||
"end" : 39
|
||||
},
|
||||
"genres" : [
|
||||
{
|
||||
"genreid" : 9,
|
||||
"title" : "Ambient",
|
||||
"label" : "Ambient",
|
||||
"thumbnail" : ""
|
||||
},
|
||||
{
|
||||
"label" : "Bluegrass",
|
||||
"thumbnail" : "",
|
||||
"genreid" : 33,
|
||||
"title" : "Bluegrass"
|
||||
},
|
||||
{
|
||||
"title" : "Blues",
|
||||
"genreid" : 1,
|
||||
"thumbnail" : "",
|
||||
"label" : "Blues"
|
||||
},
|
||||
{
|
||||
"label" : "Blues Compilation",
|
||||
"thumbnail" : "",
|
||||
"genreid" : 35,
|
||||
"title" : "Blues Compilation"
|
||||
},
|
||||
{
|
||||
"thumbnail" : "",
|
||||
"label" : "Brutal Death Metal",
|
||||
"title" : "Brutal Death Metal",
|
||||
"genreid" : 32
|
||||
},
|
||||
{
|
||||
"title" : "Celtic",
|
||||
"genreid" : 24,
|
||||
"thumbnail" : "",
|
||||
"label" : "Celtic"
|
||||
},
|
||||
{
|
||||
"genreid" : 11,
|
||||
"title" : "Classic for Kids",
|
||||
"label" : "Classic for Kids",
|
||||
"thumbnail" : ""
|
||||
},
|
||||
{
|
||||
"thumbnail" : "",
|
||||
"label" : "Classical",
|
||||
"title" : "Classical",
|
||||
"genreid" : 3
|
||||
},
|
||||
{
|
||||
"genreid" : 13,
|
||||
"title" : "Country",
|
||||
"label" : "Country",
|
||||
"thumbnail" : ""
|
||||
},
|
||||
{
|
||||
"title" : "Dancehall",
|
||||
"genreid" : 31,
|
||||
"thumbnail" : "",
|
||||
"label" : "Dancehall"
|
||||
},
|
||||
{
|
||||
"title" : "Easy Listening",
|
||||
"genreid" : 20,
|
||||
"thumbnail" : "",
|
||||
"label" : "Easy Listening"
|
||||
},
|
||||
{
|
||||
"title" : "Folk",
|
||||
"genreid" : 4,
|
||||
"thumbnail" : "",
|
||||
"label" : "Folk"
|
||||
},
|
||||
{
|
||||
"label" : "Folklore",
|
||||
"thumbnail" : "",
|
||||
"genreid" : 22,
|
||||
"title" : "Folklore"
|
||||
},
|
||||
{
|
||||
"label" : "Goregrind",
|
||||
"thumbnail" : "",
|
||||
"genreid" : 14,
|
||||
"title" : "Goregrind"
|
||||
},
|
||||
{
|
||||
"genreid" : 8,
|
||||
"title" : "Grind Core",
|
||||
"label" : "Grind Core",
|
||||
"thumbnail" : ""
|
||||
},
|
||||
{
|
||||
"title" : "Grindcore",
|
||||
"genreid" : 18,
|
||||
"thumbnail" : "",
|
||||
"label" : "Grindcore"
|
||||
},
|
||||
{
|
||||
"label" : "Hardcore",
|
||||
"thumbnail" : "",
|
||||
"genreid" : 12,
|
||||
"title" : "Hardcore"
|
||||
},
|
||||
{
|
||||
"label" : "Hardcore Thrash",
|
||||
"thumbnail" : "",
|
||||
"genreid" : 30,
|
||||
"title" : "Hardcore Thrash"
|
||||
},
|
||||
{
|
||||
"title" : "Hip-Hop",
|
||||
"genreid" : 17,
|
||||
"thumbnail" : "",
|
||||
"label" : "Hip-Hop"
|
||||
},
|
||||
{
|
||||
"title" : "Instrumental",
|
||||
"genreid" : 39,
|
||||
"thumbnail" : "",
|
||||
"label" : "Instrumental"
|
||||
},
|
||||
{
|
||||
"genreid" : 5,
|
||||
"title" : "Jazz",
|
||||
"label" : "Jazz",
|
||||
"thumbnail" : ""
|
||||
},
|
||||
{
|
||||
"label" : "Jazz+Funk",
|
||||
"thumbnail" : "",
|
||||
"genreid" : 21,
|
||||
"title" : "Jazz+Funk"
|
||||
},
|
||||
{
|
||||
"thumbnail" : "",
|
||||
"label" : "Metal",
|
||||
"title" : "Metal",
|
||||
"genreid" : 10
|
||||
},
|
||||
{
|
||||
"label" : "Oldies",
|
||||
"thumbnail" : "",
|
||||
"genreid" : 36,
|
||||
"title" : "Oldies"
|
||||
},
|
||||
{
|
||||
"genreid" : 23,
|
||||
"title" : "Other",
|
||||
"label" : "Other",
|
||||
"thumbnail" : ""
|
||||
},
|
||||
{
|
||||
"title" : "Pop",
|
||||
"genreid" : 25,
|
||||
"thumbnail" : "",
|
||||
"label" : "Pop"
|
||||
},
|
||||
{
|
||||
"thumbnail" : "",
|
||||
"label" : "porno grind",
|
||||
"title" : "porno grind",
|
||||
"genreid" : 26
|
||||
},
|
||||
{
|
||||
"title" : "Punk",
|
||||
"genreid" : 7,
|
||||
"thumbnail" : "",
|
||||
"label" : "Punk"
|
||||
},
|
||||
{
|
||||
"label" : "Punk Rock",
|
||||
"thumbnail" : "",
|
||||
"genreid" : 16,
|
||||
"title" : "Punk Rock"
|
||||
},
|
||||
{
|
||||
"title" : "Rap",
|
||||
"genreid" : 28,
|
||||
"thumbnail" : "",
|
||||
"label" : "Rap"
|
||||
},
|
||||
{
|
||||
"title" : "Reggae",
|
||||
"genreid" : 2,
|
||||
"thumbnail" : "",
|
||||
"label" : "Reggae"
|
||||
},
|
||||
{
|
||||
"title" : "Rhythm and Blues",
|
||||
"genreid" : 19,
|
||||
"thumbnail" : "",
|
||||
"label" : "Rhythm and Blues"
|
||||
},
|
||||
{
|
||||
"title" : "Rock",
|
||||
"genreid" : 34,
|
||||
"thumbnail" : "",
|
||||
"label" : "Rock"
|
||||
},
|
||||
{
|
||||
"genreid" : 15,
|
||||
"title" : "Salsa",
|
||||
"label" : "Salsa",
|
||||
"thumbnail" : ""
|
||||
},
|
||||
{
|
||||
"thumbnail" : "",
|
||||
"label" : "Soul",
|
||||
"title" : "Soul",
|
||||
"genreid" : 29
|
||||
},
|
||||
{
|
||||
"thumbnail" : "",
|
||||
"label" : "Soundtrack",
|
||||
"title" : "Soundtrack",
|
||||
"genreid" : 6
|
||||
},
|
||||
{
|
||||
"thumbnail" : "",
|
||||
"label" : "Sprachkurs",
|
||||
"title" : "Sprachkurs",
|
||||
"genreid" : 27
|
||||
},
|
||||
{
|
||||
"title" : "Κινηματογραφική",
|
||||
"genreid" : 37,
|
||||
"thumbnail" : "",
|
||||
"label" : "Κινηματογραφική"
|
||||
},
|
||||
{
|
||||
"title" : "ゲーム音楽",
|
||||
"genreid" : 38,
|
||||
"thumbnail" : "",
|
||||
"label" : "ゲーム音楽"
|
||||
}
|
||||
]
|
||||
},
|
||||
"jsonrpc" : "2.0"
|
||||
}
|
||||
75661
app/src/debug/assets/AudioLibrary.GetSongs.json
Normal file
75661
app/src/debug/assets/AudioLibrary.GetSongs.json
Normal file
File diff suppressed because it is too large
Load diff
27526
app/src/debug/assets/Video.Details.Movie.json
Normal file
27526
app/src/debug/assets/Video.Details.Movie.json
Normal file
File diff suppressed because it is too large
Load diff
50320
app/src/debug/assets/VideoLibrary.GetEpisodes.json
Normal file
50320
app/src/debug/assets/VideoLibrary.GetEpisodes.json
Normal file
File diff suppressed because it is too large
Load diff
581
app/src/debug/assets/VideoLibrary.GetMusicVideos.json
Normal file
581
app/src/debug/assets/VideoLibrary.GetMusicVideos.json
Normal file
|
|
@ -0,0 +1,581 @@
|
|||
{
|
||||
"id" : "libMovies",
|
||||
"jsonrpc" : "2.0",
|
||||
"result" : {
|
||||
"musicvideos" : [
|
||||
{
|
||||
"album" : "...Baby One More Time",
|
||||
"director" : [
|
||||
"Nigel Dick"
|
||||
],
|
||||
"art" : {
|
||||
"poster" : "image://http%3a%2f%2fwww.theaudiodb.com%2fimages%2fmedia%2falbum%2fthumb%2fbaby-one-more-time-4dcff7453745a.jpg/"
|
||||
},
|
||||
"plot" : "\"(You Drive Me) Crazy\" is a song by American recording artist Britney Spears from her debut studio album, ...Baby One More Time (1999). Written and produced by Max Martin, Per Magnusson and David Kreuger, with additional writing by Jörgen Elofsson and remix by Martin and Rami Yacoub, it was released as the album's third single on August 23, 1999 by Jive Records. It was remixed for the soundtrack of Drive Me Crazy. \"(You Drive Me) Crazy\" is a teen pop song that draws influences from R&B and rock. The song garnered positive reviews from music critics, some of whom praised its simple formula and noted similarities to Spears' debut single, \"...Baby One More Time\".\n\n\"(You Drive Me) Crazy\" was a commercial success, and peaked inside the top ten on the singles charts of seventeen countries. In the United Kingdom, it became Spears' third consecutive single to peak inside the top five, while it reached number 10 in the United States' Hot 100, and peaked at number one in Belgium (Wallonia). An accompanying music video, directed by Nigel Dick, and portrayed Spears as a waitress of a dance club, and performed a highly choreographed dance routine with the other waitresses. The video premiered on MTV's Making the Video special, and featured cameo appearances of actors Melissa Joan Hart and Adrien Grenier. As part of promotion for the song, Spears performed the song at the 1999 MTV Europe Music Awards and 1999 Billboard Music Awards. It has also been included on five of her concert tours.",
|
||||
"resume" : {
|
||||
"position" : 0,
|
||||
"total" : 0
|
||||
},
|
||||
"thumbnail" : "image://http%3a%2f%2fwww.theaudiodb.com%2fimages%2fmedia%2falbum%2fthumb%2fbaby-one-more-time-4dcff7453745a.jpg/",
|
||||
"fanart" : "",
|
||||
"rating" : 0,
|
||||
"year" : 1999,
|
||||
"file" : "/Users/martijn/Projects/dummymediafiles/media/musicvideos/Britney Spears - (You Drive Me) Crazy (1999).mp4",
|
||||
"dateadded" : "2016-12-29 16:50:28",
|
||||
"userrating" : 0,
|
||||
"artist" : [
|
||||
"Britney Spears"
|
||||
],
|
||||
"lastplayed" : "2017-02-28 11:07:15",
|
||||
"studio" : [],
|
||||
"tag" : [],
|
||||
"title" : "(You Drive Me) Crazy",
|
||||
"label" : "(You Drive Me) Crazy",
|
||||
"runtime" : 12,
|
||||
"track" : -1,
|
||||
"genre" : [
|
||||
"Pop"
|
||||
],
|
||||
"streamdetails" : {
|
||||
"audio" : [
|
||||
{
|
||||
"channels" : 2,
|
||||
"language" : "und",
|
||||
"codec" : "aac"
|
||||
}
|
||||
],
|
||||
"video" : [
|
||||
{
|
||||
"width" : 480,
|
||||
"language" : "und",
|
||||
"height" : 360,
|
||||
"duration" : 12,
|
||||
"stereomode" : "",
|
||||
"aspect" : 1.33333301544189,
|
||||
"codec" : "h264"
|
||||
}
|
||||
],
|
||||
"subtitle" : []
|
||||
},
|
||||
"musicvideoid" : 60,
|
||||
"premiered" : "1999-01-01",
|
||||
"playcount" : 1
|
||||
},
|
||||
{
|
||||
"resume" : {
|
||||
"total" : 0,
|
||||
"position" : 0
|
||||
},
|
||||
"art" : {
|
||||
"poster" : "image://http%3a%2f%2fwww.theaudiodb.com%2fimages%2fmedia%2falbum%2fthumb%2fsvpxyw1364737910.jpg/"
|
||||
},
|
||||
"director" : [],
|
||||
"plot" : "",
|
||||
"album" : "Rubber Factory",
|
||||
"file" : "/Users/martijn/Projects/dummymediafiles/media/musicvideos/The Black Keys - 10 A.M. Automatic (2004).mp4",
|
||||
"rating" : 0,
|
||||
"year" : 2004,
|
||||
"fanart" : "",
|
||||
"thumbnail" : "image://http%3a%2f%2fwww.theaudiodb.com%2fimages%2fmedia%2falbum%2fthumb%2fsvpxyw1364737910.jpg/",
|
||||
"track" : -1,
|
||||
"label" : "10 A.M. Automatic",
|
||||
"runtime" : 12,
|
||||
"lastplayed" : "",
|
||||
"studio" : [],
|
||||
"title" : "10 A.M. Automatic",
|
||||
"tag" : [],
|
||||
"dateadded" : "2016-12-29 16:50:29",
|
||||
"artist" : [
|
||||
"The Black Keys"
|
||||
],
|
||||
"userrating" : 0,
|
||||
"premiered" : "2004-01-01",
|
||||
"playcount" : 0,
|
||||
"musicvideoid" : 370,
|
||||
"streamdetails" : {
|
||||
"subtitle" : [],
|
||||
"audio" : [
|
||||
{
|
||||
"channels" : 2,
|
||||
"language" : "und",
|
||||
"codec" : "aac"
|
||||
}
|
||||
],
|
||||
"video" : [
|
||||
{
|
||||
"language" : "",
|
||||
"height" : 360,
|
||||
"width" : 480,
|
||||
"duration" : 12,
|
||||
"stereomode" : "",
|
||||
"aspect" : 1.33333301544189,
|
||||
"codec" : "avc1"
|
||||
}
|
||||
]
|
||||
},
|
||||
"genre" : [
|
||||
"Indie"
|
||||
]
|
||||
},
|
||||
{
|
||||
"runtime" : 12,
|
||||
"label" : "99 Problems",
|
||||
"track" : -1,
|
||||
"userrating" : 0,
|
||||
"artist" : [
|
||||
"Jay-Z"
|
||||
],
|
||||
"dateadded" : "2016-12-29 16:50:28",
|
||||
"title" : "99 Problems",
|
||||
"tag" : [],
|
||||
"studio" : [
|
||||
"Anonymous Content"
|
||||
],
|
||||
"lastplayed" : "",
|
||||
"musicvideoid" : 164,
|
||||
"streamdetails" : {
|
||||
"audio" : [
|
||||
{
|
||||
"codec" : "aac",
|
||||
"channels" : 2,
|
||||
"language" : "und"
|
||||
}
|
||||
],
|
||||
"video" : [
|
||||
{
|
||||
"aspect" : 1.33333301544189,
|
||||
"stereomode" : "",
|
||||
"codec" : "avc1",
|
||||
"duration" : 12,
|
||||
"width" : 480,
|
||||
"language" : "",
|
||||
"height" : 360
|
||||
}
|
||||
],
|
||||
"subtitle" : []
|
||||
},
|
||||
"premiered" : "2003-01-01",
|
||||
"playcount" : 0,
|
||||
"genre" : [
|
||||
"Hip-Hop"
|
||||
],
|
||||
"director" : [
|
||||
"Mark Romanek"
|
||||
],
|
||||
"art" : {
|
||||
"poster" : "image://http%3a%2f%2fwww.theaudiodb.com%2fimages%2fmedia%2falbum%2fthumb%2fthe-black-album-4ee5475d9f478.jpg/"
|
||||
},
|
||||
"plot" : "\"99 Problems\" is the third single released by American rapper Jay-Z in 2004 from The Black Album. The song was originally written by rapper Ice-T in 1993. Throughout the song Jay-Z tells a story about dealing with a racist police officer who wants to illegally search his car, dealing with rap critics, and dealing with an aggressor. The song reached number 30 on the Billboard Hot 100.\nThe track was produced by Rick Rubin, his first hip hop production in many years. Rubin provided Jay-Z with a guitar riff and stripped-down beat that were once his trademarks. In creating the track Rubin used some classic 1980s sample staples such as \"The Big Beat\" by Billy Squier, \"Long Red\" by Mountain, and \"Get Me Back On Time\" by Wilson Pickett. These songs were long coveted by early hip hop producers, in particular the drum beat from Big Beat, used most famously by Run–D.M.C. on \"Here We Go\" in 1985 and by British rapper Dizzee Rascal a year prior to Jay-Z on his break-through hit \"Fix Up, Look Sharp\". It also featured on the popular Ultimate Breaks and Beats series.\nWhile the song's meaning is widely debated, the chorus \"If you're having girl problems, I feel bad for you son/I've got 99 problems but a bitch ain't one\" was defined in Jay-Z's book, Decoded, as referring to a police dog. Jay-Z wrote that in 1994 he was pulled over by police while carrying cocaine in a secret compartment in his sunroof. Jay-Z refused to let the police search the car and the police called for the drug sniffing dogs. However, the dogs never showed up and the police had to let Jay-Z go. Moments after he drove away, he wrote that he saw a police car with the dogs drive by.\nThe title and chorus are taken from Ice-T's \"99 Problems\" from his 1993 album Home Invasion. The song featured Brother Marquis of 2 Live Crew. The original song was more profane and describes a wide range of sexual conquests. Portions of Ice-T's original lyrics were similarly quoted in a song by fellow rapper Trick Daddy on a track also titled \"99 Problems\" from his 2001 album Thugs Are Us. Jay-Z begins his third verse directly quoting lines from Bun B's opening verse off the track \"Touched\" from the UGK album Ridin' Dirty.",
|
||||
"resume" : {
|
||||
"position" : 0,
|
||||
"total" : 0
|
||||
},
|
||||
"album" : "The Black Album",
|
||||
"year" : 2003,
|
||||
"rating" : 0,
|
||||
"file" : "/Users/martijn/Projects/dummymediafiles/media/musicvideos/Jay-Z - 99 Problems (2004).mp4",
|
||||
"thumbnail" : "image://http%3a%2f%2fwww.theaudiodb.com%2fimages%2fmedia%2falbum%2fthumb%2fthe-black-album-4ee5475d9f478.jpg/",
|
||||
"fanart" : ""
|
||||
},
|
||||
{
|
||||
"album" : "Dirty",
|
||||
"resume" : {
|
||||
"total" : 0,
|
||||
"position" : 0
|
||||
},
|
||||
"plot" : "",
|
||||
"art" : {
|
||||
"poster" : "image://http%3a%2f%2fwww.theaudiodb.com%2fimages%2fmedia%2falbum%2fthumb%2fdirty-50030abf884f2.jpg/"
|
||||
},
|
||||
"director" : [
|
||||
"Tamra Davis"
|
||||
],
|
||||
"fanart" : "",
|
||||
"thumbnail" : "image://http%3a%2f%2fwww.theaudiodb.com%2fimages%2fmedia%2falbum%2fthumb%2fdirty-50030abf884f2.jpg/",
|
||||
"file" : "/Users/martijn/Projects/dummymediafiles/media/musicvideos/Sonic Youth - 100% (1992).mp4",
|
||||
"rating" : 0,
|
||||
"year" : 1992,
|
||||
"lastplayed" : "",
|
||||
"studio" : [],
|
||||
"tag" : [],
|
||||
"title" : "100%",
|
||||
"dateadded" : "2016-12-29 16:50:28",
|
||||
"userrating" : 0,
|
||||
"artist" : [
|
||||
"Sonic Youth"
|
||||
],
|
||||
"track" : -1,
|
||||
"label" : "100%",
|
||||
"runtime" : 12,
|
||||
"genre" : [
|
||||
"Alternative Rock"
|
||||
],
|
||||
"premiered" : "1992-01-01",
|
||||
"playcount" : 0,
|
||||
"streamdetails" : {
|
||||
"audio" : [
|
||||
{
|
||||
"language" : "und",
|
||||
"channels" : 2,
|
||||
"codec" : "aac"
|
||||
}
|
||||
],
|
||||
"video" : [
|
||||
{
|
||||
"stereomode" : "",
|
||||
"aspect" : 1.33333301544189,
|
||||
"codec" : "avc1",
|
||||
"width" : 480,
|
||||
"language" : "",
|
||||
"height" : 360,
|
||||
"duration" : 12
|
||||
}
|
||||
],
|
||||
"subtitle" : []
|
||||
},
|
||||
"musicvideoid" : 349
|
||||
},
|
||||
{
|
||||
"resume" : {
|
||||
"position" : 0,
|
||||
"total" : 0
|
||||
},
|
||||
"art" : {
|
||||
"thumb" : "image://video@%2fUsers%2fmartijn%2fProjects%2fdummymediafiles%2fmedia%2fmusicvideos%2fPeter%20Himmelman%20-%20245%20Days%20(1990).mp4/"
|
||||
},
|
||||
"director" : [],
|
||||
"plot" : "",
|
||||
"album" : "Synesthesia",
|
||||
"file" : "/Users/martijn/Projects/dummymediafiles/media/musicvideos/Peter Himmelman - 245 Days (1990).mp4",
|
||||
"year" : 1989,
|
||||
"rating" : 0,
|
||||
"fanart" : "",
|
||||
"thumbnail" : "image://video@%2fUsers%2fmartijn%2fProjects%2fdummymediafiles%2fmedia%2fmusicvideos%2fPeter%20Himmelman%20-%20245%20Days%20(1990).mp4/",
|
||||
"track" : -1,
|
||||
"runtime" : 12,
|
||||
"label" : "245 Days",
|
||||
"tag" : [],
|
||||
"title" : "245 Days",
|
||||
"lastplayed" : "",
|
||||
"studio" : [],
|
||||
"artist" : [
|
||||
"Peter Himmelman"
|
||||
],
|
||||
"userrating" : 0,
|
||||
"dateadded" : "2016-12-29 16:50:28",
|
||||
"premiered" : "1989-01-01",
|
||||
"playcount" : 0,
|
||||
"streamdetails" : {
|
||||
"audio" : [
|
||||
{
|
||||
"language" : "und",
|
||||
"channels" : 2,
|
||||
"codec" : "aac"
|
||||
}
|
||||
],
|
||||
"video" : [
|
||||
{
|
||||
"stereomode" : "",
|
||||
"codec" : "avc1",
|
||||
"aspect" : 1.33333301544189,
|
||||
"duration" : 12,
|
||||
"width" : 480,
|
||||
"language" : "",
|
||||
"height" : 360
|
||||
}
|
||||
],
|
||||
"subtitle" : []
|
||||
},
|
||||
"musicvideoid" : 297,
|
||||
"genre" : [
|
||||
"..."
|
||||
]
|
||||
},
|
||||
{
|
||||
"dateadded" : "2016-12-29 16:50:28",
|
||||
"artist" : [
|
||||
"Public Enemy"
|
||||
],
|
||||
"userrating" : 0,
|
||||
"studio" : [],
|
||||
"lastplayed" : "2017-03-02 10:43:26",
|
||||
"tag" : [],
|
||||
"title" : "911 Is a Joke",
|
||||
"label" : "911 Is a Joke",
|
||||
"runtime" : 12,
|
||||
"track" : -1,
|
||||
"genre" : [
|
||||
"Hip-Hop"
|
||||
],
|
||||
"streamdetails" : {
|
||||
"subtitle" : [],
|
||||
"audio" : [
|
||||
{
|
||||
"codec" : "aac",
|
||||
"language" : "und",
|
||||
"channels" : 2
|
||||
}
|
||||
],
|
||||
"video" : [
|
||||
{
|
||||
"codec" : "h264",
|
||||
"stereomode" : "",
|
||||
"aspect" : 1.33333301544189,
|
||||
"language" : "und",
|
||||
"height" : 360,
|
||||
"width" : 480,
|
||||
"duration" : 12
|
||||
}
|
||||
]
|
||||
},
|
||||
"musicvideoid" : 306,
|
||||
"premiered" : "1990-01-01",
|
||||
"playcount" : 1,
|
||||
"album" : "Fear of a Black Planet",
|
||||
"director" : [],
|
||||
"art" : {
|
||||
"poster" : "image://http%3a%2f%2fwww.theaudiodb.com%2fimages%2fmedia%2falbum%2fthumb%2ffear-of-a-black-planet-526965153a1e4.jpg/"
|
||||
},
|
||||
"plot" : "\"911 Is a Joke\" is a 1990 song by American hip hop group Public Enemy, from their third album, Fear of a Black Planet. The song is solely done by Flavor Flav. It was released as a single and became a hit in June 1990, reaching number 15 on the Hot R&B/Hip-Hop Singles & Tracks chart, and number 1 on the Hot Rap Singles chart, becoming their second number-one rap chart hit after \"Fight the Power\". It also reached number one on the Bubbling Under Hot 100 Singles chart. This was due largely to its sales, which were unusually high for the level of mainstream airplay it received; Billboard reported that only one of the stations on its Top 40 panel was playing it.\n\nThe song is about the lack of response to emergency calls in a black neighborhood, but specifically references the poor response by paramedic crews and not the police, which is a common misconception regarding the track; the \"911\" in the title of the song refers to 9-1-1, the emergency telephone number used in North America.",
|
||||
"resume" : {
|
||||
"total" : 0,
|
||||
"position" : 0
|
||||
},
|
||||
"thumbnail" : "image://http%3a%2f%2fwww.theaudiodb.com%2fimages%2fmedia%2falbum%2fthumb%2ffear-of-a-black-planet-526965153a1e4.jpg/",
|
||||
"fanart" : "",
|
||||
"rating" : 0,
|
||||
"year" : 1990,
|
||||
"file" : "/Users/martijn/Projects/dummymediafiles/media/musicvideos/Public Enemy - 911 is a Joke (1990).mp4"
|
||||
},
|
||||
{
|
||||
"label" : "A Case of You",
|
||||
"runtime" : 12,
|
||||
"track" : -1,
|
||||
"dateadded" : "2016-12-29 16:50:28",
|
||||
"userrating" : 0,
|
||||
"artist" : [
|
||||
"James Blake"
|
||||
],
|
||||
"lastplayed" : "",
|
||||
"studio" : [],
|
||||
"tag" : [],
|
||||
"title" : "A Case of You",
|
||||
"streamdetails" : {
|
||||
"audio" : [
|
||||
{
|
||||
"channels" : 2,
|
||||
"language" : "und",
|
||||
"codec" : "aac"
|
||||
}
|
||||
],
|
||||
"video" : [
|
||||
{
|
||||
"width" : 480,
|
||||
"height" : 360,
|
||||
"language" : "",
|
||||
"duration" : 12,
|
||||
"stereomode" : "",
|
||||
"aspect" : 1.33333301544189,
|
||||
"codec" : "avc1"
|
||||
}
|
||||
],
|
||||
"subtitle" : []
|
||||
},
|
||||
"musicvideoid" : 160,
|
||||
"premiered" : "2011-01-01",
|
||||
"playcount" : 0,
|
||||
"genre" : [
|
||||
"Electronic"
|
||||
],
|
||||
"director" : [],
|
||||
"plot" : "",
|
||||
"art" : {
|
||||
"poster" : "image://http%3a%2f%2fwww.theaudiodb.com%2fimages%2fmedia%2falbum%2fthumb%2ftyswrr1377556931.jpg/"
|
||||
},
|
||||
"resume" : {
|
||||
"total" : 0,
|
||||
"position" : 0
|
||||
},
|
||||
"album" : "Enough Thunder",
|
||||
"rating" : 0,
|
||||
"year" : 2011,
|
||||
"file" : "/Users/martijn/Projects/dummymediafiles/media/musicvideos/James Blake - A Case Of You (2011).mp4",
|
||||
"thumbnail" : "image://http%3a%2f%2fwww.theaudiodb.com%2fimages%2fmedia%2falbum%2fthumb%2ftyswrr1377556931.jpg/",
|
||||
"fanart" : ""
|
||||
},
|
||||
{
|
||||
"track" : -1,
|
||||
"label" : "A Little Respect",
|
||||
"runtime" : 12,
|
||||
"studio" : [],
|
||||
"lastplayed" : "",
|
||||
"tag" : [],
|
||||
"title" : "A Little Respect",
|
||||
"dateadded" : "2016-12-29 16:50:28",
|
||||
"artist" : [
|
||||
"Wheatus"
|
||||
],
|
||||
"userrating" : 0,
|
||||
"premiered" : "2000-01-01",
|
||||
"playcount" : 0,
|
||||
"musicvideoid" : 430,
|
||||
"streamdetails" : {
|
||||
"subtitle" : [],
|
||||
"audio" : [
|
||||
{
|
||||
"channels" : 2,
|
||||
"language" : "und",
|
||||
"codec" : "aac"
|
||||
}
|
||||
],
|
||||
"video" : [
|
||||
{
|
||||
"stereomode" : "",
|
||||
"codec" : "avc1",
|
||||
"aspect" : 1.33333301544189,
|
||||
"duration" : 12,
|
||||
"language" : "",
|
||||
"height" : 360,
|
||||
"width" : 480
|
||||
}
|
||||
]
|
||||
},
|
||||
"genre" : [
|
||||
"Rock"
|
||||
],
|
||||
"resume" : {
|
||||
"total" : 0,
|
||||
"position" : 0
|
||||
},
|
||||
"art" : {
|
||||
"poster" : "image://http%3a%2f%2fwww.theaudiodb.com%2fimages%2fmedia%2falbum%2fthumb%2fwheatus-5144f1c42ea0a.jpg/"
|
||||
},
|
||||
"plot" : "",
|
||||
"director" : [],
|
||||
"album" : "Wheatus",
|
||||
"file" : "/Users/martijn/Projects/dummymediafiles/media/musicvideos/Wheatus - A Little Respect (2000).mp4",
|
||||
"rating" : 0,
|
||||
"year" : 2000,
|
||||
"fanart" : "",
|
||||
"thumbnail" : "image://http%3a%2f%2fwww.theaudiodb.com%2fimages%2fmedia%2falbum%2fthumb%2fwheatus-5144f1c42ea0a.jpg/"
|
||||
},
|
||||
{
|
||||
"rating" : 0,
|
||||
"year" : 1996,
|
||||
"file" : "/Users/martijn/Projects/dummymediafiles/media/musicvideos/Counting Crows - A Long December (1996).mp4",
|
||||
"thumbnail" : "image://http%3a%2f%2fwww.theaudiodb.com%2fimages%2fmedia%2falbum%2fthumb%2frecovering-the-satellites-4dcfda1792ec0.jpg/",
|
||||
"fanart" : "",
|
||||
"plot" : "",
|
||||
"director" : [],
|
||||
"art" : {
|
||||
"poster" : "image://http%3a%2f%2fwww.theaudiodb.com%2fimages%2fmedia%2falbum%2fthumb%2frecovering-the-satellites-4dcfda1792ec0.jpg/"
|
||||
},
|
||||
"resume" : {
|
||||
"total" : 0,
|
||||
"position" : 0
|
||||
},
|
||||
"album" : "Recovering the Satellites",
|
||||
"musicvideoid" : 79,
|
||||
"streamdetails" : {
|
||||
"video" : [
|
||||
{
|
||||
"duration" : 12,
|
||||
"width" : 480,
|
||||
"height" : 360,
|
||||
"language" : "",
|
||||
"aspect" : 1.33333301544189,
|
||||
"stereomode" : "",
|
||||
"codec" : "avc1"
|
||||
}
|
||||
],
|
||||
"audio" : [
|
||||
{
|
||||
"language" : "und",
|
||||
"channels" : 2,
|
||||
"codec" : "aac"
|
||||
}
|
||||
],
|
||||
"subtitle" : []
|
||||
},
|
||||
"premiered" : "1996-01-01",
|
||||
"playcount" : 0,
|
||||
"genre" : [
|
||||
"Alternative Rock"
|
||||
],
|
||||
"label" : "A Long December",
|
||||
"runtime" : 12,
|
||||
"track" : -1,
|
||||
"dateadded" : "2016-12-29 16:50:28",
|
||||
"artist" : [
|
||||
"Counting Crows"
|
||||
],
|
||||
"userrating" : 0,
|
||||
"studio" : [],
|
||||
"lastplayed" : "",
|
||||
"tag" : [],
|
||||
"title" : "A Long December"
|
||||
},
|
||||
{
|
||||
"genre" : [
|
||||
"Folk"
|
||||
],
|
||||
"premiered" : "1985-01-01",
|
||||
"playcount" : 0,
|
||||
"streamdetails" : {
|
||||
"subtitle" : [],
|
||||
"audio" : [
|
||||
{
|
||||
"channels" : 2,
|
||||
"language" : "und",
|
||||
"codec" : "aac"
|
||||
}
|
||||
],
|
||||
"video" : [
|
||||
{
|
||||
"stereomode" : "",
|
||||
"aspect" : 1.33333301544189,
|
||||
"codec" : "avc1",
|
||||
"duration" : 12,
|
||||
"language" : "",
|
||||
"height" : 360,
|
||||
"width" : 480
|
||||
}
|
||||
]
|
||||
},
|
||||
"musicvideoid" : 390,
|
||||
"tag" : [],
|
||||
"title" : "A Pair of Brown Eyes",
|
||||
"lastplayed" : "",
|
||||
"studio" : [],
|
||||
"artist" : [
|
||||
"The Pogues"
|
||||
],
|
||||
"userrating" : 0,
|
||||
"dateadded" : "2016-12-29 16:50:28",
|
||||
"track" : -1,
|
||||
"runtime" : 12,
|
||||
"label" : "A Pair of Brown Eyes",
|
||||
"fanart" : "",
|
||||
"thumbnail" : "image://http%3a%2f%2fwww.theaudiodb.com%2fimages%2fmedia%2falbum%2fthumb%2frum-sodomy--the-lash-52f1dda495de3.jpg/",
|
||||
"file" : "/Users/martijn/Projects/dummymediafiles/media/musicvideos/The Pogues - A Pair of Brown Eyes (1985).mp4",
|
||||
"year" : 1985,
|
||||
"rating" : 0,
|
||||
"album" : "Rum Sodomy & the Lash",
|
||||
"resume" : {
|
||||
"position" : 0,
|
||||
"total" : 0
|
||||
},
|
||||
"art" : {
|
||||
"poster" : "image://http%3a%2f%2fwww.theaudiodb.com%2fimages%2fmedia%2falbum%2fthumb%2frum-sodomy--the-lash-52f1dda495de3.jpg/"
|
||||
},
|
||||
"director" : [],
|
||||
"plot" : ""
|
||||
}
|
||||
],
|
||||
"limits" : {
|
||||
"total" : 439,
|
||||
"start" : 0,
|
||||
"end" : 10
|
||||
}
|
||||
}
|
||||
}
|
||||
465
app/src/debug/assets/VideoLibrary.GetSeasons.json
Normal file
465
app/src/debug/assets/VideoLibrary.GetSeasons.json
Normal file
|
|
@ -0,0 +1,465 @@
|
|||
{
|
||||
"id" : "libTVShowSeasons",
|
||||
"jsonrpc" : "2.0",
|
||||
"result" : {
|
||||
"limits" : {
|
||||
"end" : 21,
|
||||
"total" : 21,
|
||||
"start" : 0
|
||||
},
|
||||
"seasons" : [
|
||||
{
|
||||
"playcount" : 0,
|
||||
"episode" : 8,
|
||||
"label" : "Season 1",
|
||||
"userrating" : 0,
|
||||
"thumbnail" : "",
|
||||
"watchedepisodes" : 3,
|
||||
"seasonid" : 6,
|
||||
"art" : {
|
||||
"tvshow.poster" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fposters%2f260473-1.jpg/",
|
||||
"tvshow.banner" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fgraphical%2f260473-g.jpg/"
|
||||
},
|
||||
"fanart" : "",
|
||||
"showtitle" : "3",
|
||||
"season" : 1,
|
||||
"tvshowid" : 2
|
||||
},
|
||||
{
|
||||
"userrating" : 0,
|
||||
"thumbnail" : "",
|
||||
"episode" : 49,
|
||||
"playcount" : 0,
|
||||
"label" : "Season 1",
|
||||
"seasonid" : 9,
|
||||
"watchedepisodes" : 2,
|
||||
"art" : {
|
||||
"tvshow.poster" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fposters%2f146391-2.jpg/",
|
||||
"tvshow.fanart" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2ffanart%2foriginal%2f146391-2.jpg/"
|
||||
},
|
||||
"fanart" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2ffanart%2foriginal%2f146391-2.jpg/",
|
||||
"season" : 1,
|
||||
"showtitle" : "4 Stjerners Middag",
|
||||
"tvshowid" : 3
|
||||
},
|
||||
{
|
||||
"episode" : 41,
|
||||
"playcount" : 0,
|
||||
"label" : "Season 2",
|
||||
"userrating" : 0,
|
||||
"thumbnail" : "",
|
||||
"art" : {
|
||||
"tvshow.fanart" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2ffanart%2foriginal%2f146391-2.jpg/",
|
||||
"tvshow.poster" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fposters%2f146391-2.jpg/"
|
||||
},
|
||||
"seasonid" : 10,
|
||||
"watchedepisodes" : 0,
|
||||
"fanart" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2ffanart%2foriginal%2f146391-2.jpg/",
|
||||
"tvshowid" : 3,
|
||||
"season" : 2,
|
||||
"showtitle" : "4 Stjerners Middag"
|
||||
},
|
||||
{
|
||||
"playcount" : 0,
|
||||
"episode" : 20,
|
||||
"label" : "Season 3",
|
||||
"userrating" : 0,
|
||||
"thumbnail" : "",
|
||||
"art" : {
|
||||
"tvshow.poster" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fposters%2f146391-2.jpg/",
|
||||
"tvshow.fanart" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2ffanart%2foriginal%2f146391-2.jpg/"
|
||||
},
|
||||
"watchedepisodes" : 0,
|
||||
"seasonid" : 11,
|
||||
"fanart" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2ffanart%2foriginal%2f146391-2.jpg/",
|
||||
"tvshowid" : 3,
|
||||
"showtitle" : "4 Stjerners Middag",
|
||||
"season" : 3
|
||||
},
|
||||
{
|
||||
"episode" : 8,
|
||||
"playcount" : 1,
|
||||
"label" : "Season 1",
|
||||
"thumbnail" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fseasons%2f301824-1-4.jpg/",
|
||||
"userrating" : 0,
|
||||
"art" : {
|
||||
"tvshow.banner" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fgraphical%2f301824-g.jpg/",
|
||||
"season.poster" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fseasons%2f301824-1-4.jpg/",
|
||||
"tvshow.poster" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fposters%2f301824-8.jpg/",
|
||||
"poster" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fseasons%2f301824-1-4.jpg/",
|
||||
"tvshow.fanart" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2ffanart%2foriginal%2f301824-10.jpg/"
|
||||
},
|
||||
"watchedepisodes" : 8,
|
||||
"seasonid" : 3,
|
||||
"fanart" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2ffanart%2foriginal%2f301824-10.jpg/",
|
||||
"tvshowid" : 1,
|
||||
"showtitle" : "11.22.63",
|
||||
"season" : 1
|
||||
},
|
||||
{
|
||||
"tvshowid" : 137,
|
||||
"showtitle" : "The A-Team",
|
||||
"season" : 1,
|
||||
"fanart" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2ffanart%2foriginal%2f77904-5.jpg/",
|
||||
"art" : {
|
||||
"banner" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fseasonswide%2f77904-1.jpg/",
|
||||
"season.poster" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fseasons%2f77904-1.jpg/",
|
||||
"tvshow.banner" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fgraphical%2f77904-g9.jpg/",
|
||||
"tvshow.poster" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fposters%2f77904-3.jpg/",
|
||||
"season.banner" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fseasonswide%2f77904-1.jpg/",
|
||||
"poster" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fseasons%2f77904-1.jpg/",
|
||||
"tvshow.fanart" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2ffanart%2foriginal%2f77904-5.jpg/"
|
||||
},
|
||||
"watchedepisodes" : 0,
|
||||
"seasonid" : 553,
|
||||
"thumbnail" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fseasons%2f77904-1.jpg/",
|
||||
"userrating" : 0,
|
||||
"label" : "Season 1",
|
||||
"playcount" : 0,
|
||||
"episode" : 13
|
||||
},
|
||||
{
|
||||
"userrating" : 0,
|
||||
"thumbnail" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fseasons%2f77904-2.jpg/",
|
||||
"playcount" : 0,
|
||||
"label" : "Season 2",
|
||||
"episode" : 23,
|
||||
"seasonid" : 554,
|
||||
"watchedepisodes" : 0,
|
||||
"art" : {
|
||||
"banner" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fseasonswide%2f77904-2.jpg/",
|
||||
"tvshow.banner" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fgraphical%2f77904-g9.jpg/",
|
||||
"season.poster" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fseasons%2f77904-2.jpg/",
|
||||
"tvshow.fanart" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2ffanart%2foriginal%2f77904-5.jpg/",
|
||||
"season.banner" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fseasonswide%2f77904-2.jpg/",
|
||||
"tvshow.poster" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fposters%2f77904-3.jpg/",
|
||||
"poster" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fseasons%2f77904-2.jpg/"
|
||||
},
|
||||
"fanart" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2ffanart%2foriginal%2f77904-5.jpg/",
|
||||
"showtitle" : "The A-Team",
|
||||
"season" : 2,
|
||||
"tvshowid" : 137
|
||||
},
|
||||
{
|
||||
"fanart" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2ffanart%2foriginal%2f77904-5.jpg/",
|
||||
"showtitle" : "The A-Team",
|
||||
"season" : 3,
|
||||
"tvshowid" : 137,
|
||||
"episode" : 25,
|
||||
"playcount" : 0,
|
||||
"label" : "Season 3",
|
||||
"thumbnail" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fseasons%2f77904-3.jpg/",
|
||||
"userrating" : 0,
|
||||
"seasonid" : 555,
|
||||
"watchedepisodes" : 0,
|
||||
"art" : {
|
||||
"poster" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fseasons%2f77904-3.jpg/",
|
||||
"tvshow.poster" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fposters%2f77904-3.jpg/",
|
||||
"season.banner" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fseasonswide%2f77904-3.jpg/",
|
||||
"tvshow.fanart" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2ffanart%2foriginal%2f77904-5.jpg/",
|
||||
"tvshow.banner" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fgraphical%2f77904-g9.jpg/",
|
||||
"season.poster" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fseasons%2f77904-3.jpg/",
|
||||
"banner" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fseasonswide%2f77904-3.jpg/"
|
||||
}
|
||||
},
|
||||
{
|
||||
"fanart" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2ffanart%2foriginal%2f77904-5.jpg/",
|
||||
"showtitle" : "The A-Team",
|
||||
"season" : 4,
|
||||
"tvshowid" : 137,
|
||||
"episode" : 23,
|
||||
"playcount" : 0,
|
||||
"label" : "Season 4",
|
||||
"userrating" : 0,
|
||||
"thumbnail" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fseasons%2f77904-4.jpg/",
|
||||
"watchedepisodes" : 0,
|
||||
"seasonid" : 556,
|
||||
"art" : {
|
||||
"tvshow.fanart" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2ffanart%2foriginal%2f77904-5.jpg/",
|
||||
"poster" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fseasons%2f77904-4.jpg/",
|
||||
"season.banner" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fseasonswide%2f77904-4.jpg/",
|
||||
"tvshow.poster" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fposters%2f77904-3.jpg/",
|
||||
"season.poster" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fseasons%2f77904-4.jpg/",
|
||||
"tvshow.banner" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fgraphical%2f77904-g9.jpg/",
|
||||
"banner" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fseasonswide%2f77904-4.jpg/"
|
||||
}
|
||||
},
|
||||
{
|
||||
"label" : "Season 5",
|
||||
"playcount" : 0,
|
||||
"episode" : 13,
|
||||
"thumbnail" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fseasons%2f77904-5.jpg/",
|
||||
"userrating" : 0,
|
||||
"seasonid" : 557,
|
||||
"watchedepisodes" : 0,
|
||||
"art" : {
|
||||
"tvshow.banner" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fgraphical%2f77904-g9.jpg/",
|
||||
"season.poster" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fseasons%2f77904-5.jpg/",
|
||||
"poster" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fseasons%2f77904-5.jpg/",
|
||||
"tvshow.poster" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fposters%2f77904-3.jpg/",
|
||||
"tvshow.fanart" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2ffanart%2foriginal%2f77904-5.jpg/"
|
||||
},
|
||||
"fanart" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2ffanart%2foriginal%2f77904-5.jpg/",
|
||||
"season" : 5,
|
||||
"showtitle" : "The A-Team",
|
||||
"tvshowid" : 137
|
||||
},
|
||||
{
|
||||
"watchedepisodes" : 9,
|
||||
"seasonid" : 14,
|
||||
"art" : {
|
||||
"tvshow.banner" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fgraphical%2f3449-g.jpg/",
|
||||
"season.poster" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fseasons%2f75926-1-2.jpg/",
|
||||
"tvshow.poster" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fposters%2f75926-1.jpg/",
|
||||
"poster" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fseasons%2f75926-1-2.jpg/",
|
||||
"tvshow.fanart" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2ffanart%2foriginal%2f75926-1.jpg/"
|
||||
},
|
||||
"userrating" : 0,
|
||||
"thumbnail" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fseasons%2f75926-1-2.jpg/",
|
||||
"playcount" : 0,
|
||||
"episode" : 22,
|
||||
"label" : "Season 1",
|
||||
"season" : 1,
|
||||
"showtitle" : "According to Jim",
|
||||
"tvshowid" : 4,
|
||||
"fanart" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2ffanart%2foriginal%2f75926-1.jpg/"
|
||||
},
|
||||
{
|
||||
"art" : {
|
||||
"tvshow.fanart" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2ffanart%2foriginal%2f75926-1.jpg/",
|
||||
"poster" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fseasons%2f75926-2-2.jpg/",
|
||||
"tvshow.poster" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fposters%2f75926-1.jpg/",
|
||||
"tvshow.banner" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fgraphical%2f3449-g.jpg/",
|
||||
"season.poster" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fseasons%2f75926-2-2.jpg/"
|
||||
},
|
||||
"seasonid" : 15,
|
||||
"watchedepisodes" : 0,
|
||||
"playcount" : 0,
|
||||
"label" : "Season 2",
|
||||
"episode" : 28,
|
||||
"userrating" : 0,
|
||||
"thumbnail" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fseasons%2f75926-2-2.jpg/",
|
||||
"tvshowid" : 4,
|
||||
"season" : 2,
|
||||
"showtitle" : "According to Jim",
|
||||
"fanart" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2ffanart%2foriginal%2f75926-1.jpg/"
|
||||
},
|
||||
{
|
||||
"userrating" : 0,
|
||||
"thumbnail" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fseasons%2f75926-3-2.jpg/",
|
||||
"playcount" : 0,
|
||||
"label" : "Season 3",
|
||||
"episode" : 29,
|
||||
"watchedepisodes" : 0,
|
||||
"seasonid" : 16,
|
||||
"art" : {
|
||||
"poster" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fseasons%2f75926-3-2.jpg/",
|
||||
"tvshow.poster" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fposters%2f75926-1.jpg/",
|
||||
"tvshow.fanart" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2ffanart%2foriginal%2f75926-1.jpg/",
|
||||
"season.poster" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fseasons%2f75926-3-2.jpg/",
|
||||
"tvshow.banner" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fgraphical%2f3449-g.jpg/"
|
||||
},
|
||||
"fanart" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2ffanart%2foriginal%2f75926-1.jpg/",
|
||||
"showtitle" : "According to Jim",
|
||||
"season" : 3,
|
||||
"tvshowid" : 4
|
||||
},
|
||||
{
|
||||
"fanart" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2ffanart%2foriginal%2f75926-1.jpg/",
|
||||
"season" : 4,
|
||||
"showtitle" : "According to Jim",
|
||||
"tvshowid" : 4,
|
||||
"episode" : 27,
|
||||
"playcount" : 0,
|
||||
"label" : "Season 4",
|
||||
"userrating" : 0,
|
||||
"thumbnail" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fseasons%2f75926-4-2.jpg/",
|
||||
"watchedepisodes" : 0,
|
||||
"seasonid" : 17,
|
||||
"art" : {
|
||||
"season.poster" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fseasons%2f75926-4-2.jpg/",
|
||||
"tvshow.banner" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fgraphical%2f3449-g.jpg/",
|
||||
"poster" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fseasons%2f75926-4-2.jpg/",
|
||||
"tvshow.poster" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fposters%2f75926-1.jpg/",
|
||||
"tvshow.fanart" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2ffanart%2foriginal%2f75926-1.jpg/"
|
||||
}
|
||||
},
|
||||
{
|
||||
"showtitle" : "According to Jim",
|
||||
"season" : 5,
|
||||
"tvshowid" : 4,
|
||||
"fanart" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2ffanart%2foriginal%2f75926-1.jpg/",
|
||||
"seasonid" : 18,
|
||||
"watchedepisodes" : 0,
|
||||
"art" : {
|
||||
"season.poster" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fseasons%2f3449-5.jpg/",
|
||||
"tvshow.banner" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fgraphical%2f3449-g.jpg/",
|
||||
"tvshow.fanart" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2ffanart%2foriginal%2f75926-1.jpg/",
|
||||
"poster" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fseasons%2f3449-5.jpg/",
|
||||
"tvshow.poster" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fposters%2f75926-1.jpg/"
|
||||
},
|
||||
"userrating" : 0,
|
||||
"thumbnail" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fseasons%2f3449-5.jpg/",
|
||||
"label" : "Season 5",
|
||||
"playcount" : 0,
|
||||
"episode" : 22
|
||||
},
|
||||
{
|
||||
"fanart" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2ffanart%2foriginal%2f75926-1.jpg/",
|
||||
"season" : 6,
|
||||
"showtitle" : "According to Jim",
|
||||
"tvshowid" : 4,
|
||||
"playcount" : 0,
|
||||
"episode" : 18,
|
||||
"label" : "Season 6",
|
||||
"userrating" : 0,
|
||||
"thumbnail" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fseasons%2f75926-6-2.jpg/",
|
||||
"seasonid" : 19,
|
||||
"watchedepisodes" : 0,
|
||||
"art" : {
|
||||
"tvshow.fanart" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2ffanart%2foriginal%2f75926-1.jpg/",
|
||||
"poster" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fseasons%2f75926-6-2.jpg/",
|
||||
"tvshow.poster" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fposters%2f75926-1.jpg/",
|
||||
"season.poster" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fseasons%2f75926-6-2.jpg/",
|
||||
"tvshow.banner" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fgraphical%2f3449-g.jpg/"
|
||||
}
|
||||
},
|
||||
{
|
||||
"fanart" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2ffanart%2foriginal%2f75926-1.jpg/",
|
||||
"tvshowid" : 4,
|
||||
"season" : 7,
|
||||
"showtitle" : "According to Jim",
|
||||
"episode" : 18,
|
||||
"playcount" : 0,
|
||||
"label" : "Season 7",
|
||||
"thumbnail" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fseasons%2f75926-7-2.jpg/",
|
||||
"userrating" : 0,
|
||||
"art" : {
|
||||
"tvshow.banner" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fgraphical%2f3449-g.jpg/",
|
||||
"season.poster" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fseasons%2f75926-7-2.jpg/",
|
||||
"tvshow.fanart" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2ffanart%2foriginal%2f75926-1.jpg/",
|
||||
"tvshow.poster" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fposters%2f75926-1.jpg/",
|
||||
"poster" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fseasons%2f75926-7-2.jpg/"
|
||||
},
|
||||
"watchedepisodes" : 0,
|
||||
"seasonid" : 20
|
||||
},
|
||||
{
|
||||
"watchedepisodes" : 0,
|
||||
"seasonid" : 21,
|
||||
"art" : {
|
||||
"tvshow.fanart" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2ffanart%2foriginal%2f75926-1.jpg/",
|
||||
"tvshow.poster" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fposters%2f75926-1.jpg/",
|
||||
"poster" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fseasons%2f75926-8-2.jpg/",
|
||||
"tvshow.banner" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fgraphical%2f3449-g.jpg/",
|
||||
"season.poster" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fseasons%2f75926-8-2.jpg/"
|
||||
},
|
||||
"playcount" : 0,
|
||||
"episode" : 18,
|
||||
"label" : "Season 8",
|
||||
"userrating" : 0,
|
||||
"thumbnail" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fseasons%2f75926-8-2.jpg/",
|
||||
"season" : 8,
|
||||
"showtitle" : "According to Jim",
|
||||
"tvshowid" : 4,
|
||||
"fanart" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2ffanart%2foriginal%2f75926-1.jpg/"
|
||||
},
|
||||
{
|
||||
"fanart" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2ffanart%2foriginal%2f252308-4.jpg/",
|
||||
"tvshowid" : 138,
|
||||
"season" : 1,
|
||||
"showtitle" : "The Adventures of Abney & Teal",
|
||||
"userrating" : 0,
|
||||
"thumbnail" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fseasons%2f252308-1.jpg/",
|
||||
"episode" : 26,
|
||||
"playcount" : 0,
|
||||
"label" : "Season 1",
|
||||
"art" : {
|
||||
"season.banner" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fseasonswide%2f252308-1.jpg/",
|
||||
"tvshow.poster" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fposters%2f252308-1.jpg/",
|
||||
"poster" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fseasons%2f252308-1.jpg/",
|
||||
"tvshow.fanart" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2ffanart%2foriginal%2f252308-4.jpg/",
|
||||
"banner" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fseasonswide%2f252308-1.jpg/",
|
||||
"season.poster" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fseasons%2f252308-1.jpg/",
|
||||
"tvshow.banner" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fgraphical%2f252308-g3.jpg/"
|
||||
},
|
||||
"watchedepisodes" : 0,
|
||||
"seasonid" : 560
|
||||
},
|
||||
{
|
||||
"season" : 2,
|
||||
"showtitle" : "The Adventures of Abney & Teal",
|
||||
"tvshowid" : 138,
|
||||
"fanart" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2ffanart%2foriginal%2f252308-4.jpg/",
|
||||
"seasonid" : 561,
|
||||
"watchedepisodes" : 0,
|
||||
"art" : {
|
||||
"poster" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fseasons%2f252308-2.jpg/",
|
||||
"season.banner" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fseasonswide%2f252308-2.jpg/",
|
||||
"tvshow.poster" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fposters%2f252308-1.jpg/",
|
||||
"tvshow.fanart" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2ffanart%2foriginal%2f252308-4.jpg/",
|
||||
"season.poster" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fseasons%2f252308-2.jpg/",
|
||||
"tvshow.banner" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fgraphical%2f252308-g3.jpg/",
|
||||
"banner" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fseasonswide%2f252308-2.jpg/"
|
||||
},
|
||||
"thumbnail" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fseasons%2f252308-2.jpg/",
|
||||
"userrating" : 0,
|
||||
"playcount" : 0,
|
||||
"label" : "Season 2",
|
||||
"episode" : 26
|
||||
},
|
||||
{
|
||||
"episode" : 1,
|
||||
"playcount" : 1,
|
||||
"label" : "Season 3",
|
||||
"userrating" : 0,
|
||||
"thumbnail" : "",
|
||||
"watchedepisodes" : 1,
|
||||
"seasonid" : 24,
|
||||
"art" : {
|
||||
"tvshow.fanart" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2ffanart%2foriginal%2f104171-1.jpg/",
|
||||
"tvshow.poster" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fposters%2f104171-1.jpg/",
|
||||
"tvshow.banner" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fgraphical%2f104171-g.jpg/"
|
||||
},
|
||||
"fanart" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2ffanart%2foriginal%2f104171-1.jpg/",
|
||||
"season" : 3,
|
||||
"showtitle" : "Air Ways",
|
||||
"tvshowid" : 5
|
||||
},
|
||||
{
|
||||
"art" : {
|
||||
"tvshow.banner" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fgraphical%2f259064-g.jpg/",
|
||||
"tvshow.poster" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fposters%2f259064-1.jpg/",
|
||||
"tvshow.fanart" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2ffanart%2foriginal%2f259064-1.jpg/"
|
||||
},
|
||||
"watchedepisodes" : 0,
|
||||
"seasonid" : 27,
|
||||
"thumbnail" : "",
|
||||
"userrating" : 0,
|
||||
"episode" : 10,
|
||||
"playcount" : 0,
|
||||
"label" : "Season 1",
|
||||
"tvshowid" : 6,
|
||||
"showtitle" : "American Colony Meet the Hutterites",
|
||||
"season" : 1,
|
||||
"fanart" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2ffanart%2foriginal%2f259064-1.jpg/"
|
||||
},
|
||||
{
|
||||
"tvshowid" : 7,
|
||||
"showtitle" : "Amish: Out Of Order",
|
||||
"season" : 1,
|
||||
"fanart" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2ffanart%2foriginal%2f258525-1.jpg/",
|
||||
"art" : {
|
||||
"tvshow.fanart" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2ffanart%2foriginal%2f258525-1.jpg/"
|
||||
},
|
||||
"seasonid" : 30,
|
||||
"watchedepisodes" : 0,
|
||||
"userrating" : 0,
|
||||
"thumbnail" : "",
|
||||
"episode" : 9,
|
||||
"playcount" : 0,
|
||||
"label" : "Season 1"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
691
app/src/debug/assets/VideoLibrary.GetTVShows.json
Normal file
691
app/src/debug/assets/VideoLibrary.GetTVShows.json
Normal file
|
|
@ -0,0 +1,691 @@
|
|||
{
|
||||
"id" : "libTVShows",
|
||||
"jsonrpc" : "2.0",
|
||||
"result" : {
|
||||
"tvshows" : [
|
||||
{
|
||||
"tvshowid" : 2,
|
||||
"sorttitle" : "",
|
||||
"season" : 1,
|
||||
"lastplayed" : "2017-02-06 16:56:12",
|
||||
"runtime" : 3600,
|
||||
"uniqueid" : {
|
||||
"unknown" : "260473"
|
||||
},
|
||||
"imdbnumber" : "260473",
|
||||
"art" : {
|
||||
"poster" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fposters%2f260473-1.jpg/",
|
||||
"banner" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fgraphical%2f260473-g.jpg/"
|
||||
},
|
||||
"originaltitle" : "",
|
||||
"year" : 2012,
|
||||
"title" : "3",
|
||||
"mpaa" : "",
|
||||
"userrating" : 0,
|
||||
"studio" : [
|
||||
"CBS"
|
||||
],
|
||||
"rating" : 10,
|
||||
"fanart" : "",
|
||||
"tag" : [],
|
||||
"premiered" : "2012-07-26",
|
||||
"file" : "/Users/martijn/Projects/dummymediafiles/media/tvshows/3 (2012)/",
|
||||
"cast" : [],
|
||||
"genre" : [
|
||||
"Reality"
|
||||
],
|
||||
"episodeguide" : "<episodeguide><url cache=\"260473-en.xml\">http://thetvdb.com/api/1D62F2F90030C444/series/260473/all/en.zip</url></episodeguide>",
|
||||
"watchedepisodes" : 3,
|
||||
"dateadded" : "2016-08-26 09:16:59",
|
||||
"label" : "3",
|
||||
"ratings" : {
|
||||
"default" : {
|
||||
"votes" : 0,
|
||||
"rating" : 10,
|
||||
"default" : true
|
||||
}
|
||||
},
|
||||
"episode" : 8,
|
||||
"plot" : "Instead of competing against each other, the women searching for love in this relationship series are there to share the experience with one another, offering emotional support during the dating and decision-making process as they whittle down the group of nearly 100 men they start out with and each tries to find a good match. The women -- 29-year-old entrepreneur April Francis, 34-year-old pharmaceutical sales rep Rachel Harley and 24-year-old model Libby Lopez -- bring different backgrounds and experiences to the table, but their common goal unites them as they embark on their journey. Alex Miranda hosts.",
|
||||
"playcount" : 0,
|
||||
"votes" : "0",
|
||||
"thumbnail" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fposters%2f260473-1.jpg/"
|
||||
},
|
||||
{
|
||||
"cast" : [],
|
||||
"file" : "/Users/martijn/Projects/dummymediafiles/media/tvshows/4-stjerners middag (2010)/",
|
||||
"premiered" : "2010-01-25",
|
||||
"watchedepisodes" : 2,
|
||||
"episodeguide" : "<episodeguide><url cache=\"146391-en.xml\">http://thetvdb.com/api/1D62F2F90030C444/series/146391/all/en.zip</url></episodeguide>",
|
||||
"genre" : [
|
||||
"Reality"
|
||||
],
|
||||
"dateadded" : "2016-08-26 09:16:59",
|
||||
"votes" : "1",
|
||||
"thumbnail" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fposters%2f146391-2.jpg/",
|
||||
"ratings" : {
|
||||
"default" : {
|
||||
"votes" : 1,
|
||||
"default" : true,
|
||||
"rating" : 10
|
||||
}
|
||||
},
|
||||
"episode" : 110,
|
||||
"playcount" : 0,
|
||||
"label" : "4 Stjerners Middag",
|
||||
"plot" : "Danish version of the British \"Come Dine With Me\". Every week four celebrities invites each other home for dinner, one by one. The goal is to make a perfect evening for the three guests, and collect as many points as possible, to be the host of the week. The host picks out the three course dinner and is responsible for buying groceries and preparing the meal.",
|
||||
"studio" : [
|
||||
"TVNorge"
|
||||
],
|
||||
"rating" : 10,
|
||||
"fanart" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2ffanart%2foriginal%2f146391-2.jpg/",
|
||||
"tag" : [],
|
||||
"art" : {
|
||||
"poster" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fposters%2f146391-2.jpg/",
|
||||
"fanart" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2ffanart%2foriginal%2f146391-2.jpg/"
|
||||
},
|
||||
"imdbnumber" : "146391",
|
||||
"title" : "4 Stjerners Middag",
|
||||
"year" : 2010,
|
||||
"originaltitle" : "",
|
||||
"userrating" : 0,
|
||||
"mpaa" : "",
|
||||
"tvshowid" : 3,
|
||||
"season" : 3,
|
||||
"sorttitle" : "",
|
||||
"lastplayed" : "2017-02-06 17:02:53",
|
||||
"uniqueid" : {
|
||||
"unknown" : "146391"
|
||||
},
|
||||
"runtime" : 3600
|
||||
},
|
||||
{
|
||||
"title" : "11.22.63",
|
||||
"year" : 2016,
|
||||
"originaltitle" : "",
|
||||
"mpaa" : "TV-MA",
|
||||
"userrating" : 0,
|
||||
"imdbnumber" : "301824",
|
||||
"art" : {
|
||||
"fanart" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2ffanart%2foriginal%2f301824-10.jpg/",
|
||||
"poster" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fposters%2f301824-8.jpg/",
|
||||
"banner" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fgraphical%2f301824-g.jpg/"
|
||||
},
|
||||
"lastplayed" : "2017-02-28 11:14:40",
|
||||
"runtime" : 3000,
|
||||
"uniqueid" : {
|
||||
"unknown" : "301824"
|
||||
},
|
||||
"tvshowid" : 1,
|
||||
"sorttitle" : "",
|
||||
"season" : 1,
|
||||
"dateadded" : "2016-08-26 09:17:01",
|
||||
"ratings" : {
|
||||
"default" : {
|
||||
"votes" : 31,
|
||||
"rating" : 7.69999980926514,
|
||||
"default" : true
|
||||
}
|
||||
},
|
||||
"episode" : 8,
|
||||
"label" : "11.22.63",
|
||||
"playcount" : 1,
|
||||
"plot" : "A teacher discovers a time portal that leads to October 21st, 1960 and goes on a quest to try and prevent the assassination of John F. Kennedy, which is complicated by the presence of Lee Harvey Oswald and the fact that he's falling in love with the past itself.",
|
||||
"thumbnail" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fposters%2f301824-8.jpg/",
|
||||
"votes" : "31",
|
||||
"premiered" : "2016-02-15",
|
||||
"cast" : [
|
||||
{
|
||||
"thumbnail" : "image://nfs%3a%2f%2f192.168.2.3%2f%2fvar%2fdata%2fmedia%2fvideos%2fmovies%2fromance%2f.actors%2fJames_Franco.jpg/",
|
||||
"order" : 0,
|
||||
"name" : "James Franco",
|
||||
"role" : "Jake Epping"
|
||||
},
|
||||
{
|
||||
"thumbnail" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2factors%2f358603.jpg/",
|
||||
"order" : 1,
|
||||
"role" : "Sadie Dunhill",
|
||||
"name" : "Sarah Gadon"
|
||||
},
|
||||
{
|
||||
"order" : 2,
|
||||
"thumbnail" : "image://nfs%3a%2f%2f192.168.2.3%2f%2fvar%2fdata%2fmedia%2fvideos%2fmovies%2fdrama%2f.actors%2fChris_Cooper.jpg/",
|
||||
"role" : "Al Templeton",
|
||||
"name" : "Chris Cooper"
|
||||
},
|
||||
{
|
||||
"thumbnail" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2factors%2f380762.jpg/",
|
||||
"order" : 3,
|
||||
"role" : "Harry Dunning",
|
||||
"name" : "Leon Rippy"
|
||||
},
|
||||
{
|
||||
"name" : "Kevin J. O'Connor",
|
||||
"role" : "Yellow Card Man",
|
||||
"thumbnail" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2factors%2f370399.jpg/",
|
||||
"order" : 4
|
||||
},
|
||||
{
|
||||
"thumbnail" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2factors%2f358610.jpg/",
|
||||
"order" : 5,
|
||||
"role" : "Bill Turcotte",
|
||||
"name" : "George MacKay"
|
||||
},
|
||||
{
|
||||
"thumbnail" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2factors%2f358609.jpg/",
|
||||
"order" : 6,
|
||||
"role" : "Lee Harvey Oswald",
|
||||
"name" : "Daniel Webber"
|
||||
},
|
||||
{
|
||||
"thumbnail" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2factors%2f358608.jpg/",
|
||||
"order" : 7,
|
||||
"role" : "Johnny Clayton",
|
||||
"name" : "T.R. Knight"
|
||||
},
|
||||
{
|
||||
"order" : 8,
|
||||
"thumbnail" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2factors%2f358607.jpg/",
|
||||
"role" : "Marquerite Oswald",
|
||||
"name" : "Cherry Jones"
|
||||
},
|
||||
{
|
||||
"order" : 9,
|
||||
"thumbnail" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2factors%2f358606.jpg/",
|
||||
"name" : "Lucy Fry",
|
||||
"role" : "Marina Oswald"
|
||||
},
|
||||
{
|
||||
"role" : "Frank Dunning",
|
||||
"name" : "Josh Duhamel",
|
||||
"order" : 10,
|
||||
"thumbnail" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2factors%2f358602.jpg/"
|
||||
},
|
||||
{
|
||||
"order" : 22,
|
||||
"role" : "George de Mohrenschildt",
|
||||
"name" : "Johny Coyne"
|
||||
},
|
||||
{
|
||||
"thumbnail" : "image://nfs%3a%2f%2f192.168.2.3%2f%2fvar%2fdata%2fmedia%2fvideos%2fmovies%2fdrama%2f.actors%2fNick_Searcy.jpg/",
|
||||
"order" : 23,
|
||||
"role" : "Deke Simmons",
|
||||
"name" : "Nick Searcy"
|
||||
}
|
||||
],
|
||||
"file" : "/Users/martijn/Projects/dummymediafiles/media/tvshows/11_22_63 (2016)/",
|
||||
"genre" : [
|
||||
"Drama",
|
||||
"Mini-Series",
|
||||
"Science-Fiction"
|
||||
],
|
||||
"episodeguide" : "<episodeguide><url cache=\"301824-en.xml\">http://thetvdb.com/api/1D62F2F90030C444/series/301824/all/en.zip</url></episodeguide>",
|
||||
"watchedepisodes" : 8,
|
||||
"fanart" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2ffanart%2foriginal%2f301824-10.jpg/",
|
||||
"tag" : [],
|
||||
"studio" : [
|
||||
"Hulu"
|
||||
],
|
||||
"rating" : 7.69999980926514
|
||||
},
|
||||
{
|
||||
"tvshowid" : 137,
|
||||
"sorttitle" : "",
|
||||
"season" : 5,
|
||||
"lastplayed" : "",
|
||||
"runtime" : 2700,
|
||||
"uniqueid" : {
|
||||
"unknown" : "77904"
|
||||
},
|
||||
"imdbnumber" : "77904",
|
||||
"art" : {
|
||||
"poster" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fposters%2f77904-3.jpg/",
|
||||
"fanart" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2ffanart%2foriginal%2f77904-5.jpg/",
|
||||
"banner" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fgraphical%2f77904-g9.jpg/"
|
||||
},
|
||||
"year" : 1983,
|
||||
"originaltitle" : "",
|
||||
"title" : "The A-Team",
|
||||
"mpaa" : "TV-PG",
|
||||
"userrating" : 0,
|
||||
"studio" : [
|
||||
"NBC"
|
||||
],
|
||||
"rating" : 7.80000019073486,
|
||||
"fanart" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2ffanart%2foriginal%2f77904-5.jpg/",
|
||||
"tag" : [],
|
||||
"premiered" : "1983-01-23",
|
||||
"file" : "/Users/martijn/Projects/dummymediafiles/media/tvshows/The A-Team (1983)/",
|
||||
"cast" : [
|
||||
{
|
||||
"name" : "George Peppard",
|
||||
"role" : "Col. John \"Hannibal\" Smith",
|
||||
"thumbnail" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2factors%2f82360.jpg/",
|
||||
"order" : 0
|
||||
},
|
||||
{
|
||||
"thumbnail" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2factors%2f59826.jpg/",
|
||||
"order" : 1,
|
||||
"name" : "Dirk Benedict",
|
||||
"role" : "Lt. Templeton \"Faceman\" Peck"
|
||||
},
|
||||
{
|
||||
"name" : "Mr. T",
|
||||
"role" : "Sgt. Bosco Albert \"B.A.\" Baracus",
|
||||
"thumbnail" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2factors%2f59825.jpg/",
|
||||
"order" : 2
|
||||
},
|
||||
{
|
||||
"order" : 3,
|
||||
"thumbnail" : "image://nfs%3a%2f%2f192.168.2.3%2f%2fvar%2fdata%2fmedia%2fvideos%2fmovies%2fscifi%2f.actors%2fDwight_Schultz.jpg/",
|
||||
"role" : "Capt. H.M. \"Howling Mad\" Murdock",
|
||||
"name" : "Dwight Schultz"
|
||||
},
|
||||
{
|
||||
"order" : 4,
|
||||
"thumbnail" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2factors%2f59831.jpg/",
|
||||
"role" : "Tawnia Baker",
|
||||
"name" : "Marla Heasley"
|
||||
},
|
||||
{
|
||||
"name" : "Melinda Culea",
|
||||
"role" : "Amy Amanda 'Triple A' Allen",
|
||||
"order" : 5,
|
||||
"thumbnail" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2factors%2f59827.jpg/"
|
||||
},
|
||||
{
|
||||
"name" : "William Lucking",
|
||||
"role" : "Col. Lynch",
|
||||
"order" : 6,
|
||||
"thumbnail" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2factors%2f59836.jpg/"
|
||||
},
|
||||
{
|
||||
"role" : "Col. Roderick Decker",
|
||||
"name" : "Lance LeGault",
|
||||
"thumbnail" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2factors%2f59828.jpg/",
|
||||
"order" : 7
|
||||
},
|
||||
{
|
||||
"name" : "Eddie Velez",
|
||||
"role" : "Frankie \"Dishpan Man\" Santana",
|
||||
"order" : 8,
|
||||
"thumbnail" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2factors%2f59838.jpg/"
|
||||
},
|
||||
{
|
||||
"role" : "Col. Briggs",
|
||||
"name" : "Charles Napier",
|
||||
"thumbnail" : "image://nfs%3a%2f%2f192.168.2.3%2f%2fvar%2fdata%2fmedia%2fvideos%2fmovies%2fcomedy%2f.actors%2fCharles_Napier.jpg/",
|
||||
"order" : 9
|
||||
},
|
||||
{
|
||||
"role" : "Capt. Crane",
|
||||
"name" : "Carl Franklin",
|
||||
"thumbnail" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2factors%2f59830.jpg/",
|
||||
"order" : 10
|
||||
},
|
||||
{
|
||||
"role" : "Gen. Hunt Stockwell",
|
||||
"name" : "Robert Vaughn",
|
||||
"order" : 19
|
||||
}
|
||||
],
|
||||
"genre" : [
|
||||
"Action",
|
||||
"Adventure"
|
||||
],
|
||||
"watchedepisodes" : 0,
|
||||
"episodeguide" : "<episodeguide><url cache=\"77904-en.xml\">http://thetvdb.com/api/1D62F2F90030C444/series/77904/all/en.zip</url></episodeguide>",
|
||||
"dateadded" : "2016-08-26 09:16:58",
|
||||
"episode" : 97,
|
||||
"ratings" : {
|
||||
"default" : {
|
||||
"votes" : 45,
|
||||
"rating" : 7.80000019073486,
|
||||
"default" : true
|
||||
}
|
||||
},
|
||||
"label" : "The A-Team",
|
||||
"playcount" : 0,
|
||||
"plot" : "The A-Team is about a group of ex-United States Army Special Forces personnel who work as soldiers of fortune, while on the run from the Army after being branded as war criminals for a crime they didn't commit.",
|
||||
"thumbnail" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fposters%2f77904-3.jpg/",
|
||||
"votes" : "45"
|
||||
},
|
||||
{
|
||||
"art" : {
|
||||
"poster" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fposters%2f75926-1.jpg/",
|
||||
"fanart" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2ffanart%2foriginal%2f75926-1.jpg/",
|
||||
"banner" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fgraphical%2f3449-g.jpg/"
|
||||
},
|
||||
"imdbnumber" : "75926",
|
||||
"userrating" : 0,
|
||||
"mpaa" : "TV-PG",
|
||||
"year" : 2001,
|
||||
"title" : "According to Jim",
|
||||
"originaltitle" : "",
|
||||
"sorttitle" : "",
|
||||
"season" : 8,
|
||||
"tvshowid" : 4,
|
||||
"uniqueid" : {
|
||||
"unknown" : "75926"
|
||||
},
|
||||
"runtime" : 1800,
|
||||
"lastplayed" : "2017-02-28 12:18:10",
|
||||
"watchedepisodes" : 9,
|
||||
"episodeguide" : "<episodeguide><url cache=\"75926-en.xml\">http://thetvdb.com/api/1D62F2F90030C444/series/75926/all/en.zip</url></episodeguide>",
|
||||
"genre" : [
|
||||
"Comedy"
|
||||
],
|
||||
"file" : "/Users/martijn/Projects/dummymediafiles/media/tvshows/According to Jim (2001)/",
|
||||
"cast" : [
|
||||
{
|
||||
"name" : "James Belushi",
|
||||
"role" : "Jim",
|
||||
"thumbnail" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2factors%2f41995.jpg/",
|
||||
"order" : 0
|
||||
},
|
||||
{
|
||||
"order" : 1,
|
||||
"thumbnail" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2factors%2f41994.jpg/",
|
||||
"role" : "Cheryl",
|
||||
"name" : "Courtney Thorne-Smith"
|
||||
},
|
||||
{
|
||||
"thumbnail" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2factors%2f41992.jpg/",
|
||||
"order" : 2,
|
||||
"name" : "Kimberly Williams-Paisley",
|
||||
"role" : "Dana"
|
||||
},
|
||||
{
|
||||
"role" : "Andy",
|
||||
"name" : "Larry Joe Campbell",
|
||||
"order" : 3,
|
||||
"thumbnail" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2factors%2f41993.jpg/"
|
||||
},
|
||||
{
|
||||
"role" : "Ruby",
|
||||
"name" : "Taylor Atelian",
|
||||
"thumbnail" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2factors%2f41991.jpg/",
|
||||
"order" : 4
|
||||
},
|
||||
{
|
||||
"name" : "Conner Rayburn",
|
||||
"role" : "Kyle",
|
||||
"thumbnail" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2factors%2f41990.jpg/",
|
||||
"order" : 5
|
||||
},
|
||||
{
|
||||
"thumbnail" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2factors%2f41989.jpg/",
|
||||
"order" : 6,
|
||||
"role" : "Gracie",
|
||||
"name" : "Billi Bruno"
|
||||
}
|
||||
],
|
||||
"premiered" : "2001-10-03",
|
||||
"thumbnail" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fposters%2f75926-1.jpg/",
|
||||
"votes" : "34",
|
||||
"label" : "According to Jim",
|
||||
"ratings" : {
|
||||
"default" : {
|
||||
"votes" : 34,
|
||||
"default" : true,
|
||||
"rating" : 8
|
||||
}
|
||||
},
|
||||
"playcount" : 0,
|
||||
"episode" : 182,
|
||||
"plot" : "Jim is an abrasive but lovable suburban father. Much like his real life counterpart, Jim's character is noted as a fan of Blues music, as well as the Chicago Blackhawks, Chicago Bulls, Chicago Bears, and the Chicago Cubs. He's married to a gorgeous woman, Cheryl, and raises his five children – Ruby, Gracie, Kyle, and twins, Gordan and Jonathan – in a big house. Everything is perfect for Jim, if it wasn't for the messy situations he gets himself into and his laziness, which often makes him search for alternative ways of doing things with less effort. Of course, having his wife's siblings hanging out at his house all the time is no help. While Andy might be one of his best friends, Dana often teams up with Cheryl against Jim.",
|
||||
"dateadded" : "2016-08-26 09:16:59",
|
||||
"rating" : 8,
|
||||
"studio" : [
|
||||
"ABC (US)"
|
||||
],
|
||||
"tag" : [],
|
||||
"fanart" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2ffanart%2foriginal%2f75926-1.jpg/"
|
||||
},
|
||||
{
|
||||
"watchedepisodes" : 0,
|
||||
"episodeguide" : "<episodeguide><url cache=\"252308-en.xml\">http://thetvdb.com/api/1D62F2F90030C444/series/252308/all/en.zip</url></episodeguide>",
|
||||
"genre" : [
|
||||
"Animation",
|
||||
"Children"
|
||||
],
|
||||
"file" : "/Users/martijn/Projects/dummymediafiles/media/tvshows/The Adventures of Abney & Teal (2011)/",
|
||||
"cast" : [],
|
||||
"premiered" : "2011-09-26",
|
||||
"thumbnail" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fposters%2f252308-1.jpg/",
|
||||
"votes" : "1",
|
||||
"episode" : 52,
|
||||
"ratings" : {
|
||||
"default" : {
|
||||
"votes" : 1,
|
||||
"rating" : 9,
|
||||
"default" : true
|
||||
}
|
||||
},
|
||||
"plot" : "Animated adventures of two friends who live on an island in the middle of a lake, in the middle of a park, in the middle of the big city.",
|
||||
"playcount" : 0,
|
||||
"label" : "The Adventures of Abney & Teal",
|
||||
"dateadded" : "2016-08-26 09:16:57",
|
||||
"rating" : 9,
|
||||
"studio" : [
|
||||
"CBeebies"
|
||||
],
|
||||
"tag" : [],
|
||||
"fanart" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2ffanart%2foriginal%2f252308-4.jpg/",
|
||||
"art" : {
|
||||
"poster" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fposters%2f252308-1.jpg/",
|
||||
"fanart" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2ffanart%2foriginal%2f252308-4.jpg/",
|
||||
"banner" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fgraphical%2f252308-g3.jpg/"
|
||||
},
|
||||
"imdbnumber" : "252308",
|
||||
"userrating" : 0,
|
||||
"mpaa" : "TV-Y",
|
||||
"year" : 2011,
|
||||
"title" : "The Adventures of Abney & Teal",
|
||||
"originaltitle" : "",
|
||||
"season" : 2,
|
||||
"sorttitle" : "",
|
||||
"tvshowid" : 138,
|
||||
"uniqueid" : {
|
||||
"unknown" : "252308"
|
||||
},
|
||||
"runtime" : 660,
|
||||
"lastplayed" : ""
|
||||
},
|
||||
{
|
||||
"tag" : [],
|
||||
"fanart" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2ffanart%2foriginal%2f104171-1.jpg/",
|
||||
"rating" : 3,
|
||||
"studio" : [
|
||||
"Seven Network"
|
||||
],
|
||||
"episode" : 1,
|
||||
"ratings" : {
|
||||
"default" : {
|
||||
"votes" : 1,
|
||||
"default" : true,
|
||||
"rating" : 3
|
||||
}
|
||||
},
|
||||
"label" : "Air Ways",
|
||||
"plot" : "Follow the ups and downs of travel on Tiger Airways as viewers get an unprecedented look into the day-to-day running of a budget airline in Australia. A cancelled flight causes chaos, staff witness an unexpected proposal, a baggage problem riles a mum's temper, and a sleep-deprived teenager awakens to a rude shock.\r\nNarrated by Corinne Grant",
|
||||
"playcount" : 1,
|
||||
"votes" : "1",
|
||||
"thumbnail" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fposters%2f104171-1.jpg/",
|
||||
"dateadded" : "2016-08-26 09:16:57",
|
||||
"genre" : [
|
||||
"Reality"
|
||||
],
|
||||
"episodeguide" : "<episodeguide><url cache=\"104171-en.xml\">http://thetvdb.com/api/1D62F2F90030C444/series/104171/all/en.zip</url></episodeguide>",
|
||||
"watchedepisodes" : 1,
|
||||
"premiered" : "2009-07-21",
|
||||
"cast" : [],
|
||||
"file" : "/Users/martijn/Projects/dummymediafiles/media/tvshows/Airways (2009)/",
|
||||
"runtime" : 1800,
|
||||
"uniqueid" : {
|
||||
"unknown" : "104171"
|
||||
},
|
||||
"lastplayed" : "2017-02-06 16:41:56",
|
||||
"sorttitle" : "",
|
||||
"season" : 1,
|
||||
"tvshowid" : 5,
|
||||
"mpaa" : "TV-PG",
|
||||
"userrating" : 0,
|
||||
"originaltitle" : "",
|
||||
"year" : 2009,
|
||||
"title" : "Air Ways",
|
||||
"imdbnumber" : "104171",
|
||||
"art" : {
|
||||
"poster" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fposters%2f104171-1.jpg/",
|
||||
"fanart" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2ffanart%2foriginal%2f104171-1.jpg/",
|
||||
"banner" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fgraphical%2f104171-g.jpg/"
|
||||
}
|
||||
},
|
||||
{
|
||||
"sorttitle" : "",
|
||||
"season" : 0,
|
||||
"tvshowid" : 127,
|
||||
"runtime" : 0,
|
||||
"uniqueid" : {
|
||||
"unknown" : "278782"
|
||||
},
|
||||
"lastplayed" : "",
|
||||
"imdbnumber" : "278782",
|
||||
"art" : {
|
||||
"poster" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fposters%2f278782-1.jpg/"
|
||||
},
|
||||
"mpaa" : "",
|
||||
"userrating" : 0,
|
||||
"year" : 1969,
|
||||
"originaltitle" : "",
|
||||
"title" : "Al Jazeera Special Series",
|
||||
"rating" : 10,
|
||||
"studio" : [],
|
||||
"tag" : [],
|
||||
"fanart" : "",
|
||||
"genre" : [],
|
||||
"episodeguide" : "<episodeguide><url cache=\"278782-en.xml\">http://thetvdb.com/api/1D62F2F90030C444/series/278782/all/en.zip</url></episodeguide>",
|
||||
"watchedepisodes" : 0,
|
||||
"premiered" : "1969-12-31",
|
||||
"file" : "/Users/martijn/Projects/dummymediafiles/media/tvshows/Special Series (2016)/",
|
||||
"cast" : [],
|
||||
"ratings" : {
|
||||
"default" : {
|
||||
"votes" : 1,
|
||||
"rating" : 10,
|
||||
"default" : true
|
||||
}
|
||||
},
|
||||
"episode" : 0,
|
||||
"label" : "Al Jazeera Special Series",
|
||||
"plot" : "",
|
||||
"playcount" : 1,
|
||||
"votes" : "1",
|
||||
"thumbnail" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fposters%2f278782-1.jpg/",
|
||||
"dateadded" : ""
|
||||
},
|
||||
{
|
||||
"originaltitle" : "",
|
||||
"year" : 2012,
|
||||
"title" : "American Colony Meet the Hutterites",
|
||||
"userrating" : 0,
|
||||
"mpaa" : "",
|
||||
"art" : {
|
||||
"fanart" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2ffanart%2foriginal%2f259064-1.jpg/",
|
||||
"poster" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fposters%2f259064-1.jpg/",
|
||||
"banner" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fgraphical%2f259064-g.jpg/"
|
||||
},
|
||||
"imdbnumber" : "259064",
|
||||
"lastplayed" : "",
|
||||
"uniqueid" : {
|
||||
"unknown" : "259064"
|
||||
},
|
||||
"runtime" : 1800,
|
||||
"tvshowid" : 6,
|
||||
"sorttitle" : "",
|
||||
"season" : 1,
|
||||
"dateadded" : "2016-08-26 09:16:57",
|
||||
"thumbnail" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2fposters%2f259064-1.jpg/",
|
||||
"votes" : "1",
|
||||
"plot" : "Meet the Hutterites—a small religious colony in rural Montana who holds desperately to their sacred traditions while fighting the modern temptations of the outside world. King Colony is made up of 59 people and they are almost all related. This family lives together, works together, and worships God together, 7 days a week, 365 days a year, for their entire lives. And, like any family, this one doesn’t always agree. Most of the colony is holding tight to the age-old traditions of their ancestors, while others are flirting with modern society. Some feel that bringing modern technology, education, and ideas into the colony will only help it, while others fear that this modern way of thinking threatens their very existence. We follow the men, the women, the young, and the old, as they strive to live as proper Hutterites. Some will succeed, some will fail, and everyone will have a choice to make. This is the very first glimpse into the world of the Hutterites.",
|
||||
"ratings" : {
|
||||
"default" : {
|
||||
"votes" : 1,
|
||||
"default" : true,
|
||||
"rating" : 1
|
||||
}
|
||||
},
|
||||
"label" : "American Colony Meet the Hutterites",
|
||||
"episode" : 10,
|
||||
"playcount" : 0,
|
||||
"cast" : [],
|
||||
"file" : "/Users/martijn/Projects/dummymediafiles/media/tvshows/American Colony: Meet the Hutterites (2012)/",
|
||||
"premiered" : "2012-05-29",
|
||||
"episodeguide" : "<episodeguide><url cache=\"259064-en.xml\">http://thetvdb.com/api/1D62F2F90030C444/series/259064/all/en.zip</url></episodeguide>",
|
||||
"watchedepisodes" : 0,
|
||||
"genre" : [
|
||||
"Documentary"
|
||||
],
|
||||
"fanart" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2ffanart%2foriginal%2f259064-1.jpg/",
|
||||
"tag" : [],
|
||||
"studio" : [
|
||||
"National Geographic"
|
||||
],
|
||||
"rating" : 1
|
||||
},
|
||||
{
|
||||
"runtime" : 3600,
|
||||
"uniqueid" : {
|
||||
"unknown" : "258525"
|
||||
},
|
||||
"lastplayed" : "",
|
||||
"season" : 1,
|
||||
"sorttitle" : "",
|
||||
"tvshowid" : 7,
|
||||
"mpaa" : "TV-PG",
|
||||
"userrating" : 0,
|
||||
"year" : 2012,
|
||||
"originaltitle" : "",
|
||||
"title" : "Amish: Out Of Order",
|
||||
"imdbnumber" : "258525",
|
||||
"art" : {
|
||||
"fanart" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2ffanart%2foriginal%2f258525-1.jpg/"
|
||||
},
|
||||
"tag" : [],
|
||||
"fanart" : "image://http%3a%2f%2fthetvdb.com%2fbanners%2ffanart%2foriginal%2f258525-1.jpg/",
|
||||
"rating" : 8.5,
|
||||
"studio" : [
|
||||
"National Geographic"
|
||||
],
|
||||
"label" : "Amish: Out Of Order",
|
||||
"ratings" : {
|
||||
"default" : {
|
||||
"rating" : 8.5,
|
||||
"default" : true,
|
||||
"votes" : 0
|
||||
}
|
||||
},
|
||||
"plot" : "It takes a lot to leave the only life you’ve ever known—for one you’ve been told will lead you straight to hell. And with little possibility of normal contact with your family ever again, turning your back on the Amish order is an immense undertaking, and a choice that’s not made without tremendous consideration. In the new ten-part series Amish: Out of Order, follow the trials and tribulations of individuals who have made the decision to leave the Amish community behind. Due to their religious beliefs, most Amish refuse to be photographed or videotaped—even ex-Amish risk permanent shunning by their family and community for appearing on camera. The ex-Amish in this program accept that risk.",
|
||||
"playcount" : 0,
|
||||
"episode" : 9,
|
||||
"thumbnail" : "",
|
||||
"votes" : "0",
|
||||
"dateadded" : "2016-08-26 09:16:59",
|
||||
"genre" : [
|
||||
"Documentary",
|
||||
"Reality"
|
||||
],
|
||||
"watchedepisodes" : 0,
|
||||
"episodeguide" : "<episodeguide><url cache=\"258525-en.xml\">http://thetvdb.com/api/1D62F2F90030C444/series/258525/all/en.zip</url></episodeguide>",
|
||||
"premiered" : "2012-04-24",
|
||||
"cast" : [],
|
||||
"file" : "/Users/martijn/Projects/dummymediafiles/media/tvshows/Amish: Out of Order (2012)/"
|
||||
}
|
||||
],
|
||||
"limits" : {
|
||||
"start" : 0,
|
||||
"total" : 177,
|
||||
"end" : 10
|
||||
}
|
||||
}
|
||||
}
|
||||
75
app/src/debug/java/org/xbmc/kore/testutils/CursorUtils.java
Normal file
75
app/src/debug/java/org/xbmc/kore/testutils/CursorUtils.java
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
/*
|
||||
* 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;
|
||||
|
||||
import android.database.Cursor;
|
||||
|
||||
public class CursorUtils {
|
||||
/**
|
||||
* Converts the current row in cursor to a string with each line
|
||||
* containing a column name and value pair.
|
||||
* @param cursor
|
||||
* @return
|
||||
*/
|
||||
public static String cursorToString(Cursor cursor) {
|
||||
StringBuffer stringBuffer = new StringBuffer();
|
||||
for (String name : cursor.getColumnNames()) {
|
||||
int index = cursor.getColumnIndex(name);
|
||||
stringBuffer.append(name + "=" + cursor.getString(index) + "\n");
|
||||
}
|
||||
return stringBuffer.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Moves cursor to first position item is found at column index
|
||||
* @param cursor
|
||||
* @param columnIndex
|
||||
* @param item integer to search for at given column index
|
||||
* @return true if item found, false otherwise
|
||||
*/
|
||||
public static boolean moveCursorToFirstOccurrence(Cursor cursor, int columnIndex, int item) {
|
||||
if (( cursor == null ) || ( ! cursor.moveToFirst() ))
|
||||
return false;
|
||||
|
||||
do {
|
||||
if ( cursor.getInt(columnIndex) == item )
|
||||
return true;
|
||||
} while (cursor.moveToNext());
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Counts the occurences item is found at given column index
|
||||
* @param cursor
|
||||
* @param columnIndex
|
||||
* @param item integer to search for at given column index
|
||||
* @return amount of occurences, -1 if an error occured
|
||||
*/
|
||||
public static int countOccurences(Cursor cursor, int columnIndex, int item) {
|
||||
if (( cursor == null ) || ( ! cursor.moveToFirst() ))
|
||||
return -1;
|
||||
|
||||
int count = 0;
|
||||
do {
|
||||
if ( cursor.getInt(columnIndex) == item )
|
||||
count++;
|
||||
} while (cursor.moveToNext());
|
||||
|
||||
return count;
|
||||
}
|
||||
}
|
||||
181
app/src/debug/java/org/xbmc/kore/testutils/Database.java
Normal file
181
app/src/debug/java/org/xbmc/kore/testutils/Database.java
Normal file
|
|
@ -0,0 +1,181 @@
|
|||
/*
|
||||
* 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;
|
||||
|
||||
import android.content.ContentResolver;
|
||||
import android.content.ContentValues;
|
||||
import android.content.Context;
|
||||
|
||||
import org.xbmc.kore.host.HostInfo;
|
||||
import org.xbmc.kore.host.HostManager;
|
||||
import org.xbmc.kore.jsonrpc.ApiException;
|
||||
import org.xbmc.kore.jsonrpc.ApiList;
|
||||
import org.xbmc.kore.host.HostConnection;
|
||||
import org.xbmc.kore.jsonrpc.method.AudioLibrary;
|
||||
import org.xbmc.kore.jsonrpc.method.VideoLibrary;
|
||||
import org.xbmc.kore.jsonrpc.type.AudioType;
|
||||
import org.xbmc.kore.jsonrpc.type.LibraryType;
|
||||
import org.xbmc.kore.jsonrpc.type.VideoType;
|
||||
import org.xbmc.kore.provider.MediaContract;
|
||||
import org.xbmc.kore.service.library.SyncMusic;
|
||||
import org.xbmc.kore.service.library.SyncMusicVideos;
|
||||
import org.xbmc.kore.service.library.SyncTVShows;
|
||||
import org.xbmc.kore.service.library.SyncUtils;
|
||||
import org.xbmc.kore.utils.LogUtils;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
|
||||
public class Database {
|
||||
public static final String TAG = LogUtils.makeLogTag(Database.class);
|
||||
|
||||
public static HostInfo fill(HostInfo hostInfo, Context context, ContentResolver contentResolver) throws ApiException, IOException {
|
||||
SyncMusic syncMusic = new SyncMusic(null);
|
||||
insertMovies(context, contentResolver, hostInfo.getId());
|
||||
insertArtists(context, contentResolver, syncMusic, hostInfo.getId());
|
||||
insertGenres(context, contentResolver, syncMusic, hostInfo.getId());
|
||||
insertAlbums(context, contentResolver, syncMusic, hostInfo.getId());
|
||||
insertSongs(context, contentResolver, syncMusic, hostInfo.getId());
|
||||
|
||||
SyncTVShows syncTVShows = new SyncTVShows(hostInfo.getId(), null);
|
||||
insertTVShows(context, contentResolver, syncTVShows);
|
||||
|
||||
SyncMusicVideos syncMusicVideos = new SyncMusicVideos(hostInfo.getId(), null);
|
||||
insertMusicVideos(context, contentResolver, syncMusicVideos);
|
||||
|
||||
return hostInfo;
|
||||
}
|
||||
|
||||
public static void flush(ContentResolver contentResolver) {
|
||||
contentResolver.delete(MediaContract.Hosts.CONTENT_URI, null, null);
|
||||
}
|
||||
|
||||
public static HostInfo addHost(Context context) {
|
||||
return addHost(context, "127.0.0.1", HostConnection.PROTOCOL_TCP,
|
||||
HostInfo.DEFAULT_HTTP_PORT, HostInfo.DEFAULT_TCP_PORT, false,
|
||||
HostInfo.DEFAULT_KODI_VERSION_MAJOR);
|
||||
|
||||
}
|
||||
|
||||
public static HostInfo addHost(Context context, String hostname, int protocol, int httpPort,
|
||||
int tcpPort, boolean useEventServer, int kodiMajorVersion) {
|
||||
return HostManager.getInstance(context).addHost("TestHost", hostname, protocol, httpPort,
|
||||
tcpPort, null, null, "52:54:00:12:35:02", 9, true,
|
||||
useEventServer, HostInfo.DEFAULT_EVENT_SERVER_PORT,
|
||||
kodiMajorVersion,
|
||||
HostInfo.DEFAULT_KODI_VERSION_MINOR,
|
||||
HostInfo.DEFAULT_KODI_VERSION_REVISION,
|
||||
HostInfo.DEFAULT_KODI_VERSION_TAG,
|
||||
false);
|
||||
}
|
||||
|
||||
private static void insertMovies(Context context, ContentResolver contentResolver, int hostId)
|
||||
throws ApiException, IOException {
|
||||
VideoLibrary.GetMovies getMovies = new VideoLibrary.GetMovies();
|
||||
String result = FileUtils.readFile(context, "Video.Details.Movie.json");
|
||||
ApiList<VideoType.DetailsMovie> movieList = getMovies.resultFromJson(result);
|
||||
|
||||
|
||||
ContentValues[] movieValuesBatch = new ContentValues[movieList.items.size()];
|
||||
int castCount = 0;
|
||||
|
||||
// Iterate on each movie
|
||||
for (int i = 0; i < movieList.items.size(); i++) {
|
||||
VideoType.DetailsMovie movie = movieList.items.get(i);
|
||||
movieValuesBatch[i] = SyncUtils.contentValuesFromMovie(hostId, movie);
|
||||
castCount += movie.cast.size();
|
||||
}
|
||||
|
||||
contentResolver.bulkInsert(MediaContract.Movies.CONTENT_URI, movieValuesBatch);
|
||||
|
||||
ContentValues[] movieCastValuesBatch = new ContentValues[castCount];
|
||||
int count = 0;
|
||||
// Iterate on each movie/cast
|
||||
for (VideoType.DetailsMovie movie : movieList.items) {
|
||||
for (VideoType.Cast cast : movie.cast) {
|
||||
movieCastValuesBatch[count] = SyncUtils.contentValuesFromCast(hostId, cast);
|
||||
movieCastValuesBatch[count].put(MediaContract.MovieCastColumns.MOVIEID, movie.movieid);
|
||||
count++;
|
||||
}
|
||||
}
|
||||
|
||||
contentResolver.bulkInsert(MediaContract.MovieCast.CONTENT_URI, movieCastValuesBatch);
|
||||
}
|
||||
|
||||
private static void insertArtists(Context context, ContentResolver contentResolver, SyncMusic syncMusic, int hostId) throws ApiException, IOException {
|
||||
AudioLibrary.GetArtists getArtists = new AudioLibrary.GetArtists(false);
|
||||
String result = FileUtils.readFile(context, "AudioLibrary.GetArtists.json");
|
||||
ArrayList<AudioType.DetailsArtist> artistList = (ArrayList<AudioType.DetailsArtist>) getArtists.resultFromJson(result).items;
|
||||
|
||||
syncMusic.insertArtists(hostId, artistList, contentResolver);
|
||||
}
|
||||
|
||||
private static void insertGenres(Context context, ContentResolver contentResolver, SyncMusic syncMusic, int hostId) throws ApiException, IOException {
|
||||
AudioLibrary.GetGenres getGenres = new AudioLibrary.GetGenres();
|
||||
ArrayList<LibraryType.DetailsGenre> genreList =
|
||||
(ArrayList<LibraryType.DetailsGenre>) getGenres.resultFromJson(FileUtils.readFile(context,
|
||||
"AudioLibrary.GetGenres.json"));
|
||||
|
||||
syncMusic.insertGenresItems(hostId, genreList, contentResolver);
|
||||
}
|
||||
|
||||
private static void insertAlbums(Context context, ContentResolver contentResolver, SyncMusic syncMusic, int hostId) throws ApiException, IOException {
|
||||
AudioLibrary.GetAlbums getAlbums = new AudioLibrary.GetAlbums();
|
||||
String result = FileUtils.readFile(context, "AudioLibrary.GetAlbums.json");
|
||||
ArrayList<AudioType.DetailsAlbum> albumList = (ArrayList<AudioType.DetailsAlbum>) getAlbums.resultFromJson(result).items;
|
||||
|
||||
syncMusic.insertAlbumsItems(hostId, albumList, contentResolver);
|
||||
}
|
||||
|
||||
private static void insertSongs(Context context, ContentResolver contentResolver, SyncMusic syncMusic, int hostId) throws ApiException, IOException {
|
||||
AudioLibrary.GetSongs getSongs = new AudioLibrary.GetSongs();
|
||||
ArrayList<AudioType.DetailsSong> songList =
|
||||
(ArrayList<AudioType.DetailsSong>) getSongs.resultFromJson(FileUtils.readFile(context, "AudioLibrary.GetSongs.json")).items;
|
||||
|
||||
syncMusic.insertSongsItems(hostId, songList, contentResolver);
|
||||
}
|
||||
|
||||
private static void insertTVShows(Context context, ContentResolver contentResolver, SyncTVShows syncTVShows)
|
||||
throws ApiException, IOException {
|
||||
VideoLibrary.GetTVShows getTVShows = new VideoLibrary.GetTVShows();
|
||||
String result = FileUtils.readFile(context, "VideoLibrary.GetTVShows.json");
|
||||
ArrayList<VideoType.DetailsTVShow> tvShowList = (ArrayList<VideoType.DetailsTVShow>) getTVShows.resultFromJson(result).items;
|
||||
|
||||
syncTVShows.insertTVShows(tvShowList, contentResolver);
|
||||
|
||||
for ( VideoType.DetailsTVShow tvShow : tvShowList ) {
|
||||
VideoLibrary.GetSeasons getSeasons = new VideoLibrary.GetSeasons(tvShow.tvshowid);
|
||||
result = FileUtils.readFile(context, "VideoLibrary.GetSeasons.json");
|
||||
ArrayList<VideoType.DetailsSeason> detailsSeasons = (ArrayList<VideoType.DetailsSeason>) getSeasons.resultFromJson(result);
|
||||
syncTVShows.insertSeason(tvShow.tvshowid, detailsSeasons, contentResolver);
|
||||
}
|
||||
|
||||
VideoLibrary.GetEpisodes getEpisodes = new VideoLibrary.GetEpisodes(0);
|
||||
result = FileUtils.readFile(context, "VideoLibrary.GetEpisodes.json");
|
||||
ArrayList<VideoType.DetailsEpisode> detailsEpisodes = (ArrayList<VideoType.DetailsEpisode>) getEpisodes.resultFromJson(result);
|
||||
syncTVShows.insertEpisodes(detailsEpisodes, contentResolver);
|
||||
}
|
||||
|
||||
private static void insertMusicVideos(Context context, ContentResolver contentResolver, SyncMusicVideos syncMusicVideos)
|
||||
throws ApiException, IOException {
|
||||
VideoLibrary.GetMusicVideos getMusicVideos = new VideoLibrary.GetMusicVideos();
|
||||
String result = FileUtils.readFile(context, "VideoLibrary.GetMusicVideos.json");
|
||||
ArrayList<VideoType.DetailsMusicVideo> musicVideoList = (ArrayList<VideoType.DetailsMusicVideo>) getMusicVideos.resultFromJson(result);
|
||||
|
||||
syncMusicVideos.insertMusicVideos(musicVideoList, contentResolver);
|
||||
}
|
||||
}
|
||||
38
app/src/debug/java/org/xbmc/kore/testutils/FileUtils.java
Normal file
38
app/src/debug/java/org/xbmc/kore/testutils/FileUtils.java
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
/*
|
||||
* 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;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
public class FileUtils {
|
||||
public static String readFile(Context context, String filename) throws IOException {
|
||||
InputStream is = context.getAssets().open(filename);
|
||||
|
||||
int size = is.available();
|
||||
|
||||
byte[] buffer = new byte[size];
|
||||
|
||||
is.read(buffer);
|
||||
|
||||
is.close();
|
||||
|
||||
return new String(buffer, "UTF-8");
|
||||
}
|
||||
}
|
||||
144
app/src/debug/java/org/xbmc/kore/testutils/TestUtils.java
Normal file
144
app/src/debug/java/org/xbmc/kore/testutils/TestUtils.java
Normal file
|
|
@ -0,0 +1,144 @@
|
|||
/*
|
||||
* 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;
|
||||
|
||||
import android.database.Cursor;
|
||||
|
||||
import org.xbmc.kore.testutils.tcpserver.handlers.jsonrpc.response.methods.Player;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class TestUtils {
|
||||
/**
|
||||
* Tests if cursor contains all numbers from ids given column index.
|
||||
* @param cursor
|
||||
* @param columnIndex
|
||||
* @param numbers
|
||||
*/
|
||||
public static void testCursorContainsNumbers(Cursor cursor, int columnIndex, int... numbers) {
|
||||
HashMap<Integer, Boolean> idsFound = new HashMap<>();
|
||||
for(int number : numbers) {
|
||||
idsFound.put(number, false);
|
||||
}
|
||||
|
||||
assertTrue(cursor.moveToFirst());
|
||||
do {
|
||||
idsFound.put(cursor.getInt(columnIndex), true);
|
||||
} while(cursor.moveToNext());
|
||||
|
||||
for(Map.Entry<Integer, Boolean> entry : idsFound.entrySet() ) {
|
||||
int key = entry.getKey();
|
||||
assertTrue("Id " + key + " not found", entry.getValue());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests if cursor contains all numbers from start until end for given column index.
|
||||
* Including the start and end integers.
|
||||
* @param columnIndex
|
||||
* @param cursor
|
||||
* @param start
|
||||
* @param end
|
||||
*/
|
||||
public static void testCursorContainsRange(Cursor cursor, int columnIndex, int start, int end) {
|
||||
HashMap<Integer, Boolean> idsFound = new HashMap<>();
|
||||
for(int i = start; i <= end; i++) {
|
||||
idsFound.put(i, false);
|
||||
}
|
||||
|
||||
assertTrue(cursor.moveToFirst());
|
||||
do {
|
||||
idsFound.put(cursor.getInt(columnIndex), true);
|
||||
} while(cursor.moveToNext());
|
||||
|
||||
for(Map.Entry<Integer, Boolean> entry : idsFound.entrySet() ) {
|
||||
int key = entry.getKey();
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,128 @@
|
|||
/*
|
||||
* 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.testutils.eventserver;
|
||||
|
||||
import org.xbmc.kore.eventclient.Packet;
|
||||
import org.xbmc.kore.utils.LogUtils;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* Class that implements a single event packet.
|
||||
* <pre>
|
||||
* -----------------------------
|
||||
* | -H1 Signature ("XBMC") | - 4 x CHAR 4B
|
||||
* | -H2 Version (eg. 2.0) | - 2 x UNSIGNED CHAR 2B
|
||||
* | -H3 PacketType | - 1 x UNSIGNED SHORT 2B
|
||||
* | -H4 Sequence number | - 1 x UNSIGNED LONG 4B
|
||||
* | -H5 No. of packets in msg | - 1 x UNSIGNED LONG 4B
|
||||
* | -H6 Payloadsize of packet | - 1 x UNSIGNED SHORT 2B
|
||||
* | -H7 Client's unique token | - 1 x UNSIGNED LONG 4B
|
||||
* | -H8 Reserved | - 10 x UNSIGNED CHAR 10B
|
||||
* |---------------------------|
|
||||
* | -P1 payload | -
|
||||
* -----------------------------
|
||||
* </pre>
|
||||
*/
|
||||
abstract public class EventPacket {
|
||||
|
||||
private static final String TAG = LogUtils.makeLogTag(EventPacket.class);
|
||||
|
||||
//Package types
|
||||
public final static byte PT_BUTTON = 0x03;
|
||||
|
||||
private String signature;
|
||||
private String version;
|
||||
private int packetType;
|
||||
private long sequenceNumber;
|
||||
private long numberOfPackets;
|
||||
private int payloadSize;
|
||||
private long token;
|
||||
|
||||
private byte[] payload;
|
||||
|
||||
private EventPacket() {}
|
||||
|
||||
EventPacket(byte[] packet) {
|
||||
signature = new String(new byte[] {packet[0], packet[1], packet[2], packet[3]});
|
||||
version = ((int) packet[4]) + "." + ((int) packet[5]);
|
||||
packetType = ByteBuffer.wrap(packet, 6, 2).getChar();
|
||||
sequenceNumber = ByteBuffer.wrap(packet, 8, 4).getInt();
|
||||
numberOfPackets = ByteBuffer.wrap(packet, 12, 4).getInt();
|
||||
payloadSize = ByteBuffer.wrap(packet, 16, 2).getChar();
|
||||
token = ByteBuffer.wrap(packet, 18, 4).getInt();
|
||||
//Reserved 22 - 32
|
||||
payload = new byte[payloadSize];
|
||||
ByteBuffer.wrap(packet, 32, payloadSize).get(payload);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return signature + ":" +
|
||||
version + ":" +
|
||||
packetType + ":" +
|
||||
sequenceNumber + ":" +
|
||||
numberOfPackets + ":" +
|
||||
payloadSize + ":" +
|
||||
token+ ":" +
|
||||
payload;
|
||||
}
|
||||
|
||||
public int getPacketType() {
|
||||
return packetType;
|
||||
}
|
||||
|
||||
public byte[] getPayload() {
|
||||
return payload;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the packet type from a {@link Packet} as a single byte.
|
||||
* <br/>
|
||||
* Note that, although the specification specifies two bytes,
|
||||
* we only use a single byte in {@link Packet} for the packet types.
|
||||
* @param packet
|
||||
* @return second byte of packet type
|
||||
*/
|
||||
static public byte getPacketType(byte[] packet) {
|
||||
return packet[7];
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the string from payload terminated by 0x00.
|
||||
* @param payload byte array holding the characters
|
||||
* @param offset starting offset of string
|
||||
* @return string from payload or null if not found
|
||||
*/
|
||||
String getStringFromPayload(byte[] payload, int offset) {
|
||||
int strTerminatorIndex = offset;
|
||||
for (; strTerminatorIndex < payload.length; strTerminatorIndex++) {
|
||||
if (payload[strTerminatorIndex] == 0x00)
|
||||
break;
|
||||
}
|
||||
|
||||
if (strTerminatorIndex == payload.length)
|
||||
return null;
|
||||
|
||||
int stringLength = strTerminatorIndex - offset;
|
||||
byte[] bytes = new byte[stringLength];
|
||||
System.arraycopy(payload, offset, bytes, 0, stringLength);
|
||||
|
||||
return new String(bytes);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,72 @@
|
|||
/*
|
||||
* 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.testutils.eventserver;
|
||||
|
||||
import org.xbmc.kore.utils.LogUtils;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
public class EventPacketBUTTON extends EventPacket {
|
||||
private final static String TAG = LogUtils.makeLogTag(EventPacketBUTTON.class);
|
||||
|
||||
private short code;
|
||||
private String mapName;
|
||||
private String buttonName;
|
||||
private boolean repeat;
|
||||
private boolean down;
|
||||
private boolean queue;
|
||||
private short amount;
|
||||
private byte axis;
|
||||
private short flags;
|
||||
|
||||
public EventPacketBUTTON(byte[] packet) {
|
||||
super(packet);
|
||||
|
||||
byte[] payload = getPayload();
|
||||
|
||||
try {
|
||||
code = ByteBuffer.wrap(payload, 0, 2).getShort();
|
||||
flags = ByteBuffer.wrap(payload, 2, 2).getShort();
|
||||
amount = ByteBuffer.wrap(payload, 4, 2).getShort();
|
||||
|
||||
mapName = getStringFromPayload(payload, 6);
|
||||
|
||||
int nextStringPosition = 6 + mapName.getBytes().length + 1;
|
||||
buttonName = getStringFromPayload(payload, nextStringPosition);
|
||||
} catch (ArrayIndexOutOfBoundsException e) {
|
||||
LogUtils.LOGE(TAG, "Error handling payload " + new String(payload));
|
||||
}
|
||||
}
|
||||
|
||||
public String getButtonName() {
|
||||
return buttonName;
|
||||
}
|
||||
|
||||
public String getMapName() {
|
||||
return mapName;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return super.toString() +
|
||||
", code: " + code +
|
||||
", flags: " + flags +
|
||||
", amount: " + amount +
|
||||
", mapName: " + mapName +
|
||||
", buttonName: " + buttonName;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,84 @@
|
|||
/*
|
||||
* 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.testutils.eventserver;
|
||||
|
||||
import org.xbmc.kore.utils.LogUtils;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.DatagramPacket;
|
||||
import java.net.DatagramSocket;
|
||||
import java.net.SocketException;
|
||||
|
||||
public class MockEventServer extends Thread {
|
||||
private static final String TAG = LogUtils.makeLogTag(MockEventServer.class);
|
||||
|
||||
private int listenPort = 9997;
|
||||
private boolean keepRunning;
|
||||
private EventPacket packet;
|
||||
private DatagramSocket datagramSocket;
|
||||
|
||||
public MockEventServer() {
|
||||
}
|
||||
|
||||
public void setListenPort(int portNumber) {
|
||||
this.listenPort = portNumber;
|
||||
}
|
||||
|
||||
public void run() {
|
||||
try {
|
||||
datagramSocket = new DatagramSocket(this.listenPort);
|
||||
} catch (SocketException e) {
|
||||
System.out.println("MockEventServer: Failed to open socket: " + e.getMessage());
|
||||
return;
|
||||
}
|
||||
|
||||
keepRunning = true;
|
||||
while(keepRunning) {
|
||||
byte[] buf = new byte[1024];
|
||||
DatagramPacket datagramPacket = new DatagramPacket(buf, buf.length);
|
||||
try {
|
||||
datagramSocket.receive(datagramPacket);
|
||||
packet = new EventPacketBUTTON(datagramPacket.getData());
|
||||
} catch (IOException e) {
|
||||
System.out.println("MockEventServer: error receiving packet: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the last received packet
|
||||
* @return
|
||||
*/
|
||||
public EventPacket getEventPacket() {
|
||||
return packet;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops the server from listening for new packets
|
||||
*/
|
||||
public void shutdown() {
|
||||
keepRunning = false;
|
||||
datagramSocket.close();
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the state of the event server
|
||||
*/
|
||||
public void reset() {
|
||||
packet = null;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,224 @@
|
|||
/*
|
||||
* 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;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import okhttp3.internal.Util;
|
||||
|
||||
import org.xbmc.kore.utils.LogUtils;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import java.io.PrintWriter;
|
||||
import java.net.InetAddress;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.ServerSocket;
|
||||
import java.net.Socket;
|
||||
import java.util.Collections;
|
||||
import java.util.Iterator;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import javax.net.ServerSocketFactory;
|
||||
|
||||
public class MockTcpServer {
|
||||
public static final String TAG = LogUtils.makeLogTag(MockTcpServer.class);
|
||||
|
||||
private final ServerSocketFactory serverSocketFactory = ServerSocketFactory.getDefault();
|
||||
private ServerSocket serverSocket;
|
||||
private boolean running;
|
||||
private ExecutorService executor;
|
||||
private int port = -1;
|
||||
private InetSocketAddress inetSocketAddress;
|
||||
|
||||
private final Set<Socket> openClientSockets =
|
||||
Collections.newSetFromMap(new ConcurrentHashMap<>());
|
||||
|
||||
private final TcpServerConnectionHandler connectionHandler;
|
||||
|
||||
// TODO
|
||||
// Enhance handler to handle multiple connections simultaneously. It can now handle one
|
||||
// connection at a time, which makes the current setup of the MockTcpServer (with threading)
|
||||
// overkill.
|
||||
public interface TcpServerConnectionHandler {
|
||||
/**
|
||||
* Processes received input
|
||||
*/
|
||||
void processInput(Socket socket);
|
||||
|
||||
/**
|
||||
* Gets the answer for this handler that should be returned to the server after input has been
|
||||
* processed successfully
|
||||
* @return answer or null if no answer is available
|
||||
*/
|
||||
String getResponse();
|
||||
}
|
||||
|
||||
public MockTcpServer(TcpServerConnectionHandler handler) {
|
||||
connectionHandler = handler;
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts the server on localhost on a random free port
|
||||
*/
|
||||
public void start() throws IOException {
|
||||
start(new InetSocketAddress(InetAddress.getByName("localhost"), 0));
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param inetSocketAddress set portnumber to 0 to select a random free port
|
||||
*/
|
||||
public void start(InetSocketAddress inetSocketAddress) throws IOException {
|
||||
if (running) throw new IllegalStateException("start() already called");
|
||||
running = true;
|
||||
this.inetSocketAddress = inetSocketAddress;
|
||||
|
||||
serverSocket = serverSocketFactory.createServerSocket();
|
||||
// Reuse port if not using a random port
|
||||
serverSocket.setReuseAddress(inetSocketAddress.getPort() != 0);
|
||||
serverSocket.bind(inetSocketAddress, 50);
|
||||
|
||||
executor = Executors.newCachedThreadPool(Util.threadFactory("MockTcpServer", false));
|
||||
|
||||
port = serverSocket.getLocalPort();
|
||||
|
||||
LogUtils.LOGD(TAG, "start: server started on " + serverSocket.getInetAddress() + ":" + port);
|
||||
|
||||
executor.execute(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
while (running) {
|
||||
try {
|
||||
Socket socket = acceptConnection();
|
||||
serveConnection(socket);
|
||||
} catch(IOException e){
|
||||
//Socket closed
|
||||
LogUtils.LOGD(TAG, "acceptConnection: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Socket acceptConnection() throws IOException {
|
||||
Socket socket = serverSocket.accept();
|
||||
|
||||
synchronized (openClientSockets) {
|
||||
openClientSockets.add(socket);
|
||||
}
|
||||
|
||||
return socket;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public synchronized void shutdown() throws IOException {
|
||||
if (!running) return;
|
||||
|
||||
if (serverSocket == null) throw new IllegalStateException("shutdown() before start()");
|
||||
|
||||
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 {
|
||||
if (!executor.awaitTermination(5, TimeUnit.SECONDS)) {
|
||||
throw new IOException("Gave up waiting for executor to shut down");
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
LogUtils.LOGD(TAG, "shutdown: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the local port of this server socket or -1 if it is not bound
|
||||
* @return the local port this server is listening on.
|
||||
*/
|
||||
public int getPort() {
|
||||
return port;
|
||||
}
|
||||
|
||||
public String getHostName() {
|
||||
if ( inetSocketAddress == null )
|
||||
throw new RuntimeException("Must start server before getting hostname");
|
||||
|
||||
return inetSocketAddress.getHostName();
|
||||
}
|
||||
|
||||
private void serveConnection(final Socket socket) {
|
||||
executor.execute(() -> {
|
||||
try {
|
||||
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);
|
||||
}
|
||||
});
|
||||
|
||||
executor.execute(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
while ( ! (serverSocket.isClosed() || socket.isClosed()) ) {
|
||||
sendResponse();
|
||||
Thread.sleep(100);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
LogUtils.LOGW(TAG, " sending response from " + socket.getInetAddress() + " failed: " + e);
|
||||
} catch (InterruptedException e) {
|
||||
LogUtils.LOGW(TAG, " wait interrupted" + e);
|
||||
}
|
||||
}
|
||||
|
||||
private void sendResponse() throws IOException {
|
||||
PrintWriter out =
|
||||
new PrintWriter(socket.getOutputStream(), false);
|
||||
String answer = connectionHandler.getResponse();
|
||||
if (answer != null) {
|
||||
LogUtils.LOGD(TAG, "serveConnection: sendResponse: " +answer);
|
||||
out.print(answer);
|
||||
out.flush();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String toString() {
|
||||
return "MockTcpServer[" + port + "]";
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,134 @@
|
|||
/*
|
||||
* 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.testutils.tcpserver.handlers.jsonrpc.JsonResponse;
|
||||
import org.xbmc.kore.testutils.tcpserver.handlers.jsonrpc.response.methods.Application;
|
||||
import org.xbmc.kore.utils.LogUtils;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
import static org.xbmc.kore.testutils.tcpserver.handlers.jsonrpc.response.notifications.Application.OnVolumeChanged;
|
||||
|
||||
/**
|
||||
* Simulates Application JSON-RPC API
|
||||
*/
|
||||
public class ApplicationHandler extends ConnectionHandler {
|
||||
private static final String TAG = LogUtils.makeLogTag(ApplicationHandler.class);
|
||||
|
||||
private boolean muted;
|
||||
private int volume;
|
||||
private static final String ID_NODE = "id";
|
||||
private static final String PARAMS_NODE = "params";
|
||||
private static final String PROPERTIES_NODE = "properties";
|
||||
|
||||
/**
|
||||
* Sets the muted state and sends a notification
|
||||
* @param muted
|
||||
* @param notify true if OnVolumeChanged should be sent, false otherwise
|
||||
*/
|
||||
public void setMuted(boolean muted, boolean notify) {
|
||||
this.muted = muted;
|
||||
|
||||
if (notify)
|
||||
addNotification(new OnVolumeChanged(muted, volume));
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the volume and sends a notification
|
||||
* @param volume
|
||||
* @param notify true if OnVolumeChanged should be sent, false otherwise
|
||||
*/
|
||||
public void setVolume(int volume, boolean notify) {
|
||||
this.volume = volume;
|
||||
|
||||
if (notify)
|
||||
addNotification(new OnVolumeChanged(muted, volume));
|
||||
}
|
||||
|
||||
public int getVolume() {
|
||||
return volume;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reset() {
|
||||
super.reset();
|
||||
this.volume = 0;
|
||||
this.muted = false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String[] getType() {
|
||||
return new String[]{Application.GetProperties.METHOD_NAME,
|
||||
Application.SetMute.METHOD_NAME,
|
||||
Application.SetVolume.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 Application.GetProperties.METHOD_NAME:
|
||||
Application.GetProperties response = new Application.GetProperties(methodId);
|
||||
|
||||
JsonNode jsonNode = jsonRequest.get(PARAMS_NODE).get(PROPERTIES_NODE);
|
||||
for (JsonNode node : jsonNode) {
|
||||
switch(node.asText()) {
|
||||
case Application.GetProperties.MUTED:
|
||||
response.addMuteState(muted);
|
||||
break;
|
||||
case Application.GetProperties.VOLUME:
|
||||
response.addVolume(volume);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
jsonResponses.add(response);
|
||||
break;
|
||||
case Application.SetMute.METHOD_NAME:
|
||||
setMuted(!muted, true);
|
||||
jsonResponses.add(new Application.SetMute(methodId, muted));
|
||||
break;
|
||||
case Application.SetVolume.METHOD_NAME:
|
||||
JsonNode params = jsonRequest.get(PARAMS_NODE);
|
||||
String value = params.get("volume").asText();
|
||||
switch (value) {
|
||||
case GlobalType.IncrementDecrement.INCREMENT:
|
||||
setVolume(volume + 1, true);
|
||||
break;
|
||||
case GlobalType.IncrementDecrement.DECREMENT:
|
||||
setVolume(volume - 1, true);
|
||||
break;
|
||||
default:
|
||||
setVolume(Integer.parseInt(value), true);
|
||||
break;
|
||||
}
|
||||
jsonResponses.add(new Application.SetVolume(methodId, volume));
|
||||
break;
|
||||
default:
|
||||
LogUtils.LOGD(TAG, "method: " + method + ", not implemented");
|
||||
}
|
||||
return jsonResponses;
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,89 @@
|
|||
/*
|
||||
* 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.testutils.tcpserver.handlers;
|
||||
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
|
||||
import org.xbmc.kore.jsonrpc.method.Input;
|
||||
import org.xbmc.kore.testutils.tcpserver.handlers.jsonrpc.JsonResponse;
|
||||
import org.xbmc.kore.utils.LogUtils;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
/**
|
||||
* Simulates Input JSON-RPC API
|
||||
*/
|
||||
public class InputHandler extends ConnectionHandler {
|
||||
private static final String TAG = LogUtils.makeLogTag(InputHandler.class);
|
||||
|
||||
private static final String ACTION = "action";
|
||||
private static final String PARAMS_NODE = "params";
|
||||
|
||||
private String action;
|
||||
private String methodName;
|
||||
|
||||
@Override
|
||||
public String[] getType() {
|
||||
return new String[]{Input.ExecuteAction.METHOD_NAME,
|
||||
Input.Back.METHOD_NAME,
|
||||
Input.Up.METHOD_NAME,
|
||||
Input.Down.METHOD_NAME,
|
||||
Input.Left.METHOD_NAME,
|
||||
Input.Right.METHOD_NAME,
|
||||
Input.Select.METHOD_NAME,
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public ArrayList<JsonResponse> createResponse(String method, ObjectNode jsonRequest) {
|
||||
ArrayList<JsonResponse> jsonResponses = new ArrayList<>();
|
||||
|
||||
methodName = method;
|
||||
|
||||
switch (method) {
|
||||
case Input.ExecuteAction.METHOD_NAME:
|
||||
action = jsonRequest.get(PARAMS_NODE).get(ACTION).asText();
|
||||
break;
|
||||
case Input.Left.METHOD_NAME:
|
||||
case Input.Right.METHOD_NAME:
|
||||
case Input.Up.METHOD_NAME:
|
||||
case Input.Down.METHOD_NAME:
|
||||
case Input.Select.METHOD_NAME:
|
||||
// These inputs do not have an action
|
||||
break;
|
||||
default:
|
||||
LogUtils.LOGD(TAG, "method: " + method + ", not implemented");
|
||||
}
|
||||
return jsonResponses;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the last received action
|
||||
* @return
|
||||
*/
|
||||
public String getAction() {
|
||||
return action;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the last received method name
|
||||
* @return
|
||||
*/
|
||||
public String getMethodName() {
|
||||
return methodName;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,257 @@
|
|||
/*
|
||||
* 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.core.JsonParser;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
|
||||
import org.xbmc.kore.testutils.tcpserver.MockTcpServer;
|
||||
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.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
|
||||
import static org.xbmc.kore.jsonrpc.ApiMethod.ID_NODE;
|
||||
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 final HashMap<String, ConnectionHandler> handlersByType = new HashMap<>();
|
||||
private int amountOfOpenBrackets = 0;
|
||||
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||
|
||||
//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 final HashMap<String, MethodPendingState> methodIdsHandled = new HashMap<>();
|
||||
private final HashSet<String> notificationsHandled = new HashSet<>();
|
||||
|
||||
public void addHandler(ConnectionHandler handler) {
|
||||
synchronized (handlersByType) {
|
||||
for (String type : handler.getType()) {
|
||||
handlersByType.put(type, handler);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
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--;
|
||||
}
|
||||
|
||||
return amountOfOpenBrackets == 0;
|
||||
}
|
||||
|
||||
private void processJSONInput(String input) {
|
||||
try {
|
||||
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();
|
||||
|
||||
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.LOGD(TAG, "processJSONInput: error parsing: " + input);
|
||||
LogUtils.LOGE(TAG, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getResponse() {
|
||||
StringBuilder stringBuilder = new StringBuilder();
|
||||
|
||||
//Handle client responses
|
||||
synchronized (clientResponses) {
|
||||
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();
|
||||
}
|
||||
|
||||
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) {
|
||||
try {
|
||||
notificationsHandled.add(jsonResponse.getMethod());
|
||||
stringBuilder.append(jsonResponse.toJsonString()).append("\n");
|
||||
} catch (Exception e) {
|
||||
LogUtils.LOGD(TAG, "getResponse: Error handling notification: " + jsonResponse.toJsonString());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
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 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)
|
||||
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) {
|
||||
|
||||
}
|
||||
|
||||
private boolean isMethodHandled(String methodName) {
|
||||
for(MethodPendingState methodPending : methodIdsHandled.values()) {
|
||||
if (methodName.contentEquals(methodPending.name)) {
|
||||
return methodPending.handled;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private void setMethodHandled(String methodId) {
|
||||
|
||||
}
|
||||
|
||||
private static class MethodPendingState {
|
||||
boolean handled;
|
||||
String name;
|
||||
|
||||
MethodPendingState(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
/*
|
||||
* 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 extends ConnectionHandler {
|
||||
|
||||
@Override
|
||||
public String[] getType() {
|
||||
return new String[] {JSONRPC.Ping.METHOD_NAME};
|
||||
}
|
||||
|
||||
@Override
|
||||
public ArrayList<JsonResponse> createResponse(String method, ObjectNode jsonRequest) {
|
||||
ArrayList<JsonResponse> jsonResponses = new ArrayList<>();
|
||||
jsonResponses.add(new Ping(jsonRequest.get("id").asInt()));
|
||||
return jsonResponses;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,361 @@
|
|||
/*
|
||||
* 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.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.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 extends ConnectionHandler {
|
||||
private static final String TAG = LogUtils.makeLogTag(PlayerHandler.class);
|
||||
|
||||
public static String[] repeatModes = {
|
||||
"off",
|
||||
"one",
|
||||
"all"
|
||||
};
|
||||
|
||||
public enum PLAY_STATE {PLAYING, STOPPED, PAUSED}
|
||||
private PLAY_STATE playState = STOPPED;
|
||||
private int currentRepeatMode;
|
||||
private boolean shuffled;
|
||||
private int elapsedTime;
|
||||
|
||||
private Player.GetItem mediaItem;
|
||||
private List<PlaylistHolder> playlists = new ArrayList<>();
|
||||
private Playlist.playlistID activePlaylistId = Playlist.playlistID.AUDIO;
|
||||
private String playerType = PlayerType.GetActivePlayersReturnType.AUDIO;
|
||||
|
||||
@Override
|
||||
public void reset() {
|
||||
super.reset();
|
||||
this.shuffled = false;
|
||||
this.currentRepeatMode = 0;
|
||||
this.elapsedTime = 0;
|
||||
this.playState = STOPPED;
|
||||
playerType = PlayerType.GetActivePlayersReturnType.AUDIO;
|
||||
playlists = null;
|
||||
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.Stop.METHOD_NAME,
|
||||
Player.Open.METHOD_NAME};
|
||||
}
|
||||
|
||||
@Override
|
||||
public ArrayList<JsonResponse> createResponse(String method, ObjectNode jsonRequest) {
|
||||
ArrayList<JsonResponse> jsonResponses = new ArrayList<>();
|
||||
JsonResponse response = null;
|
||||
|
||||
int methodId = jsonRequest.get("id").asInt();
|
||||
|
||||
switch (method) {
|
||||
case Player.GetActivePlayers.METHOD_NAME:
|
||||
response = handleGetActivePlayers(methodId);
|
||||
break;
|
||||
case Player.GetProperties.METHOD_NAME:
|
||||
response = updatePlayerProperties(createPlayerProperties(methodId));
|
||||
break;
|
||||
case Player.GetItem.METHOD_NAME:
|
||||
response = handleGetItem(methodId);
|
||||
break;
|
||||
case Player.SetRepeat.METHOD_NAME:
|
||||
response = handleSetRepeat(methodId, jsonRequest);
|
||||
break;
|
||||
case Player.SetShuffle.METHOD_NAME:
|
||||
response = handleSetShuffle(methodId, jsonRequest);
|
||||
break;
|
||||
case Player.Open.METHOD_NAME:
|
||||
response = handleOpen(methodId, jsonRequest);
|
||||
break;
|
||||
case Player.PlayPause.METHOD_NAME:
|
||||
response = handlePlayPause(methodId, jsonRequest);
|
||||
break;
|
||||
case Player.Seek.METHOD_NAME:
|
||||
response = handleSeek(methodId, jsonRequest);
|
||||
break;
|
||||
case Player.Stop.METHOD_NAME:
|
||||
handleStop();
|
||||
break;
|
||||
default:
|
||||
LogUtils.LOGD(TAG, "getResponse: unknown method received: "+method);
|
||||
}
|
||||
|
||||
if (response != null)
|
||||
jsonResponses.add(response);
|
||||
|
||||
return jsonResponses;
|
||||
}
|
||||
|
||||
private void setMediaType(Player.GetItem.TYPE mediaType) {
|
||||
switch (mediaType) {
|
||||
case movie:
|
||||
playerType = PlayerType.GetActivePlayersReturnType.VIDEO;
|
||||
break;
|
||||
case song:
|
||||
playerType = PlayerType.GetActivePlayersReturnType.AUDIO;
|
||||
break;
|
||||
case unknown:
|
||||
playerType = PlayerType.GetActivePlayersReturnType.AUDIO;
|
||||
break;
|
||||
case musicvideo:
|
||||
playerType = PlayerType.GetActivePlayersReturnType.VIDEO;
|
||||
break;
|
||||
case picture:
|
||||
playerType = PlayerType.GetActivePlayersReturnType.PICTURE;
|
||||
break;
|
||||
case channel:
|
||||
playerType = PlayerType.GetActivePlayersReturnType.VIDEO;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts playing current item in the playlist
|
||||
*/
|
||||
public void startPlay() {
|
||||
if (playlists != null && 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) {
|
||||
if (playlists == null) return;
|
||||
|
||||
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(Player.GetItem.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 getTimeElapsed() {
|
||||
return elapsedTime;
|
||||
}
|
||||
|
||||
public PLAY_STATE getPlayState() {
|
||||
return playState;
|
||||
}
|
||||
|
||||
private String getMediaItemType() {
|
||||
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 1;
|
||||
}
|
||||
}
|
||||
|
||||
private Player.GetProperties updatePlayerProperties(Player.GetProperties playerProperties) {
|
||||
if (playState == PLAYING)
|
||||
elapsedTime++;
|
||||
|
||||
if ( mediaItem != null ) {
|
||||
if ( elapsedTime > mediaItem.getDuration() && currentRepeatMode != 0 ) {
|
||||
elapsedTime = 0;
|
||||
}
|
||||
|
||||
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(activePlaylistId.ordinal());
|
||||
properties.addRepeat(repeatModes[currentRepeatMode]);
|
||||
properties.addShuffled(false);
|
||||
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 JsonResponse handleGetItem(int methodId) {
|
||||
if (playlists != null && playlists.size() > 0) {
|
||||
mediaItem = playlists.get(activePlaylistId.ordinal()).getCurrentItem();
|
||||
}
|
||||
|
||||
try {
|
||||
mediaItem = new Player.GetItem(methodId, mediaItem.toJsonString());
|
||||
} catch (IOException e) {
|
||||
LogUtils.LOGE(TAG, "handleGetItem: Error creating new Player.GetItem object");
|
||||
}
|
||||
return mediaItem;
|
||||
}
|
||||
|
||||
private JsonResponse handleGetActivePlayers(int methodId) {
|
||||
if (playState == STOPPED) {
|
||||
return new Player.GetActivePlayers(methodId);
|
||||
} else {
|
||||
return new Player.GetActivePlayers(methodId, getPlayerId(), playerType);
|
||||
}
|
||||
}
|
||||
|
||||
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 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 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();
|
||||
|
||||
startPlay(Playlist.playlistID.values()[playlistId], playlistIndex);
|
||||
|
||||
return new Player.Open(methodId);
|
||||
}
|
||||
|
||||
private JsonResponse handlePlayPause(int methodId, ObjectNode jsonRequest) {
|
||||
playState = playState == PLAYING ? PAUSED : PLAYING; //toggle playstate
|
||||
|
||||
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 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();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,133 @@
|
|||
/*
|
||||
* 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, boolean sentNotification) {
|
||||
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);
|
||||
|
||||
if (sentNotification) {
|
||||
OnAdd onAddNotification = new OnAdd(item.getLibraryId(), item.getType(), playlistId, playlist.getIndexOf(item));
|
||||
addNotification(onAddNotification);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,258 @@
|
|||
/*
|
||||
* 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;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.node.ArrayNode;
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
|
||||
import org.xbmc.kore.utils.LogUtils;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public abstract class JsonResponse {
|
||||
private final String TAG = LogUtils.makeLogTag(JsonResponse.class);
|
||||
|
||||
private final ObjectNode jsonResponse;
|
||||
|
||||
private static final ObjectMapper objectMapper = new ObjectMapper();
|
||||
|
||||
private static final String RESULT_NODE = "result";
|
||||
private static final String PARAMS_NODE = "params";
|
||||
private static final String METHOD_NODE = "method";
|
||||
private static final String DATA_NODE = "data";
|
||||
protected static final String ID_NODE = "id";
|
||||
private static final String JSONRPC_NODE = "jsonrpc";
|
||||
|
||||
public enum TYPE {
|
||||
OBJECT,
|
||||
ARRAY
|
||||
};
|
||||
|
||||
public JsonResponse() {
|
||||
jsonResponse = objectMapper.createObjectNode();
|
||||
jsonResponse.put(JSONRPC_NODE, "2.0");
|
||||
}
|
||||
|
||||
public JsonResponse(int id) {
|
||||
this();
|
||||
jsonResponse.put(ID_NODE, id);
|
||||
}
|
||||
|
||||
public JsonResponse(int id, String jsonString) throws IOException {
|
||||
jsonResponse = (ObjectNode) objectMapper.readTree(jsonString);
|
||||
jsonResponse.put(JSONRPC_NODE, "2.0");
|
||||
jsonResponse.put(ID_NODE, id);
|
||||
}
|
||||
|
||||
protected ObjectNode createObjectNode() {
|
||||
return objectMapper.createObjectNode();
|
||||
}
|
||||
|
||||
protected ArrayNode createArrayNode() {
|
||||
return objectMapper.createArrayNode();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the node used to hold the result. First call will create the
|
||||
* result node for the given type
|
||||
* @param type that result node should be when first created
|
||||
* @return result node
|
||||
*/
|
||||
protected JsonNode getResultNode(TYPE type) {
|
||||
JsonNode result;
|
||||
if(jsonResponse.has(RESULT_NODE)) {
|
||||
result = jsonResponse.get(RESULT_NODE);
|
||||
if( result.isArray() && type != TYPE.ARRAY ) {
|
||||
LogUtils.LOGE(TAG, "requested result node of type Object but response contains result node of type Array");
|
||||
return null;
|
||||
}
|
||||
} else {
|
||||
switch (type) {
|
||||
case ARRAY:
|
||||
result = objectMapper.createArrayNode();
|
||||
break;
|
||||
case OBJECT:
|
||||
default:
|
||||
result = objectMapper.createObjectNode();
|
||||
break;
|
||||
}
|
||||
jsonResponse.set(RESULT_NODE, result);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the parameters node of the json request object
|
||||
* Creates one if necessary
|
||||
* @return Parameters node
|
||||
*/
|
||||
private ObjectNode getParametersNode() {
|
||||
ObjectNode params;
|
||||
if (jsonResponse.has(PARAMS_NODE)) {
|
||||
params = (ObjectNode)jsonResponse.get(PARAMS_NODE);
|
||||
} else {
|
||||
params = objectMapper.createObjectNode();
|
||||
jsonResponse.set(PARAMS_NODE, params);
|
||||
}
|
||||
|
||||
return params;
|
||||
}
|
||||
|
||||
private ObjectNode getDataNode() {
|
||||
ObjectNode data = null;
|
||||
if (jsonResponse.has(PARAMS_NODE)) {
|
||||
ObjectNode params = (ObjectNode)jsonResponse.get(PARAMS_NODE);
|
||||
if(params.has(DATA_NODE)) {
|
||||
data = (ObjectNode) params.get(DATA_NODE);
|
||||
}
|
||||
}
|
||||
|
||||
if ( data == null ) {
|
||||
data = objectMapper.createObjectNode();
|
||||
ObjectNode params = getParametersNode();
|
||||
params.set(DATA_NODE, data);
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
protected void setResultToResponse(JsonNode value) {
|
||||
jsonResponse.set(RESULT_NODE, value);
|
||||
}
|
||||
|
||||
protected void setResultToResponse(boolean value) {
|
||||
jsonResponse.put(RESULT_NODE, value);
|
||||
}
|
||||
|
||||
protected void setResultToResponse(int value) {
|
||||
jsonResponse.put(RESULT_NODE, value);
|
||||
}
|
||||
|
||||
protected void setResultToResponse(String value) {
|
||||
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
|
||||
* 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, String 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." );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
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 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);
|
||||
}
|
||||
|
||||
protected void addDataToResponse(String parameter, int value) {
|
||||
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);
|
||||
}
|
||||
|
||||
protected void addMethodToResponse(String method) {
|
||||
jsonResponse.put(METHOD_NODE, method);
|
||||
}
|
||||
|
||||
public ObjectNode getResponseNode() {
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
/*
|
||||
* 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.testutils.tcpserver.handlers.jsonrpc.response.methods;
|
||||
|
||||
import org.xbmc.kore.testutils.tcpserver.handlers.jsonrpc.JsonResponse;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* Serverside JSON RPC responses in Addons.*
|
||||
*/
|
||||
public class Addons {
|
||||
|
||||
/**
|
||||
* JSON response for Addons.GetAddons request
|
||||
*
|
||||
* @return JSON string
|
||||
*/
|
||||
public static class GetAddons extends JsonResponse {
|
||||
public final static String METHOD_NAME = "Addons.GetAddons";
|
||||
|
||||
public GetAddons(int id, String jsonString) throws IOException {
|
||||
super(id, jsonString);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,99 @@
|
|||
/*
|
||||
* 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.node.ObjectNode;
|
||||
|
||||
import org.xbmc.kore.testutils.tcpserver.handlers.jsonrpc.JsonResponse;
|
||||
|
||||
/**
|
||||
* Serverside JSON RPC responses in Application.*
|
||||
*/
|
||||
public class Application {
|
||||
|
||||
/**
|
||||
* JSON response for Application.SetMute request
|
||||
*
|
||||
* Example:
|
||||
* Query: {"jsonrpc":"2.0","method":"Application.SetMute","id":1,"params":{"mute":"toggle"}}
|
||||
* Answer: muted: {"id":1,"jsonrpc":"2.0","result":false}
|
||||
* not muted: {"id":1,"jsonrpc":"2.0","result":true}
|
||||
*
|
||||
* @return JSON string
|
||||
*/
|
||||
public static class SetMute extends JsonResponse {
|
||||
public final static String METHOD_NAME = "Application.SetMute";
|
||||
|
||||
public SetMute(int id, boolean muteState) {
|
||||
super(id);
|
||||
setResultToResponse(muteState);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* JSON response for GetProperties requests
|
||||
*
|
||||
* Example:
|
||||
* Query: {"jsonrpc":"2.0","method":"Application.GetProperties","id":1,"params":{"properties":["muted"]}}
|
||||
* Answer: {"id":1,"jsonrpc":"2.0","result":{"muted":true}}
|
||||
*
|
||||
* @return JSON string
|
||||
*/
|
||||
public static class GetProperties extends JsonResponse {
|
||||
public final static String METHOD_NAME = "Application.GetProperties";
|
||||
|
||||
public final static String MUTED = "muted";
|
||||
public final static String VOLUME = "volume";
|
||||
|
||||
private ObjectNode node = null;
|
||||
|
||||
public GetProperties(int id) {
|
||||
super(id);
|
||||
}
|
||||
|
||||
public void addMuteState(boolean muteState) {
|
||||
node = (ObjectNode) getResultNode(TYPE.OBJECT);
|
||||
node.put(MUTED, muteState);
|
||||
}
|
||||
|
||||
public void addVolume(int volume) {
|
||||
node = (ObjectNode) getResultNode(TYPE.OBJECT);
|
||||
node.put(VOLUME, volume);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* JSON response for Application.SetVolume request
|
||||
*
|
||||
* Examples:
|
||||
* Query: {"jsonrpc":"2.0","method":"Application.SetVolume","id":1,"params":{"volume":100}}
|
||||
* Answer: {"id":1,"jsonrpc":"2.0","result":100}
|
||||
*
|
||||
* Query: {"jsonrpc":"2.0","method":"Application.SetVolume","id":1,"params":{"volume":"decrement"}}
|
||||
* Answer: {"id":1,"jsonrpc":"2.0","result":99}
|
||||
*
|
||||
* @return JSON string
|
||||
*/
|
||||
public static class SetVolume extends JsonResponse {
|
||||
public final static String METHOD_NAME = "Application.SetVolume";
|
||||
|
||||
public SetVolume(int id, int volume) {
|
||||
super(id);
|
||||
setResultToResponse(volume);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,526 @@
|
|||
/*
|
||||
* 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.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;
|
||||
|
||||
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
|
||||
*
|
||||
* 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 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();
|
||||
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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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";
|
||||
|
||||
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();
|
||||
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 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) {
|
||||
itemNode.put(TYPE, type.name());
|
||||
}
|
||||
|
||||
public String getType() {
|
||||
return itemNode.get(TYPE).textValue();
|
||||
}
|
||||
|
||||
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 int getDuration() {
|
||||
return itemNode.get(DURATION).asInt();
|
||||
}
|
||||
|
||||
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 int getRuntime() {
|
||||
return itemNode.get(RUNTIME).asInt();
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
/*
|
||||
* 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 org.xbmc.kore.testutils.tcpserver.handlers.jsonrpc.JsonResponse;
|
||||
|
||||
public class Application {
|
||||
/**
|
||||
* JSON response for Application.OnVolumeChanged notification
|
||||
*
|
||||
* Example:
|
||||
* Answer: {"jsonrpc":"2.0","method":"Application.OnVolumeChanged","params":{"data":{"muted":false,"volume":100},"sender":"xbmc"}}
|
||||
*
|
||||
* @return JSON string
|
||||
*/
|
||||
public static class OnVolumeChanged extends JsonResponse {
|
||||
public final static String METHOD_NAME = "Application.OnVolumeChanged";
|
||||
|
||||
public OnVolumeChanged(boolean muteState, int volume) {
|
||||
super();
|
||||
addMethodToResponse(METHOD_NAME);
|
||||
addDataToResponse("volume", volume);
|
||||
addDataToResponse("muted", muteState);
|
||||
addParameterToResponse("sender", "xbmc");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,190 @@
|
|||
/*
|
||||
* 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 {
|
||||
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.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
|
||||
*
|
||||
* 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.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" }}
|
||||
*/
|
||||
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");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
224
app/src/main/AndroidManifest.xml
Normal file
224
app/src/main/AndroidManifest.xml
Normal file
|
|
@ -0,0 +1,224 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<!-- Permissions -->
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
|
||||
<uses-permission android:name="android.permission.CHANGE_WIFI_MULTICAST_STATE"/>
|
||||
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
|
||||
<uses-permission android:name="android.permission.VIBRATE"/>
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />
|
||||
<!--
|
||||
Added notification permission so that code in MediaSessionService doesn't show errors.
|
||||
This is not really necessary, given that notifications are sent via MediaService, and those
|
||||
are exempt from being explicitly requested but it doesn't hurt
|
||||
-->
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
|
||||
<!-- Dangerous permissions -->
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="32"/>
|
||||
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO"/>
|
||||
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES"/>
|
||||
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO"/>
|
||||
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="29"/>
|
||||
<uses-permission android:name="android.permission.READ_PHONE_STATE"/>
|
||||
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:requestLegacyExternalStorage="true"
|
||||
android:supportsRtl="true"
|
||||
android:networkSecurityConfig="@xml/network_security_config">
|
||||
|
||||
<!-- Activities -->
|
||||
<activity
|
||||
android:name=".ui.sections.remote.RemoteActivity"
|
||||
android:exported="true">
|
||||
|
||||
<!-- Main intent filter to open this activity from the launcher -->
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
<category android:name="android.intent.category.MULTIWINDOW_LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<!-- Auxiliary activity to handle "Play on Kodi" share intents -->
|
||||
<activity
|
||||
android:name=".ShareOpenActivity"
|
||||
android:exported="true"
|
||||
android:theme="@android:style/Theme.Translucent.NoTitleBar">
|
||||
|
||||
<!-- Send image/video/audio directly to Kodi -->
|
||||
<intent-filter android:label="@string/play_on_kodi">
|
||||
<action android:name="android.intent.action.SEND" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<data android:mimeType="image/*" />
|
||||
<data android:mimeType="video/*" />
|
||||
<data android:mimeType="audio/*" />
|
||||
</intent-filter>
|
||||
|
||||
<!--
|
||||
Open plain text URLs (eg, from youtube player, chrome when on a supported site, etc).
|
||||
This is a very generic filter, but it's the only way to get sharing requests from the youtube app
|
||||
-->
|
||||
<intent-filter android:label="@string/play_on_kodi">
|
||||
<action android:name="android.intent.action.SEND" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<data android:mimeType="text/plain" />
|
||||
</intent-filter>
|
||||
|
||||
<!-- Open URLs of image/video/audio -->
|
||||
<intent-filter android:label="@string/play_on_kodi">
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data android:scheme="http" />
|
||||
<data android:scheme="https" />
|
||||
<data android:mimeType="video/*" />
|
||||
<data android:mimeType="audio/*" />
|
||||
<data android:mimeType="application/vnd.apple.mpegurl" />
|
||||
</intent-filter>
|
||||
|
||||
<!-- Open supported URLs - youtube, vimeo, svtplay, etc. -->
|
||||
<intent-filter android:label="@string/play_on_kodi">
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data android:scheme="http" />
|
||||
<data android:scheme="https" />
|
||||
<data android:host="youtu.be" />
|
||||
<data android:host="youtube.com" />
|
||||
<data android:host="m.youtube.com" />
|
||||
<data android:host="www.youtube.com" />
|
||||
<data android:host="vimeo.com" />
|
||||
<data android:host="www.vimeo.com" />
|
||||
<data android:host="player.vimeo.com" />
|
||||
<data android:host="www.svtplay.se" />
|
||||
<data android:host="soundcloud.com" />
|
||||
<data android:host="m.soundcloud.com" />
|
||||
<data android:host="www.arte.tv" />
|
||||
<data android:host="twitch.tv" />
|
||||
<data android:host="m.twitch.tv" />
|
||||
</intent-filter>
|
||||
|
||||
<!-- To provide backwards compatibility with the old DirectShare API -->
|
||||
<meta-data
|
||||
android:name="android.service.chooser.chooser_target_service"
|
||||
android:value="androidx.sharetarget.ChooserTargetServiceCompat" />
|
||||
<meta-data
|
||||
android:name="android.app.shortcuts"
|
||||
android:resource="@xml/shortcuts" />
|
||||
</activity>
|
||||
|
||||
<!-- Auxiliary activity to handle "Queue on Kodi" share intents -->
|
||||
<activity
|
||||
android:name=".ShareQueueActivity"
|
||||
android:exported="true"
|
||||
android:theme="@android:style/Theme.Translucent.NoTitleBar">
|
||||
|
||||
<!-- See intent filters on Share Open Activity, as these are similar -->
|
||||
<intent-filter android:label="@string/queue_on_kodi">
|
||||
<action android:name="android.intent.action.SEND" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<data android:mimeType="image/*" />
|
||||
<data android:mimeType="video/*" />
|
||||
<data android:mimeType="audio/*" />
|
||||
</intent-filter>
|
||||
|
||||
<intent-filter android:label="@string/queue_on_kodi">
|
||||
<action android:name="android.intent.action.SEND" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<data android:mimeType="text/plain" />
|
||||
</intent-filter>
|
||||
|
||||
<intent-filter android:label="@string/queue_on_kodi">
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data android:scheme="http" />
|
||||
<data android:scheme="https" />
|
||||
<data android:mimeType="video/*" />
|
||||
<data android:mimeType="audio/*" />
|
||||
<data android:mimeType="application/vnd.apple.mpegurl" />
|
||||
</intent-filter>
|
||||
|
||||
<intent-filter android:label="@string/queue_on_kodi">
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data android:scheme="http" />
|
||||
<data android:scheme="https" />
|
||||
<data android:host="youtu.be" />
|
||||
<data android:host="youtube.com" />
|
||||
<data android:host="m.youtube.com" />
|
||||
<data android:host="www.youtube.com" />
|
||||
<data android:host="vimeo.com" />
|
||||
<data android:host="www.vimeo.com" />
|
||||
<data android:host="player.vimeo.com" />
|
||||
<data android:host="www.svtplay.se" />
|
||||
<data android:host="soundcloud.com" />
|
||||
<data android:host="m.soundcloud.com" />
|
||||
<data android:host="www.arte.tv" />
|
||||
<data android:host="twitch.tv" />
|
||||
<data android:host="m.twitch.tv" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity android:name=".ui.sections.hosts.HostManagerActivity" />
|
||||
<activity android:name=".ui.sections.hosts.AddHostActivity" />
|
||||
<activity android:name=".ui.sections.hosts.EditHostActivity" />
|
||||
<activity android:name=".ui.sections.video.MoviesActivity" />
|
||||
<activity android:name=".ui.sections.video.TVShowsActivity" />
|
||||
<activity android:name=".ui.sections.audio.MusicActivity" />
|
||||
<activity android:name=".ui.sections.addon.AddonsActivity" />
|
||||
<activity android:name=".ui.sections.settings.SettingsActivity" />
|
||||
<activity android:name=".ui.sections.file.FileActivity" />
|
||||
<activity android:name=".ui.sections.localfile.LocalFileActivity" />
|
||||
<activity android:name=".ui.sections.video.PVRActivity" />
|
||||
<activity android:name=".ui.sections.video.AllCastActivity" />
|
||||
<activity android:name=".ui.sections.favourites.FavouritesActivity" />
|
||||
|
||||
<!-- Providers -->
|
||||
<provider
|
||||
android:name=".provider.MediaProvider"
|
||||
android:authorities="org.xbmc.kore.provider"
|
||||
android:exported="false" />
|
||||
|
||||
<!-- Services -->
|
||||
<service
|
||||
android:name=".service.library.LibrarySyncService"
|
||||
android:exported="false" />
|
||||
<service
|
||||
android:name=".service.MediaSessionService"
|
||||
android:foregroundServiceType="mediaPlayback"
|
||||
android:exported="false">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MEDIA_BUTTON" />
|
||||
</intent-filter>
|
||||
</service>
|
||||
|
||||
<!-- Broadcast Receivers -->
|
||||
<receiver
|
||||
android:name="androidx.media.session.MediaButtonReceiver"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MEDIA_BUTTON" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<uses-library android:required="false" android:name="com.sec.android.app.multiwindow"/>
|
||||
<meta-data android:name="com.sec.android.support.multiwindow" android:value="true" />
|
||||
<meta-data android:name="com.sec.android.multiwindow.DEFAULT_SIZE_W" android:value="632.0dip" />
|
||||
<meta-data android:name="com.sec.android.multiwindow.DEFAULT_SIZE_H" android:value="598.0dip" />
|
||||
<meta-data android:name="com.sec.android.multiwindow.MINIMUM_SIZE_W" android:value="632.0dip" />
|
||||
<meta-data android:name="com.sec.android.multiwindow.MINIMUM_SIZE_H" android:value="598.0dip" />
|
||||
|
||||
<meta-data android:name="com.lge.support.SPLIT_WINDOW" android:value="true" />
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
275
app/src/main/java/org/xbmc/kore/Settings.java
Normal file
275
app/src/main/java/org/xbmc/kore/Settings.java
Normal file
|
|
@ -0,0 +1,275 @@
|
|||
/*
|
||||
* Copyright 2015 Synced Synapse. 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;
|
||||
|
||||
import android.app.DownloadManager;
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import androidx.preference.PreferenceManager;
|
||||
import android.text.format.DateUtils;
|
||||
|
||||
import org.xbmc.kore.utils.LogUtils;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Class that contains various constants and the keys for settings stored in shared preferences
|
||||
*/
|
||||
public class Settings {
|
||||
private static final String TAG = LogUtils.makeLogTag(Settings.class);
|
||||
|
||||
/**
|
||||
* The update interval for the records in the DB. If the last update is older than this value
|
||||
* a refresh will be triggered. Applicable to TV Shows and Movies.
|
||||
*/
|
||||
// public static final long DB_UPDATE_INTERVAL = 12 * DateUtils.HOUR_IN_MILLIS;
|
||||
public static final long DB_UPDATE_INTERVAL = 5 * DateUtils.MINUTE_IN_MILLIS;
|
||||
|
||||
// Sort orders
|
||||
public static final int SORT_BY_NAME = 0,
|
||||
SORT_BY_DATE_ADDED = 1,
|
||||
SORT_BY_RATING = 2,
|
||||
SORT_BY_YEAR = 3,
|
||||
SORT_BY_LENGTH = 4,
|
||||
SORT_BY_ALBUM = 5,
|
||||
SORT_BY_ARTIST = 6,
|
||||
SORT_BY_ARTIST_YEAR = 7,
|
||||
SORT_BY_LAST_PLAYED = 8,
|
||||
UNSORTED = 9;
|
||||
|
||||
/**
|
||||
* Preferences keys.
|
||||
* Some of these settings are automatically managed by the Preferences mechanism.
|
||||
* Make sure these are the same as in preferences.xml
|
||||
*/
|
||||
|
||||
//Theme color and variant constants, keep in sync to the defined in arrays.xml
|
||||
public static final String THEME_VARIANT_LIGHT = "light", THEME_VARIANT_DARK = "dark", THEME_VARIANT_SYSTEM = "auto";
|
||||
public static final String THEME_COLOR_KORE = "kore", THEME_COLOR_GREEN = "green",
|
||||
THEME_COLOR_YELLOW = "yellow", THEME_COLOR_PURPLE = "purple", THEME_COLOR_SYSTEM = "system_colors";
|
||||
|
||||
// Theme
|
||||
public static final String KEY_PREF_THEME_COLOR = "pref_theme_color";
|
||||
public static final String DEFAULT_PREF_THEME_COLOR = THEME_COLOR_KORE;
|
||||
|
||||
public static final String KEY_PREF_THEME_VARIANT = "pref_theme_variant";
|
||||
public static final String DEFAULT_PREF_THEME_VARIANT = THEME_VARIANT_SYSTEM;
|
||||
|
||||
// Switch to remote
|
||||
public static final String KEY_PREF_SWITCH_TO_REMOTE_AFTER_MEDIA_START = "pref_switch_to_remote_after_media_start";
|
||||
public static final boolean DEFAULT_PREF_SWITCH_TO_REMOTE_AFTER_MEDIA_START = true;
|
||||
|
||||
// Keep remote activity above lockscreen
|
||||
public static final String KEY_PREF_KEEP_REMOTE_ABOVE_LOCKSCREEN = "pref_keep_remote_above_lockscreen";
|
||||
public static final boolean DEFAULT_KEY_PREF_KEEP_REMOTE_ABOVE_LOCKSCREEN = false;
|
||||
|
||||
// Keep screen on when on the remote activity
|
||||
public static final String KEY_PREF_KEEP_SCREEN_ON = "pref_keep_screen_on";
|
||||
public static final boolean DEFAULT_KEY_PREF_KEEP_SCREEN_ON = false;
|
||||
|
||||
// Show now playing panel
|
||||
public static final String KEY_PREF_SHOW_NOW_PLAYING_PANEL = "pref_show_nowplayingpanel";
|
||||
public static final boolean DEFAULT_PREF_SHOW_NOW_PLAYING_PANEL = true;
|
||||
|
||||
// Pause during calls
|
||||
public static final String KEY_PREF_PAUSE_DURING_CALLS = "pref_pause_during_calls";
|
||||
public static final boolean DEFAULT_PREF_PAUSE_DURING_CALLS = false;
|
||||
|
||||
// Other keys used in preferences.xml
|
||||
public static final String KEY_PREF_ABOUT = "pref_about";
|
||||
|
||||
// Filter watched movies on movie list
|
||||
public static final String KEY_PREF_MOVIES_FILTER_HIDE_WATCHED = "movies_filter_hide_watched";
|
||||
public static final boolean DEFAULT_PREF_MOVIES_FILTER_HIDE_WATCHED = false;
|
||||
|
||||
// Sort order on movies
|
||||
public static final String KEY_PREF_MOVIES_SORT_ORDER = "movies_sort_order";
|
||||
public static final int DEFAULT_PREF_MOVIES_SORT_ORDER = SORT_BY_NAME;
|
||||
|
||||
// Show watched status on movie list
|
||||
public static final String KEY_PREF_MOVIES_SHOW_WATCHED_STATUS = "movies_show_watched_status";
|
||||
public static final boolean DEFAULT_PREF_MOVIES_SHOW_WATCHED_STATUS = true;
|
||||
|
||||
// Show watched status on movie list
|
||||
public static final String KEY_PREF_MOVIES_SHOW_RATING = "movies_show_rating";
|
||||
public static final boolean DEFAULT_PREF_MOVIES_SHOW_RATING = true;
|
||||
|
||||
// Sort order on albums
|
||||
public static final String KEY_PREF_ALBUMS_SORT_ORDER = "albums_sort_order";
|
||||
public static final int DEFAULT_PREF_ALBUMS_SORT_ORDER = SORT_BY_ALBUM;
|
||||
|
||||
// Ignore articles on movie sorting
|
||||
public static final String KEY_PREF_MOVIES_IGNORE_PREFIXES = "movies_ignore_prefixes";
|
||||
public static final boolean DEFAULT_PREF_MOVIES_IGNORE_PREFIXES = false;
|
||||
|
||||
// Filter watched tv shows on tvshows list
|
||||
public static final String KEY_PREF_TVSHOWS_FILTER_HIDE_WATCHED = "tvshows_filter_hide_watched";
|
||||
public static final boolean DEFAULT_PREF_TVSHOWS_FILTER_HIDE_WATCHED = false;
|
||||
|
||||
// Filter watched episodes on episodes list
|
||||
public static final String KEY_PREF_TVSHOW_EPISODES_FILTER_HIDE_WATCHED = "tvshow_episodes_filter_hide_watched";
|
||||
public static final boolean DEFAULT_PREF_TVSHOW_EPISODES_FILTER_HIDE_WATCHED = false;
|
||||
|
||||
// Sort order on tv shows
|
||||
public static final String KEY_PREF_TVSHOWS_SORT_ORDER = "tvshows_sort_order";
|
||||
public static final int DEFAULT_PREF_TVSHOWS_SORT_ORDER = SORT_BY_NAME;
|
||||
|
||||
// Ignore articles on tv show sorting
|
||||
public static final String KEY_PREF_TVSHOWS_IGNORE_PREFIXES = "tvshows_ignore_prefixes";
|
||||
public static final boolean DEFAULT_PREF_TVSHOWS_IGNORE_PREFIXES = false;
|
||||
|
||||
// Show watched status on movie list
|
||||
public static final String KEY_PREF_TVSHOWS_SHOW_WATCHED_STATUS = "tvshows_show_watched_status";
|
||||
public static final boolean DEFAULT_PREF_TVSHOWS_SHOW_WATCHED_STATUS = true;
|
||||
|
||||
// Filter watched pvr recordings on movie list
|
||||
public static final String KEY_PREF_PVR_RECORDINGS_FILTER_HIDE_WATCHED = "pvr_recordings_filter_hide_watched";
|
||||
public static final boolean DEFAULT_PREF_PVR_RECORDINGS_FILTER_HIDE_WATCHED = false;
|
||||
|
||||
// Sort order on pvr recordings
|
||||
public static final String KEY_PREF_PVR_RECORDINGS_SORT_ORDER = "pvr_recordings_sort_order";
|
||||
public static final int DEFAULT_PREF_PVR_RECORDINGS_SORT_ORDER = UNSORTED;
|
||||
|
||||
// Filter disabled addons on addons list
|
||||
public static final String KEY_PREF_ADDONS_FILTER_HIDE_DISABLED = "addons_filter_hide_disabled";
|
||||
public static final boolean DEFAULT_PREF_ADDONS_FILTER_HIDE_DISABLED = false;
|
||||
|
||||
// Use hardware volume keys to control volume
|
||||
public static final String USE_HW_VOL_KEYS_NEVER = "never", USE_HW_VOL_KEYS_ALWAYS = "always",
|
||||
USE_HW_VOL_KEYS_WHEN_IN_FOREGROUND = "when_in_foreground";
|
||||
|
||||
public static final String KEY_PREF_USE_HW_VOL_KEYS = "pref_use_hw_vol_keys";
|
||||
public static final String DEFAULT_PREF_USE_HW_VOL_KEYS = USE_HW_VOL_KEYS_NEVER;
|
||||
|
||||
// Vibrate on remote button press
|
||||
public static final String KEY_PREF_VIBRATE_REMOTE_BUTTONS = "pref_vibrate_remote_buttons";
|
||||
public static final boolean DEFAULT_PREF_VIBRATE_REMOTE_BUTTONS = false;
|
||||
|
||||
// Current host id
|
||||
public static final String KEY_PREF_CURRENT_HOST_ID = "current_host_id";
|
||||
public static final int DEFAULT_PREF_CURRENT_HOST_ID = -1;
|
||||
|
||||
public static final String KEY_PREF_REMOTE_BAR_ITEMS = "pref_remote_bar_items";
|
||||
public static String getRemoteBarItemsPrefKey(int hostId) {
|
||||
return Settings.KEY_PREF_REMOTE_BAR_ITEMS + hostId;
|
||||
}
|
||||
|
||||
public static final String KEY_PREF_ALWAYS_SENDTOKODI_ADDON = "pref_always_sendtokodi_addon";
|
||||
public static final boolean DEFAULT_PREF_ALWAYS_SENDTOKODI_ADDON = false;
|
||||
|
||||
public static final String KEY_PREF_YOUTUBE_ADDON_ID = "pref_youtube_addon_id";
|
||||
public static final String DEFAULT_PREF_YOUTUBE_ADDON_ID = "plugin.video.youtube";
|
||||
|
||||
public static final String KEY_PREF_NAV_DRAWER_ITEMS = "pref_nav_drawer_items";
|
||||
public static String getNavDrawerItemsPrefKey(int hostId) {
|
||||
return Settings.KEY_PREF_NAV_DRAWER_ITEMS + hostId;
|
||||
}
|
||||
|
||||
public static final String KEY_PREF_DOWNLOAD_TYPES = "pref_download_conn_types";
|
||||
|
||||
public static final String KEY_PREF_SINGLE_COLUMN = "pref_single_multi_column";
|
||||
public static final boolean DEFAULT_PREF_SINGLE_COLUMN = false;
|
||||
|
||||
public static final String KEY_PREF_LANGUAGE = "pref_language";
|
||||
public static final String KEY_PREF_SELECTED_LANGUAGE = "pref_selected_language";
|
||||
|
||||
/**
|
||||
* Determines the bit flags used by {@link DownloadManager.Request} to correspond to the enabled network connections
|
||||
* from the settings screen.
|
||||
* @return {@link DownloadManager.Request} network types bit flags that are enabled or 0 if none are enabled
|
||||
*/
|
||||
public static int allowedDownloadNetworkTypes(Context context) {
|
||||
SharedPreferences sharedPref = PreferenceManager.getDefaultSharedPreferences(context);
|
||||
Set<String> connPrefs = sharedPref.getStringSet(Settings.KEY_PREF_DOWNLOAD_TYPES,
|
||||
new HashSet<>(Arrays.asList(new String[]{"0"})));
|
||||
int result = 0; // default none
|
||||
for(String pref : connPrefs) {
|
||||
switch( Integer.parseInt(pref) ) {
|
||||
case 0:
|
||||
result |= DownloadManager.Request.NETWORK_WIFI;
|
||||
break;
|
||||
case 1:
|
||||
result |= DownloadManager.Request.NETWORK_MOBILE;
|
||||
break;
|
||||
case 2: // currently -1 means all network types in DownloadManager
|
||||
result |= ~0;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Keys for bookmarked addons stored in preferences
|
||||
*/
|
||||
private static final String KEY_PREF_BOOKMARKED_ADDONS = "bookmarked";
|
||||
public static String getBookmarkedAddonsPrefKey(int hostId) {
|
||||
return Settings.KEY_PREF_BOOKMARKED_ADDONS + hostId;
|
||||
}
|
||||
private static final String KEY_PREF_NAME_BOOKMARKED_ADDON = "name_";
|
||||
public static String getNameBookmarkedAddonsPrefKey(int hostId) {
|
||||
return Settings.KEY_PREF_NAME_BOOKMARKED_ADDON + hostId + "_";
|
||||
}
|
||||
public static final String DEFAULT_PREF_NAME_BOOKMARKED_ADDON = "Content";
|
||||
|
||||
/**
|
||||
* Returns a theme resource Id given the value stored in Shared Preferences
|
||||
* @param prefThemeColor Shared Preferences colour for the theme
|
||||
* @param prefThemeVariant Shared Preferences variant for the theme
|
||||
* @return Android resource id of the theme
|
||||
*/
|
||||
public static int getThemeResourceId(String prefThemeColor, String prefThemeVariant) {
|
||||
switch (prefThemeColor) {
|
||||
case THEME_COLOR_YELLOW:
|
||||
switch (prefThemeVariant) {
|
||||
case THEME_VARIANT_LIGHT:
|
||||
return R.style.Theme_Kore_Yellow_Light;
|
||||
case THEME_VARIANT_DARK:
|
||||
return R.style.Theme_Kore_Yellow_Dark;
|
||||
default:
|
||||
return R.style.Theme_Kore_Yellow_Auto;
|
||||
}
|
||||
case THEME_COLOR_PURPLE:
|
||||
switch (prefThemeVariant) {
|
||||
case THEME_VARIANT_LIGHT:
|
||||
return R.style.Theme_Kore_Purple_Light;
|
||||
case THEME_VARIANT_DARK:
|
||||
return R.style.Theme_Kore_Purple_Dark;
|
||||
default:
|
||||
return R.style.Theme_Kore_Purple_Auto;
|
||||
}
|
||||
case THEME_COLOR_GREEN:
|
||||
switch (prefThemeVariant) {
|
||||
case THEME_VARIANT_LIGHT:
|
||||
return R.style.Theme_Kore_Green_Light;
|
||||
case THEME_VARIANT_DARK:
|
||||
return R.style.Theme_Kore_Green_Dark;
|
||||
default:
|
||||
return R.style.Theme_Kore_Green_Auto;
|
||||
}
|
||||
default: // "kore" and "system_colors" share this
|
||||
switch (prefThemeVariant) {
|
||||
case THEME_VARIANT_LIGHT:
|
||||
return R.style.Theme_Kore_Default_Light;
|
||||
case THEME_VARIANT_DARK:
|
||||
return R.style.Theme_Kore_Default_Dark;
|
||||
default:
|
||||
return R.style.Theme_Kore_Default_Auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
302
app/src/main/java/org/xbmc/kore/ShareOpenActivity.java
Normal file
302
app/src/main/java/org/xbmc/kore/ShareOpenActivity.java
Normal file
|
|
@ -0,0 +1,302 @@
|
|||
package org.xbmc.kore;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.webkit.MimeTypeMap;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.core.content.pm.ShortcutManagerCompat;
|
||||
import androidx.preference.PreferenceManager;
|
||||
|
||||
import org.xbmc.kore.host.HostInfo;
|
||||
import org.xbmc.kore.host.HostManager;
|
||||
import org.xbmc.kore.host.actions.OpenSharedUrl;
|
||||
import org.xbmc.kore.jsonrpc.ApiCallback;
|
||||
import org.xbmc.kore.jsonrpc.type.PlaylistType;
|
||||
import org.xbmc.kore.ui.sections.localfile.HttpApp;
|
||||
import org.xbmc.kore.ui.sections.remote.RemoteActivity;
|
||||
import org.xbmc.kore.utils.LogUtils;
|
||||
import org.xbmc.kore.utils.PluginUrlUtils;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URL;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* Auxiliary activity with no UI that handles share intents to Play or Queue an item on Kodi.
|
||||
* Decodes the passed intent, determine which methods to call on Kodi, sends the appropriate calls
|
||||
* and opens the {@link RemoteActivity} if necessary.
|
||||
*/
|
||||
public class ShareOpenActivity extends Activity {
|
||||
private static final String TAG = LogUtils.makeLogTag(ShareOpenActivity.class);
|
||||
|
||||
// ACTION to be used with the shortcut API that directly opens the remote
|
||||
public static final String DEFAULT_OPEN_ACTION = "org.xbmc.kore.OPEN_REMOTE_VIEW";
|
||||
// CATEGORY for dynamic Share Targets
|
||||
public static final String SHARE_TARGET_CATEGORY = "org.xbmc.kore.SHARE_TARGET";
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
handleStartIntent(getIntent());
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the intent that started this activity, namely to start playing something on Kodi
|
||||
* @param intent Start intent for the activity
|
||||
*/
|
||||
protected void handleStartIntent(Intent intent) {
|
||||
handleStartIntent(intent, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the intent that started this activity, namely to start playing something on Kodi
|
||||
* @param intent Start intent for the activity
|
||||
* @param queue Whether to queue the item
|
||||
*/
|
||||
protected void handleStartIntent(Intent intent, boolean queue) {
|
||||
LogUtils.LOGD(TAG, "Got Share Intent: " + intent);
|
||||
final HostManager hostManager = HostManager.getInstance(this);
|
||||
|
||||
// If a host was passed from the intent switch to it
|
||||
String shortcutId = intent.getStringExtra(ShortcutManagerCompat.EXTRA_SHORTCUT_ID);
|
||||
if (shortcutId != null) {
|
||||
int hostId = Integer.parseInt(shortcutId);
|
||||
for (HostInfo host : hostManager.getHosts()) {
|
||||
if (host.getId() == hostId) {
|
||||
LogUtils.LOGD(TAG, "Switching hosts");
|
||||
hostManager.switchHost(host);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final String action = intent.getAction();
|
||||
final String intentType = intent.getType();
|
||||
// Check action: open the Remote activity if no action specified, no host connection (no hosts configured?),
|
||||
// default open specified (switch host?) or any other action other than Send or View
|
||||
if (action == null ||
|
||||
hostManager.getConnection() == null ||
|
||||
action.equals(DEFAULT_OPEN_ACTION) ||
|
||||
!(action.equals(Intent.ACTION_SEND) || action.equals(Intent.ACTION_VIEW))) {
|
||||
startActivity(new Intent(this, RemoteActivity.class));
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
|
||||
Uri videoUri;
|
||||
if (action.equals(Intent.ACTION_SEND) && intentType != null && intentType.equals("text/plain")) {
|
||||
// Get the URI, which is stored in Extras
|
||||
videoUri = getPlainTextUri(intent.getStringExtra(Intent.EXTRA_TEXT));
|
||||
} else {
|
||||
videoUri = intent.getData();
|
||||
}
|
||||
|
||||
if (videoUri == null) {
|
||||
// Check if `intent` contains a URL or a link to a local file:
|
||||
videoUri = getShareLocalUriOrHiddenUri(intent);
|
||||
}
|
||||
|
||||
if (videoUri == null) {
|
||||
// Couldn't understand the URI
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
|
||||
String url = toPluginUrl(videoUri);
|
||||
|
||||
if (url == null) {
|
||||
url = videoUri.toString();
|
||||
}
|
||||
|
||||
// Determine which playlist to use
|
||||
int playlistType;
|
||||
if (intentType == null) {
|
||||
playlistType = PlaylistType.VIDEO_PLAYLISTID;
|
||||
} else if (intentType.matches("audio.*")) {
|
||||
playlistType = PlaylistType.MUSIC_PLAYLISTID;
|
||||
} else if (intentType.matches("video.*")) {
|
||||
playlistType = PlaylistType.VIDEO_PLAYLISTID;
|
||||
} else if (intentType.matches("image.*")) {
|
||||
playlistType = PlaylistType.PICTURE_PLAYLISTID;
|
||||
} else {
|
||||
// Generic links? Default to video:
|
||||
playlistType = PlaylistType.VIDEO_PLAYLISTID;
|
||||
}
|
||||
|
||||
String title = getString(R.string.app_name);
|
||||
String text = getString(R.string.item_added_to_playlist);
|
||||
final Context context = this;
|
||||
new OpenSharedUrl(this, url, title, text, queue, playlistType)
|
||||
.execute(hostManager.getConnection(),
|
||||
new ApiCallback<>() {
|
||||
@Override
|
||||
public void onSuccess(Boolean wasAlreadyPlaying) {
|
||||
String msg = queue && wasAlreadyPlaying ? getString(R.string.item_added_to_playlist)
|
||||
: getString(R.string.item_sent_to_kodi);
|
||||
Toast.makeText(context, msg, Toast.LENGTH_SHORT)
|
||||
.show();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(int errorCode, String description) {
|
||||
LogUtils.LOGE(TAG, "Share failed: " + description);
|
||||
Toast.makeText(context, description, Toast.LENGTH_SHORT)
|
||||
.show();
|
||||
}
|
||||
}, new Handler(Looper.getMainLooper()));
|
||||
|
||||
// Don't display Kore after queueing from another app, otherwise start the remote
|
||||
if (!queue)
|
||||
startActivity(new Intent(this, RemoteActivity.class)
|
||||
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP));
|
||||
// Always finish as we don't have anything to show
|
||||
finish();
|
||||
}
|
||||
|
||||
private Uri getUrlInsideIntent(Intent intent) {
|
||||
// Some apps hide the link in the clip, try to detect any link by casting the intent
|
||||
// to string a looking with a regular expression:
|
||||
|
||||
Matcher matcher = Pattern.compile("https?://[^\\s]+").matcher(intent.toString());
|
||||
String matchedString;
|
||||
if (matcher.find()) {
|
||||
matchedString = matcher.group(0);
|
||||
if (matchedString != null && matchedString.endsWith("}")) {
|
||||
matchedString = matchedString.substring(0, matchedString.length() - 1);
|
||||
}
|
||||
return Uri.parse(matchedString);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private Uri getShareLocalUriOrHiddenUri(Intent intent) {
|
||||
Uri contentUri = intent.getData();
|
||||
|
||||
if (contentUri == null) {
|
||||
Bundle bundle = intent.getExtras();
|
||||
contentUri = (Uri) bundle.get(Intent.EXTRA_STREAM);
|
||||
}
|
||||
if (contentUri == null) {
|
||||
return getUrlInsideIntent(intent);
|
||||
}
|
||||
|
||||
HttpApp http_app;
|
||||
try {
|
||||
http_app = HttpApp.getInstance(getApplicationContext(), 8080);
|
||||
} catch (IOException ioe) {
|
||||
Toast.makeText(getApplicationContext(),
|
||||
getString(R.string.error_starting_http_server),
|
||||
Toast.LENGTH_LONG).show();
|
||||
return null;
|
||||
}
|
||||
http_app.addUri(contentUri);
|
||||
String url = http_app.getLinkToFile();
|
||||
|
||||
return Uri.parse(url);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the Uri that the some apps passes in EXTRA_TEXT
|
||||
* YouTube sends something like: [Video title]: [YouTube URL] so we need
|
||||
* to get the second part
|
||||
*
|
||||
* @param extraText EXTRA_TEXT passed in the intent
|
||||
* @return Uri present in extraText if present
|
||||
*/
|
||||
private Uri getPlainTextUri(String extraText) {
|
||||
if (extraText == null) return null;
|
||||
|
||||
for (String word : extraText.split(" ")) {
|
||||
if (word.startsWith("http://") || word.startsWith("https://")) {
|
||||
try {
|
||||
URL validUri = new URL(word);
|
||||
return Uri.parse(word);
|
||||
} catch (MalformedURLException exc) {
|
||||
LogUtils.LOGD(TAG, "Got a malformed URL in an intent: " + word);
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a video url to a Kodi plugin URL.
|
||||
*
|
||||
* @param playuri some URL
|
||||
* @return plugin URL
|
||||
*/
|
||||
private String toPluginUrl(Uri playuri) {
|
||||
String host = playuri.getHost();
|
||||
String extension = MimeTypeMap.getFileExtensionFromUrl(playuri.toString());
|
||||
String mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension);
|
||||
|
||||
if (host == null)
|
||||
return null;
|
||||
|
||||
boolean alwaysSendToKodi = PreferenceManager.getDefaultSharedPreferences(getApplicationContext())
|
||||
.getBoolean(Settings.KEY_PREF_ALWAYS_SENDTOKODI_ADDON,
|
||||
Settings.DEFAULT_PREF_ALWAYS_SENDTOKODI_ADDON);
|
||||
|
||||
if (!alwaysSendToKodi) {
|
||||
if (host.endsWith("youtube.com") || host.endsWith("youtu.be")) {
|
||||
String preferredYouTubeAddonId = PreferenceManager.getDefaultSharedPreferences(getApplicationContext())
|
||||
.getString(Settings.KEY_PREF_YOUTUBE_ADDON_ID, Settings.DEFAULT_PREF_YOUTUBE_ADDON_ID);
|
||||
if (preferredYouTubeAddonId.equals("plugin.video.invidious")) {
|
||||
return PluginUrlUtils.toInvidiousYouTubePluginUrl(playuri);
|
||||
} else {
|
||||
return PluginUrlUtils.toDefaultYouTubePluginUrl(playuri);
|
||||
}
|
||||
} else if (host.endsWith("vimeo.com")) {
|
||||
return PluginUrlUtils.toVimeoPluginUrl(playuri);
|
||||
} else if (host.endsWith("svtplay.se")) {
|
||||
return PluginUrlUtils.toSvtPlayPluginUrl(playuri);
|
||||
} else if (host.endsWith("soundcloud.com")) {
|
||||
return PluginUrlUtils.toSoundCloudPluginUrl(playuri);
|
||||
} else if (host.endsWith("twitch.tv")) {
|
||||
return PluginUrlUtils.toTwitchPluginUrl(playuri);
|
||||
} else if (PluginUrlUtils.isHostArte(host)) {
|
||||
return PluginUrlUtils.toArtePluginUrl(playuri);
|
||||
}
|
||||
}
|
||||
if (host.startsWith("app.primevideo.com")) {
|
||||
// Prime Video cannot be handled by SendToKodi as it requires authentication:
|
||||
Matcher amazonMatcher = Pattern.compile("gti=([^&]+)").matcher(playuri.toString());
|
||||
if (amazonMatcher.find()) {
|
||||
String gti = amazonMatcher.group(1);
|
||||
return "plugin://plugin.video.amazon-test/?asin=" + gti + "&mode=PlayVideo&adult=0&name=&trailer=0&selbitrate=0";
|
||||
}
|
||||
} else if (!isMediaFile(mimeType)) {
|
||||
// SendToKodi is a Kodi addon that is able to extract URLs from generic
|
||||
// web URIs using the Python library "youtube-dl".
|
||||
// Use it as a last resort, unless the URI extension is a known media file
|
||||
// (in that case Kodi does not require an addon to play the link):
|
||||
return "plugin://plugin.video.sendtokodi/?" + playuri;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
boolean isMediaFile(String mimeType) {
|
||||
if (mimeType == null) {
|
||||
return false;
|
||||
} else if (mimeType.startsWith("audio")) {
|
||||
return true;
|
||||
} else if (mimeType.startsWith("image")) {
|
||||
return true;
|
||||
} else if (mimeType.startsWith("video")) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
16
app/src/main/java/org/xbmc/kore/ShareQueueActivity.java
Normal file
16
app/src/main/java/org/xbmc/kore/ShareQueueActivity.java
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
package org.xbmc.kore;
|
||||
|
||||
import android.content.Intent;
|
||||
|
||||
/**
|
||||
* Auxiliary activity with no UI that handles share intents to Queue an item on Kodi.
|
||||
* Delegates to {@link ShareOpenActivity} with queue set
|
||||
*/
|
||||
public class ShareQueueActivity extends ShareOpenActivity {
|
||||
|
||||
@Override
|
||||
protected void handleStartIntent(Intent intent) {
|
||||
handleStartIntent(intent, true);
|
||||
}
|
||||
|
||||
}
|
||||
191
app/src/main/java/org/xbmc/kore/eventclient/ButtonCodes.java
Normal file
191
app/src/main/java/org/xbmc/kore/eventclient/ButtonCodes.java
Normal file
|
|
@ -0,0 +1,191 @@
|
|||
/*
|
||||
* Copyright (C) 2005-2009 Team XBMC
|
||||
* http://xbmc.org
|
||||
*
|
||||
* This Program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2, or (at your option)
|
||||
* any later version.
|
||||
*
|
||||
* This Program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with XBMC Remote; see the file license. If not, write to
|
||||
* the Free Software Foundation, 675 Mass Ave, Cambridge, MA 02139, USA.
|
||||
* http://www.gnu.org/copyleft/gpl.html
|
||||
*
|
||||
*/
|
||||
|
||||
package org.xbmc.kore.eventclient;
|
||||
|
||||
/**
|
||||
* Remote control and keyboard strings, taken from xbmc/ButtonTranslator.cpp
|
||||
*
|
||||
* @author Team XBMC
|
||||
*/
|
||||
public final class ButtonCodes {
|
||||
/**
|
||||
* "KB" => standard keyboard map ( <keyboard> section )
|
||||
* "XG" => xbox gamepad map ( <gamepad> section )
|
||||
* "R1" => xbox remote map ( <remote> section )
|
||||
* "R2" => xbox universal remote map ( <universalremote> section )
|
||||
* "LI:devicename" => LIRC remote map where 'devicename' is the actual device's name
|
||||
*/
|
||||
public static final String MAP_KEYBOARD = "KB";
|
||||
public static final String MAP_GAMEPAD = "XG";
|
||||
public static final String MAP_REMOTE = "R1";
|
||||
public static final String MAP_UNIVERSAL_REMOTE = "R2";
|
||||
|
||||
public static final String REMOTE_LEFT = "left";
|
||||
public static final String REMOTE_RIGHT = "right";
|
||||
public static final String REMOTE_UP = "up";
|
||||
public static final String REMOTE_DOWN = "down";
|
||||
public static final String REMOTE_SELECT = "select";
|
||||
public static final String REMOTE_BACK = "back";
|
||||
public static final String REMOTE_MENU = "menu";
|
||||
public static final String REMOTE_INFO = "info";
|
||||
public static final String REMOTE_DISPLAY = "display";
|
||||
public static final String REMOTE_TITLE = "title";
|
||||
public static final String REMOTE_PLAY = "play";
|
||||
public static final String REMOTE_PAUSE = "pause";
|
||||
public static final String REMOTE_REVERSE = "reverse";
|
||||
public static final String REMOTE_FORWARD = "forward";
|
||||
public static final String REMOTE_SKIP_PLUS = "skipplus";
|
||||
public static final String REMOTE_SKIP_MINUS = "skipminus";
|
||||
public static final String REMOTE_STOP = "stop";
|
||||
public static final String REMOTE_0 = "zero";
|
||||
public static final String REMOTE_1 = "one";
|
||||
public static final String REMOTE_2 = "two";
|
||||
public static final String REMOTE_3 = "three";
|
||||
public static final String REMOTE_4 = "four";
|
||||
public static final String REMOTE_5 = "five";
|
||||
public static final String REMOTE_6 = "six";
|
||||
public static final String REMOTE_7 = "seven";
|
||||
public static final String REMOTE_8 = "eight";
|
||||
public static final String REMOTE_9 = "nine";
|
||||
// additional keys from the media center extender for xbox remote
|
||||
public static final String REMOTE_POWER = "power";
|
||||
public static final String REMOTE_MY_TV = "mytv";
|
||||
public static final String REMOTE_MY_MUSIC = "mymusic";
|
||||
public static final String REMOTE_MY_PICTURES = "mypictures";
|
||||
public static final String REMOTE_MY_VIDEOS = "myvideo";
|
||||
public static final String REMOTE_RECORD = "record";
|
||||
public static final String REMOTE_START = "start";
|
||||
public static final String REMOTE_VOLUME_PLUS = "volumeplus";
|
||||
public static final String REMOTE_VOLUME_MINUS = "volumeminus";
|
||||
public static final String REMOTE_CHANNEL_PLUS = "channelplus";
|
||||
public static final String REMOTE_CHANNEL_MINUS = "channelminus";
|
||||
public static final String REMOTE_PAGE_PLUS = "pageplus";
|
||||
public static final String REMOTE_PAGE_MINUS = "pageminus";
|
||||
public static final String REMOTE_MUTE = "mute";
|
||||
public static final String REMOTE_RECORDED_TV = "recordedtv";
|
||||
public static final String REMOTE_GUIDE = "guide";
|
||||
public static final String REMOTE_LIVE_TV = "livetv";
|
||||
public static final String REMOTE_STAR = "star";
|
||||
public static final String REMOTE_HASH = "hash";
|
||||
public static final String REMOTE_CLEAR = "clear";
|
||||
public static final String REMOTE_ENTER = "enter";
|
||||
public static final String REMOTE_XBOX = "xbox";
|
||||
|
||||
public static final String KEYBOARD_RETURN = "return";
|
||||
public static final String KEYBOARD_ENTER = "enter";
|
||||
public static final String KEYBOARD_ESCAPE = "escape";
|
||||
public static final String KEYBOARD_ESC = "esc";
|
||||
public static final String KEYBOARD_TAB = "tab";
|
||||
public static final String KEYBOARD_SPACE = "space";
|
||||
public static final String KEYBOARD_LEFT = "left";
|
||||
public static final String KEYBOARD_RIGHT = "right";
|
||||
public static final String KEYBOARD_UP = "up";
|
||||
public static final String KEYBOARD_DOWN = "down";
|
||||
public static final String KEYBOARD_INSERT = "insert";
|
||||
public static final String KEYBOARD_DELETE = "delete";
|
||||
public static final String KEYBOARD_HOME = "home";
|
||||
public static final String KEYBOARD_END = "end";
|
||||
public static final String KEYBOARD_F1 = "f1";
|
||||
public static final String KEYBOARD_F2 = "f2";
|
||||
public static final String KEYBOARD_F3 = "f3";
|
||||
public static final String KEYBOARD_F4 = "f4";
|
||||
public static final String KEYBOARD_F5 = "f5";
|
||||
public static final String KEYBOARD_F6 = "f6";
|
||||
public static final String KEYBOARD_F7 = "f7";
|
||||
public static final String KEYBOARD_F8 = "f8";
|
||||
public static final String KEYBOARD_F9 = "f9";
|
||||
public static final String KEYBOARD_F10 = "f10";
|
||||
public static final String KEYBOARD_F11 = "f11";
|
||||
public static final String KEYBOARD_F12 = "f12";
|
||||
public static final String KEYBOARD_NUMPAD_ZERO = "numpadzero";
|
||||
public static final String KEYBOARD_NUMPAD_1 = "numpadone";
|
||||
public static final String KEYBOARD_NUMPAD_2 = "numpadtwo";
|
||||
public static final String KEYBOARD_NUMPAD_3 = "numpadthree";
|
||||
public static final String KEYBOARD_NUMPAD_4 = "numpadfour";
|
||||
public static final String KEYBOARD_NUMPAD_5 = "numpadfive";
|
||||
public static final String KEYBOARD_NUMPAD_6 = "numpadsix";
|
||||
public static final String KEYBOARD_NUMPAD_7 = "numpadseven";
|
||||
public static final String KEYBOARD_NUMPAD_8 = "numpadeight";
|
||||
public static final String KEYBOARD_NUMPAD_9 = "numpadnine";
|
||||
public static final String KEYBOARD_NUMPAD_TIMES = "numpadtimes";
|
||||
public static final String KEYBOARD_NUMPAD_PLUS = "numpadplus";
|
||||
public static final String KEYBOARD_NUMPAD_MINUS = "numpadminus";
|
||||
public static final String KEYBOARD_NUMPAD_PERIOD = "numpadperiod";
|
||||
public static final String KEYBOARD_NUMPAD_DIVIDE = "numpaddivide";
|
||||
public static final String KEYBOARD_PAGEUP = "pageup";
|
||||
public static final String KEYBOARD_PAGEDOWN = "pagedown";
|
||||
public static final String KEYBOARD_PRINTSCREEN = "printscreen";
|
||||
public static final String KEYBOARD_BACKSPACE = "backspace";
|
||||
public static final String KEYBOARD_MENU = "menu";
|
||||
public static final String KEYBOARD_PAUSE = "pause";
|
||||
public static final String KEYBOARD_LEFTSHIFT = "leftshift";
|
||||
public static final String KEYBOARD_RIGHTSHIFT = "rightshift";
|
||||
public static final String KEYBOARD_LEFTCTRL = "leftctrl";
|
||||
public static final String KEYBOARD_RIGHTCTRL = "rightctrl";
|
||||
public static final String KEYBOARD_LEFTALT = "leftalt";
|
||||
public static final String KEYBOARD_RIGHTALT = "rightalt";
|
||||
public static final String KEYBOARD_LEFTWINDOWS = "leftwindows";
|
||||
public static final String KEYBOARD_RIGHTWINDOWS = "rightwindows";
|
||||
public static final String KEYBOARD_CAPSLOCK = "capslock";
|
||||
public static final String KEYBOARD_NUMLOCK = "numlock";
|
||||
public static final String KEYBOARD_SCROLLLOCK = "scrolllock";
|
||||
public static final String KEYBOARD_SEMICOLON = "semicolon";
|
||||
public static final String KEYBOARD_COLON = "colon";
|
||||
public static final String KEYBOARD_EQUALS = "equals";
|
||||
public static final String KEYBOARD_PLUS = "plus";
|
||||
public static final String KEYBOARD_COMMA = "comma";
|
||||
public static final String KEYBOARD_LESSTHAN = "lessthan";
|
||||
public static final String KEYBOARD_MINUS = "minus";
|
||||
public static final String KEYBOARD_UNDERLINE = "underline";
|
||||
public static final String KEYBOARD_PERIOD = "period";
|
||||
public static final String KEYBOARD_GREATERTHAN = "greaterthan";
|
||||
public static final String KEYBOARD_FORWARDSLASH = "forwardslash";
|
||||
public static final String KEYBOARD_QUESTIONMARK = "questionmark";
|
||||
public static final String KEYBOARD_LEFTQUOTE = "leftquote";
|
||||
public static final String KEYBOARD_TILDE = "tilde";
|
||||
public static final String KEYBOARD_OPENSQUAREBRACKET = "opensquarebracket";
|
||||
public static final String KEYBOARD_OPENBRACE = "openbrace";
|
||||
public static final String KEYBOARD_BACKSLASH = "backslash";
|
||||
public static final String KEYBOARD_PIPE = "pipe";
|
||||
public static final String KEYBOARD_CLOSESQUAREBRACKET = "closesquarebracket";
|
||||
public static final String KEYBOARD_CLOSEBRACE = "closebrace";
|
||||
public static final String KEYBOARD_QUOTE = "quote";
|
||||
public static final String KEYBOARD_DOUBLEQUOTE = "doublequote";
|
||||
public static final String KEYBOARD_LAUNCH_MAIL = "launch_mail";
|
||||
public static final String KEYBOARD_BROWSER_HOME = "browser_home";
|
||||
public static final String KEYBOARD_BROWSER_FAVORITES = "browser_favorites";
|
||||
public static final String KEYBOARD_BROWSER_REFRESH = "browser_refresh";
|
||||
public static final String KEYBOARD_BROWSER_SEARCH = "browser_search";
|
||||
public static final String KEYBOARD_LAUNCH_APP1_PC_ICON = "launch_app1_pc_icon";
|
||||
public static final String KEYBOARD_LAUNCH_MEDIA_SELECT = "launch_media_select";
|
||||
public static final String KEYBOARD_PLAY_PAUSE = "play_pause";
|
||||
public static final String KEYBOARD_STOP = "stop";
|
||||
public static final String KEYBOARD_VOLUME_UP = "volume_up";
|
||||
public static final String KEYBOARD_VOLUME_MUTE = "volume_mute";
|
||||
public static final String KEYBOARD_VOLUME_DOWN = "volume_down";
|
||||
public static final String KEYBOARD_PREV_TRACK = "prev_track";
|
||||
public static final String KEYBOARD_NEXT_TRACK = "next_track";
|
||||
|
||||
public static final String GAMEPAD_LEFT_ANALOG_TRIGGER = "leftanalogtrigger";
|
||||
public static final String GAMEPAD_RIGHT_ANALOG_TRIGGER = "rightanalogtrigger";
|
||||
|
||||
}
|
||||
313
app/src/main/java/org/xbmc/kore/eventclient/EventClient.java
Normal file
313
app/src/main/java/org/xbmc/kore/eventclient/EventClient.java
Normal file
|
|
@ -0,0 +1,313 @@
|
|||
/*
|
||||
* Copyright (C) 2008-2013 Team XBMC
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
*/
|
||||
|
||||
package org.xbmc.kore.eventclient;
|
||||
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.net.InetAddress;
|
||||
import java.util.Locale;
|
||||
|
||||
/**
|
||||
* Event Client Class
|
||||
*
|
||||
* Implements an XBMC-Client. This class can be used to implement your own application which
|
||||
* should act as a Input device for XBMC. Also starts a Ping-Thread, which tells the XBMC EventServer
|
||||
* that the client is alive. Therefore if you close your application you SHOULD call stopClient()!
|
||||
* @author Stefan Agner
|
||||
*
|
||||
*/
|
||||
public class EventClient
|
||||
{
|
||||
private final boolean hasIcon;
|
||||
private String deviceName;
|
||||
private PingThread oPingThread;
|
||||
private byte iconType = Packet.ICON_PNG;
|
||||
private byte[] iconData;
|
||||
private InetAddress hostAddress;
|
||||
private int hostPort;
|
||||
|
||||
/**
|
||||
* Starts a XBMC EventClient.
|
||||
* @param hostAddress Address of the Host running XBMC
|
||||
* @param hostPort Port of the Host running XBMC (default 9777)
|
||||
* @param deviceName Name of the Device
|
||||
* @param iconFile Path to the Iconfile (PNG, JPEG or GIF)
|
||||
* @throws IOException Exception
|
||||
*/
|
||||
public EventClient(InetAddress hostAddress, int hostPort, String deviceName, String iconFile) throws IOException
|
||||
{
|
||||
byte iconType = Packet.ICON_PNG;
|
||||
// Assume png as icon type
|
||||
if(iconFile.toLowerCase(Locale.US).endsWith(".jpeg"))
|
||||
iconType = Packet.ICON_JPEG;
|
||||
if(iconFile.toLowerCase(Locale.US).endsWith(".jpg"))
|
||||
iconType = Packet.ICON_JPEG;
|
||||
if(iconFile.toLowerCase(Locale.US).endsWith(".gif"))
|
||||
iconType = Packet.ICON_GIF;
|
||||
|
||||
// Read the icon file to the byte array...
|
||||
FileInputStream iconFileStream = new FileInputStream(iconFile);
|
||||
byte[] iconData = new byte[iconFileStream.available()];
|
||||
iconFileStream.read(iconData);
|
||||
|
||||
hasIcon = true;
|
||||
|
||||
// Call start-Method...
|
||||
startClient(hostAddress, hostPort, deviceName, iconType, iconData);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Starts a XBMC EventClient.
|
||||
* @param hostAddress Address of the Host running XBMC
|
||||
* @param hostPort Port of the Host running XBMC (default 9777)
|
||||
* @param deviceName Name of the Device
|
||||
* @param iconType Type of the icon file (see Packet.ICON_PNG, Packet.ICON_JPEG or Packet.ICON_GIF)
|
||||
* @param iconData The icon itself as a Byte-Array
|
||||
*/
|
||||
public EventClient(InetAddress hostAddress, int hostPort, String deviceName, byte iconType, byte[] iconData) throws IOException
|
||||
{
|
||||
hasIcon = true;
|
||||
startClient(hostAddress, hostPort, deviceName, iconType, iconData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts a XBMC EventClient without an icon.
|
||||
* @param hostAddress Address of the Host running XBMC
|
||||
* @param hostPort Port of the Host running XBMC (default 9777)
|
||||
* @param deviceName Name of the Device
|
||||
*/
|
||||
public EventClient(InetAddress hostAddress, int hostPort, String deviceName) throws IOException
|
||||
{
|
||||
hasIcon = false;
|
||||
byte iconType = Packet.ICON_NONE;
|
||||
byte[] iconData = null;
|
||||
startClient(hostAddress, hostPort, deviceName, iconType, iconData);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Starts a XBMC EventClient.
|
||||
* @param hostAddress Address of the Host running XBMC
|
||||
* @param hostPort Port of the Host running XBMC (default 9777)
|
||||
* @param deviceName Name of the Device
|
||||
* @param iconType Type of the icon file (see Packet.ICON_PNG, Packet.ICON_JPEG or Packet.ICON_GIF)
|
||||
* @param iconData The icon itself as a Byte-Array
|
||||
*/
|
||||
private void startClient(InetAddress hostAddress, int hostPort, String deviceName, byte iconType, byte[] iconData) throws IOException
|
||||
{
|
||||
// Save host address and port
|
||||
this.hostAddress = hostAddress;
|
||||
this.hostPort = hostPort;
|
||||
this.deviceName = deviceName;
|
||||
|
||||
this.iconType = iconType;
|
||||
this.iconData = iconData;
|
||||
|
||||
// Send Hello Packet...
|
||||
PacketHELO p;
|
||||
if(hasIcon)
|
||||
p = new PacketHELO(deviceName, iconType, iconData);
|
||||
else
|
||||
p = new PacketHELO(deviceName);
|
||||
|
||||
p.send(hostAddress, hostPort);
|
||||
|
||||
// Start Thread (for Ping packets...)
|
||||
oPingThread = new PingThread(hostAddress, hostPort, 20000);
|
||||
oPingThread.start();
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops the XBMC EventClient (especially the Ping-Thread)
|
||||
*/
|
||||
public void stopClient() throws IOException
|
||||
{
|
||||
// Stop Ping-Thread...
|
||||
oPingThread.giveup();
|
||||
oPingThread.interrupt();
|
||||
|
||||
PacketBYE p = new PacketBYE();
|
||||
p.send(hostAddress, hostPort);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Displays a notification window in XBMC.
|
||||
* @param title Message title
|
||||
* @param message The actual message
|
||||
*/
|
||||
public void sendNotification(String title, String message) throws IOException
|
||||
{
|
||||
PacketNOTIFICATION p;
|
||||
if(hasIcon)
|
||||
p = new PacketNOTIFICATION(title, message, iconType, iconData);
|
||||
else
|
||||
p = new PacketNOTIFICATION(title, message);
|
||||
p.send(hostAddress, hostPort);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a Button event
|
||||
* @param code raw button code (default: 0)
|
||||
* @param repeat this key press should repeat until released (default: 1)
|
||||
* Note that queued pressed cannot repeat.
|
||||
* @param down if this is 1, it implies a press event, 0 implies a release
|
||||
* event. (default: 1)
|
||||
* @param queue a queued key press means that the button event is
|
||||
* executed just once after which the next key press is processed.
|
||||
* It can be used for macros. Currently there is no support for
|
||||
* time delays between queued presses. (default: 0)
|
||||
* @param amount unimplemented for now; in the future it will be used for
|
||||
* specifying magnitude of analog key press events
|
||||
* @param axis Axis
|
||||
*/
|
||||
public void sendButton(short code, boolean repeat, boolean down, boolean queue, short amount, byte axis) throws IOException
|
||||
{
|
||||
PacketBUTTON p = new PacketBUTTON(code, repeat, down, queue, amount, axis);
|
||||
p.send(hostAddress, hostPort);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a Button event
|
||||
* @param map_name a combination of map_name and button_name refers to a
|
||||
* mapping in the user's Keymap.xml or Lircmap.xml.
|
||||
* map_name can be one of the following:
|
||||
* <ul>
|
||||
* <li>"KB" => standard keyboard map ( <keyboard> section )</li>
|
||||
* <li>"XG" => xbox gamepad map ( <gamepad> section )</li>
|
||||
* <li>"R1" => xbox remote map ( <remote> section )</li>
|
||||
* <li>"R2" => xbox universal remote map ( <universalremote> section )</li>
|
||||
* <li>"LI:devicename" => LIRC remote map where 'devicename' is the
|
||||
* actual device's name</li></ul>
|
||||
* @param button_name a button name defined in the map specified in map_name.
|
||||
* For example, if map_name is "KB" refering to the <keyboard> section in Keymap.xml
|
||||
* then, valid button_names include "printscreen", "minus", "x", etc.
|
||||
* @param repeat this key press should repeat until released (default: 1)
|
||||
* Note that queued pressed cannot repeat.
|
||||
* @param down if this is 1, it implies a press event, 0 implies a release
|
||||
* event. (default: 1)
|
||||
* @param queue a queued key press means that the button event is
|
||||
* executed just once after which the next key press is processed.
|
||||
* It can be used for macros. Currently there is no support for
|
||||
* time delays between queued presses. (default: 0)
|
||||
* @param amount unimplemented for now; in the future it will be used for
|
||||
* specifying magnitude of analog key press events
|
||||
* @param axis Axis
|
||||
*/
|
||||
public void sendButton(String map_name, String button_name, boolean repeat, boolean down, boolean queue, short amount, byte axis) throws IOException
|
||||
{
|
||||
PacketBUTTON p = new PacketBUTTON(map_name, button_name, repeat, down, queue, amount, axis);
|
||||
p.send(hostAddress, hostPort);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the mouse position in XBMC
|
||||
* @param x horitontal position ranging from 0 to 65535
|
||||
* @param y vertical position ranging from 0 to 65535
|
||||
*/
|
||||
public void sendMouse(int x, int y) throws IOException
|
||||
{
|
||||
PacketMOUSE p = new PacketMOUSE(x, y);
|
||||
p.send(hostAddress, hostPort);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a ping to the XBMC EventServer
|
||||
*/
|
||||
public void ping() throws IOException
|
||||
{
|
||||
PacketPING p = new PacketPING();
|
||||
p.send(hostAddress, hostPort);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tells XBMC to log the message to xbmc.log with the loglevel as specified.
|
||||
* @param loglevel the loglevel, follows XBMC standard.
|
||||
* <ul>
|
||||
* <li>0 = DEBUG</li>
|
||||
* <li>1 = INFO</li>
|
||||
* <li>2 = NOTICE</li>
|
||||
* <li>3 = WARNING</li>
|
||||
* <li>4 = ERROR</li>
|
||||
* <li>5 = SEVERE</li>
|
||||
* </ul>
|
||||
* @param logmessage the message to log
|
||||
*/
|
||||
public void sendLog(byte loglevel, String logmessage) throws IOException
|
||||
{
|
||||
PacketLOG p = new PacketLOG(loglevel, logmessage);
|
||||
p.send(hostAddress, hostPort);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tells XBMC to do the action specified, based on the type it knows were it needs to be sent.
|
||||
* @param actionmessage Actionmessage (as in scripting/skinning)
|
||||
*/
|
||||
public void sendAction(String actionmessage) throws IOException
|
||||
{
|
||||
PacketACTION p = new PacketACTION(actionmessage);
|
||||
p.send(hostAddress, hostPort);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements a PingThread which tells XBMC EventServer that the Client is alive (this should
|
||||
* be done at least every 60 seconds!
|
||||
* @author Stefan Agner
|
||||
*
|
||||
*/
|
||||
static class PingThread extends Thread
|
||||
{
|
||||
private final InetAddress hostAddress;
|
||||
private final int hostPort;
|
||||
private final int sleepTime;
|
||||
private boolean giveup = false;
|
||||
|
||||
public PingThread(InetAddress hostAddress, int hostPort, int sleepTime)
|
||||
{
|
||||
super("XBMC EventClient Ping-Thread");
|
||||
this.hostAddress = hostAddress;
|
||||
this.hostPort = hostPort;
|
||||
this.sleepTime = sleepTime;
|
||||
}
|
||||
|
||||
public void giveup()
|
||||
{
|
||||
giveup = true;
|
||||
}
|
||||
|
||||
public void run()
|
||||
{
|
||||
while(!giveup)
|
||||
{
|
||||
try {
|
||||
PacketPING p = new PacketPING();
|
||||
p.send(hostAddress, hostPort);
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
try {
|
||||
Thread.sleep(sleepTime);
|
||||
} catch (InterruptedException ignored) {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,268 @@
|
|||
/*
|
||||
* Copyright 2015 Synced Synapse. 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.eventclient;
|
||||
|
||||
|
||||
import android.os.Handler;
|
||||
import android.os.HandlerThread;
|
||||
import android.os.Process;
|
||||
|
||||
import org.xbmc.kore.host.HostInfo;
|
||||
import org.xbmc.kore.jsonrpc.ApiCallback;
|
||||
import org.xbmc.kore.host.HostConnection;
|
||||
import org.xbmc.kore.jsonrpc.method.Application;
|
||||
import org.xbmc.kore.jsonrpc.type.ApplicationType;
|
||||
import org.xbmc.kore.utils.LogUtils;
|
||||
import org.xbmc.kore.utils.NetUtils;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.InetAddress;
|
||||
import java.net.UnknownHostException;
|
||||
|
||||
/**
|
||||
* Class that establishes and maintains a connection to Kodi's EventServer
|
||||
* This class keeps pinging Kodi to keep the connection alive and contains
|
||||
* auxiliary methods that allow the sending of packets to Kodi.
|
||||
* Make sure to call quit() when done with it, so that it gracefully shuts down
|
||||
*/
|
||||
public class EventServerConnection {
|
||||
private static final String TAG = LogUtils.makeLogTag(EventServerConnection.class);
|
||||
|
||||
private static final int PING_INTERVAL = 45000; // ms
|
||||
private static final String DEVICE_NAME = "Kore Remote";
|
||||
|
||||
/**
|
||||
* Host to connect too
|
||||
*/
|
||||
private final HostInfo hostInfo;
|
||||
private InetAddress hostInetAddress = null;
|
||||
|
||||
// Handler on which packets will be posted, to send them asynchronously
|
||||
private final Handler commHandler;
|
||||
private final HandlerThread handlerThread;
|
||||
|
||||
private final PacketPING packetPING = new PacketPING();
|
||||
private final Runnable pingRunnable = new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
LogUtils.LOGD(TAG, "Pinging EventServer");
|
||||
if (hostInetAddress != null) {
|
||||
try {
|
||||
packetPING.send(hostInetAddress, hostInfo.getEventServerPort());
|
||||
} catch (IOException exc) {
|
||||
LogUtils.LOGD(TAG, "Got an IOException when sending a PING Packet to Kodi's EventServer");
|
||||
}
|
||||
}
|
||||
commHandler.postDelayed(this, PING_INTERVAL);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Interface to notify users if the connection was successful
|
||||
*/
|
||||
public interface EventServerConnectionCallback {
|
||||
void OnConnectResult(boolean success);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor. Starts the thread that keeps the connection alive. Make sure to call quit() when done.
|
||||
* @param hostInfo Host to connect to
|
||||
* @param callback Callback to call with the connection result
|
||||
* @param callbackHandler Handler on which to call the callback
|
||||
*/
|
||||
public EventServerConnection(final HostInfo hostInfo,
|
||||
final EventServerConnectionCallback callback,
|
||||
final Handler callbackHandler) {
|
||||
this.hostInfo = hostInfo;
|
||||
|
||||
LogUtils.LOGD(TAG, "Starting EventServer Thread");
|
||||
// Handler thread that will keep pinging and send the requests to Kodi
|
||||
handlerThread = new HandlerThread("EventServerConnection", Process.THREAD_PRIORITY_BACKGROUND);
|
||||
handlerThread.start();
|
||||
|
||||
// Get the HandlerThread's Looper and use it for our Handler
|
||||
commHandler = new Handler(handlerThread.getLooper());
|
||||
|
||||
// Now, get the host InetAddress in the background
|
||||
commHandler.post(() -> {
|
||||
try {
|
||||
hostInetAddress = NetUtils.getInet4AddressByName(hostInfo.getAddress());
|
||||
} catch (UnknownHostException exc) {
|
||||
LogUtils.LOGD(TAG, "Got an UnknownHostException, disabling EventServer");
|
||||
hostInetAddress = null;
|
||||
}
|
||||
// Call the callback on the caller's thread
|
||||
callbackHandler.post(() -> callback.OnConnectResult(hostInetAddress != null));
|
||||
if (hostInetAddress != null) {
|
||||
// Start pinging
|
||||
commHandler.postDelayed(pingRunnable, PING_INTERVAL);
|
||||
} else {
|
||||
quitHandlerThread(handlerThread);
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Stops the HandlerThread that is being used to send packets to Kodi
|
||||
*/
|
||||
public void quit() {
|
||||
LogUtils.LOGD(TAG, "Quiting EventServer handler thread");
|
||||
quitHandlerThread(handlerThread);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a packet to Kodi's Event Server
|
||||
* Only sends the packet if connected, i.e. if quit() has not been not called
|
||||
* @param p Packet to send
|
||||
*/
|
||||
public void sendPacket(final Packet p) {
|
||||
if (!handlerThread.isAlive() || (hostInetAddress == null)) {
|
||||
return;
|
||||
}
|
||||
|
||||
LogUtils.LOGD(TAG, "Sending Packet");
|
||||
|
||||
commHandler.post(() -> {
|
||||
try {
|
||||
p.send(hostInetAddress, hostInfo.getEventServerPort());
|
||||
} catch (IOException exc) {
|
||||
LogUtils.LOGD(TAG, "Got an IOException when sending a packet to Kodi's EventServer");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Establishes a connection to the EventServer and reports the result
|
||||
* @param hostInfo Host to connect to
|
||||
* @param callerCallback Callback on which to post the result
|
||||
* @param callerHandler Handler on which to post the callback call
|
||||
*/
|
||||
public static void testEventServerConnection(final HostInfo hostInfo,
|
||||
final EventServerConnectionCallback callerCallback,
|
||||
final Handler callerHandler) {
|
||||
final HandlerThread auxThread = new HandlerThread("EventServerConnectionTest", Process.THREAD_PRIORITY_DEFAULT);
|
||||
auxThread.start();
|
||||
|
||||
// Get the HandlerThread's Looper and use it for our Handler
|
||||
final Handler auxHandler = new Handler(auxThread.getLooper());
|
||||
|
||||
auxHandler.post(() -> {
|
||||
// Get the InetAddress
|
||||
final InetAddress hostInetAddress;
|
||||
try {
|
||||
hostInetAddress = NetUtils.getInet4AddressByName(hostInfo.getAddress());
|
||||
} catch (UnknownHostException exc) {
|
||||
LogUtils.LOGD(TAG, "Couldn't get host InetAddress");
|
||||
reportTestResult(callerHandler, callerCallback, false);
|
||||
quitHandlerThread(auxThread);
|
||||
return;
|
||||
}
|
||||
|
||||
// Send a HELO packet
|
||||
Packet p = new PacketHELO(DEVICE_NAME);
|
||||
try {
|
||||
p.send(hostInetAddress, hostInfo.getEventServerPort());
|
||||
} catch (IOException exc) {
|
||||
LogUtils.LOGD(TAG, "Couldn't send HELO packet to host");
|
||||
reportTestResult(callerHandler, callerCallback, false);
|
||||
quitHandlerThread(auxThread);
|
||||
return;
|
||||
}
|
||||
|
||||
// The previous checks don't really test the connection, as this is UDP. Apart from checking if
|
||||
// any HostUnreachable ICMP message is returned (which may or may not happen), there's no direct way
|
||||
// to check if the messages were delivered, so the solution is to force something to happen on
|
||||
// Kodi and them read Kodi's state to check if it was applied.
|
||||
// We are going to get the mute status of Kodi via jsonrpc, change it via EventServer and check if
|
||||
// it was changed via jsonrpc, reverting it back afterwards
|
||||
final HostConnection auxHostConnection = new HostConnection(
|
||||
new HostInfo(hostInfo.getName(), hostInfo.getAddress(),
|
||||
HostConnection.PROTOCOL_HTTP, hostInfo.getHttpPort(), hostInfo.getTcpPort(),
|
||||
hostInfo.getUsername(), hostInfo.getPassword(), false, 0, hostInfo.isHttps, hostInfo.getShowAsDirectShareTarget()));
|
||||
final Application.GetProperties action = new Application.GetProperties(Application.GetProperties.MUTED);
|
||||
final Packet mutePacket = new PacketBUTTON(ButtonCodes.MAP_REMOTE, ButtonCodes.REMOTE_MUTE,
|
||||
false, true, true, (short) 0, (byte) 0);
|
||||
|
||||
// Get the initial mute status
|
||||
action.execute(auxHostConnection, new ApiCallback<ApplicationType.PropertyValue>() {
|
||||
@Override
|
||||
public void onSuccess(ApplicationType.PropertyValue result) {
|
||||
final boolean initialMuteStatus = result.muted;
|
||||
// Switch mute status
|
||||
try {
|
||||
mutePacket.send(hostInetAddress, hostInfo.getEventServerPort());
|
||||
} catch (IOException exc) {
|
||||
LogUtils.LOGD(TAG, "Couldn't send first MUTE packet to host");
|
||||
reportTestResult(callerHandler, callerCallback, false);
|
||||
quitHandlerThread(auxThread);
|
||||
return;
|
||||
}
|
||||
|
||||
// Sleep a while to make sure the previous command was executed
|
||||
try {
|
||||
Thread.sleep(2000);
|
||||
} catch (InterruptedException exc) {
|
||||
// Ignore
|
||||
}
|
||||
|
||||
// Now get the new status and compare
|
||||
action.execute(auxHostConnection, new ApiCallback<ApplicationType.PropertyValue>() {
|
||||
@Override
|
||||
public void onSuccess(ApplicationType.PropertyValue result) {
|
||||
// Report result (mute status is different)
|
||||
reportTestResult(callerHandler, callerCallback, initialMuteStatus != result.muted);
|
||||
|
||||
// Revert mute status
|
||||
try {
|
||||
mutePacket.send(hostInetAddress, hostInfo.getEventServerPort());
|
||||
} catch (IOException exc) {
|
||||
LogUtils.LOGD(TAG, "Couldn't revert MUTE status");
|
||||
}
|
||||
quitHandlerThread(auxThread);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(int errorCode, String description) {
|
||||
LogUtils.LOGD(TAG, "Got an error on Application.GetProperties: " + description);
|
||||
reportTestResult(callerHandler, callerCallback, false);
|
||||
quitHandlerThread(auxThread);
|
||||
}
|
||||
}, auxHandler);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(int errorCode, String description) {
|
||||
LogUtils.LOGD(TAG, "Got an error on Application.GetProperties: " + description);
|
||||
reportTestResult(callerHandler, callerCallback, false);
|
||||
quitHandlerThread(auxThread);
|
||||
}
|
||||
}, auxHandler);
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
private static void reportTestResult(final Handler callerHandler,
|
||||
final EventServerConnectionCallback callback,
|
||||
final boolean result) {
|
||||
callerHandler.post(() -> callback.OnConnectResult(result));
|
||||
}
|
||||
|
||||
private static void quitHandlerThread(HandlerThread handlerThread) {
|
||||
handlerThread.quitSafely();
|
||||
}
|
||||
}
|
||||
288
app/src/main/java/org/xbmc/kore/eventclient/Packet.java
Normal file
288
app/src/main/java/org/xbmc/kore/eventclient/Packet.java
Normal file
|
|
@ -0,0 +1,288 @@
|
|||
/*
|
||||
* Copyright (C) 2008-2013 Team XBMC
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
*/
|
||||
|
||||
package org.xbmc.kore.eventclient;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.DatagramPacket;
|
||||
import java.net.DatagramSocket;
|
||||
import java.net.InetAddress;
|
||||
|
||||
/**
|
||||
* XBMC Event Client Class
|
||||
* <p>
|
||||
* Implementation of XBMC's UDP based input system.
|
||||
* A set of classes that abstract the various packets that the event server
|
||||
* currently supports. In addition, there's also a class, XBMCClient, that
|
||||
* provides functions that sends the various packets. Use XBMCClient if you
|
||||
* don't need complete control over packet structure.
|
||||
* </p>
|
||||
* <p>
|
||||
* The basic workflow involves:
|
||||
* <ol>
|
||||
* <li>Send a HELO packet</li>
|
||||
* <li>Send x number of valid packets</li>
|
||||
* <li>Send a BYE packet</li>
|
||||
* </ol>
|
||||
* </p>
|
||||
* <p>
|
||||
* IMPORTANT NOTE ABOUT TIMEOUTS:
|
||||
* A client is considered to be timed out if XBMC doesn't received a packet
|
||||
* at least once every 60 seconds. To "ping" XBMC with an empty packet use
|
||||
* PacketPING or XBMCClient.ping(). See the documentation for details.
|
||||
* </p>
|
||||
* <p>
|
||||
* Base class that implements a single event packet.
|
||||
* - Generic packet structure (maximum 1024 bytes per packet)
|
||||
* - Header is 32 bytes long, so 992 bytes available for payload
|
||||
* - large payloads can be split into multiple packets using H4 and H5
|
||||
* H5 should contain total no. of packets in such a case
|
||||
* - H6 contains length of P1, which is limited to 992 bytes
|
||||
* - if H5 is 0 or 1, then H4 will be ignored (single packet msg)
|
||||
* - H7 must be set to zeros for now
|
||||
* </p>
|
||||
* <pre>
|
||||
* -----------------------------
|
||||
* | -H1 Signature ("XBMC") | - 4 x CHAR 4B
|
||||
* | -H2 Version (eg. 2.0) | - 2 x UNSIGNED CHAR 2B
|
||||
* | -H3 PacketType | - 1 x UNSIGNED SHORT 2B
|
||||
* | -H4 Sequence number | - 1 x UNSIGNED LONG 4B
|
||||
* | -H5 No. of packets in msg | - 1 x UNSIGNED LONG 4B
|
||||
* | -H6 Payloadsize of packet | - 1 x UNSIGNED SHORT 2B
|
||||
* | -H7 Client's unique token | - 1 x UNSIGNED LONG 4B
|
||||
* | -H8 Reserved | - 10 x UNSIGNED CHAR 10B
|
||||
* |---------------------------|
|
||||
* | -P1 payload | -
|
||||
* -----------------------------
|
||||
* </pre>
|
||||
* @author Stefan Agner
|
||||
*
|
||||
*/
|
||||
public abstract class Packet {
|
||||
|
||||
private final byte[] sig;
|
||||
private byte[] payload = new byte[0];
|
||||
private final byte minver;
|
||||
private final byte majver;
|
||||
|
||||
private final short packettype;
|
||||
|
||||
|
||||
private final static short MAX_PACKET_SIZE = 1024;
|
||||
private final static short HEADER_SIZE = 32;
|
||||
private final static short MAX_PAYLOAD_SIZE = MAX_PACKET_SIZE - HEADER_SIZE;
|
||||
|
||||
protected final static byte PT_HELO = 0x01;
|
||||
protected final static byte PT_BYE = 0x02;
|
||||
protected final static byte PT_BUTTON = 0x03;
|
||||
protected final static byte PT_MOUSE = 0x04;
|
||||
protected final static byte PT_PING = 0x05;
|
||||
protected final static byte PT_BROADCAST = 0x06;
|
||||
protected final static byte PT_NOTIFICATION = 0x07;
|
||||
protected final static byte PT_BLOB = 0x08;
|
||||
protected final static byte PT_LOG = 0x09;
|
||||
protected final static byte PT_ACTION = 0x0A;
|
||||
protected final static byte PT_DEBUG = (byte)0xFF;
|
||||
|
||||
public final static byte ICON_NONE = 0x00;
|
||||
public final static byte ICON_JPEG = 0x01;
|
||||
public final static byte ICON_PNG = 0x02;
|
||||
public final static byte ICON_GIF = 0x03;
|
||||
|
||||
private static final int uid = (int)(Math.random() * Integer.MAX_VALUE);
|
||||
|
||||
/**
|
||||
* This is an Abstract class and cannot be instanced. Please use one of the Packet implementation Classes
|
||||
* (PacketXXX).
|
||||
*
|
||||
* Implements an XBMC Event Client Packet. Type is to be specified at creation time, Payload can be added
|
||||
* with the various appendPayload methods. Packet can be sent through UDP-Socket with method "send".
|
||||
* @param packettype Type of Packet (PT_XXX)
|
||||
*/
|
||||
protected Packet(short packettype)
|
||||
{
|
||||
sig = new byte[] {'X', 'B', 'M', 'C' };
|
||||
minver = 0;
|
||||
majver = 2;
|
||||
this.packettype = packettype;
|
||||
}
|
||||
|
||||
/**
|
||||
* Appends a String to the payload (terminated with 0x00)
|
||||
* @param payload Payload as String
|
||||
*/
|
||||
protected void appendPayload(String payload)
|
||||
{
|
||||
byte[] payloadarr = payload.getBytes();
|
||||
int oldpayloadsize = this.payload.length;
|
||||
byte[] oldpayload = this.payload;
|
||||
this.payload = new byte[oldpayloadsize+payloadarr.length+1]; // Create new Array with more place (+1 for string terminator)
|
||||
System.arraycopy(oldpayload, 0, this.payload, 0, oldpayloadsize);
|
||||
System.arraycopy(payloadarr, 0, this.payload, oldpayloadsize, payloadarr.length);
|
||||
}
|
||||
|
||||
/**
|
||||
* Appends a single Byte to the payload
|
||||
* @param payload Payload
|
||||
*/
|
||||
protected void appendPayload(byte payload)
|
||||
{
|
||||
appendPayload(new byte[] { payload });
|
||||
}
|
||||
|
||||
/**
|
||||
* Appends a Byte-Array to the payload
|
||||
* @param payloadarr Payload
|
||||
*/
|
||||
protected void appendPayload(byte[] payloadarr)
|
||||
{
|
||||
int oldpayloadsize = this.payload.length;
|
||||
byte[] oldpayload = this.payload;
|
||||
this.payload = new byte[oldpayloadsize+payloadarr.length];
|
||||
System.arraycopy(oldpayload, 0, this.payload, 0, oldpayloadsize);
|
||||
System.arraycopy(payloadarr, 0, this.payload, oldpayloadsize, payloadarr.length);
|
||||
}
|
||||
|
||||
/**
|
||||
* Appends an integer to the payload
|
||||
* @param i Payload
|
||||
*/
|
||||
protected void appendPayload(int i) {
|
||||
appendPayload(intToByteArray(i));
|
||||
}
|
||||
|
||||
/**
|
||||
* Appends a short to the payload
|
||||
* @param s Payload
|
||||
*/
|
||||
protected void appendPayload(short s) {
|
||||
appendPayload(shortToByteArray(s));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Number of Packets which will be sent with current Payload...
|
||||
* @return Number of Packets
|
||||
*/
|
||||
public int getNumPackets()
|
||||
{
|
||||
// return (payload.length + (MAX_PAYLOAD_SIZE - 1)) / MAX_PAYLOAD_SIZE;
|
||||
return 1 + Math.max(payload.length - 1, 0) / MAX_PAYLOAD_SIZE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Header for a specific Packet in this sequence...
|
||||
* @param seq Current sequence number
|
||||
* @param maxseq Maximal sequence number
|
||||
* @param actpayloadsize Payloadsize of this packet
|
||||
* @return Byte-Array with Header information (currently 32-Byte long, see HEADER_SIZE)
|
||||
*/
|
||||
private byte[] getHeader(int seq, int maxseq, short actpayloadsize)
|
||||
{
|
||||
byte[] header = new byte[HEADER_SIZE];
|
||||
System.arraycopy(sig, 0, header, 0, 4);
|
||||
header[4] = majver;
|
||||
header[5] = minver;
|
||||
byte[] packettypearr = shortToByteArray(this.packettype);
|
||||
System.arraycopy(packettypearr, 0, header, 6, 2);
|
||||
byte[] seqarr = intToByteArray(seq);
|
||||
System.arraycopy(seqarr, 0, header, 8, 4);
|
||||
byte[] maxseqarr = intToByteArray(maxseq);
|
||||
System.arraycopy(maxseqarr, 0, header, 12, 4);
|
||||
byte[] payloadsize = shortToByteArray(actpayloadsize);
|
||||
System.arraycopy(payloadsize, 0, header, 16, 2);
|
||||
byte[] uid = intToByteArray(Packet.uid);
|
||||
System.arraycopy(uid, 0, header, 18, 4);
|
||||
byte[] reserved = new byte[10];
|
||||
System.arraycopy(reserved, 0, header, 22, 10);
|
||||
|
||||
return header;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the whole UDP-Message with Header and Payload of a specific Packet in sequence
|
||||
* @param seq Current sequence number
|
||||
* @return Byte-Array with UDP-Message
|
||||
*/
|
||||
private byte[] getUDPMessage(int seq)
|
||||
{
|
||||
int maxseq = getNumPackets();
|
||||
if(seq > maxseq)
|
||||
return null;
|
||||
|
||||
short actpayloadsize;
|
||||
|
||||
if(seq == maxseq)
|
||||
actpayloadsize = (short)((payload.length - 1) % MAX_PAYLOAD_SIZE + 1);
|
||||
else
|
||||
actpayloadsize = MAX_PAYLOAD_SIZE;
|
||||
|
||||
byte[] pack = new byte[HEADER_SIZE+actpayloadsize];
|
||||
|
||||
System.arraycopy(getHeader(seq, maxseq, actpayloadsize), 0, pack, 0, HEADER_SIZE);
|
||||
System.arraycopy(payload, (seq-1)*MAX_PAYLOAD_SIZE, pack, HEADER_SIZE, actpayloadsize);
|
||||
|
||||
return pack;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends this packet to the EventServer
|
||||
* @param adr Address of the EventServer
|
||||
* @param port Port of the EventServer
|
||||
*/
|
||||
public void send(InetAddress adr, int port) throws IOException
|
||||
{
|
||||
int maxseq = getNumPackets();
|
||||
DatagramSocket s = new DatagramSocket();
|
||||
// For each Packet in Sequence...
|
||||
for(int seq=1;seq<=maxseq;seq++)
|
||||
{
|
||||
// Get Message and send them...
|
||||
byte[] pack = getUDPMessage(seq);
|
||||
if (pack == null) continue;
|
||||
DatagramPacket p = new DatagramPacket(pack, pack.length);
|
||||
p.setAddress(adr);
|
||||
p.setPort(port);
|
||||
s.send(p);
|
||||
}
|
||||
s.close();
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper Method to convert an integer to a Byte array
|
||||
* @param value Value
|
||||
* @return Byte-Array
|
||||
*/
|
||||
private static byte[] intToByteArray(int value) {
|
||||
return new byte[] {
|
||||
(byte)(value >>> 24),
|
||||
(byte)(value >>> 16),
|
||||
(byte)(value >>> 8),
|
||||
(byte)value};
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper Method to convert an short to a Byte array
|
||||
* @param value Value
|
||||
* @return Byte-Array
|
||||
*/
|
||||
private static byte[] shortToByteArray(short value) {
|
||||
return new byte[] {
|
||||
(byte)(value >>> 8),
|
||||
(byte)value};
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
/*
|
||||
* Copyright (C) 2008-2013 Team XBMC
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
*/
|
||||
|
||||
package org.xbmc.kore.eventclient;
|
||||
/**
|
||||
* XBMC Event Client Class
|
||||
*
|
||||
* An ACTION packet tells XBMC to do the action specified, based on the type it knows were it needs to be sent.
|
||||
* The idea is that this will be as in scripting/skining and keymapping, just triggered from afar.
|
||||
* @author Stefan Agner
|
||||
*
|
||||
*/
|
||||
public class PacketACTION extends Packet {
|
||||
|
||||
public final static byte ACTION_EXECBUILTIN = 0x01;
|
||||
public final static byte ACTION_BUTTON = 0x02;
|
||||
|
||||
|
||||
/**
|
||||
* An ACTION packet tells XBMC to do the action specified, based on the type it knows were it needs to be sent.
|
||||
* @param actionmessage Actionmessage (as in scripting/skinning)
|
||||
*/
|
||||
public PacketACTION(String actionmessage)
|
||||
{
|
||||
super(PT_ACTION);
|
||||
byte actiontype = ACTION_EXECBUILTIN;
|
||||
appendPayload(actionmessage, actiontype);
|
||||
}
|
||||
|
||||
/**
|
||||
* An ACTION packet tells XBMC to do the action specified, based on the type it knows were it needs to be sent.
|
||||
* @param actionmessage Actionmessage (as in scripting/skinning)
|
||||
* @param actiontype Actiontype (ACTION_EXECBUILTIN or ACTION_BUTTON)
|
||||
*/
|
||||
public PacketACTION(String actionmessage, byte actiontype)
|
||||
{
|
||||
super(PT_ACTION);
|
||||
appendPayload(actionmessage, actiontype);
|
||||
}
|
||||
|
||||
private void appendPayload(String actionmessage, byte actiontype)
|
||||
{
|
||||
appendPayload(actiontype);
|
||||
appendPayload(actionmessage);
|
||||
}
|
||||
}
|
||||
158
app/src/main/java/org/xbmc/kore/eventclient/PacketBUTTON.java
Normal file
158
app/src/main/java/org/xbmc/kore/eventclient/PacketBUTTON.java
Normal file
|
|
@ -0,0 +1,158 @@
|
|||
/*
|
||||
* Copyright (C) 2008-2013 Team XBMC
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
*/
|
||||
|
||||
package org.xbmc.kore.eventclient;
|
||||
|
||||
/**
|
||||
* XBMC Event Client Class
|
||||
* <p>
|
||||
* A button packet send a key press or release event to XBMC
|
||||
*
|
||||
* @author Stefan Agner
|
||||
*/
|
||||
public class PacketBUTTON extends Packet {
|
||||
|
||||
protected final static byte BT_USE_NAME = 0x01;
|
||||
protected final static byte BT_DOWN = 0x02;
|
||||
protected final static byte BT_UP = 0x04;
|
||||
protected final static byte BT_USE_AMOUNT = 0x08;
|
||||
protected final static byte BT_QUEUE = 0x10;
|
||||
protected final static byte BT_NO_REPEAT = 0x20;
|
||||
protected final static byte BT_VKEY = 0x40;
|
||||
protected final static byte BT_AXIS = (byte)0x80;
|
||||
protected final static byte BT_AXISSINGLE = (byte)0x100;
|
||||
|
||||
/**
|
||||
* A button packet send a key press or release event to XBMC
|
||||
*
|
||||
* @param code raw button code (default: 0)
|
||||
* @param repeat this key press should repeat until released (default: 1)
|
||||
* Note that queued pressed cannot repeat.
|
||||
* @param down if this is 1, it implies a press event, 0 implies a release
|
||||
* event. (default: 1)
|
||||
* @param queue a queued key press means that the button event is
|
||||
* executed just once after which the next key press is processed.
|
||||
* It can be used for macros. Currently there is no support for
|
||||
* time delays between queued presses. (default: 0)
|
||||
* @param amount unimplemented for now; in the future it will be used for
|
||||
* specifying magnitude of analog key press events
|
||||
* @param axis Axis
|
||||
*/
|
||||
public PacketBUTTON(short code, boolean repeat, boolean down, boolean queue, short amount, byte axis) {
|
||||
super(PT_BUTTON);
|
||||
String map_name = "";
|
||||
String button_name = "";
|
||||
short flags = 0;
|
||||
appendPayload(code, map_name, button_name, repeat, down, queue, amount, axis, flags);
|
||||
}
|
||||
|
||||
/**
|
||||
* A button packet send a key press or release event to XBMC
|
||||
*
|
||||
* @param map_name a combination of map_name and button_name refers to a
|
||||
* mapping in the user's Keymap.xml or Lircmap.xml.
|
||||
* map_name can be one of the following:
|
||||
* <ul>
|
||||
* <li>"KB" => standard keyboard map ( <keyboard> section )</li>
|
||||
* <li>"XG" => xbox gamepad map ( <gamepad> section )</li>
|
||||
* <li>"R1" => xbox remote map ( <remote> section )</li>
|
||||
* <li>"R2" => xbox universal remote map ( <universalremote> section )</li>
|
||||
* <li>"LI:devicename" => LIRC remote map where 'devicename' is the
|
||||
* actual device's name</li></ul>
|
||||
* @param button_name a button name defined in the map specified in map_name.
|
||||
* For example, if map_name is "KB" refering to the <keyboard> section in Keymap.xml
|
||||
* then, valid button_names include "printscreen", "minus", "x", etc.
|
||||
* @param repeat this key press should repeat until released (default: 1)
|
||||
* Note that queued pressed cannot repeat.
|
||||
* @param down if this is 1, it implies a press event, 0 implies a release
|
||||
* event. (default: 1)
|
||||
* @param queue a queued key press means that the button event is
|
||||
* executed just once after which the next key press is processed.
|
||||
* It can be used for macros. Currently there is no support for
|
||||
* time delays between queued presses. (default: 0)
|
||||
* @param amount unimplemented for now; in the future it will be used for
|
||||
* specifying magnitude of analog key press events
|
||||
* @param axis Axis
|
||||
*/
|
||||
public PacketBUTTON(String map_name, String button_name, boolean repeat, boolean down, boolean queue, short amount, byte axis) {
|
||||
super(PT_BUTTON);
|
||||
short code = 0;
|
||||
short flags = BT_USE_NAME;
|
||||
appendPayload(code, map_name, button_name, repeat, down, queue, amount, axis, flags);
|
||||
}
|
||||
|
||||
/**
|
||||
* Appends Payload for a Button Packet (this method is used by the different Constructors of this Packet)
|
||||
*
|
||||
* @param code raw button code (default: 0)
|
||||
* @param map_name a combination of map_name and button_name refers to a
|
||||
* mapping in the user's Keymap.xml or Lircmap.xml.
|
||||
* map_name can be one of the following:
|
||||
* <ul>
|
||||
* <li>"KB" => standard keyboard map ( <keyboard> section )</li>
|
||||
* <li>"XG" => xbox gamepad map ( <gamepad> section )</li>
|
||||
* <li>"R1" => xbox remote map ( <remote> section )</li>
|
||||
* <li>"R2" => xbox universal remote map ( <universalremote> section )</li>
|
||||
* <li>"LI:devicename" => LIRC remote map where 'devicename' is the
|
||||
* actual device's name</li></ul>
|
||||
* @param button_name a button name defined in the map specified in map_name.
|
||||
* For example, if map_name is "KB" refering to the <keyboard> section in Keymap.xml
|
||||
* then, valid button_names include "printscreen", "minus", "x", etc.
|
||||
* @param repeat this key press should repeat until released (default: 1)
|
||||
* Note that queued pressed cannot repeat.
|
||||
* @param down if this is 1, it implies a press event, 0 implies a release
|
||||
* event. (default: 1)
|
||||
* @param queue a queued key press means that the button event is
|
||||
* executed just once after which the next key press is processed.
|
||||
* It can be used for macros. Currently there is no support for
|
||||
* time delays between queued presses. (default: 0)
|
||||
* @param amount unimplemented for now; in the future it will be used for
|
||||
* specifying magnitude of analog key press events
|
||||
* @param axis Axis
|
||||
* @param flags Packet specific flags
|
||||
*/
|
||||
private void appendPayload(short code, String map_name, String button_name, boolean repeat, boolean down, boolean queue, short amount, byte axis, short flags) {
|
||||
if (amount > 0)
|
||||
flags |= BT_USE_AMOUNT;
|
||||
else
|
||||
amount = 0;
|
||||
|
||||
if (down)
|
||||
flags |= BT_DOWN;
|
||||
else
|
||||
flags |= BT_UP;
|
||||
|
||||
if (!repeat)
|
||||
flags |= BT_NO_REPEAT;
|
||||
|
||||
if (queue)
|
||||
flags |= BT_QUEUE;
|
||||
|
||||
if (axis == 1)
|
||||
flags |= BT_AXISSINGLE;
|
||||
else if (axis == 2)
|
||||
flags |= BT_AXIS;
|
||||
|
||||
|
||||
appendPayload(code);
|
||||
appendPayload(flags);
|
||||
appendPayload(amount);
|
||||
appendPayload(map_name);
|
||||
appendPayload(button_name);
|
||||
}
|
||||
}
|
||||
37
app/src/main/java/org/xbmc/kore/eventclient/PacketBYE.java
Normal file
37
app/src/main/java/org/xbmc/kore/eventclient/PacketBYE.java
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
/*
|
||||
* Copyright (C) 2008-2013 Team XBMC
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
*/
|
||||
|
||||
package org.xbmc.kore.eventclient;
|
||||
/**
|
||||
* XBMC Event Client Class
|
||||
*
|
||||
* A BYE packet terminates the connection to XBMC.
|
||||
* @author Stefan Agner
|
||||
*
|
||||
*/
|
||||
public class PacketBYE extends Packet
|
||||
{
|
||||
|
||||
/**
|
||||
* A BYE packet terminates the connection to XBMC.
|
||||
*/
|
||||
public PacketBYE()
|
||||
{
|
||||
super(PT_BYE);
|
||||
}
|
||||
}
|
||||
63
app/src/main/java/org/xbmc/kore/eventclient/PacketHELO.java
Normal file
63
app/src/main/java/org/xbmc/kore/eventclient/PacketHELO.java
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
/*
|
||||
* Copyright (C) 2008-2013 Team XBMC
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
*/
|
||||
|
||||
package org.xbmc.kore.eventclient;
|
||||
|
||||
/**
|
||||
* XBMC Event Client Class
|
||||
*
|
||||
* A HELO packet establishes a valid connection to XBMC. It is the
|
||||
* first packet that should be sent.
|
||||
* @author Stefan Agner
|
||||
*
|
||||
*/
|
||||
public class PacketHELO extends Packet {
|
||||
|
||||
|
||||
/**
|
||||
* A HELO packet establishes a valid connection to XBMC.
|
||||
* @param devicename Name of the device which connects to XBMC
|
||||
*/
|
||||
public PacketHELO(String devicename)
|
||||
{
|
||||
super(PT_HELO);
|
||||
this.appendPayload(devicename);
|
||||
this.appendPayload(ICON_NONE);
|
||||
this.appendPayload((short)0); // port no
|
||||
this.appendPayload(0); // reserved1
|
||||
this.appendPayload(0); // reserved2
|
||||
}
|
||||
|
||||
/**
|
||||
* A HELO packet establishes a valid connection to XBMC.
|
||||
* @param devicename Name of the device which connects to XBMC
|
||||
* @param iconType Type of the icon (Packet.ICON_PNG, Packet.ICON_JPEG or Packet.ICON_GIF)
|
||||
* @param iconData The icon as a Byte-Array
|
||||
*/
|
||||
public PacketHELO(String devicename, byte iconType, byte[] iconData)
|
||||
{
|
||||
super(PT_HELO);
|
||||
this.appendPayload(devicename);
|
||||
this.appendPayload(iconType);
|
||||
this.appendPayload((short)0); // port no
|
||||
this.appendPayload(0); // reserved1
|
||||
this.appendPayload(0); // reserved2
|
||||
this.appendPayload(iconData); // reserved2
|
||||
}
|
||||
|
||||
}
|
||||
48
app/src/main/java/org/xbmc/kore/eventclient/PacketLOG.java
Normal file
48
app/src/main/java/org/xbmc/kore/eventclient/PacketLOG.java
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
/*
|
||||
* Copyright (C) 2008-2013 Team XBMC
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
*/
|
||||
|
||||
package org.xbmc.kore.eventclient;
|
||||
/**
|
||||
* XBMC Event Client Class
|
||||
*
|
||||
* A LOG packet tells XBMC to log the message to xbmc.log with the loglevel as specified.
|
||||
* @author Stefan Agner
|
||||
*
|
||||
*/
|
||||
public class PacketLOG extends Packet {
|
||||
|
||||
/**
|
||||
* A LOG packet tells XBMC to log the message to xbmc.log with the loglevel as specified.
|
||||
* @param loglevel the loglevel, follows XBMC standard.
|
||||
* <ul>
|
||||
* <li>0 = DEBUG</li>
|
||||
* <li>1 = INFO</li>
|
||||
* <li>2 = NOTICE</li>
|
||||
* <li>3 = WARNING</li>
|
||||
* <li>4 = ERROR</li>
|
||||
* <li>5 = SEVERE</li>
|
||||
* </ul>
|
||||
* @param logmessage the message to log
|
||||
*/
|
||||
public PacketLOG(byte loglevel, String logmessage)
|
||||
{
|
||||
super(PT_LOG);
|
||||
appendPayload(loglevel);
|
||||
appendPayload(logmessage);
|
||||
}
|
||||
}
|
||||
45
app/src/main/java/org/xbmc/kore/eventclient/PacketMOUSE.java
Normal file
45
app/src/main/java/org/xbmc/kore/eventclient/PacketMOUSE.java
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
/*
|
||||
* Copyright (C) 2008-2013 Team XBMC
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
*/
|
||||
|
||||
package org.xbmc.kore.eventclient;
|
||||
/**
|
||||
* XBMC Event Client Class
|
||||
*
|
||||
* A MOUSE packets sets the mouse position in XBMC
|
||||
* @author Stefan Agner
|
||||
*
|
||||
*/
|
||||
public class PacketMOUSE extends Packet {
|
||||
|
||||
protected final static byte MS_ABSOLUTE = 0x01;
|
||||
|
||||
/**
|
||||
* A MOUSE packets sets the mouse position in XBMC
|
||||
* @param x horitontal position ranging from 0 to 65535
|
||||
* @param y vertical position ranging from 0 to 65535
|
||||
*/
|
||||
public PacketMOUSE(int x, int y)
|
||||
{
|
||||
super(PT_MOUSE);
|
||||
byte flags = 0;
|
||||
flags |= MS_ABSOLUTE;
|
||||
appendPayload(flags);
|
||||
appendPayload((short)x);
|
||||
appendPayload((short)y);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,71 @@
|
|||
/*
|
||||
* Copyright (C) 2008-2013 Team XBMC
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
*/
|
||||
|
||||
package org.xbmc.kore.eventclient;
|
||||
/**
|
||||
* XBMC Event Client Class
|
||||
*
|
||||
* This packet displays a notification window in XBMC. It can contain
|
||||
* a caption, a message and an icon.
|
||||
* @author Stefan Agner
|
||||
*
|
||||
*/
|
||||
public class PacketNOTIFICATION extends Packet {
|
||||
|
||||
/**
|
||||
* This packet displays a notification window in XBMC.
|
||||
* @param title Message title
|
||||
* @param message The actual message
|
||||
* @param iconType Type of the icon (Packet.ICON_PNG, Packet.ICON_JPEG or Packet.ICON_GIF)
|
||||
* @param iconData The icon as a Byte-Array
|
||||
*/
|
||||
public PacketNOTIFICATION(String title, String message, byte iconType, byte[] iconData)
|
||||
{
|
||||
super(PT_NOTIFICATION);
|
||||
appendPayload(title, message, iconType, iconData);
|
||||
}
|
||||
|
||||
/**
|
||||
* This packet displays a notification window in XBMC.
|
||||
* @param title Message title
|
||||
* @param message The actual message
|
||||
*/
|
||||
public PacketNOTIFICATION(String title, String message)
|
||||
{
|
||||
super(PT_NOTIFICATION);
|
||||
appendPayload(title, message, Packet.ICON_NONE, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Appends the payload to the packet...
|
||||
* @param title Message title
|
||||
* @param message The actual message
|
||||
* @param iconType Type of the icon (Packet.ICON_PNG, Packet.ICON_JPEG or Packet.ICON_GIF)
|
||||
* @param iconData The icon as a Byte-Array
|
||||
*/
|
||||
private void appendPayload(String title, String message, byte iconType, byte[] iconData)
|
||||
{
|
||||
appendPayload(title);
|
||||
appendPayload(message);
|
||||
appendPayload(iconType);
|
||||
appendPayload(0); // reserved
|
||||
if(iconData!=null)
|
||||
appendPayload(iconData);
|
||||
|
||||
}
|
||||
}
|
||||
37
app/src/main/java/org/xbmc/kore/eventclient/PacketPING.java
Normal file
37
app/src/main/java/org/xbmc/kore/eventclient/PacketPING.java
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
/*
|
||||
* Copyright (C) 2008-2013 Team XBMC
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
*/
|
||||
|
||||
package org.xbmc.kore.eventclient;
|
||||
/**
|
||||
* XBMC Event Client Class
|
||||
*
|
||||
* A PING packet tells XBMC that the client is still alive. All valid
|
||||
* packets act as ping (not just this one). A client needs to ping
|
||||
* XBMC at least once in 60 seconds or it will time
|
||||
* @author Stefan Agner
|
||||
*
|
||||
*/
|
||||
public class PacketPING extends Packet {
|
||||
/**
|
||||
* A PING packet tells XBMC that the client is still alive.
|
||||
*/
|
||||
public PacketPING()
|
||||
{
|
||||
super(PT_PING);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
package org.xbmc.kore.host;
|
||||
|
||||
import android.os.Handler;
|
||||
|
||||
import org.xbmc.kore.jsonrpc.ApiCallback;
|
||||
import org.xbmc.kore.jsonrpc.ApiException;
|
||||
import org.xbmc.kore.jsonrpc.ApiMethod;
|
||||
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.Future;
|
||||
|
||||
/**
|
||||
* Superclass that facilitates the execution of composite actions, ie sequence of calls to
|
||||
* {@link org.xbmc.kore.jsonrpc.ApiMethod}, on Kodi, tp be done in a synchronous way, without using callbacks on each
|
||||
* call and globally handling errors.
|
||||
* The goal is to be able to call methods on Kodi using {@link HostConnection#execute(ApiMethod)}, getting back the
|
||||
* future and immediatelly await the result of its completion by calling its {@link Future#get()} method, handling any
|
||||
* errors in a global try/catch block.
|
||||
* This is not a major abstraction, just a helper class that allows for client code to be written similarly to a single
|
||||
* call to {@link HostConnection#execute(ApiMethod, ApiCallback, Handler)} but where the called method is composite.
|
||||
*
|
||||
* Subclasses should implement the abstract method {@link HostCompositeAction#execInBackground()} with the specific
|
||||
* logic that is meant to be executed, knowing that it will be executed in a background thread, thereby allowing
|
||||
* the use of {@link HostConnection#execute(ApiMethod)} and awaiting on the resulting {@link Future#get()}.
|
||||
*
|
||||
* Clients should call {@link HostCompositeAction#execute(HostConnection, ApiCallback, Handler)}, which creates a
|
||||
* background thread, calls runInBackground and sends the result to the given callback.
|
||||
*/
|
||||
public abstract class HostCompositeAction<T> {
|
||||
|
||||
protected HostConnection hostConnection;
|
||||
|
||||
/**
|
||||
* Composite action to be executed synchronously
|
||||
* @return result
|
||||
*/
|
||||
public abstract T execInBackground() throws ExecutionException, InterruptedException;
|
||||
|
||||
/**
|
||||
* Calls {@link HostCompositeAction#execInBackground()} in a background thread, and posts the result through the
|
||||
* given callback on the specified handler
|
||||
*
|
||||
* @param hostConnection Host connection on which to call the method
|
||||
* @param callback Callbacks to post the response to
|
||||
* @param handler Handler to invoke callbacks on
|
||||
*/
|
||||
public void execute(HostConnection hostConnection, ApiCallback<T> callback, Handler handler) {
|
||||
this.hostConnection = hostConnection;
|
||||
// Just a protection
|
||||
if (hostConnection == null) return;
|
||||
|
||||
hostConnection.getExecutorService().execute(() -> {
|
||||
try {
|
||||
T result = execInBackground();
|
||||
handler.post(() -> callback.onSuccess(result));
|
||||
} catch (ExecutionException e) {
|
||||
handler.post(() -> callback.onError(ApiException.API_ERROR, e.getMessage()));
|
||||
} catch (InterruptedException e) {
|
||||
handler.post(() -> callback.onError(ApiException.API_WAITING_ON_RESULT_INTERRUPTED, e.getMessage()));
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
1032
app/src/main/java/org/xbmc/kore/host/HostConnection.java
Normal file
1032
app/src/main/java/org/xbmc/kore/host/HostConnection.java
Normal file
File diff suppressed because it is too large
Load diff
1153
app/src/main/java/org/xbmc/kore/host/HostConnectionObserver.java
Normal file
1153
app/src/main/java/org/xbmc/kore/host/HostConnectionObserver.java
Normal file
File diff suppressed because it is too large
Load diff
389
app/src/main/java/org/xbmc/kore/host/HostInfo.java
Normal file
389
app/src/main/java/org/xbmc/kore/host/HostInfo.java
Normal file
|
|
@ -0,0 +1,389 @@
|
|||
/*
|
||||
* Copyright 2015 Synced Synapse. 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.host;
|
||||
|
||||
import org.xbmc.kore.utils.LogUtils;
|
||||
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.HashMap;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* XBMC Host information container.
|
||||
*/
|
||||
public class HostInfo {
|
||||
private static final String TAG = LogUtils.makeLogTag(HostInfo.class);
|
||||
|
||||
private static final String JSON_RPC_ENDPOINT = "/jsonrpc";
|
||||
|
||||
/**
|
||||
* Default HTTPS port
|
||||
*/
|
||||
public static final int DEFAULT_HTTPS_PORT = 443;
|
||||
|
||||
/**
|
||||
* Default HTTP port for XBMC (80 on Windows, 8080 on others)
|
||||
*/
|
||||
public static final int DEFAULT_HTTP_PORT = 8080;
|
||||
|
||||
/**
|
||||
* Default TCP port for XBMC
|
||||
*/
|
||||
public static final int DEFAULT_TCP_PORT = 9090;
|
||||
|
||||
/**
|
||||
* Default WoL port
|
||||
*/
|
||||
public static final int DEFAULT_WOL_PORT = 9;
|
||||
|
||||
/**
|
||||
* Default EventServer port for Kodi
|
||||
*/
|
||||
public static final int DEFAULT_EVENT_SERVER_PORT = 9777;
|
||||
|
||||
public static final int KODI_V12_FRODO = 12;
|
||||
public static final int KODI_V13_GOTHAM = 13;
|
||||
public static final int KODI_V14_HELIX = 14;
|
||||
public static final int KODI_V15_ISENGARD = 15;
|
||||
public static final int KODI_V16_JARVIS = 16;
|
||||
public static final int KODI_V17_KRYPTON = 17;
|
||||
public static final int KODI_V18_LEIA = 18;
|
||||
public static final int KODI_V19_MATRIX = 19;
|
||||
public static final int KODI_V20_NEXUS = 20;
|
||||
|
||||
public static final int DEFAULT_KODI_VERSION_MAJOR = KODI_V16_JARVIS;
|
||||
public static final int DEFAULT_KODI_VERSION_MINOR = 1;
|
||||
public static final String DEFAULT_KODI_VERSION_REVISION = "Unknown";
|
||||
public static final String DEFAULT_KODI_VERSION_TAG = "stable";
|
||||
|
||||
public static Map<Integer, String> versionNames = new HashMap<>();
|
||||
static {
|
||||
versionNames.put(KODI_V12_FRODO, "Frodo");
|
||||
versionNames.put(KODI_V13_GOTHAM, "Gotham");
|
||||
versionNames.put(KODI_V14_HELIX, "Helix");
|
||||
versionNames.put(KODI_V15_ISENGARD, "Isengard");
|
||||
versionNames.put(KODI_V16_JARVIS, "Jarvis");
|
||||
versionNames.put(KODI_V17_KRYPTON, "Kripton");
|
||||
versionNames.put(KODI_V18_LEIA, "Leia");
|
||||
versionNames.put(KODI_V19_MATRIX, "Matrix");
|
||||
versionNames.put(KODI_V20_NEXUS, "Nexus");
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal id of the host
|
||||
*/
|
||||
private final int id;
|
||||
|
||||
/**
|
||||
* Friendly name of the host
|
||||
*/
|
||||
private final String name;
|
||||
|
||||
/**
|
||||
* Connection information
|
||||
*/
|
||||
private final String address;
|
||||
private final int httpPort;
|
||||
private final int tcpPort;
|
||||
public final boolean isHttps;
|
||||
|
||||
private boolean useEventServer;
|
||||
private final int eventServerPort;
|
||||
|
||||
/**
|
||||
* Authentication information
|
||||
*/
|
||||
private final String username;
|
||||
private final String password;
|
||||
|
||||
/**
|
||||
* Mac address and Wake On Lan port
|
||||
*/
|
||||
private String macAddress;
|
||||
private int wolPort;
|
||||
|
||||
/**
|
||||
* Direct share target
|
||||
*/
|
||||
private boolean showAsDirectShareTarget;
|
||||
|
||||
/**
|
||||
* Prefered protocol to communicate with this host
|
||||
*/
|
||||
private int protocol;
|
||||
|
||||
|
||||
/**
|
||||
* Kodi Version
|
||||
*/
|
||||
private int kodiVersionMajor;
|
||||
private int kodiVersionMinor;
|
||||
private String kodiVersionRevision;
|
||||
private String kodiVersionTag;
|
||||
|
||||
/**
|
||||
* Last time updated (in millis)
|
||||
*/
|
||||
private final long updated;
|
||||
|
||||
private final String auxImageHttpAddress;
|
||||
|
||||
/**
|
||||
* Full constructor. This constructor should be used when instantiating from the database
|
||||
*
|
||||
* @param name Friendly name of the host
|
||||
* @param id ID
|
||||
* @param address URL
|
||||
* @param protocol Protocol
|
||||
* @param httpPort HTTP Port
|
||||
* @param tcpPort TCP Port
|
||||
* @param username Username for basic auth
|
||||
* @param password Password for basic auth
|
||||
*/
|
||||
public HostInfo(int id, String name, String address, int protocol, int httpPort, int tcpPort,
|
||||
String username, String password, String macAddress, int wolPort, boolean showAsDirectShareTarget,
|
||||
boolean useEventServer, int eventServerPort,
|
||||
int kodiVersionMajor, int kodiVersionMinor, String kodiVersionRevision, String kodiVersionTag,
|
||||
long updated, boolean isHttps) {
|
||||
this.id = id;
|
||||
this.name = name;
|
||||
this.address = address;
|
||||
if (!HostConnection.isValidProtocol(protocol)) {
|
||||
throw new IllegalArgumentException("Invalid protocol specified.");
|
||||
}
|
||||
this.protocol = protocol;
|
||||
this.httpPort = httpPort;
|
||||
this.isHttps = isHttps;
|
||||
this.tcpPort = tcpPort;
|
||||
this.username = username;
|
||||
this.password = password;
|
||||
this.macAddress = macAddress;
|
||||
this.wolPort = wolPort;
|
||||
this.showAsDirectShareTarget = showAsDirectShareTarget;
|
||||
|
||||
this.useEventServer = useEventServer;
|
||||
this.eventServerPort = eventServerPort;
|
||||
|
||||
this.kodiVersionMajor = kodiVersionMajor;
|
||||
this.kodiVersionMinor = kodiVersionMinor;
|
||||
this.kodiVersionRevision = kodiVersionRevision;
|
||||
this.kodiVersionTag = kodiVersionTag;
|
||||
this.updated = updated;
|
||||
|
||||
// For performance reasons
|
||||
this.auxImageHttpAddress = getHttpURL() + "/image/";
|
||||
}
|
||||
|
||||
/**
|
||||
* Auxiliary constructor for HTTP protocol.
|
||||
* This constructor should only be used to test connections. It doesn't represent an
|
||||
* instance of the host in the database.
|
||||
*
|
||||
* @param name Friendly name of the host
|
||||
* @param address URL
|
||||
* @param httpPort HTTP Port
|
||||
* @param username Username for basic auth
|
||||
* @param password Password for basic auth
|
||||
*/
|
||||
public HostInfo(String name, String address, int protocol, int httpPort,
|
||||
int tcpPort, String username, String password,
|
||||
boolean useEventServer, int eventServerPort, boolean isHttps,
|
||||
boolean showAsDirectShareTarget) {
|
||||
this(-1, name, address, protocol, httpPort, tcpPort, username,
|
||||
password, null, DEFAULT_WOL_PORT, showAsDirectShareTarget, useEventServer,
|
||||
eventServerPort, DEFAULT_KODI_VERSION_MAJOR, DEFAULT_KODI_VERSION_MINOR,
|
||||
DEFAULT_KODI_VERSION_REVISION, DEFAULT_KODI_VERSION_TAG,
|
||||
0, isHttps);
|
||||
}
|
||||
|
||||
public int getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public String getAddress() {
|
||||
return address;
|
||||
}
|
||||
|
||||
public int getHttpPort() {
|
||||
return httpPort;
|
||||
}
|
||||
|
||||
public int getTcpPort() {
|
||||
return tcpPort;
|
||||
}
|
||||
|
||||
public String getUsername() {
|
||||
return username;
|
||||
}
|
||||
|
||||
public String getPassword() {
|
||||
return password;
|
||||
}
|
||||
|
||||
public String getMacAddress() {
|
||||
return macAddress;
|
||||
}
|
||||
|
||||
public void setMacAddress(String macAddress) {
|
||||
this.macAddress = macAddress;
|
||||
}
|
||||
|
||||
public int getWolPort() {
|
||||
return wolPort;
|
||||
}
|
||||
|
||||
public void setWolPort(int wolPort) {
|
||||
this.wolPort = wolPort;
|
||||
}
|
||||
|
||||
public boolean getShowAsDirectShareTarget() {
|
||||
return showAsDirectShareTarget;
|
||||
}
|
||||
|
||||
public void setShowAsDirectShareTarget(boolean showAsDirectShareTarget) {
|
||||
this.showAsDirectShareTarget = showAsDirectShareTarget;
|
||||
}
|
||||
|
||||
public int getProtocol() {
|
||||
return protocol;
|
||||
}
|
||||
|
||||
public boolean getUseEventServer() {
|
||||
return useEventServer;
|
||||
}
|
||||
|
||||
public int getEventServerPort() {
|
||||
return eventServerPort;
|
||||
}
|
||||
|
||||
public int getKodiVersionMajor() {
|
||||
return kodiVersionMajor;
|
||||
}
|
||||
|
||||
public int getKodiVersionMinor() {
|
||||
return kodiVersionMinor;
|
||||
}
|
||||
|
||||
public String getKodiVersionRevision() {
|
||||
return kodiVersionRevision;
|
||||
}
|
||||
|
||||
public String getKodiVersionTag() {
|
||||
return kodiVersionTag;
|
||||
}
|
||||
|
||||
public String getKodiVersionDesc() {
|
||||
if (versionNames.containsKey(kodiVersionMajor)) {
|
||||
return String.format(Locale.getDefault(), "%s (%d.%d)", versionNames.get(kodiVersionMajor), kodiVersionMajor, kodiVersionMinor);
|
||||
} else {
|
||||
return String.format(Locale.getDefault(), "%d.%d", kodiVersionMajor, kodiVersionMinor);
|
||||
}
|
||||
}
|
||||
|
||||
public long getUpdated() {
|
||||
return updated;
|
||||
}
|
||||
|
||||
/**
|
||||
* Overrides the protocol for this host info
|
||||
* @param protocol Protocol
|
||||
*/
|
||||
public void setProtocol(int protocol) {
|
||||
if (!HostConnection.isValidProtocol(protocol)) {
|
||||
throw new IllegalArgumentException("Invalid protocol specified.");
|
||||
}
|
||||
this.protocol = protocol;
|
||||
}
|
||||
|
||||
/**
|
||||
* Overrides the use of EventServer
|
||||
* @param useEventServer Whether to use EventServer
|
||||
*/
|
||||
public void setUseEventServer(boolean useEventServer) {
|
||||
this.useEventServer = useEventServer;
|
||||
}
|
||||
|
||||
public void setKodiVersionMajor(int kodiVersionMajor) {
|
||||
this.kodiVersionMajor = kodiVersionMajor;
|
||||
}
|
||||
|
||||
public void setKodiVersionMinor(int kodiVersionMinor) {
|
||||
this.kodiVersionMinor = kodiVersionMinor;
|
||||
}
|
||||
|
||||
public void setKodiVersionRevision(String kodiVersionRevision) {
|
||||
this.kodiVersionRevision = kodiVersionRevision;
|
||||
}
|
||||
|
||||
public void setKodiVersionTag(String kodiVersionTag) {
|
||||
this.kodiVersionTag = kodiVersionTag;
|
||||
}
|
||||
|
||||
public boolean isGothamOrLater() {
|
||||
return kodiVersionMajor >= KODI_V13_GOTHAM;
|
||||
}
|
||||
|
||||
public boolean isKryptonOrLater() {
|
||||
return kodiVersionMajor >= KODI_V17_KRYPTON;
|
||||
}
|
||||
|
||||
public boolean isLeiaOrLater() {
|
||||
return kodiVersionMajor >= KODI_V18_LEIA;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the URL of the host
|
||||
* @return HTTP URL eg. http://192.168.1.1:8080
|
||||
*/
|
||||
public String getHttpURL() {
|
||||
String scheme = isHttps ? "https://" : "http://";
|
||||
return scheme + address + ":" + httpPort;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the JSON RPC endpoint URL of the host
|
||||
* @return HTTP URL eg. http://192.168.1.1:8080/jsonrpc
|
||||
*/
|
||||
public String getJsonRpcHttpEndpoint() {
|
||||
return getHttpURL() + JSON_RPC_ENDPOINT;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the URL of an image, given the image identifier returned by XBMC
|
||||
* @param image image identifier stored in XBMC
|
||||
* @return URL on the XBMC host on which the image can be fetched
|
||||
*/
|
||||
public String getImageUrl(String image) {
|
||||
if (image == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
// return getHttpURL() + "/image/" + URLEncoder.encode(image, "UTF-8");
|
||||
return auxImageHttpAddress + URLEncoder.encode(image, StandardCharsets.UTF_8.name());
|
||||
} catch (UnsupportedEncodingException e) {
|
||||
// Ignore for now...
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
524
app/src/main/java/org/xbmc/kore/host/HostManager.java
Normal file
524
app/src/main/java/org/xbmc/kore/host/HostManager.java
Normal file
|
|
@ -0,0 +1,524 @@
|
|||
/*
|
||||
* Copyright 2015 Synced Synapse. 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.host;
|
||||
|
||||
import android.content.ContentValues;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.text.format.DateUtils;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.core.content.pm.ShortcutInfoCompat;
|
||||
import androidx.core.content.pm.ShortcutManagerCompat;
|
||||
import androidx.core.graphics.drawable.IconCompat;
|
||||
import androidx.preference.PreferenceManager;
|
||||
|
||||
import com.squareup.picasso.OkHttp3Downloader;
|
||||
import com.squareup.picasso.Picasso;
|
||||
|
||||
import org.xbmc.kore.R;
|
||||
import org.xbmc.kore.Settings;
|
||||
import org.xbmc.kore.ShareOpenActivity;
|
||||
import org.xbmc.kore.jsonrpc.ApiCallback;
|
||||
import org.xbmc.kore.jsonrpc.method.Application;
|
||||
import org.xbmc.kore.jsonrpc.type.ApplicationType;
|
||||
import org.xbmc.kore.provider.MediaContract;
|
||||
import org.xbmc.kore.utils.LogUtils;
|
||||
import org.xbmc.kore.utils.NetUtils;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import okhttp3.Cache;
|
||||
import okhttp3.OkHttpClient;
|
||||
|
||||
/**
|
||||
* Manages XBMC Hosts
|
||||
* Singleton that loads the list of registered hosts, keeps a
|
||||
* {@link HostConnection} to the active host
|
||||
* and allows for creation and removal of hosts
|
||||
*/
|
||||
public class HostManager {
|
||||
private static final String TAG = LogUtils.makeLogTag(HostManager.class);
|
||||
|
||||
// Singleton instance
|
||||
private static volatile HostManager instance = null;
|
||||
|
||||
private final Context context;
|
||||
|
||||
/**
|
||||
* Arraylist that will hold all the hosts in the database
|
||||
*/
|
||||
private ArrayList<HostInfo> hosts = new ArrayList<>();
|
||||
|
||||
/**
|
||||
* Current host
|
||||
*/
|
||||
private HostInfo currentHostInfo = null;
|
||||
/**
|
||||
* Current host connection
|
||||
*/
|
||||
private HostConnection currentHostConnection = null;
|
||||
|
||||
/**
|
||||
* Picasso to download images from current XBMC
|
||||
*/
|
||||
private Picasso currentPicasso = null;
|
||||
|
||||
/**
|
||||
* Current connection observer
|
||||
*/
|
||||
private HostConnectionObserver currentHostConnectionObserver = null;
|
||||
|
||||
/**
|
||||
* Singleton constructor
|
||||
* @param context Context (can pass Activity context, will get App Context)
|
||||
*/
|
||||
protected HostManager(Context context) {
|
||||
this.context = context.getApplicationContext();
|
||||
}
|
||||
|
||||
/**
|
||||
* Singleton access method
|
||||
* @param context Android app context
|
||||
* @return HostManager singleton
|
||||
*/
|
||||
public static HostManager getInstance(@NonNull Context context) {
|
||||
if (instance == null) {
|
||||
synchronized (HostManager.class) {
|
||||
if (instance == null) {
|
||||
instance = new HostManager(context);
|
||||
}
|
||||
}
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current host list
|
||||
* @return Host list
|
||||
*/
|
||||
public ArrayList<HostInfo> getHosts() {
|
||||
return getHosts(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current host list, maybe forcing a reload from the database
|
||||
* @param forcedReload Whether to force a reload from the database
|
||||
* @return Host list
|
||||
*/
|
||||
public ArrayList<HostInfo> getHosts(boolean forcedReload) {
|
||||
if (forcedReload || (hosts.isEmpty())) {
|
||||
hosts.clear();
|
||||
|
||||
Cursor cursor = context.getContentResolver()
|
||||
.query(MediaContract.Hosts.CONTENT_URI,
|
||||
MediaContract.Hosts.ALL_COLUMNS,
|
||||
null, null, null);
|
||||
if (cursor == null) return hosts;
|
||||
|
||||
if (cursor.getCount() > 0) {
|
||||
while (cursor.moveToNext()) {
|
||||
int idx = 0;
|
||||
int id = cursor.getInt(idx++);
|
||||
long updated = cursor.getLong(idx++);
|
||||
String name = cursor.getString(idx++);
|
||||
String address = cursor.getString(idx++);
|
||||
int protocol = cursor.getInt(idx++);
|
||||
int httpPort = cursor.getInt(idx++);
|
||||
int tcpPort = cursor.getInt(idx++);
|
||||
String username = cursor.getString(idx++);
|
||||
String password = cursor.getString(idx++);
|
||||
String macAddress = cursor.getString(idx++);
|
||||
int wolPort = cursor.getInt(idx++);
|
||||
boolean directShare = (cursor.getInt(idx++) != 0);
|
||||
boolean useEventServer = (cursor.getInt(idx++) != 0);
|
||||
int eventServerPort = cursor.getInt(idx++);
|
||||
|
||||
int kodiVersionMajor = cursor.getInt(idx++);
|
||||
int kodiVersionMinor = cursor.getInt(idx++);
|
||||
String kodiVersionRevision = cursor.getString(idx++);
|
||||
String kodiVersionTag = cursor.getString(idx++);
|
||||
boolean isHttps = (cursor.getInt(idx++) != 0);
|
||||
|
||||
hosts.add(new HostInfo(
|
||||
id, name, address, protocol, httpPort, tcpPort,
|
||||
username, password, macAddress, wolPort, directShare, useEventServer, eventServerPort,
|
||||
kodiVersionMajor, kodiVersionMinor, kodiVersionRevision, kodiVersionTag,
|
||||
updated, isHttps));
|
||||
}
|
||||
}
|
||||
cursor.close();
|
||||
}
|
||||
return hosts;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current active host info
|
||||
* @return Active host info
|
||||
*/
|
||||
public HostInfo getHostInfo() {
|
||||
if (currentHostInfo == null) {
|
||||
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
|
||||
int currentHostId = prefs.getInt(Settings.KEY_PREF_CURRENT_HOST_ID, Settings.DEFAULT_PREF_CURRENT_HOST_ID);
|
||||
|
||||
ArrayList<HostInfo> hosts = getHosts();
|
||||
|
||||
// No host selected. Check if there are hosts configured and default to the first one
|
||||
if (currentHostId == -1) {
|
||||
if (!hosts.isEmpty()) {
|
||||
currentHostInfo = hosts.get(0);
|
||||
currentHostId = currentHostInfo.getId();
|
||||
prefs.edit()
|
||||
.putInt(Settings.KEY_PREF_CURRENT_HOST_ID, currentHostId)
|
||||
.apply();
|
||||
}
|
||||
} else {
|
||||
for (HostInfo host : hosts) {
|
||||
if (host.getId() == currentHostId) {
|
||||
currentHostInfo = host;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return currentHostInfo;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current active host connection
|
||||
* @return Active host connection
|
||||
*/
|
||||
public HostConnection getConnection() {
|
||||
if (currentHostConnection == null) {
|
||||
synchronized (this) {
|
||||
if (currentHostConnection == null) {
|
||||
currentHostInfo = getHostInfo();
|
||||
|
||||
if (currentHostInfo != null) {
|
||||
currentHostConnection = new HostConnection(currentHostInfo);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return currentHostConnection;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current host {@link Picasso} image downloader
|
||||
* @return {@link Picasso} instance suitable to download images from the current xbmc
|
||||
*/
|
||||
public Picasso getPicasso() {
|
||||
if (currentPicasso == null) {
|
||||
currentHostInfo = getHostInfo();
|
||||
if (currentHostInfo != null) {
|
||||
// currentPicasso = new Picasso.Builder(context)
|
||||
// .downloader(new BasicAuthUrlConnectionDownloader(context,
|
||||
// currentHostInfo.getUsername(), currentHostInfo.getPassword()))
|
||||
// .indicatorsEnabled(BuildConfig.DEBUG)
|
||||
// .build();
|
||||
|
||||
// Create the okHttpCliente, with default timeout, authentication and cache
|
||||
File cacheDir = NetUtils.createDefaultCacheDir(context);
|
||||
long cacheSize = NetUtils.calculateDiskCacheSize(cacheDir);
|
||||
OkHttpClient picassoClient = new OkHttpClient.Builder()
|
||||
.connectTimeout(getConnection().getConnectTimeout(), TimeUnit.MILLISECONDS)
|
||||
.authenticator(getConnection().getOkHttpAuthenticator())
|
||||
.cache(new Cache(cacheDir, cacheSize))
|
||||
.build();
|
||||
|
||||
currentPicasso = new Picasso.Builder(context)
|
||||
.downloader(new OkHttp3Downloader(picassoClient))
|
||||
// .indicatorsEnabled(BuildConfig.DEBUG)
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
return currentPicasso;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current {@link HostConnectionObserver} for the current connection
|
||||
* @return The {@link HostConnectionObserver} for the current connection
|
||||
*/
|
||||
public HostConnectionObserver getHostConnectionObserver() {
|
||||
if (currentHostConnectionObserver == null) {
|
||||
currentHostConnection = getConnection();
|
||||
if (currentHostConnection != null) {
|
||||
currentHostConnectionObserver = new HostConnectionObserver(currentHostConnection);
|
||||
}
|
||||
}
|
||||
return currentHostConnectionObserver;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the current host.
|
||||
* @param hostInfo Host info
|
||||
*/
|
||||
public void switchHost(HostInfo hostInfo) {
|
||||
releaseCurrentHost();
|
||||
|
||||
currentHostInfo = hostInfo;
|
||||
if (currentHostInfo != null) {
|
||||
PreferenceManager.getDefaultSharedPreferences(context)
|
||||
.edit()
|
||||
.putInt(Settings.KEY_PREF_CURRENT_HOST_ID, currentHostInfo.getId())
|
||||
.apply();
|
||||
|
||||
// Switched host, update dynamic shortcuts to only include the others
|
||||
updateDynamicShortcuts();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add all kodi hosts, except the current one, to the dynamic shortcuts list
|
||||
* The current one is always accessible via the default intent filters
|
||||
*/
|
||||
private void updateDynamicShortcuts() {
|
||||
ShortcutManagerCompat.removeAllDynamicShortcuts(context);
|
||||
|
||||
ArrayList<HostInfo> hosts = getHosts();
|
||||
for (HostInfo host : hosts) {
|
||||
if (host.getId() != currentHostInfo.getId() &&
|
||||
host.getShowAsDirectShareTarget()) {
|
||||
String id = Integer.toString(host.getId());
|
||||
Intent defaultOpenIntent = new Intent(ShareOpenActivity.DEFAULT_OPEN_ACTION)
|
||||
.setClass(context, ShareOpenActivity.class)
|
||||
.addCategory(ShareOpenActivity.SHARE_TARGET_CATEGORY)
|
||||
.putExtra(ShortcutManagerCompat.EXTRA_SHORTCUT_ID, id);
|
||||
ShortcutInfoCompat shortcut = new ShortcutInfoCompat.Builder(context, id)
|
||||
.setShortLabel(host.getName())
|
||||
.setLongLabel(host.getName())
|
||||
.setIcon(IconCompat.createWithResource(context, R.mipmap.ic_launcher))
|
||||
.setCategories(Collections.singleton(ShareOpenActivity.SHARE_TARGET_CATEGORY))
|
||||
.setIntent(defaultOpenIntent)
|
||||
.build();
|
||||
ShortcutManagerCompat.pushDynamicShortcut(context, shortcut);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new XBMC host to the database
|
||||
* @param hostInfo Host to add
|
||||
* @return Newly created {@link org.xbmc.kore.host.HostInfo}
|
||||
*/
|
||||
public HostInfo addHost(HostInfo hostInfo) {
|
||||
return addHost(hostInfo.getName(), hostInfo.getAddress(), hostInfo.getProtocol(),
|
||||
hostInfo.getHttpPort(), hostInfo.getTcpPort(),
|
||||
hostInfo.getUsername(), hostInfo.getPassword(),
|
||||
hostInfo.getMacAddress(), hostInfo.getWolPort(),
|
||||
hostInfo.getShowAsDirectShareTarget(), hostInfo.getUseEventServer(),
|
||||
hostInfo.getEventServerPort(), hostInfo.getKodiVersionMajor(),
|
||||
hostInfo.getKodiVersionMinor(), hostInfo.getKodiVersionRevision(),
|
||||
hostInfo.getKodiVersionTag(), hostInfo.isHttps);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new XBMC host to the database
|
||||
* @param name Name of this instance
|
||||
* @param address Hostname or IP Address
|
||||
* @param protocol Protocol to use
|
||||
* @param httpPort HTTP port
|
||||
* @param tcpPort TCP port
|
||||
* @param username Username for HTTP
|
||||
* @param password Password for HTTP
|
||||
* @return Newly created {@link org.xbmc.kore.host.HostInfo}
|
||||
*/
|
||||
public HostInfo addHost(String name, String address, int protocol, int httpPort, int tcpPort,
|
||||
String username, String password, String macAddress, int wolPort, boolean directShare,
|
||||
boolean useEventServer, int eventServerPort,
|
||||
int kodiVersionMajor, int kodiVersionMinor, String kodiVersionRevision, String kodiVersionTag,
|
||||
boolean isHttps) {
|
||||
|
||||
ContentValues values = new ContentValues();
|
||||
values.put(MediaContract.HostsColumns.NAME, name);
|
||||
values.put(MediaContract.HostsColumns.ADDRESS, address);
|
||||
values.put(MediaContract.HostsColumns.PROTOCOL, protocol);
|
||||
values.put(MediaContract.HostsColumns.HTTP_PORT, httpPort);
|
||||
values.put(MediaContract.HostsColumns.TCP_PORT, tcpPort);
|
||||
values.put(MediaContract.HostsColumns.USERNAME, username);
|
||||
values.put(MediaContract.HostsColumns.PASSWORD, password);
|
||||
values.put(MediaContract.HostsColumns.MAC_ADDRESS, macAddress);
|
||||
values.put(MediaContract.HostsColumns.WOL_PORT, wolPort);
|
||||
values.put(MediaContract.HostsColumns.DIRECT_SHARE, directShare);
|
||||
values.put(MediaContract.HostsColumns.USE_EVENT_SERVER, useEventServer);
|
||||
values.put(MediaContract.HostsColumns.EVENT_SERVER_PORT, eventServerPort);
|
||||
values.put(MediaContract.HostsColumns.KODI_VERSION_MAJOR, kodiVersionMajor);
|
||||
values.put(MediaContract.HostsColumns.KODI_VERSION_MINOR, kodiVersionMinor);
|
||||
values.put(MediaContract.HostsColumns.KODI_VERSION_REVISION, kodiVersionRevision);
|
||||
values.put(MediaContract.HostsColumns.KODI_VERSION_TAG, kodiVersionTag);
|
||||
values.put(MediaContract.HostsColumns.IS_HTTPS, isHttps);
|
||||
|
||||
Uri newUri = context.getContentResolver()
|
||||
.insert(MediaContract.Hosts.CONTENT_URI, values);
|
||||
long newId = Long.parseLong(MediaContract.Hosts.getHostId(newUri));
|
||||
|
||||
// Refresh the list and return the created host
|
||||
hosts = getHosts(true);
|
||||
HostInfo newHost = null;
|
||||
for (HostInfo host : hosts) {
|
||||
if (host.getId() == newId) {
|
||||
newHost = host;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return newHost;
|
||||
}
|
||||
|
||||
/**
|
||||
* Edits a host on the database
|
||||
* @param hostId Id of the host to edit
|
||||
* @param newHostInfo New values to update
|
||||
* @return New {@link HostInfo} object
|
||||
*/
|
||||
public HostInfo editHost(int hostId, HostInfo newHostInfo) {
|
||||
ContentValues values = new ContentValues();
|
||||
values.put(MediaContract.HostsColumns.NAME, newHostInfo.getName());
|
||||
values.put(MediaContract.HostsColumns.ADDRESS, newHostInfo.getAddress());
|
||||
values.put(MediaContract.HostsColumns.PROTOCOL, newHostInfo.getProtocol());
|
||||
values.put(MediaContract.HostsColumns.HTTP_PORT, newHostInfo.getHttpPort());
|
||||
values.put(MediaContract.HostsColumns.TCP_PORT, newHostInfo.getTcpPort());
|
||||
values.put(MediaContract.HostsColumns.USERNAME, newHostInfo.getUsername());
|
||||
values.put(MediaContract.HostsColumns.PASSWORD, newHostInfo.getPassword());
|
||||
values.put(MediaContract.HostsColumns.MAC_ADDRESS, newHostInfo.getMacAddress());
|
||||
values.put(MediaContract.HostsColumns.WOL_PORT, newHostInfo.getWolPort());
|
||||
values.put(MediaContract.HostsColumns.DIRECT_SHARE, newHostInfo.getShowAsDirectShareTarget());
|
||||
values.put(MediaContract.HostsColumns.USE_EVENT_SERVER, newHostInfo.getUseEventServer());
|
||||
values.put(MediaContract.HostsColumns.EVENT_SERVER_PORT, newHostInfo.getEventServerPort());
|
||||
values.put(MediaContract.HostsColumns.KODI_VERSION_MAJOR, newHostInfo.getKodiVersionMajor());
|
||||
values.put(MediaContract.HostsColumns.KODI_VERSION_MINOR, newHostInfo.getKodiVersionMinor());
|
||||
values.put(MediaContract.HostsColumns.KODI_VERSION_REVISION, newHostInfo.getKodiVersionRevision());
|
||||
values.put(MediaContract.HostsColumns.KODI_VERSION_TAG, newHostInfo.getKodiVersionTag());
|
||||
values.put(MediaContract.HostsColumns.IS_HTTPS, newHostInfo.isHttps);
|
||||
|
||||
context.getContentResolver()
|
||||
.update(MediaContract.Hosts.buildHostUri(hostId), values, null, null);
|
||||
|
||||
// Refresh the list and return the created host
|
||||
hosts = getHosts(true);
|
||||
HostInfo newHost = null;
|
||||
for (HostInfo host : hosts) {
|
||||
if (host.getId() == hostId) {
|
||||
newHost = host;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return newHost;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a host from the database.
|
||||
* If the delete host is the current one, we will try too change the current one to another
|
||||
* or set it to null if there's no other
|
||||
* @param hostId Id of the host to delete
|
||||
*/
|
||||
public void deleteHost(final int hostId) {
|
||||
// Async call delete. The triggers to delete all host information can take some time
|
||||
new Thread(() -> context.getContentResolver()
|
||||
.delete(MediaContract.Hosts.buildHostUri(hostId), null, null)).start();
|
||||
|
||||
// Refresh information
|
||||
int index = -1;
|
||||
for (int i = 0; i < hosts.size(); i++) {
|
||||
if (hosts.get(i).getId() == hostId) {
|
||||
index = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (index != -1)
|
||||
hosts.remove(index);
|
||||
// If we just deleted the current connection, switch to another
|
||||
if ((currentHostInfo != null) && (currentHostInfo.getId() == hostId)) {
|
||||
releaseCurrentHost();
|
||||
if (!hosts.isEmpty())
|
||||
switchHost(hosts.get(0));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Releases all state related to the current connection
|
||||
*/
|
||||
private void releaseCurrentHost() {
|
||||
if (currentHostConnectionObserver != null) {
|
||||
currentHostConnectionObserver.stopObserving();
|
||||
currentHostConnectionObserver = null;
|
||||
}
|
||||
|
||||
if (currentHostConnection != null) {
|
||||
currentHostConnection.disconnect();
|
||||
currentHostConnection = null;
|
||||
}
|
||||
|
||||
if (currentPicasso != null) {
|
||||
// Calling shutdown here causes a picasso error:
|
||||
// Handler (com.squareup.picasso.Stats$StatsHandler) {41b13d40} sending message to a Handler on a dead thread
|
||||
// Check: https://github.com/square/picasso/issues/445
|
||||
// So, for now, just let it be...
|
||||
// currentPicasso.shutdown();
|
||||
currentPicasso = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Check Kodi's version every 2 hours
|
||||
private final static long KODI_VERSION_CHECK_INTERVAL_MILLIS = 2 * DateUtils.HOUR_IN_MILLIS;
|
||||
|
||||
/**
|
||||
* Periodic checks Kodi's version and updates the DB to reflect that.
|
||||
* This should be called somewhere that gets executed periodically
|
||||
*
|
||||
*/
|
||||
public void checkAndUpdateKodiVersion() {
|
||||
if (currentHostInfo == null) {
|
||||
currentHostInfo = getHostInfo();
|
||||
if (currentHostInfo == null) return;
|
||||
}
|
||||
|
||||
if (currentHostInfo.getUpdated() + KODI_VERSION_CHECK_INTERVAL_MILLIS < java.lang.System.currentTimeMillis()) {
|
||||
LogUtils.LOGD(TAG, "Checking Kodi version...");
|
||||
final int checkHostId = currentHostInfo.getId();
|
||||
final Application.GetProperties getProperties = new Application.GetProperties(Application.GetProperties.VERSION);
|
||||
getProperties.execute(getConnection(), new ApiCallback<ApplicationType.PropertyValue>() {
|
||||
@Override
|
||||
public void onSuccess(ApplicationType.PropertyValue result) {
|
||||
// Simple check to see if we didn't switched host in the meantime.
|
||||
// Given that this and all calls to switchHost are run on the UI thread, there's no need for more
|
||||
if (checkHostId != currentHostInfo.getId()) return;
|
||||
LogUtils.LOGD(TAG, "Successfully checked Kodi version.");
|
||||
currentHostInfo.setKodiVersionMajor(result.version.major);
|
||||
currentHostInfo.setKodiVersionMinor(result.version.minor);
|
||||
currentHostInfo.setKodiVersionRevision(result.version.revision);
|
||||
currentHostInfo.setKodiVersionTag(result.version.tag);
|
||||
|
||||
currentHostInfo = editHost(currentHostInfo.getId(), currentHostInfo);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(int errorCode, String description) {
|
||||
// Couldn't get Kodi version... Ignore
|
||||
LogUtils.LOGD(TAG, "Couldn't get Kodi version. Error: " + description);
|
||||
}
|
||||
}, new Handler(Looper.getMainLooper()));
|
||||
}
|
||||
}
|
||||
}
|
||||
176
app/src/main/java/org/xbmc/kore/host/actions/GetPlaylist.java
Normal file
176
app/src/main/java/org/xbmc/kore/host/actions/GetPlaylist.java
Normal file
|
|
@ -0,0 +1,176 @@
|
|||
/*
|
||||
* 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.host.actions;
|
||||
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.xbmc.kore.host.HostCompositeAction;
|
||||
import org.xbmc.kore.host.HostConnection;
|
||||
import org.xbmc.kore.jsonrpc.ApiMethod;
|
||||
import org.xbmc.kore.jsonrpc.method.Playlist;
|
||||
import org.xbmc.kore.jsonrpc.type.ListType;
|
||||
import org.xbmc.kore.jsonrpc.type.PlaylistType;
|
||||
import org.xbmc.kore.utils.LogUtils;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
|
||||
/**
|
||||
* Retrieves the playlist items for the first non-empty playlist or null if no playlists are available.
|
||||
*/
|
||||
public class GetPlaylist extends HostCompositeAction<ArrayList<GetPlaylist.GetPlaylistResult>> {
|
||||
private static final String TAG = LogUtils.makeLogTag(GetPlaylist.class);
|
||||
|
||||
private final static String[] propertiesToGet = new String[] {
|
||||
ListType.FieldsAll.ART,
|
||||
ListType.FieldsAll.ARTIST,
|
||||
ListType.FieldsAll.ALBUMARTIST,
|
||||
ListType.FieldsAll.ALBUM,
|
||||
ListType.FieldsAll.DISPLAYARTIST,
|
||||
ListType.FieldsAll.EPISODE,
|
||||
ListType.FieldsAll.FANART,
|
||||
ListType.FieldsAll.FILE,
|
||||
ListType.FieldsAll.SEASON,
|
||||
ListType.FieldsAll.SHOWTITLE,
|
||||
ListType.FieldsAll.STUDIO,
|
||||
ListType.FieldsAll.TAGLINE,
|
||||
ListType.FieldsAll.THUMBNAIL,
|
||||
ListType.FieldsAll.TITLE,
|
||||
ListType.FieldsAll.TRACK,
|
||||
ListType.FieldsAll.DURATION,
|
||||
ListType.FieldsAll.RUNTIME,
|
||||
};
|
||||
|
||||
static private HashMap<String, Integer> playlistsTypesAndIds;
|
||||
private String playlistType;
|
||||
private int playlistId = -1;
|
||||
|
||||
/**
|
||||
* Use this to get the first non-empty playlist
|
||||
*/
|
||||
public GetPlaylist() {}
|
||||
|
||||
/**
|
||||
* Use this to get a playlist for a specific playlist type
|
||||
* @param playlistType should be one of the types from {@link org.xbmc.kore.jsonrpc.type.PlaylistType.GetPlaylistsReturnType}.
|
||||
* If null the first non-empty playlist is returned.
|
||||
*/
|
||||
public GetPlaylist(String playlistType) {
|
||||
this.playlistType = playlistType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Use this to get a playlist for a specific playlist id
|
||||
* @param playlistId Kodi's playlist id
|
||||
*/
|
||||
public GetPlaylist(int playlistId) {
|
||||
this.playlistId = playlistId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ArrayList<GetPlaylistResult> execInBackground() throws ExecutionException, InterruptedException {
|
||||
if (playlistsTypesAndIds == null)
|
||||
playlistsTypesAndIds = getPlaylists(hostConnection);
|
||||
|
||||
if (playlistType != null) {
|
||||
GetPlaylistResult getPlaylistResult = retrievePlaylistItemsForType(playlistType);
|
||||
ArrayList<GetPlaylistResult> playlists = new ArrayList<>();
|
||||
playlists.add(getPlaylistResult);
|
||||
return playlists;
|
||||
} else if (playlistId > -1 ) {
|
||||
GetPlaylistResult getPlaylistResult = retrievePlaylistItemsForId(playlistId);
|
||||
ArrayList<GetPlaylistResult> playlists = new ArrayList<>();
|
||||
playlists.add(getPlaylistResult);
|
||||
return playlists;
|
||||
} else
|
||||
return retrieveNonEmptyPlaylists();
|
||||
}
|
||||
|
||||
private GetPlaylistResult retrievePlaylistItemsForId(int playlistId)
|
||||
throws InterruptedException, ExecutionException {
|
||||
List<ListType.ItemsAll> playlistItems = retrievePlaylistItems(hostConnection, playlistId);
|
||||
return new GetPlaylistResult(playlistId, getPlaylistType(playlistId), playlistItems);
|
||||
}
|
||||
|
||||
private GetPlaylistResult retrievePlaylistItemsForType(String type)
|
||||
throws InterruptedException, ExecutionException {
|
||||
Integer id = playlistsTypesAndIds.get(type);
|
||||
if (id == null) id = -1;
|
||||
List<ListType.ItemsAll> playlistItems = retrievePlaylistItems(hostConnection, id);
|
||||
return new GetPlaylistResult(id, type, playlistItems);
|
||||
}
|
||||
|
||||
private ArrayList<GetPlaylistResult> retrieveNonEmptyPlaylists()
|
||||
throws InterruptedException, ExecutionException {
|
||||
ArrayList<GetPlaylistResult> playlists = new ArrayList<>();
|
||||
|
||||
for (String type : playlistsTypesAndIds.keySet()) {
|
||||
Integer id = playlistsTypesAndIds.get(type);
|
||||
if (id == null) id = -1;
|
||||
List<ListType.ItemsAll> playlistItems = retrievePlaylistItems(hostConnection, id);
|
||||
if (!playlistItems.isEmpty())
|
||||
playlists.add(new GetPlaylistResult(id, type, playlistItems));
|
||||
}
|
||||
return playlists;
|
||||
}
|
||||
|
||||
private HashMap<String, Integer> getPlaylists(HostConnection hostConnection)
|
||||
throws ExecutionException, InterruptedException {
|
||||
HashMap<String, Integer> playlistsHashMap = new HashMap<>();
|
||||
ArrayList<PlaylistType.GetPlaylistsReturnType> playlistsReturnTypes = hostConnection.execute(new Playlist.GetPlaylists()).get();
|
||||
for (PlaylistType.GetPlaylistsReturnType type : playlistsReturnTypes) {
|
||||
playlistsHashMap.put(type.type, type.playlistid);
|
||||
}
|
||||
return playlistsHashMap;
|
||||
}
|
||||
|
||||
private List<ListType.ItemsAll> retrievePlaylistItems(HostConnection hostConnection, int playlistId)
|
||||
throws InterruptedException, ExecutionException {
|
||||
ApiMethod<List<ListType.ItemsAll>> apiMethod = new Playlist.GetItems(playlistId, propertiesToGet);
|
||||
return hostConnection.execute(apiMethod).get();
|
||||
}
|
||||
|
||||
private String getPlaylistType(int playlistId) {
|
||||
for (String key : playlistsTypesAndIds.keySet()) {
|
||||
Integer id = playlistsTypesAndIds.get(key);
|
||||
if (id != null && id == playlistId)
|
||||
return key;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static class GetPlaylistResult {
|
||||
final public String type;
|
||||
final public int id;
|
||||
final public List<ListType.ItemsAll> items;
|
||||
|
||||
private GetPlaylistResult(int playlistId, String type, List<ListType.ItemsAll> items) {
|
||||
this.id = playlistId;
|
||||
this.type = type;
|
||||
this.items = items;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(@Nullable Object obj) {
|
||||
return obj instanceof GetPlaylistResult &&
|
||||
this.items.equals(((GetPlaylistResult) obj).items);
|
||||
}
|
||||
}
|
||||
}
|
||||
110
app/src/main/java/org/xbmc/kore/host/actions/OpenSharedUrl.java
Normal file
110
app/src/main/java/org/xbmc/kore/host/actions/OpenSharedUrl.java
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
package org.xbmc.kore.host.actions;
|
||||
|
||||
/*
|
||||
* This file is a part of the Kore project.
|
||||
*/
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import org.xbmc.kore.R;
|
||||
import org.xbmc.kore.host.HostCompositeAction;
|
||||
import org.xbmc.kore.host.HostConnection;
|
||||
import org.xbmc.kore.jsonrpc.method.Player;
|
||||
import org.xbmc.kore.jsonrpc.method.Playlist;
|
||||
import org.xbmc.kore.jsonrpc.type.PlayerType;
|
||||
import org.xbmc.kore.jsonrpc.type.PlaylistType;
|
||||
import org.xbmc.kore.utils.LogUtils;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.concurrent.Callable;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
|
||||
/**
|
||||
* Opens or queues a video URL on Kodi.
|
||||
*/
|
||||
public class OpenSharedUrl extends HostCompositeAction<Boolean> {
|
||||
private static final String TAG = LogUtils.makeLogTag(OpenSharedUrl.class);
|
||||
|
||||
private final Context context;
|
||||
private final String url;
|
||||
private final String notificationTitle;
|
||||
private final String notificationText;
|
||||
private final boolean queue;
|
||||
private final int playlistType;
|
||||
|
||||
/**
|
||||
* Creates the composite action
|
||||
* @param context Context
|
||||
* @param url The url to play
|
||||
* @param notificationTitle The title of the notification to be shown when the host is currently playing a video
|
||||
* @param notificationText The notification to be shown when the host is currently playing a video
|
||||
* @param queue Whether to open or queue the item
|
||||
* @param playlistType Playlist type to queue to
|
||||
*/
|
||||
public OpenSharedUrl(Context context, String url, String notificationTitle, String notificationText, boolean queue, int playlistType) {
|
||||
this.context = context;
|
||||
this.url = url;
|
||||
this.notificationTitle = notificationTitle;
|
||||
this.notificationText = notificationText;
|
||||
this.queue = queue;
|
||||
this.playlistType = playlistType;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return whether the host is currently playing a video. If so, the shared url
|
||||
* is added to the playlist and not played immediately.
|
||||
* @throws Error when any of the commands sent fails
|
||||
* @throws InterruptedException when {@code cancel(true)} is called on the resulting
|
||||
* future while waiting on one of the internal futures.
|
||||
*/
|
||||
@Override
|
||||
public Boolean execInBackground() throws ExecutionException, InterruptedException {
|
||||
int stage = R.string.error_get_active_player;
|
||||
try {
|
||||
List<PlayerType.GetActivePlayersReturnType> players =
|
||||
hostConnection.execute(new Player.GetActivePlayers())
|
||||
.get();
|
||||
boolean mediaIsPlaying = false;
|
||||
for (PlayerType.GetActivePlayersReturnType player : players) {
|
||||
if (player.type.equals(PlayerType.GetActivePlayersReturnType.VIDEO)) {
|
||||
mediaIsPlaying = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
stage = R.string.error_queue_media_file;
|
||||
if (!mediaIsPlaying) {
|
||||
LogUtils.LOGD(TAG, "Clearing playlist number " + playlistType);
|
||||
hostConnection.execute(new Playlist.Clear(playlistType))
|
||||
.get();
|
||||
}
|
||||
|
||||
PlaylistType.Item item = new PlaylistType.Item();
|
||||
item.file = url;
|
||||
if (queue) {
|
||||
// Queue media file to playlist:
|
||||
LogUtils.LOGD(TAG, "Queueing file");
|
||||
hostConnection.execute(new Playlist.Add(playlistType, item))
|
||||
.get();
|
||||
|
||||
if (!mediaIsPlaying) {
|
||||
stage = R.string.error_play_media_file;
|
||||
hostConnection.execute(new Player.Open(Player.Open.TYPE_PLAYLIST, playlistType))
|
||||
.get();
|
||||
} else {
|
||||
// no get() to ignore the exception that will be thrown by OkHttp
|
||||
hostConnection.execute(new Player.Notification(notificationTitle, notificationText));
|
||||
}
|
||||
} else {
|
||||
// Don't queue, just play the media file directly:
|
||||
stage = R.string.error_play_media_file;
|
||||
hostConnection.execute(new Player.Open(item))
|
||||
.get();
|
||||
}
|
||||
|
||||
return mediaIsPlaying;
|
||||
} catch (ExecutionException e) {
|
||||
throw new ExecutionException(context.getString(stage, e.getMessage()), e.getCause());
|
||||
}
|
||||
}
|
||||
}
|
||||
45
app/src/main/java/org/xbmc/kore/jsonrpc/ApiCallback.java
Normal file
45
app/src/main/java/org/xbmc/kore/jsonrpc/ApiCallback.java
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
/*
|
||||
* Copyright 2015 Synced Synapse. 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.jsonrpc;
|
||||
|
||||
import org.xbmc.kore.host.HostConnection;
|
||||
|
||||
/**
|
||||
* Callback from a JSON RPC method execution.
|
||||
* When executing a method in JSON RPC, through
|
||||
* {@link HostConnection#execute(ApiMethod, ApiCallback, android.os.Handler)},
|
||||
* an object implementing this interface should be provided, to call after receiving the response
|
||||
* from XBMC. Depending on the response {@link ApiCallback#onSuccess(Object)} or {@link
|
||||
* ApiCallback#onError(int, String)} will be called.
|
||||
* * @param <T> Result type
|
||||
*/
|
||||
public interface ApiCallback<T> {
|
||||
|
||||
/**
|
||||
* Callback that will be called after a sucessfull reponse from the XBMC JSON RPC method
|
||||
* @param result The result that was obtained and sucessfully parsed from XBMC
|
||||
*/
|
||||
void onSuccess(T result);
|
||||
|
||||
/**
|
||||
* Calllback that will be called when an error occurs executing the method on XBMC.
|
||||
* This can be a general error (like a connection error), or an error reported by XBMC (like
|
||||
* an incorrect call)
|
||||
* @param errorCode Error code. Check {@link ApiException} for detailed error codes
|
||||
* @param description Error description
|
||||
*/
|
||||
void onError(int errorCode, String description);
|
||||
}
|
||||
128
app/src/main/java/org/xbmc/kore/jsonrpc/ApiException.java
Normal file
128
app/src/main/java/org/xbmc/kore/jsonrpc/ApiException.java
Normal file
|
|
@ -0,0 +1,128 @@
|
|||
/*
|
||||
* Copyright 2015 Synced Synapse. 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.jsonrpc;
|
||||
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
import org.xbmc.kore.utils.JsonUtils;
|
||||
|
||||
/**
|
||||
* Exception class for errors on JSON API.
|
||||
* Some communication exceptions are catched and casted to this type.
|
||||
* Response error from the JSON API are also returned as an instance of this exception.
|
||||
*/
|
||||
public class ApiException extends Exception {
|
||||
|
||||
/**
|
||||
* We got an invalid JSON response
|
||||
*/
|
||||
public static final int INVALID_JSON_RESPONSE_FROM_HOST = 0;
|
||||
|
||||
/**
|
||||
* IO Exception while connecting
|
||||
*/
|
||||
public static final int IO_EXCEPTION_WHILE_CONNECTING = 1;
|
||||
|
||||
/**
|
||||
* IO Exception while sending
|
||||
*/
|
||||
public static final int IO_EXCEPTION_WHILE_SENDING_REQUEST = 2;
|
||||
|
||||
/**
|
||||
* IO Exception while sending
|
||||
*/
|
||||
public static final int IO_EXCEPTION_WHILE_READING_RESPONSE = 3;
|
||||
|
||||
/**
|
||||
* HTTP response code unknown/unhandled
|
||||
*/
|
||||
public static final int HTTP_RESPONSE_CODE_UNKNOWN = 4;
|
||||
|
||||
/**
|
||||
* HTTP response code unknown/unhandled
|
||||
*/
|
||||
public static final int HTTP_RESPONSE_CODE_UNAUTHORIZED = 5;
|
||||
|
||||
/**
|
||||
* HTTP response code unknown/unhandled
|
||||
*/
|
||||
public static final int HTTP_RESPONSE_CODE_NOT_FOUND = 6;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
public static final int HTTP_HOST_URL_INVALID = 7;
|
||||
|
||||
/**
|
||||
* API returned an error
|
||||
*/
|
||||
public static int API_ERROR = 100;
|
||||
|
||||
/**
|
||||
* Attempted to send a method while not connected to host
|
||||
*/
|
||||
public static int API_NO_CONNECTION = 101;
|
||||
|
||||
/**
|
||||
* Attempted to execute a method with the same id of another already running
|
||||
*/
|
||||
public static int API_METHOD_WITH_SAME_ID_ALREADY_EXECUTING = 102;
|
||||
|
||||
public static int API_WAITING_ON_RESULT_TIMEDOUT = 103;
|
||||
|
||||
public static int API_WAITING_ON_RESULT_INTERRUPTED = 104;
|
||||
|
||||
private final int code;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
* @param code Exception code
|
||||
* @param message Message
|
||||
*/
|
||||
public ApiException(int code, String message) {
|
||||
super(message);
|
||||
this.code = code;
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct exception from other exception
|
||||
* @param code Exception code
|
||||
* @param originalException Original exception
|
||||
*/
|
||||
public ApiException(int code, Exception originalException) {
|
||||
super(originalException);
|
||||
this.code = code;
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct exception from JSON response
|
||||
* @param code Exception code
|
||||
* @param jsonResponse Json response, with an Error node
|
||||
*/
|
||||
public ApiException(int code, ObjectNode jsonResponse) {
|
||||
super((jsonResponse.get(ApiMethod.ERROR_NODE) != null) ?
|
||||
JsonUtils.stringFromJsonNode(jsonResponse.get(ApiMethod.ERROR_NODE), "message") :
|
||||
"No message returned");
|
||||
this.code = code;
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal code of the exception
|
||||
* @return Code of the exception
|
||||
*/
|
||||
public int getCode() {
|
||||
return code;
|
||||
}
|
||||
}
|
||||
111
app/src/main/java/org/xbmc/kore/jsonrpc/ApiFuture.java
Normal file
111
app/src/main/java/org/xbmc/kore/jsonrpc/ApiFuture.java
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
package org.xbmc.kore.jsonrpc;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import java.util.concurrent.CancellationException;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
|
||||
/**
|
||||
* A Java future implementation, with explicit methods to complete the Future
|
||||
* <p>
|
||||
* Don't forget that a call to {@link ApiFuture#get()} blocks the current
|
||||
* thread until it's unblocked by {@link ApiFuture#cancel(boolean)},
|
||||
* {@link ApiFuture#complete(Object)} or {@link ApiFuture#completeExceptionally(Throwable)}
|
||||
*
|
||||
* @param <T> The type of the result returned by {@link ApiFuture#get()}
|
||||
*/
|
||||
public class ApiFuture<T> implements Future<T> {
|
||||
private enum Status { WAITING, OK, ERROR, CANCELLED }
|
||||
private final Object lock = new Object();
|
||||
private Status status = Status.WAITING;
|
||||
private T ok;
|
||||
private Throwable error;
|
||||
|
||||
public ApiFuture() {}
|
||||
|
||||
@Override
|
||||
public T get() throws InterruptedException, ExecutionException {
|
||||
try {
|
||||
return get(0, TimeUnit.MILLISECONDS);
|
||||
} catch (TimeoutException e) {
|
||||
throw new IllegalStateException("Request timed out. This should not happen when time out is disabled!");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public T get(long timeout, @NonNull TimeUnit unit)
|
||||
throws InterruptedException, ExecutionException, TimeoutException
|
||||
{
|
||||
boolean timed = timeout > 0;
|
||||
long remaining = unit.toNanos(timeout);
|
||||
while (true) synchronized (lock) {
|
||||
switch (status) {
|
||||
case OK: return ok;
|
||||
case ERROR: throw new ExecutionException(error);
|
||||
case CANCELLED: throw new CancellationException();
|
||||
case WAITING:
|
||||
if (timed && remaining <= 0) {
|
||||
throw new TimeoutException();
|
||||
}
|
||||
if (!timed) {
|
||||
lock.wait();
|
||||
} else {
|
||||
long start = System.nanoTime();
|
||||
TimeUnit.NANOSECONDS.timedWait(lock, remaining);
|
||||
remaining -= System.nanoTime() - start;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean setResultAndNotify(Status status, T ok, Throwable error) {
|
||||
synchronized (lock) {
|
||||
if (this.status != Status.WAITING) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this.status = status;
|
||||
if (status == Status.OK) this.ok = ok;
|
||||
if (status == Status.ERROR) this.error = error;
|
||||
|
||||
this.lock.notifyAll();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean cancel(boolean b) {
|
||||
return setResultAndNotify(Status.CANCELLED, null, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isCancelled() {
|
||||
return status == Status.CANCELLED;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isDone() {
|
||||
return status != Status.WAITING;
|
||||
}
|
||||
|
||||
/**
|
||||
* If not already completed, sets the value returned by get() to the given value.
|
||||
* @param value - the result value
|
||||
* @return true if this invocation caused this CompletableFuture to transition to a completed state, else false
|
||||
*/
|
||||
public boolean complete(T value) {
|
||||
return setResultAndNotify(Status.OK, value, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* If not already completed, causes invocations of get() to throw the given exception.
|
||||
* @param ex = the exception
|
||||
* @return true if this invocation caused this CompletableFuture to transition to a completed state, else false
|
||||
*/
|
||||
public boolean completeExceptionally(Throwable ex) {
|
||||
return setResultAndNotify(Status.ERROR, null, ex);
|
||||
}
|
||||
}
|
||||
31
app/src/main/java/org/xbmc/kore/jsonrpc/ApiList.java
Normal file
31
app/src/main/java/org/xbmc/kore/jsonrpc/ApiList.java
Normal file
|
|
@ -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.jsonrpc;
|
||||
|
||||
import org.xbmc.kore.jsonrpc.type.ListType;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class ApiList<T> {
|
||||
public final List<T> items;
|
||||
public final ListType.LimitsReturned limits;
|
||||
|
||||
public ApiList(List<T> items, ListType.LimitsReturned limits) {
|
||||
this.items = items;
|
||||
this.limits = limits;
|
||||
}
|
||||
}
|
||||
277
app/src/main/java/org/xbmc/kore/jsonrpc/ApiMethod.java
Normal file
277
app/src/main/java/org/xbmc/kore/jsonrpc/ApiMethod.java
Normal file
|
|
@ -0,0 +1,277 @@
|
|||
/*
|
||||
* Copyright 2015 Synced Synapse. 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.jsonrpc;
|
||||
|
||||
|
||||
import android.os.Handler;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.node.ArrayNode;
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
|
||||
import org.xbmc.kore.host.HostConnection;
|
||||
import org.xbmc.kore.jsonrpc.type.ApiParameter;
|
||||
import org.xbmc.kore.utils.LogUtils;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* Abstract class base of all the JSON RPC API calls
|
||||
*
|
||||
* Every subclass represents a method on the JSON RPC API.
|
||||
*
|
||||
* Each subclass should implement constructors to represent each of the API call variations, and
|
||||
* call this class {@link #execute(HostConnection, ApiCallback, android.os.Handler) execute()} to send
|
||||
* the call to the server.
|
||||
*
|
||||
* This class is a template which should be typed with the return type of specific the method call.
|
||||
*/
|
||||
public abstract class ApiMethod<T> {
|
||||
private static final String TAG = LogUtils.makeLogTag(ApiMethod.class);
|
||||
|
||||
public static final String RESULT_NODE = "result";
|
||||
public static final String ERROR_NODE = "error";
|
||||
public static final String ID_NODE = "id";
|
||||
public static final String METHOD_NODE = "method";
|
||||
public static final String PARAMS_NODE = "params";
|
||||
|
||||
/**
|
||||
* Id of the method call. Autoincremented for each method call
|
||||
*/
|
||||
private static int lastId = 0;
|
||||
protected final int id;
|
||||
|
||||
protected static final ObjectMapper objectMapper = new ObjectMapper();
|
||||
/**
|
||||
* Json object that will be used to generate the json representation of the current method call
|
||||
*/
|
||||
protected final ObjectNode jsonRequest;
|
||||
|
||||
/**
|
||||
* Constructor, sets up the necessary items to make the call later
|
||||
*/
|
||||
public ApiMethod() {
|
||||
this(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor, sets up the necessary items to make the call later
|
||||
*/
|
||||
public ApiMethod(boolean sendId) {
|
||||
// Create the rpc request object with the common fields according to JSON RPC spec
|
||||
jsonRequest = objectMapper.createObjectNode();
|
||||
jsonRequest.put("jsonrpc", "2.0");
|
||||
jsonRequest.put(METHOD_NODE, getMethodName());
|
||||
|
||||
if(sendId) {
|
||||
synchronized (this) {
|
||||
this.id = (++lastId % 10000);
|
||||
}
|
||||
jsonRequest.put(ID_NODE, id);
|
||||
}
|
||||
else {
|
||||
id = -1;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the parameters node of the json request object
|
||||
* Creates one if necessary
|
||||
* @return Parameters node
|
||||
*/
|
||||
protected ObjectNode getParametersNode() {
|
||||
ObjectNode params;
|
||||
if (jsonRequest.has(PARAMS_NODE)) {
|
||||
params = (ObjectNode)jsonRequest.get(PARAMS_NODE);
|
||||
} else {
|
||||
params = objectMapper.createObjectNode();
|
||||
jsonRequest.set(PARAMS_NODE, params);
|
||||
}
|
||||
|
||||
return params;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a parameter to the request
|
||||
* @param parameter Parameter name
|
||||
* @param value Value to add
|
||||
*/
|
||||
protected void addParameterToRequest(String parameter, int value) {
|
||||
getParametersNode().put(parameter, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a parameter to the request
|
||||
* @param parameter Parameter name
|
||||
* @param value Value to add
|
||||
*/
|
||||
protected void addParameterToRequest(String parameter, String value) {
|
||||
if (value != null)
|
||||
getParametersNode().put(parameter, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a parameter to the request
|
||||
* @param parameter Parameter name
|
||||
* @param value Value to add
|
||||
*/
|
||||
protected void addParameterToRequest(String parameter, Integer value) {
|
||||
if (value != null)
|
||||
getParametersNode().put(parameter, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a parameter to the request
|
||||
* @param parameter Parameter name
|
||||
* @param value Value to add
|
||||
*/
|
||||
protected void addParameterToRequest(String parameter, Double value) {
|
||||
if (value != null)
|
||||
getParametersNode().put(parameter, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a parameter to the request
|
||||
* @param parameter Parameter name
|
||||
* @param value Value to add
|
||||
*/
|
||||
protected void addParameterToRequest(String parameter, boolean value) {
|
||||
getParametersNode().put(parameter, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a parameter to the request
|
||||
* @param parameter Parameter name
|
||||
* @param values Values to add
|
||||
*/
|
||||
protected void addParameterToRequest(String parameter, String[] values) {
|
||||
if (values != null) {
|
||||
final ArrayNode arrayNode = objectMapper.createArrayNode();
|
||||
for (String value : values) {
|
||||
arrayNode.add(value);
|
||||
}
|
||||
getParametersNode().set(parameter, arrayNode);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a parameter to the request
|
||||
* @param parameter Parameter name
|
||||
* @param value Value to add
|
||||
*/
|
||||
protected void addParameterToRequest(String parameter, ApiParameter value) {
|
||||
if (value != null)
|
||||
getParametersNode().set(parameter, value.toJsonNode());
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a parameter to the request
|
||||
* @param parameter Parameter name
|
||||
* @param value Value to add
|
||||
*/
|
||||
protected void addParameterToRequest(String parameter, JsonNode value) {
|
||||
if (value != null)
|
||||
getParametersNode().set(parameter, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the id to identify the current method call.
|
||||
* An id is generated for each object that is created.
|
||||
* @return Method call id
|
||||
*/
|
||||
public int getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the string json representation of the current method.
|
||||
* @return Json string representation of the current method
|
||||
*/
|
||||
public String toJsonString() { return jsonRequest.toString(); }
|
||||
|
||||
/**
|
||||
* Returns the json object representation of the current method.
|
||||
* @return JsonObject representation of the current method
|
||||
*/
|
||||
public ObjectNode toJsonObject() { return jsonRequest; }
|
||||
|
||||
/**
|
||||
* Calls the method represented by this object on the server asynchronously.
|
||||
* The results will be posted through the callback function on the specified handler.
|
||||
*
|
||||
* @param hostConnection Host connection on which to call the method
|
||||
* @param callback Callbacks to post the response to
|
||||
* @param handler Handler to invoke callbacks on
|
||||
*/
|
||||
public void execute(HostConnection hostConnection, ApiCallback<T> callback, Handler handler) {
|
||||
if (hostConnection != null) {
|
||||
hostConnection.execute(this, callback, handler);
|
||||
} else if (callback != null) {
|
||||
callback.onError(ApiException.API_NO_CONNECTION, "No connection specified.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current method name
|
||||
* @return Current method name
|
||||
*/
|
||||
public abstract String getMethodName();
|
||||
|
||||
/**
|
||||
* Constructs an object of this method's return type from a json response.
|
||||
* This method must be implemented by each subcall to parse the json reponse and create
|
||||
* an return object of the appropriate type for this api method.
|
||||
*
|
||||
* @param jsonResult Json response obtained from a call
|
||||
* @return Result object of the appropriate type for this api method
|
||||
*/
|
||||
public T resultFromJson(String jsonResult) throws ApiException{
|
||||
try {
|
||||
return resultFromJson((ObjectNode)objectMapper.readTree(jsonResult));
|
||||
} catch (IOException e) {
|
||||
throw new ApiException(ApiException.INVALID_JSON_RESPONSE_FROM_HOST, e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs an object of this method's return type from a json response.
|
||||
* This method must be implemented by each subcall to parse the json reponse and create
|
||||
* an return object of the appropriate type for this api method.
|
||||
*
|
||||
* @param jsonObject Json response obtained from a call
|
||||
* @return Result object of the appropriate type for this api method
|
||||
*/
|
||||
public abstract T resultFromJson(ObjectNode jsonObject) throws ApiException;
|
||||
|
||||
/**
|
||||
* Default callback for methods which the result doesnt matter
|
||||
*/
|
||||
public static <T> ApiCallback<T> getDefaultActionCallback() {
|
||||
|
||||
return new ApiCallback<T>() {
|
||||
@Override
|
||||
public void onSuccess(T result) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(int errorCode, String description) {
|
||||
LogUtils.LOGD(TAG, "Got an error calling a method. Error code: " + errorCode + ", description: " + description);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
80
app/src/main/java/org/xbmc/kore/jsonrpc/ApiNotification.java
Normal file
80
app/src/main/java/org/xbmc/kore/jsonrpc/ApiNotification.java
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
/*
|
||||
* Copyright 2015 Synced Synapse. 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.jsonrpc;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
import org.xbmc.kore.jsonrpc.notification.Player;
|
||||
|
||||
/**
|
||||
* Abstract class, based of all the JSON RPC notifications
|
||||
*
|
||||
* Each specific notification should be a subclass of this.
|
||||
*/
|
||||
public abstract class ApiNotification {
|
||||
public static final String METHOD_NODE = "method";
|
||||
public static final String PARAMS_NODE = "params";
|
||||
|
||||
public final String sender;
|
||||
|
||||
/**
|
||||
* Constructor from a notification node (starting on "params" node)
|
||||
* @param node node
|
||||
*/
|
||||
public ApiNotification(ObjectNode node) {
|
||||
sender = node.get("sender").textValue();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns this notification name
|
||||
*/
|
||||
public abstract String getNotificationName();
|
||||
|
||||
/**
|
||||
* Returns a specific notification present in the Json Node
|
||||
*
|
||||
* @param node Json node with notification
|
||||
* @return Specific notification object
|
||||
*/
|
||||
public static ApiNotification notificationFromJsonNode(JsonNode node) {
|
||||
String method = node.get(METHOD_NODE).asText();
|
||||
ObjectNode params = (ObjectNode)node.get(PARAMS_NODE);
|
||||
|
||||
ApiNotification result = null;
|
||||
switch (method) {
|
||||
case Player.OnPause.NOTIFICATION_NAME:
|
||||
result = new Player.OnPause(params);
|
||||
break;
|
||||
case Player.OnPlay.NOTIFICATION_NAME:
|
||||
result = new Player.OnPlay(params);
|
||||
break;
|
||||
case Player.OnResume.NOTIFICATION_NAME:
|
||||
result = new Player.OnResume(params);
|
||||
break;
|
||||
case Player.OnSeek.NOTIFICATION_NAME:
|
||||
result = new Player.OnSeek(params);
|
||||
break;
|
||||
case Player.OnSpeedChanged.NOTIFICATION_NAME:
|
||||
result = new Player.OnSpeedChanged(params);
|
||||
break;
|
||||
case Player.OnStop.NOTIFICATION_NAME:
|
||||
result = new Player.OnStop(params);
|
||||
break;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
/*
|
||||
* Copyright 2015 Synced Synapse. 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.jsonrpc.event;
|
||||
|
||||
import android.os.Bundle;
|
||||
|
||||
import org.xbmc.kore.service.library.LibrarySyncService;
|
||||
|
||||
/**
|
||||
* Event to post on {@link de.greenrobot.event.EventBus} that notifies of a sync
|
||||
*/
|
||||
public class MediaSyncEvent {
|
||||
public static final int STATUS_FAIL = 0;
|
||||
public static final int STATUS_SUCCESS = 1;
|
||||
|
||||
public final String syncType;
|
||||
public final int status;
|
||||
public final int errorCode;
|
||||
public final String errorMessage;
|
||||
public final Bundle syncExtras;
|
||||
|
||||
/**
|
||||
* Creates a new sync event
|
||||
*
|
||||
* @param syncType One of the constants in {@link LibrarySyncService}
|
||||
*/
|
||||
public MediaSyncEvent(String syncType, Bundle syncExtras, int status) {
|
||||
this(syncType, syncExtras, status, -1, null);
|
||||
// Assert that status is success
|
||||
if (status != STATUS_SUCCESS)
|
||||
throw new IllegalArgumentException("This MediaSyncEvent constructor should only be " +
|
||||
"called with a successful status.");
|
||||
}
|
||||
|
||||
public MediaSyncEvent(String syncType, Bundle syncExtras,
|
||||
int status, int errorCode, String errorMessage) {
|
||||
this.syncType = syncType;
|
||||
this.syncExtras = syncExtras;
|
||||
this.status = status;
|
||||
this.errorCode = errorCode;
|
||||
this.errorMessage = errorMessage;
|
||||
}
|
||||
}
|
||||
168
app/src/main/java/org/xbmc/kore/jsonrpc/method/Addons.java
Normal file
168
app/src/main/java/org/xbmc/kore/jsonrpc/method/Addons.java
Normal file
|
|
@ -0,0 +1,168 @@
|
|||
/*
|
||||
* Copyright 2015 Synced Synapse. 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.jsonrpc.method;
|
||||
|
||||
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.ApiException;
|
||||
import org.xbmc.kore.jsonrpc.ApiMethod;
|
||||
import org.xbmc.kore.jsonrpc.type.AddonType;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* JSON RPC methods in Addons.*
|
||||
*/
|
||||
public class Addons {
|
||||
|
||||
/**
|
||||
* Executes the given addon with the given parameters (if possible)
|
||||
*/
|
||||
public static final class ExecuteAddon extends ApiMethod<String> {
|
||||
public final static String METHOD_NAME = "Addons.ExecuteAddon";
|
||||
|
||||
/**
|
||||
* Known addon ids
|
||||
*/
|
||||
public final static String ADDON_SUBTITLES = "script.xbmc.subtitles";
|
||||
|
||||
/**
|
||||
* Executes the given addon with the given parameters (if possible)
|
||||
*/
|
||||
public ExecuteAddon(String addonId) {
|
||||
super();
|
||||
addParameterToRequest("addonid", addonId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getMethodName() {
|
||||
return METHOD_NAME;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String resultFromJson(ObjectNode jsonObject) throws ApiException {
|
||||
return jsonObject.get(RESULT_NODE).textValue();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all available addons
|
||||
*/
|
||||
public static final class GetAddons extends ApiMethod<List<AddonType.Details>> {
|
||||
public final static String METHOD_NAME = "Addons.GetAddons";
|
||||
|
||||
private final static String LIST_NODE = "addons";
|
||||
|
||||
/**
|
||||
* Gets all available addons
|
||||
* @param enabled Whether to get enabled addons
|
||||
* @param properties Properties to retrieve. See {AddonType.Fields}
|
||||
*/
|
||||
public GetAddons(boolean enabled, String... properties) {
|
||||
super();
|
||||
addParameterToRequest("enabled", enabled);
|
||||
addParameterToRequest("properties", properties);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all available addons
|
||||
* @param properties Properties to retrieve. See {AddonType.Fields}
|
||||
*/
|
||||
public GetAddons(String... properties) {
|
||||
super();
|
||||
addParameterToRequest("properties", properties);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getMethodName() {
|
||||
return METHOD_NAME;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<AddonType.Details> resultFromJson(ObjectNode jsonObject) throws ApiException {
|
||||
JsonNode resultNode = jsonObject.get(RESULT_NODE);
|
||||
ArrayNode items = resultNode.has(LIST_NODE) ?
|
||||
(ArrayNode)resultNode.get(LIST_NODE) : null;
|
||||
if (items == null) {
|
||||
return new ArrayList<>(0);
|
||||
}
|
||||
ArrayList<AddonType.Details> result = new ArrayList<>(items.size());
|
||||
|
||||
for (JsonNode item : items) {
|
||||
result.add(new AddonType.Details(item));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the details of a specific addon
|
||||
*/
|
||||
public static final class GetAddonDetails extends ApiMethod<AddonType.Details> {
|
||||
public final static String METHOD_NAME = "Addons.GetAddonDetails";
|
||||
|
||||
/**
|
||||
* Gets the details of a specific addon
|
||||
* @param addonid Addon id
|
||||
* @param properties Properties to retrieve. See {AddonType.Fields}
|
||||
*/
|
||||
public GetAddonDetails(String addonid, String... properties) {
|
||||
super();
|
||||
addParameterToRequest("addonid", addonid);
|
||||
addParameterToRequest("properties", properties);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getMethodName() {
|
||||
return METHOD_NAME;
|
||||
}
|
||||
|
||||
@Override
|
||||
public AddonType.Details resultFromJson(ObjectNode jsonObject) throws ApiException {
|
||||
return new AddonType.Details(jsonObject.get(RESULT_NODE).get("addon"));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Enables/Disables a specific addon
|
||||
*/
|
||||
public static final class SetAddonEnabled extends ApiMethod<String> {
|
||||
public final static String METHOD_NAME = "Addons.SetAddonEnabled";
|
||||
|
||||
/**
|
||||
* Enables/Disables a specific addon
|
||||
*/
|
||||
public SetAddonEnabled(String addonId, boolean enabled) {
|
||||
super();
|
||||
addParameterToRequest("addonid", addonId);
|
||||
addParameterToRequest("enabled", enabled);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getMethodName() {
|
||||
return METHOD_NAME;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String resultFromJson(ObjectNode jsonObject) throws ApiException {
|
||||
return jsonObject.get(RESULT_NODE).textValue();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
140
app/src/main/java/org/xbmc/kore/jsonrpc/method/Application.java
Normal file
140
app/src/main/java/org/xbmc/kore/jsonrpc/method/Application.java
Normal file
|
|
@ -0,0 +1,140 @@
|
|||
/*
|
||||
* Copyright 2015 Synced Synapse. 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.jsonrpc.method;
|
||||
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
import org.xbmc.kore.jsonrpc.ApiException;
|
||||
import org.xbmc.kore.jsonrpc.ApiMethod;
|
||||
import org.xbmc.kore.jsonrpc.type.ApplicationType;
|
||||
import org.xbmc.kore.utils.JsonUtils;
|
||||
|
||||
/**
|
||||
* All JSON RPC methods in Application.*
|
||||
*/
|
||||
public class Application {
|
||||
|
||||
/**
|
||||
* Quit application
|
||||
*/
|
||||
public static final class Quit extends ApiMethod<String> {
|
||||
public final static String METHOD_NAME = "Application.Quit";
|
||||
|
||||
/**
|
||||
* Quit application
|
||||
*/
|
||||
public Quit() {
|
||||
super();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getMethodName() {
|
||||
return METHOD_NAME;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String resultFromJson(ObjectNode jsonObject) throws ApiException {
|
||||
return jsonObject.get(RESULT_NODE).textValue();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the current volume
|
||||
*/
|
||||
public static final class SetVolume extends ApiMethod<Integer> {
|
||||
public final static String METHOD_NAME = "Application.SetVolume";
|
||||
|
||||
/**
|
||||
* Increment or decrement the volume
|
||||
* @param volume String enum in {@link org.xbmc.kore.jsonrpc.type.GlobalType.IncrementDecrement}
|
||||
*/
|
||||
public SetVolume(String volume) {
|
||||
super();
|
||||
addParameterToRequest("volume", volume);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the volume
|
||||
* @param volume volume between 0 and 100
|
||||
*/
|
||||
public SetVolume(int volume) {
|
||||
super();
|
||||
addParameterToRequest("volume", volume);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getMethodName() { return METHOD_NAME; }
|
||||
|
||||
@Override
|
||||
public Integer resultFromJson(ObjectNode jsonObject) throws ApiException {
|
||||
return JsonUtils.intFromJsonNode(jsonObject, RESULT_NODE);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle mute/unmute
|
||||
*/
|
||||
public static final class SetMute extends ApiMethod<Boolean> {
|
||||
public final static String METHOD_NAME = "Application.SetMute";
|
||||
|
||||
/**
|
||||
* Toggle mute/unmute
|
||||
*/
|
||||
public SetMute() {
|
||||
super();
|
||||
addParameterToRequest("mute", "toggle");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getMethodName() { return METHOD_NAME; }
|
||||
|
||||
@Override
|
||||
public Boolean resultFromJson(ObjectNode jsonObject) throws ApiException {
|
||||
return JsonUtils.booleanFromJsonNode(jsonObject, RESULT_NODE);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the values of the given properties.
|
||||
*/
|
||||
public static class GetProperties extends ApiMethod<ApplicationType.PropertyValue> {
|
||||
public final static String METHOD_NAME = "Application.GetProperties";
|
||||
|
||||
/**
|
||||
* Properties
|
||||
*/
|
||||
public final static String VOLUME = "volume";
|
||||
public final static String MUTED = "muted";
|
||||
public final static String NAME = "name";
|
||||
public final static String VERSION = "version";
|
||||
|
||||
/**
|
||||
* Retrieves the values of the given properties.
|
||||
* @param properties See this class constants.
|
||||
*/
|
||||
public GetProperties(String... properties) {
|
||||
super();
|
||||
addParameterToRequest("properties", properties);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getMethodName() { return METHOD_NAME; }
|
||||
|
||||
@Override
|
||||
public ApplicationType.PropertyValue resultFromJson(ObjectNode jsonObject) throws ApiException {
|
||||
return new ApplicationType.PropertyValue(jsonObject.get(RESULT_NODE));
|
||||
}
|
||||
}
|
||||
}
|
||||
306
app/src/main/java/org/xbmc/kore/jsonrpc/method/AudioLibrary.java
Normal file
306
app/src/main/java/org/xbmc/kore/jsonrpc/method/AudioLibrary.java
Normal file
|
|
@ -0,0 +1,306 @@
|
|||
/*
|
||||
* Copyright 2015 Synced Synapse. 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.jsonrpc.method;
|
||||
|
||||
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.ApiException;
|
||||
import org.xbmc.kore.jsonrpc.ApiList;
|
||||
import org.xbmc.kore.jsonrpc.ApiMethod;
|
||||
import org.xbmc.kore.jsonrpc.type.AudioType;
|
||||
import org.xbmc.kore.jsonrpc.type.LibraryType;
|
||||
import org.xbmc.kore.jsonrpc.type.ListType;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* JSON RPC methods in AudioLibrary.*
|
||||
*/
|
||||
public class AudioLibrary {
|
||||
|
||||
/**
|
||||
* Cleans the audio library from non-existent items.
|
||||
*/
|
||||
public static class Clean extends ApiMethod<String> {
|
||||
public final static String METHOD_NAME = "AudioLibrary.Clean";
|
||||
|
||||
/**
|
||||
* Cleans the video library from non-existent items.
|
||||
*/
|
||||
public Clean() {
|
||||
super();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getMethodName() {
|
||||
return METHOD_NAME;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String resultFromJson(ObjectNode jsonObject) throws ApiException {
|
||||
return jsonObject.get(RESULT_NODE).textValue();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Scans the audio sources for new library items.
|
||||
*/
|
||||
public static class Scan extends ApiMethod<String> {
|
||||
public final static String METHOD_NAME = "AudioLibrary.Scan";
|
||||
|
||||
/**
|
||||
* Scans the audio sources for new library items.
|
||||
*/
|
||||
public Scan() {
|
||||
super();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getMethodName() {
|
||||
return METHOD_NAME;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String resultFromJson(ObjectNode jsonObject) throws ApiException {
|
||||
return jsonObject.get(RESULT_NODE).textValue();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve all artists
|
||||
*/
|
||||
public static class GetArtists extends ApiMethod<ApiList<AudioType.DetailsArtist>> {
|
||||
public final static String METHOD_NAME = "AudioLibrary.GetArtists";
|
||||
|
||||
private final static String LIST_NODE = "artists";
|
||||
|
||||
/**
|
||||
* Retrieve all artists
|
||||
*
|
||||
* @param albumartistsonly Whether or not to include artists only appearing in
|
||||
* compilations. If the parameter is not passed or is passed as
|
||||
* null the GUI setting will be used
|
||||
* @param properties Properties to retrieve. See {@link AudioType.FieldsArtists} for a
|
||||
* list of accepted values
|
||||
*/
|
||||
public GetArtists(boolean albumartistsonly, String... properties) {
|
||||
super();
|
||||
addParameterToRequest("albumartistsonly", albumartistsonly);
|
||||
addParameterToRequest("properties", properties);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve all artists with limits
|
||||
*
|
||||
* @param limits Limits to retrieve. See {@link ListType.Limits}
|
||||
* @param albumartistsonly Whether or not to include artists only appearing in
|
||||
* compilations. If the parameter is not passed or is passed as
|
||||
* null the GUI setting will be used
|
||||
* @param properties Properties to retrieve. See {@link AudioType.FieldsArtists} for a
|
||||
* list of accepted values
|
||||
*/
|
||||
public GetArtists(ListType.Limits limits, boolean albumartistsonly, String... properties) {
|
||||
super();
|
||||
addParameterToRequest("limits", limits);
|
||||
addParameterToRequest("albumartistsonly", albumartistsonly);
|
||||
addParameterToRequest("properties", properties);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getMethodName() {
|
||||
return METHOD_NAME;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ApiList<AudioType.DetailsArtist> resultFromJson(ObjectNode jsonObject) throws ApiException {
|
||||
ListType.LimitsReturned limits = new ListType.LimitsReturned(jsonObject);
|
||||
|
||||
JsonNode resultNode = jsonObject.get(RESULT_NODE);
|
||||
ArrayNode items = resultNode.has(LIST_NODE) ?
|
||||
(ArrayNode)resultNode.get(LIST_NODE) : null;
|
||||
if (items == null) {
|
||||
return new ApiList<>(new ArrayList<>(0), limits);
|
||||
}
|
||||
ArrayList<AudioType.DetailsArtist> result = new ArrayList<>(items.size());
|
||||
|
||||
for (JsonNode item : items) {
|
||||
result.add(new AudioType.DetailsArtist(item));
|
||||
}
|
||||
|
||||
return new ApiList<>(result, limits);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve all albums from specified artist or genre
|
||||
*/
|
||||
public static class GetAlbums extends ApiMethod<ApiList<AudioType.DetailsAlbum>> {
|
||||
public final static String METHOD_NAME = "AudioLibrary.GetAlbums";
|
||||
|
||||
private final static String LIST_NODE = "albums";
|
||||
|
||||
/**
|
||||
* Retrieve all albums
|
||||
*
|
||||
* @param properties Properties to retrieve. See {@link AudioType.FieldsAlbum} for a
|
||||
* list of accepted values
|
||||
*/
|
||||
public GetAlbums(String... properties) {
|
||||
super();
|
||||
addParameterToRequest("properties", properties);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve all albums with limits
|
||||
*
|
||||
* @param limits Limits to retrieve. See {@link ListType.Limits}
|
||||
* @param properties Properties to retrieve. See {@link AudioType.FieldsAlbum} for a
|
||||
* list of accepted values
|
||||
*/
|
||||
public GetAlbums(ListType.Limits limits, String... properties) {
|
||||
super();
|
||||
addParameterToRequest("limits", limits);
|
||||
addParameterToRequest("properties", properties);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getMethodName() {
|
||||
return METHOD_NAME;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ApiList<AudioType.DetailsAlbum> resultFromJson(ObjectNode jsonObject)
|
||||
throws ApiException {
|
||||
ListType.LimitsReturned limits = new ListType.LimitsReturned(jsonObject);
|
||||
|
||||
JsonNode resultNode = jsonObject.get(RESULT_NODE);
|
||||
ArrayNode items = resultNode.has(LIST_NODE) ?
|
||||
(ArrayNode)resultNode.get(LIST_NODE) : null;
|
||||
if (items == null) {
|
||||
return new ApiList<>(new ArrayList<>(0), limits);
|
||||
}
|
||||
ArrayList<AudioType.DetailsAlbum> result = new ArrayList<>(items.size());
|
||||
for (JsonNode item : items) {
|
||||
result.add(new AudioType.DetailsAlbum(item));
|
||||
}
|
||||
|
||||
return new ApiList<>(result, limits);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve all genres
|
||||
*/
|
||||
public static class GetGenres extends ApiMethod<List<LibraryType.DetailsGenre>> {
|
||||
public final static String METHOD_NAME = "AudioLibrary.GetGenres";
|
||||
|
||||
private final static String LIST_NODE = "genres";
|
||||
|
||||
/**
|
||||
* Retrieve all genres
|
||||
*
|
||||
* @param properties Properties to retrieve. See {@link LibraryType.FieldsGenre} for a
|
||||
* list of accepted values
|
||||
*/
|
||||
public GetGenres(String... properties) {
|
||||
super();
|
||||
addParameterToRequest("properties", properties);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getMethodName() {
|
||||
return METHOD_NAME;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<LibraryType.DetailsGenre> resultFromJson(ObjectNode jsonObject)
|
||||
throws ApiException {
|
||||
JsonNode resultNode = jsonObject.get(RESULT_NODE);
|
||||
ArrayNode items = resultNode.has(LIST_NODE) ?
|
||||
(ArrayNode)resultNode.get(LIST_NODE) : null;
|
||||
if (items == null) {
|
||||
return new ArrayList<>(0);
|
||||
}
|
||||
ArrayList<LibraryType.DetailsGenre> result = new ArrayList<>(items.size());
|
||||
for (JsonNode item : items) {
|
||||
result.add(new LibraryType.DetailsGenre(item));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve all songs from specified album, artist or genre
|
||||
*/
|
||||
public static class GetSongs extends ApiMethod<ApiList<AudioType.DetailsSong>> {
|
||||
public final static String METHOD_NAME = "AudioLibrary.GetSongs";
|
||||
|
||||
private final static String LIST_NODE = "songs";
|
||||
|
||||
/**
|
||||
* Retrieve all songs
|
||||
*
|
||||
* @param properties Properties to retrieve. See {@link AudioType.FieldsSong} for a
|
||||
* list of accepted values
|
||||
*/
|
||||
public GetSongs(String... properties) {
|
||||
super();
|
||||
addParameterToRequest("properties", properties);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve all songs with limits
|
||||
*
|
||||
* @param limits Limits to retrieve. See {@link ListType.Limits}
|
||||
* @param properties Properties to retrieve. See {@link AudioType.FieldsSong} for a
|
||||
* list of accepted values
|
||||
*/
|
||||
public GetSongs(ListType.Limits limits, String... properties) {
|
||||
super();
|
||||
addParameterToRequest("limits", limits);
|
||||
addParameterToRequest("properties", properties);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getMethodName() {
|
||||
return METHOD_NAME;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ApiList<AudioType.DetailsSong> resultFromJson(ObjectNode jsonObject)
|
||||
throws ApiException {
|
||||
ListType.LimitsReturned limits = new ListType.LimitsReturned(jsonObject);
|
||||
|
||||
JsonNode resultNode = jsonObject.get(RESULT_NODE);
|
||||
ArrayNode items = resultNode.has(LIST_NODE) ?
|
||||
(ArrayNode)resultNode.get(LIST_NODE) : null;
|
||||
if (items == null) {
|
||||
return new ApiList<>(new ArrayList<>(0), limits);
|
||||
}
|
||||
ArrayList<AudioType.DetailsSong> result = new ArrayList<>(items.size());
|
||||
for (JsonNode item : items) {
|
||||
result.add(new AudioType.DetailsSong(item));
|
||||
}
|
||||
|
||||
return new ApiList<>(result, limits);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue