commit 9972271b9c7ca26db3fb96be4e890e87bd458e66 Author: Synced Synapse Date: Wed Jan 14 11:12:47 2015 +0000 First dump diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7b58b4d --- /dev/null +++ b/.gitignore @@ -0,0 +1,25 @@ +# Gradle +.gradle + +# Generated files +/build + +# IntelliJ +*.iml +*.ipr +*.iws +.idea/ +#/.idea/workspace.xml +#/.idea/libraries + +# Local configuration file (sdk path, etc) +*.properties + +# Keystores +*.keystore + +# Windows +.DS_Store + +# Linux +*~ diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..d645695 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + 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. diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build diff --git a/app/build.gradle b/app/build.gradle new file mode 100644 index 0000000..f7408b7 --- /dev/null +++ b/app/build.gradle @@ -0,0 +1,65 @@ +apply plugin: 'com.android.application' + +android { + compileSdkVersion 21 + buildToolsVersion "21.1.2" + + defaultConfig { + applicationId "com.syncedsynapse.kore2" + minSdkVersion 15 + targetSdkVersion 21 + versionCode 2 + versionName "0.9.0" + + buildConfigField("String", "IAP_KEY", "\"${rootProject.property("IAP_KEY")}\"") + } + + signingConfigs { + release { + def Properties keyProps = new Properties() + keyProps.load(new FileInputStream(file('keystore.properties'))) + + storeFile file(keyProps["store"]) + keyAlias keyProps["alias"] + storePassword keyProps["storePass"] + keyPassword keyProps["pass"] + } + } + + buildTypes { +// debug { +// minifyEnabled true +// proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' +// } + + release { + signingConfig signingConfigs.release + minifyEnabled true + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } + + packagingOptions { + exclude 'META-INF/DEPENDENCIES' + exclude 'META-INF/NOTICE' + exclude 'META-INF/LICENSE' + exclude 'META-INF/LICENSE.txt' + exclude 'META-INF/NOTICE.txt' + } +} + +dependencies { + compile 'com.android.support:support-v4:21.0.3' + compile 'com.android.support:appcompat-v7:21.0.3' + compile 'com.android.support:cardview-v7:21.0.0' + + compile 'com.fasterxml.jackson.core:jackson-databind:2.4.2' + compile 'com.jakewharton:butterknife:5.1.2' + compile 'com.squareup.picasso:picasso:2.4.0' + compile 'de.greenrobot:eventbus:2.2.1' + compile 'javax.jmdns:jmdns:3.4.1' + compile 'com.astuetz:pagerslidingtabstrip:1.0.1' + compile 'com.melnykov:floatingactionbutton:1.1.0' + + compile fileTree(dir: 'libs', include: ['*.jar']) +} diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..0c8dbe1 --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,43 @@ +# 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.** + +# Butterknife +-dontwarn butterknife.internal.** +-keep class **$$ViewInjector { *; } +-keepnames class * { @butterknife.InjectView *;} + +# 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 android.support.v7.widget.SearchView { *; } + +# 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 *; +#} diff --git a/app/src/androidTest/java/com/syncedsynapse/kore2/ApplicationTest.java b/app/src/androidTest/java/com/syncedsynapse/kore2/ApplicationTest.java new file mode 100644 index 0000000..3cca216 --- /dev/null +++ b/app/src/androidTest/java/com/syncedsynapse/kore2/ApplicationTest.java @@ -0,0 +1,13 @@ +package com.syncedsynapse.kore2; + +import android.app.Application; +import android.test.ApplicationTestCase; + +/** + * Testing Fundamentals + */ +public class ApplicationTest extends ApplicationTestCase { + public ApplicationTest() { + super(Application.class); + } +} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..4fe8569 --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/aidl/com/android/vending/billing/IInAppBillingService.aidl b/app/src/main/aidl/com/android/vending/billing/IInAppBillingService.aidl new file mode 100644 index 0000000..2a492f7 --- /dev/null +++ b/app/src/main/aidl/com/android/vending/billing/IInAppBillingService.aidl @@ -0,0 +1,144 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.vending.billing; + +import android.os.Bundle; + +/** + * InAppBillingService is the service that provides in-app billing version 3 and beyond. + * This service provides the following features: + * 1. Provides a new API to get details of in-app items published for the app including + * price, type, title and description. + * 2. The purchase flow is synchronous and purchase information is available immediately + * after it completes. + * 3. Purchase information of in-app purchases is maintained within the Google Play system + * till the purchase is consumed. + * 4. An API to consume a purchase of an inapp item. All purchases of one-time + * in-app items are consumable and thereafter can be purchased again. + * 5. An API to get current purchases of the user immediately. This will not contain any + * consumed purchases. + * + * All calls will give a response code with the following possible values + * RESULT_OK = 0 - success + * RESULT_USER_CANCELED = 1 - user pressed back or canceled a dialog + * RESULT_BILLING_UNAVAILABLE = 3 - this billing API version is not supported for the type requested + * RESULT_ITEM_UNAVAILABLE = 4 - requested SKU is not available for purchase + * RESULT_DEVELOPER_ERROR = 5 - invalid arguments provided to the API + * RESULT_ERROR = 6 - Fatal error during the API action + * RESULT_ITEM_ALREADY_OWNED = 7 - Failure to purchase since item is already owned + * RESULT_ITEM_NOT_OWNED = 8 - Failure to consume since item is not owned + */ +interface IInAppBillingService { + /** + * Checks support for the requested billing API version, package and in-app type. + * Minimum API version supported by this interface is 3. + * @param apiVersion the billing version which the app is using + * @param packageName the package name of the calling app + * @param type type of the in-app item being purchased "inapp" for one-time purchases + * and "subs" for subscription. + * @return RESULT_OK(0) on success, corresponding result code on failures + */ + int isBillingSupported(int apiVersion, String packageName, String type); + + /** + * Provides details of a list of SKUs + * Given a list of SKUs of a valid type in the skusBundle, this returns a bundle + * with a list JSON strings containing the productId, price, title and description. + * This API can be called with a maximum of 20 SKUs. + * @param apiVersion billing API version that the Third-party is using + * @param packageName the package name of the calling app + * @param skusBundle bundle containing a StringArrayList of SKUs with key "ITEM_ID_LIST" + * @return Bundle containing the following key-value pairs + * "RESPONSE_CODE" with int value, RESULT_OK(0) if success, other response codes on + * failure as listed above. + * "DETAILS_LIST" with a StringArrayList containing purchase information + * in JSON format similar to: + * '{ "productId" : "exampleSku", "type" : "inapp", "price" : "$5.00", + * "title : "Example Title", "description" : "This is an example description" }' + */ + Bundle getSkuDetails(int apiVersion, String packageName, String type, in Bundle skusBundle); + + /** + * Returns a pending intent to launch the purchase flow for an in-app item by providing a SKU, + * the type, a unique purchase token and an optional developer payload. + * @param apiVersion billing API version that the app is using + * @param packageName package name of the calling app + * @param sku the SKU of the in-app item as published in the developer console + * @param type the type of the in-app item ("inapp" for one-time purchases + * and "subs" for subscription). + * @param developerPayload optional argument to be sent back with the purchase information + * @return Bundle containing the following key-value pairs + * "RESPONSE_CODE" with int value, RESULT_OK(0) if success, other response codes on + * failure as listed above. + * "BUY_INTENT" - PendingIntent to start the purchase flow + * + * The Pending intent should be launched with startIntentSenderForResult. When purchase flow + * has completed, the onActivityResult() will give a resultCode of OK or CANCELED. + * If the purchase is successful, the result data will contain the following key-value pairs + * "RESPONSE_CODE" with int value, RESULT_OK(0) if success, other response codes on + * failure as listed above. + * "INAPP_PURCHASE_DATA" - String in JSON format similar to + * '{"orderId":"12999763169054705758.1371079406387615", + * "packageName":"com.example.app", + * "productId":"exampleSku", + * "purchaseTime":1345678900000, + * "purchaseToken" : "122333444455555", + * "developerPayload":"example developer payload" }' + * "INAPP_DATA_SIGNATURE" - String containing the signature of the purchase data that + * was signed with the private key of the developer + * TODO: change this to app-specific keys. + */ + Bundle getBuyIntent(int apiVersion, String packageName, String sku, String type, + String developerPayload); + + /** + * Returns the current SKUs owned by the user of the type and package name specified along with + * purchase information and a signature of the data to be validated. + * This will return all SKUs that have been purchased in V3 and managed items purchased using + * V1 and V2 that have not been consumed. + * @param apiVersion billing API version that the app is using + * @param packageName package name of the calling app + * @param type the type of the in-app items being requested + * ("inapp" for one-time purchases and "subs" for subscription). + * @param continuationToken to be set as null for the first call, if the number of owned + * skus are too many, a continuationToken is returned in the response bundle. + * This method can be called again with the continuation token to get the next set of + * owned skus. + * @return Bundle containing the following key-value pairs + * "RESPONSE_CODE" with int value, RESULT_OK(0) if success, other response codes on + * failure as listed above. + * "INAPP_PURCHASE_ITEM_LIST" - StringArrayList containing the list of SKUs + * "INAPP_PURCHASE_DATA_LIST" - StringArrayList containing the purchase information + * "INAPP_DATA_SIGNATURE_LIST"- StringArrayList containing the signatures + * of the purchase information + * "INAPP_CONTINUATION_TOKEN" - String containing a continuation token for the + * next set of in-app purchases. Only set if the + * user has more owned skus than the current list. + */ + Bundle getPurchases(int apiVersion, String packageName, String type, String continuationToken); + + /** + * Consume the last purchase of the given SKU. This will result in this item being removed + * from all subsequent responses to getPurchases() and allow re-purchase of this item. + * @param apiVersion billing API version that the app is using + * @param packageName package name of the calling app + * @param purchaseToken token in the purchase information JSON that identifies the purchase + * to be consumed + * @return 0 if consumption succeeded. Appropriate error values for failures. + */ + int consumePurchase(int apiVersion, String packageName, String purchaseToken); +} diff --git a/app/src/main/java/com/syncedsynapse/kore2/Settings.java b/app/src/main/java/com/syncedsynapse/kore2/Settings.java new file mode 100644 index 0000000..d91d226 --- /dev/null +++ b/app/src/main/java/com/syncedsynapse/kore2/Settings.java @@ -0,0 +1,136 @@ +/* + * 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 com.syncedsynapse.kore2; + +import android.content.Context; +import android.content.SharedPreferences; +import android.text.format.DateUtils; + +import com.syncedsynapse.kore2.utils.LogUtils; + +/** + * Singleton that holds the settings of the app. + * + * Interfaces with {@link android.content.SharedPreferences} to load/store these 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. Aplicable to TV Shows and Movies. + */ + public static final long DB_UPDATE_INTERVAL = 12 * DateUtils.HOUR_IN_MILLIS; +// public static final long DB_UPDATE_INTERVAL = DateUtils.MINUTE_IN_MILLIS; + + // Constants for Shared Preferences + private static final String SETTINGS_KEY = "SETTINGS_SHARED_PREFS"; + + // Tags to save the values + private static final String CURRENT_HOST_ID = "CURRENT_HOST_ID"; + private static final String MAX_CAST_PICTURES = "MAX_CAST_PICTURES"; + private static final String MOVIES_FILTER_HIDE_WATCHED = "MOVIES_FILTER_HIDE_WATCHED"; + private static final String TVSHOWS_FILTER_HIDE_WATCHED = "TVSHOWS_FILTER_HIDE_WATCHED"; + private static final String TVSHOW_EPISODES_FILTER_HIDE_WATCHED = "TVSHOW_EPISODES_FILTER_HIDE_WATCHED"; + + private static final String SHOW_THANKS_FOR_COFFEE_MESSAGE = "SHOW_THANKS_FOR_COFFEE_MESSAGE"; + private static final String HAS_BOUGHT_COFFEE = "HAS_BOUGHT_COFFEE"; + + // Default values + private static final int DEFAULT_MAX_CAST_PICTURES = 12; + + // Singleton instance + private static Settings instance = null; + private Context context; + + /** + * Current saved host id + */ + public int currentHostId; + /** + * Maximum pictures to show on cast list (-1 to show all) + */ + public int maxCastPictures; + /** + * Filter watched movies on movie list + */ + public boolean moviesFilterHideWatched; + /** + * Filter watched tv shows on list (all episodes) + */ + public boolean tvshowsFilterHideWatched; + /** + * Filter watched episodes of a tv shows on list + */ + public boolean tvshowEpisodesFilterHideWatched; + + /** + * Show the thanks for coffee message + */ + public boolean showThanksForCofeeMessage; + + /** + * Local variable to save the last state of the coffe purchase + */ + public boolean hasBoughtCoffee; + + /** + * Protected singleton constructor. Loads all the preferences + * @param context App context + */ + protected Settings(Context context) { + this.context = context.getApplicationContext(); + + SharedPreferences preferences = context.getSharedPreferences(SETTINGS_KEY, Context.MODE_PRIVATE); + + currentHostId = preferences.getInt(CURRENT_HOST_ID, -1); + maxCastPictures = preferences.getInt(MAX_CAST_PICTURES, DEFAULT_MAX_CAST_PICTURES); +// maxCastPictures = 12; + moviesFilterHideWatched = preferences.getBoolean(MOVIES_FILTER_HIDE_WATCHED, false); + tvshowsFilterHideWatched = preferences.getBoolean(TVSHOWS_FILTER_HIDE_WATCHED, false); + tvshowEpisodesFilterHideWatched = preferences.getBoolean(TVSHOW_EPISODES_FILTER_HIDE_WATCHED, false); + showThanksForCofeeMessage = preferences.getBoolean(SHOW_THANKS_FOR_COFFEE_MESSAGE, true); + hasBoughtCoffee = preferences.getBoolean(HAS_BOUGHT_COFFEE, false); + } + + /** + * Returns the singleton {@link com.syncedsynapse.kore2.Settings} object. + * @param context App context + * @return Singleton instance + */ + public static Settings getInstance(Context context) { + if (instance == null) + instance = new Settings(context); + return instance; + } + + /** + * Save the current values in {@link android.content.SharedPreferences} + */ + public void save() { + SharedPreferences preferences = context.getSharedPreferences(SETTINGS_KEY, Context.MODE_PRIVATE); + SharedPreferences.Editor editor = preferences.edit(); + + editor.putInt(CURRENT_HOST_ID, currentHostId); + editor.putInt(MAX_CAST_PICTURES, maxCastPictures); + editor.putBoolean(MOVIES_FILTER_HIDE_WATCHED, moviesFilterHideWatched); + editor.putBoolean(TVSHOWS_FILTER_HIDE_WATCHED, tvshowsFilterHideWatched); + editor.putBoolean(TVSHOW_EPISODES_FILTER_HIDE_WATCHED, tvshowEpisodesFilterHideWatched); + editor.putBoolean(SHOW_THANKS_FOR_COFFEE_MESSAGE, showThanksForCofeeMessage); + editor.putBoolean(HAS_BOUGHT_COFFEE, hasBoughtCoffee); + editor.apply(); + } +} diff --git a/app/src/main/java/com/syncedsynapse/kore2/billing/Base64.java b/app/src/main/java/com/syncedsynapse/kore2/billing/Base64.java new file mode 100644 index 0000000..4dc9cb2 --- /dev/null +++ b/app/src/main/java/com/syncedsynapse/kore2/billing/Base64.java @@ -0,0 +1,570 @@ +// Portions copyright 2002, Google, Inc. +// +// 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 com.syncedsynapse.kore2.billing; + +// This code was converted from code at http://iharder.sourceforge.net/base64/ +// Lots of extraneous features were removed. +/* The original code said: + *

+ * I am placing this code in the Public Domain. Do with it as you will. + * This software comes with no guarantees or warranties but with + * plenty of well-wishing instead! + * Please visit + * http://iharder.net/xmlizable + * periodically to check for updates or to contribute improvements. + *

+ * + * @author Robert Harder + * @author rharder@usa.net + * @version 1.3 + */ + +/** + * Base64 converter class. This code is not a complete MIME encoder; + * it simply converts binary data to base64 data and back. + * + *

Note {@link CharBase64} is a GWT-compatible implementation of this + * class. + */ +public class Base64 { + /** Specify encoding (value is {@code true}). */ + public final static boolean ENCODE = true; + + /** Specify decoding (value is {@code false}). */ + public final static boolean DECODE = false; + + /** The equals sign (=) as a byte. */ + private final static byte EQUALS_SIGN = (byte) '='; + + /** The new line character (\n) as a byte. */ + private final static byte NEW_LINE = (byte) '\n'; + + /** + * The 64 valid Base64 values. + */ + private final static byte[] ALPHABET = + {(byte) 'A', (byte) 'B', (byte) 'C', (byte) 'D', (byte) 'E', (byte) 'F', + (byte) 'G', (byte) 'H', (byte) 'I', (byte) 'J', (byte) 'K', + (byte) 'L', (byte) 'M', (byte) 'N', (byte) 'O', (byte) 'P', + (byte) 'Q', (byte) 'R', (byte) 'S', (byte) 'T', (byte) 'U', + (byte) 'V', (byte) 'W', (byte) 'X', (byte) 'Y', (byte) 'Z', + (byte) 'a', (byte) 'b', (byte) 'c', (byte) 'd', (byte) 'e', + (byte) 'f', (byte) 'g', (byte) 'h', (byte) 'i', (byte) 'j', + (byte) 'k', (byte) 'l', (byte) 'm', (byte) 'n', (byte) 'o', + (byte) 'p', (byte) 'q', (byte) 'r', (byte) 's', (byte) 't', + (byte) 'u', (byte) 'v', (byte) 'w', (byte) 'x', (byte) 'y', + (byte) 'z', (byte) '0', (byte) '1', (byte) '2', (byte) '3', + (byte) '4', (byte) '5', (byte) '6', (byte) '7', (byte) '8', + (byte) '9', (byte) '+', (byte) '/'}; + + /** + * The 64 valid web safe Base64 values. + */ + private final static byte[] WEBSAFE_ALPHABET = + {(byte) 'A', (byte) 'B', (byte) 'C', (byte) 'D', (byte) 'E', (byte) 'F', + (byte) 'G', (byte) 'H', (byte) 'I', (byte) 'J', (byte) 'K', + (byte) 'L', (byte) 'M', (byte) 'N', (byte) 'O', (byte) 'P', + (byte) 'Q', (byte) 'R', (byte) 'S', (byte) 'T', (byte) 'U', + (byte) 'V', (byte) 'W', (byte) 'X', (byte) 'Y', (byte) 'Z', + (byte) 'a', (byte) 'b', (byte) 'c', (byte) 'd', (byte) 'e', + (byte) 'f', (byte) 'g', (byte) 'h', (byte) 'i', (byte) 'j', + (byte) 'k', (byte) 'l', (byte) 'm', (byte) 'n', (byte) 'o', + (byte) 'p', (byte) 'q', (byte) 'r', (byte) 's', (byte) 't', + (byte) 'u', (byte) 'v', (byte) 'w', (byte) 'x', (byte) 'y', + (byte) 'z', (byte) '0', (byte) '1', (byte) '2', (byte) '3', + (byte) '4', (byte) '5', (byte) '6', (byte) '7', (byte) '8', + (byte) '9', (byte) '-', (byte) '_'}; + + /** + * Translates a Base64 value to either its 6-bit reconstruction value + * or a negative number indicating some other meaning. + **/ + private final static byte[] DECODABET = {-9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 0 - 8 + -5, -5, // Whitespace: Tab and Linefeed + -9, -9, // Decimal 11 - 12 + -5, // Whitespace: Carriage Return + -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 14 - 26 + -9, -9, -9, -9, -9, // Decimal 27 - 31 + -5, // Whitespace: Space + -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 33 - 42 + 62, // Plus sign at decimal 43 + -9, -9, -9, // Decimal 44 - 46 + 63, // Slash at decimal 47 + 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, // Numbers zero through nine + -9, -9, -9, // Decimal 58 - 60 + -1, // Equals sign at decimal 61 + -9, -9, -9, // Decimal 62 - 64 + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, // Letters 'A' through 'N' + 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, // Letters 'O' through 'Z' + -9, -9, -9, -9, -9, -9, // Decimal 91 - 96 + 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, // Letters 'a' through 'm' + 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, // Letters 'n' through 'z' + -9, -9, -9, -9, -9 // Decimal 123 - 127 + /* ,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 128 - 139 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 140 - 152 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 153 - 165 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 166 - 178 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 179 - 191 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 192 - 204 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 205 - 217 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 218 - 230 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 231 - 243 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9 // Decimal 244 - 255 */ + }; + + /** The web safe decodabet */ + private final static byte[] WEBSAFE_DECODABET = + {-9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 0 - 8 + -5, -5, // Whitespace: Tab and Linefeed + -9, -9, // Decimal 11 - 12 + -5, // Whitespace: Carriage Return + -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 14 - 26 + -9, -9, -9, -9, -9, // Decimal 27 - 31 + -5, // Whitespace: Space + -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 33 - 44 + 62, // Dash '-' sign at decimal 45 + -9, -9, // Decimal 46-47 + 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, // Numbers zero through nine + -9, -9, -9, // Decimal 58 - 60 + -1, // Equals sign at decimal 61 + -9, -9, -9, // Decimal 62 - 64 + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, // Letters 'A' through 'N' + 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, // Letters 'O' through 'Z' + -9, -9, -9, -9, // Decimal 91-94 + 63, // Underscore '_' at decimal 95 + -9, // Decimal 96 + 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, // Letters 'a' through 'm' + 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, // Letters 'n' through 'z' + -9, -9, -9, -9, -9 // Decimal 123 - 127 + /* ,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 128 - 139 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 140 - 152 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 153 - 165 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 166 - 178 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 179 - 191 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 192 - 204 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 205 - 217 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 218 - 230 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 231 - 243 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9 // Decimal 244 - 255 */ + }; + + // Indicates white space in encoding + private final static byte WHITE_SPACE_ENC = -5; + // Indicates equals sign in encoding + private final static byte EQUALS_SIGN_ENC = -1; + + /** Defeats instantiation. */ + private Base64() { + } + + /* ******** E N C O D I N G M E T H O D S ******** */ + + /** + * Encodes up to three bytes of the array source + * and writes the resulting four Base64 bytes to destination. + * The source and destination arrays can be manipulated + * anywhere along their length by specifying + * srcOffset and destOffset. + * This method does not check to make sure your arrays + * are large enough to accommodate srcOffset + 3 for + * the source array or destOffset + 4 for + * the destination array. + * The actual number of significant bytes in your array is + * given by numSigBytes. + * + * @param source the array to convert + * @param srcOffset the index where conversion begins + * @param numSigBytes the number of significant bytes in your array + * @param destination the array to hold the conversion + * @param destOffset the index where output will be put + * @param alphabet is the encoding alphabet + * @return the destination array + * @since 1.3 + */ + private static byte[] encode3to4(byte[] source, int srcOffset, + int numSigBytes, byte[] destination, int destOffset, byte[] alphabet) { + // 1 2 3 + // 01234567890123456789012345678901 Bit position + // --------000000001111111122222222 Array position from threeBytes + // --------| || || || | Six bit groups to index alphabet + // >>18 >>12 >> 6 >> 0 Right shift necessary + // 0x3f 0x3f 0x3f Additional AND + + // Create buffer with zero-padding if there are only one or two + // significant bytes passed in the array. + // We have to shift left 24 in order to flush out the 1's that appear + // when Java treats a value as negative that is cast from a byte to an int. + int inBuff = + (numSigBytes > 0 ? ((source[srcOffset] << 24) >>> 8) : 0) + | (numSigBytes > 1 ? ((source[srcOffset + 1] << 24) >>> 16) : 0) + | (numSigBytes > 2 ? ((source[srcOffset + 2] << 24) >>> 24) : 0); + + switch (numSigBytes) { + case 3: + destination[destOffset] = alphabet[(inBuff >>> 18)]; + destination[destOffset + 1] = alphabet[(inBuff >>> 12) & 0x3f]; + destination[destOffset + 2] = alphabet[(inBuff >>> 6) & 0x3f]; + destination[destOffset + 3] = alphabet[(inBuff) & 0x3f]; + return destination; + case 2: + destination[destOffset] = alphabet[(inBuff >>> 18)]; + destination[destOffset + 1] = alphabet[(inBuff >>> 12) & 0x3f]; + destination[destOffset + 2] = alphabet[(inBuff >>> 6) & 0x3f]; + destination[destOffset + 3] = EQUALS_SIGN; + return destination; + case 1: + destination[destOffset] = alphabet[(inBuff >>> 18)]; + destination[destOffset + 1] = alphabet[(inBuff >>> 12) & 0x3f]; + destination[destOffset + 2] = EQUALS_SIGN; + destination[destOffset + 3] = EQUALS_SIGN; + return destination; + default: + return destination; + } // end switch + } // end encode3to4 + + /** + * Encodes a byte array into Base64 notation. + * Equivalent to calling + * {@code encodeBytes(source, 0, source.length)} + * + * @param source The data to convert + * @since 1.4 + */ + public static String encode(byte[] source) { + return encode(source, 0, source.length, ALPHABET, true); + } + + /** + * Encodes a byte array into web safe Base64 notation. + * + * @param source The data to convert + * @param doPadding is {@code true} to pad result with '=' chars + * if it does not fall on 3 byte boundaries + */ + public static String encodeWebSafe(byte[] source, boolean doPadding) { + return encode(source, 0, source.length, WEBSAFE_ALPHABET, doPadding); + } + + /** + * Encodes a byte array into Base64 notation. + * + * @param source the data to convert + * @param off offset in array where conversion should begin + * @param len length of data to convert + * @param alphabet the encoding alphabet + * @param doPadding is {@code true} to pad result with '=' chars + * if it does not fall on 3 byte boundaries + * @since 1.4 + */ + public static String encode(byte[] source, int off, int len, byte[] alphabet, + boolean doPadding) { + byte[] outBuff = encode(source, off, len, alphabet, Integer.MAX_VALUE); + int outLen = outBuff.length; + + // If doPadding is false, set length to truncate '=' + // padding characters + while (doPadding == false && outLen > 0) { + if (outBuff[outLen - 1] != '=') { + break; + } + outLen -= 1; + } + + return new String(outBuff, 0, outLen); + } + + /** + * Encodes a byte array into Base64 notation. + * + * @param source the data to convert + * @param off offset in array where conversion should begin + * @param len length of data to convert + * @param alphabet is the encoding alphabet + * @param maxLineLength maximum length of one line. + * @return the BASE64-encoded byte array + */ + public static byte[] encode(byte[] source, int off, int len, byte[] alphabet, + int maxLineLength) { + int lenDiv3 = (len + 2) / 3; // ceil(len / 3) + int len43 = lenDiv3 * 4; + byte[] outBuff = new byte[len43 // Main 4:3 + + (len43 / maxLineLength)]; // New lines + + int d = 0; + int e = 0; + int len2 = len - 2; + int lineLength = 0; + for (; d < len2; d += 3, e += 4) { + + // The following block of code is the same as + // encode3to4( source, d + off, 3, outBuff, e, alphabet ); + // but inlined for faster encoding (~20% improvement) + int inBuff = + ((source[d + off] << 24) >>> 8) + | ((source[d + 1 + off] << 24) >>> 16) + | ((source[d + 2 + off] << 24) >>> 24); + outBuff[e] = alphabet[(inBuff >>> 18)]; + outBuff[e + 1] = alphabet[(inBuff >>> 12) & 0x3f]; + outBuff[e + 2] = alphabet[(inBuff >>> 6) & 0x3f]; + outBuff[e + 3] = alphabet[(inBuff) & 0x3f]; + + lineLength += 4; + if (lineLength == maxLineLength) { + outBuff[e + 4] = NEW_LINE; + e++; + lineLength = 0; + } // end if: end of line + } // end for: each piece of array + + if (d < len) { + encode3to4(source, d + off, len - d, outBuff, e, alphabet); + + lineLength += 4; + if (lineLength == maxLineLength) { + // Add a last newline + outBuff[e + 4] = NEW_LINE; + e++; + } + e += 4; + } + + assert (e == outBuff.length); + return outBuff; + } + + + /* ******** D E C O D I N G M E T H O D S ******** */ + + + /** + * Decodes four bytes from array source + * and writes the resulting bytes (up to three of them) + * to destination. + * The source and destination arrays can be manipulated + * anywhere along their length by specifying + * srcOffset and destOffset. + * This method does not check to make sure your arrays + * are large enough to accommodate srcOffset + 4 for + * the source array or destOffset + 3 for + * the destination array. + * This method returns the actual number of bytes that + * were converted from the Base64 encoding. + * + * + * @param source the array to convert + * @param srcOffset the index where conversion begins + * @param destination the array to hold the conversion + * @param destOffset the index where output will be put + * @param decodabet the decodabet for decoding Base64 content + * @return the number of decoded bytes converted + * @since 1.3 + */ + private static int decode4to3(byte[] source, int srcOffset, + byte[] destination, int destOffset, byte[] decodabet) { + // Example: Dk== + if (source[srcOffset + 2] == EQUALS_SIGN) { + int outBuff = + ((decodabet[source[srcOffset]] << 24) >>> 6) + | ((decodabet[source[srcOffset + 1]] << 24) >>> 12); + + destination[destOffset] = (byte) (outBuff >>> 16); + return 1; + } else if (source[srcOffset + 3] == EQUALS_SIGN) { + // Example: DkL= + int outBuff = + ((decodabet[source[srcOffset]] << 24) >>> 6) + | ((decodabet[source[srcOffset + 1]] << 24) >>> 12) + | ((decodabet[source[srcOffset + 2]] << 24) >>> 18); + + destination[destOffset] = (byte) (outBuff >>> 16); + destination[destOffset + 1] = (byte) (outBuff >>> 8); + return 2; + } else { + // Example: DkLE + int outBuff = + ((decodabet[source[srcOffset]] << 24) >>> 6) + | ((decodabet[source[srcOffset + 1]] << 24) >>> 12) + | ((decodabet[source[srcOffset + 2]] << 24) >>> 18) + | ((decodabet[source[srcOffset + 3]] << 24) >>> 24); + + destination[destOffset] = (byte) (outBuff >> 16); + destination[destOffset + 1] = (byte) (outBuff >> 8); + destination[destOffset + 2] = (byte) (outBuff); + return 3; + } + } // end decodeToBytes + + + /** + * Decodes data from Base64 notation. + * + * @param s the string to decode (decoded in default encoding) + * @return the decoded data + * @since 1.4 + */ + public static byte[] decode(String s) throws Base64DecoderException { + byte[] bytes = s.getBytes(); + return decode(bytes, 0, bytes.length); + } + + /** + * Decodes data from web safe Base64 notation. + * Web safe encoding uses '-' instead of '+', '_' instead of '/' + * + * @param s the string to decode (decoded in default encoding) + * @return the decoded data + */ + public static byte[] decodeWebSafe(String s) throws Base64DecoderException { + byte[] bytes = s.getBytes(); + return decodeWebSafe(bytes, 0, bytes.length); + } + + /** + * Decodes Base64 content in byte array format and returns + * the decoded byte array. + * + * @param source The Base64 encoded data + * @return decoded data + * @since 1.3 + * @throws Base64DecoderException + */ + public static byte[] decode(byte[] source) throws Base64DecoderException { + return decode(source, 0, source.length); + } + + /** + * Decodes web safe Base64 content in byte array format and returns + * the decoded data. + * Web safe encoding uses '-' instead of '+', '_' instead of '/' + * + * @param source the string to decode (decoded in default encoding) + * @return the decoded data + */ + public static byte[] decodeWebSafe(byte[] source) + throws Base64DecoderException { + return decodeWebSafe(source, 0, source.length); + } + + /** + * Decodes Base64 content in byte array format and returns + * the decoded byte array. + * + * @param source the Base64 encoded data + * @param off the offset of where to begin decoding + * @param len the length of characters to decode + * @return decoded data + * @since 1.3 + * @throws Base64DecoderException + */ + public static byte[] decode(byte[] source, int off, int len) + throws Base64DecoderException { + return decode(source, off, len, DECODABET); + } + + /** + * Decodes web safe Base64 content in byte array format and returns + * the decoded byte array. + * Web safe encoding uses '-' instead of '+', '_' instead of '/' + * + * @param source the Base64 encoded data + * @param off the offset of where to begin decoding + * @param len the length of characters to decode + * @return decoded data + */ + public static byte[] decodeWebSafe(byte[] source, int off, int len) + throws Base64DecoderException { + return decode(source, off, len, WEBSAFE_DECODABET); + } + + /** + * Decodes Base64 content using the supplied decodabet and returns + * the decoded byte array. + * + * @param source the Base64 encoded data + * @param off the offset of where to begin decoding + * @param len the length of characters to decode + * @param decodabet the decodabet for decoding Base64 content + * @return decoded data + */ + public static byte[] decode(byte[] source, int off, int len, byte[] decodabet) + throws Base64DecoderException { + int len34 = len * 3 / 4; + byte[] outBuff = new byte[2 + len34]; // Upper limit on size of output + int outBuffPosn = 0; + + byte[] b4 = new byte[4]; + int b4Posn = 0; + int i = 0; + byte sbiCrop = 0; + byte sbiDecode = 0; + for (i = 0; i < len; i++) { + sbiCrop = (byte) (source[i + off] & 0x7f); // Only the low seven bits + sbiDecode = decodabet[sbiCrop]; + + if (sbiDecode >= WHITE_SPACE_ENC) { // White space Equals sign or better + if (sbiDecode >= EQUALS_SIGN_ENC) { + // An equals sign (for padding) must not occur at position 0 or 1 + // and must be the last byte[s] in the encoded value + if (sbiCrop == EQUALS_SIGN) { + int bytesLeft = len - i; + byte lastByte = (byte) (source[len - 1 + off] & 0x7f); + if (b4Posn == 0 || b4Posn == 1) { + throw new Base64DecoderException( + "invalid padding byte '=' at byte offset " + i); + } else if ((b4Posn == 3 && bytesLeft > 2) + || (b4Posn == 4 && bytesLeft > 1)) { + throw new Base64DecoderException( + "padding byte '=' falsely signals end of encoded value " + + "at offset " + i); + } else if (lastByte != EQUALS_SIGN && lastByte != NEW_LINE) { + throw new Base64DecoderException( + "encoded value has invalid trailing byte"); + } + break; + } + + b4[b4Posn++] = sbiCrop; + if (b4Posn == 4) { + outBuffPosn += decode4to3(b4, 0, outBuff, outBuffPosn, decodabet); + b4Posn = 0; + } + } + } else { + throw new Base64DecoderException("Bad Base64 input character at " + i + + ": " + source[i + off] + "(decimal)"); + } + } + + // Because web safe encoding allows non padding base64 encodes, we + // need to pad the rest of the b4 buffer with equal signs when + // b4Posn != 0. There can be at most 2 equal signs at the end of + // four characters, so the b4 buffer must have two or three + // characters. This also catches the case where the input is + // padded with EQUALS_SIGN + if (b4Posn != 0) { + if (b4Posn == 1) { + throw new Base64DecoderException("single trailing character at offset " + + (len - 1)); + } + b4[b4Posn++] = EQUALS_SIGN; + outBuffPosn += decode4to3(b4, 0, outBuff, outBuffPosn, decodabet); + } + + byte[] out = new byte[outBuffPosn]; + System.arraycopy(outBuff, 0, out, 0, outBuffPosn); + return out; + } +} diff --git a/app/src/main/java/com/syncedsynapse/kore2/billing/Base64DecoderException.java b/app/src/main/java/com/syncedsynapse/kore2/billing/Base64DecoderException.java new file mode 100644 index 0000000..b1265d6 --- /dev/null +++ b/app/src/main/java/com/syncedsynapse/kore2/billing/Base64DecoderException.java @@ -0,0 +1,32 @@ +// Copyright 2002, Google, Inc. +// +// 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 com.syncedsynapse.kore2.billing; + +/** + * Exception thrown when encountering an invalid Base64 input character. + * + * @author nelson + */ +public class Base64DecoderException extends Exception { + public Base64DecoderException() { + super(); + } + + public Base64DecoderException(String s) { + super(s); + } + + private static final long serialVersionUID = 1L; +} diff --git a/app/src/main/java/com/syncedsynapse/kore2/billing/IabException.java b/app/src/main/java/com/syncedsynapse/kore2/billing/IabException.java new file mode 100644 index 0000000..df53a0c --- /dev/null +++ b/app/src/main/java/com/syncedsynapse/kore2/billing/IabException.java @@ -0,0 +1,43 @@ +/* Copyright (c) 2012 Google Inc. + * + * 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 com.syncedsynapse.kore2.billing; + +/** + * Exception thrown when something went wrong with in-app billing. + * An IabException has an associated IabResult (an error). + * To get the IAB result that caused this exception to be thrown, + * call {@link #getResult()}. + */ +public class IabException extends Exception { + IabResult mResult; + + public IabException(IabResult r) { + this(r, null); + } + public IabException(int response, String message) { + this(new IabResult(response, message)); + } + public IabException(IabResult r, Exception cause) { + super(r.getMessage(), cause); + mResult = r; + } + public IabException(int response, String message, Exception cause) { + this(new IabResult(response, message), cause); + } + + /** Returns the IAB result (error) that this exception signals. */ + public IabResult getResult() { return mResult; } +} diff --git a/app/src/main/java/com/syncedsynapse/kore2/billing/IabHelper.java b/app/src/main/java/com/syncedsynapse/kore2/billing/IabHelper.java new file mode 100644 index 0000000..df5add5 --- /dev/null +++ b/app/src/main/java/com/syncedsynapse/kore2/billing/IabHelper.java @@ -0,0 +1,1017 @@ +/* Copyright (c) 2012 Google Inc. + * + * 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 com.syncedsynapse.kore2.billing; + +import android.app.Activity; +import android.app.PendingIntent; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.IntentSender.SendIntentException; +import android.content.ServiceConnection; +import android.content.pm.ResolveInfo; +import android.os.Bundle; +import android.os.Handler; +import android.os.IBinder; +import android.os.RemoteException; +import android.text.TextUtils; +import android.util.Log; + +import com.android.vending.billing.IInAppBillingService; + +import org.json.JSONException; + +import java.util.ArrayList; +import java.util.List; + + +/** + * Provides convenience methods for in-app billing. You can create one instance of this + * class for your application and use it to process in-app billing operations. + * It provides synchronous (blocking) and asynchronous (non-blocking) methods for + * many common in-app billing operations, as well as automatic signature + * verification. + * + * After instantiating, you must perform setup in order to start using the object. + * To perform setup, call the {@link #startSetup} method and provide a listener; + * that listener will be notified when setup is complete, after which (and not before) + * you may call other methods. + * + * After setup is complete, you will typically want to request an inventory of owned + * items and subscriptions. See {@link #queryInventory}, {@link #queryInventoryAsync} + * and related methods. + * + * When you are done with this object, don't forget to call {@link #dispose} + * to ensure proper cleanup. This object holds a binding to the in-app billing + * service, which will leak unless you dispose of it correctly. If you created + * the object on an Activity's onCreate method, then the recommended + * place to dispose of it is the Activity's onDestroy method. + * + * A note about threading: When using this object from a background thread, you may + * call the blocking versions of methods; when using from a UI thread, call + * only the asynchronous versions and handle the results via callbacks. + * Also, notice that you can only call one asynchronous operation at a time; + * attempting to start a second asynchronous operation while the first one + * has not yet completed will result in an exception being thrown. + * + * @author Bruno Oliveira (Google) + * + */ +public class IabHelper { + // Is debug logging enabled? + boolean mDebugLog = false; + String mDebugTag = "IabHelper"; + + // Is setup done? + boolean mSetupDone = false; + + // Has this object been disposed of? (If so, we should ignore callbacks, etc) + boolean mDisposed = false; + + // Are subscriptions supported? + boolean mSubscriptionsSupported = false; + + // Is an asynchronous operation in progress? + // (only one at a time can be in progress) + boolean mAsyncInProgress = false; + + // (for logging/debugging) + // if mAsyncInProgress == true, what asynchronous operation is in progress? + String mAsyncOperation = ""; + + // Context we were passed during initialization + Context mContext; + + // Connection to the service + IInAppBillingService mService; + ServiceConnection mServiceConn; + + // The request code used to launch purchase flow + int mRequestCode; + + // The item type of the current purchase flow + String mPurchasingItemType; + + // Public key for verifying signature, in base64 encoding + String mSignatureBase64 = null; + + // Billing response codes + public static final int BILLING_RESPONSE_RESULT_OK = 0; + public static final int BILLING_RESPONSE_RESULT_USER_CANCELED = 1; + public static final int BILLING_RESPONSE_RESULT_BILLING_UNAVAILABLE = 3; + public static final int BILLING_RESPONSE_RESULT_ITEM_UNAVAILABLE = 4; + public static final int BILLING_RESPONSE_RESULT_DEVELOPER_ERROR = 5; + public static final int BILLING_RESPONSE_RESULT_ERROR = 6; + public static final int BILLING_RESPONSE_RESULT_ITEM_ALREADY_OWNED = 7; + public static final int BILLING_RESPONSE_RESULT_ITEM_NOT_OWNED = 8; + + // IAB Helper error codes + public static final int IABHELPER_ERROR_BASE = -1000; + public static final int IABHELPER_REMOTE_EXCEPTION = -1001; + public static final int IABHELPER_BAD_RESPONSE = -1002; + public static final int IABHELPER_VERIFICATION_FAILED = -1003; + public static final int IABHELPER_SEND_INTENT_FAILED = -1004; + public static final int IABHELPER_USER_CANCELLED = -1005; + public static final int IABHELPER_UNKNOWN_PURCHASE_RESPONSE = -1006; + public static final int IABHELPER_MISSING_TOKEN = -1007; + public static final int IABHELPER_UNKNOWN_ERROR = -1008; + public static final int IABHELPER_SUBSCRIPTIONS_NOT_AVAILABLE = -1009; + public static final int IABHELPER_INVALID_CONSUMPTION = -1010; + + // Keys for the responses from InAppBillingService + public static final String RESPONSE_CODE = "RESPONSE_CODE"; + public static final String RESPONSE_GET_SKU_DETAILS_LIST = "DETAILS_LIST"; + public static final String RESPONSE_BUY_INTENT = "BUY_INTENT"; + public static final String RESPONSE_INAPP_PURCHASE_DATA = "INAPP_PURCHASE_DATA"; + public static final String RESPONSE_INAPP_SIGNATURE = "INAPP_DATA_SIGNATURE"; + public static final String RESPONSE_INAPP_ITEM_LIST = "INAPP_PURCHASE_ITEM_LIST"; + public static final String RESPONSE_INAPP_PURCHASE_DATA_LIST = "INAPP_PURCHASE_DATA_LIST"; + public static final String RESPONSE_INAPP_SIGNATURE_LIST = "INAPP_DATA_SIGNATURE_LIST"; + public static final String INAPP_CONTINUATION_TOKEN = "INAPP_CONTINUATION_TOKEN"; + + // Item types + public static final String ITEM_TYPE_INAPP = "inapp"; + public static final String ITEM_TYPE_SUBS = "subs"; + + // some fields on the getSkuDetails response bundle + public static final String GET_SKU_DETAILS_ITEM_LIST = "ITEM_ID_LIST"; + public static final String GET_SKU_DETAILS_ITEM_TYPE_LIST = "ITEM_TYPE_LIST"; + + /** + * Creates an instance. After creation, it will not yet be ready to use. You must perform + * setup by calling {@link #startSetup} and wait for setup to complete. This constructor does not + * block and is safe to call from a UI thread. + * + * @param ctx Your application or Activity context. Needed to bind to the in-app billing service. + * @param base64PublicKey Your application's public key, encoded in base64. + * This is used for verification of purchase signatures. You can find your app's base64-encoded + * public key in your application's page on Google Play Developer Console. Note that this + * is NOT your "developer public key". + */ + public IabHelper(Context ctx, String base64PublicKey) { + mContext = ctx.getApplicationContext(); + mSignatureBase64 = base64PublicKey; + logDebug("IAB helper created."); + } + + /** + * Enables or disable debug logging through LogCat. + */ + public void enableDebugLogging(boolean enable, String tag) { + checkNotDisposed(); + mDebugLog = enable; + mDebugTag = tag; + } + + public void enableDebugLogging(boolean enable) { + checkNotDisposed(); + mDebugLog = enable; + } + + /** + * Callback for setup process. This listener's {@link #onIabSetupFinished} method is called + * when the setup process is complete. + */ + public interface OnIabSetupFinishedListener { + /** + * Called to notify that setup is complete. + * + * @param result The result of the setup process. + */ + public void onIabSetupFinished(IabResult result); + } + + /** + * Starts the setup process. This will start up the setup process asynchronously. + * You will be notified through the listener when the setup process is complete. + * This method is safe to call from a UI thread. + * + * @param listener The listener to notify when the setup process is complete. + */ + public void startSetup(final OnIabSetupFinishedListener listener) { + // If already set up, can't do it again. + checkNotDisposed(); + if (mSetupDone) throw new IllegalStateException("IAB helper is already set up."); + + // Connection to IAB service + logDebug("Starting in-app billing setup."); + mServiceConn = new ServiceConnection() { + @Override + public void onServiceDisconnected(ComponentName name) { + logDebug("Billing service disconnected."); + mService = null; + } + + @Override + public void onServiceConnected(ComponentName name, IBinder service) { + if (mDisposed) return; + logDebug("Billing service connected."); + mService = IInAppBillingService.Stub.asInterface(service); + String packageName = mContext.getPackageName(); + try { + logDebug("Checking for in-app billing 3 support."); + + // check for in-app billing v3 support + int response = mService.isBillingSupported(3, packageName, ITEM_TYPE_INAPP); + if (response != BILLING_RESPONSE_RESULT_OK) { + if (listener != null) listener.onIabSetupFinished(new IabResult(response, + "Error checking for billing v3 support.")); + + // if in-app purchases aren't supported, neither are subscriptions. + mSubscriptionsSupported = false; + return; + } + logDebug("In-app billing version 3 supported for " + packageName); + + // check for v3 subscriptions support + response = mService.isBillingSupported(3, packageName, ITEM_TYPE_SUBS); + if (response == BILLING_RESPONSE_RESULT_OK) { + logDebug("Subscriptions AVAILABLE."); + mSubscriptionsSupported = true; + } + else { + logDebug("Subscriptions NOT AVAILABLE. Response: " + response); + } + + mSetupDone = true; + } + catch (RemoteException e) { + if (listener != null) { + listener.onIabSetupFinished(new IabResult(IABHELPER_REMOTE_EXCEPTION, + "RemoteException while setting up in-app billing.")); + } + e.printStackTrace(); + return; + } + + if (listener != null) { + listener.onIabSetupFinished(new IabResult(BILLING_RESPONSE_RESULT_OK, "Setup successful.")); + } + } + }; + + Intent serviceIntent = new Intent("com.android.vending.billing.InAppBillingService.BIND"); + serviceIntent.setPackage("com.android.vending"); + + List services; + try { + services = mContext.getPackageManager().queryIntentServices(serviceIntent, 0); + } catch (Exception exc) { + services = null; + } + + if ((services == null) || (services.isEmpty())) { + // no service available to handle that Intent + if (listener != null) { + mServiceConn = null; + listener.onIabSetupFinished( + new IabResult(BILLING_RESPONSE_RESULT_BILLING_UNAVAILABLE, + "Billing service unavailable on device.")); + } + } + else { + // service available to handle that Intent + mContext.bindService(serviceIntent, mServiceConn, Context.BIND_AUTO_CREATE); + } + } + + /** + * Dispose of object, releasing resources. It's very important to call this + * method when you are done with this object. It will release any resources + * used by it such as service connections. Naturally, once the object is + * disposed of, it can't be used again. + */ + public void dispose() { + logDebug("Disposing."); + mSetupDone = false; + if (mServiceConn != null) { + logDebug("Unbinding from service."); + if (mContext != null) mContext.unbindService(mServiceConn); + } + mDisposed = true; + mContext = null; + mServiceConn = null; + mService = null; + mPurchaseListener = null; + } + + private void checkNotDisposed() { + if (mDisposed) throw new IllegalStateException("IabHelper was disposed of, so it cannot be used."); + } + + /** Returns whether subscriptions are supported. */ + public boolean subscriptionsSupported() { + checkNotDisposed(); + return mSubscriptionsSupported; + } + + + /** + * Callback that notifies when a purchase is finished. + */ + public interface OnIabPurchaseFinishedListener { + /** + * Called to notify that an in-app purchase finished. If the purchase was successful, + * then the sku parameter specifies which item was purchased. If the purchase failed, + * the sku and extraData parameters may or may not be null, depending on how far the purchase + * process went. + * + * @param result The result of the purchase. + * @param info The purchase information (null if purchase failed) + */ + public void onIabPurchaseFinished(IabResult result, Purchase info); + } + + // The listener registered on launchPurchaseFlow, which we have to call back when + // the purchase finishes + OnIabPurchaseFinishedListener mPurchaseListener; + + public void launchPurchaseFlow(Activity act, String sku, int requestCode, OnIabPurchaseFinishedListener listener) { + launchPurchaseFlow(act, sku, requestCode, listener, ""); + } + + public void launchPurchaseFlow(Activity act, String sku, int requestCode, + OnIabPurchaseFinishedListener listener, String extraData) { + launchPurchaseFlow(act, sku, ITEM_TYPE_INAPP, requestCode, listener, extraData); + } + + public void launchSubscriptionPurchaseFlow(Activity act, String sku, int requestCode, + OnIabPurchaseFinishedListener listener) { + launchSubscriptionPurchaseFlow(act, sku, requestCode, listener, ""); + } + + public void launchSubscriptionPurchaseFlow(Activity act, String sku, int requestCode, + OnIabPurchaseFinishedListener listener, String extraData) { + launchPurchaseFlow(act, sku, ITEM_TYPE_SUBS, requestCode, listener, extraData); + } + + /** + * Initiate the UI flow for an in-app purchase. Call this method to initiate an in-app purchase, + * which will involve bringing up the Google Play screen. The calling activity will be paused while + * the user interacts with Google Play, and the result will be delivered via the activity's + * {@link android.app.Activity#onActivityResult} method, at which point you must call + * this object's {@link #handleActivityResult} method to continue the purchase flow. This method + * MUST be called from the UI thread of the Activity. + * + * @param act The calling activity. + * @param sku The sku of the item to purchase. + * @param itemType indicates if it's a product or a subscription (ITEM_TYPE_INAPP or ITEM_TYPE_SUBS) + * @param requestCode A request code (to differentiate from other responses -- + * as in {@link android.app.Activity#startActivityForResult}). + * @param listener The listener to notify when the purchase process finishes + * @param extraData Extra data (developer payload), which will be returned with the purchase data + * when the purchase completes. This extra data will be permanently bound to that purchase + * and will always be returned when the purchase is queried. + */ + public void launchPurchaseFlow(Activity act, String sku, String itemType, int requestCode, + OnIabPurchaseFinishedListener listener, String extraData) { + checkNotDisposed(); + checkSetupDone("launchPurchaseFlow"); + flagStartAsync("launchPurchaseFlow"); + IabResult result; + + if (itemType.equals(ITEM_TYPE_SUBS) && !mSubscriptionsSupported) { + IabResult r = new IabResult(IABHELPER_SUBSCRIPTIONS_NOT_AVAILABLE, + "Subscriptions are not available."); + flagEndAsync(); + if (listener != null) listener.onIabPurchaseFinished(r, null); + return; + } + + try { + logDebug("Constructing buy intent for " + sku + ", item type: " + itemType); + Bundle buyIntentBundle = mService.getBuyIntent(3, mContext.getPackageName(), sku, itemType, extraData); + int response = getResponseCodeFromBundle(buyIntentBundle); + if (response != BILLING_RESPONSE_RESULT_OK) { + logError("Unable to buy item, Error response: " + getResponseDesc(response)); + flagEndAsync(); + result = new IabResult(response, "Unable to buy item"); + if (listener != null) listener.onIabPurchaseFinished(result, null); + return; + } + + PendingIntent pendingIntent = buyIntentBundle.getParcelable(RESPONSE_BUY_INTENT); + logDebug("Launching buy intent for " + sku + ". Request code: " + requestCode); + mRequestCode = requestCode; + mPurchaseListener = listener; + mPurchasingItemType = itemType; + act.startIntentSenderForResult(pendingIntent.getIntentSender(), + requestCode, new Intent(), + Integer.valueOf(0), Integer.valueOf(0), + Integer.valueOf(0)); + } + catch (SendIntentException e) { + logError("SendIntentException while launching purchase flow for sku " + sku); + e.printStackTrace(); + flagEndAsync(); + + result = new IabResult(IABHELPER_SEND_INTENT_FAILED, "Failed to send intent."); + if (listener != null) listener.onIabPurchaseFinished(result, null); + } + catch (RemoteException e) { + logError("RemoteException while launching purchase flow for sku " + sku); + e.printStackTrace(); + flagEndAsync(); + + result = new IabResult(IABHELPER_REMOTE_EXCEPTION, "Remote exception while starting purchase flow"); + if (listener != null) listener.onIabPurchaseFinished(result, null); + } + } + + /** + * Handles an activity result that's part of the purchase flow in in-app billing. If you + * are calling {@link #launchPurchaseFlow}, then you must call this method from your + * Activity's {@link android.app.Activity@onActivityResult} method. This method + * MUST be called from the UI thread of the Activity. + * + * @param requestCode The requestCode as you received it. + * @param resultCode The resultCode as you received it. + * @param data The data (Intent) as you received it. + * @return Returns true if the result was related to a purchase flow and was handled; + * false if the result was not related to a purchase, in which case you should + * handle it normally. + */ + public boolean handleActivityResult(int requestCode, int resultCode, Intent data) { + IabResult result; + if (requestCode != mRequestCode) return false; + + checkNotDisposed(); + checkSetupDone("handleActivityResult"); + + // end of async purchase operation that started on launchPurchaseFlow + flagEndAsync(); + + if (data == null) { + logError("Null data in IAB activity result."); + result = new IabResult(IABHELPER_BAD_RESPONSE, "Null data in IAB result"); + if (mPurchaseListener != null) mPurchaseListener.onIabPurchaseFinished(result, null); + return true; + } + + int responseCode = getResponseCodeFromIntent(data); + String purchaseData = data.getStringExtra(RESPONSE_INAPP_PURCHASE_DATA); + String dataSignature = data.getStringExtra(RESPONSE_INAPP_SIGNATURE); + + if (resultCode == Activity.RESULT_OK && responseCode == BILLING_RESPONSE_RESULT_OK) { + logDebug("Successful resultcode from purchase activity."); + logDebug("Purchase data: " + purchaseData); + logDebug("Data signature: " + dataSignature); + logDebug("Extras: " + data.getExtras()); + logDebug("Expected item type: " + mPurchasingItemType); + + if (purchaseData == null || dataSignature == null) { + logError("BUG: either purchaseData or dataSignature is null."); + logDebug("Extras: " + data.getExtras().toString()); + result = new IabResult(IABHELPER_UNKNOWN_ERROR, "IAB returned null purchaseData or dataSignature"); + if (mPurchaseListener != null) mPurchaseListener.onIabPurchaseFinished(result, null); + return true; + } + + Purchase purchase = null; + try { + purchase = new Purchase(mPurchasingItemType, purchaseData, dataSignature); + String sku = purchase.getSku(); + + // Verify signature + if (!Security.verifyPurchase(mSignatureBase64, purchaseData, dataSignature)) { + logError("Purchase signature verification FAILED for sku " + sku); + result = new IabResult(IABHELPER_VERIFICATION_FAILED, "Signature verification failed for sku " + sku); + if (mPurchaseListener != null) mPurchaseListener.onIabPurchaseFinished(result, purchase); + return true; + } + logDebug("Purchase signature successfully verified."); + } + catch (JSONException e) { + logError("Failed to parse purchase data."); + e.printStackTrace(); + result = new IabResult(IABHELPER_BAD_RESPONSE, "Failed to parse purchase data."); + if (mPurchaseListener != null) mPurchaseListener.onIabPurchaseFinished(result, null); + return true; + } + + if (mPurchaseListener != null) { + mPurchaseListener.onIabPurchaseFinished(new IabResult(BILLING_RESPONSE_RESULT_OK, "Success"), purchase); + } + } + else if (resultCode == Activity.RESULT_OK) { + // result code was OK, but in-app billing response was not OK. + logDebug("Result code was OK but in-app billing response was not OK: " + getResponseDesc(responseCode)); + if (mPurchaseListener != null) { + result = new IabResult(responseCode, "Problem purchashing item."); + mPurchaseListener.onIabPurchaseFinished(result, null); + } + } + else if (resultCode == Activity.RESULT_CANCELED) { + logDebug("Purchase canceled - Response: " + getResponseDesc(responseCode)); + result = new IabResult(IABHELPER_USER_CANCELLED, "User canceled."); + if (mPurchaseListener != null) mPurchaseListener.onIabPurchaseFinished(result, null); + } + else { + logError("Purchase failed. Result code: " + Integer.toString(resultCode) + + ". Response: " + getResponseDesc(responseCode)); + result = new IabResult(IABHELPER_UNKNOWN_PURCHASE_RESPONSE, "Unknown purchase response."); + if (mPurchaseListener != null) mPurchaseListener.onIabPurchaseFinished(result, null); + } + return true; + } + + public Inventory queryInventory(boolean querySkuDetails, List moreSkus) throws IabException { + return queryInventory(querySkuDetails, moreSkus, null); + } + + /** + * Queries the inventory. This will query all owned items from the server, as well as + * information on additional skus, if specified. This method may block or take long to execute. + * Do not call from a UI thread. For that, use the non-blocking version {@link #refreshInventoryAsync}. + * + * @param querySkuDetails if true, SKU details (price, description, etc) will be queried as well + * as purchase information. + * @param moreItemSkus additional PRODUCT skus to query information on, regardless of ownership. + * Ignored if null or if querySkuDetails is false. + * @param moreSubsSkus additional SUBSCRIPTIONS skus to query information on, regardless of ownership. + * Ignored if null or if querySkuDetails is false. + * @throws IabException if a problem occurs while refreshing the inventory. + */ + public Inventory queryInventory(boolean querySkuDetails, List moreItemSkus, + List moreSubsSkus) throws IabException { + checkNotDisposed(); + checkSetupDone("queryInventory"); + try { + Inventory inv = new Inventory(); + int r = queryPurchases(inv, ITEM_TYPE_INAPP); + if (r != BILLING_RESPONSE_RESULT_OK) { + throw new IabException(r, "Error refreshing inventory (querying owned items)."); + } + + if (querySkuDetails) { + r = querySkuDetails(ITEM_TYPE_INAPP, inv, moreItemSkus); + if (r != BILLING_RESPONSE_RESULT_OK) { + throw new IabException(r, "Error refreshing inventory (querying prices of items)."); + } + } + + // if subscriptions are supported, then also query for subscriptions + if (mSubscriptionsSupported) { + r = queryPurchases(inv, ITEM_TYPE_SUBS); + if (r != BILLING_RESPONSE_RESULT_OK) { + throw new IabException(r, "Error refreshing inventory (querying owned subscriptions)."); + } + + if (querySkuDetails) { + r = querySkuDetails(ITEM_TYPE_SUBS, inv, moreItemSkus); + if (r != BILLING_RESPONSE_RESULT_OK) { + throw new IabException(r, "Error refreshing inventory (querying prices of subscriptions)."); + } + } + } + + return inv; + } + catch (RemoteException e) { + throw new IabException(IABHELPER_REMOTE_EXCEPTION, "Remote exception while refreshing inventory.", e); + } + catch (JSONException e) { + throw new IabException(IABHELPER_BAD_RESPONSE, "Error parsing JSON response while refreshing inventory.", e); + } + catch (NullPointerException e) { + throw new IabException(IABHELPER_UNKNOWN_ERROR, "NullPointer while refreshing inventory.", e); + } + } + + /** + * Listener that notifies when an inventory query operation completes. + */ + public interface QueryInventoryFinishedListener { + /** + * Called to notify that an inventory query operation completed. + * + * @param result The result of the operation. + * @param inv The inventory. + */ + public void onQueryInventoryFinished(IabResult result, Inventory inv); + } + + + /** + * Asynchronous wrapper for inventory query. This will perform an inventory + * query as described in {@link #queryInventory}, but will do so asynchronously + * and call back the specified listener upon completion. This method is safe to + * call from a UI thread. + * + * @param querySkuDetails as in {@link #queryInventory} + * @param moreSkus as in {@link #queryInventory} + * @param listener The listener to notify when the refresh operation completes. + */ + public void queryInventoryAsync(final boolean querySkuDetails, + final List moreSkus, + final QueryInventoryFinishedListener listener) { + final Handler handler = new Handler(); + checkNotDisposed(); + checkSetupDone("queryInventory"); + flagStartAsync("refresh inventory"); + (new Thread(new Runnable() { + public void run() { + IabResult result = new IabResult(BILLING_RESPONSE_RESULT_OK, "Inventory refresh successful."); + Inventory inv = null; + try { + inv = queryInventory(querySkuDetails, moreSkus); + } + catch (IabException ex) { + result = ex.getResult(); + } + + flagEndAsync(); + + final IabResult result_f = result; + final Inventory inv_f = inv; + if (!mDisposed && listener != null) { + handler.post(new Runnable() { + public void run() { + listener.onQueryInventoryFinished(result_f, inv_f); + } + }); + } + } + })).start(); + } + + public void queryInventoryAsync(QueryInventoryFinishedListener listener) { + queryInventoryAsync(true, null, listener); + } + + public void queryInventoryAsync(boolean querySkuDetails, QueryInventoryFinishedListener listener) { + queryInventoryAsync(querySkuDetails, null, listener); + } + + + /** + * Consumes a given in-app product. Consuming can only be done on an item + * that's owned, and as a result of consumption, the user will no longer own it. + * This method may block or take long to return. Do not call from the UI thread. + * For that, see {@link #consumeAsync}. + * + * @param itemInfo The PurchaseInfo that represents the item to consume. + * @throws IabException if there is a problem during consumption. + */ + void consume(Purchase itemInfo) throws IabException { + checkNotDisposed(); + checkSetupDone("consume"); + + if (!itemInfo.mItemType.equals(ITEM_TYPE_INAPP)) { + throw new IabException(IABHELPER_INVALID_CONSUMPTION, + "Items of type '" + itemInfo.mItemType + "' can't be consumed."); + } + + try { + String token = itemInfo.getToken(); + String sku = itemInfo.getSku(); + if (token == null || token.equals("")) { + logError("Can't consume "+ sku + ". No token."); + throw new IabException(IABHELPER_MISSING_TOKEN, "PurchaseInfo is missing token for sku: " + + sku + " " + itemInfo); + } + + logDebug("Consuming sku: " + sku + ", token: " + token); + int response = mService.consumePurchase(3, mContext.getPackageName(), token); + if (response == BILLING_RESPONSE_RESULT_OK) { + logDebug("Successfully consumed sku: " + sku); + } + else { + logDebug("Error consuming consuming sku " + sku + ". " + getResponseDesc(response)); + throw new IabException(response, "Error consuming sku " + sku); + } + } + catch (RemoteException e) { + throw new IabException(IABHELPER_REMOTE_EXCEPTION, "Remote exception while consuming. PurchaseInfo: " + itemInfo, e); + } + } + + /** + * Callback that notifies when a consumption operation finishes. + */ + public interface OnConsumeFinishedListener { + /** + * Called to notify that a consumption has finished. + * + * @param purchase The purchase that was (or was to be) consumed. + * @param result The result of the consumption operation. + */ + public void onConsumeFinished(Purchase purchase, IabResult result); + } + + /** + * Callback that notifies when a multi-item consumption operation finishes. + */ + public interface OnConsumeMultiFinishedListener { + /** + * Called to notify that a consumption of multiple items has finished. + * + * @param purchases The purchases that were (or were to be) consumed. + * @param results The results of each consumption operation, corresponding to each + * sku. + */ + public void onConsumeMultiFinished(List purchases, List results); + } + + /** + * Asynchronous wrapper to item consumption. Works like {@link #consume}, but + * performs the consumption in the background and notifies completion through + * the provided listener. This method is safe to call from a UI thread. + * + * @param purchase The purchase to be consumed. + * @param listener The listener to notify when the consumption operation finishes. + */ + public void consumeAsync(Purchase purchase, OnConsumeFinishedListener listener) { + checkNotDisposed(); + checkSetupDone("consume"); + List purchases = new ArrayList(); + purchases.add(purchase); + consumeAsyncInternal(purchases, listener, null); + } + + /** + * Same as {@link consumeAsync}, but for multiple items at once. + * @param purchases The list of PurchaseInfo objects representing the purchases to consume. + * @param listener The listener to notify when the consumption operation finishes. + */ + public void consumeAsync(List purchases, OnConsumeMultiFinishedListener listener) { + checkNotDisposed(); + checkSetupDone("consume"); + consumeAsyncInternal(purchases, null, listener); + } + + /** + * Returns a human-readable description for the given response code. + * + * @param code The response code + * @return A human-readable string explaining the result code. + * It also includes the result code numerically. + */ + public static String getResponseDesc(int code) { + String[] iab_msgs = ("0:OK/1:User Canceled/2:Unknown/" + + "3:Billing Unavailable/4:Item unavailable/" + + "5:Developer Error/6:Error/7:Item Already Owned/" + + "8:Item not owned").split("/"); + String[] iabhelper_msgs = ("0:OK/-1001:Remote exception during initialization/" + + "-1002:Bad response received/" + + "-1003:Purchase signature verification failed/" + + "-1004:Send intent failed/" + + "-1005:User cancelled/" + + "-1006:Unknown purchase response/" + + "-1007:Missing token/" + + "-1008:Unknown error/" + + "-1009:Subscriptions not available/" + + "-1010:Invalid consumption attempt").split("/"); + + if (code <= IABHELPER_ERROR_BASE) { + int index = IABHELPER_ERROR_BASE - code; + if (index >= 0 && index < iabhelper_msgs.length) return iabhelper_msgs[index]; + else return String.valueOf(code) + ":Unknown IAB Helper Error"; + } + else if (code < 0 || code >= iab_msgs.length) + return String.valueOf(code) + ":Unknown"; + else + return iab_msgs[code]; + } + + + // Checks that setup was done; if not, throws an exception. + void checkSetupDone(String operation) { + if (!mSetupDone) { + logError("Illegal state for operation (" + operation + "): IAB helper is not set up."); + throw new IllegalStateException("IAB helper is not set up. Can't perform operation: " + operation); + } + } + + // Workaround to bug where sometimes response codes come as Long instead of Integer + int getResponseCodeFromBundle(Bundle b) { + Object o = b.get(RESPONSE_CODE); + if (o == null) { + logDebug("Bundle with null response code, assuming OK (known issue)"); + return BILLING_RESPONSE_RESULT_OK; + } + else if (o instanceof Integer) return ((Integer)o).intValue(); + else if (o instanceof Long) return (int)((Long)o).longValue(); + else { + logError("Unexpected type for bundle response code."); + logError(o.getClass().getName()); + throw new RuntimeException("Unexpected type for bundle response code: " + o.getClass().getName()); + } + } + + // Workaround to bug where sometimes response codes come as Long instead of Integer + int getResponseCodeFromIntent(Intent i) { + Object o = i.getExtras().get(RESPONSE_CODE); + if (o == null) { + logError("Intent with no response code, assuming OK (known issue)"); + return BILLING_RESPONSE_RESULT_OK; + } + else if (o instanceof Integer) return ((Integer)o).intValue(); + else if (o instanceof Long) return (int)((Long)o).longValue(); + else { + logError("Unexpected type for intent response code."); + logError(o.getClass().getName()); + throw new RuntimeException("Unexpected type for intent response code: " + o.getClass().getName()); + } + } + + void flagStartAsync(String operation) { + // HACK: This check was causing problems because somehow the billing infrastructure wasn't + // calling onActivityResult if there's an error, so it never reseted mAsyncInProgress + // Comment this for now +// if (mAsyncInProgress) throw new IllegalStateException("Can't start async operation (" + +// operation + ") because another async operation(" + mAsyncOperation + ") is in progress."); + mAsyncOperation = operation; + mAsyncInProgress = true; + logDebug("Starting async operation: " + operation); + } + + void flagEndAsync() { + logDebug("Ending async operation: " + mAsyncOperation); + mAsyncOperation = ""; + mAsyncInProgress = false; + } + + int queryPurchases(Inventory inv, String itemType) throws JSONException, RemoteException { + // Query purchases + logDebug("Querying owned items, item type: " + itemType); + logDebug("Package name: " + mContext.getPackageName()); + boolean verificationFailed = false; + String continueToken = null; + + do { + logDebug("Calling getPurchases with continuation token: " + continueToken); + Bundle ownedItems = mService.getPurchases(3, mContext.getPackageName(), + itemType, continueToken); + + int response = getResponseCodeFromBundle(ownedItems); + logDebug("Owned items response: " + String.valueOf(response)); + if (response != BILLING_RESPONSE_RESULT_OK) { + logDebug("getPurchases() failed: " + getResponseDesc(response)); + return response; + } + if (!ownedItems.containsKey(RESPONSE_INAPP_ITEM_LIST) + || !ownedItems.containsKey(RESPONSE_INAPP_PURCHASE_DATA_LIST) + || !ownedItems.containsKey(RESPONSE_INAPP_SIGNATURE_LIST)) { + logError("Bundle returned from getPurchases() doesn't contain required fields."); + return IABHELPER_BAD_RESPONSE; + } + + ArrayList ownedSkus = ownedItems.getStringArrayList( + RESPONSE_INAPP_ITEM_LIST); + ArrayList purchaseDataList = ownedItems.getStringArrayList( + RESPONSE_INAPP_PURCHASE_DATA_LIST); + ArrayList signatureList = ownedItems.getStringArrayList( + RESPONSE_INAPP_SIGNATURE_LIST); + + for (int i = 0; i < purchaseDataList.size(); ++i) { + String purchaseData = purchaseDataList.get(i); + String signature = signatureList.get(i); + String sku = ownedSkus.get(i); + + //logWarn("SKU " + sku); + //if (sku.equals("android.test.purchased")) { + // logWarn("HERE!!!!!"); + // try { + // consume(new Purchase(itemType, purchaseData, signature)); + // } catch (IabException exc) { + // return IABHELPER_BAD_RESPONSE; + // } + //} + + if (Security.verifyPurchase(mSignatureBase64, purchaseData, signature)) { + logDebug("Sku is owned: " + sku); + Purchase purchase = new Purchase(itemType, purchaseData, signature); + + if (TextUtils.isEmpty(purchase.getToken())) { + logWarn("BUG: empty/null token!"); + logDebug("Purchase data: " + purchaseData); + } + + // Record ownership and token + inv.addPurchase(purchase); + } + else { + logWarn("Purchase signature verification **FAILED**. Not adding item."); + logDebug(" Purchase data: " + purchaseData); + logDebug(" Signature: " + signature); + verificationFailed = true; + } + } + + continueToken = ownedItems.getString(INAPP_CONTINUATION_TOKEN); + logDebug("Continuation token: " + continueToken); + } while (!TextUtils.isEmpty(continueToken)); + + return verificationFailed ? IABHELPER_VERIFICATION_FAILED : BILLING_RESPONSE_RESULT_OK; + } + + int querySkuDetails(String itemType, Inventory inv, List moreSkus) + throws RemoteException, JSONException { + logDebug("Querying SKU details."); + ArrayList skuList = new ArrayList(); + skuList.addAll(inv.getAllOwnedSkus(itemType)); + if (moreSkus != null) { + for (String sku : moreSkus) { + if (!skuList.contains(sku)) { + skuList.add(sku); + } + } + } + + if (skuList.size() == 0) { + logDebug("queryPrices: nothing to do because there are no SKUs."); + return BILLING_RESPONSE_RESULT_OK; + } + + Bundle querySkus = new Bundle(); + querySkus.putStringArrayList(GET_SKU_DETAILS_ITEM_LIST, skuList); + Bundle skuDetails = mService.getSkuDetails(3, mContext.getPackageName(), + itemType, querySkus); + + if (!skuDetails.containsKey(RESPONSE_GET_SKU_DETAILS_LIST)) { + int response = getResponseCodeFromBundle(skuDetails); + if (response != BILLING_RESPONSE_RESULT_OK) { + logDebug("getSkuDetails() failed: " + getResponseDesc(response)); + return response; + } + else { + logError("getSkuDetails() returned a bundle with neither an error nor a detail list."); + return IABHELPER_BAD_RESPONSE; + } + } + + ArrayList responseList = skuDetails.getStringArrayList( + RESPONSE_GET_SKU_DETAILS_LIST); + + for (String thisResponse : responseList) { + SkuDetails d = new SkuDetails(itemType, thisResponse); + logDebug("Got sku details: " + d); + inv.addSkuDetails(d); + } + return BILLING_RESPONSE_RESULT_OK; + } + + + void consumeAsyncInternal(final List purchases, + final OnConsumeFinishedListener singleListener, + final OnConsumeMultiFinishedListener multiListener) { + final Handler handler = new Handler(); + flagStartAsync("consume"); + (new Thread(new Runnable() { + public void run() { + final List results = new ArrayList(); + for (Purchase purchase : purchases) { + try { + consume(purchase); + results.add(new IabResult(BILLING_RESPONSE_RESULT_OK, "Successful consume of sku " + purchase.getSku())); + } + catch (IabException ex) { + results.add(ex.getResult()); + } + } + + flagEndAsync(); + if (!mDisposed && singleListener != null) { + handler.post(new Runnable() { + public void run() { + singleListener.onConsumeFinished(purchases.get(0), results.get(0)); + } + }); + } + if (!mDisposed && multiListener != null) { + handler.post(new Runnable() { + public void run() { + multiListener.onConsumeMultiFinished(purchases, results); + } + }); + } + } + })).start(); + } + + void logDebug(String msg) { + if (mDebugLog) Log.d(mDebugTag, msg); + } + + void logError(String msg) { + Log.e(mDebugTag, "In-app billing error: " + msg); + } + + void logWarn(String msg) { + Log.w(mDebugTag, "In-app billing warning: " + msg); + } +} diff --git a/app/src/main/java/com/syncedsynapse/kore2/billing/IabResult.java b/app/src/main/java/com/syncedsynapse/kore2/billing/IabResult.java new file mode 100644 index 0000000..12bb4d5 --- /dev/null +++ b/app/src/main/java/com/syncedsynapse/kore2/billing/IabResult.java @@ -0,0 +1,45 @@ +/* Copyright (c) 2012 Google Inc. + * + * 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 com.syncedsynapse.kore2.billing; + +/** + * Represents the result of an in-app billing operation. + * A result is composed of a response code (an integer) and possibly a + * message (String). You can get those by calling + * {@link #getResponse} and {@link #getMessage()}, respectively. You + * can also inquire whether a result is a success or a failure by + * calling {@link #isSuccess()} and {@link #isFailure()}. + */ +public class IabResult { + int mResponse; + String mMessage; + + public IabResult(int response, String message) { + mResponse = response; + if (message == null || message.trim().length() == 0) { + mMessage = IabHelper.getResponseDesc(response); + } + else { + mMessage = message + " (response: " + IabHelper.getResponseDesc(response) + ")"; + } + } + public int getResponse() { return mResponse; } + public String getMessage() { return mMessage; } + public boolean isSuccess() { return mResponse == IabHelper.BILLING_RESPONSE_RESULT_OK; } + public boolean isFailure() { return !isSuccess(); } + public String toString() { return "IabResult: " + getMessage(); } +} + diff --git a/app/src/main/java/com/syncedsynapse/kore2/billing/Inventory.java b/app/src/main/java/com/syncedsynapse/kore2/billing/Inventory.java new file mode 100644 index 0000000..ddc1be9 --- /dev/null +++ b/app/src/main/java/com/syncedsynapse/kore2/billing/Inventory.java @@ -0,0 +1,91 @@ +/* Copyright (c) 2012 Google Inc. + * + * 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 com.syncedsynapse.kore2.billing; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Represents a block of information about in-app items. + * An Inventory is returned by such methods as {@link IabHelper#queryInventory}. + */ +public class Inventory { + Map mSkuMap = new HashMap(); + Map mPurchaseMap = new HashMap(); + + Inventory() { } + + /** Returns the listing details for an in-app product. */ + public SkuDetails getSkuDetails(String sku) { + return mSkuMap.get(sku); + } + + /** Returns purchase information for a given product, or null if there is no purchase. */ + public Purchase getPurchase(String sku) { + return mPurchaseMap.get(sku); + } + + /** Returns whether or not there exists a purchase of the given product. */ + public boolean hasPurchase(String sku) { + return mPurchaseMap.containsKey(sku); + } + + /** Return whether or not details about the given product are available. */ + public boolean hasDetails(String sku) { + return mSkuMap.containsKey(sku); + } + + /** + * Erase a purchase (locally) from the inventory, given its product ID. This just + * modifies the Inventory object locally and has no effect on the server! This is + * useful when you have an existing Inventory object which you know to be up to date, + * and you have just consumed an item successfully, which means that erasing its + * purchase data from the Inventory you already have is quicker than querying for + * a new Inventory. + */ + public void erasePurchase(String sku) { + if (mPurchaseMap.containsKey(sku)) mPurchaseMap.remove(sku); + } + + /** Returns a list of all owned product IDs. */ + List getAllOwnedSkus() { + return new ArrayList(mPurchaseMap.keySet()); + } + + /** Returns a list of all owned product IDs of a given type */ + List getAllOwnedSkus(String itemType) { + List result = new ArrayList(); + for (Purchase p : mPurchaseMap.values()) { + if (p.getItemType().equals(itemType)) result.add(p.getSku()); + } + return result; + } + + /** Returns a list of all purchases. */ + List getAllPurchases() { + return new ArrayList(mPurchaseMap.values()); + } + + void addSkuDetails(SkuDetails d) { + mSkuMap.put(d.getSku(), d); + } + + void addPurchase(Purchase p) { + mPurchaseMap.put(p.getSku(), p); + } +} diff --git a/app/src/main/java/com/syncedsynapse/kore2/billing/Purchase.java b/app/src/main/java/com/syncedsynapse/kore2/billing/Purchase.java new file mode 100644 index 0000000..7264039 --- /dev/null +++ b/app/src/main/java/com/syncedsynapse/kore2/billing/Purchase.java @@ -0,0 +1,63 @@ +/* Copyright (c) 2012 Google Inc. + * + * 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 com.syncedsynapse.kore2.billing; + +import org.json.JSONException; +import org.json.JSONObject; + +/** + * Represents an in-app billing purchase. + */ +public class Purchase { + String mItemType; // ITEM_TYPE_INAPP or ITEM_TYPE_SUBS + String mOrderId; + String mPackageName; + String mSku; + long mPurchaseTime; + int mPurchaseState; + String mDeveloperPayload; + String mToken; + String mOriginalJson; + String mSignature; + + public Purchase(String itemType, String jsonPurchaseInfo, String signature) throws JSONException { + mItemType = itemType; + mOriginalJson = jsonPurchaseInfo; + JSONObject o = new JSONObject(mOriginalJson); + mOrderId = o.optString("orderId"); + mPackageName = o.optString("packageName"); + mSku = o.optString("productId"); + mPurchaseTime = o.optLong("purchaseTime"); + mPurchaseState = o.optInt("purchaseState"); + mDeveloperPayload = o.optString("developerPayload"); + mToken = o.optString("token", o.optString("purchaseToken")); + mSignature = signature; + } + + public String getItemType() { return mItemType; } + public String getOrderId() { return mOrderId; } + public String getPackageName() { return mPackageName; } + public String getSku() { return mSku; } + public long getPurchaseTime() { return mPurchaseTime; } + public int getPurchaseState() { return mPurchaseState; } + public String getDeveloperPayload() { return mDeveloperPayload; } + public String getToken() { return mToken; } + public String getOriginalJson() { return mOriginalJson; } + public String getSignature() { return mSignature; } + + @Override + public String toString() { return "PurchaseInfo(type:" + mItemType + "):" + mOriginalJson; } +} diff --git a/app/src/main/java/com/syncedsynapse/kore2/billing/Security.java b/app/src/main/java/com/syncedsynapse/kore2/billing/Security.java new file mode 100644 index 0000000..72b570c --- /dev/null +++ b/app/src/main/java/com/syncedsynapse/kore2/billing/Security.java @@ -0,0 +1,121 @@ +/* Copyright (c) 2012 Google Inc. + * + * 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 com.syncedsynapse.kore2.billing; + +import android.text.TextUtils; +import android.util.Log; + +import java.security.InvalidKeyException; +import java.security.KeyFactory; +import java.security.NoSuchAlgorithmException; +import java.security.PublicKey; +import java.security.Signature; +import java.security.SignatureException; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.X509EncodedKeySpec; + +/** + * Security-related methods. For a secure implementation, all of this code + * should be implemented on a server that communicates with the + * application on the device. For the sake of simplicity and clarity of this + * example, this code is included here and is executed on the device. If you + * must verify the purchases on the phone, you should obfuscate this code to + * make it harder for an attacker to replace the code with stubs that treat all + * purchases as verified. + */ +public class Security { + private static final String TAG = "IABUtil/Security"; + + private static final String KEY_FACTORY_ALGORITHM = "RSA"; + private static final String SIGNATURE_ALGORITHM = "SHA1withRSA"; + + /** + * Verifies that the data was signed with the given signature, and returns + * the verified purchase. The data is in JSON format and signed + * with a private key. The data also contains the {@link PurchaseState} + * and product ID of the purchase. + * @param base64PublicKey the base64-encoded public key to use for verifying. + * @param signedData the signed JSON string (signed, not encrypted) + * @param signature the signature for the data, signed with the private key + */ + public static boolean verifyPurchase(String base64PublicKey, String signedData, String signature) { + if (TextUtils.isEmpty(signedData) || TextUtils.isEmpty(base64PublicKey) || + TextUtils.isEmpty(signature)) { + Log.e(TAG, "Purchase verification failed: missing data."); + return false; + } + + PublicKey key = Security.generatePublicKey(base64PublicKey); + return Security.verify(key, signedData, signature); + // TODO: Uncomment this + //return true; + } + + /** + * Generates a PublicKey instance from a string containing the + * Base64-encoded public key. + * + * @param encodedPublicKey Base64-encoded public key + * @throws IllegalArgumentException if encodedPublicKey is invalid + */ + public static PublicKey generatePublicKey(String encodedPublicKey) { + try { + byte[] decodedKey = Base64.decode(encodedPublicKey); + KeyFactory keyFactory = KeyFactory.getInstance(KEY_FACTORY_ALGORITHM); + return keyFactory.generatePublic(new X509EncodedKeySpec(decodedKey)); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException(e); + } catch (InvalidKeySpecException e) { + Log.e(TAG, "Invalid key specification."); + throw new IllegalArgumentException(e); + } catch (Base64DecoderException e) { + Log.e(TAG, "Base64 decoding failed."); + throw new IllegalArgumentException(e); + } + } + + /** + * Verifies that the signature from the server matches the computed + * signature on the data. Returns true if the data is correctly signed. + * + * @param publicKey public key associated with the developer account + * @param signedData signed data from server + * @param signature server signature + * @return true if the data and signature match + */ + public static boolean verify(PublicKey publicKey, String signedData, String signature) { + Signature sig; + try { + sig = Signature.getInstance(SIGNATURE_ALGORITHM); + sig.initVerify(publicKey); + sig.update(signedData.getBytes()); + if (!sig.verify(Base64.decode(signature))) { + Log.e(TAG, "Signature verification failed."); + return false; + } + return true; + } catch (NoSuchAlgorithmException e) { + Log.e(TAG, "NoSuchAlgorithmException."); + } catch (InvalidKeyException e) { + Log.e(TAG, "Invalid key specification."); + } catch (SignatureException e) { + Log.e(TAG, "Signature exception."); + } catch (Base64DecoderException e) { + Log.e(TAG, "Base64 decoding failed."); + } + return false; + } +} diff --git a/app/src/main/java/com/syncedsynapse/kore2/billing/SkuDetails.java b/app/src/main/java/com/syncedsynapse/kore2/billing/SkuDetails.java new file mode 100644 index 0000000..c6fffc5 --- /dev/null +++ b/app/src/main/java/com/syncedsynapse/kore2/billing/SkuDetails.java @@ -0,0 +1,58 @@ +/* Copyright (c) 2012 Google Inc. + * + * 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 com.syncedsynapse.kore2.billing; + +import org.json.JSONException; +import org.json.JSONObject; + +/** + * Represents an in-app product's listing details. + */ +public class SkuDetails { + String mItemType; + String mSku; + String mType; + String mPrice; + String mTitle; + String mDescription; + String mJson; + + public SkuDetails(String jsonSkuDetails) throws JSONException { + this(IabHelper.ITEM_TYPE_INAPP, jsonSkuDetails); + } + + public SkuDetails(String itemType, String jsonSkuDetails) throws JSONException { + mItemType = itemType; + mJson = jsonSkuDetails; + JSONObject o = new JSONObject(mJson); + mSku = o.optString("productId"); + mType = o.optString("type"); + mPrice = o.optString("price"); + mTitle = o.optString("title"); + mDescription = o.optString("description"); + } + + public String getSku() { return mSku; } + public String getType() { return mType; } + public String getPrice() { return mPrice; } + public String getTitle() { return mTitle; } + public String getDescription() { return mDescription; } + + @Override + public String toString() { + return "SkuDetails:" + mJson; + } +} diff --git a/app/src/main/java/com/syncedsynapse/kore2/host/HostConnectionObserver.java b/app/src/main/java/com/syncedsynapse/kore2/host/HostConnectionObserver.java new file mode 100644 index 0000000..f6862e1 --- /dev/null +++ b/app/src/main/java/com/syncedsynapse/kore2/host/HostConnectionObserver.java @@ -0,0 +1,603 @@ +/* + * 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 com.syncedsynapse.kore2.host; + +import android.os.Handler; + +import com.syncedsynapse.kore2.jsonrpc.ApiCallback; +import com.syncedsynapse.kore2.jsonrpc.HostConnection; +import com.syncedsynapse.kore2.jsonrpc.method.JSONRPC; +import com.syncedsynapse.kore2.jsonrpc.method.Player; +import com.syncedsynapse.kore2.jsonrpc.notification.Input; +import com.syncedsynapse.kore2.jsonrpc.notification.System; +import com.syncedsynapse.kore2.jsonrpc.type.ListType; +import com.syncedsynapse.kore2.jsonrpc.type.PlayerType; +import com.syncedsynapse.kore2.utils.LogUtils; + +import java.util.ArrayList; +import java.util.List; + +/** + * Object that listens to a connection and notifies observers about changes in its state + * This class serves as an adpater to the {@link HostConnection.PlayerNotificationsObserver}, + * to enable to get notifications not only through TCP but also through HTTP. + * Depending on the connection protocol this class registers itself as an observer for + * {@link HostConnection.PlayerNotificationsObserver} and forwards the notifications it gets, + * or, if through HTTP, starts a periodic polling of XBMC, and tries to discern when a change in + * the player has occurred, notifying the listeners + * + * NOTE: An object of this class should always be called from the same thread. + */ +public class HostConnectionObserver + implements HostConnection.PlayerNotificationsObserver, + HostConnection.SystemNotificationsObserver, + HostConnection.InputNotificationsObserver { + public static final String TAG = LogUtils.makeLogTag(HostConnectionObserver.class); + + /** + * Interface that an observer has to implement to receive player events + */ + public interface PlayerEventsObserver { + /** + * Constants for possible events. Useful to save the last event and compare with the + * current one to check for differences + */ + public static final int PLAYER_NO_RESULT = 0, + PLAYER_CONNECTION_ERROR = 1, + PLAYER_IS_PLAYING = 2, + PLAYER_IS_PAUSED = 3, + PLAYER_IS_STOPPED = 4; + + /** + * Notifies that something is playing + * @param getActivePlayerResult Active player obtained by a call to {@link com.syncedsynapse.kore2.jsonrpc.method.Player.GetActivePlayers} + * @param getPropertiesResult Properties obtained by a call to {@link com.syncedsynapse.kore2.jsonrpc.method.Player.GetProperties} + * @param getItemResult Currently playing item, obtained by a call to {@link com.syncedsynapse.kore2.jsonrpc.method.Player.GetItem} + */ + public void playerOnPlay(PlayerType.GetActivePlayersReturnType getActivePlayerResult, + PlayerType.PropertyValue getPropertiesResult, + ListType.ItemsAll getItemResult); + + /** + * Notifies that something is paused + * @param getActivePlayerResult Active player obtained by a call to {@link com.syncedsynapse.kore2.jsonrpc.method.Player.GetActivePlayers} + * @param getPropertiesResult Properties obtained by a call to {@link com.syncedsynapse.kore2.jsonrpc.method.Player.GetProperties} + * @param getItemResult Currently paused item, obtained by a call to {@link com.syncedsynapse.kore2.jsonrpc.method.Player.GetItem} + */ + public void playerOnPause(PlayerType.GetActivePlayersReturnType getActivePlayerResult, + PlayerType.PropertyValue getPropertiesResult, + ListType.ItemsAll getItemResult); + + /** + * Notifies that media is stopped/nothing is playing + */ + public void playerOnStop(); + + /** + * Called when we get a connection error + * @param errorCode + * @param description + */ + public void playerOnConnectionError(int errorCode, String description); + + /** + * Notifies that we don't have a result yet + */ + public void playerNoResultsYet(); + + /** + * Notifies that XBMC has quit/shutdown/sleep + */ + public void systemOnQuit(); + + /** + * Notifies that XBMC has requested input + */ + public void inputOnInputRequested(String title, String type, String value); + } + + /** + * The connection on which to listen + */ + private HostConnection connection; + + /** + * The list of observers + */ + private List playerEventsObservers = new ArrayList(); + +// /** +// * Handlers for which observer, on which to notify them +// */ +// private Map observerHandlerMap = new HashMap(); + + private Handler checkerHandler = new Handler(); + private Runnable httpCheckerRunnable = new Runnable() { + @Override + public void run() { + final int HTTP_NOTIFICATION_CHECK_INTERVAL = 3000; + // If no one is listening to this, just exit + if (playerEventsObservers.size() == 0) return; + + // Check whats playing + checkWhatsPlaying(); + + // Keep checking + checkerHandler.postDelayed(this, HTTP_NOTIFICATION_CHECK_INTERVAL); + } + }; + + private Runnable tcpCheckerRunnable = new Runnable() { + @Override + public void run() { + final int PING_AFTER_ERROR_CHECK_INTERVAL = 2000, + PING_AFTER_SUCCESS_CHECK_INTERVAL = 10000; + // If no one is listening to this, just exit + if (playerEventsObservers.size() == 0) return; + + JSONRPC.Ping ping = new JSONRPC.Ping(); + ping.execute(connection, new ApiCallback() { + @Override + public void onSucess(String result) { + // Ok, we've got a ping, if we were in a error or uninitialized state, update + if ((lastCallResult == PlayerEventsObserver.PLAYER_NO_RESULT) || + (lastCallResult == PlayerEventsObserver.PLAYER_CONNECTION_ERROR)) { + checkWhatsPlaying(); + } + checkerHandler.postDelayed(tcpCheckerRunnable, PING_AFTER_SUCCESS_CHECK_INTERVAL); + } + + @Override + public void onError(int errorCode, String description) { + // Notify a connection error + notifyConnectionError(errorCode, description, playerEventsObservers); + checkerHandler.postDelayed(tcpCheckerRunnable, PING_AFTER_ERROR_CHECK_INTERVAL); + } + }, checkerHandler); +// if ((lastCallResult == PlayerEventsObserver.PLAYER_NO_RESULT) || +// (lastCallResult == PlayerEventsObserver.PLAYER_CONNECTION_ERROR)) { +// checkerHandler.postDelayed(tcpCheckerRunnable, PING_AFTER_ERROR_CHECK_INTERVAL); +// } else { +// checkerHandler.postDelayed(tcpCheckerRunnable, PING_AFTER_SUCCESS_CHECK_INTERVAL); +// } + } + }; + + private int lastCallResult = PlayerEventsObserver.PLAYER_NO_RESULT; + private PlayerType.GetActivePlayersReturnType lastGetActivePlayerResult = null; + private PlayerType.PropertyValue lastGetPropertiesResult = null; + private ListType.ItemsAll lastGetItemResult = null; + private int lastErrorCode; + private String lastErrorDescription; + + public HostConnectionObserver(HostConnection connection) { + this.connection = connection; + } + + /** + * Registers a new observer that will be notified about player events + * @param observer Observer + */ + public synchronized void registerPlayerObserver(PlayerEventsObserver observer) { + if (this.connection == null) + return; + + // Save this observer and a new handle to notify him + playerEventsObservers.add(observer); +// observerHandlerMap.put(observer, new Handler()); + + if (playerEventsObservers.size() == 1) { + // If this is the first observer, start checking through HTTP or register us + // as a connection observer, which we will pass to the "real" observer + if (connection.getProtocol() == HostConnection.PROTOCOL_TCP) { + connection.registerPlayerNotificationsObserver(this, checkerHandler); + connection.registerSystemNotificationsObserver(this, checkerHandler); + connection.registerInputNotificationsObserver(this, checkerHandler); + // Start the ping checker + checkerHandler.post(tcpCheckerRunnable); + } else { + checkerHandler.post(httpCheckerRunnable); + } + } + } + + /** + * Unregisters a previously registered observer + * @param observer Observer to unregister + */ + public synchronized void unregisterPlayerObserver(PlayerEventsObserver observer) { + // Remove this observer and its associated handler + playerEventsObservers.remove(observer); +// observerHandlerMap.remove(observer); + + LogUtils.LOGD(TAG, "Unregistering observer. Still got " + playerEventsObservers.size() + + " observers."); + + if (playerEventsObservers.size() == 0) { + // No more observers, so unregister us from the host connection, or stop + // the http checker thread + if (connection.getProtocol() == HostConnection.PROTOCOL_TCP) { + connection.unregisterPlayerNotificationsObserver(this); + connection.unregisterSystemNotificationsObserver(this); + connection.unregisterInputNotificationsObserver(this); + checkerHandler.removeCallbacks(tcpCheckerRunnable); + } else { + checkerHandler.removeCallbacks(httpCheckerRunnable); + } + lastCallResult = PlayerEventsObserver.PLAYER_NO_RESULT; + } + } + + /** + * Unregisters all observers + */ + public void unregisterAllObservers() { +// observerHandlerMap.clear(); + playerEventsObservers.clear(); + + if (connection.getProtocol() == HostConnection.PROTOCOL_TCP) { + connection.unregisterPlayerNotificationsObserver(this); + connection.unregisterSystemNotificationsObserver(this); + connection.unregisterInputNotificationsObserver(this); + checkerHandler.removeCallbacks(tcpCheckerRunnable); + } else { + checkerHandler.removeCallbacks(httpCheckerRunnable); + } + lastCallResult = PlayerEventsObserver.PLAYER_NO_RESULT; + } + + /** + * The {@link HostConnection.PlayerNotificationsObserver} interface methods + */ + public void onPlay(com.syncedsynapse.kore2.jsonrpc.notification.Player.OnPlay notification) { + // Just start our chain calls + chainCallGetActivePlayers(); + } + + public void onPause(com.syncedsynapse.kore2.jsonrpc.notification.Player.OnPause + notification) { + // Just start our chain calls + chainCallGetActivePlayers(); + } + + public void onSpeedChanged(com.syncedsynapse.kore2.jsonrpc.notification.Player + .OnSpeedChanged notification) { + // Just start our chain calls + chainCallGetActivePlayers(); + } + + public void onSeek(com.syncedsynapse.kore2.jsonrpc.notification.Player.OnSeek notification) { + // Just start our chain calls + chainCallGetActivePlayers(); + } + + public void onStop(com.syncedsynapse.kore2.jsonrpc.notification.Player.OnStop notification) { + // Just start our chain calls + notifyNothingIsPlaying(playerEventsObservers); + } + + /** + * The {@link HostConnection.SystemNotificationsObserver} interface methods + */ + public void onQuit(System.OnQuit notification) { + for (final PlayerEventsObserver observer : playerEventsObservers) { + observer.systemOnQuit(); + } + } + + public void onRestart(System.OnRestart notification) { + for (final PlayerEventsObserver observer : playerEventsObservers) { + observer.systemOnQuit(); + } + } + + public void onSleep(System.OnSleep notification) { + for (final PlayerEventsObserver observer : playerEventsObservers) { + observer.systemOnQuit(); + } + } + + public void onInputRequested(Input.OnInputRequested notification) { + for (final PlayerEventsObserver observer : playerEventsObservers) { + observer.inputOnInputRequested(notification.title, notification.type, notification.value); + } + } + + /** + * Checks whats playing and notifies observers + */ + private void checkWhatsPlaying() { + LogUtils.LOGD(TAG, "Checking whats playing"); + + // Start the calls: Player.GetActivePlayers -> Player.GetProperties -> Player.GetItem + chainCallGetActivePlayers(); + } + + /** + * Calls Player.GetActivePlayers + * On success chains execution to chainCallGetProperties + */ + private void chainCallGetActivePlayers() { + Player.GetActivePlayers getActivePlayers = new Player.GetActivePlayers(); + getActivePlayers.execute(connection, new ApiCallback>() { + @Override + public void onSucess(ArrayList result) { + if (result.isEmpty()) { + LogUtils.LOGD(TAG, "Nothing is playing"); + notifyNothingIsPlaying(playerEventsObservers); + return; + } + chainCallGetProperties(result.get(0)); + } + + @Override + public void onError(int errorCode, String description) { + LogUtils.LOGD(TAG, "Notifying error"); + notifyConnectionError(errorCode, description, playerEventsObservers); + } + }, checkerHandler); + } + + /** + * Calls Player.GetProperties + * On success chains execution to chainCallGetItem + */ + private void chainCallGetProperties(final PlayerType.GetActivePlayersReturnType getActivePlayersResult) { + String propertiesToGet[] = new String[] { + // Check is something more is needed + PlayerType.PropertyName.SPEED, + PlayerType.PropertyName.PERCENTAGE, + PlayerType.PropertyName.POSITION, + PlayerType.PropertyName.TIME, + PlayerType.PropertyName.TOTALTIME, + PlayerType.PropertyName.REPEAT, + PlayerType.PropertyName.SHUFFLED, + PlayerType.PropertyName.CURRENTAUDIOSTREAM, + PlayerType.PropertyName.CURRENTSUBTITLE, + PlayerType.PropertyName.AUDIOSTREAMS, + PlayerType.PropertyName.SUBTITLES, + }; + + Player.GetProperties getProperties = new Player.GetProperties(getActivePlayersResult.playerid, propertiesToGet); + getProperties.execute(connection, new ApiCallback() { + @Override + public void onSucess(PlayerType.PropertyValue result) { + chainCallGetItem(getActivePlayersResult, result); + } + + @Override + public void onError(int errorCode, String description) { + notifyConnectionError(errorCode, description, playerEventsObservers); + } + }, checkerHandler); + } + + /** + * Calls Player.GetItem + * On success notifies observers + */ + private void chainCallGetItem(final PlayerType.GetActivePlayersReturnType getActivePlayersResult, + final PlayerType.PropertyValue getPropertiesResult) { +// COMMENT, LYRICS, MUSICBRAINZTRACKID, MUSICBRAINZARTISTID, MUSICBRAINZALBUMID, +// MUSICBRAINZALBUMARTISTID, TRAILER, ORIGINALTITLE, LASTPLAYED, MPAA, COUNTRY, +// PRODUCTIONCODE, SET, SHOWLINK, FILE, +// ARTISTID, ALBUMID, TVSHOW_ID, SETID, WATCHEDEPISODES, DISC, TAG, GENREID, +// ALBUMARTISTID, DESCRIPTION, THEME, MOOD, STYLE, ALBUMLABEL, SORTTITLE, UNIQUEID, +// DATEADDED, CHANNEL, CHANNELTYPE, HIDDEN, LOCKED, CHANNELNUMBER, STARTTIME, ENDTIME, +// EPISODEGUIDE, ORIGINALTITLE, PLAYCOUNT, PLOTOUTLINE, SET, + String[] propertiesToGet = new String[] { + ListType.FieldsAll.ART, + ListType.FieldsAll.ARTIST, + ListType.FieldsAll.ALBUMARTIST, + ListType.FieldsAll.ALBUM, + ListType.FieldsAll.CAST, + ListType.FieldsAll.DIRECTOR, + ListType.FieldsAll.DISPLAYARTIST, + ListType.FieldsAll.DURATION, + ListType.FieldsAll.EPISODE, + ListType.FieldsAll.FANART, + ListType.FieldsAll.FILE, + ListType.FieldsAll.FIRSTAIRED, + ListType.FieldsAll.GENRE, + ListType.FieldsAll.IMDBNUMBER, + ListType.FieldsAll.PLOT, + ListType.FieldsAll.PREMIERED, + ListType.FieldsAll.RATING, + ListType.FieldsAll.RESUME, + ListType.FieldsAll.RUNTIME, + ListType.FieldsAll.SEASON, + ListType.FieldsAll.SHOWTITLE, + ListType.FieldsAll.STREAMDETAILS, + ListType.FieldsAll.STUDIO, + ListType.FieldsAll.TAGLINE, + ListType.FieldsAll.THUMBNAIL, + ListType.FieldsAll.TITLE, + ListType.FieldsAll.TOP250, + ListType.FieldsAll.TRACK, + ListType.FieldsAll.VOTES, + ListType.FieldsAll.WRITER, + ListType.FieldsAll.YEAR, + ListType.FieldsAll.DESCRIPTION, + }; +// propertiesToGet = ListType.FieldsAll.allValues; + Player.GetItem getItem = new Player.GetItem(getActivePlayersResult.playerid, propertiesToGet); + getItem.execute(connection, new ApiCallback() { + @Override + public void onSucess(ListType.ItemsAll result) { + // Ok, now we got a result + notifySomethingIsPlaying(getActivePlayersResult, getPropertiesResult, result, playerEventsObservers); + } + + @Override + public void onError(int errorCode, String description) { + notifyConnectionError(errorCode, description, playerEventsObservers); + } + }, checkerHandler); + } + + // Whether to foorce a reply or if the results are equal to the last one, don't reply + private boolean forceReply = false; + + /** + * Notifies a list of observers of a connection error + * Only notifies them if the result is different from the last one + * @param errorCode Error code to report + * @param description Description to report + * @param observers List of observers + */ + private void notifyConnectionError(final int errorCode, final String description, List observers) { + // Reply if different from last result + if (forceReply || + (lastCallResult != PlayerEventsObserver.PLAYER_CONNECTION_ERROR) || + (lastErrorCode != errorCode)) { + lastCallResult = PlayerEventsObserver.PLAYER_CONNECTION_ERROR; + lastErrorCode = errorCode; + lastErrorDescription = description; + forceReply = false; + for (final PlayerEventsObserver observer : observers) { + notifyConnectionError(errorCode, description, observer); + } + } + } + + /** + * Notifies a specific observer of a connection error + * Always notifies the observer, and doesn't save results in last call + * @param errorCode Error code to report + * @param description Description to report + * @param observer Observers + */ + private void notifyConnectionError(final int errorCode, final String description, PlayerEventsObserver observer) { + observer.playerOnConnectionError(errorCode, description); +// Handler observerHandler = observerHandlerMap.get(observer); +// observerHandler.post(new Runnable() { +// @Override +// public void run() { +// observer.playerOnConnectionError(errorCode, description); +// } +// }); + } + + + /** + * Nothing is playing, notify observers calling playerOnStop + * Only notifies them if the result is different from the last one + * @param observers List of observers + */ + private void notifyNothingIsPlaying(List observers) { + // Reply if forced or different from last result + if (forceReply || + (lastCallResult != PlayerEventsObserver.PLAYER_IS_STOPPED)) { + lastCallResult = PlayerEventsObserver.PLAYER_IS_STOPPED; + forceReply = false; + for (final PlayerEventsObserver observer : observers) { + notifyNothingIsPlaying(observer); + } + } + } + + /** + * Notifies a specific observer + * Always notifies the observer, and doesn't save results in last call + * @param observer Observer + */ + private void notifyNothingIsPlaying(PlayerEventsObserver observer) { + observer.playerOnStop(); + } + + /** + * Something is playing or paused, notify observers + * Only notifies them if the result is different from the last one + * @param getActivePlayersResult + * @param getPropertiesResult + * @param getItemResult + * @param observers List of observers + */ + private void notifySomethingIsPlaying(final PlayerType.GetActivePlayersReturnType getActivePlayersResult, + final PlayerType.PropertyValue getPropertiesResult, + final ListType.ItemsAll getItemResult, + List observers) { + int currentCallResult = (getPropertiesResult.speed == 0) ? + PlayerEventsObserver.PLAYER_IS_PAUSED : PlayerEventsObserver.PLAYER_IS_PLAYING; + if (forceReply || + (lastCallResult != currentCallResult) || + (lastGetPropertiesResult.speed != getPropertiesResult.speed) || + (lastGetPropertiesResult.shuffled != getPropertiesResult.shuffled) || + (!lastGetPropertiesResult.repeat.equals(getPropertiesResult.repeat)) || + (lastGetItemResult.id != getItemResult.id)) { + lastCallResult = currentCallResult; + lastGetActivePlayerResult = getActivePlayersResult; + lastGetPropertiesResult = getPropertiesResult; + lastGetItemResult = getItemResult; + forceReply = false; + for (final PlayerEventsObserver observer : observers) { + notifySomethingIsPlaying(getActivePlayersResult, getPropertiesResult, getItemResult, observer); + } + } + } + + /** + * Something is playing or paused, notify a specific observer + * Always notifies the observer, and doesn't save results in last call + * @param getActivePlayersResult + * @param getPropertiesResult + * @param getItemResult + * @param observer Specific observer + */ + private void notifySomethingIsPlaying(final PlayerType.GetActivePlayersReturnType getActivePlayersResult, + final PlayerType.PropertyValue getPropertiesResult, + final ListType.ItemsAll getItemResult, + PlayerEventsObserver observer) { + if (getPropertiesResult.speed == 0) { + // Paused + observer.playerOnPause(getActivePlayersResult, getPropertiesResult, getItemResult); + } else { + // Playing + observer.playerOnPlay(getActivePlayersResult, getPropertiesResult, getItemResult); + } + } + + /** + * Replies to the observer with the last result we got. + * If we have no result, nothing will be called on the observer interface. + * @param observer Obserser to call with last result + */ + public void replyWithLastResult(PlayerEventsObserver observer) { + switch (lastCallResult) { + case PlayerEventsObserver.PLAYER_CONNECTION_ERROR: + notifyConnectionError(lastErrorCode, lastErrorDescription, observer); + break; + case PlayerEventsObserver.PLAYER_IS_STOPPED: + notifyNothingIsPlaying(observer); + break; + case PlayerEventsObserver.PLAYER_IS_PAUSED: + case PlayerEventsObserver.PLAYER_IS_PLAYING: + notifySomethingIsPlaying(lastGetActivePlayerResult, lastGetPropertiesResult, lastGetItemResult, observer); + break; + case PlayerEventsObserver.PLAYER_NO_RESULT: + observer.playerNoResultsYet(); + break; + } + } + + /** + * Forces a refresh of the current cached results + */ + public void forceRefreshResults() { + forceReply = true; + chainCallGetActivePlayers(); + } +} diff --git a/app/src/main/java/com/syncedsynapse/kore2/host/HostInfo.java b/app/src/main/java/com/syncedsynapse/kore2/host/HostInfo.java new file mode 100644 index 0000000..e00ea31 --- /dev/null +++ b/app/src/main/java/com/syncedsynapse/kore2/host/HostInfo.java @@ -0,0 +1,224 @@ +/* + * 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 com.syncedsynapse.kore2.host; + +import com.syncedsynapse.kore2.jsonrpc.HostConnection; +import com.syncedsynapse.kore2.utils.LogUtils; + +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; + +/** + * 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 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; + + /** + * Internal id of the host + */ + private int id; + + /** + * Friendly name of the host + */ + private String name; + + /** + * Connection information + */ + private String address; + private int httpPort; + private int tcpPort; + + /** + * Authentication information + */ + private String username; + private String password; + + /** + * Mac address and Wake On Lan port + */ + private String macAddress; + private int wolPort; + + /** + * Prefered protocol to communicate with this host + */ + private int protocol; + + private 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) { + 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.tcpPort = tcpPort; + this.username = username; + this.password = password; + this.macAddress = macAddress; + this.wolPort = wolPort; + + // For performance reasons + this.auxImageHttpAddress = getHttpURL() + "/image/"; + } + + /** + * Auxiliary constructor for HTTP protocol. + * This constructoor 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 httpPort, int tcpPort, String username, String password) { + this(-1, name, address, HostConnection.PROTOCOL_TCP, httpPort, tcpPort, username, + password, null, DEFAULT_WOL_PORT); + } + + 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 int getProtocol() { + return protocol; + } + + /** + * 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; + } + + /** + * Returns the URL of the host + * @return HTTP URL eg. http://192.168.1.1:8080 + */ + public String getHttpURL() { + return "http://" + 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 "http://" + address + ":" + httpPort + 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, "UTF-8"); + } catch (UnsupportedEncodingException e) { + // Ignore for now... + return null; + } + } +} diff --git a/app/src/main/java/com/syncedsynapse/kore2/host/HostManager.java b/app/src/main/java/com/syncedsynapse/kore2/host/HostManager.java new file mode 100644 index 0000000..b8c9d69 --- /dev/null +++ b/app/src/main/java/com/syncedsynapse/kore2/host/HostManager.java @@ -0,0 +1,396 @@ +/* + * 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 com.syncedsynapse.kore2.host; + +import android.content.ContentValues; +import android.content.Context; +import android.database.Cursor; +import android.net.Uri; + +import com.squareup.picasso.Picasso; +import com.syncedsynapse.kore2.Settings; +import com.syncedsynapse.kore2.provider.MediaContract; +import com.syncedsynapse.kore2.jsonrpc.HostConnection; +import com.syncedsynapse.kore2.utils.BasicAuthPicassoDownloader; +import com.syncedsynapse.kore2.utils.LogUtils; + +import java.util.ArrayList; + +/** + * 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 Context context; + + /** + * Arraylist that will hold all the hosts in the database + */ + private ArrayList 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(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 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 getHosts(boolean forcedReload) { + if (forcedReload || (hosts.size() == 0)) { + hosts.clear(); + + Cursor cursor = context.getContentResolver() + .query(MediaContract.Hosts.CONTENT_URI, + MediaContract.Hosts.ALL_COLUMNS, + null, null, null); + + 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++); + + hosts.add(new HostInfo(id, name, address, protocol, httpPort, tcpPort, + username, password, macAddress, wolPort)); + } + } + cursor.close(); + } + return hosts; + } + + /** + * Returns the current active host info + * @return Active host info + */ + public HostInfo getHostInfo() { + if (currentHostInfo == null) { + Settings settings = Settings.getInstance(context); + ArrayList hosts = getHosts(); + + // No host selected. Check if there are hosts configured and default to the first one + if (settings.currentHostId == -1) { + if (hosts.size() > 0) { + currentHostInfo = hosts.get(0); + settings.currentHostId = currentHostInfo.getId(); + settings.save(); + } + } else { + for (HostInfo host : hosts) { + if (host.getId() == settings.currentHostId) { + currentHostInfo = host; + break; + } + } + } + } + return currentHostInfo; + } + + /** + * Returns the current active host connection + * @return Active host connection + */ + public HostConnection getConnection() { + 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 BasicAuthPicassoDownloader(context, + currentHostInfo.getUsername(), currentHostInfo.getPassword())) + .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) { + Settings settings = Settings.getInstance(context); + settings.currentHostId = currentHostInfo.getId(); + settings.save(); + } + } + +// /** +// * Sets the current host. +// * Throws {@link java.lang.IllegalArgumentException} if the host doesn't exist +// * @param hostId Host id +// */ +// public void switchHost(int hostId) { +// ArrayList hosts = getHosts(); +// HostInfo newHostInfo = null; +// +// for (HostInfo host : hosts) { +// if (host.getId() == hostId) { +// newHostInfo = host; +// break; +// } +// } +// +// if (newHostInfo == null) { +// throw new IllegalArgumentException("Host doesn't exist!"); +// } +// switchHost(newHostInfo); +// } + + /** + * Adds a new XBMC host to the database + * @param hostInfo Host to add + * @return Newly created {@link com.syncedsynapse.kore2.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()); + } + + + /** + * 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 com.syncedsynapse.kore2.host.HostInfo} + */ + public HostInfo addHost(String name, String address, int protocol, int httpPort, int tcpPort, + String username, String password, String macAddress, int wolPort) { + + 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); + + Uri newUri = context.getContentResolver() + .insert(MediaContract.Hosts.CONTENT_URI, values); + long newId = Long.valueOf(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()); + + 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(new Runnable() { + @Override + public void run() { + 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.size() > 0) + switchHost(hosts.get(0)); + } + } + + /** + * Releases all state related to the current connection + */ + private void releaseCurrentHost() { + if (currentHostConnectionObserver != null) { + currentHostConnectionObserver.unregisterAllObservers(); + 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; + } + } +} diff --git a/app/src/main/java/com/syncedsynapse/kore2/jsonrpc/ApiCallback.java b/app/src/main/java/com/syncedsynapse/kore2/jsonrpc/ApiCallback.java new file mode 100644 index 0000000..a7d1cbe --- /dev/null +++ b/app/src/main/java/com/syncedsynapse/kore2/jsonrpc/ApiCallback.java @@ -0,0 +1,43 @@ +/* + * 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 com.syncedsynapse.kore2.jsonrpc; + +/** + * 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#onSucess(Object)} or {@link + * ApiCallback#onError(int, String)} will be called. + * * @param Result type + */ +public interface ApiCallback { + + /** + * 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 + */ + public abstract void onSucess(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 + */ + public abstract void onError(int errorCode, String description); +} diff --git a/app/src/main/java/com/syncedsynapse/kore2/jsonrpc/ApiException.java b/app/src/main/java/com/syncedsynapse/kore2/jsonrpc/ApiException.java new file mode 100644 index 0000000..bf02ab9 --- /dev/null +++ b/app/src/main/java/com/syncedsynapse/kore2/jsonrpc/ApiException.java @@ -0,0 +1,119 @@ +/* + * 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 com.syncedsynapse.kore2.jsonrpc; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.syncedsynapse.kore2.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; + + /** + * 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; + + private 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; + } +} diff --git a/app/src/main/java/com/syncedsynapse/kore2/jsonrpc/ApiMethod.java b/app/src/main/java/com/syncedsynapse/kore2/jsonrpc/ApiMethod.java new file mode 100644 index 0000000..3b8b50c --- /dev/null +++ b/app/src/main/java/com/syncedsynapse/kore2/jsonrpc/ApiMethod.java @@ -0,0 +1,282 @@ +/* + * 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 com.syncedsynapse.kore2.jsonrpc; + + +import android.os.Handler; + +import com.fasterxml.jackson.core.JsonProcessingException; +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 com.syncedsynapse.kore2.jsonrpc.type.ApiParameter; +import com.syncedsynapse.kore2.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 { + 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() { + synchronized (this) { + this.id = (++lastId % 10000); + } + + // 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()); + jsonRequest.put(ID_NODE, id); + } + + /** + * 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.put(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 (int i = 0; i < values.length; i++) { + arrayNode.add(values[i]); + } + getParametersNode().put(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().put(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().put(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. +// * This call is always asynchronous. The results will be posted, through the callback parameter, +// * on the same thread that is calling this method. +// * Note: The current thread must have a Looper prepared, otherwise this will fail because we +// * try to get handler on the thread. +// * +// * @param hostConnection Host connection on which to call the method +// * @param callback Callbacks to post the response to +// */ +// public void execute(HostConnection hostConnection, ApiCallback callback) { +// execute(hostConnection, callback, new Handler(Looper.myLooper())); +// } + + /** + * Calls the method represented by this object on the server. + * This call is always asynchronous. The results will be posted, through the callback parameter, + * 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 callback, Handler handler) { + if (hostConnection != null) { + hostConnection.execute(this, callback, handler); + } else { + 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 (JsonProcessingException e) { + throw new ApiException(ApiException.INVALID_JSON_RESPONSE_FROM_HOST, e); + } 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 ApiCallback getDefaultActionCallback() { + + return new ApiCallback() { + @Override + public void onSucess(T result) { + } + + @Override + public void onError(int errorCode, String description) { + LogUtils.LOGD(TAG, "Got an error calling a method. Error code: " + errorCode + ", description: " + description); + } + }; + } + +} diff --git a/app/src/main/java/com/syncedsynapse/kore2/jsonrpc/ApiNotification.java b/app/src/main/java/com/syncedsynapse/kore2/jsonrpc/ApiNotification.java new file mode 100644 index 0000000..f7316cf --- /dev/null +++ b/app/src/main/java/com/syncedsynapse/kore2/jsonrpc/ApiNotification.java @@ -0,0 +1,71 @@ +/* + * 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 com.syncedsynapse.kore2.jsonrpc; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.syncedsynapse.kore2.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 { + protected static final String METHOD_NODE = "method"; + protected 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; + if (method.equals(Player.OnPause.NOTIFICATION_NAME)) { + result = new Player.OnPause(params); + } else if (method.equals(Player.OnPlay.NOTIFICATION_NAME)) { + result = new Player.OnPlay(params); + } else if (method.equals(Player.OnSeek.NOTIFICATION_NAME)) { + result = new Player.OnSeek(params); + } else if (method.equals(Player.OnSpeedChanged.NOTIFICATION_NAME)) { + result = new Player.OnSpeedChanged(params); + } else if (method.equals(Player.OnStop.NOTIFICATION_NAME)) { + result = new Player.OnStop(params); + } + + return result; + } +} diff --git a/app/src/main/java/com/syncedsynapse/kore2/jsonrpc/HostConnection.java b/app/src/main/java/com/syncedsynapse/kore2/jsonrpc/HostConnection.java new file mode 100644 index 0000000..a7b0d57 --- /dev/null +++ b/app/src/main/java/com/syncedsynapse/kore2/jsonrpc/HostConnection.java @@ -0,0 +1,814 @@ +/* + * 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 com.syncedsynapse.kore2.jsonrpc; + +import android.os.*; +import android.os.Process; +import android.util.Base64; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.syncedsynapse.kore2.host.HostInfo; +import com.syncedsynapse.kore2.jsonrpc.notification.*; +import com.syncedsynapse.kore2.jsonrpc.notification.System; +import com.syncedsynapse.kore2.utils.LogUtils; + +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.OutputStreamWriter; +import java.net.HttpURLConnection; +import java.net.InetSocketAddress; +import java.net.ProtocolException; +import java.net.Socket; +import java.net.SocketException; +import java.net.URL; +import java.util.HashMap; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +/** + * Class responsible for communicating with the host. + */ +public class HostConnection { + public static final String TAG = LogUtils.makeLogTag(HostConnection.class); + + private static final int TIMEOUT = 5000; // ns + + /** + * Communicate via TCP + */ + public static final int PROTOCOL_TCP = 0; + /** + * Communicate via HTTP + */ + public static final int PROTOCOL_HTTP = 1; + + /** + * Interface that an observer must implement to be notified of player notifications + */ + public interface PlayerNotificationsObserver { + public void onPlay(Player.OnPlay notification); + public void onPause(Player.OnPause notification); + public void onSpeedChanged(Player.OnSpeedChanged notification); + public void onSeek(Player.OnSeek notification); + public void onStop(Player.OnStop notification); + } + + /** + * Interface that an observer must implement to be notified of System notifications + */ + public interface SystemNotificationsObserver { + public void onQuit(System.OnQuit notification); + public void onRestart(System.OnRestart notification); + public void onSleep(System.OnSleep notification); + } + + /** + * Interface that an observer must implement to be notified of Input notifications + */ + public interface InputNotificationsObserver { + public void onInputRequested(Input.OnInputRequested notification); + } + + /** + * Host to connect too + */ + private final HostInfo hostInfo; + + /** + * The protocol to use: {@link #PROTOCOL_HTTP} or {@link #PROTOCOL_TCP} + * This is initially obtained from the {@link HostInfo}, but can be later changed through + * {@link #setProtocol(int)} + */ + private int protocol; + + private final ObjectMapper objectMapper = new ObjectMapper(); + + /** + * Socket used to communicate through TCP + */ + private Socket socket = null; + /** + * Listener {@link Thread} that will be listening on the TCP socket + */ + private Thread listenerThread = null; + + /** + * {@link java.util.HashMap} that will hold the {@link MethodCallInfo} with the information + * necessary to respond to clients (TCP only) + */ + private final HashMap> clientCallbacks = new HashMap>(); + + /** + * The observers that will be notified of player notifications + */ + private final HashMap playerNotificationsObservers = + new HashMap(); + + /** + * The observers that will be notified of system notifications + */ + private final HashMap systemNotificationsObservers = + new HashMap(); + + /** + * The observers that will be notified of input notifications + */ + private final HashMap inputNotificationsObservers = + new HashMap(); + + private ExecutorService executorService; + + public HostConnection(final HostInfo hostInfo) { + this.hostInfo = hostInfo; + // Start with the default host protocol + this.protocol = hostInfo.getProtocol(); + // Create a single threaded executor + this.executorService = Executors.newSingleThreadExecutor(); + } + + /** + * Returns this connection protocol + * @return {@link #PROTOCOL_HTTP} or {@link #PROTOCOL_TCP} + */ + public int getProtocol() { + return protocol; + } + + /** + * Overrides the protocol for this connection + * @param protocol {@link #PROTOCOL_HTTP} or {@link #PROTOCOL_TCP} + */ + public void setProtocol(int protocol) { + if (!isValidProtocol(protocol)) { + throw new IllegalArgumentException("Invalid protocol specified."); + } + this.protocol = protocol; + } + + public static boolean isValidProtocol(int protocol) { + return ((protocol == PROTOCOL_TCP) || (protocol == PROTOCOL_HTTP)); + } + + /** + * Registers an observer for player notifications + * @param observer The {@link PlayerNotificationsObserver} + */ + public void registerPlayerNotificationsObserver(PlayerNotificationsObserver observer, + Handler handler) { + playerNotificationsObservers.put(observer, handler); + } + + /** + * Unregisters and observer from the player notifications + * @param observer The {@link PlayerNotificationsObserver} to unregister + */ + public void unregisterPlayerNotificationsObserver(PlayerNotificationsObserver observer) { + playerNotificationsObservers.remove(observer); + } + + /** + * Registers an observer for system notifications + * @param observer The {@link SystemNotificationsObserver} + */ + public void registerSystemNotificationsObserver(SystemNotificationsObserver observer, + Handler handler) { + systemNotificationsObservers.put(observer, handler); + } + + /** + * Unregisters and observer from the system notifications + * @param observer The {@link SystemNotificationsObserver} + */ + public void unregisterSystemNotificationsObserver(SystemNotificationsObserver observer) { + systemNotificationsObservers.remove(observer); + } + + /** + * Registers an observer for input notifications + * @param observer The {@link InputNotificationsObserver} + */ + public void registerInputNotificationsObserver(InputNotificationsObserver observer, + Handler handler) { + inputNotificationsObservers.put(observer, handler); + } + + /** + * Unregisters and observer from the input notifications + * @param observer The {@link InputNotificationsObserver} + */ + public void unregisterInputNotificationsObserver(InputNotificationsObserver observer) { + inputNotificationsObservers.remove(observer); + } + + /** + * Calls the a method on the server + * This call is always asynchronous. The results will be posted, through the + * {@link ApiCallback callback} parameter, on the specified {@link android.os.Handler}. + * + * @param method Method object that represents the methood too call + * @param callback {@link ApiCallback} to post the response to + * @param handler {@link Handler} to invoke callbacks on + * @param Method return type + */ + public void execute(final ApiMethod method, final ApiCallback callback, + final Handler handler) { + LogUtils.LOGD(TAG, "Starting method execute. Method: " + method.getMethodName() + + " on host: " + hostInfo.getJsonRpcHttpEndpoint()); + + // Launch background thread + Runnable command = new Runnable() { + @Override + public void run() { + Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); + if (protocol == PROTOCOL_HTTP) { + executeThroughHTTP(method, callback, handler); + } else { + executeThroughTcp(method, callback, handler); + } + } + }; + + executorService.execute(command); + //new Thread(command).start(); + } + + /** + * Sends the JSON RPC request through HTTP + */ + private void executeThroughHTTP(final ApiMethod method, final ApiCallback callback, + final Handler handler) { + String jsonRequest = method.toJsonString(); + try { + HttpURLConnection connection = openHttpConnection(hostInfo); + sendHttpRequest(connection, jsonRequest); + // Read response and convert it + final T result = method.resultFromJson(parseJsonResponse(readHttpResponse(connection))); + + if ((handler != null) && (callback != null)) { + handler.post(new Runnable() { + @Override + public void run() { + callback.onSucess(result); + } + }); + } + } catch (final ApiException e) { + // Got an error, call error handler + + if ((handler != null) && (callback != null)) { + handler.post(new Runnable() { + @Override + public void run() { + callback.onError(e.getCode(), e.getMessage()); + } + }); + } + } + } + + /** + * Sends the JSON RPC request through HTTP, and calls the callback with the raw response, + * not parsed into the internal representation. + * Useful for sync methods that don't want to incur the overhead of constructing the + * internal objects. + * + * @param method Method object that represents the method too call + * @param callback {@link ApiCallback} to post the response to. This will be the raw + * {@link ObjectNode} received + * @param handler {@link Handler} to invoke callbacks on + * @param Method return type + */ + public void executeRaw(final ApiMethod method, final ApiCallback callback, + final Handler handler) { + String jsonRequest = method.toJsonString(); + try { + HttpURLConnection connection = openHttpConnection(hostInfo); + sendHttpRequest(connection, jsonRequest); + // Read response and convert it + final ObjectNode result = parseJsonResponse(readHttpResponse(connection)); + + if ((handler != null) && (callback != null)) { + handler.post(new Runnable() { + @Override + public void run() { + callback.onSucess(result); + } + }); + } + } catch (final ApiException e) { + // Got an error, call error handler + if ((handler != null) && (callback != null)) { + handler.post(new Runnable() { + @Override + public void run() { + callback.onError(e.getCode(), e.getMessage()); + } + }); + } + } + } + + + /** + * Auxiliary method to open a HTTP connection. + * This method calls connect() so that any errors are cathced + * @param hostInfo Host info + * @return Connection set up + * @throws ApiException + */ + private HttpURLConnection openHttpConnection(HostInfo hostInfo) throws ApiException { + try { +// LogUtils.LOGD(TAG, "Opening HTTP connection."); + HttpURLConnection connection = (HttpURLConnection) new URL(hostInfo.getJsonRpcHttpEndpoint()).openConnection(); + connection.setRequestMethod("POST"); + connection.setConnectTimeout(TIMEOUT); + connection.setReadTimeout(TIMEOUT); + connection.setRequestProperty("Content-Type", "application/json"); + connection.setDoOutput(true); + + // http basic authorization + if ((hostInfo.getUsername() != null) && !hostInfo.getUsername().isEmpty() && + (hostInfo.getPassword() != null) && !hostInfo.getPassword().isEmpty()) { + final String token = Base64.encodeToString((hostInfo.getUsername() + ":" + + hostInfo.getPassword()).getBytes(), Base64.DEFAULT); + connection.setRequestProperty("Authorization", "Basic " + token); + } + + // Check the connection + connection.connect(); + return connection; + } catch (ProtocolException e) { + // Won't try to catch this + LogUtils.LOGE(TAG, "Got protocol exception while opening HTTP connection.", e); + throw new RuntimeException(e); + } catch (IOException e) { + LogUtils.LOGW(TAG, "Failed to open HTTP connection.", e); + throw new ApiException(ApiException.IO_EXCEPTION_WHILE_CONNECTING, e); + } + } + + /** + * Send an HTTP POST request + * @param connection Open connection + * @param request Request to send + * @throws ApiException + */ + private void sendHttpRequest(HttpURLConnection connection, String request) throws ApiException { + try { + LogUtils.LOGD(TAG, "Sending request via HTTP: " + request); + OutputStreamWriter out = new OutputStreamWriter(connection.getOutputStream()); + out.write(request); + out.flush(); + out.close(); + } catch (IOException e) { + LogUtils.LOGW(TAG, "Failed to send HTTP request.", e); + throw new ApiException(ApiException.IO_EXCEPTION_WHILE_SENDING_REQUEST, e); + } + + } + + /** + * Reads the response from the server + * @param connection Connection + * @return Response read + * @throws ApiException + */ + private String readHttpResponse(HttpURLConnection connection) throws ApiException { + try { +// LogUtils.LOGD(TAG, "Reading HTTP response."); + int responseCode = connection.getResponseCode(); + + switch (responseCode) { + case 200: + // All ok, read response + BufferedReader in = new BufferedReader(new InputStreamReader(connection.getInputStream())); + StringBuilder response = new StringBuilder(); + String inputLine; + while ((inputLine = in.readLine()) != null) + response.append(inputLine); + in.close(); + LogUtils.LOGD(TAG, "HTTP response: " + response.toString()); + return response.toString(); + case 401: + LogUtils.LOGD(TAG, "HTTP response read error. Got a 401."); + throw new ApiException(ApiException.HTTP_RESPONSE_CODE_UNAUTHORIZED, + "Server returned response code: " + responseCode); + case 404: + LogUtils.LOGD(TAG, "HTTP response read error. Got a 404."); + throw new ApiException(ApiException.HTTP_RESPONSE_CODE_NOT_FOUND, + "Server returned response code: " + responseCode); + default: + LogUtils.LOGD(TAG, "HTTP response read error. Got: " + responseCode); + throw new ApiException(ApiException.HTTP_RESPONSE_CODE_UNKNOWN, + "Server returned response code: " + responseCode); + } + } catch (IOException e) { + LogUtils.LOGW(TAG, "Failed to read HTTP response.", e); + throw new ApiException(ApiException.IO_EXCEPTION_WHILE_READING_RESPONSE, e); + } + } + + /** + * Parses the JSON response from the server. + * If it is a valid result returns the JSON {@link com.fasterxml.jackson.databind.node.ObjectNode} that represents it. + * If it is an error (contains the error tag), returns an {@link ApiException} with the info. + * @param response JSON response + * @return {@link com.fasterxml.jackson.databind.node.ObjectNode} constructed + * @throws ApiException + */ + private ObjectNode parseJsonResponse(String response) throws ApiException { +// LogUtils.LOGD(TAG, "Parsing JSON response"); + try { + ObjectNode jsonResponse = (ObjectNode) objectMapper.readTree(response); + + if (jsonResponse.has(ApiMethod.ERROR_NODE)) { + throw new ApiException(ApiException.API_ERROR, jsonResponse); + } + + if (!jsonResponse.has(ApiMethod.RESULT_NODE)) { + // Something strange is going on + throw new ApiException(ApiException.INVALID_JSON_RESPONSE_FROM_HOST, + "Result doesn't contain a result node."); + } + + return jsonResponse; + } catch (JsonProcessingException e) { + LogUtils.LOGW(TAG, "Got an exception while parsing JSON response.", e); + throw new ApiException(ApiException.INVALID_JSON_RESPONSE_FROM_HOST, e); + } catch (IOException e) { + LogUtils.LOGW(TAG, "Got an exception while parsing JSON response.", e); + throw new ApiException(ApiException.INVALID_JSON_RESPONSE_FROM_HOST, e); + } + } + + /** + * Sends the JSON RPC request through TCP + * Keeps a background thread running, listening on a socket + */ + private void executeThroughTcp(final ApiMethod method, final ApiCallback callback, + final Handler handler) { + + // TODO: We're going to create a background listener thread. + // Also create a thread that periodically checks if the connection should be shutdown + // based on not having activity. Use android timer or Thread.sleep + String methodId = String.valueOf(method.getId()); + try { + // Save this method/callback for later response + // Check if a method with this id is already running and raise an error if so + synchronized (clientCallbacks) { + if (clientCallbacks.containsKey(methodId)) { + if ((handler != null) && (callback != null)) { + handler.post(new Runnable() { + @Override + public void run() { + callback.onError(ApiException.API_METHOD_WITH_SAME_ID_ALREADY_EXECUTING, + "A method with the same Id is already executing"); + } + }); + } + return; + } + clientCallbacks.put(methodId, new MethodCallInfo(method, callback, handler)); + } + + // TODO: Validate if this shouldn't be enclosed by a synchronized. + if (socket == null) { + // Open connection to the server and setup reader thread + socket = openTcpConnection(hostInfo); + listenerThread = newListenerThread(socket); + listenerThread.start(); + } + + // Write request + sendTcpRequest(socket, method.toJsonString()); + } catch (final ApiException e) { + callErrorCallback(methodId, e); + } + } + + /** + * Auxiliary method to open the TCP {@link Socket}. + * This method calls connect() so that any errors are cathced + * @param hostInfo Host info + * @return Connection set up + * @throws ApiException + */ + private Socket openTcpConnection(HostInfo hostInfo) throws ApiException { + try { + LogUtils.LOGD(TAG, "Opening TCP connection on host: " + hostInfo.getAddress()); + + Socket socket = new Socket(); + final InetSocketAddress address = new InetSocketAddress(hostInfo.getAddress(), hostInfo.getTcpPort()); + socket.setSoTimeout(0); // No read timeout. Read should block + socket.connect(address, TIMEOUT); + + return socket; + } catch (SocketException e) { + LogUtils.LOGW(TAG, "Failed to open TCP connection to host: " + hostInfo.getAddress()); + throw new ApiException(ApiException.IO_EXCEPTION_WHILE_CONNECTING, e); + } catch (IOException e) { + LogUtils.LOGW(TAG, "Failed to open TCP connection to host: " + hostInfo.getAddress()); + throw new ApiException(ApiException.IO_EXCEPTION_WHILE_CONNECTING, e); + } + } + + + /** + * Send a TCP request + * @param socket Socket to write to + * @param request Request to send + * @throws ApiException + */ + private void sendTcpRequest(Socket socket, String request) throws ApiException { + try { + LogUtils.LOGD(TAG, "Sending request via TCP: " + request); + BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream())); + writer.write(request); + writer.flush(); + } catch (Exception e) { + LogUtils.LOGW(TAG, "Failed to send TCP request.", e); + disconnect(); + throw new ApiException(ApiException.IO_EXCEPTION_WHILE_SENDING_REQUEST, e); + } + } + + private Thread newListenerThread(final Socket socket) { + // Launch a new thread to read from the socket + return new Thread(new Runnable() { + @Override + public void run() { + Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); + try { + LogUtils.LOGD(TAG, "Starting socket listener thread..."); + // We're going to read from the socket. This will be a blocking call and + // it will keep on going until disconnect() is called on this object. + // Note: Mind the objects used here: we use createParser because it doesn't + // close the socket after ObjectMapper.readTree. + JsonParser jsonParser = objectMapper.getFactory().createParser(socket.getInputStream()); + ObjectNode jsonResponse; + while ((jsonResponse = objectMapper.readTree(jsonParser)) != null) { + LogUtils.LOGD(TAG, "Read from socket: " + jsonResponse.toString()); +// LogUtils.LOGD_FULL(TAG, "Read from socket: " + jsonResponse.toString()); + handleTcpResponse(jsonResponse); + } + } catch (JsonProcessingException e) { + LogUtils.LOGW(TAG, "Got an exception while parsing JSON response.", e); + callErrorCallback(null, new ApiException(ApiException.INVALID_JSON_RESPONSE_FROM_HOST, e)); + } catch (IOException e) { + LogUtils.LOGW(TAG, "Error reading from socket.", e); + disconnect(); + callErrorCallback(null, new ApiException(ApiException.IO_EXCEPTION_WHILE_READING_RESPONSE, e)); + } + } + }); + } + + private void handleTcpResponse(ObjectNode jsonResponse) { + + if (!jsonResponse.has(ApiMethod.ID_NODE)) { + // It's a notification, notify observers + String notificationName = jsonResponse.get(ApiNotification.METHOD_NODE).asText(); + ObjectNode params = (ObjectNode)jsonResponse.get(ApiNotification.PARAMS_NODE); + + if (notificationName.equals(Player.OnPause.NOTIFICATION_NAME)) { + final Player.OnPause apiNotification = new Player.OnPause(params); + for (final PlayerNotificationsObserver observer : + playerNotificationsObservers.keySet()) { + Handler handler = playerNotificationsObservers.get(observer); + handler.post(new Runnable() { + @Override + public void run() { + observer.onPause(apiNotification); + } + }); + } + } else if (notificationName.equals(Player.OnPlay.NOTIFICATION_NAME)) { + final Player.OnPlay apiNotification = new Player.OnPlay(params); + for (final PlayerNotificationsObserver observer : + playerNotificationsObservers.keySet()) { + Handler handler = playerNotificationsObservers.get(observer); + handler.post(new Runnable() { + @Override + public void run() { + observer.onPlay(apiNotification); + } + }); + } + } else if (notificationName.equals(Player.OnSeek.NOTIFICATION_NAME)) { + final Player.OnSeek apiNotification = new Player.OnSeek(params); + for (final PlayerNotificationsObserver observer : + playerNotificationsObservers.keySet()) { + Handler handler = playerNotificationsObservers.get(observer); + handler.post(new Runnable() { + @Override + public void run() { + observer.onSeek(apiNotification); + } + }); + } + } else if (notificationName.equals(Player.OnSpeedChanged.NOTIFICATION_NAME)) { + final Player.OnSpeedChanged apiNotification = new Player.OnSpeedChanged(params); + for (final PlayerNotificationsObserver observer : + playerNotificationsObservers.keySet()) { + Handler handler = playerNotificationsObservers.get(observer); + handler.post(new Runnable() { + @Override + public void run() { + observer.onSpeedChanged(apiNotification); + } + }); + } + } else if (notificationName.equals(Player.OnStop.NOTIFICATION_NAME)) { + final Player.OnStop apiNotification = new Player.OnStop(params); + for (final PlayerNotificationsObserver observer : + playerNotificationsObservers.keySet()) { + Handler handler = playerNotificationsObservers.get(observer); + handler.post(new Runnable() { + @Override + public void run() { + observer.onStop(apiNotification); + } + }); + } + } else if (notificationName.equals(System.OnQuit.NOTIFICATION_NAME)) { + final System.OnQuit apiNotification = new System.OnQuit(params); + for (final SystemNotificationsObserver observer : + systemNotificationsObservers.keySet()) { + Handler handler = systemNotificationsObservers.get(observer); + handler.post(new Runnable() { + @Override + public void run() { + observer.onQuit(apiNotification); + } + }); + } + } else if (notificationName.equals(System.OnRestart.NOTIFICATION_NAME)) { + final System.OnRestart apiNotification = new System.OnRestart(params); + for (final SystemNotificationsObserver observer : + systemNotificationsObservers.keySet()) { + Handler handler = systemNotificationsObservers.get(observer); + handler.post(new Runnable() { + @Override + public void run() { + observer.onRestart(apiNotification); + } + }); + } + } else if (notificationName.equals(System.OnSleep.NOTIFICATION_NAME)) { + final System.OnSleep apiNotification = new System.OnSleep(params); + for (final SystemNotificationsObserver observer : + systemNotificationsObservers.keySet()) { + Handler handler = systemNotificationsObservers.get(observer); + handler.post(new Runnable() { + @Override + public void run() { + observer.onSleep(apiNotification); + } + }); + } + } else if (notificationName.equals(Input.OnInputRequested.NOTIFICATION_NAME)) { + final Input.OnInputRequested apiNotification = new Input.OnInputRequested(params); + for (final InputNotificationsObserver observer : + inputNotificationsObservers.keySet()) { + Handler handler = inputNotificationsObservers.get(observer); + handler.post(new Runnable() { + @Override + public void run() { + observer.onInputRequested(apiNotification); + } + }); + } + } + + LogUtils.LOGD(TAG, "Got a notification: " + jsonResponse.get("method").textValue()); + } else { + String methodId = jsonResponse.get(ApiMethod.ID_NODE).asText(); + + if (jsonResponse.has(ApiMethod.ERROR_NODE)) { + // Error response + callErrorCallback(methodId, new ApiException(ApiException.API_ERROR, jsonResponse)); + } else { + // Sucess response + final MethodCallInfo methodCallInfo = clientCallbacks.get(methodId); +// LogUtils.LOGD(TAG, "Sending response to method: " + methodCallInfo.method.getMethodName()); + + if (methodCallInfo != null) { + try { + final T result = (T) methodCallInfo.method.resultFromJson(jsonResponse); + final ApiCallback callback = (ApiCallback) methodCallInfo.callback; + + if ((methodCallInfo.handler != null) && (callback != null)) { + methodCallInfo.handler.post(new Runnable() { + @Override + public void run() { + callback.onSucess(result); + } + }); + } + + // We've replied, remove the client from the list + synchronized (clientCallbacks) { + clientCallbacks.remove(methodId); + } + } catch (ApiException e) { + callErrorCallback(methodId, e); + } + } + } + } + } + + private void callErrorCallback(String methodId, final ApiException error) { + synchronized (clientCallbacks) { + if (methodId != null) { + // Send error back to client + final MethodCallInfo methodCallInfo = clientCallbacks.get(methodId); + if (methodCallInfo != null) { + final ApiCallback callback = (ApiCallback) methodCallInfo.callback; + + if ((methodCallInfo.handler != null) && (callback != null)) { + methodCallInfo.handler.post(new Runnable() { + @Override + public void run() { + callback.onError(error.getCode(), error.getMessage()); + } + }); + } + } + clientCallbacks.remove(methodId); + } else { + // Notify all pending clients, it might be an error for them + for (String id : clientCallbacks.keySet()) { + final MethodCallInfo methodCallInfo = clientCallbacks.get(id); + final ApiCallback callback = (ApiCallback)methodCallInfo.callback; + + if ((methodCallInfo.handler != null) && (callback != null)) { + methodCallInfo.handler.post(new Runnable() { + @Override + public void run() { + callback.onError(error.getCode(), error.getMessage()); + } + }); + } + } + clientCallbacks.clear(); + } + } + } + + /** + * Cleans up used resources. + * This method should always be called if the protocoll used is TCP, so we can shutdown gracefully + */ + public void disconnect() { + if (protocol == PROTOCOL_HTTP) + return; + + try { + if (socket != null) { + // Remove pending calls + if (!socket.isClosed()) { + socket.close(); + } + } + } catch (IOException e) { + LogUtils.LOGE(TAG, "Error while closing socket", e); + } finally { + socket = null; + } + } + + /** + * Helper class to aggregate a method, callback and handler + * @param + */ + private static class MethodCallInfo { + public final ApiMethod method; + public final ApiCallback callback; + public final Handler handler; + + public MethodCallInfo(ApiMethod method, ApiCallback callback, Handler handler) { + this.method = method; + this.callback = callback; + this.handler = handler; + } + } +} diff --git a/app/src/main/java/com/syncedsynapse/kore2/jsonrpc/event/MediaSyncEvent.java b/app/src/main/java/com/syncedsynapse/kore2/jsonrpc/event/MediaSyncEvent.java new file mode 100644 index 0000000..f2df9fb --- /dev/null +++ b/app/src/main/java/com/syncedsynapse/kore2/jsonrpc/event/MediaSyncEvent.java @@ -0,0 +1,54 @@ +/* + * 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 com.syncedsynapse.kore2.jsonrpc.event; + +import android.os.Bundle; + +/** + * 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 com.syncedsynapse.kore2.service.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; + } +} diff --git a/app/src/main/java/com/syncedsynapse/kore2/jsonrpc/method/Addons.java b/app/src/main/java/com/syncedsynapse/kore2/jsonrpc/method/Addons.java new file mode 100644 index 0000000..1c447f1 --- /dev/null +++ b/app/src/main/java/com/syncedsynapse/kore2/jsonrpc/method/Addons.java @@ -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 com.syncedsynapse.kore2.jsonrpc.method; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.syncedsynapse.kore2.jsonrpc.ApiException; +import com.syncedsynapse.kore2.jsonrpc.ApiMethod; +import com.syncedsynapse.kore2.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 { + 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> { + 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 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 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 { + 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 { + 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(); + } + } + +} diff --git a/app/src/main/java/com/syncedsynapse/kore2/jsonrpc/method/Application.java b/app/src/main/java/com/syncedsynapse/kore2/jsonrpc/method/Application.java new file mode 100644 index 0000000..19dfc2c --- /dev/null +++ b/app/src/main/java/com/syncedsynapse/kore2/jsonrpc/method/Application.java @@ -0,0 +1,131 @@ +/* + * 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 com.syncedsynapse.kore2.jsonrpc.method; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.syncedsynapse.kore2.jsonrpc.ApiException; +import com.syncedsynapse.kore2.jsonrpc.ApiMethod; +import com.syncedsynapse.kore2.jsonrpc.type.ApplicationType; +import com.syncedsynapse.kore2.utils.JsonUtils; + +/** + * All JSON RPC methods in Application.* + */ +public class Application { + + /** + * Quit application + */ + public static final class Quit extends ApiMethod { + 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 { + public final static String METHOD_NAME = "Application.SetVolume"; + + /** + * Set the current volume + * @param volume String enum in {@link com.syncedsynapse.kore2.jsonrpc.type.GlobalType.IncrementDecrement} + */ + public SetVolume(String 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 { + 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 { + 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)); + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/syncedsynapse/kore2/jsonrpc/method/AudioLibrary.java b/app/src/main/java/com/syncedsynapse/kore2/jsonrpc/method/AudioLibrary.java new file mode 100644 index 0000000..3e8749e --- /dev/null +++ b/app/src/main/java/com/syncedsynapse/kore2/jsonrpc/method/AudioLibrary.java @@ -0,0 +1,256 @@ +/* + * 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 com.syncedsynapse.kore2.jsonrpc.method; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.syncedsynapse.kore2.jsonrpc.ApiException; +import com.syncedsynapse.kore2.jsonrpc.ApiMethod; +import com.syncedsynapse.kore2.jsonrpc.type.AudioType; +import com.syncedsynapse.kore2.jsonrpc.type.LibraryType; + +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 { + 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 { + 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> { + 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); + } + + @Override + public String getMethodName() { + return METHOD_NAME; + } + + @Override + public List 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 result = new ArrayList(items.size()); + + for (JsonNode item : items) { + result.add(new AudioType.DetailsArtist(item)); + } + + return result; + } + } + + /** + * Retrieve all albums from specified artist or genre + */ + public static class GetAlbums extends ApiMethod> { + public final static String METHOD_NAME = "AudioLibrary.GetAlbums"; + + private final static String LIST_NODE = "albums"; + + /** + * Retrieve all albums from specified artist or genre + * + * @param properties Properties to retrieve. See {@link AudioType.FieldsAlbum} for a + * list of accepted values + */ + public GetAlbums(String... properties) { + super(); + addParameterToRequest("properties", properties); + } + + @Override + public String getMethodName() { + return METHOD_NAME; + } + + @Override + public List 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 result = new ArrayList(items.size()); + for (JsonNode item : items) { + result.add(new AudioType.DetailsAlbum(item)); + } + + return result; + } + } + + /** + * Retrieve all genres + */ + public static class GetGenres extends ApiMethod> { + 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 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 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> { + public final static String METHOD_NAME = "AudioLibrary.GetSongs"; + + private final static String LIST_NODE = "songs"; + + /** + * Retrieve all songs from specified album, artist or genre + * + * @param properties Properties to retrieve. See {@link AudioType.FieldsSong} for a + * list of accepted values + */ + public GetSongs(String... properties) { + super(); + addParameterToRequest("properties", properties); + } + + @Override + public String getMethodName() { + return METHOD_NAME; + } + + @Override + public List 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 result = new ArrayList(items.size()); + for (JsonNode item : items) { + result.add(new AudioType.DetailsSong(item)); + } + + return result; + } + } + +} diff --git a/app/src/main/java/com/syncedsynapse/kore2/jsonrpc/method/Files.java b/app/src/main/java/com/syncedsynapse/kore2/jsonrpc/method/Files.java new file mode 100644 index 0000000..1f83fb1 --- /dev/null +++ b/app/src/main/java/com/syncedsynapse/kore2/jsonrpc/method/Files.java @@ -0,0 +1,51 @@ +/* + * 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 com.syncedsynapse.kore2.jsonrpc.method; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.syncedsynapse.kore2.jsonrpc.ApiException; +import com.syncedsynapse.kore2.jsonrpc.ApiMethod; +import com.syncedsynapse.kore2.jsonrpc.type.FilesType; + +/** + * All JSON RPC methods in Files.* + */ +public class Files { + + /** + * Prepare Download + * Provides a way to download a given file (e.g. providing an URL to the real file location) + */ + public static final class PrepareDownload extends ApiMethod { + public final static String METHOD_NAME = "Files.PrepareDownload"; + + /** + * Provides a way to download a given file (e.g. providing an URL to the real file location) + */ + public PrepareDownload(String path) { + super(); + addParameterToRequest("path", path); + } + + @Override + public String getMethodName() { return METHOD_NAME; } + + @Override + public FilesType.PrepareDownloadReturnType resultFromJson(ObjectNode jsonObject) throws ApiException { + return new FilesType.PrepareDownloadReturnType(jsonObject.get(RESULT_NODE)); + } + } +} diff --git a/app/src/main/java/com/syncedsynapse/kore2/jsonrpc/method/GUI.java b/app/src/main/java/com/syncedsynapse/kore2/jsonrpc/method/GUI.java new file mode 100644 index 0000000..fd21c83 --- /dev/null +++ b/app/src/main/java/com/syncedsynapse/kore2/jsonrpc/method/GUI.java @@ -0,0 +1,184 @@ +/* + * 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 com.syncedsynapse.kore2.jsonrpc.method; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.syncedsynapse.kore2.jsonrpc.ApiException; +import com.syncedsynapse.kore2.jsonrpc.ApiMethod; + +/** + * All JSON RPC methods in GUI.* + */ +public class GUI { + + public static final class ActivateWindow extends ApiMethod { + public final static String METHOD_NAME = "GUI.ActivateWindow"; + + /** All windows that we can navigate to */ + public final static String HOME = "home"; + public final static String PROGRAMS = "programs"; + public final static String PICTURES = "pictures"; + public final static String FILEMANAGER = "filemanager"; + public final static String FILES = "files"; + public final static String SETTINGS = "settings"; + public final static String MUSIC = "music"; + public final static String VIDEO = "video"; + public final static String VIDEOS = "videos"; + public final static String TV = "tv"; + public final static String PVR = "pvr"; + public final static String PVRGUIDEINFO = "pvrguideinfo"; + public final static String PVRRECORDINGINFO = "pvrrecordinginfo"; + public final static String PVRTIMERSETTING = "pvrtimersetting"; + public final static String PVRGROUPMANAGER = "pvrgroupmanager"; + public final static String PVRCHANNELMANAGER = "pvrchannelmanager"; + public final static String PVRGUIDESEARCH = "pvrguidesearch"; + public final static String PVRCHANNELSCAN = "pvrchannelscan"; + public final static String PVRUPDATEPROGRESS = "pvrupdateprogress"; + public final static String PVROSDCHANNELS = "pvrosdchannels"; + public final static String PVROSDGUIDE = "pvrosdguide"; + public final static String PVROSDDIRECTOR = "pvrosddirector"; + public final static String PVROSDCUTTER = "pvrosdcutter"; + public final static String PVROSDTELETEXT = "pvrosdteletext"; + public final static String SYSTEMINFO = "systeminfo"; + public final static String TESTPATTERN = "testpattern"; + public final static String SCREENCALIBRATION = "screencalibration"; + public final static String GUICALIBRATION = "guicalibration"; + public final static String PICTURESSETTINGS = "picturessettings"; + public final static String PROGRAMSSETTINGS = "programssettings"; + public final static String WEATHERSETTINGS = "weathersettings"; + public final static String MUSICSETTINGS = "musicsettings"; + public final static String SYSTEMSETTINGS = "systemsettings"; + public final static String VIDEOSSETTINGS = "videossettings"; + public final static String NETWORKSETTINGS = "networksettings"; + public final static String SERVICESETTINGS = "servicesettings"; + public final static String APPEARANCESETTINGS = "appearancesettings"; + public final static String PVRSETTINGS = "pvrsettings"; + public final static String TVSETTINGS = "tvsettings"; + public final static String SCRIPTS = "scripts"; + public final static String VIDEOFILES = "videofiles"; + public final static String VIDEOLIBRARY = "videolibrary"; + public final static String VIDEOPLAYLIST = "videoplaylist"; + public final static String LOGINSCREEN = "loginscreen"; + public final static String PROFILES = "profiles"; + public final static String SKINSETTINGS = "skinsettings"; + public final static String ADDONBROWSER = "addonbrowser"; + public final static String YESNODIALOG = "yesnodialog"; + public final static String PROGRESSDIALOG = "progressdialog"; + public final static String VIRTUALKEYBOARD = "virtualkeyboard"; + public final static String VOLUMEBAR = "volumebar"; + public final static String SUBMENU = "submenu"; + public final static String FAVOURITES = "favourites"; + public final static String CONTEXTMENU = "contextmenu"; + public final static String INFODIALOG = "infodialog"; + public final static String NUMERICINPUT = "numericinput"; + public final static String GAMEPADINPUT = "gamepadinput"; + public final static String SHUTDOWNMENU = "shutdownmenu"; + public final static String MUTEBUG = "mutebug"; + public final static String PLAYERCONTROLS = "playercontrols"; + public final static String SEEKBAR = "seekbar"; + public final static String MUSICOSD = "musicosd"; + public final static String ADDONSETTINGS = "addonsettings"; + public final static String VISUALISATIONSETTINGS = "visualisationsettings"; + public final static String VISUALISATIONPRESETLIST = "visualisationpresetlist"; + public final static String OSDVIDEOSETTINGS = "osdvideosettings"; + public final static String OSDAUDIOSETTINGS = "osdaudiosettings"; + public final static String VIDEOBOOKMARKS = "videobookmarks"; + public final static String FILEBROWSER = "filebrowser"; + public final static String NETWORKSETUP = "networksetup"; + public final static String MEDIASOURCE = "mediasource"; + public final static String PROFILESETTINGS = "profilesettings"; + public final static String LOCKSETTINGS = "locksettings"; + public final static String CONTENTSETTINGS = "contentsettings"; + public final static String SONGINFORMATION = "songinformation"; + public final static String SMARTPLAYLISTEDITOR = "smartplaylisteditor"; + public final static String SMARTPLAYLISTRULE = "smartplaylistrule"; + public final static String BUSYDIALOG = "busydialog"; + public final static String PICTUREINFO = "pictureinfo"; + public final static String ACCESSPOINTS = "accesspoints"; + public final static String FULLSCREENINFO = "fullscreeninfo"; + public final static String KARAOKESELECTOR = "karaokeselector"; + public final static String KARAOKELARGESELECTOR = "karaokelargeselector"; + public final static String SLIDERDIALOG = "sliderdialog"; + public final static String ADDONINFORMATION = "addoninformation"; + public final static String MUSICPLAYLIST = "musicplaylist"; + public final static String MUSICFILES = "musicfiles"; + public final static String MUSICLIBRARY = "musiclibrary"; + public final static String MUSICPLAYLISTEDITOR = "musicplaylisteditor"; + public final static String TELETEXT = "teletext"; + public final static String SELECTDIALOG = "selectdialog"; + public final static String MUSICINFORMATION = "musicinformation"; + public final static String OKDIALOG = "okdialog"; + public final static String MOVIEINFORMATION = "movieinformation"; + public final static String TEXTVIEWER = "textviewer"; + public final static String FULLSCREENVIDEO = "fullscreenvideo"; + public final static String FULLSCREENLIVETV = "fullscreenlivetv"; + public final static String VISUALISATION = "visualisation"; + public final static String SLIDESHOW = "slideshow"; + public final static String FILESTACKINGDIALOG = "filestackingdialog"; + public final static String KARAOKE = "karaoke"; + public final static String WEATHER = "weather"; + public final static String SCREENSAVER = "screensaver"; + public final static String VIDEOOSD = "videoosd"; + public final static String VIDEOMENU = "videomenu"; + public final static String VIDEOTIMESEEK = "videotimeseek"; + public final static String MUSICOVERLAY = "musicoverlay"; + public final static String VIDEOOVERLAY = "videooverlay"; + public final static String STARTWINDOW = "startwindow"; + public final static String STARTUP = "startup"; + public final static String PERIPHERALS = "peripherals"; + public final static String PERIPHERALSETTINGS = "peripheralsettings"; + public final static String EXTENDEDPROGRESSDIALOG = "extendedprogressdialog"; + public final static String MEDIAFILTER = "mediafilter"; + + // Only on Gotham + public final static String SUBTITLESEARCH = "subtitlesearch"; + + /** + * For use in params, to go directly to Movies + */ + public final static String PARAM_MOVIE_TITLES = "MovieTitles"; + /** + * For use in params, to go directly to TV shows + */ + public final static String PARAM_TV_SHOWS_TITLES = "TvShowTitles"; + + /** + * Activates a window in XBMC. See class constants to check which windows are allowed. + */ + public ActivateWindow(String window) { + super(); + addParameterToRequest("window", window); + } + + /** + * Activates a window in XBMC. See class constants to check which windows are allowed. + */ + public ActivateWindow(String window, String... parameters) { + super(); + addParameterToRequest("window", window); + addParameterToRequest("parameters", parameters); + } + + @Override + public String getMethodName() { return METHOD_NAME; } + + @Override + public String resultFromJson(ObjectNode jsonObject) throws ApiException { + return jsonObject.get(RESULT_NODE).textValue(); + } + } + +} diff --git a/app/src/main/java/com/syncedsynapse/kore2/jsonrpc/method/Input.java b/app/src/main/java/com/syncedsynapse/kore2/jsonrpc/method/Input.java new file mode 100644 index 0000000..f441edb --- /dev/null +++ b/app/src/main/java/com/syncedsynapse/kore2/jsonrpc/method/Input.java @@ -0,0 +1,395 @@ +/* + * 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 com.syncedsynapse.kore2.jsonrpc.method; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.syncedsynapse.kore2.jsonrpc.ApiException; +import com.syncedsynapse.kore2.jsonrpc.ApiMethod; + +/** + * All JSON RPC methods in Input.* + */ +public class Input { + + /** + * Execute action + * Executes general actions on XBMC. See class constants for available actions. + */ + public static final class ExecuteAction extends ApiMethod { + public final static String METHOD_NAME = "Input.ExecuteAction"; + + /** Available actions */ + public final static String LEFT = "left"; + public final static String RIGHT = "right"; + public final static String UP = "up"; + public final static String DOWN = "down"; + public final static String PAGEUP = "pageup"; + public final static String PAGEDOWN = "pagedown"; + public final static String SELECT = "select"; + public final static String HIGHLIGHT = "highlight"; + public final static String PARENTDIR = "parentdir"; + public final static String PARENTFOLDER = "parentfolder"; + public final static String BACK = "back"; + public final static String PREVIOUSMENU = "previousmenu"; + public final static String INFO = "info"; + public final static String PAUSE = "pause"; + public final static String STOP = "stop"; + public final static String SKIPNEXT = "skipnext"; + public final static String SKIPPREVIOUS = "skipprevious"; + public final static String FULLSCREEN = "fullscreen"; + public final static String ASPECTRATIO = "aspectratio"; + public final static String STEPFORWARD = "stepforward"; + public final static String STEPBACK = "stepback"; + public final static String BIGSTEPFORWARD = "bigstepforward"; + public final static String BIGSTEPBACK = "bigstepback"; + public final static String OSD = "osd"; + public final static String SHOWSUBTITLES = "showsubtitles"; + public final static String NEXTSUBTITLE = "nextsubtitle"; + public final static String CODECINFO = "codecinfo"; + public final static String NEXTPICTURE = "nextpicture"; + public final static String PREVIOUSPICTURE = "previouspicture"; + public final static String ZOOMOUT = "zoomout"; + public final static String ZOOMIN = "zoomin"; + public final static String PLAYLIST = "playlist"; + public final static String QUEUE = "queue"; + public final static String ZOOMNORMAL = "zoomnormal"; + public final static String ZOOMLEVEL1 = "zoomlevel1"; + public final static String ZOOMLEVEL2 = "zoomlevel2"; + public final static String ZOOMLEVEL3 = "zoomlevel3"; + public final static String ZOOMLEVEL4 = "zoomlevel4"; + public final static String ZOOMLEVEL5 = "zoomlevel5"; + public final static String ZOOMLEVEL6 = "zoomlevel6"; + public final static String ZOOMLEVEL7 = "zoomlevel7"; + public final static String ZOOMLEVEL8 = "zoomlevel8"; + public final static String ZOOMLEVEL9 = "zoomlevel9"; + public final static String NEXTCALIBRATION = "nextcalibration"; + public final static String RESETCALIBRATION = "resetcalibration"; + public final static String ANALOGMOVE = "analogmove"; + public final static String ROTATE = "rotate"; + public final static String ROTATECCW = "rotateccw"; + public final static String CLOSE = "close"; + public final static String SUBTITLEDELAYMINUS = "subtitledelayminus"; + public final static String SUBTITLEDELAY = "subtitledelay"; + public final static String SUBTITLEDELAYPLUS = "subtitledelayplus"; + public final static String AUDIODELAYMINUS = "audiodelayminus"; + public final static String AUDIODELAY = "audiodelay"; + public final static String AUDIODELAYPLUS = "audiodelayplus"; + public final static String SUBTITLESHIFTUP = "subtitleshiftup"; + public final static String SUBTITLESHIFTDOWN = "subtitleshiftdown"; + public final static String SUBTITLEALIGN = "subtitlealign"; + public final static String AUDIONEXTLANGUAGE = "audionextlanguage"; + public final static String VERTICALSHIFTUP = "verticalshiftup"; + public final static String VERTICALSHIFTDOWN = "verticalshiftdown"; + public final static String NEXTRESOLUTION = "nextresolution"; + public final static String AUDIOTOGGLEDIGITAL = "audiotoggledigital"; + public final static String NUMBER0 = "number0"; + public final static String NUMBER1 = "number1"; + public final static String NUMBER2 = "number2"; + public final static String NUMBER3 = "number3"; + public final static String NUMBER4 = "number4"; + public final static String NUMBER5 = "number5"; + public final static String NUMBER6 = "number6"; + public final static String NUMBER7 = "number7"; + public final static String NUMBER8 = "number8"; + public final static String NUMBER9 = "number9"; + public final static String OSDLEFT = "osdleft"; + public final static String OSDRIGHT = "osdright"; + public final static String OSDUP = "osdup"; + public final static String OSDDOWN = "osddown"; + public final static String OSDSELECT = "osdselect"; + public final static String OSDVALUEPLUS = "osdvalueplus"; + public final static String OSDVALUEMINUS = "osdvalueminus"; + public final static String SMALLSTEPBACK = "smallstepback"; + public final static String FASTFORWARD = "fastforward"; + public final static String REWIND = "rewind"; + public final static String PLAY = "play"; + public final static String PLAYPAUSE = "playpause"; + public final static String DELETE = "delete"; + public final static String COPY = "copy"; + public final static String MOVE = "move"; + public final static String MPLAYEROSD = "mplayerosd"; + public final static String HIDESUBMENU = "hidesubmenu"; + public final static String SCREENSHOT = "screenshot"; + public final static String RENAME = "rename"; + public final static String TOGGLEWATCHED = "togglewatched"; + public final static String SCANITEM = "scanitem"; + public final static String RELOADKEYMAPS = "reloadkeymaps"; + public final static String VOLUMEUP = "volumeup"; + public final static String VOLUMEDOWN = "volumedown"; + public final static String MUTE = "mute"; + public final static String BACKSPACE = "backspace"; + public final static String SCROLLUP = "scrollup"; + public final static String SCROLLDOWN = "scrolldown"; + public final static String ANALOGFASTFORWARD = "analogfastforward"; + public final static String ANALOGREWIND = "analogrewind"; + public final static String MOVEITEMUP = "moveitemup"; + public final static String MOVEITEMDOWN = "moveitemdown"; + public final static String CONTEXTMENU = "contextmenu"; + public final static String SHIFT = "shift"; + public final static String SYMBOLS = "symbols"; + public final static String CURSORLEFT = "cursorleft"; + public final static String CURSORRIGHT = "cursorright"; + public final static String SHOWTIME = "showtime"; + public final static String ANALOGSEEKFORWARD = "analogseekforward"; + public final static String ANALOGSEEKBACK = "analogseekback"; + public final static String SHOWPRESET = "showpreset"; + public final static String PRESETLIST = "presetlist"; + public final static String NEXTPRESET = "nextpreset"; + public final static String PREVIOUSPRESET = "previouspreset"; + public final static String LOCKPRESET = "lockpreset"; + public final static String RANDOMPRESET = "randompreset"; + public final static String INCREASEVISRATING = "increasevisrating"; + public final static String DECREASEVISRATING = "decreasevisrating"; + public final static String SHOWVIDEOMENU = "showvideomenu"; + public final static String ENTER = "enter"; + public final static String INCREASERATING = "increaserating"; + public final static String DECREASERATING = "decreaserating"; + public final static String TOGGLEFULLSCREEN = "togglefullscreen"; + public final static String NEXTSCENE = "nextscene"; + public final static String PREVIOUSSCENE = "previousscene"; + public final static String NEXTLETTER = "nextletter"; + public final static String PREVLETTER = "prevletter"; + public final static String JUMPSMS2 = "jumpsms2"; + public final static String JUMPSMS3 = "jumpsms3"; + public final static String JUMPSMS4 = "jumpsms4"; + public final static String JUMPSMS5 = "jumpsms5"; + public final static String JUMPSMS6 = "jumpsms6"; + public final static String JUMPSMS7 = "jumpsms7"; + public final static String JUMPSMS8 = "jumpsms8"; + public final static String JUMPSMS9 = "jumpsms9"; + public final static String FILTER = "filter"; + public final static String FILTERCLEAR = "filterclear"; + public final static String FILTERSMS2 = "filtersms2"; + public final static String FILTERSMS3 = "filtersms3"; + public final static String FILTERSMS4 = "filtersms4"; + public final static String FILTERSMS5 = "filtersms5"; + public final static String FILTERSMS6 = "filtersms6"; + public final static String FILTERSMS7 = "filtersms7"; + public final static String FILTERSMS8 = "filtersms8"; + public final static String FILTERSMS9 = "filtersms9"; + public final static String FIRSTPAGE = "firstpage"; + public final static String LASTPAGE = "lastpage"; + public final static String GUIPROFILE = "guiprofile"; + public final static String RED = "red"; + public final static String GREEN = "green"; + public final static String YELLOW = "yellow"; + public final static String BLUE = "blue"; + public final static String INCREASEPAR = "increasepar"; + public final static String DECREASEPAR = "decreasepar"; + public final static String VOLAMPUP = "volampup"; + public final static String VOLAMPDOWN = "volampdown"; + public final static String CHANNELUP = "channelup"; + public final static String CHANNELDOWN = "channeldown"; + public final static String PREVIOUSCHANNELGROUP = "previouschannelgroup"; + public final static String NEXTCHANNELGROUP = "nextchannelgroup"; + public final static String LEFTCLICK = "leftclick"; + public final static String RIGHTCLICK = "rightclick"; + public final static String MIDDLECLICK = "middleclick"; + public final static String DOUBLECLICK = "doubleclick"; + public final static String WHEELUP = "wheelup"; + public final static String WHEELDOWN = "wheeldown"; + public final static String MOUSEDRAG = "mousedrag"; + public final static String MOUSEMOVE = "mousemove"; + public final static String NOOP = "noop"; + + /** + * Executes general actions on XBMC. See class constants for available actions. + */ + public ExecuteAction(String action) { + super(); + addParameterToRequest("action", action); + } + + @Override + public String getMethodName() { return METHOD_NAME; } + + @Override + public String resultFromJson(ObjectNode jsonObject) throws ApiException { + return jsonObject.get(RESULT_NODE).textValue(); + } + } + + /** + * Goes to home window in GUI + */ + public static final class Home extends ApiMethod { + public final static String METHOD_NAME = "Input.Home"; + /** + * Goes to home window in GUI + */ + public Home() { + super(); + } + + @Override + public String getMethodName() { return METHOD_NAME; } + + @Override + public String resultFromJson(ObjectNode jsonObject) throws ApiException { + return jsonObject.get(RESULT_NODE).textValue(); + } + } + + /** + * Navigate left in GUI + */ + public static final class Left extends ApiMethod { + public final static String METHOD_NAME = "Input.Left"; + /** + * Navigate left in GUI + */ + public Left() { + super(); + } + + @Override + public String getMethodName() { return METHOD_NAME; } + + @Override + public String resultFromJson(ObjectNode jsonObject) throws ApiException { + return jsonObject.get(RESULT_NODE).textValue(); + } + } + + /** + * Navigate right in GUI + */ + public static final class Right extends ApiMethod { + public final static String METHOD_NAME = "Input.Right"; + /** + * Navigate right in GUI + */ + public Right() { + super(); + } + + @Override + public String getMethodName() { return METHOD_NAME; } + + @Override + public String resultFromJson(ObjectNode jsonObject) throws ApiException { + return jsonObject.get(RESULT_NODE).textValue(); + } + } + + /** + * Navigate up in GUI + */ + public static final class Up extends ApiMethod { + public final static String METHOD_NAME = "Input.Up"; + /** + * Navigate up in GUI + */ + public Up() { + super(); + } + + @Override + public String getMethodName() { return METHOD_NAME; } + + @Override + public String resultFromJson(ObjectNode jsonObject) throws ApiException { + return jsonObject.get(RESULT_NODE).textValue(); + } + } + + /** + * Navigate down in GUI + */ + public static final class Down extends ApiMethod { + public final static String METHOD_NAME = "Input.Down"; + /** + * Navigate down in GUI + */ + public Down() { + super(); + } + + @Override + public String getMethodName() { return METHOD_NAME; } + + @Override + public String resultFromJson(ObjectNode jsonObject) throws ApiException { + return jsonObject.get(RESULT_NODE).textValue(); + } + } + + /** + * Navigate back in GUI + */ + public static final class Back extends ApiMethod { + public final static String METHOD_NAME = "Input.Back"; + /** + * Navigate down in GUI + */ + public Back() { + super(); + } + + @Override + public String getMethodName() { return METHOD_NAME; } + + @Override + public String resultFromJson(ObjectNode jsonObject) throws ApiException { + return jsonObject.get(RESULT_NODE).textValue(); + } + } + + /** + * Select in GUI + */ + public static final class Select extends ApiMethod { + public final static String METHOD_NAME = "Input.Select"; + /** + * Select in GUI + */ + public Select() { + super(); + } + + @Override + public String getMethodName() { return METHOD_NAME; } + + @Override + public String resultFromJson(ObjectNode jsonObject) throws ApiException { + return jsonObject.get(RESULT_NODE).textValue(); + } + } + + /** + * Send a generic (unicode) text + */ + public static final class SendText extends ApiMethod { + public final static String METHOD_NAME = "Input.SendText"; + /** + * Send a generic (unicode) text + */ + public SendText(String text, boolean done) { + super(); + addParameterToRequest("text", text); + addParameterToRequest("done", done); + } + + @Override + public String getMethodName() { return METHOD_NAME; } + + @Override + public String resultFromJson(ObjectNode jsonObject) throws ApiException { + return jsonObject.get(RESULT_NODE).textValue(); + } + } + +} diff --git a/app/src/main/java/com/syncedsynapse/kore2/jsonrpc/method/JSONRPC.java b/app/src/main/java/com/syncedsynapse/kore2/jsonrpc/method/JSONRPC.java new file mode 100644 index 0000000..d059c19 --- /dev/null +++ b/app/src/main/java/com/syncedsynapse/kore2/jsonrpc/method/JSONRPC.java @@ -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 com.syncedsynapse.kore2.jsonrpc.method; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.syncedsynapse.kore2.jsonrpc.ApiException; +import com.syncedsynapse.kore2.jsonrpc.ApiMethod; + +public class JSONRPC { + + /** + * Ping responder + */ + public static final class Ping extends ApiMethod { + public final static String METHOD_NAME = "JSONRPC.Ping"; + + /** + * Ping responder + */ + public Ping() { + super(); + } + + @Override + public String getMethodName() { return METHOD_NAME; } + + @Override + public String resultFromJson(ObjectNode jsonObject) throws ApiException { + return jsonObject.get(RESULT_NODE).textValue(); + } + } +} diff --git a/app/src/main/java/com/syncedsynapse/kore2/jsonrpc/method/Player.java b/app/src/main/java/com/syncedsynapse/kore2/jsonrpc/method/Player.java new file mode 100644 index 0000000..2511765 --- /dev/null +++ b/app/src/main/java/com/syncedsynapse/kore2/jsonrpc/method/Player.java @@ -0,0 +1,469 @@ +/* + * 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 com.syncedsynapse.kore2.jsonrpc.method; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.syncedsynapse.kore2.jsonrpc.ApiException; +import com.syncedsynapse.kore2.jsonrpc.ApiMethod; +import com.syncedsynapse.kore2.jsonrpc.type.ListType; +import com.syncedsynapse.kore2.jsonrpc.type.PlayerType; +import com.syncedsynapse.kore2.jsonrpc.type.PlayerType.GetActivePlayersReturnType; +import com.syncedsynapse.kore2.jsonrpc.type.PlaylistType; +import com.syncedsynapse.kore2.utils.JsonUtils; + +import java.util.ArrayList; + +/** + * All JSON RPC methods in Playyer.* + */ +public class Player { + + /** + * Returns all active players. + */ + public static final class GetActivePlayers extends ApiMethod> { + public final static String METHOD_NAME = "Player.GetActivePlayers"; + + /** + * Returns all active players. + */ + public GetActivePlayers() { + super(); + } + + @Override + public String getMethodName() { return METHOD_NAME; } + + @Override + public ArrayList resultFromJson(ObjectNode jsonObject) throws ApiException { + ArrayNode resultNode = (ArrayNode)jsonObject.get(RESULT_NODE); + ArrayList res = new ArrayList(); + if (resultNode != null) { + for (JsonNode node : resultNode) { + res.add(new GetActivePlayersReturnType(node)); + } + } + return res; + } + } + + /** + * Retrieves the currently played item + */ + public static final class GetItem extends ApiMethod { + public final static String METHOD_NAME = "Player.GetItem"; + + /** + * Retrieves the currently played item + * @param playerId Player id for which to retrieve the item + * @param properties Properties to retrieve. + * See {@link ListType.FieldsAll} for a list of accepted values + */ + public GetItem(int playerId, String... properties) { + super(); + addParameterToRequest("playerid", playerId); + addParameterToRequest("properties", properties); + } + + @Override + public String getMethodName() { return METHOD_NAME; } + + @Override + public ListType.ItemsAll resultFromJson(ObjectNode jsonObject) throws ApiException { + return new ListType.ItemsAll(jsonObject.get(RESULT_NODE).get("item")); + } + } + + /** + * Retrieves the values of the given properties + */ + public static final class GetProperties extends ApiMethod { + public final static String METHOD_NAME = "Player.GetProperties"; + + /** + * Retrieves the values of the given properties + * @param playerId Player id for which to retrieve the item + * @param properties Properties to retrieve. + * See {@link PlayerType.PropertyName} constants for a list of accepted values + */ + public GetProperties(int playerId, String... properties) { + super(); + addParameterToRequest("playerid", playerId); + addParameterToRequest("properties", properties); + } + + @Override + public String getMethodName() { return METHOD_NAME; } + + @Override + public PlayerType.PropertyValue resultFromJson(ObjectNode jsonObject) throws ApiException { + return new PlayerType.PropertyValue(jsonObject.get(RESULT_NODE)); + } + } + + + /** + * Pauses or unpause playback and returns the new state + */ + public static final class PlayPause extends ApiMethod { + public final static String METHOD_NAME = "Player.PlayPause"; + + /** + * Pauses or unpause playback and returns the new state + * @param playerId Player id for which to toggle the state + */ + public PlayPause(int playerId) { + super(); + addParameterToRequest("playerid", playerId); + addParameterToRequest("play", "toggle"); + } + + @Override + public String getMethodName() { return METHOD_NAME; } + + @Override + public Integer resultFromJson(ObjectNode jsonObject) throws ApiException { + return JsonUtils.intFromJsonNode(jsonObject.get(RESULT_NODE), "speed"); + } + } + + /** + * Set the speed of the current playback + */ + public static final class SetSpeed extends ApiMethod { + public final static String METHOD_NAME = "Player.SetSpeed"; + + /** + * Set the speed of the current playback + * @param playerId Player id for which to toggle the state + * @param speed String enum in {@link com.syncedsynapse.kore2.jsonrpc.type.GlobalType.IncrementDecrement} + */ + public SetSpeed(int playerId, String speed) { + super(); + addParameterToRequest("playerid", playerId); + addParameterToRequest("speed", speed); + } + + @Override + public String getMethodName() { return METHOD_NAME; } + + @Override + public Integer resultFromJson(ObjectNode jsonObject) throws ApiException { + return JsonUtils.intFromJsonNode(jsonObject.get(RESULT_NODE), "speed"); + } + } + + /** + * Stops playback + */ + public static final class Stop extends ApiMethod { + public final static String METHOD_NAME = "Player.Stop"; + + /** + * Stops playback + * @param playerId Player id for which to stop playback + */ + public Stop(int playerId) { + super(); + addParameterToRequest("playerid", playerId); + } + + @Override + public String getMethodName() { return METHOD_NAME; } + + @Override + public String resultFromJson(ObjectNode jsonObject) throws ApiException { + return jsonObject.get(RESULT_NODE).textValue(); + } + } + + /** + * Go to previous/next/specific item in the playlist. + */ + public static final class GoTo extends ApiMethod { + public final static String METHOD_NAME = "Player.GoTo"; + + /** + * Go to constants + */ + public static final String PREVIOUS = "previous"; + public static final String NEXT = "next"; + + /** + * Go to previous/next/specific item in the playlist. + * @param playerId Player id for which to stop playback + * @param to Where to go. See this class constants for values + */ + public GoTo(int playerId, String to) { + super(); + addParameterToRequest("playerid", playerId); + addParameterToRequest("to", to); + } + + /** + * Go to previous/next/specific item in the playlist. + * @param playerId Player id for which to stop playback + * @param to position in playlist + */ + public GoTo(int playerId, int to) { + super(); + addParameterToRequest("playerid", playerId); + addParameterToRequest("to", to); + } + + @Override + public String getMethodName() { return METHOD_NAME; } + + @Override + public String resultFromJson(ObjectNode jsonObject) throws ApiException { + return jsonObject.get(RESULT_NODE).textValue(); + } + } + + /** + * Seek through the playing item + */ + public static final class Seek extends ApiMethod { + public final static String METHOD_NAME = "Player.Seek"; + + /** + * Seek through the playing item (by time) + * @param playerId Player id for which to stop playback + * @param value Where to seek + */ + public Seek(int playerId, PlayerType.PositionTime value) { + super(); + addParameterToRequest("playerid", playerId); + addParameterToRequest("value", value); + } + + /** + * Seek through the playing item (by percentage) + * @param playerId Player id for which to stop playback + * @param value Percentage + */ + public Seek(int playerId, int value) { + super(); + addParameterToRequest("playerid", playerId); + addParameterToRequest("value", value); + } + + @Override + public String getMethodName() { return METHOD_NAME; } + + @Override + public PlayerType.SeekReturnType resultFromJson(ObjectNode jsonObject) throws ApiException { + return new PlayerType.SeekReturnType(jsonObject.get(RESULT_NODE)); + } + } + + /** + * Set the subtitle displayed by the player + */ + public static final class SetSubtitle extends ApiMethod { + public final static String METHOD_NAME = "Player.SetSubtitle"; + + /** + * SetSubtitle constants + */ + public static final String PREVIOUS = "previous"; + public static final String NEXT = "next"; + public static final String OFF = "off"; + public static final String ON = "on"; + + /** + * Set the subtitle displayed by the player + * @param playerId Player id for which to stop playback + * @param subtitle One of the constanstants of this class + * @param enable Whether to enable subtitles to be displayed after setting the new subtitle + */ + public SetSubtitle(int playerId, String subtitle, boolean enable) { + super(); + addParameterToRequest("playerid", playerId); + addParameterToRequest("subtitle", subtitle); + addParameterToRequest("enable", enable); + } + + /** + * Set the subtitle displayed by the player + * @param playerId Player id for which to stop playback + * @param subtitle Index of the subtitle to display + * @param enable Whether to enable subtitles to be displayed after setting the new subtitle + */ + public SetSubtitle(int playerId, int subtitle, boolean enable) { + super(); + addParameterToRequest("playerid", playerId); + addParameterToRequest("subtitle", subtitle); + addParameterToRequest("enable", enable); + } + + @Override + public String getMethodName() { return METHOD_NAME; } + + @Override + public String resultFromJson(ObjectNode jsonObject) throws ApiException { + return jsonObject.get(RESULT_NODE).textValue(); + } + } + + /** + * Set the audio stream played by the player + */ + public static final class SetAudioStream extends ApiMethod { + public final static String METHOD_NAME = "Player.SetAudioStream"; + + /** + * SetAudioStream constants + */ + public static final String PREVIOUS = "previous"; + public static final String NEXT = "next"; + + /** + * Set the audio stream played by the player + * @param playerId Player id for which to stop playback + * @param stream One of the constanstants of this class + */ + public SetAudioStream(int playerId, String stream) { + super(); + addParameterToRequest("playerid", playerId); + addParameterToRequest("stream", stream); + } + + /** + * Set the audio stream played by the player + * @param playerId Player id for which to stop playback + * @param stream Index of the audio stream to play + */ + public SetAudioStream(int playerId, int stream) { + super(); + addParameterToRequest("playerid", playerId); + addParameterToRequest("stream", stream); + } + + @Override + public String getMethodName() { return METHOD_NAME; } + + @Override + public String resultFromJson(ObjectNode jsonObject) throws ApiException { + return jsonObject.get(RESULT_NODE).textValue(); + } + } + + /** + * Set the repeat mode of the player + */ + public static final class SetRepeat extends ApiMethod { + public final static String METHOD_NAME = "Player.SetRepeat"; + + /** + * Set the repeat mode of the player + * @param playerId Player id for which to stop playback + * @param repeat Repeat mode, see {@link com.syncedsynapse.kore2.jsonrpc.type.PlayerType.Repeat} + */ + public SetRepeat(int playerId, String repeat) { + super(); + addParameterToRequest("playerid", playerId); + addParameterToRequest("repeat", repeat); + } + + @Override + public String getMethodName() { return METHOD_NAME; } + + @Override + public String resultFromJson(ObjectNode jsonObject) throws ApiException { + return jsonObject.get(RESULT_NODE).textValue(); + } + } + + /** + * Shuffle/Unshuffle items in the player + */ + public static final class SetShuffle extends ApiMethod { + public final static String METHOD_NAME = "Player.SetShuffle"; + + /** + * Shuffle/Unshuffle items in the player + * @param playerId Player id for which to shuffle + * @param shuffle True/false + */ + public SetShuffle(int playerId, boolean shuffle) { + super(); + addParameterToRequest("playerid", playerId); + addParameterToRequest("shuffle", shuffle); + } + + /** + * Shuffle/Unshuffle items in the player + * @param playerId Player id for which to shuffle + */ + public SetShuffle(int playerId) { + super(); + addParameterToRequest("playerid", playerId); + addParameterToRequest("shuffle", "toggle"); + } + + @Override + public String getMethodName() { return METHOD_NAME; } + + @Override + public String resultFromJson(ObjectNode jsonObject) throws ApiException { + return jsonObject.get(RESULT_NODE).textValue(); + } + } + + /** + * Start playback of either the playlist with the given ID, a slideshow with the pictures + * from the given directory or a single file or an item from the database. + */ + public static final class Open extends ApiMethod { + public final static String METHOD_NAME = "Player.Open"; + + /** + * Start playback of either the playlist with the given ID, a slideshow with the pictures + * from the given directory or a single file or an item from the database. + * @param playlistId + * @param position + */ + public Open(int playlistId, int position) { + super(); + final ObjectNode item = objectMapper.createObjectNode(); + item.put("playlistid", playlistId); + item.put("position", position); + addParameterToRequest("item", item); + } + + /** + * Start playback of either the playlist with the given ID, a slideshow with the pictures + * from the given directory or a single file or an item from the database. + * @param playlistItem Item to play + */ + public Open(PlaylistType.Item playlistItem) { + super(); + addParameterToRequest("item", playlistItem.toJsonNode()); + } + + @Override + public String getMethodName() { return METHOD_NAME; } + + @Override + public String resultFromJson(ObjectNode jsonObject) throws ApiException { + return jsonObject.get(RESULT_NODE).textValue(); + } + } + +} diff --git a/app/src/main/java/com/syncedsynapse/kore2/jsonrpc/method/Playlist.java b/app/src/main/java/com/syncedsynapse/kore2/jsonrpc/method/Playlist.java new file mode 100644 index 0000000..c4542f0 --- /dev/null +++ b/app/src/main/java/com/syncedsynapse/kore2/jsonrpc/method/Playlist.java @@ -0,0 +1,173 @@ +/* + * 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 com.syncedsynapse.kore2.jsonrpc.method; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.syncedsynapse.kore2.jsonrpc.ApiException; +import com.syncedsynapse.kore2.jsonrpc.ApiMethod; +import com.syncedsynapse.kore2.jsonrpc.type.ListType; +import com.syncedsynapse.kore2.jsonrpc.type.PlaylistType; +import com.syncedsynapse.kore2.jsonrpc.type.PlaylistType.GetPlaylistsReturnType; + +import java.util.ArrayList; +import java.util.List; + +/** + * JSON RPC methods in Playlist.* + */ +public class Playlist { + + /** + * Returns all existing playlists + */ + public static final class GetPlaylists extends ApiMethod> { + public final static String METHOD_NAME = "Playlist.GetPlaylists"; + + /** + * Returns all existing playlists + */ + public GetPlaylists() { + super(); + } + + @Override + public String getMethodName() { return METHOD_NAME; } + + @Override + public ArrayList resultFromJson(ObjectNode jsonObject) throws ApiException { + ArrayNode resultNode = (ArrayNode)jsonObject.get(RESULT_NODE); + ArrayList res = new ArrayList(); + if (resultNode != null) { + for (JsonNode node : resultNode) { + res.add(new GetPlaylistsReturnType(node)); + } + } + return res; + } + } + + /** + * Get all items from playlist + */ + public static final class GetItems extends ApiMethod> { + public final static String METHOD_NAME = "Playlist.GetItems"; + + /** + * Get all items from playlist + * @param playlistId Playlist id for which to get the items + * @param properties Properties to retrieve. + * See {@link ListType.FieldsAll} for a list of accepted values + */ + public GetItems(int playlistId, String... properties) { + super(); + addParameterToRequest("playlistid", playlistId); + addParameterToRequest("properties", properties); + } + + @Override + public String getMethodName() { return METHOD_NAME; } + + @Override + public List resultFromJson(ObjectNode jsonObject) throws ApiException { + JsonNode resultNode = jsonObject.get(RESULT_NODE); + if (!resultNode.has("items") || (!resultNode.get("items").isArray()) || + ((resultNode.get("items")).size() == 0)) { + return new ArrayList(0); + } + ArrayNode items = (ArrayNode)resultNode.get("items"); + ArrayList result = new ArrayList(items.size()); + + for (JsonNode item : items) { + result.add(new ListType.ItemsAll(item)); + } + + return result; + } + } + + /** + * Clear playlist + */ + public static final class Clear extends ApiMethod { + public final static String METHOD_NAME = "Playlist.Clear"; + + /** + * Clear playlist + */ + public Clear(int playlistId) { + super(); + addParameterToRequest("playlistid", playlistId); + } + + @Override + public String getMethodName() { return METHOD_NAME; } + + @Override + public String resultFromJson(ObjectNode jsonObject) throws ApiException { + return jsonObject.get(RESULT_NODE).textValue(); + } + } + + /** + * Remove item from playlist. Does not work for picture playlists (aka slideshows). + */ + public static final class Remove extends ApiMethod { + public final static String METHOD_NAME = "Playlist.Remove"; + + /** + * Remove item from playlist. Does not work for picture playlists (aka slideshows). + */ + public Remove(int playlistId, int position) { + super(); + addParameterToRequest("playlistid", playlistId); + addParameterToRequest("position", position); + } + + @Override + public String getMethodName() { return METHOD_NAME; } + + @Override + public String resultFromJson(ObjectNode jsonObject) throws ApiException { + return jsonObject.get(RESULT_NODE).textValue(); + } + } + + /** + * Add item(s) to playlist + */ + public static final class Add extends ApiMethod { + public final static String METHOD_NAME = "Playlist.Add"; + + /** + * Add item(s) to playlist + */ + public Add(int playlistId, PlaylistType.Item item) { + super(); + addParameterToRequest("playlistid", playlistId); + addParameterToRequest("item", item); + } + + @Override + public String getMethodName() { return METHOD_NAME; } + + @Override + public String resultFromJson(ObjectNode jsonObject) throws ApiException { + return jsonObject.get(RESULT_NODE).textValue(); + } + } +} diff --git a/app/src/main/java/com/syncedsynapse/kore2/jsonrpc/method/System.java b/app/src/main/java/com/syncedsynapse/kore2/jsonrpc/method/System.java new file mode 100644 index 0000000..ce162ab --- /dev/null +++ b/app/src/main/java/com/syncedsynapse/kore2/jsonrpc/method/System.java @@ -0,0 +1,75 @@ +/* + * 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 com.syncedsynapse.kore2.jsonrpc.method; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.syncedsynapse.kore2.jsonrpc.ApiException; +import com.syncedsynapse.kore2.jsonrpc.ApiMethod; + +/** + * All JSON RPC methods in System.* + */ +public class System { + + /** + * Shuts the system running XBMC down + */ + public static final class Shutdown extends ApiMethod { + public final static String METHOD_NAME = "System.Shutdown"; + + /** + * Shuts the system running XBMC down + */ + public Shutdown() { + super(); + } + + @Override + public String getMethodName() { + return METHOD_NAME; + } + + @Override + public String resultFromJson(ObjectNode jsonObject) throws ApiException { + return jsonObject.get(RESULT_NODE).textValue(); + } + } + + /** + * Suspends the system running XBMC + */ + public static final class Suspend extends ApiMethod { + public final static String METHOD_NAME = "System.Suspend"; + + /** + * Suspends the system running XBMC + */ + public Suspend() { + super(); + } + + @Override + public String getMethodName() { + return METHOD_NAME; + } + + @Override + public String resultFromJson(ObjectNode jsonObject) throws ApiException { + return jsonObject.get(RESULT_NODE).textValue(); + } + } + +} diff --git a/app/src/main/java/com/syncedsynapse/kore2/jsonrpc/method/VideoLibrary.java b/app/src/main/java/com/syncedsynapse/kore2/jsonrpc/method/VideoLibrary.java new file mode 100644 index 0000000..4b975d1 --- /dev/null +++ b/app/src/main/java/com/syncedsynapse/kore2/jsonrpc/method/VideoLibrary.java @@ -0,0 +1,439 @@ +/* + * 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 com.syncedsynapse.kore2.jsonrpc.method; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.syncedsynapse.kore2.jsonrpc.ApiException; +import com.syncedsynapse.kore2.jsonrpc.ApiMethod; +import com.syncedsynapse.kore2.jsonrpc.type.VideoType; + +import java.util.ArrayList; +import java.util.List; + +/** + * JSON RPC methods in VideoLibrary.* + */ +public class VideoLibrary { + + /** + * Cleans the video library from non-existent items. + */ + public static class Clean extends ApiMethod { + public final static String METHOD_NAME = "VideoLibrary.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 video sources for new library items. + */ + public static class Scan extends ApiMethod { + public final static String METHOD_NAME = "VideoLibrary.Scan"; + + /** + * Scans the video 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 movies + */ + public static class GetMovies extends ApiMethod> { + public final static String METHOD_NAME = "VideoLibrary.GetMovies"; + + private final static String LIST_NODE = "movies"; + + /** + * Retrieve all movies + * + * @param properties Properties to retrieve. See {@link VideoType.FieldsMovie} for a list of + * accepted values + */ + public GetMovies(String... properties) { + super(); + addParameterToRequest("properties", properties); + } + + @Override + public String getMethodName() { + return METHOD_NAME; + } + + @Override + public List 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 result = new ArrayList(items.size()); + + for (JsonNode item : items) { + result.add(new VideoType.DetailsMovie(item)); + } + + return result; + } + } + + /** + * Retrieve details about a specific movie + */ + public static class GetMovieDetails extends ApiMethod { + public final static String METHOD_NAME = "VideoLibrary.GetMovieDetails"; + + /** + * Retrieve details about a specific movie + * + * @param movieId Movie id + * @param properties Properties to retrieve. See {@link VideoType.FieldsMovie} for a list of + * accepted values + */ + public GetMovieDetails(int movieId, String... properties) { + super(); + addParameterToRequest("movieid", movieId); + addParameterToRequest("properties", properties); + } + + @Override + public String getMethodName() { + return METHOD_NAME; + } + + @Override + public VideoType.DetailsMovie resultFromJson(ObjectNode jsonObject) + throws ApiException { + return new VideoType.DetailsMovie(jsonObject.get(RESULT_NODE).get("moviedetails")); + } + } + + /** + * Update the given movie with the given details + * Just the parameters we can change in the gui + */ + public static class SetMovieDetails extends ApiMethod { + public final static String METHOD_NAME = "VideoLibrary.SetMovieDetails"; + + /** + * Update the given movie with the given details + * + * @param movieid Movie id + */ + public SetMovieDetails(int movieid, Integer playcount, Double rating) { + super(); + addParameterToRequest("movieid", movieid); + if (playcount != null) addParameterToRequest("playcount", playcount); + if (rating != null) addParameterToRequest("rating", rating); + } + + @Override + public String getMethodName() { + return METHOD_NAME; + } + + @Override + public String resultFromJson(ObjectNode jsonObject) throws ApiException { + return jsonObject.get(RESULT_NODE).textValue(); + } + } + + /** + * Retrieve all TV Shows + */ + public static class GetTVShows extends ApiMethod> { + public final static String METHOD_NAME = "VideoLibrary.GetTVShows"; + + private final static String LIST_NODE = "tvshows"; + + /** + * Retrieve all tv shows + * + * @param properties Properties to retrieve. See {@link VideoType.FieldsTVShow} for a + * list of accepted values + */ + public GetTVShows(String... properties) { + super(); + addParameterToRequest("properties", properties); + } + + @Override + public String getMethodName() { + return METHOD_NAME; + } + + @Override + public List 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 result = new ArrayList(items.size()); + + for (JsonNode item : items) { + result.add(new VideoType.DetailsTVShow(item)); + } + + return result; + } + } + + /** + * Retrieve details about a specific tv show + */ + public static class GetTVShowDetails extends ApiMethod { + public final static String METHOD_NAME = "VideoLibrary.GetTVShowDetails"; + + /** + * Retrieve details about a specific tv show + * + * @param tvshowId Show id + * @param properties Properties to retrieve. See {@link VideoType.FieldsTVShow} for a + * list of accepted values + */ + public GetTVShowDetails(int tvshowId, String... properties) { + super(); + addParameterToRequest("tvshowid", tvshowId); + addParameterToRequest("properties", properties); + } + + @Override + public String getMethodName() { + return METHOD_NAME; + } + + @Override + public VideoType.DetailsTVShow resultFromJson(ObjectNode jsonObject) + throws ApiException { + return new VideoType.DetailsTVShow(jsonObject.get(RESULT_NODE).get("tvshowdetails")); + } + } + + /** + * Update the given episode with the given details + * Just the parameters we can change in the gui + */ + public static class SetEpisodeDetails extends ApiMethod { + public final static String METHOD_NAME = "VideoLibrary.SetEpisodeDetails"; + + /** + * Update the given episode with the given details + * + * @param episodeid Episode id + */ + public SetEpisodeDetails(int episodeid, Integer playcount, Double rating) { + super(); + addParameterToRequest("episodeid", episodeid); + if (playcount != null) addParameterToRequest("playcount", playcount); + if (rating != null) addParameterToRequest("rating", rating); + } + + @Override + public String getMethodName() { + return METHOD_NAME; + } + + @Override + public String resultFromJson(ObjectNode jsonObject) throws ApiException { + return jsonObject.get(RESULT_NODE).textValue(); + } + } + + /** + * Retrieve all tv seasons + */ + public static class GetSeasons extends ApiMethod> { + public final static String METHOD_NAME = "VideoLibrary.GetSeasons"; + + private final static String LIST_NODE = "seasons"; + + /** + * Retrieve all tv seasons + * + * @param tvshowid TV Show id + * @param properties Properties to retrieve. See {@link VideoType.FieldsSeason} for a + * list of accepted values + */ + public GetSeasons(int tvshowid, String... properties) { + super(); + addParameterToRequest("tvshowid", tvshowid); + addParameterToRequest("properties", properties); + } + + @Override + public String getMethodName() { + return METHOD_NAME; + } + + @Override + public List 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 result = new ArrayList(items + .size()); + + for (JsonNode item : items) { + result.add(new VideoType.DetailsSeason(item)); + } + + return result; + } + } + + /** + * Retrieve all tv show episodes + */ + public static class GetEpisodes extends ApiMethod> { + public final static String METHOD_NAME = "VideoLibrary.GetEpisodes"; + + private final static String LIST_NODE = "episodes"; + + /** + * Retrieve all tv show episodes + * + * @param tvshowid TV Show id + * @param properties Properties to retrieve. See {@link VideoType.FieldsEpisode} for a + * list of accepted values + */ + public GetEpisodes(int tvshowid, String... properties) { + super(); + addParameterToRequest("tvshowid", tvshowid); + addParameterToRequest("properties", properties); + } + + /** + * Retrieve all tv show episodes + * + * @param tvshowid TV Show id + * @param season Season + * @param properties Properties to retrieve. See {@link VideoType.FieldsEpisode} for a + * list of accepted values + */ + public GetEpisodes(int tvshowid, int season, String... properties) { + super(); + addParameterToRequest("tvshowid", tvshowid); + addParameterToRequest("season", season); + addParameterToRequest("properties", properties); + } + + @Override + public String getMethodName() { + return METHOD_NAME; + } + + @Override + public List 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 result = new ArrayList(items + .size()); + + for (JsonNode item : items) { + result.add(new VideoType.DetailsEpisode(item)); + } + + return result; + } + } + + /** + * Retrieve all music videos + */ + public static class GetMusicVideos extends ApiMethod> { + public final static String METHOD_NAME = "VideoLibrary.GetMusicVideos"; + + private final static String LIST_NODE = "musicvideos"; + + /** + * Retrieve all music videos + * + * @param properties Properties to retrieve. See {@link VideoType.FieldsMusicVideo} for a + * list of accepted values + */ + public GetMusicVideos(String... properties) { + super(); + addParameterToRequest("properties", properties); + } + + @Override + public String getMethodName() { + return METHOD_NAME; + } + + @Override + public List 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 result = + new ArrayList(items.size()); + + for (JsonNode item : items) { + result.add(new VideoType.DetailsMusicVideo(item)); + } + + return result; + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/syncedsynapse/kore2/jsonrpc/notification/Input.java b/app/src/main/java/com/syncedsynapse/kore2/jsonrpc/notification/Input.java new file mode 100644 index 0000000..bcf3c48 --- /dev/null +++ b/app/src/main/java/com/syncedsynapse/kore2/jsonrpc/notification/Input.java @@ -0,0 +1,50 @@ +/* + * 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 com.syncedsynapse.kore2.jsonrpc.notification; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.syncedsynapse.kore2.jsonrpc.ApiNotification; +import com.syncedsynapse.kore2.utils.JsonUtils; + +/** + * Input.* notifications + */ +public class Input { + + /** + * Input.OnInputRequested + * The user is requested to provide some information + */ + public static class OnInputRequested extends ApiNotification { + public static final String NOTIFICATION_NAME = "Input.OnInputRequested"; + + public static final String DATA_NODE = "data"; + + public final String title; + public final String type; + public final String value; + + public OnInputRequested(ObjectNode node) { + super(node); + ObjectNode dataNode = (ObjectNode)node.get(DATA_NODE); + title = JsonUtils.stringFromJsonNode(dataNode, "title"); + type = JsonUtils.stringFromJsonNode(dataNode, "type"); + value = JsonUtils.stringFromJsonNode(dataNode, "value"); + } + + public String getNotificationName() { return NOTIFICATION_NAME; } + } +} diff --git a/app/src/main/java/com/syncedsynapse/kore2/jsonrpc/notification/Player.java b/app/src/main/java/com/syncedsynapse/kore2/jsonrpc/notification/Player.java new file mode 100644 index 0000000..520ffbd --- /dev/null +++ b/app/src/main/java/com/syncedsynapse/kore2/jsonrpc/notification/Player.java @@ -0,0 +1,194 @@ +/* + * 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 com.syncedsynapse.kore2.jsonrpc.notification; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.syncedsynapse.kore2.jsonrpc.ApiNotification; +import com.syncedsynapse.kore2.jsonrpc.type.GlobalType; +import com.syncedsynapse.kore2.utils.JsonUtils; + +/** + * All Player.* notifications + */ +public class Player { + + /** + * Player.OnPause notification + * Playback of a media item has been paused. If there is no ID available extra information will be provided. + */ + public static class OnPause extends ApiNotification { + public static final String NOTIFICATION_NAME = "Player.OnPause"; + + public final NotificationsData data; + + public OnPause(ObjectNode node) { + super(node); + data = new NotificationsData(node.get(NotificationsData.DATA_NODE)); + } + + public String getNotificationName() { return NOTIFICATION_NAME; } + } + + /** + * Player.OnPlay notification + * Playback of a media item has been started or the playback speed has changed. If there is no + * ID available extra information will be provided. + */ + public static class OnPlay extends ApiNotification { + public static final String NOTIFICATION_NAME = "Player.OnPlay"; + + public final NotificationsData data; + + public OnPlay(ObjectNode node) { + super(node); + data = new NotificationsData(node.get(NotificationsData.DATA_NODE)); + } + + public String getNotificationName() { return NOTIFICATION_NAME; } + } + + /** + * Player.OnSeek notification + * The playback position has been changed. If there is no ID available extra information will + * be provided. + */ + public static class OnSeek extends ApiNotification { + public static final String NOTIFICATION_NAME = "Player.OnSeek"; + + public final NotificationsItem item; + public final GlobalType.Time time; + public final GlobalType.Time seekoffset; + + public OnSeek(ObjectNode node) { + super(node); + ObjectNode dataNode = (ObjectNode)node.get("data"); + item = new NotificationsItem(dataNode.get(NotificationsItem.ITEM_NODE)); + ObjectNode playerNode = (ObjectNode)dataNode.get("player"); + time = new GlobalType.Time(playerNode.get("time")); + seekoffset = new GlobalType.Time(playerNode.get("seekoffset")); + } + + public String getNotificationName() { return NOTIFICATION_NAME; } + } + + /** + * Player.OnSpeedChanged notification + * Speed of the playback of a media item has been changed. If there is no ID available extra information will be provided. + * be provided. + */ + public static class OnSpeedChanged extends ApiNotification { + public static final String NOTIFICATION_NAME = "Player.OnSpeedChanged"; + + public final NotificationsData data; + + public OnSpeedChanged(ObjectNode node) { + super(node); + data = new NotificationsData(node.get(NotificationsData.DATA_NODE)); + } + + public String getNotificationName() { return NOTIFICATION_NAME; } + } + + /** + * Player.OnStop notification + * Playback of a media item has been stopped. If there is no ID available extra information will be provided. + */ + public static class OnStop extends ApiNotification { + public static final String NOTIFICATION_NAME = "Player.OnStop"; + + public final boolean end; + public final NotificationsItem item; + + public OnStop(ObjectNode node) { + super(node); + ObjectNode dataNode = (ObjectNode)node.get("data"); + end = JsonUtils.booleanFromJsonNode(dataNode, "end"); + item = new NotificationsItem(dataNode.get(NotificationsItem.ITEM_NODE)); + } + + public String getNotificationName() { return NOTIFICATION_NAME; } + } + + /** + * Notification data for Player + */ + public static class NotificationsPlayer { + public static final String PLAYER_NODE = "player"; + + public final int playerId; + public final int speed; + + public NotificationsPlayer(JsonNode node) { + playerId = JsonUtils.intFromJsonNode(node, "playerid"); + speed = JsonUtils.intFromJsonNode(node, "speed", 0); + } + } + + /** + * General notification data + */ + public static class NotificationsItem { + public static final String ITEM_NODE = "item"; + /** + * The item types + */ + public static final String TYPE_UNKNOWN = "unknown", + TYPE_MOVIE = "movie", + TYPE_EPISODE = "episode", + TYPE_MUSIC_VIDEO = "musicvideo", + TYPE_SONG = "song", + TYPE_PICTURE = "picture", + TYPE_CHANNEL = "channel"; + + public final String type; + public final int id; + public final String title; + public final int year; + public final int episode; + public final int season; + public final String showtitle; + public final String album; + public final String artist; + public final int track; + + public NotificationsItem(JsonNode node) { + type = JsonUtils.stringFromJsonNode(node, "type", TYPE_UNKNOWN); + id = JsonUtils.intFromJsonNode(node, "speed"); + title = JsonUtils.stringFromJsonNode(node, "title"); + year = JsonUtils.intFromJsonNode(node, "year", 0); + episode = JsonUtils.intFromJsonNode(node, "episode", 0); + season = JsonUtils.intFromJsonNode(node, "season", 0); + showtitle = JsonUtils.stringFromJsonNode(node, "showtitle"); + album = JsonUtils.stringFromJsonNode(node, "album"); + artist = JsonUtils.stringFromJsonNode(node, "artist"); + track = JsonUtils.intFromJsonNode(node, "track", 0); + } + } + + public static class NotificationsData { + public static final String DATA_NODE = "data"; + + public final NotificationsPlayer player; + public final NotificationsItem item; + + public NotificationsData(JsonNode node) { + item = new NotificationsItem((ObjectNode)node.get(NotificationsItem.ITEM_NODE)); + player = new NotificationsPlayer((ObjectNode)node.get(NotificationsPlayer.PLAYER_NODE)); + } + } + +} diff --git a/app/src/main/java/com/syncedsynapse/kore2/jsonrpc/notification/System.java b/app/src/main/java/com/syncedsynapse/kore2/jsonrpc/notification/System.java new file mode 100644 index 0000000..18c3de6 --- /dev/null +++ b/app/src/main/java/com/syncedsynapse/kore2/jsonrpc/notification/System.java @@ -0,0 +1,67 @@ +/* + * 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 com.syncedsynapse.kore2.jsonrpc.notification; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.syncedsynapse.kore2.jsonrpc.ApiNotification; + +/** + * System.* notifications + */ +public class System { + + /** + * System.OnQuit notification + * XBMC will be closed + */ + public static class OnQuit extends ApiNotification { + public static final String NOTIFICATION_NAME = "System.OnQuit"; + + public OnQuit(ObjectNode node) { + super(node); + } + + public String getNotificationName() { return NOTIFICATION_NAME; } + } + + /** + * System.OnRestart notification + * The system will be restarted. + */ + public static class OnRestart extends ApiNotification { + public static final String NOTIFICATION_NAME = "System.OnRestart"; + + public OnRestart(ObjectNode node) { + super(node); + } + + public String getNotificationName() { return NOTIFICATION_NAME; } + } + + /** + * System.OnSleep notification + * The system will be suspended. + */ + public static class OnSleep extends ApiNotification { + public static final String NOTIFICATION_NAME = "System.OnSleep"; + + public OnSleep(ObjectNode node) { + super(node); + } + + public String getNotificationName() { return NOTIFICATION_NAME; } + } +} diff --git a/app/src/main/java/com/syncedsynapse/kore2/jsonrpc/type/AddonType.java b/app/src/main/java/com/syncedsynapse/kore2/jsonrpc/type/AddonType.java new file mode 100644 index 0000000..476e5cc --- /dev/null +++ b/app/src/main/java/com/syncedsynapse/kore2/jsonrpc/type/AddonType.java @@ -0,0 +1,143 @@ +/* + * 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 com.syncedsynapse.kore2.jsonrpc.type; + +import com.fasterxml.jackson.databind.JsonNode; +import com.syncedsynapse.kore2.utils.JsonUtils; + +/** + * Types in Addon.* + */ +public class AddonType { + /** + * Enums for Addon.Fields + */ + public interface Fields { + public final String NAME = "name"; + public final String VERSION = "version"; + public final String SUMMARY = "summary"; + public final String DESCRIPTION = "description"; + public final String PATH = "path"; + public final String AUTHOR = "author"; + public final String THUMBNAIL = "thumbnail"; + public final String DISCLAIMER = "disclaimer"; + public final String FANART = "fanart"; + public final String DEPENDENCIES = "dependencies"; + public final String BROKEN = "broken"; + public final String EXTRAINFO = "extrainfo"; + public final String RATING = "rating"; + public final String ENABLED = "enabled"; + + public final static String[] allValues = new String[] { + NAME, VERSION, SUMMARY, DESCRIPTION, PATH, AUTHOR, THUMBNAIL, DISCLAIMER, + FANART, DEPENDENCIES, BROKEN, EXTRAINFO, RATING, ENABLED + }; + } + + /** + * Enums for Addon.Types + */ + public interface Types { + public final String UNKNOWN = "unknown"; + public final String XBMC_METADATA_SCRAPER_ALBUMS = "xbmc.metadata.scraper.albums"; + public final String XBMC_METADATA_SCRAPER_ARTISTS = "xbmc.metadata.scraper.artists"; + public final String XBMC_METADATA_SCRAPER_MOVIES = "xbmc.metadata.scraper.movies"; + public final String XBMC_METADATA_SCRAPER_MUSICVIDEOS = "xbmc.metadata.scraper.musicvideos"; + public final String XBMC_METADATA_SCRAPER_TVSHOWS = "xbmc.metadata.scraper.tvshows"; + public final String XBMC_UI_SCREENSAVER = "xbmc.ui.screensaver"; + public final String XBMC_PLAYER_MUSICVIZ = "xbmc.player.musicviz"; + public final String XBMC_PYTHON_PLUGINSOURCE = "xbmc.python.pluginsource"; + public final String XBMC_PYTHON_SCRIPT = "xbmc.python.script"; + public final String XBMC_PYTHON_WEATHER = "xbmc.python.weather"; + public final String XBMC_PYTHON_SUBTITLES = "xbmc.python.subtitles"; + public final String XBMC_PYTHON_LYRICS = "xbmc.python.lyrics"; + public final String XBMC_GUI_SKIN = "xbmc.gui.skin"; + public final String XBMC_GUI_WEBINTERFACE = "xbmc.gui.webinterface"; + public final String XBMC_PVRCLIENT = "xbmc.pvrclient"; + public final String XBMC_ADDON_VIDEO = "xbmc.addon.video"; + public final String XBMC_ADDON_AUDIO = "xbmc.addon.audio"; + public final String XBMC_ADDON_IMAGE = "xbmc.addon.image"; + public final String XBMC_ADDON_EXECUTABLE = "xbmc.addon.executable"; + public final String XBMC_SERVICE = "xbmc.service"; + + public final static String[] allValues = new String[]{ + UNKNOWN, XBMC_METADATA_SCRAPER_ALBUMS, XBMC_METADATA_SCRAPER_ARTISTS, + XBMC_METADATA_SCRAPER_MOVIES, XBMC_METADATA_SCRAPER_MUSICVIDEOS, + XBMC_METADATA_SCRAPER_TVSHOWS, XBMC_UI_SCREENSAVER, XBMC_PLAYER_MUSICVIZ, + XBMC_PYTHON_PLUGINSOURCE, XBMC_PYTHON_SCRIPT, XBMC_PYTHON_WEATHER, + XBMC_PYTHON_SUBTITLES, XBMC_PYTHON_LYRICS, XBMC_GUI_SKIN, XBMC_GUI_WEBINTERFACE, + XBMC_PVRCLIENT, XBMC_ADDON_VIDEO, XBMC_ADDON_AUDIO, XBMC_ADDON_IMAGE, + XBMC_ADDON_EXECUTABLE, XBMC_SERVICE + }; + } + + public static class Details extends ItemType.DetailsBase { + public static final String ADDONID = "addonid"; + public static final String AUTHOR = "author"; + public static final String BROKEN = "broken"; +// public static final String DEPENDENCIES = "dependencies"; + public static final String DESCRIPTION = "description"; + public static final String DISCLAIMER = "disclaimer"; + public static final String ENABLED = "enabled"; +// public static final String EXTRAINFO = "extrainfo"; + public static final String FANART = "fanart"; + public static final String NAME = "name"; + public static final String PATH = "path"; + public static final String RATING = "rating"; + public static final String SUMMARY = "summary"; + public static final String THUMBNAIL = "thumbnail"; + public static final String TYPE = "type"; + public static final String VERSION = "version"; + + public final String addonid; + public final String author; + public final boolean broken; + public final String description; + public final String disclaimer; + public final Boolean enabled; + public final String fanart; + public final String name; + public final String path; + public final int rating; + public final String summary; + public final String thumbnail; + public final String type; + public final String version; + + /** + * Constructor + * @param node JSON object representing a Detail object + */ + public Details(JsonNode node) { + super(node); + addonid = JsonUtils.stringFromJsonNode(node, ADDONID); + author = JsonUtils.stringFromJsonNode(node, AUTHOR); + broken = JsonUtils.booleanFromJsonNode(node, BROKEN, false); + description = JsonUtils.stringFromJsonNode(node, DESCRIPTION); + disclaimer = JsonUtils.stringFromJsonNode(node, DISCLAIMER); + enabled = JsonUtils.booleanFromJsonNode(node, ENABLED, false); + fanart = JsonUtils.stringFromJsonNode(node, FANART); + name = JsonUtils.stringFromJsonNode(node, NAME); + path = JsonUtils.stringFromJsonNode(node, PATH); + rating = JsonUtils.intFromJsonNode(node, RATING, 0); + summary = JsonUtils.stringFromJsonNode(node, SUMMARY); + thumbnail = JsonUtils.stringFromJsonNode(node, THUMBNAIL); + type = JsonUtils.stringFromJsonNode(node, TYPE); + version = JsonUtils.stringFromJsonNode(node, VERSION); + } + + } +} diff --git a/app/src/main/java/com/syncedsynapse/kore2/jsonrpc/type/ApiParameter.java b/app/src/main/java/com/syncedsynapse/kore2/jsonrpc/type/ApiParameter.java new file mode 100644 index 0000000..6413790 --- /dev/null +++ b/app/src/main/java/com/syncedsynapse/kore2/jsonrpc/type/ApiParameter.java @@ -0,0 +1,25 @@ +/* + * 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 com.syncedsynapse.kore2.jsonrpc.type; + +import com.fasterxml.jackson.databind.JsonNode; + +/** + * Interface that should be implemented by all API types that can be parameters to methods + */ +public interface ApiParameter { + public JsonNode toJsonNode(); +} diff --git a/app/src/main/java/com/syncedsynapse/kore2/jsonrpc/type/ApplicationType.java b/app/src/main/java/com/syncedsynapse/kore2/jsonrpc/type/ApplicationType.java new file mode 100644 index 0000000..bad23f5 --- /dev/null +++ b/app/src/main/java/com/syncedsynapse/kore2/jsonrpc/type/ApplicationType.java @@ -0,0 +1,79 @@ +/* + * 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 com.syncedsynapse.kore2.jsonrpc.type; + +import com.fasterxml.jackson.databind.JsonNode; +import com.syncedsynapse.kore2.utils.JsonUtils; + +/** + * Types defined in Application.* + */ +public class ApplicationType { + + /** + * Application.Property.Value + */ + public static class PropertyValue { + public static final String MUTED = "muted"; + public static final String NAME = "name"; + public static final String VERSION = "version"; + public static final String VOLUME = "volume"; + + // class members + public final Boolean muted; + public final String name; + public final Version version; + public final Integer volume; + + /** + * Contructor + * @param node JSON object representing a PropertyValue + */ + public PropertyValue(JsonNode node) { + muted = JsonUtils.booleanFromJsonNode(node, MUTED, false); + name = JsonUtils.stringFromJsonNode(node, NAME); + version = new Version(node.get(VERSION)); + volume = JsonUtils.intFromJsonNode(node, VOLUME, 0); + } + + /** + * Version + */ + public static class Version { + public static final String MAJOR = "major"; + public static final String MINOR = "minor"; + public static final String REVISION = "revision"; + public static final String TAG = "tag"; + + public final Integer major; + public final Integer minor; + public final String revision; + public final String tag; + + /** + * Constructor + * @param node JSON object representing a Version + */ + public Version(JsonNode node) { + major = JsonUtils.intFromJsonNode(node, MAJOR, 0); + minor = JsonUtils.intFromJsonNode(node, MINOR, 0); + revision = JsonUtils.stringFromJsonNode(node, REVISION); + tag = JsonUtils.stringFromJsonNode(node, TAG); + } + + } + } +} diff --git a/app/src/main/java/com/syncedsynapse/kore2/jsonrpc/type/AudioType.java b/app/src/main/java/com/syncedsynapse/kore2/jsonrpc/type/AudioType.java new file mode 100644 index 0000000..769e350 --- /dev/null +++ b/app/src/main/java/com/syncedsynapse/kore2/jsonrpc/type/AudioType.java @@ -0,0 +1,333 @@ +/* + * 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 com.syncedsynapse.kore2.jsonrpc.type; + +import com.fasterxml.jackson.databind.JsonNode; +import com.syncedsynapse.kore2.utils.JsonUtils; + +import java.util.List; + +/** + * Types from Audio.* + */ +public class AudioType { + + /** + * Enums for Video.Fields.Artists + */ + public interface FieldsArtists { + public final String INSTRUMENT = "instrument"; + public final String STYLE = "style"; + public final String MOOD = "mood"; + public final String BORN = "born"; + public final String FORMED = "formed"; + public final String DESCRIPTION = "description"; + public final String GENRE = "genre"; + public final String DIED = "died"; + public final String DISBANDED = "disbanded"; + public final String YEARSACTIVE = "yearsactive"; + public final String MUSICBRAINZARTISTID = "musicbrainzartistid"; + public final String FANART = "fanart"; + public final String THUMBNAIL = "thumbnail"; + public final String COMPILATIONARTIST = "compilationartist"; + + public final static String[] allValues = new String[]{ + INSTRUMENT, STYLE, MOOD, BORN, FORMED, DESCRIPTION, GENRE, DIED, DISBANDED, + YEARSACTIVE, MUSICBRAINZARTISTID, FANART, THUMBNAIL, COMPILATIONARTIST + }; + } + + /** + * Audio.Details.Base + */ + public static class DetailsBase extends MediaType.DetailsBase { + public static final String GENRE = "genre"; + + public final List genre; + + /** + * Constructor + * @param node Json node + */ + public DetailsBase(JsonNode node) { + super(node); + genre = JsonUtils.stringListFromJsonNode(node, GENRE); + } + } + + /** + * Audio.Details.Media + */ + public static class DetailsMedia extends DetailsBase { + public static final String ARTIST = "artist"; + public static final String ARTISTID = "artistid"; + public static final String DISPLAYARTIST = "displayartist"; + public static final String GENREID = "genreid"; + public static final String MUSICBRAINZALBUMARTISTID = "musicbrainzalbumartistid"; + public static final String MUSICBRAINZALBUMID = "musicbrainzalbumid"; + public static final String RATING = "rating"; + public static final String TITLE = "title"; + public static final String YEAR = "year"; + + // class members + public final List artist; + public final List artistid; + public final String displayartist; + public final List genreid; + public final String musicbrainzalbumartistid; + public final String musicbrainzalbumid; + public final int rating; + public final String title; + public final int year; + + public DetailsMedia(JsonNode node) { + super(node); + artist = JsonUtils.stringListFromJsonNode(node, ARTIST); + artistid = JsonUtils.integerListFromJsonNode(node, ARTISTID); + displayartist = JsonUtils.stringFromJsonNode(node, DISPLAYARTIST); + genreid = JsonUtils.integerListFromJsonNode(node, GENREID); + musicbrainzalbumartistid = JsonUtils.stringFromJsonNode(node, MUSICBRAINZALBUMARTISTID); + musicbrainzalbumid = JsonUtils.stringFromJsonNode(node, MUSICBRAINZALBUMID); + rating = JsonUtils.intFromJsonNode(node, RATING); + title = JsonUtils.stringFromJsonNode(node, TITLE); + year = JsonUtils.intFromJsonNode(node, YEAR); + } + } + + /** + * Audio.Details.Artist + */ + public static class DetailsArtist extends DetailsBase { + public static final String ARTIST = "artist"; + public static final String ARTISTID = "artistid"; + public static final String BORN = "born"; + public static final String COMPILATIONARTIST = "compilationartist"; + public static final String DESCRIPTION = "description"; + public static final String DIED = "died"; + public static final String DISBANDED = "disbanded"; + public static final String FORMED = "formed"; + public static final String INSTRUMENT = "instrument"; + public static final String MOOD = "mood"; + public static final String MUSICBRAINZARTISTID = "musicbrainzartistid"; + public static final String STYLE = "style"; + public static final String YEARSACTIVE = "yearsactive"; + + public final String artist; + public final int artistid; + public final String born; + public final boolean compilationartist; + public final String description; + public final String died; + public final String disbanded; + public final String formed; + public final List instrument; + public final List mood; + public final String musicbrainzartistid; + public final List style; + public final List yearsactive; + + + /** + * Constructor + * @param node Json node + */ + public DetailsArtist(JsonNode node) { + super(node); + artist = JsonUtils.stringFromJsonNode(node, ARTIST); + artistid = JsonUtils.intFromJsonNode(node, ARTISTID); + born = JsonUtils.stringFromJsonNode(node, BORN); + compilationartist = JsonUtils.booleanFromJsonNode(node, COMPILATIONARTIST, false); + description = JsonUtils.stringFromJsonNode(node, DESCRIPTION); + died = JsonUtils.stringFromJsonNode(node, DIED); + disbanded = JsonUtils.stringFromJsonNode(node, DISBANDED); + formed = JsonUtils.stringFromJsonNode(node, FORMED); + instrument = JsonUtils.stringListFromJsonNode(node, INSTRUMENT); + mood = JsonUtils.stringListFromJsonNode(node, MOOD); + musicbrainzartistid = JsonUtils.stringFromJsonNode(node, MUSICBRAINZARTISTID); + style = JsonUtils.stringListFromJsonNode(node, STYLE); + yearsactive = JsonUtils.stringListFromJsonNode(node, YEARSACTIVE); + } + } + + /** + * Enums for Audio.Fields.Album + */ + public interface FieldsAlbum { + public final String TITLE = "title"; + public final String DESCRIPTION = "description"; + public final String ARTIST = "artist"; + public final String GENRE = "genre"; + public final String THEME = "theme"; + public final String MOOD = "mood"; + public final String STYLE = "style"; + public final String TYPE = "type"; + public final String ALBUMLABEL = "albumlabel"; + public final String RATING = "rating"; + public final String YEAR = "year"; + public final String MUSICBRAINZALBUMID = "musicbrainzalbumid"; + public final String MUSICBRAINZALBUMARTISTID = "musicbrainzalbumartistid"; + public final String FANART = "fanart"; + public final String THUMBNAIL = "thumbnail"; + public final String PLAYCOUNT = "playcount"; + public final String GENREID = "genreid"; + public final String ARTISTID = "artistid"; + public final String DISPLAYARTIST = "displayartist"; + + public final static String[] allValues = new String[]{ + TITLE, DESCRIPTION, ARTIST, GENRE, THEME, MOOD, STYLE, TYPE, ALBUMLABEL, RATING, + YEAR, MUSICBRAINZALBUMID, MUSICBRAINZALBUMARTISTID, FANART, THUMBNAIL, + PLAYCOUNT, GENREID, ARTISTID, DISPLAYARTIST + }; + } + + /** + * Audio.Details.Album + */ + public static class DetailsAlbum extends DetailsMedia { + public static final String ALBUMID = "albumid"; + public static final String ALBUMLABEL = "albumlabel"; + public static final String DESCRIPTION = "description"; + public static final String MOOD = "mood"; + public static final String PLAYCOUNT = "playcount"; + public static final String STYLE = "style"; + public static final String THEME = "theme"; + public static final String TYPE = "type"; + + public final int albumid; + public final String albumlabel; + public final String description; + public final List mood; + public final int playcount; + public final List style; + public final List theme; + public final String type; + + /** + * Constructor + * @param node Json node + */ + public DetailsAlbum(JsonNode node) { + super(node); + albumid = JsonUtils.intFromJsonNode(node, ALBUMID); + albumlabel = JsonUtils.stringFromJsonNode(node, ALBUMLABEL); + description = JsonUtils.stringFromJsonNode(node, DESCRIPTION); + mood = JsonUtils.stringListFromJsonNode(node, MOOD); + playcount = JsonUtils.intFromJsonNode(node, PLAYCOUNT); + style = JsonUtils.stringListFromJsonNode(node, STYLE); + theme = JsonUtils.stringListFromJsonNode(node, THEME); + type = JsonUtils.stringFromJsonNode(node, TYPE); + } + } + + /** + * Enums for Audio.Fields.Song + */ + public interface FieldsSong { + public final String TITLE = "title"; + public final String ARTIST = "artist"; + public final String ALBUMARTIST = "albumartist"; + public final String GENRE = "genre"; + public final String YEAR = "year"; + public final String RATING = "rating"; + public final String ALBUM = "album"; + public final String TRACK = "track"; + public final String DURATION = "duration"; + public final String COMMENT = "comment"; + public final String LYRICS = "lyrics"; + public final String MUSICBRAINZTRACKID = "musicbrainztrackid"; + public final String MUSICBRAINZARTISTID = "musicbrainzartistid"; + public final String MUSICBRAINZALBUMID = "musicbrainzalbumid"; + public final String MUSICBRAINZALBUMARTISTID = "musicbrainzalbumartistid"; + public final String PLAYCOUNT = "playcount"; + public final String FANART = "fanart"; + public final String THUMBNAIL = "thumbnail"; + public final String FILE = "file"; + public final String ALBUMID = "albumid"; + public final String LASTPLAYED = "lastplayed"; + public final String DISC = "disc"; + public final String GENREID = "genreid"; + public final String ARTISTID = "artistid"; + public final String DISPLAYARTIST = "displayartist"; + public final String ALBUMARTISTID = "albumartistid"; + + public final static String[] allValues = new String[]{ + TITLE, ARTIST, ALBUMARTIST, GENRE, YEAR, RATING, ALBUM, TRACK, DURATION, + COMMENT, LYRICS, MUSICBRAINZTRACKID, MUSICBRAINZARTISTID, MUSICBRAINZALBUMID, + MUSICBRAINZALBUMARTISTID, PLAYCOUNT, FANART, THUMBNAIL, FILE, ALBUMID, + LASTPLAYED, DISC, GENREID, ARTISTID, DISPLAYARTIST, ALBUMARTISTID + }; + } + + /** + * Audio.Details.Song + */ + public static class DetailsSong extends DetailsMedia { + public static final String ALBUM = "album"; + public static final String ALBUMARTIST = "albumartist"; + public static final String ALBUMARTISTID = "albumartistid"; + public static final String ALBUMID = "albumid"; + public static final String COMMENT = "comment"; + public static final String DISC = "disc"; + public static final String DURATION = "duration"; + public static final String FILE = "file"; + public static final String LASTPLAYED = "lastplayed"; + public static final String LYRICS = "lyrics"; + public static final String MUSICBRAINZARTISTID = "musicbrainzartistid"; + public static final String MUSICBRAINZTRACKID = "musicbrainztrackid"; + public static final String PLAYCOUNT = "playcount"; + public static final String SONGID = "songid"; + public static final String TRACK = "track"; + + public final String album; + public final List albumartist; + public final List albumartistid; + public final int albumid; + public final String comment; + public final int disc; + public final int duration; + public final String file; + public final String lastplayed; + public final String lyrics; + public final String musicbrainzartistid; + public final String musicbrainztrackid; + public final int playcount; + public final int songid; + public final int track; + + /** + * Constructor + * @param node Json node + */ + public DetailsSong(JsonNode node) { + super(node); + album = JsonUtils.stringFromJsonNode(node, ALBUM); + albumid = JsonUtils.intFromJsonNode(node, ALBUMID); + albumartist = JsonUtils.stringListFromJsonNode(node, ALBUMARTIST); + albumartistid = JsonUtils.integerListFromJsonNode(node, ALBUMARTISTID); + comment = JsonUtils.stringFromJsonNode(node, COMMENT); + disc = JsonUtils.intFromJsonNode(node, DISC); + duration = JsonUtils.intFromJsonNode(node, DURATION); + file = JsonUtils.stringFromJsonNode(node, FILE); + lastplayed = JsonUtils.stringFromJsonNode(node, LASTPLAYED); + lyrics= JsonUtils.stringFromJsonNode(node, LYRICS); + musicbrainzartistid = JsonUtils.stringFromJsonNode(node, MUSICBRAINZARTISTID); + musicbrainztrackid = JsonUtils.stringFromJsonNode(node, MUSICBRAINZTRACKID); + playcount = JsonUtils.intFromJsonNode(node, PLAYCOUNT); + songid = JsonUtils.intFromJsonNode(node, SONGID); + track = JsonUtils.intFromJsonNode(node, TRACK); + } + } +} diff --git a/app/src/main/java/com/syncedsynapse/kore2/jsonrpc/type/FilesType.java b/app/src/main/java/com/syncedsynapse/kore2/jsonrpc/type/FilesType.java new file mode 100644 index 0000000..6aa43fc --- /dev/null +++ b/app/src/main/java/com/syncedsynapse/kore2/jsonrpc/type/FilesType.java @@ -0,0 +1,48 @@ +/* + * 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 com.syncedsynapse.kore2.jsonrpc.type; + +import com.fasterxml.jackson.databind.JsonNode; +import com.syncedsynapse.kore2.utils.JsonUtils; + +/** + * Return types for methods in Files.* + */ +public class FilesType { + /** + * GetActivePlayers return type + */ + public static final class PrepareDownloadReturnType { + public final static String DETAILS = "details"; + public final static String MODE = "mode"; + public final static String PROTOCOL = "protocol"; + public final static String PATH = "path"; + + // Returned info +// public final String details; + public final String mode; + public final String protocol; + public final String path; + + public PrepareDownloadReturnType(JsonNode node) { + mode = JsonUtils.stringFromJsonNode(node, MODE); + protocol = JsonUtils.stringFromJsonNode(node, PROTOCOL); + + JsonNode details = node.get(DETAILS); + path = JsonUtils.stringFromJsonNode(details, PATH); + } + } +} diff --git a/app/src/main/java/com/syncedsynapse/kore2/jsonrpc/type/GlobalType.java b/app/src/main/java/com/syncedsynapse/kore2/jsonrpc/type/GlobalType.java new file mode 100644 index 0000000..f739781 --- /dev/null +++ b/app/src/main/java/com/syncedsynapse/kore2/jsonrpc/type/GlobalType.java @@ -0,0 +1,57 @@ +/* + * 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 com.syncedsynapse.kore2.jsonrpc.type; + +import com.fasterxml.jackson.databind.JsonNode; +import com.syncedsynapse.kore2.utils.JsonUtils; + +/** + * Types from Global.* + */ +public class GlobalType { + + /** + * Global.Time + */ + public static class Time { + public static final String HOURS = "hours"; + public static final String MILLISECONDS = "milliseconds"; + public static final String MINUTES = "minutes"; + public static final String SECONDS = "seconds"; + + public final int hours; + public final int milliseconds; + public final int minutes; + public final int seconds; + + public Time(JsonNode node) { + hours = JsonUtils.intFromJsonNode(node, HOURS, 0); + milliseconds = JsonUtils.intFromJsonNode(node, MILLISECONDS, 0); + minutes = JsonUtils.intFromJsonNode(node, MINUTES, 0); + seconds = JsonUtils.intFromJsonNode(node, SECONDS, 0); + } + } + + /** + * Global.IncrementDecrement + */ + public interface IncrementDecrement { + public final String INCREMENT = "increment"; + public final String DECREMENT = "decrement"; + } + +} + diff --git a/app/src/main/java/com/syncedsynapse/kore2/jsonrpc/type/ItemType.java b/app/src/main/java/com/syncedsynapse/kore2/jsonrpc/type/ItemType.java new file mode 100644 index 0000000..ffafa9b --- /dev/null +++ b/app/src/main/java/com/syncedsynapse/kore2/jsonrpc/type/ItemType.java @@ -0,0 +1,40 @@ +/* + * 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 com.syncedsynapse.kore2.jsonrpc.type; + +import com.fasterxml.jackson.databind.JsonNode; + +/** + * Types from Item.* + */ +public class ItemType { + /** + * Item.Details.Base + */ + public static class DetailsBase { + public static final String LABEL = "label"; + + public final String label; + + public DetailsBase(JsonNode node) { + JsonNode labelNode = node.get(LABEL); + if (labelNode != null) + label = labelNode.asText(); + else + label = null; + } + } +} diff --git a/app/src/main/java/com/syncedsynapse/kore2/jsonrpc/type/LibraryType.java b/app/src/main/java/com/syncedsynapse/kore2/jsonrpc/type/LibraryType.java new file mode 100644 index 0000000..5666c16 --- /dev/null +++ b/app/src/main/java/com/syncedsynapse/kore2/jsonrpc/type/LibraryType.java @@ -0,0 +1,62 @@ +/* + * 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 com.syncedsynapse.kore2.jsonrpc.type; + +import com.fasterxml.jackson.databind.JsonNode; +import com.syncedsynapse.kore2.utils.JsonUtils; + +/** + * Types from Library.* + */ +public class LibraryType { + /** + * Enums for Library.Fields.Genre + */ + public interface FieldsGenre { + public final String TITLE = "title"; + public final String THUMBNAIL = "thumbnail"; + + public final static String[] allValues = new String[]{ + TITLE, THUMBNAIL + }; + } + + /** + * Library.Details.Genre + */ + public static class DetailsGenre extends ItemType.DetailsBase { + public static final String GENREID = "genreid"; + public static final String THUMBNAIL = "thumbnail"; + public static final String TITLE = "title"; + + // class members + public final Integer genreid; + public final String thumbnail; + public final String title; + + /** + * Constructor + * @param node Json node + */ + public DetailsGenre(JsonNode node) { + super(node); + genreid = JsonUtils.intFromJsonNode(node, GENREID); + thumbnail = JsonUtils.stringFromJsonNode(node, THUMBNAIL); + title = JsonUtils.stringFromJsonNode(node, TITLE); + } + } + +} diff --git a/app/src/main/java/com/syncedsynapse/kore2/jsonrpc/type/ListType.java b/app/src/main/java/com/syncedsynapse/kore2/jsonrpc/type/ListType.java new file mode 100644 index 0000000..5c474be --- /dev/null +++ b/app/src/main/java/com/syncedsynapse/kore2/jsonrpc/type/ListType.java @@ -0,0 +1,405 @@ +/* + * 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 com.syncedsynapse.kore2.jsonrpc.type; + +import com.fasterxml.jackson.databind.JsonNode; +import com.syncedsynapse.kore2.utils.JsonUtils; + +import java.util.List; + +/** + * Types defined in List.* + */ +public class ListType { + + /** + * List.Item.Base type + */ + public static class ItemBase { + public static final String TYPE_MOVIE = "movie"; + public static final String TYPE_EPISODE = "episode"; + public static final String TYPE_SONG = "song"; + public static final String TYPE_MUSIC_VIDEO = "musicvideo"; + + // From List.Item.Base + public static final String ALBUM = "album"; + public static final String ALBUMARTIST = "albumartist"; + public static final String ALBUMARTISTID = "albumartistid"; + public static final String ALBUMID = "albumid"; + public static final String ALBUMLABEL = "albumlabel"; + public static final String CAST = "cast"; + public static final String COMMENT = "comment"; + public static final String COUNTRY = "country"; + public static final String DESCRIPTION = "description"; + public static final String DISC = "disc"; + public static final String DURATION = "duration"; + public static final String EPISODE = "episode"; + public static final String EPISODEGUIDE = "episodeguide"; + public static final String FIRSTAIRED = "firstaired"; + public static final String ID = "id"; + public static final String IMDBNUMBER = "imdbnumber"; + public static final String LYRICS = "lyrics"; + public static final String MOOD = "mood"; + public static final String MPAA = "mpaa"; + public static final String MUSICBRAINZARTISTID = "musicbrainzartistid"; + public static final String MUSICBRAINZTRACKID = "musicbrainztrackid"; + public static final String ORIGINALTITLE = "originaltitle"; + public static final String PLOTOUTLINE = "plotoutline"; + public static final String PREMIERED = "premiered"; + public static final String PRODUCTIONCODE = "productioncode"; + public static final String SEASON = "season"; + public static final String SET = "set"; + public static final String SETID = "setid"; + public static final String SHOWLINK = "showlink"; + public static final String SHOWTITLE = "showtitle"; + public static final String SORTTITLE = "sorttitle"; + public static final String STUDIO = "studio"; + public static final String STYLE = "style"; + public static final String TAG = "tag"; + public static final String TAGLINE = "tagline"; + public static final String THEME = "theme"; + public static final String TOP250 = "top250"; + public static final String TRACK = "track"; + public static final String TRAILER = "trailer"; + public static final String TVSHOWID = "tvshowid"; + public static final String TYPE = "type"; + public static final String UNIQUEID = "uniqueid"; + public static final String VOTES = "votes"; + public static final String WATCHEDEPISODES = "watchedepisodes"; + public static final String WRITER = "writer"; + + public final String album; + public final List albumartist; + public final List albumartistid; + public final int albumid; + public final String albumlabel; + public final List cast; + public final String comment; + public final List country; + public final String description; + public final int disc; + public final int duration; + public final int episode; + public final String episodeguide; + public final String firstaired; + public final int id; + public final String imdbnumber; + public final String lyrics; + public final List mood; + public final String mpaa; + public final String musicbrainzartistid; + public final String musicbrainztrackid; + public final String originaltitle; + public final String plotoutline; + public final String premiered; + public final String productioncode; + public final int season; + public final String set; + public final int setid; + public final List showlink; + public final String showtitle; + public final String sorttitle; + public final List studio; + public final List style; + public final List tag; + public final String tagline; + public final List theme; + public final int top250; + public final int track; + public final String trailer; + public final int tvshowid; + public final String type; + // public final HashMap uniqueid; + public final String votes; + public final int watchedepisodes; + public final List writer; + + + // From Video.Details.Base + public static final String ART = "art"; + public static final String PLAYCOUNT = "playcount"; + + public MediaType.Artwork art; + public int playcount; + + + // From Audio.Details.Media + public static final String ARTIST = "artist"; + public static final String ARTISTID = "artistid"; + public static final String DISPLAYARTIST = "displayartist"; + public static final String GENREID = "genreid"; + public static final String MUSICBRAINZALBUMARTISTID = "musicbrainzalbumartistid"; + public static final String MUSICBRAINZALBUMID = "musicbrainzalbumid"; + public static final String RATING = "rating"; + public static final String TITLE = "title"; + public static final String YEAR = "year"; + + public final List artist; + public final List artistid; + public final String displayartist; + public final List genreid; + public final String musicbrainzalbumartistid; + public final String musicbrainzalbumid; + public final double rating; + public final String title; + public final int year; + + + // From Video.Details.Item + public static final String DATEADDED = "dateadded"; + public static final String FILE = "file"; + public static final String LASTPLAYED = "lastplayed"; + public static final String PLOT = "plot"; + + public final String dateadded; + public final String file; + public final String lastplayed; + public final String plot; + + + // From Video.Details.File + public static final String DIRECTOR = "director"; + public static final String RESUME = "resume"; + public static final String RUNTIME = "runtime"; + public static final String STREAMDETAILS = "streamdetails"; + + public final List director; + public final VideoType.Resume resume; + public final int runtime; + public final VideoType.Streams streamdetails; + + + // From Media.Details.Base + public static final String FANART = "fanart"; + public static final String THUMBNAIL = "thumbnail"; + + public final String fanart; + public final String thumbnail; + + + // From Audio.Details.Base + public static final String GENRE = "genre"; + + public final List genre; + + + // From Item.Details.Base. + public static final String LABEL = "label"; + + public final String label; + + + /** + * Constructor. + * + * @param node JSON object + */ + public ItemBase(JsonNode node) { + album = JsonUtils.stringFromJsonNode(node, ALBUM, null); + albumartist = JsonUtils.stringListFromJsonNode(node, ALBUMARTIST); + albumartistid = JsonUtils.integerListFromJsonNode(node, ALBUMARTISTID); + albumid = JsonUtils.intFromJsonNode(node, ALBUMID, -1); + albumlabel = JsonUtils.stringFromJsonNode(node, ALBUMLABEL, null); + art = new MediaType.Artwork(node.get(ART)); + artist = JsonUtils.stringListFromJsonNode(node, ARTIST); + artistid = JsonUtils.integerListFromJsonNode(node, ARTISTID); + cast = VideoType.Cast.castListFromJsonNode(node, CAST); + comment = JsonUtils.stringFromJsonNode(node, COMMENT, null); + country = JsonUtils.stringListFromJsonNode(node, COUNTRY); + dateadded = JsonUtils.stringFromJsonNode(node, DATEADDED, null); + description = JsonUtils.stringFromJsonNode(node, DESCRIPTION, null); + director = JsonUtils.stringListFromJsonNode(node, DIRECTOR); + disc = JsonUtils.intFromJsonNode(node, DISC, 0); + displayartist = JsonUtils.stringFromJsonNode(node, DISPLAYARTIST, null); + duration = JsonUtils.intFromJsonNode(node, DURATION, 0); + episode = JsonUtils.intFromJsonNode(node, EPISODE, 0); + episodeguide = JsonUtils.stringFromJsonNode(node, EPISODEGUIDE, null); + fanart = JsonUtils.stringFromJsonNode(node, FANART, null); + file = JsonUtils.stringFromJsonNode(node, FILE, null); + firstaired = JsonUtils.stringFromJsonNode(node, FIRSTAIRED, null); + genre = JsonUtils.stringListFromJsonNode(node, GENRE); + genreid = JsonUtils.integerListFromJsonNode(node, GENREID); + id = JsonUtils.intFromJsonNode(node, ID, -1); + imdbnumber = JsonUtils.stringFromJsonNode(node, IMDBNUMBER, null); + label = JsonUtils.stringFromJsonNode(node, LABEL, null); + lastplayed = JsonUtils.stringFromJsonNode(node, LASTPLAYED, null); + lyrics = JsonUtils.stringFromJsonNode(node, LYRICS, null); + mood = JsonUtils.stringListFromJsonNode(node, MOOD); + mpaa = JsonUtils.stringFromJsonNode(node, MPAA, null); + musicbrainzalbumartistid = JsonUtils.stringFromJsonNode(node, MUSICBRAINZALBUMARTISTID, null); + musicbrainzalbumid = JsonUtils.stringFromJsonNode(node, MUSICBRAINZALBUMID, null); + musicbrainzartistid = JsonUtils.stringFromJsonNode(node, MUSICBRAINZARTISTID, null); + musicbrainztrackid = JsonUtils.stringFromJsonNode(node, MUSICBRAINZTRACKID, null); + originaltitle = JsonUtils.stringFromJsonNode(node, ORIGINALTITLE, null); + playcount = JsonUtils.intFromJsonNode(node, PLAYCOUNT, 0); + plot = JsonUtils.stringFromJsonNode(node, PLOT, null); + plotoutline = JsonUtils.stringFromJsonNode(node, PLOTOUTLINE, null); + premiered = JsonUtils.stringFromJsonNode(node, PREMIERED, null); + productioncode = JsonUtils.stringFromJsonNode(node, PRODUCTIONCODE, null); + rating = JsonUtils.doubleFromJsonNode(node, RATING, 0); + resume = node.has(RESUME) ? new VideoType.Resume(node.get(RESUME)) : null; + runtime = JsonUtils.intFromJsonNode(node, RUNTIME, -1); + season = JsonUtils.intFromJsonNode(node, SEASON, 0); + set = JsonUtils.stringFromJsonNode(node, SET, null); + setid = JsonUtils.intFromJsonNode(node, SETID, -1); + showlink = JsonUtils.stringListFromJsonNode(node, SHOWLINK); + showtitle = JsonUtils.stringFromJsonNode(node, SHOWTITLE, null); + sorttitle = JsonUtils.stringFromJsonNode(node, SORTTITLE, null); + streamdetails = node.has(STREAMDETAILS) ? new VideoType.Streams(node.get(STREAMDETAILS)) : null; + studio = JsonUtils.stringListFromJsonNode(node, STUDIO); + style = JsonUtils.stringListFromJsonNode(node, STYLE); + tag = JsonUtils.stringListFromJsonNode(node, TAG); + tagline = JsonUtils.stringFromJsonNode(node, TAGLINE, null); + theme = JsonUtils.stringListFromJsonNode(node, THEME); + thumbnail = JsonUtils.stringFromJsonNode(node, THUMBNAIL, null); + title = JsonUtils.stringFromJsonNode(node, TITLE, null); + top250 = JsonUtils.intFromJsonNode(node, TOP250, 0); + track = JsonUtils.intFromJsonNode(node, TRACK, 0); + trailer = JsonUtils.stringFromJsonNode(node, TRAILER, null); + tvshowid = JsonUtils.intFromJsonNode(node, TVSHOWID, -1); + type = JsonUtils.stringFromJsonNode(node, TYPE, null); +// uniqueid = getStringMap(node, UNIQUEID); + votes = JsonUtils.stringFromJsonNode(node, VOTES, null); + watchedepisodes = JsonUtils.intFromJsonNode(node, WATCHEDEPISODES, -1); + writer = JsonUtils.stringListFromJsonNode(node, WRITER); + year = JsonUtils.intFromJsonNode(node, YEAR, -1); + } + } + + public static class ItemsAll extends ItemBase { + public static final String CHANNEL = "channel"; + public static final String CHANNELNUMBER = "channelnumber"; + public static final String CHANNELTYPE = "channeltype"; + public static final String ENDTIME = "endtime"; + public static final String HIDDEN = "hidden"; + public static final String LOCKED = "locked"; + public static final String STARTTIME = "starttime"; + + // class members + public final String channel; + public final int channelnumber; + public final String channeltype; + public final String endtime; + public final boolean hidden; + public final boolean locked; + public final String starttime; + + public ItemsAll(JsonNode node) { + super(node); + + channel = JsonUtils.stringFromJsonNode(node, CHANNEL, null); + channelnumber = JsonUtils.intFromJsonNode(node, CHANNELNUMBER, 0); + channeltype = JsonUtils.stringFromJsonNode(node, CHANNELTYPE, "tv"); + endtime = JsonUtils.stringFromJsonNode(node, ENDTIME, null); + hidden = JsonUtils.booleanFromJsonNode(node, HIDDEN, false); + locked = JsonUtils.booleanFromJsonNode(node, LOCKED, false); + starttime = JsonUtils.stringFromJsonNode(node, STARTTIME, null); + } + } + + /** + * Enums for List.Fields.All + */ + public interface FieldsAll { + // We are ignoring Item.Fields.Base as it seems to have nothing useful + public final String TITLE = "title"; + public final String ARTIST = "artist"; + public final String ALBUMARTIST = "albumartist"; + public final String GENRE = "genre"; + public final String YEAR = "year"; + public final String RATING = "rating"; + public final String ALBUM = "album"; + public final String TRACK = "track"; + public final String DURATION = "duration"; + public final String COMMENT = "comment"; + public final String LYRICS = "lyrics"; + public final String MUSICBRAINZTRACKID = "musicbrainztrackid"; + public final String MUSICBRAINZARTISTID = "musicbrainzartistid"; + public final String MUSICBRAINZALBUMID = "musicbrainzalbumid"; + public final String MUSICBRAINZALBUMARTISTID = "musicbrainzalbumartistid"; + public final String PLAYCOUNT = "playcount"; + public final String FANART = "fanart"; + public final String DIRECTOR = "director"; + public final String TRAILER = "trailer"; + public final String TAGLINE = "tagline"; + public final String PLOT = "plot"; + public final String PLOTOUTLINE = "plotoutline"; + public final String ORIGINALTITLE = "originaltitle"; + public final String LASTPLAYED = "lastplayed"; + public final String WRITER = "writer"; + public final String STUDIO = "studio"; + public final String MPAA = "mpaa"; + public final String CAST = "cast"; + public final String COUNTRY = "country"; + public final String IMDBNUMBER = "imdbnumber"; + public final String PREMIERED = "premiered"; + public final String PRODUCTIONCODE = "productioncode"; + public final String RUNTIME = "runtime"; + public final String SET = "set"; + public final String SHOWLINK = "showlink"; + public final String STREAMDETAILS = "streamdetails"; + public final String TOP250 = "top250"; + public final String VOTES = "votes"; + public final String FIRSTAIRED = "firstaired"; + public final String SEASON = "season"; + public final String EPISODE = "episode"; + public final String SHOWTITLE = "showtitle"; + public final String THUMBNAIL = "thumbnail"; + public final String FILE = "file"; + public final String RESUME = "resume"; + public final String ARTISTID = "artistid"; + public final String ALBUMID = "albumid"; + public final String TVSHOWID = "tvshowid"; + public final String SETID = "setid"; + public final String WATCHEDEPISODES = "watchedepisodes"; + public final String DISC = "disc"; + public final String TAG = "tag"; + public final String ART = "art"; + public final String GENREID = "genreid"; + public final String DISPLAYARTIST = "displayartist"; + public final String ALBUMARTISTID = "albumartistid"; + public final String DESCRIPTION = "description"; + public final String THEME = "theme"; + public final String MOOD = "mood"; + public final String STYLE = "style"; + public final String ALBUMLABEL = "albumlabel"; + public final String SORTTITLE = "sorttitle"; + public final String EPISODEGUIDE = "episodeguide"; + public final String UNIQUEID = "uniqueid"; + public final String DATEADDED = "dateadded"; + public final String CHANNEL = "channel"; + public final String CHANNELTYPE = "channeltype"; + public final String HIDDEN = "hidden"; + public final String LOCKED = "locked"; + public final String CHANNELNUMBER = "channelnumber"; + public final String STARTTIME = "starttime"; + public final String ENDTIME = "endtime"; + + public final String[] allValues = new String[] { + TITLE, ARTIST, ALBUMARTIST, GENRE, YEAR, RATING, ALBUM, TRACK, DURATION, COMMENT, + LYRICS, MUSICBRAINZTRACKID, MUSICBRAINZARTISTID, MUSICBRAINZALBUMID, + MUSICBRAINZALBUMARTISTID, PLAYCOUNT, FANART, DIRECTOR, TRAILER, TAGLINE, PLOT, + PLOTOUTLINE, ORIGINALTITLE, LASTPLAYED, WRITER, STUDIO, MPAA, CAST, COUNTRY, + IMDBNUMBER, PREMIERED, PRODUCTIONCODE, RUNTIME, SET, SHOWLINK, STREAMDETAILS, + TOP250, VOTES, FIRSTAIRED, SEASON, EPISODE, SHOWTITLE, THUMBNAIL, FILE, RESUME, + ARTISTID, ALBUMID, TVSHOWID, SETID, WATCHEDEPISODES, DISC, TAG, ART, GENREID, + DISPLAYARTIST, ALBUMARTISTID, DESCRIPTION, THEME, MOOD, STYLE, ALBUMLABEL, + SORTTITLE, EPISODEGUIDE, UNIQUEID, DATEADDED, CHANNEL, CHANNELTYPE, HIDDEN, + LOCKED, CHANNELNUMBER, STARTTIME, ENDTIME + }; + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/syncedsynapse/kore2/jsonrpc/type/MediaType.java b/app/src/main/java/com/syncedsynapse/kore2/jsonrpc/type/MediaType.java new file mode 100644 index 0000000..b8eca47 --- /dev/null +++ b/app/src/main/java/com/syncedsynapse/kore2/jsonrpc/type/MediaType.java @@ -0,0 +1,87 @@ +/* + * 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 com.syncedsynapse.kore2.jsonrpc.type; + +import com.fasterxml.jackson.databind.JsonNode; +import com.syncedsynapse.kore2.utils.JsonUtils; + +/** + * Types from Media.* + */ +public class MediaType { + + + /** + * Media.Artwork + */ + public static class Artwork { + public static final String BANNER = "banner"; + public static final String TV_SHOW_BANNER = "tvshow.banner"; + public static final String FANART = "fanart"; + public static final String TV_SHOW_FANART = "tvshow.fanart"; + public static final String POSTER = "poster"; + public static final String TV_SHOW_POSTER = "tvshow.poster"; + public static final String THUMB = "thumb"; + public static final String ALBUM_THUMB = "album.thumb"; + + public String banner; + public String fanart; + public String poster; + public String thumb; + + public Artwork(JsonNode node) { + if (node == null) { + return; + } + + banner = JsonUtils.stringFromJsonNode(node, BANNER, null); + if (banner == null) + banner = JsonUtils.stringFromJsonNode(node, TV_SHOW_BANNER, null); + fanart = JsonUtils.stringFromJsonNode(node, FANART, null); + if (fanart == null) + poster = JsonUtils.stringFromJsonNode(node, TV_SHOW_FANART, null); + poster = JsonUtils.stringFromJsonNode(node, POSTER, null); + if (poster == null) + poster = JsonUtils.stringFromJsonNode(node, TV_SHOW_POSTER, null); + thumb = JsonUtils.stringFromJsonNode(node, THUMB, null); + if (thumb == null) + thumb = JsonUtils.stringFromJsonNode(node, ALBUM_THUMB, null); + } + } + + /** + * Media.Details.Base + */ + public static class DetailsBase extends ItemType.DetailsBase { + public static final String FANART = "fanart"; + public static final String THUMBNAIL = "thumbnail"; + + public final String fanart; + public final String thumbnail; + + /** + * Constructor from Json node + * @param node Json node + */ + public DetailsBase(JsonNode node) { + super(node); + fanart = JsonUtils.stringFromJsonNode(node, FANART, null); + thumbnail = JsonUtils.stringFromJsonNode(node, THUMBNAIL, null); + } + + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/syncedsynapse/kore2/jsonrpc/type/PlayerType.java b/app/src/main/java/com/syncedsynapse/kore2/jsonrpc/type/PlayerType.java new file mode 100644 index 0000000..60d20a0 --- /dev/null +++ b/app/src/main/java/com/syncedsynapse/kore2/jsonrpc/type/PlayerType.java @@ -0,0 +1,331 @@ +/* + * 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 com.syncedsynapse.kore2.jsonrpc.type; + +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 com.syncedsynapse.kore2.utils.JsonUtils; + +import java.util.ArrayList; +import java.util.List; + +/** + * Return types for methods in Player.* + */ +public class PlayerType { + + /** + * GetActivePlayers return type + */ + public static final class GetActivePlayersReturnType { + private final static String PLAYERID = "playerid"; + private final static String TYPE = "type"; + + public final static String VIDEO = "video"; + public final static String AUDIO = "audio"; + public final static String PICTURE = "picture"; + + /** + * Player id currently active + */ + public final int playerid; + /** + * Type of player. See this class public constants + */ + public final String type; + + public GetActivePlayersReturnType(JsonNode node) { + playerid = node.has(PLAYERID) ? node.get(PLAYERID).asInt(-1) : -1; + type = node.has(TYPE) ? node.get(TYPE).textValue() : null; + } + } + + /** + * Enums for Player.Property.Name + */ + public interface PropertyName { + public final String TYPE = "type"; + public final String PARTYMODE = "partymode"; + public final String SPEED = "speed"; + public final String TIME = "time"; + public final String PERCENTAGE = "percentage"; + public final String TOTALTIME = "totaltime"; + public final String PLAYLISTID = "playlistid"; + public final String POSITION = "position"; + public final String REPEAT = "repeat"; + public final String SHUFFLED = "shuffled"; + public final String CANSEEK = "canseek"; + public final String CANCHANGESPEED = "canchangespeed"; + public final String CANMOVE = "canmove"; + public final String CANZOOM = "canzoom"; + public final String CANROTATE = "canrotate"; + public final String CANSHUFFLE = "canshuffle"; + public final String CANREPEAT = "canrepeat"; + public final String CURRENTAUDIOSTREAM = "currentaudiostream"; + public final String AUDIOSTREAMS = "audiostreams"; + public final String SUBTITLEENABLED = "subtitleenabled"; + public final String CURRENTSUBTITLE = "currentsubtitle"; + public final String SUBTITLES = "subtitles"; + public final String LIVE = "live"; + + public final String[] allValues = new String[]{ + TYPE, PARTYMODE, SPEED, TIME, PERCENTAGE, TOTALTIME, PLAYLISTID, POSITION, REPEAT, + SHUFFLED, CANSEEK, CANCHANGESPEED, CANMOVE, CANZOOM, CANROTATE, CANSHUFFLE, + CANREPEAT, CURRENTAUDIOSTREAM, AUDIOSTREAMS, SUBTITLEENABLED, CURRENTSUBTITLE, + SUBTITLES, LIVE + }; + } + + /** + * Player.Property.Value + */ + public static class PropertyValue { + /** + * Player.Type + */ + public static final String TYPE_VIDEO = "video"; + public static final String TYPE_AUDIO = "audio"; + public static final String TYPE_PICTURE = "picture"; + + /** + * Properties + */ + public static final String AUDIOSTREAMS = "audiostreams"; + public static final String CANCHANGESPEED = "canchangespeed"; + public static final String CANMOVE = "canmove"; + public static final String CANREPEAT = "canrepeat"; + public static final String CANROTATE = "canrotate"; + public static final String CANSEEK = "canseek"; + public static final String CANSHUFFLE = "canshuffle"; + public static final String CANZOOM = "canzoom"; + public static final String CURRENTAUDIOSTREAM = "currentaudiostream"; + public static final String CURRENTSUBTITLE = "currentsubtitle"; + public static final String LIVE = "live"; + public static final String PARTYMODE = "partymode"; + public static final String PERCENTAGE = "percentage"; + public static final String PLAYLISTID = "playlistid"; + public static final String POSITION = "position"; + public static final String REPEAT = "repeat"; + public static final String SHUFFLED = "shuffled"; + public static final String SPEED = "speed"; + public static final String SUBTITLEENABLED = "subtitleenabled"; + public static final String SUBTITLES = "subtitles"; + public static final String TIME = "time"; + public static final String TOTALTIME = "totaltime"; + public static final String TYPE = "type"; + + public final List audiostreams; + public final boolean canchangespeed; + public final boolean canmove; + public final boolean canrepeat; + public final boolean canrotate; + public final boolean canseek; + public final boolean canshuffle; + public final boolean canzoom; + public final AudioStreamExtended currentaudiostream; + public final Subtitle currentsubtitle; + public final boolean live; + public final boolean partymode; + public final double percentage; + public final int playlistid; + public final int position; + public final String repeat; + public final boolean shuffled; + public final int speed; + public final boolean subtitleenabled; + public final List subtitles; + public final GlobalType.Time time; + public final GlobalType.Time totaltime; + public final String type; + + + public PropertyValue(JsonNode node) { + audiostreams = node.has(AUDIOSTREAMS) ? AudioStream.getListAudioStream(node.get(AUDIOSTREAMS)) : null; + canchangespeed = JsonUtils.booleanFromJsonNode(node, CANCHANGESPEED, false); + canmove = JsonUtils.booleanFromJsonNode(node, CANMOVE, false); + canrepeat = JsonUtils.booleanFromJsonNode(node, CANREPEAT, false); + canrotate = JsonUtils.booleanFromJsonNode(node, CANROTATE, false); + canseek = JsonUtils.booleanFromJsonNode(node, CANSEEK, false); + canshuffle = JsonUtils.booleanFromJsonNode(node, CANSHUFFLE, false); + canzoom = JsonUtils.booleanFromJsonNode(node, CANZOOM, false); + currentaudiostream = node.has(CURRENTAUDIOSTREAM) ? new AudioStreamExtended(node.get(CURRENTAUDIOSTREAM)) : null; + currentsubtitle = node.has(CURRENTSUBTITLE) ? new Subtitle(node.get(CURRENTSUBTITLE)) : null; + live = JsonUtils.booleanFromJsonNode(node, LIVE, false); + partymode = JsonUtils.booleanFromJsonNode(node, PARTYMODE, false); + percentage = JsonUtils.doubleFromJsonNode(node, PERCENTAGE, 0); + playlistid = JsonUtils.intFromJsonNode(node, PLAYLISTID, -1); + position = JsonUtils.intFromJsonNode(node, POSITION, -1); + repeat = JsonUtils.stringFromJsonNode(node, REPEAT, "off"); + shuffled = JsonUtils.booleanFromJsonNode(node, SHUFFLED, false); + speed = JsonUtils.intFromJsonNode(node, SPEED, 0); + subtitleenabled = JsonUtils.booleanFromJsonNode(node, SUBTITLEENABLED, false); + subtitles = node.has(SUBTITLES) ? Subtitle.getListSubtitle(node.get(SUBTITLES)) : null; + time = node.has(TIME) ? new GlobalType.Time(node.get(TIME)) : null; + totaltime = node.has(TOTALTIME) ? new GlobalType.Time(node.get(TOTALTIME)) : null; + type = JsonUtils.stringFromJsonNode(node, TYPE, "video"); + } + } + + /** + * Player.Audio.Stream + */ + public static class AudioStream { + public static final String INDEX = "index"; + public static final String LANGUAGE = "language"; + public static final String NAME = "name"; + + public final int index; + public final String language; + public final String name; + + public AudioStream(JsonNode node) { + index = JsonUtils.intFromJsonNode(node, INDEX); + language = JsonUtils.stringFromJsonNode(node, LANGUAGE); + name = JsonUtils.stringFromJsonNode(node, NAME); + } + + public static List getListAudioStream(JsonNode node) { + final ArrayNode arrayNode = (ArrayNode)node; + final List result = new ArrayList(node.size()); + + for (JsonNode audioStreamNode : arrayNode) { + result.add(new AudioStream(audioStreamNode)); + } + return result; + } + } + + /** + * Player.Audio.Stream.Extended + */ + public static class AudioStreamExtended extends AudioStream { + public static final String BITRATE = "bitrate"; + public static final String CHANNELS = "channels"; + public static final String CODEC = "codec"; + + public final int bitrate; + public final int channels; + public final String codec; + + public AudioStreamExtended(JsonNode node) { + super(node); + bitrate = JsonUtils.intFromJsonNode(node, BITRATE); + channels = JsonUtils.intFromJsonNode(node, CHANNELS); + codec = JsonUtils.stringFromJsonNode(node, CODEC); + } + } + + /** + * Player.Subtitle + */ + public static class Subtitle { + public static final String INDEX = "index"; + public static final String LANGUAGE = "language"; + public static final String NAME = "name"; + + public final int index; + public final String language; + public final String name; + + public Subtitle(JsonNode node) { + index = JsonUtils.intFromJsonNode(node, INDEX); + language = JsonUtils.stringFromJsonNode(node, LANGUAGE); + name = JsonUtils.stringFromJsonNode(node, NAME); + } + + public static List getListSubtitle(JsonNode node) { + final ArrayNode arrayNode = (ArrayNode)node; + final List result = new ArrayList(node.size()); + + for (JsonNode subtitleNode : arrayNode) { + result.add(new Subtitle(subtitleNode)); + } + return result; + } + } + + /** + * Player.Position.Time + */ + public static class PositionTime + implements ApiParameter { + public static final String HOURS = "hours"; + public static final String MILLISECONDS = "milliseconds"; + public static final String MINUTES = "minutes"; + public static final String SECONDS = "seconds"; + + protected static final ObjectMapper objectMapper = new ObjectMapper(); + + public final int hours; + public final int milliseconds; + public final int minutes; + public final int seconds; + + public PositionTime(int hours, int minutes, int seconds, int milliseconds) { + this.hours = hours; + this.minutes = minutes; + this.seconds = seconds; + this.milliseconds = milliseconds; + } + + public JsonNode toJsonNode() { + final ObjectNode node = objectMapper.createObjectNode(); + node.put(HOURS, hours); + node.put(MILLISECONDS, milliseconds); + node.put(MINUTES, minutes); + node.put(SECONDS, seconds); + return node; + } + } + + /** + * Player.Seek return type + */ + public static final class SeekReturnType { + private final static String PERCENTAGE = "percentage"; + private final static String TIME = "time"; + private final static String TOTAL_TIME = "totaltime"; + + /** + * Percentage + */ + public final int percentage; + /** + * Time and Total time + */ + public final GlobalType.Time time; + public final GlobalType.Time totalTime; + + public SeekReturnType(JsonNode node) { + percentage = JsonUtils.intFromJsonNode(node, PERCENTAGE); + time = new GlobalType.Time(node.get(TIME)); + totalTime = new GlobalType.Time(node.get(TOTAL_TIME)); + } + } + + /** + * Player.Repeat constants + */ + public interface Repeat { + public final String OFF = "off"; + public final String ONE = "one"; + public final String ALL = "all"; + + public final String CYCLE = "cycle"; + } +} \ No newline at end of file diff --git a/app/src/main/java/com/syncedsynapse/kore2/jsonrpc/type/PlaylistType.java b/app/src/main/java/com/syncedsynapse/kore2/jsonrpc/type/PlaylistType.java new file mode 100644 index 0000000..4dc8de3 --- /dev/null +++ b/app/src/main/java/com/syncedsynapse/kore2/jsonrpc/type/PlaylistType.java @@ -0,0 +1,113 @@ +/* + * 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 com.syncedsynapse.kore2.jsonrpc.type; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.syncedsynapse.kore2.utils.JsonUtils; + +/** + * Return types for methods in Playlist.* + */ +public class PlaylistType { + + /** + * GetPlaylists return type + */ + public static final class GetPlaylistsReturnType { + private final static String PLAYLISTID = "playlistid"; + private final static String TYPE = "type"; + + public final static String UNKNOWN = "unknown"; + public final static String VIDEO = "video"; + public final static String AUDIO = "audio"; + public final static String PICTURE = "picture"; + public final static String MIXED = "mixed"; + + /** + * Playlist id + */ + public final int playlistid; + /** + * Type of playlist. See this class public constants + */ + public final String type; + + public GetPlaylistsReturnType(JsonNode node) { + playlistid = node.get(PLAYLISTID).asInt(-1); + type = JsonUtils.stringFromJsonNode(node, TYPE, UNKNOWN); + } + } + + /** + * Playlist.Item + */ + public static class Item implements ApiParameter { + + protected static final ObjectMapper objectMapper = new ObjectMapper(); + + // class members + public int albumid = -1; + public int artistid = -1; + public String directory = null; + public int episodeid = -1; + public String file = null; + public int genreid = -1; + public int movieid = -1; + public int musicvideoid = -1; + public int songid = -1; + + /** + * Constructors + */ + public Item() { + } + + @Override + public JsonNode toJsonNode() { + final ObjectNode node = objectMapper.createObjectNode(); + if (albumid != -1) { + node.put("albumid", albumid); + } + if (artistid != -1) { + node.put("artistid", artistid); + } + if (directory != null) { + node.put("directory", directory); + } + if (episodeid != -1) { + node.put("episodeid", episodeid); + } + if (file != null) { + node.put("file", file); + } + if (genreid != -1) { + node.put("genreid", genreid); + } + if (movieid != -1) { + node.put("movieid", movieid); + } + if (musicvideoid != -1) { + node.put("musicvideoid", musicvideoid); + } + if (songid != -1) { + node.put("songid", songid); + } + return node; + } + } +} diff --git a/app/src/main/java/com/syncedsynapse/kore2/jsonrpc/type/VideoType.java b/app/src/main/java/com/syncedsynapse/kore2/jsonrpc/type/VideoType.java new file mode 100644 index 0000000..0f6ea74 --- /dev/null +++ b/app/src/main/java/com/syncedsynapse/kore2/jsonrpc/type/VideoType.java @@ -0,0 +1,673 @@ +/* + * 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 com.syncedsynapse.kore2.jsonrpc.type; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.syncedsynapse.kore2.utils.JsonUtils; + +import java.util.ArrayList; +import java.util.List; + +/** + * Types from Video.* + */ +public class VideoType { + + public static class Cast { + public static final String NAME = "name"; + public static final String ORDER = "order"; + public static final String ROLE = "role"; + public static final String THUMBNAIL = "thumbnail"; + + public final String name; + public final int order; + public final String role; + public final String thumbnail; + + public Cast(JsonNode node) { + name = JsonUtils.stringFromJsonNode(node, NAME); + order = JsonUtils.intFromJsonNode(node, ORDER, 0); + role = JsonUtils.stringFromJsonNode(node, ROLE); + thumbnail = JsonUtils.stringFromJsonNode(node, THUMBNAIL); + } + + public Cast(String name, int order, String role, String thumbnail) { + this.name = name; + this.order = order; + this.role = role; + this.thumbnail = thumbnail; + } + + public static List castListFromJsonNode(JsonNode node, String key) { + if ((node == null) || (!node.has(key))) { + return new ArrayList(0); + } + + ArrayNode arrayNode = (ArrayNode) node.get(key); + ArrayList castList = new ArrayList(arrayNode.size()); + for (JsonNode innerNode : arrayNode) { + castList.add(new Cast(innerNode)); + } + return castList; + } + } + + public static class Resume { + public static final String POSITION = "position"; + public static final String TOTAL = "total"; + + public final double position; + public final double total; + + public Resume(JsonNode node) { + position = JsonUtils.doubleFromJsonNode(node, POSITION, 0); + total = JsonUtils.doubleFromJsonNode(node, TOTAL, 0); + } + } + + public static class Streams { + + public static class Audio { + public static final String CHANNELS = "channels"; + public static final String CODEC = "codec"; + public static final String LANGUAGE = "language"; + + public final int channels; + public final String codec; + public final String language; + + public Audio(JsonNode node) { + channels = JsonUtils.intFromJsonNode(node, CHANNELS, 0); + codec = JsonUtils.stringFromJsonNode(node, CODEC); + language = JsonUtils.stringFromJsonNode(node, LANGUAGE); + } + } + + public static class Subtitle { + public static final String LANGUAGE = "language"; + + public final String language; + + public Subtitle(JsonNode node) { + language = JsonUtils.stringFromJsonNode(node, LANGUAGE, null); + } + } + + public static class Video { + public static final String ASPECT = "aspect"; + public static final String CODEC = "codec"; + public static final String DURATION = "duration"; + public static final String HEIGHT = "height"; + public static final String WIDTH = "width"; + + public final double aspect; + public final String codec; + public final int duration; + public final int height; + public final int width; + + public Video(JsonNode node) { + aspect = JsonUtils.doubleFromJsonNode(node, ASPECT, 0); + codec = JsonUtils.stringFromJsonNode(node, CODEC, null); + duration = JsonUtils.intFromJsonNode(node, DURATION, -1); + height = JsonUtils.intFromJsonNode(node, HEIGHT, -1); + width = JsonUtils.intFromJsonNode(node, WIDTH, -1); + } + } + + public static final String AUDIO = "audio"; + public static final String SUBTITLE = "subtitle"; + public static final String VIDEO = "video"; + + // class members + public final List

Set the current page of both the ViewPager and indicator.

+ *

+ *

This must be used if you need to set the page before the views are drawn + * on screen (e.g., default start page).

+ * + * @param item + */ + void setCurrentItem(int item); + + /** + * Set a page change listener which will receive forwarded events. + * + * @param listener + */ + void setOnPageChangeListener(ViewPager.OnPageChangeListener listener); + + /** + * Notify the indicator that the fragment list has changed. + */ + void notifyDataSetChanged(); +} diff --git a/app/src/main/java/com/syncedsynapse/kore2/utils/BasicAuthPicassoDownloader.java b/app/src/main/java/com/syncedsynapse/kore2/utils/BasicAuthPicassoDownloader.java new file mode 100644 index 0000000..99549d2 --- /dev/null +++ b/app/src/main/java/com/syncedsynapse/kore2/utils/BasicAuthPicassoDownloader.java @@ -0,0 +1,57 @@ +/* + * 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 com.syncedsynapse.kore2.utils; + +import android.text.TextUtils; +import android.util.Base64; + +import com.squareup.picasso.UrlConnectionDownloader; + +import java.net.HttpURLConnection; + +/** + * Picasso Downloader that sets basic authentication in the headers + */ +public class BasicAuthPicassoDownloader extends UrlConnectionDownloader { + + protected final String username; + protected final String password; + + public BasicAuthPicassoDownloader(android.content.Context context) { + super(context); + this.username = null; + this.password = null; + } + + public BasicAuthPicassoDownloader(android.content.Context context, String username, String password) { + super(context); + this.username = username; + this.password = password; + } + + @Override + protected HttpURLConnection openConnection(android.net.Uri uri) + throws java.io.IOException { + HttpURLConnection urlConnection = super.openConnection(uri); + + if (!TextUtils.isEmpty(username) && !TextUtils.isEmpty(password)) { + String creds = username + ":" + password; + urlConnection.setRequestProperty("Authorization", "Basic " + + Base64.encodeToString(creds.getBytes(), Base64.NO_WRAP)); + } + return urlConnection; + } +} diff --git a/app/src/main/java/com/syncedsynapse/kore2/utils/CharacterDrawable.java b/app/src/main/java/com/syncedsynapse/kore2/utils/CharacterDrawable.java new file mode 100644 index 0000000..b70e341 --- /dev/null +++ b/app/src/main/java/com/syncedsynapse/kore2/utils/CharacterDrawable.java @@ -0,0 +1,99 @@ +/* + * 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 com.syncedsynapse.kore2.utils; + +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.ColorFilter; +import android.graphics.Paint; +import android.graphics.PixelFormat; +import android.graphics.Typeface; +import android.graphics.drawable.ColorDrawable; + +public class CharacterDrawable extends ColorDrawable { + private final char character; + private final Paint textPaint; +// private final Paint borderPaint; +// private static final int STROKE_WIDTH = 10; +// private static final float SHADE_FACTOR = 0.9f; + + private static final Typeface typeface; + static { + if (Utils.isJellybeanMR1OrLater()) { + typeface = Typeface.create("sans-serif-thin", Typeface.NORMAL); + } else if (Utils.isJellybeanOrLater()) { + typeface = Typeface.create("sans-serif-light", Typeface.NORMAL); + } else { + typeface = Typeface.create("sans-serif", Typeface.NORMAL); + } + } + + public CharacterDrawable(char character, int color) { + super(color); + this.character = character; + this.textPaint = new Paint(); +// this.borderPaint = new Paint(); + + // text paint settings + textPaint.setColor(Color.WHITE); + textPaint.setAntiAlias(true); + textPaint.setFakeBoldText(false); + + textPaint.setStyle(Paint.Style.FILL); + textPaint.setTextAlign(Paint.Align.CENTER); + textPaint.setTypeface(typeface); + + // border paint settings +// borderPaint.setColor(getDarkerShade(color)); +// borderPaint.setStyle(Paint.Style.STROKE); +// borderPaint.setStrokeWidth(STROKE_WIDTH); + } + +// private int getDarkerShade(int color) { +// return Color.rgb((int)(SHADE_FACTOR * Color.red(color)), +// (int)(SHADE_FACTOR * Color.green(color)), +// (int)(SHADE_FACTOR * Color.blue(color))); +// } + + @Override + public void draw(Canvas canvas) { + super.draw(canvas); + + // draw border +// canvas.drawRect(getBounds(), borderPaint); + + // draw text + int width = canvas.getWidth(); + int height = canvas.getHeight(); + textPaint.setTextSize(height / 2); + canvas.drawText(String.valueOf(character), width/2, height/2 - ((textPaint.descent() + textPaint.ascent()) / 2) , textPaint); + } + + @Override + public void setAlpha(int alpha) { + textPaint.setAlpha(alpha); + } + + @Override + public void setColorFilter(ColorFilter cf) { + textPaint.setColorFilter(cf); + } + + @Override + public int getOpacity() { + return PixelFormat.TRANSLUCENT; + } +} \ No newline at end of file diff --git a/app/src/main/java/com/syncedsynapse/kore2/utils/FileDownloadHelper.java b/app/src/main/java/com/syncedsynapse/kore2/utils/FileDownloadHelper.java new file mode 100644 index 0000000..9e5ab7f --- /dev/null +++ b/app/src/main/java/com/syncedsynapse/kore2/utils/FileDownloadHelper.java @@ -0,0 +1,397 @@ +/* + * 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 com.syncedsynapse.kore2.utils; + +import android.app.DownloadManager; +import android.content.Context; +import android.net.Uri; +import android.os.Environment; +import android.os.Handler; +import android.text.TextUtils; +import android.util.Base64; +import android.widget.Toast; + +import com.syncedsynapse.kore2.R; +import com.syncedsynapse.kore2.host.HostInfo; +import com.syncedsynapse.kore2.jsonrpc.ApiCallback; +import com.syncedsynapse.kore2.jsonrpc.HostConnection; +import com.syncedsynapse.kore2.jsonrpc.method.Files; +import com.syncedsynapse.kore2.jsonrpc.method.JSONRPC; +import com.syncedsynapse.kore2.jsonrpc.type.FilesType; + +import java.io.File; +import java.util.List; + +/** + * Various methods to help with file downloading + */ +public class FileDownloadHelper { + private static final String TAG = LogUtils.makeLogTag(FileDownloadHelper.class); + + public static final int OVERWRITE_FILES = 0, + DOWNLOAD_WITH_NEW_NAME = 1; + + public static abstract class MediaInfo { + public final String fileName; + + public MediaInfo(final String fileName) { + this.fileName = fileName; + } + + /** + * Check whether the directory on which to load the file exists + * @return Whether the directory exists + */ + public boolean downloadDirectoryExists() { + File file = new File(getAbsoluteDirectoryPath()); +// LogUtils.LOGD(TAG, "Checking directory: " + file.getPath()); +// LogUtils.LOGD(TAG, "Exists: " + file.exists()); + return file.exists(); + } + + /** + * Check whether the file to download already exists + * @return Whether the file exists + */ + public boolean downloadFileExists() { + if (!downloadDirectoryExists()) + return false; + File file = new File(getAbsoluteFilePath()); + return file.exists(); + } + + public String getAbsoluteDirectoryPath() { + File externalFilesDir = Environment.getExternalStoragePublicDirectory(getExternalPublicDirType()); + return externalFilesDir.getPath() + "/" + getRelativeDirectoryPath(); + + } + + public String getAbsoluteFilePath() { + return getAbsoluteDirectoryPath() + "/" + getDownloadFileName(); + } + + public String getRelativeFilePath() { + return getRelativeDirectoryPath() + "/" + getDownloadFileName(); + } + + public abstract String getExternalPublicDirType(); + public abstract String getRelativeDirectoryPath(); + public abstract String getDownloadFileName(); + + public abstract String getDownloadTitle(Context context); + public String getDownloadDescrition(Context context) { + return context.getString(R.string.download_file_description); + } + } + + /** + * Info for downloading songs + */ + public static class SongInfo extends MediaInfo { + public final String artist; + public final String album; + public final int songId; + public final int track; + public final String title; + + public SongInfo(final String artist, final String album, + final int songId, final int track, final String title, + final String fileName) { + super(fileName); + this.artist = artist; + this.album = album; + this.songId = songId; + this.track = track; + this.title = title; + } + + public String getRelativeDirectoryPath() { + return (TextUtils.isEmpty(album) || TextUtils.isEmpty(artist)) ? + null : artist + "/" + album; + } + + public String getDownloadFileName() { + String ext = getFilenameExtension(fileName); + return (ext != null) ? + String.valueOf(track) + " - " + title + ext : + null; + } + + public String getExternalPublicDirType() { + return Environment.DIRECTORY_MUSIC; + } + + public String getDownloadTitle(Context context) { + return title; + } + } + + /** + * Info for downloading movies + */ + public static class MovieInfo extends MediaInfo { + public final String title; + + public MovieInfo(final String title, final String fileName) { + super(fileName); + this.title = title; + } + + public String getRelativeDirectoryPath() { + return (TextUtils.isEmpty(title)) ? + null : title; + } + + public String getDownloadFileName() { + String ext = getFilenameExtension(fileName); + return (ext != null) ? + title + ext : null; + } + + public String getExternalPublicDirType() { + return Environment.DIRECTORY_MOVIES; + } + + public String getDownloadTitle(Context context) { + return title; + } + } + + /** + * Info for downloading TVShows + */ + public static class TVShowInfo extends MediaInfo { + public final String tvshowTitle; + public final int season; + public final int episodeNumber; + public final String title; + + public TVShowInfo(final String tvshowTitle, final int season, + final int episodeNumber, final String title, + final String fileName) { + super(fileName); + this.tvshowTitle = tvshowTitle; + this.season = season; + this.episodeNumber = episodeNumber; + this.title = title; + } + + public String getRelativeDirectoryPath() { + if (season > 0) { + return (TextUtils.isEmpty(tvshowTitle)) ? + null : tvshowTitle + "/Season" + String.valueOf(season); + } else { + return (TextUtils.isEmpty(tvshowTitle)) ? + null : tvshowTitle; + } + } + + public String getDownloadFileName() { + String ext = getFilenameExtension(fileName); + return (ext != null) ? + String.valueOf(episodeNumber) + " - " + title + ext : null; + } + + public String getExternalPublicDirType() { + return Environment.DIRECTORY_MOVIES; + } + + public String getDownloadTitle(Context context) { + return title; + } + } + + /** + * Info for downloading music videos + */ + public static class MusicVideoInfo extends MediaInfo { + public final String title; + + private static final String SUBDIRECTORY = "Music Videos"; + + public MusicVideoInfo(final String title, final String fileName) { + super(fileName); + this.title = title; + } + + public String getRelativeDirectoryPath() { + return (TextUtils.isEmpty(title)) ? + null : SUBDIRECTORY + "/" + title; + } + + public String getDownloadFileName() { + String ext = getFilenameExtension(fileName); + return (ext != null) ? + title + ext : null; + } + + public String getExternalPublicDirType() { + return Environment.DIRECTORY_MUSIC; + } + + public String getDownloadTitle(Context context) { + return title; + } + } + + /** + * Auxiliary method to get a filename extension, assuming the filename ends with .ext + * @param filename File name + * @return Extension if present, or null + */ + public static String getFilenameExtension(String filename) { + int idx = filename.lastIndexOf("."); + return (idx > 0) ? filename.substring(idx) : null; + } + + public static void downloadFiles(final Context context, final HostInfo hostInfo, + final MediaInfo mediaInfo, + final int fileHandlingMode, + final Handler callbackHandler) { + if (mediaInfo == null) + return; + + if (!checkDownloadDir(context, mediaInfo.getAbsoluteDirectoryPath())) + return; + + // Check if we are connected to the host + final HostConnection httpHostConnection = new HostConnection(hostInfo); + httpHostConnection.setProtocol(HostConnection.PROTOCOL_HTTP); + + JSONRPC.Ping action = new JSONRPC.Ping(); + action.execute(httpHostConnection, new ApiCallback() { + @Override + public void onSucess(String result) { + // Ok, continue, iterate through the song list and launch a download for each + final DownloadManager downloadManager = (DownloadManager)context.getSystemService(Context.DOWNLOAD_SERVICE); + + downloadSingleFile(context, httpHostConnection, hostInfo, + mediaInfo, fileHandlingMode, downloadManager, callbackHandler); + } + + @Override + public void onError(int errorCode, String description) { + Toast.makeText(context, R.string.unable_to_connect_to_xbmc, Toast.LENGTH_SHORT) + .show(); + } + }, callbackHandler); + } + + public static void downloadFiles(final Context context, final HostInfo hostInfo, + final List mediaInfoList, + final int fileHandlingMode, + final Handler callbackHandler) { + if ((mediaInfoList == null) || (mediaInfoList.size() == 0)) + return; + + if (!checkDownloadDir(context, mediaInfoList.get(0).getAbsoluteDirectoryPath())) + return; + + // Check if we are connected to the host + final HostConnection httpHostConnection = new HostConnection(hostInfo); + httpHostConnection.setProtocol(HostConnection.PROTOCOL_HTTP); + + JSONRPC.Ping action = new JSONRPC.Ping(); + action.execute(httpHostConnection, new ApiCallback() { + @Override + public void onSucess(String result) { + // Ok, continue, iterate through the song list and launch a download for each + final DownloadManager downloadManager = (DownloadManager)context.getSystemService(Context.DOWNLOAD_SERVICE); + + for (final MediaInfo mediaInfo : mediaInfoList) { + downloadSingleFile(context, httpHostConnection, hostInfo, + mediaInfo, fileHandlingMode, downloadManager, callbackHandler); + } + } + + @Override + public void onError(int errorCode, String description) { + Toast.makeText(context, R.string.unable_to_connect_to_xbmc, Toast.LENGTH_SHORT) + .show(); + } + }, callbackHandler); + } + + private static boolean checkDownloadDir(Context context, String downloadDirPath) { + File downloadDir = new File(downloadDirPath); + if ((downloadDir.exists() && !downloadDir.isDirectory())) { + Toast.makeText(context, + "Download directory already exists and is not a directory.", + Toast.LENGTH_SHORT) + .show(); + return false; + } + if (!downloadDir.isDirectory() && !downloadDir.mkdirs()) { + Toast.makeText(context, + "Couldn't create download directory: " + downloadDir.getPath(), + Toast.LENGTH_SHORT) + .show(); + return false; + } + return true; + } + + private static void downloadSingleFile(final Context context, + final HostConnection httpHostConnection, + final HostInfo hostInfo, + final MediaInfo mediaInfo, + final int fileHandlingMode, + final DownloadManager downloadManager, + final Handler callbackHandler) { + Files.PrepareDownload action = new Files.PrepareDownload(mediaInfo.fileName); + action.execute(httpHostConnection, new ApiCallback() { + @Override + public void onSucess(FilesType.PrepareDownloadReturnType result) { + // If the file exists and it's to be overwritten, delete it, + // as the DownloadManager always creates a new name + if (fileHandlingMode == OVERWRITE_FILES) { + File file = new File(mediaInfo.getAbsoluteFilePath()); + if (file.exists()) { + file.delete(); + } + } + + // Ok, we got the path, invoke downloader + Uri uri = Uri.parse(hostInfo.getHttpURL() + "/" + result.path); + DownloadManager.Request request = new DownloadManager.Request(uri); + // http basic authorization + if ((hostInfo.getUsername() != null) && !hostInfo.getUsername().isEmpty() && + (hostInfo.getPassword() != null) && !hostInfo.getPassword().isEmpty()) { + final String token = Base64.encodeToString((hostInfo.getUsername() + ":" + + hostInfo.getPassword()).getBytes(), Base64.DEFAULT); + request.addRequestHeader("Authorization", "Basic " + token); + } + request.allowScanningByMediaScanner(); + request.setAllowedNetworkTypes(DownloadManager.Request.NETWORK_WIFI); + request.setTitle(mediaInfo.getDownloadTitle(context)); + request.setDescription(mediaInfo.getDownloadDescrition(context)); + + request.setDestinationInExternalPublicDir(mediaInfo.getExternalPublicDirType(), + mediaInfo.getRelativeFilePath()); + downloadManager.enqueue(request); + } + + @Override + public void onError(int errorCode, String description) { + Toast.makeText(context, + String.format(context.getString(R.string.error_getting_file_information), + mediaInfo.getDownloadTitle(context)), + Toast.LENGTH_SHORT) + .show(); + } + }, callbackHandler); + } +} diff --git a/app/src/main/java/com/syncedsynapse/kore2/utils/JsonUtils.java b/app/src/main/java/com/syncedsynapse/kore2/utils/JsonUtils.java new file mode 100644 index 0000000..03acd28 --- /dev/null +++ b/app/src/main/java/com/syncedsynapse/kore2/utils/JsonUtils.java @@ -0,0 +1,106 @@ +/* + * 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 com.syncedsynapse.kore2.utils; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ArrayNode; + +import java.util.ArrayList; +import java.util.List; + +/** + * Misc util methods for use with JSON + */ +public class JsonUtils { + + public static String stringFromJsonNode(JsonNode node, String key) { + // Duplicate code for performance resons + if (node == null) return null; + JsonNode value = node.get(key); + if (value == null) return null; + return value.textValue(); + } + + public static String stringFromJsonNode(JsonNode node, String key, String defaultValue) { + if (node == null) return defaultValue; + JsonNode value = node.get(key); + if (value == null) return defaultValue; + return value.textValue(); + } + + public static double doubleFromJsonNode(JsonNode node, String key) { + return doubleFromJsonNode(node, key, 0); + } + + public static double doubleFromJsonNode(JsonNode node, String key, double defaultValue) { + if (node == null) return defaultValue; + JsonNode value = node.get(key); + if (value == null) return defaultValue; + return value.asDouble(); + } + + public static int intFromJsonNode(JsonNode node, String key) { + // Duplicate code for performance resons + if (node == null) return 0; + JsonNode value = node.get(key); + if (value == null) return 0; + return value.asInt(); + } + + public static int intFromJsonNode(JsonNode node, String key, int defaultValue) { + if (node == null) return defaultValue; + JsonNode value = node.get(key); + if (value == null) return defaultValue; + return value.asInt(); + } + + public static boolean booleanFromJsonNode(JsonNode node, String key) { + return booleanFromJsonNode(node, key, false); + } + + public static boolean booleanFromJsonNode(JsonNode node, String key, boolean defaultValue) { + if (node == null) return defaultValue; + JsonNode value = node.get(key); + if (value == null) return defaultValue; + return value.asBoolean(); + } + + public static List stringListFromJsonNode(JsonNode node, String key) { + if (node == null) return new ArrayList(0); + JsonNode value = node.get(key); + if (value == null) return new ArrayList(0); + + ArrayNode arrayNode = (ArrayNode)value; + ArrayList result = new ArrayList(arrayNode.size()); + for (JsonNode innerNode : arrayNode) { + result.add(innerNode.textValue()); + } + return result; + } + + public static List integerListFromJsonNode(JsonNode node, String key) { + if (node == null) return new ArrayList(0); + JsonNode value = node.get(key); + if (value == null) return new ArrayList(0); + + ArrayNode arrayNode = (ArrayNode)value; + ArrayList result = new ArrayList(arrayNode.size()); + for (JsonNode innerNode : arrayNode) { + result.add(innerNode.asInt()); + } + return result; + } +} diff --git a/app/src/main/java/com/syncedsynapse/kore2/utils/LogUtils.java b/app/src/main/java/com/syncedsynapse/kore2/utils/LogUtils.java new file mode 100644 index 0000000..22e3409 --- /dev/null +++ b/app/src/main/java/com/syncedsynapse/kore2/utils/LogUtils.java @@ -0,0 +1,121 @@ +/* + * Copyright 2012 Google Inc. + * + * 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 com.syncedsynapse.kore2.utils; + +import com.syncedsynapse.kore2.BuildConfig; +import com.syncedsynapse.kore2.host.HostConnectionObserver; +import com.syncedsynapse.kore2.jsonrpc.HostConnection; + +import android.util.Log; + +import java.util.Arrays; +import java.util.List; + +/** + * Log utils shamelessly ripped from Google's iosched app... + */ +public class LogUtils { + private static final int MAX_LOG_TAG_LENGTH = 23; + + // TODO: Remove this later + private static final List doNotLogTags = Arrays.asList( + HostConnection.TAG, + HostConnectionObserver.TAG + ); + + public static String makeLogTag(String str) { + if (str.length() > MAX_LOG_TAG_LENGTH) { + return str.substring(0, MAX_LOG_TAG_LENGTH - 1); + } + return str; + } + + /** + * Don't use this when obfuscating class names! + */ + public static String makeLogTag(Class cls) { + return makeLogTag(cls.getSimpleName()); + } + + public static void LOGD(final String tag, String message) { + //noinspection PointlessBooleanExpression,ConstantConditions + if ((BuildConfig.DEBUG && !doNotLogTags.contains(tag)) || + Log.isLoggable(tag, Log.DEBUG)) { + Log.d(tag, message); + } + //Log.d(tag, message); + } + + public static void LOGD(final String tag, String message, Throwable cause) { + //noinspection PointlessBooleanExpression,ConstantConditions + if (BuildConfig.DEBUG || Log.isLoggable(tag, Log.DEBUG)) { + Log.d(tag, message, cause); + } + } + + public static void LOGD_FULL(final String tag, String message) { + if (BuildConfig.DEBUG || Log.isLoggable(tag, Log.DEBUG)) { + for (int i = 0; i < message.length(); i += 1024) { + if (i + 1024 < message.length()) + LOGD(tag, message.substring(i, i + 1024)); + else + LOGD(tag, message.substring(i, message.length())); + } + } + } + + public static void LOGV(final String tag, String message) { + //noinspection PointlessBooleanExpression,ConstantConditions + if (BuildConfig.DEBUG && Log.isLoggable(tag, Log.VERBOSE)) { + Log.v(tag, message); + } + } + + public static void LOGV(final String tag, String message, Throwable cause) { + //noinspection PointlessBooleanExpression,ConstantConditions + if (BuildConfig.DEBUG && Log.isLoggable(tag, Log.VERBOSE)) { + Log.v(tag, message, cause); + } + } + + public static void LOGI(final String tag, String message) { + Log.i(tag, message); + } + + public static void LOGI(final String tag, String message, Throwable cause) { + Log.i(tag, message, cause); + } + + public static void LOGW(final String tag, String message) { + Log.w(tag, message); + } + + public static void LOGW(final String tag, String message, Throwable cause) { + Log.w(tag, message, cause); + } + + public static void LOGE(final String tag, String message) { + Log.e(tag, message); + } + + public static void LOGE(final String tag, String message, Throwable cause) { + Log.e(tag, message, cause); + } + + private LogUtils() { + } +} diff --git a/app/src/main/java/com/syncedsynapse/kore2/utils/NetUtils.java b/app/src/main/java/com/syncedsynapse/kore2/utils/NetUtils.java new file mode 100644 index 0000000..e5e73b7 --- /dev/null +++ b/app/src/main/java/com/syncedsynapse/kore2/utils/NetUtils.java @@ -0,0 +1,165 @@ +/* + * 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 com.syncedsynapse.kore2.utils; + +import java.io.BufferedReader; +import java.io.FileReader; +import java.io.IOException; +import java.net.DatagramPacket; +import java.net.DatagramSocket; +import java.net.InetAddress; +import java.net.UnknownHostException; + +/** + * Various utilities related to networking + */ +public class NetUtils { + private static final String TAG = LogUtils.makeLogTag(NetUtils.class); + + /** + * Convert a IPv4 address from an integer to an InetAddress. + * @param hostAddress an int corresponding to the IPv4 address in network byte order + */ + public static InetAddress intToInetAddress(int hostAddress) { + if (hostAddress == 0) + return null; + + byte[] addressBytes = { (byte)(0xff & hostAddress), + (byte)(0xff & (hostAddress >> 8)), + (byte)(0xff & (hostAddress >> 16)), + (byte)(0xff & (hostAddress >> 24)) }; + + try { + return InetAddress.getByAddress(addressBytes); + } catch (UnknownHostException e) { + throw new AssertionError(); + } + } + + /** + * Tries to return the MAC address of a host on the same subnet by looking at the ARP cache.. + * Note: This is a synchronous call, so it should only be called on a background thread + * + * @param hostAddress Hostname or IP address + * @return MAC address if found or null + */ + public static String getMacAddress(String hostAddress) { + String ipHostAddress; + LogUtils.LOGD(TAG, "Starting get Mac Address for: " + hostAddress); + try { + InetAddress inet = InetAddress.getByName(hostAddress); + + // Send some traffic, with a timeout + boolean reachable = inet.isReachable(1000); + + ipHostAddress = inet.getHostAddress(); + } catch (UnknownHostException e) { + LogUtils.LOGD(TAG, "Got an UnknownHostException for host: " + hostAddress, e); + return null; + } catch (IOException e) { + LogUtils.LOGD(TAG, "Couldn't check reachability of host: " + hostAddress, e); + return null; + } + + try { + // Read the arp cache + BufferedReader br = new BufferedReader(new FileReader("/proc/net/arp")); + + String arpLine; + while ((arpLine = br.readLine()) != null) { + if (arpLine.startsWith(ipHostAddress)) { + // Ok, this is the line, get the MAC Address + br.close(); + return arpLine.split("\\s+")[3].toUpperCase(); // 4th element + } + } + br.close(); + } catch (IOException e) { + LogUtils.LOGD(TAG, "Couldn check ARP cache.", e); + } + return null; + } + + /** + * Sends a Wake On Lan magic packet to a host + * Note: This is a synchronous call, so it should only be called on a background thread + * + * @param macAddress MAC address + * @param hostAddress Hostname or IP address + * @param port Port for Wake On Lan + * @return Whether the packet was successfully sent + */ + public static boolean sendWolMagicPacket(String macAddress, String hostAddress, int port) { + if (macAddress == null) { + return false; + } + + // Get MAC adress bytes + byte[] macAddressBytes = new byte[6]; + String[] hex = macAddress.split("(\\:|\\-)"); + if (hex.length != 6) { + LogUtils.LOGD(TAG, "Send magic packet: got an invalid MAC address: " + macAddress); + return false; + } + + try { + for (int i = 0; i < 6; i++) { + macAddressBytes[i] = (byte)Integer.parseInt(hex[i], 16); + } + } + catch (NumberFormatException e) { + LogUtils.LOGD(TAG, "Send magic packet: got an invalid MAC address: " + macAddress); + return false; + } + + byte[] bytes = new byte[6 + 16 * macAddressBytes.length]; + for (int i = 0; i < 6; i++) { + bytes[i] = (byte)0xff; + } + for (int i = 6; i < bytes.length; i += macAddressBytes.length) { + System.arraycopy(macAddressBytes, 0, bytes, i, macAddressBytes.length); + } + + try { + InetAddress address = InetAddress.getByName(hostAddress); + DatagramPacket packet = new DatagramPacket(bytes, bytes.length, address, port); + DatagramSocket socket = new DatagramSocket(); + socket.send(packet); + socket.close(); + } catch (IOException e) { + LogUtils.LOGD(TAG, "Exception while sending magic packet.", e); + return false; + } + return true; + } + + private static byte[] getMacBytes(String macStr) throws IllegalArgumentException { + byte[] bytes = new byte[6]; + String[] hex = macStr.split("(\\:|\\-)"); + if (hex.length != 6) { + throw new IllegalArgumentException("Invalid MAC address."); + } + try { + for (int i = 0; i < 6; i++) { + bytes[i] = (byte) Integer.parseInt(hex[i], 16); + } + } + catch (NumberFormatException e) { + throw new IllegalArgumentException("Invalid hex digit in MAC address."); + } + return bytes; + } +} diff --git a/app/src/main/java/com/syncedsynapse/kore2/utils/RepeatListener.java b/app/src/main/java/com/syncedsynapse/kore2/utils/RepeatListener.java new file mode 100644 index 0000000..ae3278f --- /dev/null +++ b/app/src/main/java/com/syncedsynapse/kore2/utils/RepeatListener.java @@ -0,0 +1,134 @@ +/* + * 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 com.syncedsynapse.kore2.utils; + +import android.os.Handler; +import android.view.MotionEvent; +import android.view.SoundEffectConstants; +import android.view.View; +import android.view.animation.Animation; +import android.widget.Button; +import android.widget.ImageButton; + +/** + * A class, that can be used as a TouchListener on any view (e.g. a Button). + * It cyclically runs a clickListener, emulating keyboard-like behaviour. First + * click is fired immediately, next after initialInterval, and subsequent after + * repeatInterval. + * + *

Interval is scheduled after the onClick completes, so it has to run fast. + * If it runs slow, it does not generate skipped onClicks. + */ +public class RepeatListener implements View.OnTouchListener { + private static final String TAG = LogUtils.makeLogTag(RepeatListener.class); + + private static Handler repeatHandler = new Handler(); + + private int initialInterval; + private final int repeatInterval; + private final View.OnClickListener clickListener; + + private Runnable handlerRunnable = new Runnable() { + @Override + public void run() { + if (downView.isShown()) { + if (repeatInterval >= 0) { + repeatHandler.postDelayed(this, repeatInterval); + } + clickListener.onClick(downView); + } + } + }; + + /** + * Animations for down/up + */ + private Animation animDown; + private Animation animUp; + + private View downView; + + /** + * Constructor for a repeat listener + * + * @param initialInterval The interval after first click event + * @param repeatInterval The interval after second and subsequent click events + * @param clickListener The OnClickListener, that will be called periodically + */ + public RepeatListener(int initialInterval, int repeatInterval, View.OnClickListener clickListener) { + this(initialInterval, repeatInterval, clickListener, null, null); + } + + /** + * Constructor for a repeat listener, with animation + * + * @param initialInterval The interval after first click event. If negative, no repeat will occur + * @param repeatInterval The interval after second and subsequent click events. If negative, no repeat will occur + * @param clickListener The OnClickListener, that will be called periodically + * @param animDown Animation to play on touch + * @param animUp Animation to play on release + */ + public RepeatListener(int initialInterval, int repeatInterval, View.OnClickListener clickListener, + Animation animDown, Animation animUp) { + this.initialInterval = initialInterval; + this.repeatInterval = repeatInterval; + this.clickListener = clickListener; + + this.animDown = animDown; + this.animUp = animUp; + } + + /** + * Handle touch events. + * + * Note: For buttons, this event Handler returns false, so that the other event handlers + * of buttons get called. For other views this event Handler consumes the event + * @param view + * @param motionEvent + * @return + */ + public boolean onTouch(View view, MotionEvent motionEvent) { + switch (motionEvent.getAction()) { + case MotionEvent.ACTION_DOWN: + repeatHandler.removeCallbacks(handlerRunnable); + if (initialInterval >= 0) { + repeatHandler.postDelayed(handlerRunnable, initialInterval); + } + downView = view; + + if (animDown != null) { + animDown.setFillAfter(true); + view.startAnimation(animDown); + } + break; + case MotionEvent.ACTION_UP: + clickListener.onClick(view); + view.playSoundEffect(SoundEffectConstants.CLICK); + // Fallthrough + case MotionEvent.ACTION_CANCEL: + repeatHandler.removeCallbacks(handlerRunnable); + downView = null; + + if (animUp != null) { + view.startAnimation(animUp); + } + break; + } + // Consume the event for views other than buttons + return !((view instanceof Button) || (view instanceof ImageButton)); + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/syncedsynapse/kore2/utils/SelectionBuilder.java b/app/src/main/java/com/syncedsynapse/kore2/utils/SelectionBuilder.java new file mode 100644 index 0000000..50334ec --- /dev/null +++ b/app/src/main/java/com/syncedsynapse/kore2/utils/SelectionBuilder.java @@ -0,0 +1,173 @@ +/* + * Copyright 2012 Google Inc. + * + * 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. + */ + +/* + * Modifications: + * -Imported from AOSP frameworks/base/core/java/com/android/internal/content + * -Changed package name + */ + +package com.syncedsynapse.kore2.utils; + +import android.content.ContentValues; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import android.text.TextUtils; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +/** + * Helper for building selection clauses for {@link android.database.sqlite.SQLiteDatabase}. Each + * appended clause is combined using {@code AND}. This class is not + * thread safe. + */ +public class SelectionBuilder { + private static final String TAG = LogUtils.makeLogTag(SelectionBuilder.class); + + private String mTable = null; + private Map mProjectionMap = new HashMap(); + private StringBuilder mSelection = new StringBuilder(); + private ArrayList mSelectionArgs = new ArrayList(); + + /** + * Reset any internal state, allowing this builder to be recycled. + */ + public SelectionBuilder reset() { + mTable = null; + mSelection.setLength(0); + mSelectionArgs.clear(); + return this; + } + + /** + * Append the given selection clause to the internal state. Each clause is + * surrounded with parenthesis and combined using {@code AND}. + */ + public SelectionBuilder where(String selection, String... selectionArgs) { + if (TextUtils.isEmpty(selection)) { + if (selectionArgs != null && selectionArgs.length > 0) { + throw new IllegalArgumentException( + "Valid selection required when including arguments="); + } + + // Shortcut when clause is empty + return this; + } + + if (mSelection.length() > 0) { + mSelection.append(" AND "); + } + + mSelection.append("(").append(selection).append(")"); + if (selectionArgs != null) { + Collections.addAll(mSelectionArgs, selectionArgs); + } + + return this; + } + + public SelectionBuilder table(String table) { + mTable = table; + return this; + } + + private void assertTable() { + if (mTable == null) { + throw new IllegalStateException("Table not specified"); + } + } + + public SelectionBuilder mapToTable(String column, String table) { + mProjectionMap.put(column, table + "." + column); + return this; + } + + public SelectionBuilder map(String fromColumn, String toClause) { + mProjectionMap.put(fromColumn, toClause + " AS " + fromColumn); + return this; + } + + /** + * Return selection string for current internal state. + * + * @see #getSelectionArgs() + */ + public String getSelection() { + return mSelection.toString(); + } + + /** + * Return selection arguments for current internal state. + * + * @see #getSelection() + */ + public String[] getSelectionArgs() { + return mSelectionArgs.toArray(new String[mSelectionArgs.size()]); + } + + private void mapColumns(String[] columns) { + for (int i = 0; i < columns.length; i++) { + final String target = mProjectionMap.get(columns[i]); + if (target != null) { + columns[i] = target; + } + } + } + + @Override + public String toString() { + return "SelectionBuilder[table=" + mTable + ", selection=" + getSelection() + + ", selectionArgs=" + Arrays.toString(getSelectionArgs()) + "]"; + } + + /** + * Execute query using the current internal state as {@code WHERE} clause. + */ + public Cursor query(SQLiteDatabase db, String[] columns, String orderBy) { + return query(db, columns, null, null, orderBy, null); + } + + /** + * Execute query using the current internal state as {@code WHERE} clause. + */ + public Cursor query(SQLiteDatabase db, String[] columns, String groupBy, + String having, String orderBy, String limit) { + assertTable(); + if (columns != null) mapColumns(columns); + return db.query(mTable, columns, getSelection(), getSelectionArgs(), groupBy, having, + orderBy, limit); + } + + /** + * Execute update using the current internal state as {@code WHERE} clause. + */ + public int update(SQLiteDatabase db, ContentValues values) { + assertTable(); + return db.update(mTable, values, getSelection(), getSelectionArgs()); + } + + /** + * Execute delete using the current internal state as {@code WHERE} clause. + */ + public int delete(SQLiteDatabase db) { + assertTable(); + return db.delete(mTable, getSelection(), getSelectionArgs()); + } +} diff --git a/app/src/main/java/com/syncedsynapse/kore2/utils/TabsAdapter.java b/app/src/main/java/com/syncedsynapse/kore2/utils/TabsAdapter.java new file mode 100644 index 0000000..5ba1415 --- /dev/null +++ b/app/src/main/java/com/syncedsynapse/kore2/utils/TabsAdapter.java @@ -0,0 +1,85 @@ +/* + * 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 com.syncedsynapse.kore2.utils; + +import android.content.Context; +import android.os.Bundle; +import android.support.v4.app.Fragment; +import android.support.v4.app.FragmentManager; +import android.support.v4.app.FragmentPagerAdapter; + +import java.util.ArrayList; + +/** + * This is a helper class that implements the management of tabs and all + * details of connecting a ViewPager with associated TabHost. + */ +public class TabsAdapter extends FragmentPagerAdapter { + private final Context context; + private final ArrayList tabInfos; + + public static final class TabInfo { + private final Class fragmentClass; + private final Bundle args; + private final int titleRes; + private final long fragmentId; + + TabInfo(Class fragmentClass, Bundle args, int titleRes, long fragmentId) { + this.fragmentClass = fragmentClass; + this.args = args; + this.titleRes = titleRes; + this.fragmentId = fragmentId; + } + } + + public TabsAdapter(Context context, FragmentManager fragmentManager) { + super(fragmentManager); + this.context = context; + this.tabInfos = new ArrayList(); + } + + public TabsAdapter addTab(Class fragmentClass, Bundle args, int titleRes, long fragmentId) { + TabInfo info = new TabInfo(fragmentClass, args, titleRes, fragmentId); + tabInfos.add(info); + return this; + } + + @Override + public int getCount() { + return tabInfos.size(); + } + + @Override + public Fragment getItem(int position) { + TabInfo info = tabInfos.get(position); + return Fragment.instantiate(context, info.fragmentClass.getName(), info.args); + } + + @Override + public long getItemId(int position) { + return tabInfos.get(position).fragmentId; + } + + @Override + public CharSequence getPageTitle(int position) { + TabInfo tabInfo = tabInfos.get(position); + if (tabInfo != null) { +// return context.getString(tabInfo.titleRes).toUpperCase(Locale.getDefault()); + return context.getString(tabInfo.titleRes); + } + return null; + } +} diff --git a/app/src/main/java/com/syncedsynapse/kore2/utils/UIUtils.java b/app/src/main/java/com/syncedsynapse/kore2/utils/UIUtils.java new file mode 100644 index 0000000..f19e685 --- /dev/null +++ b/app/src/main/java/com/syncedsynapse/kore2/utils/UIUtils.java @@ -0,0 +1,359 @@ +/* + * 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 com.syncedsynapse.kore2.utils; + +import android.content.Context; +import android.content.res.Resources; +import android.content.res.TypedArray; +import android.text.TextUtils; +import android.util.DisplayMetrics; +import android.view.LayoutInflater; +import android.view.View; +import android.view.WindowManager; +import android.widget.GridLayout; +import android.widget.ImageView; +import android.widget.TextView; +import android.widget.Toast; + +import com.syncedsynapse.kore2.R; +import com.syncedsynapse.kore2.Settings; +import com.syncedsynapse.kore2.host.HostInfo; +import com.syncedsynapse.kore2.host.HostManager; +import com.syncedsynapse.kore2.jsonrpc.type.GlobalType; +import com.syncedsynapse.kore2.jsonrpc.type.VideoType; + +import java.util.ArrayList; +import java.util.List; + +/** + * General UI Utils + */ +public class UIUtils { + + public static final float IMAGE_RESIZE_FACTOR = 1.0f; + + public static final int initialButtonRepeatInterval = 400; // ms + public static final int buttonRepeatInterval = 80; // ms + + /** + * Formats time based on seconds + * @param seconds seconds + * @return Formated string + */ + public static String formatTime(int seconds) { + return formatTime(seconds / 3600, (seconds % 3600) / 60, (seconds % 3600) % 60); + } + + /** + * Formats time + */ + public static String formatTime(GlobalType.Time time) { + return formatTime(time.hours, time.minutes, time.seconds); + } + + /** + * Formats time + */ + public static String formatTime(int hours, int minutes, int seconds) { + if (hours > 0) { + return String.format("%d:%02d:%02d", hours, minutes, seconds); + } else { + return String.format("%1d:%02d",minutes, seconds); + } + } + + /** + * Loads an image into an imageview + * @param hostManager Hostmanager connected to the host + * @param imageUrl XBMC url of the image to load + * @param imageView Image view to load into + * @param imageWidth Width of the image, for caching purposes + * @param imageHeight Height of the image, for caching purposes + */ + public static void loadImageIntoImageview(HostManager hostManager, + String imageUrl, ImageView imageView, + int imageWidth, int imageHeight) { +// if (TextUtils.isEmpty(imageUrl)) { +// imageView.setImageResource(R.drawable.delete_ic_action_picture); +// return; +// } + + if ((imageWidth) > 0 && (imageHeight > 0)) { + hostManager.getPicasso() + .load(hostManager.getHostInfo().getImageUrl(imageUrl)) + .resize(imageWidth, imageHeight) + .centerCrop() + .into(imageView); + } else { + hostManager.getPicasso() + .load(hostManager.getHostInfo().getImageUrl(imageUrl)) + .fit() + .centerCrop() + .into(imageView); + } + } + + private static TypedArray characterAvatarColors = null; + private static int avatarColorsIdx = 0; +// private static Random randomGenerator = new Random(); + + /** + * Loads an image into an imageview, presenting an alternate charater avatar if empty + * @param hostManager Hostmanager connected to the host + * @param imageUrl XBMC url of the image to load + * @param stringAvatar Character avatar too present if image is null + * @param imageView Image view to load into + * @param imageWidth Width of the image, for caching purposes + * @param imageHeight Height of the image, for caching purposes + */ + public static void loadImageWithCharacterAvatar( + Context context, HostManager hostManager, + String imageUrl, String stringAvatar, + ImageView imageView, + int imageWidth, int imageHeight) { + // Load character avatar + if (characterAvatarColors == null) { + characterAvatarColors = context.getResources() + .obtainTypedArray(R.array.character_avatar_colors); + } + + char charAvatar = TextUtils.isEmpty(stringAvatar) ? + ' ' : stringAvatar.charAt(0); + avatarColorsIdx = Math.max( + Character.getNumericValue(stringAvatar.charAt(0)) + + Character.getNumericValue(stringAvatar.charAt(stringAvatar.length() - 1)) + + stringAvatar.length(), 0) % + characterAvatarColors.length(); + int color = characterAvatarColors.getColor(avatarColorsIdx, 0xff000000); + CharacterDrawable avatarDrawable = new CharacterDrawable(charAvatar, color); + +// avatarColorsIdx = randomGenerator.nextInt(characterAvatarColors.length()); + if (TextUtils.isEmpty(imageUrl)) { + imageView.setImageDrawable(avatarDrawable); + return; + } + + if ((imageWidth) > 0 && (imageHeight > 0)) { + hostManager.getPicasso() + .load(hostManager.getHostInfo().getImageUrl(imageUrl)) + .placeholder(avatarDrawable) + .resize(imageWidth, imageHeight) + .centerCrop() + .into(imageView); + } else { + hostManager.getPicasso() + .load(hostManager.getHostInfo().getImageUrl(imageUrl)) + .fit() + .centerCrop() + .into(imageView); + } + } + + /** + * Sets play/pause button icon on a ImageView, based on speed + * @param context Activity + * @param view ImageView/ImageButton + * @param speed Current player speed + */ + public static void setPlayPauseButtonIcon(Context context, ImageView view, int speed) { + int resAttrId = (speed == 1) ? R.attr.iconPause : R.attr.iconPlay; + int defaultResourceId = (speed == 1) ? + R.drawable.ic_pause_white_24dp : + R.drawable.ic_play_arrow_white_24dp; + + TypedArray styledAttributes = context.obtainStyledAttributes(new int[]{resAttrId}); + view.setImageResource(styledAttributes.getResourceId(0, defaultResourceId)); + styledAttributes.recycle(); + } + + /** + * Fills the standard cast info list, consisting of a {@link android.widget.GridLayout} + * with actor images and a Textview with the name and the role of the additional cast. + * The number of actor presented on the {@link android.widget.GridLayout} is controlled + * through the global setting, and only actors with images are presented. + * The rest are presented in the additionalCastView TextView + * + * @param context Activity + * @param castList Cast list + * @param castListView GridLayout on which too show actors that have images + * @param additionalCastTitleView View with additional cast title + * @param additionalCastView Additional cast + */ + public static void setupCastInfo(final Context context, + List castList, GridLayout castListView, + TextView additionalCastTitleView, TextView additionalCastView) { + HostManager hostManager = HostManager.getInstance(context); + Resources resources = context.getResources(); + DisplayMetrics displayMetrics = new DisplayMetrics(); + WindowManager windowManager = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE); + windowManager.getDefaultDisplay().getMetrics(displayMetrics); + + View.OnClickListener castListClickListener = new View.OnClickListener() { + @Override + public void onClick(View v) { + Utils.openImdbForPerson(context, (String)v.getTag()); + } + }; + + + castListView.removeAllViews(); + int numColumns = castListView.getColumnCount(); + + int layoutMarginPx = 2 * resources.getDimensionPixelSize(R.dimen.remote_content_hmargin); + int imageMarginPx = 2 * resources.getDimensionPixelSize(R.dimen.image_grid_margin); + int imageWidth = (displayMetrics.widthPixels - layoutMarginPx - numColumns * imageMarginPx) / numColumns; + int imageHeight = (int)(imageWidth * 1.2); + + List noPicturesCastList = new ArrayList(); + int maxCastPictures = Settings.getInstance(context).maxCastPictures; + int currentPictureNumber = 0; + for (int i = 0; i < castList.size(); i++) { + VideoType.Cast actor = castList.get(i); + + if (((maxCastPictures == -1) || (currentPictureNumber < maxCastPictures)) && + (actor.thumbnail != null)) { + // Present the picture + currentPictureNumber++; + View castView = LayoutInflater.from(context).inflate(R.layout.grid_item_cast, castListView, false); + ImageView castPicture = (ImageView) castView.findViewById(R.id.picture); + TextView castName = (TextView) castView.findViewById(R.id.name); + TextView castRole = (TextView) castView.findViewById(R.id.role); + + castView.getLayoutParams().width = imageWidth; + castView.getLayoutParams().height = (int) (imageHeight * 1.2); + castView.setTag(actor.name); + castView.setOnClickListener(castListClickListener); + + castName.setText(actor.name); + castRole.setText(actor.role); + UIUtils.loadImageWithCharacterAvatar(context, hostManager, + actor.thumbnail, actor.name, + castPicture, imageWidth, imageHeight); + castListView.addView(castView); + } else { + noPicturesCastList.add(actor); + } + } + + // Additional cast + if (noPicturesCastList.size() > 0) { + additionalCastTitleView.setVisibility(View.VISIBLE); + additionalCastView.setVisibility(View.VISIBLE); + StringBuilder castListText = new StringBuilder(); + boolean first = true; + for (VideoType.Cast cast : noPicturesCastList) { + if (!first) castListText.append("\n"); + first = false; + if (!TextUtils.isEmpty(cast.role)) { + castListText.append(String.format(context.getString(R.string.cast_list_text), + cast.name, cast.role)); + } else { + castListText.append(cast.name); + } + } + additionalCastView.setText(castListText); + } else { + additionalCastTitleView.setVisibility(View.GONE); + additionalCastView.setVisibility(View.GONE); + } + } + + /** + * Simple wrapper to {@link NetUtils#sendWolMagicPacket(String, String, int)} + * that sends a WoL magic packet in a new thread + * + * @param context Context + * @param hostInfo Host to send WoL + */ + public static void sendWolAsync(Context context, final HostInfo hostInfo) { + if (hostInfo == null) + return; + + // Send WoL magic packet on a new thread + new Thread(new Runnable() { + @Override + public void run() { + NetUtils.sendWolMagicPacket(hostInfo.getMacAddress(), + hostInfo.getAddress(), hostInfo.getWolPort()); + } + }).start(); + Toast.makeText(context, R.string.wol_sent, Toast.LENGTH_SHORT).show(); + } + +// /** +// * Sets the default {@link android.support.v4.widget.SwipeRefreshLayout} color scheme +// * @param swipeRefreshLayout layout +// */ +// public static void setSwipeRefreshLayoutColorScheme(SwipeRefreshLayout swipeRefreshLayout) { +// Resources.Theme theme = swipeRefreshLayout.getContext().getTheme(); +// TypedArray styledAttributes = theme.obtainStyledAttributes(new int[] { +// R.attr.refreshColor1, +// R.attr.refreshColor2, +// R.attr.refreshColor3, +// R.attr.refreshColor4, +// }); +// +// swipeRefreshLayout.setColorScheme(styledAttributes.getResourceId(0, android.R.color.holo_blue_dark), +// styledAttributes.getResourceId(1, android.R.color.holo_purple), +// styledAttributes.getResourceId(2, android.R.color.holo_red_dark), +// styledAttributes.getResourceId(3, android.R.color.holo_green_dark)); +// styledAttributes.recycle(); +// } + +// /** +// * Sets a views padding top/bottom to account for the system bars +// * (Top status and action bar, bottom nav bar, right nav bar if in ladscape mode) +// * +// * @param context Context +// * @param view View to pad +// * @param padTop Whether to set views paddingTop +// * @param padRight Whether to set views paddingRight (for nav bar in landscape mode) +// * @param padBottom Whether to set views paddingBottom +// */ +// public static void setPaddingForSystemBars(Activity context, View view, +// boolean padTop, boolean padRight, boolean padBottom) { +// //if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) return; +// SystemBarTintManager tintManager = new SystemBarTintManager(context); +// SystemBarTintManager.SystemBarConfig config = tintManager.getConfig(); +// +// view.setPadding(view.getPaddingLeft(), +// padTop ? config.getPixelInsetTop(true) : view.getPaddingTop(), +// padRight? config.getPixelInsetRight() : view.getPaddingRight(), +// padBottom ? config.getPixelInsetBottom() : view.getPaddingBottom()); +// } + + /** + * Returns a theme resource Id given the value stored in Shared Preferences + * @param prefThemeValue Shared Preferences value for the theme + * @return Android resource id of the theme + */ + public static int getThemeResourceId(String prefThemeValue) { + switch (Integer.valueOf(prefThemeValue)) { + case 0: + return R.style.NightTheme; + case 1: + return R.style.DayTheme; + case 2: + return R.style.MistTheme; + case 3: + return R.style.SolarizedLightTheme; + case 4: + return R.style.SolarizedDarkTheme; + default: + return R.style.NightTheme; + } + } +} diff --git a/app/src/main/java/com/syncedsynapse/kore2/utils/Utils.java b/app/src/main/java/com/syncedsynapse/kore2/utils/Utils.java new file mode 100644 index 0000000..ff2977d --- /dev/null +++ b/app/src/main/java/com/syncedsynapse/kore2/utils/Utils.java @@ -0,0 +1,134 @@ +/* + * 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 com.syncedsynapse.kore2.utils; + +import android.content.Context; +import android.content.Intent; +import android.net.Uri; +import android.os.Build; +import android.text.TextUtils; + +import java.util.List; + +/** + * Because every project needs one of these + * */ +public class Utils { + + /** + * Returns whether the SDK is the Jellybean release or later. + */ + public static boolean isJellybeanOrLater() { + return Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN; + } + + public static boolean isJellybeanMR1OrLater() { + return Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1; + } + + public static boolean isJellybeanMR2OrLater() { + return Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2; + } + + public static boolean isKitKatOrLater() { + return Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT; + } + + public static boolean isLollipopOrLater() { + return Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP; + } + + /** + * Concats a list of strings... + * @param list + * @param delimiter + * @return + */ + public static String listStringConcat(List list, String delimiter) { + StringBuilder builder = new StringBuilder(); + boolean first = true; + for (String item : list) { + if (TextUtils.isEmpty(item)) continue; + if (!first) builder.append(delimiter); + builder.append(item); + first = false; + } + return builder.toString(); + } + + + /** + * Calls {@link Context#startActivity(Intent)} with the given implicit {@link Intent} + * after making sure there is an Activity to handle it. + *

This may happen if e.g. the web browser has been disabled through restricted + * profiles. + * + * @return Whether there was an Activity to handle the given {@link Intent}. + */ + public static boolean tryStartActivity(Context context, Intent intent) { + if (intent.resolveActivity(context.getPackageManager()) != null) { + context.startActivity(intent); + return true; + } + return false; + } + + public static final String IMDB_APP_PERSON_SEARCH_URI = "imdb:///find?q=%s&s=nm"; + public static final String IMDB_PERSON_SEARCH_URL = "http://m.imdb.com/find?q=%s&s=nm"; + + public static final String IMDB_APP_MOVIE_URI = "imdb:///title/%s/"; + public static final String IMDB_MOVIE_URL = "http://m.imdb.com/title/%s/"; + + /** + * Open the IMDb app or web page for the given person name. + */ + public static void openImdbForPerson(Context context, String name) { + if (context == null || TextUtils.isEmpty(name)) { + return; + } + + Intent intent = new Intent(Intent.ACTION_VIEW, + Uri.parse(String.format(IMDB_APP_PERSON_SEARCH_URI, name))); + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET); + // try launching IMDb app + if (!Utils.tryStartActivity(context, intent)) { + // on failure, try launching the web page + intent = new Intent(Intent.ACTION_VIEW, Uri.parse(String.format(IMDB_PERSON_SEARCH_URL, name))); + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET); + context.startActivity(intent); + } + } + + /** + * Open the IMDb app or web page for the given person name. + */ + public static void openImdbForMovie(Context context, String imdbNumber) { + if (context == null || TextUtils.isEmpty(imdbNumber)) { + return; + } + + Intent intent = new Intent(Intent.ACTION_VIEW, + Uri.parse(String.format(IMDB_APP_MOVIE_URI, imdbNumber))); + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET); + // try launching IMDb app + if (!Utils.tryStartActivity(context, intent)) { + // on failure, try launching the web page + intent = new Intent(Intent.ACTION_VIEW, Uri.parse(String.format(IMDB_MOVIE_URL, imdbNumber))); + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET); + context.startActivity(intent); + } + } +} diff --git a/app/src/main/res/anim/activity_in.xml b/app/src/main/res/anim/activity_in.xml new file mode 100644 index 0000000..5ff440b --- /dev/null +++ b/app/src/main/res/anim/activity_in.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/anim/activity_out.xml b/app/src/main/res/anim/activity_out.xml new file mode 100644 index 0000000..82a28ce --- /dev/null +++ b/app/src/main/res/anim/activity_out.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/anim/button_in.xml b/app/src/main/res/anim/button_in.xml new file mode 100644 index 0000000..0a5e9eb --- /dev/null +++ b/app/src/main/res/anim/button_in.xml @@ -0,0 +1,33 @@ + + + + + + + + + diff --git a/app/src/main/res/anim/button_out.xml b/app/src/main/res/anim/button_out.xml new file mode 100644 index 0000000..1ac0757 --- /dev/null +++ b/app/src/main/res/anim/button_out.xml @@ -0,0 +1,33 @@ + + + + + + + + + diff --git a/app/src/main/res/anim/fragment_details_enter.xml b/app/src/main/res/anim/fragment_details_enter.xml new file mode 100644 index 0000000..5797275 --- /dev/null +++ b/app/src/main/res/anim/fragment_details_enter.xml @@ -0,0 +1,30 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/anim/fragment_list_popenter.xml b/app/src/main/res/anim/fragment_list_popenter.xml new file mode 100644 index 0000000..63d02e8 --- /dev/null +++ b/app/src/main/res/anim/fragment_list_popenter.xml @@ -0,0 +1,25 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable-sw600dp-xxhdpi/remote_back_black.png b/app/src/main/res/drawable-sw600dp-xxhdpi/remote_back_black.png new file mode 100644 index 0000000..7df8e7e Binary files /dev/null and b/app/src/main/res/drawable-sw600dp-xxhdpi/remote_back_black.png differ diff --git a/app/src/main/res/drawable-sw600dp-xxhdpi/remote_back_white.png b/app/src/main/res/drawable-sw600dp-xxhdpi/remote_back_white.png new file mode 100644 index 0000000..d2ad60b Binary files /dev/null and b/app/src/main/res/drawable-sw600dp-xxhdpi/remote_back_white.png differ diff --git a/app/src/main/res/drawable-sw600dp-xxhdpi/remote_codec_black.png b/app/src/main/res/drawable-sw600dp-xxhdpi/remote_codec_black.png new file mode 100644 index 0000000..f765cb9 Binary files /dev/null and b/app/src/main/res/drawable-sw600dp-xxhdpi/remote_codec_black.png differ diff --git a/app/src/main/res/drawable-sw600dp-xxhdpi/remote_codec_white.png b/app/src/main/res/drawable-sw600dp-xxhdpi/remote_codec_white.png new file mode 100644 index 0000000..924d987 Binary files /dev/null and b/app/src/main/res/drawable-sw600dp-xxhdpi/remote_codec_white.png differ diff --git a/app/src/main/res/drawable-sw600dp-xxhdpi/remote_down_black.png b/app/src/main/res/drawable-sw600dp-xxhdpi/remote_down_black.png new file mode 100644 index 0000000..2f3a76d Binary files /dev/null and b/app/src/main/res/drawable-sw600dp-xxhdpi/remote_down_black.png differ diff --git a/app/src/main/res/drawable-sw600dp-xxhdpi/remote_down_white.png b/app/src/main/res/drawable-sw600dp-xxhdpi/remote_down_white.png new file mode 100644 index 0000000..52cd248 Binary files /dev/null and b/app/src/main/res/drawable-sw600dp-xxhdpi/remote_down_white.png differ diff --git a/app/src/main/res/drawable-sw600dp-xxhdpi/remote_info_black.png b/app/src/main/res/drawable-sw600dp-xxhdpi/remote_info_black.png new file mode 100644 index 0000000..2afd356 Binary files /dev/null and b/app/src/main/res/drawable-sw600dp-xxhdpi/remote_info_black.png differ diff --git a/app/src/main/res/drawable-sw600dp-xxhdpi/remote_info_white.png b/app/src/main/res/drawable-sw600dp-xxhdpi/remote_info_white.png new file mode 100644 index 0000000..ab3614e Binary files /dev/null and b/app/src/main/res/drawable-sw600dp-xxhdpi/remote_info_white.png differ diff --git a/app/src/main/res/drawable-sw600dp-xxhdpi/remote_left_black.png b/app/src/main/res/drawable-sw600dp-xxhdpi/remote_left_black.png new file mode 100644 index 0000000..1ee0dd8 Binary files /dev/null and b/app/src/main/res/drawable-sw600dp-xxhdpi/remote_left_black.png differ diff --git a/app/src/main/res/drawable-sw600dp-xxhdpi/remote_left_white.png b/app/src/main/res/drawable-sw600dp-xxhdpi/remote_left_white.png new file mode 100644 index 0000000..5fdac7a Binary files /dev/null and b/app/src/main/res/drawable-sw600dp-xxhdpi/remote_left_white.png differ diff --git a/app/src/main/res/drawable-sw600dp-xxhdpi/remote_menu_black.png b/app/src/main/res/drawable-sw600dp-xxhdpi/remote_menu_black.png new file mode 100644 index 0000000..fbea1ab Binary files /dev/null and b/app/src/main/res/drawable-sw600dp-xxhdpi/remote_menu_black.png differ diff --git a/app/src/main/res/drawable-sw600dp-xxhdpi/remote_menu_white.png b/app/src/main/res/drawable-sw600dp-xxhdpi/remote_menu_white.png new file mode 100644 index 0000000..f5019ff Binary files /dev/null and b/app/src/main/res/drawable-sw600dp-xxhdpi/remote_menu_white.png differ diff --git a/app/src/main/res/drawable-sw600dp-xxhdpi/remote_right_black.png b/app/src/main/res/drawable-sw600dp-xxhdpi/remote_right_black.png new file mode 100644 index 0000000..a7ee91c Binary files /dev/null and b/app/src/main/res/drawable-sw600dp-xxhdpi/remote_right_black.png differ diff --git a/app/src/main/res/drawable-sw600dp-xxhdpi/remote_right_white.png b/app/src/main/res/drawable-sw600dp-xxhdpi/remote_right_white.png new file mode 100644 index 0000000..a0320f7 Binary files /dev/null and b/app/src/main/res/drawable-sw600dp-xxhdpi/remote_right_white.png differ diff --git a/app/src/main/res/drawable-sw600dp-xxhdpi/remote_select_black.png b/app/src/main/res/drawable-sw600dp-xxhdpi/remote_select_black.png new file mode 100644 index 0000000..e6920ff Binary files /dev/null and b/app/src/main/res/drawable-sw600dp-xxhdpi/remote_select_black.png differ diff --git a/app/src/main/res/drawable-sw600dp-xxhdpi/remote_select_white.png b/app/src/main/res/drawable-sw600dp-xxhdpi/remote_select_white.png new file mode 100644 index 0000000..5fcf535 Binary files /dev/null and b/app/src/main/res/drawable-sw600dp-xxhdpi/remote_select_white.png differ diff --git a/app/src/main/res/drawable-sw600dp-xxhdpi/remote_up_black.png b/app/src/main/res/drawable-sw600dp-xxhdpi/remote_up_black.png new file mode 100644 index 0000000..084b169 Binary files /dev/null and b/app/src/main/res/drawable-sw600dp-xxhdpi/remote_up_black.png differ diff --git a/app/src/main/res/drawable-sw600dp-xxhdpi/remote_up_white.png b/app/src/main/res/drawable-sw600dp-xxhdpi/remote_up_white.png new file mode 100644 index 0000000..2e97410 Binary files /dev/null and b/app/src/main/res/drawable-sw600dp-xxhdpi/remote_up_white.png differ diff --git a/app/src/main/res/drawable-xhdpi/drawer_image.png b/app/src/main/res/drawable-xhdpi/drawer_image.png new file mode 100644 index 0000000..bacec5e Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/drawer_image.png differ diff --git a/app/src/main/res/drawable-xhdpi/drawer_shadow.9.png b/app/src/main/res/drawable-xhdpi/drawer_shadow.9.png new file mode 100644 index 0000000..fabe9d9 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/drawer_shadow.9.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_add_box_black_24dp.png b/app/src/main/res/drawable-xhdpi/ic_add_box_black_24dp.png new file mode 100644 index 0000000..0bd6fc4 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_add_box_black_24dp.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_add_box_white_24dp.png b/app/src/main/res/drawable-xhdpi/ic_add_box_white_24dp.png new file mode 100644 index 0000000..97c73d8 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_add_box_white_24dp.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_chevron_left_black_24dp.png b/app/src/main/res/drawable-xhdpi/ic_chevron_left_black_24dp.png new file mode 100644 index 0000000..919d1b6 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_chevron_left_black_24dp.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_chevron_left_white_24dp.png b/app/src/main/res/drawable-xhdpi/ic_chevron_left_white_24dp.png new file mode 100644 index 0000000..815b155 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_chevron_left_white_24dp.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_chevron_right_black_24dp.png b/app/src/main/res/drawable-xhdpi/ic_chevron_right_black_24dp.png new file mode 100644 index 0000000..6f6ab7b Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_chevron_right_black_24dp.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_chevron_right_white_24dp.png b/app/src/main/res/drawable-xhdpi/ic_chevron_right_white_24dp.png new file mode 100644 index 0000000..9afecd2 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_chevron_right_white_24dp.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_closed_caption_black_24dp.png b/app/src/main/res/drawable-xhdpi/ic_closed_caption_black_24dp.png new file mode 100644 index 0000000..3cbd3ca Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_closed_caption_black_24dp.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_closed_caption_white_24dp.png b/app/src/main/res/drawable-xhdpi/ic_closed_caption_white_24dp.png new file mode 100644 index 0000000..9fc70f1 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_closed_caption_white_24dp.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_delete_black_24dp.png b/app/src/main/res/drawable-xhdpi/ic_delete_black_24dp.png new file mode 100644 index 0000000..ddf3102 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_delete_black_24dp.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_delete_white_24dp.png b/app/src/main/res/drawable-xhdpi/ic_delete_white_24dp.png new file mode 100644 index 0000000..cdb230c Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_delete_white_24dp.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_done_black_24dp.png b/app/src/main/res/drawable-xhdpi/ic_done_black_24dp.png new file mode 100644 index 0000000..a92d99a Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_done_black_24dp.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_done_white_24dp.png b/app/src/main/res/drawable-xhdpi/ic_done_white_24dp.png new file mode 100644 index 0000000..e502447 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_done_white_24dp.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_edit_black_24dp.png b/app/src/main/res/drawable-xhdpi/ic_edit_black_24dp.png new file mode 100644 index 0000000..b419368 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_edit_black_24dp.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_edit_white_24dp.png b/app/src/main/res/drawable-xhdpi/ic_edit_white_24dp.png new file mode 100644 index 0000000..7f0ea51 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_edit_white_24dp.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_expand_less_black_24dp.png b/app/src/main/res/drawable-xhdpi/ic_expand_less_black_24dp.png new file mode 100644 index 0000000..d34201a Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_expand_less_black_24dp.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_expand_less_white_24dp.png b/app/src/main/res/drawable-xhdpi/ic_expand_less_white_24dp.png new file mode 100644 index 0000000..55875dd Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_expand_less_white_24dp.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_expand_more_black_24dp.png b/app/src/main/res/drawable-xhdpi/ic_expand_more_black_24dp.png new file mode 100644 index 0000000..23a5b9a Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_expand_more_black_24dp.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_expand_more_white_24dp.png b/app/src/main/res/drawable-xhdpi/ic_expand_more_white_24dp.png new file mode 100644 index 0000000..c0e60c2 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_expand_more_white_24dp.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_extension_black_24dp.png b/app/src/main/res/drawable-xhdpi/ic_extension_black_24dp.png new file mode 100644 index 0000000..622595b Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_extension_black_24dp.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_extension_white_24dp.png b/app/src/main/res/drawable-xhdpi/ic_extension_white_24dp.png new file mode 100644 index 0000000..dcb9a83 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_extension_white_24dp.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_fast_forward_black_24dp.png b/app/src/main/res/drawable-xhdpi/ic_fast_forward_black_24dp.png new file mode 100644 index 0000000..697ede7 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_fast_forward_black_24dp.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_fast_forward_white_24dp.png b/app/src/main/res/drawable-xhdpi/ic_fast_forward_white_24dp.png new file mode 100644 index 0000000..b7c8605 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_fast_forward_white_24dp.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_fast_rewind_black_24dp.png b/app/src/main/res/drawable-xhdpi/ic_fast_rewind_black_24dp.png new file mode 100644 index 0000000..2f1b1ef Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_fast_rewind_black_24dp.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_fast_rewind_white_24dp.png b/app/src/main/res/drawable-xhdpi/ic_fast_rewind_white_24dp.png new file mode 100644 index 0000000..379435f Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_fast_rewind_white_24dp.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_games_black_24dp.png b/app/src/main/res/drawable-xhdpi/ic_games_black_24dp.png new file mode 100644 index 0000000..1aa3192 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_games_black_24dp.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_games_white_24dp.png b/app/src/main/res/drawable-xhdpi/ic_games_white_24dp.png new file mode 100644 index 0000000..3cf4cbf Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_games_white_24dp.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_get_app_black_24dp.png b/app/src/main/res/drawable-xhdpi/ic_get_app_black_24dp.png new file mode 100644 index 0000000..f6858d5 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_get_app_black_24dp.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_get_app_white_24dp.png b/app/src/main/res/drawable-xhdpi/ic_get_app_white_24dp.png new file mode 100644 index 0000000..990dfb8 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_get_app_white_24dp.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_headset_black_24dp.png b/app/src/main/res/drawable-xhdpi/ic_headset_black_24dp.png new file mode 100644 index 0000000..ed4b3f0 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_headset_black_24dp.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_headset_white_24dp.png b/app/src/main/res/drawable-xhdpi/ic_headset_white_24dp.png new file mode 100644 index 0000000..a5defe3 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_headset_white_24dp.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_home_black_24dp.png b/app/src/main/res/drawable-xhdpi/ic_home_black_24dp.png new file mode 100644 index 0000000..9022de4 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_home_black_24dp.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_home_white_24dp.png b/app/src/main/res/drawable-xhdpi/ic_home_white_24dp.png new file mode 100644 index 0000000..d00f914 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_home_white_24dp.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_image_black_24dp.png b/app/src/main/res/drawable-xhdpi/ic_image_black_24dp.png new file mode 100644 index 0000000..bdc5db0 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_image_black_24dp.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_image_white_24dp.png b/app/src/main/res/drawable-xhdpi/ic_image_white_24dp.png new file mode 100644 index 0000000..bf56761 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_image_white_24dp.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_keyboard_black_24dp.png b/app/src/main/res/drawable-xhdpi/ic_keyboard_black_24dp.png new file mode 100644 index 0000000..fd2c63d Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_keyboard_black_24dp.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_keyboard_white_24dp.png b/app/src/main/res/drawable-xhdpi/ic_keyboard_white_24dp.png new file mode 100644 index 0000000..f0050c5 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_keyboard_white_24dp.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_launcher.png b/app/src/main/res/drawable-xhdpi/ic_launcher.png new file mode 100644 index 0000000..885d732 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_launcher.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_menu_white_24dp.png b/app/src/main/res/drawable-xhdpi/ic_menu_white_24dp.png new file mode 100644 index 0000000..ceadb18 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_menu_white_24dp.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_more_vert_black_24dp.png b/app/src/main/res/drawable-xhdpi/ic_more_vert_black_24dp.png new file mode 100644 index 0000000..846047c Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_more_vert_black_24dp.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_more_vert_white_24dp.png b/app/src/main/res/drawable-xhdpi/ic_more_vert_white_24dp.png new file mode 100644 index 0000000..1b04eda Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_more_vert_white_24dp.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_movie_black_24dp.png b/app/src/main/res/drawable-xhdpi/ic_movie_black_24dp.png new file mode 100644 index 0000000..ac344ba Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_movie_black_24dp.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_movie_white_24dp.png b/app/src/main/res/drawable-xhdpi/ic_movie_white_24dp.png new file mode 100644 index 0000000..3098fd9 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_movie_white_24dp.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_open_in_new_black_24dp.png b/app/src/main/res/drawable-xhdpi/ic_open_in_new_black_24dp.png new file mode 100644 index 0000000..2535bf0 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_open_in_new_black_24dp.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_open_in_new_white_24dp.png b/app/src/main/res/drawable-xhdpi/ic_open_in_new_white_24dp.png new file mode 100644 index 0000000..9dbbc3f Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_open_in_new_white_24dp.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_pause_black_24dp.png b/app/src/main/res/drawable-xhdpi/ic_pause_black_24dp.png new file mode 100644 index 0000000..3e3cb61 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_pause_black_24dp.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_pause_white_24dp.png b/app/src/main/res/drawable-xhdpi/ic_pause_white_24dp.png new file mode 100644 index 0000000..14b6d17 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_pause_white_24dp.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_phonelink_black_24dp.png b/app/src/main/res/drawable-xhdpi/ic_phonelink_black_24dp.png new file mode 100644 index 0000000..c9969cc Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_phonelink_black_24dp.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_phonelink_white_24dp.png b/app/src/main/res/drawable-xhdpi/ic_phonelink_white_24dp.png new file mode 100644 index 0000000..813fe44 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_phonelink_white_24dp.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_play_arrow_black_24dp.png b/app/src/main/res/drawable-xhdpi/ic_play_arrow_black_24dp.png new file mode 100644 index 0000000..843cdf9 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_play_arrow_black_24dp.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_play_arrow_white_24dp.png b/app/src/main/res/drawable-xhdpi/ic_play_arrow_white_24dp.png new file mode 100644 index 0000000..a55d199 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_play_arrow_white_24dp.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_queue_black_24dp.png b/app/src/main/res/drawable-xhdpi/ic_queue_black_24dp.png new file mode 100644 index 0000000..868f6fc Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_queue_black_24dp.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_queue_white_24dp.png b/app/src/main/res/drawable-xhdpi/ic_queue_white_24dp.png new file mode 100644 index 0000000..0475fa3 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_queue_white_24dp.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_repeat_black_24dp.png b/app/src/main/res/drawable-xhdpi/ic_repeat_black_24dp.png new file mode 100644 index 0000000..cd22344 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_repeat_black_24dp.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_repeat_one_black_24dp.png b/app/src/main/res/drawable-xhdpi/ic_repeat_one_black_24dp.png new file mode 100644 index 0000000..35ec816 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_repeat_one_black_24dp.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_repeat_one_white_24dp.png b/app/src/main/res/drawable-xhdpi/ic_repeat_one_white_24dp.png new file mode 100644 index 0000000..e4dfdbe Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_repeat_one_white_24dp.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_repeat_white_24dp.png b/app/src/main/res/drawable-xhdpi/ic_repeat_white_24dp.png new file mode 100644 index 0000000..7292200 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_repeat_white_24dp.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_search_black_24dp.png b/app/src/main/res/drawable-xhdpi/ic_search_black_24dp.png new file mode 100644 index 0000000..e22d082 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_search_black_24dp.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_search_white_24dp.png b/app/src/main/res/drawable-xhdpi/ic_search_white_24dp.png new file mode 100644 index 0000000..043759a Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_search_white_24dp.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_settings_black_24dp.png b/app/src/main/res/drawable-xhdpi/ic_settings_black_24dp.png new file mode 100644 index 0000000..1879dfc Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_settings_black_24dp.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_settings_power_black_24dp.png b/app/src/main/res/drawable-xhdpi/ic_settings_power_black_24dp.png new file mode 100644 index 0000000..790aaa7 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_settings_power_black_24dp.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_settings_power_white_24dp.png b/app/src/main/res/drawable-xhdpi/ic_settings_power_white_24dp.png new file mode 100644 index 0000000..bf41254 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_settings_power_white_24dp.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_settings_white_24dp.png b/app/src/main/res/drawable-xhdpi/ic_settings_white_24dp.png new file mode 100644 index 0000000..12e5d10 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_settings_white_24dp.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_shuffle_black_24dp.png b/app/src/main/res/drawable-xhdpi/ic_shuffle_black_24dp.png new file mode 100644 index 0000000..90062db Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_shuffle_black_24dp.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_shuffle_white_24dp.png b/app/src/main/res/drawable-xhdpi/ic_shuffle_white_24dp.png new file mode 100644 index 0000000..b1f6d8d Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_shuffle_white_24dp.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_skip_next_black_24dp.png b/app/src/main/res/drawable-xhdpi/ic_skip_next_black_24dp.png new file mode 100644 index 0000000..d56faf3 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_skip_next_black_24dp.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_skip_next_white_24dp.png b/app/src/main/res/drawable-xhdpi/ic_skip_next_white_24dp.png new file mode 100644 index 0000000..f282b92 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_skip_next_white_24dp.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_skip_previous_black_24dp.png b/app/src/main/res/drawable-xhdpi/ic_skip_previous_black_24dp.png new file mode 100644 index 0000000..acaaa5d Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_skip_previous_black_24dp.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_skip_previous_white_24dp.png b/app/src/main/res/drawable-xhdpi/ic_skip_previous_white_24dp.png new file mode 100644 index 0000000..2522877 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_skip_previous_white_24dp.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_stop_black_24dp.png b/app/src/main/res/drawable-xhdpi/ic_stop_black_24dp.png new file mode 100644 index 0000000..7560d90 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_stop_black_24dp.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_stop_white_24dp.png b/app/src/main/res/drawable-xhdpi/ic_stop_white_24dp.png new file mode 100644 index 0000000..9a6e57b Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_stop_white_24dp.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_subtitles_black_24dp.png b/app/src/main/res/drawable-xhdpi/ic_subtitles_black_24dp.png new file mode 100644 index 0000000..018d6be Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_subtitles_black_24dp.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_subtitles_white_24dp.png b/app/src/main/res/drawable-xhdpi/ic_subtitles_white_24dp.png new file mode 100644 index 0000000..5aea287 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_subtitles_white_24dp.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_tv_black_24dp.png b/app/src/main/res/drawable-xhdpi/ic_tv_black_24dp.png new file mode 100644 index 0000000..f0df6fc Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_tv_black_24dp.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_tv_white_24dp.png b/app/src/main/res/drawable-xhdpi/ic_tv_white_24dp.png new file mode 100644 index 0000000..737315f Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_tv_white_24dp.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_volume_down_black_24dp.png b/app/src/main/res/drawable-xhdpi/ic_volume_down_black_24dp.png new file mode 100644 index 0000000..5ea7346 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_volume_down_black_24dp.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_volume_down_white_24dp.png b/app/src/main/res/drawable-xhdpi/ic_volume_down_white_24dp.png new file mode 100644 index 0000000..40dc3f0 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_volume_down_white_24dp.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_volume_off_black_24dp.png b/app/src/main/res/drawable-xhdpi/ic_volume_off_black_24dp.png new file mode 100644 index 0000000..43f225e Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_volume_off_black_24dp.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_volume_off_white_24dp.png b/app/src/main/res/drawable-xhdpi/ic_volume_off_white_24dp.png new file mode 100644 index 0000000..c51daba Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_volume_off_white_24dp.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_volume_up_black_24dp.png b/app/src/main/res/drawable-xhdpi/ic_volume_up_black_24dp.png new file mode 100644 index 0000000..c2996c5 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_volume_up_black_24dp.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_volume_up_white_24dp.png b/app/src/main/res/drawable-xhdpi/ic_volume_up_white_24dp.png new file mode 100644 index 0000000..a0b4af2 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_volume_up_white_24dp.png differ diff --git a/app/src/main/res/drawable-xxhdpi/drawer_shadow.9.png b/app/src/main/res/drawable-xxhdpi/drawer_shadow.9.png new file mode 100644 index 0000000..b91e9d7 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/drawer_shadow.9.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_add_box_black_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_add_box_black_24dp.png new file mode 100644 index 0000000..bdb8d34 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_add_box_black_24dp.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_add_box_white_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_add_box_white_24dp.png new file mode 100644 index 0000000..4d054c9 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_add_box_white_24dp.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_chevron_left_black_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_chevron_left_black_24dp.png new file mode 100644 index 0000000..385f0ac Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_chevron_left_black_24dp.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_chevron_left_white_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_chevron_left_white_24dp.png new file mode 100644 index 0000000..69ac65e Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_chevron_left_white_24dp.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_chevron_right_black_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_chevron_right_black_24dp.png new file mode 100644 index 0000000..9bd90f3 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_chevron_right_black_24dp.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_chevron_right_white_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_chevron_right_white_24dp.png new file mode 100644 index 0000000..89ba1a2 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_chevron_right_white_24dp.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_closed_caption_black_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_closed_caption_black_24dp.png new file mode 100644 index 0000000..9679507 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_closed_caption_black_24dp.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_closed_caption_white_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_closed_caption_white_24dp.png new file mode 100644 index 0000000..5c19576 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_closed_caption_white_24dp.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_delete_black_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_delete_black_24dp.png new file mode 100644 index 0000000..67bbebf Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_delete_black_24dp.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_delete_white_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_delete_white_24dp.png new file mode 100644 index 0000000..0e95e9b Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_delete_white_24dp.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_done_black_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_done_black_24dp.png new file mode 100644 index 0000000..e319fe9 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_done_black_24dp.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_done_white_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_done_white_24dp.png new file mode 100644 index 0000000..f801e7a Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_done_white_24dp.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_edit_black_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_edit_black_24dp.png new file mode 100644 index 0000000..2cc20df Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_edit_black_24dp.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_edit_white_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_edit_white_24dp.png new file mode 100644 index 0000000..34ec709 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_edit_white_24dp.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_expand_less_black_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_expand_less_black_24dp.png new file mode 100644 index 0000000..882daa9 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_expand_less_black_24dp.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_expand_less_white_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_expand_less_white_24dp.png new file mode 100644 index 0000000..a74849a Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_expand_less_white_24dp.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_expand_more_black_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_expand_more_black_24dp.png new file mode 100644 index 0000000..f5013df Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_expand_more_black_24dp.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_expand_more_white_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_expand_more_white_24dp.png new file mode 100644 index 0000000..f00aa8b Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_expand_more_white_24dp.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_extension_black_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_extension_black_24dp.png new file mode 100644 index 0000000..c91c290 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_extension_black_24dp.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_extension_white_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_extension_white_24dp.png new file mode 100644 index 0000000..8c3bc3b Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_extension_white_24dp.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_fast_forward_black_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_fast_forward_black_24dp.png new file mode 100644 index 0000000..bcdbc16 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_fast_forward_black_24dp.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_fast_forward_white_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_fast_forward_white_24dp.png new file mode 100644 index 0000000..8aec1ab Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_fast_forward_white_24dp.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_fast_rewind_black_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_fast_rewind_black_24dp.png new file mode 100644 index 0000000..429de4c Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_fast_rewind_black_24dp.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_fast_rewind_white_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_fast_rewind_white_24dp.png new file mode 100644 index 0000000..857a1aa Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_fast_rewind_white_24dp.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_games_black_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_games_black_24dp.png new file mode 100644 index 0000000..a86b1a8 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_games_black_24dp.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_games_white_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_games_white_24dp.png new file mode 100644 index 0000000..f9ce49d Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_games_white_24dp.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_get_app_black_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_get_app_black_24dp.png new file mode 100644 index 0000000..20c3ed1 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_get_app_black_24dp.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_get_app_white_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_get_app_white_24dp.png new file mode 100644 index 0000000..95502de Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_get_app_white_24dp.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_headset_black_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_headset_black_24dp.png new file mode 100644 index 0000000..d11a6e0 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_headset_black_24dp.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_headset_white_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_headset_white_24dp.png new file mode 100644 index 0000000..4080871 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_headset_white_24dp.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_home_black_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_home_black_24dp.png new file mode 100644 index 0000000..5db255a Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_home_black_24dp.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_home_white_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_home_white_24dp.png new file mode 100644 index 0000000..fd40ac3 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_home_white_24dp.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_image_black_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_image_black_24dp.png new file mode 100644 index 0000000..5500a21 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_image_black_24dp.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_image_white_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_image_white_24dp.png new file mode 100644 index 0000000..d120024 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_image_white_24dp.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_keyboard_black_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_keyboard_black_24dp.png new file mode 100644 index 0000000..4d8c0ac Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_keyboard_black_24dp.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_keyboard_white_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_keyboard_white_24dp.png new file mode 100644 index 0000000..2ffbe32 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_keyboard_white_24dp.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_launcher.png b/app/src/main/res/drawable-xxhdpi/ic_launcher.png new file mode 100644 index 0000000..de8bb8d Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_launcher.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_menu_white_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_menu_white_24dp.png new file mode 100644 index 0000000..22c82ea Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_menu_white_24dp.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_more_vert_black_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_more_vert_black_24dp.png new file mode 100644 index 0000000..ed24bbf Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_more_vert_black_24dp.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_more_vert_white_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_more_vert_white_24dp.png new file mode 100644 index 0000000..2955c02 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_more_vert_white_24dp.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_movie_black_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_movie_black_24dp.png new file mode 100644 index 0000000..071d706 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_movie_black_24dp.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_movie_white_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_movie_white_24dp.png new file mode 100644 index 0000000..373585e Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_movie_white_24dp.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_open_in_new_black_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_open_in_new_black_24dp.png new file mode 100644 index 0000000..d1d5288 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_open_in_new_black_24dp.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_open_in_new_white_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_open_in_new_white_24dp.png new file mode 100644 index 0000000..cbb8dba Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_open_in_new_white_24dp.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_pause_black_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_pause_black_24dp.png new file mode 100644 index 0000000..3aa7a92 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_pause_black_24dp.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_pause_white_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_pause_white_24dp.png new file mode 100644 index 0000000..72dfa9f Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_pause_white_24dp.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_phonelink_black_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_phonelink_black_24dp.png new file mode 100644 index 0000000..8ba698b Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_phonelink_black_24dp.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_phonelink_white_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_phonelink_white_24dp.png new file mode 100644 index 0000000..d81a496 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_phonelink_white_24dp.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_play_arrow_black_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_play_arrow_black_24dp.png new file mode 100644 index 0000000..16d13ca Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_play_arrow_black_24dp.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_play_arrow_white_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_play_arrow_white_24dp.png new file mode 100644 index 0000000..043acd8 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_play_arrow_white_24dp.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_queue_black_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_queue_black_24dp.png new file mode 100644 index 0000000..06edb07 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_queue_black_24dp.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_queue_white_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_queue_white_24dp.png new file mode 100644 index 0000000..efdcf08 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_queue_white_24dp.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_repeat_black_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_repeat_black_24dp.png new file mode 100644 index 0000000..3fb5fa6 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_repeat_black_24dp.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_repeat_one_black_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_repeat_one_black_24dp.png new file mode 100644 index 0000000..f3143b5 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_repeat_one_black_24dp.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_repeat_one_white_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_repeat_one_white_24dp.png new file mode 100644 index 0000000..36ea532 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_repeat_one_white_24dp.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_repeat_white_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_repeat_white_24dp.png new file mode 100644 index 0000000..63f8de5 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_repeat_white_24dp.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_search_black_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_search_black_24dp.png new file mode 100644 index 0000000..861fb9c Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_search_black_24dp.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_search_white_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_search_white_24dp.png new file mode 100644 index 0000000..0bbeab1 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_search_white_24dp.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_settings_black_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_settings_black_24dp.png new file mode 100644 index 0000000..c835cc6 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_settings_black_24dp.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_settings_power_black_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_settings_power_black_24dp.png new file mode 100644 index 0000000..3bdff4c Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_settings_power_black_24dp.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_settings_power_white_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_settings_power_white_24dp.png new file mode 100644 index 0000000..d02b6ad Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_settings_power_white_24dp.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_settings_white_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_settings_white_24dp.png new file mode 100644 index 0000000..6bb8f6e Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_settings_white_24dp.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_shuffle_black_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_shuffle_black_24dp.png new file mode 100644 index 0000000..67600fb Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_shuffle_black_24dp.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_shuffle_white_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_shuffle_white_24dp.png new file mode 100644 index 0000000..5b3d814 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_shuffle_white_24dp.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_skip_next_black_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_skip_next_black_24dp.png new file mode 100644 index 0000000..3fa0e73 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_skip_next_black_24dp.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_skip_next_white_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_skip_next_white_24dp.png new file mode 100644 index 0000000..4fe6088 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_skip_next_white_24dp.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_skip_previous_black_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_skip_previous_black_24dp.png new file mode 100644 index 0000000..8839a84 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_skip_previous_black_24dp.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_skip_previous_white_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_skip_previous_white_24dp.png new file mode 100644 index 0000000..2c9310a Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_skip_previous_white_24dp.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_stop_black_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_stop_black_24dp.png new file mode 100644 index 0000000..e0b4499 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_stop_black_24dp.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_stop_white_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_stop_white_24dp.png new file mode 100644 index 0000000..bfa39f3 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_stop_white_24dp.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_subtitles_black_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_subtitles_black_24dp.png new file mode 100644 index 0000000..804ffa6 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_subtitles_black_24dp.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_subtitles_white_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_subtitles_white_24dp.png new file mode 100644 index 0000000..34d981f Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_subtitles_white_24dp.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_tv_black_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_tv_black_24dp.png new file mode 100644 index 0000000..78cb122 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_tv_black_24dp.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_tv_white_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_tv_white_24dp.png new file mode 100644 index 0000000..daa6a05 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_tv_white_24dp.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_volume_down_black_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_volume_down_black_24dp.png new file mode 100644 index 0000000..da58e22 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_volume_down_black_24dp.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_volume_down_white_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_volume_down_white_24dp.png new file mode 100644 index 0000000..ca9dbfd Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_volume_down_white_24dp.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_volume_off_black_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_volume_off_black_24dp.png new file mode 100644 index 0000000..6b5b342 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_volume_off_black_24dp.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_volume_off_white_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_volume_off_white_24dp.png new file mode 100644 index 0000000..9f44de0 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_volume_off_white_24dp.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_volume_up_black_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_volume_up_black_24dp.png new file mode 100644 index 0000000..8bf0ee2 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_volume_up_black_24dp.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_volume_up_white_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_volume_up_white_24dp.png new file mode 100644 index 0000000..fa8e34f Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_volume_up_white_24dp.png differ diff --git a/app/src/main/res/drawable-xxhdpi/remote_back_black.png b/app/src/main/res/drawable-xxhdpi/remote_back_black.png new file mode 100644 index 0000000..4e7bd5a Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/remote_back_black.png differ diff --git a/app/src/main/res/drawable-xxhdpi/remote_back_white.png b/app/src/main/res/drawable-xxhdpi/remote_back_white.png new file mode 100644 index 0000000..c361abf Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/remote_back_white.png differ diff --git a/app/src/main/res/drawable-xxhdpi/remote_codec_black.png b/app/src/main/res/drawable-xxhdpi/remote_codec_black.png new file mode 100644 index 0000000..382bd00 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/remote_codec_black.png differ diff --git a/app/src/main/res/drawable-xxhdpi/remote_codec_white.png b/app/src/main/res/drawable-xxhdpi/remote_codec_white.png new file mode 100644 index 0000000..d33967f Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/remote_codec_white.png differ diff --git a/app/src/main/res/drawable-xxhdpi/remote_down_black.png b/app/src/main/res/drawable-xxhdpi/remote_down_black.png new file mode 100644 index 0000000..16804cc Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/remote_down_black.png differ diff --git a/app/src/main/res/drawable-xxhdpi/remote_down_white.png b/app/src/main/res/drawable-xxhdpi/remote_down_white.png new file mode 100644 index 0000000..aa9194c Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/remote_down_white.png differ diff --git a/app/src/main/res/drawable-xxhdpi/remote_info_black.png b/app/src/main/res/drawable-xxhdpi/remote_info_black.png new file mode 100644 index 0000000..98e319d Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/remote_info_black.png differ diff --git a/app/src/main/res/drawable-xxhdpi/remote_info_white.png b/app/src/main/res/drawable-xxhdpi/remote_info_white.png new file mode 100644 index 0000000..9a344f5 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/remote_info_white.png differ diff --git a/app/src/main/res/drawable-xxhdpi/remote_left_black.png b/app/src/main/res/drawable-xxhdpi/remote_left_black.png new file mode 100644 index 0000000..fbd210f Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/remote_left_black.png differ diff --git a/app/src/main/res/drawable-xxhdpi/remote_left_white.png b/app/src/main/res/drawable-xxhdpi/remote_left_white.png new file mode 100644 index 0000000..e2315bd Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/remote_left_white.png differ diff --git a/app/src/main/res/drawable-xxhdpi/remote_menu_black.png b/app/src/main/res/drawable-xxhdpi/remote_menu_black.png new file mode 100644 index 0000000..2fd9d95 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/remote_menu_black.png differ diff --git a/app/src/main/res/drawable-xxhdpi/remote_menu_white.png b/app/src/main/res/drawable-xxhdpi/remote_menu_white.png new file mode 100644 index 0000000..151ad0e Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/remote_menu_white.png differ diff --git a/app/src/main/res/drawable-xxhdpi/remote_right_black.png b/app/src/main/res/drawable-xxhdpi/remote_right_black.png new file mode 100644 index 0000000..58bda87 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/remote_right_black.png differ diff --git a/app/src/main/res/drawable-xxhdpi/remote_right_white.png b/app/src/main/res/drawable-xxhdpi/remote_right_white.png new file mode 100644 index 0000000..648c636 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/remote_right_white.png differ diff --git a/app/src/main/res/drawable-xxhdpi/remote_select_black.png b/app/src/main/res/drawable-xxhdpi/remote_select_black.png new file mode 100644 index 0000000..8ff72a0 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/remote_select_black.png differ diff --git a/app/src/main/res/drawable-xxhdpi/remote_select_white.png b/app/src/main/res/drawable-xxhdpi/remote_select_white.png new file mode 100644 index 0000000..c4ae85b Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/remote_select_white.png differ diff --git a/app/src/main/res/drawable-xxhdpi/remote_up_black.png b/app/src/main/res/drawable-xxhdpi/remote_up_black.png new file mode 100644 index 0000000..d5ada4c Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/remote_up_black.png differ diff --git a/app/src/main/res/drawable-xxhdpi/remote_up_white.png b/app/src/main/res/drawable-xxhdpi/remote_up_white.png new file mode 100644 index 0000000..9a0525e Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/remote_up_white.png differ diff --git a/app/src/main/res/drawable/host_status_indicator.xml b/app/src/main/res/drawable/host_status_indicator.xml new file mode 100644 index 0000000..6163434 --- /dev/null +++ b/app/src/main/res/drawable/host_status_indicator.xml @@ -0,0 +1,22 @@ + + + + + + + diff --git a/app/src/main/res/layout-land/fragment_remote.xml b/app/src/main/res/layout-land/fragment_remote.xml new file mode 100644 index 0000000..3c6e73d --- /dev/null +++ b/app/src/main/res/layout-land/fragment_remote.xml @@ -0,0 +1,246 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_generic_media.xml b/app/src/main/res/layout/activity_generic_media.xml new file mode 100644 index 0000000..7a5524a --- /dev/null +++ b/app/src/main/res/layout/activity_generic_media.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_host_manager.xml b/app/src/main/res/layout/activity_host_manager.xml new file mode 100644 index 0000000..5c324d1 --- /dev/null +++ b/app/src/main/res/layout/activity_host_manager.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_remote.xml b/app/src/main/res/layout/activity_remote.xml new file mode 100644 index 0000000..5d79527 --- /dev/null +++ b/app/src/main/res/layout/activity_remote.xml @@ -0,0 +1,64 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_settings.xml b/app/src/main/res/layout/activity_settings.xml new file mode 100644 index 0000000..b4941b7 --- /dev/null +++ b/app/src/main/res/layout/activity_settings.xml @@ -0,0 +1,31 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_send_text.xml b/app/src/main/res/layout/dialog_send_text.xml new file mode 100644 index 0000000..7f3ca57 --- /dev/null +++ b/app/src/main/res/layout/dialog_send_text.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/empty_view.xml b/app/src/main/res/layout/empty_view.xml new file mode 100644 index 0000000..2a008b1 --- /dev/null +++ b/app/src/main/res/layout/empty_view.xml @@ -0,0 +1,27 @@ + + + + + diff --git a/app/src/main/res/layout/fragment_about.xml b/app/src/main/res/layout/fragment_about.xml new file mode 100644 index 0000000..340d9c8 --- /dev/null +++ b/app/src/main/res/layout/fragment_about.xml @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_add_host_finish.xml b/app/src/main/res/layout/fragment_add_host_finish.xml new file mode 100644 index 0000000..6bf0f33 --- /dev/null +++ b/app/src/main/res/layout/fragment_add_host_finish.xml @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_add_host_manual_configuration.xml b/app/src/main/res/layout/fragment_add_host_manual_configuration.xml new file mode 100644 index 0000000..b11436d --- /dev/null +++ b/app/src/main/res/layout/fragment_add_host_manual_configuration.xml @@ -0,0 +1,166 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_add_host_welcome.xml b/app/src/main/res/layout/fragment_add_host_welcome.xml new file mode 100644 index 0000000..31e350d --- /dev/null +++ b/app/src/main/res/layout/fragment_add_host_welcome.xml @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_add_host_zeroconf.xml b/app/src/main/res/layout/fragment_add_host_zeroconf.xml new file mode 100644 index 0000000..5f5861d --- /dev/null +++ b/app/src/main/res/layout/fragment_add_host_zeroconf.xml @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/fragment_addon_details.xml b/app/src/main/res/layout/fragment_addon_details.xml new file mode 100644 index 0000000..e1f41b8 --- /dev/null +++ b/app/src/main/res/layout/fragment_addon_details.xml @@ -0,0 +1,180 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_album_details.xml b/app/src/main/res/layout/fragment_album_details.xml new file mode 100644 index 0000000..616f4dd --- /dev/null +++ b/app/src/main/res/layout/fragment_album_details.xml @@ -0,0 +1,224 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_episode_details.xml b/app/src/main/res/layout/fragment_episode_details.xml new file mode 100644 index 0000000..45e578c --- /dev/null +++ b/app/src/main/res/layout/fragment_episode_details.xml @@ -0,0 +1,258 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_generic_media_list.xml b/app/src/main/res/layout/fragment_generic_media_list.xml new file mode 100644 index 0000000..515d316 --- /dev/null +++ b/app/src/main/res/layout/fragment_generic_media_list.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + + diff --git a/app/src/main/res/layout/fragment_host_list.xml b/app/src/main/res/layout/fragment_host_list.xml new file mode 100644 index 0000000..3327e26 --- /dev/null +++ b/app/src/main/res/layout/fragment_host_list.xml @@ -0,0 +1,62 @@ + + + + + + + + + +