aboutsummaryrefslogtreecommitdiff
path: root/tools
diff options
context:
space:
mode:
authorChris "Koying" Browet <cbro@semperpax.com>2017-12-29 13:09:55 +0100
committerChris "Koying" Browet <cbro@semperpax.com>2018-01-01 21:05:28 +0100
commit65a6d875ac209432538fcfc0969f699c364a29d4 (patch)
tree5fe0d2a309a1cec75f1d4f8b4ced5890622138c5 /tools
parent75861455685f537305fb27ad4bacf52bb3d48194 (diff)
ADD: [droid] Oreo leanback support
Diffstat (limited to 'tools')
-rw-r--r--tools/android/packaging/Makefile.in6
-rw-r--r--tools/android/packaging/build.gradle3
-rw-r--r--tools/android/packaging/xbmc/AndroidManifest.xml.in30
-rw-r--r--tools/android/packaging/xbmc/build.gradle.in7
-rw-r--r--tools/android/packaging/xbmc/res/drawable/ic_recommendation_80dp.pngbin0 -> 2239 bytes
-rw-r--r--tools/android/packaging/xbmc/src/Main.java.in27
-rw-r--r--tools/android/packaging/xbmc/src/XBMCJsonRPC.java.in931
-rw-r--r--tools/android/packaging/xbmc/src/channels/SyncChannelJobService.java.in211
-rw-r--r--tools/android/packaging/xbmc/src/channels/SyncProgramsJobService.java.in305
-rw-r--r--tools/android/packaging/xbmc/src/channels/model/Subscription.java.in105
-rw-r--r--tools/android/packaging/xbmc/src/channels/model/XBMCDatabase.java.in255
-rw-r--r--tools/android/packaging/xbmc/src/channels/util/SharedPreferencesHelper.java.in145
-rw-r--r--tools/android/packaging/xbmc/src/channels/util/TvUtil.java.in297
-rw-r--r--tools/android/packaging/xbmc/src/content/XBMCContentProvider.java.in12
-rw-r--r--tools/android/packaging/xbmc/src/content/XBMCFileContentProvider.java.in99
-rw-r--r--tools/android/packaging/xbmc/src/content/XBMCImageContentProvider.java.in (renamed from tools/android/packaging/xbmc/src/XBMCImageContentProvider.java.in)47
-rw-r--r--tools/android/packaging/xbmc/src/content/XBMCMediaContentProvider.java.in (renamed from tools/android/packaging/xbmc/src/XBMCMediaContentProvider.java.in)48
-rw-r--r--tools/android/packaging/xbmc/src/content/XBMCYTDLContentProvider.java.in188
-rw-r--r--tools/android/packaging/xbmc/src/model/Album.java.in11
-rw-r--r--tools/android/packaging/xbmc/src/model/File.java.in170
-rw-r--r--tools/android/packaging/xbmc/src/model/Media.java.in188
-rw-r--r--tools/android/packaging/xbmc/src/model/Movie.java.in33
-rw-r--r--tools/android/packaging/xbmc/src/model/MusicVideo.java.in9
-rw-r--r--tools/android/packaging/xbmc/src/model/Song.java.in9
-rw-r--r--tools/android/packaging/xbmc/src/model/TVEpisode.java.in33
-rw-r--r--tools/android/packaging/xbmc/src/model/TVShow.java.in11
-rw-r--r--tools/android/packaging/xbmc/strings.xml.in1
27 files changed, 2912 insertions, 269 deletions
diff --git a/tools/android/packaging/Makefile.in b/tools/android/packaging/Makefile.in
index 37e0540719..cf21dca28d 100644
--- a/tools/android/packaging/Makefile.in
+++ b/tools/android/packaging/Makefile.in
@@ -128,6 +128,7 @@ res:
cp -fp $(CMAKE_SOURCE_DIR)/media/Splash.png xbmc/res/drawable-xxxhdpi/splash.png
cp -fp media/drawable-xxxhdpi/ic_launcher.png xbmc/res/drawable-xxxhdpi/ic_launcher.png
cp -fp media/drawable-xhdpi/banner.png xbmc/res/drawable-xhdpi/banner.png
+ cp -fp $(CMAKE_SOURCE_DIR)/media/icon80x80.png xbmc/res/drawable/ic_recommendation_80dp.png
cp xbmc/strings.xml xbmc/res/values/
cp xbmc/colors.xml xbmc/res/values/
cp xbmc/searchable.xml xbmc/res/xml/
@@ -153,9 +154,8 @@ libs: $(PREFIX)/lib/@APP_NAME_LC@/lib@APP_NAME_LC@.so
"$(NDKROOT)/sources/cxx-stl/gnu-libstdc++/$(GCC_VERSION)/include $(CORE_SOURCE_DIR) $(PREFIX)/include jni" >> ./xbmc/lib/$(CPU)/gdb.setup
java: res
- mkdir -p xbmc/java/$(APP_PACKAGE_DIR) xbmc/java/$(APP_PACKAGE_DIR)/interfaces xbmc/obj
- @cp xbmc/src/*.java xbmc/java/$(APP_PACKAGE_DIR)/
- @cp xbmc/src/interfaces/*.java xbmc/java/$(APP_PACKAGE_DIR)/interfaces/
+ mkdir -p xbmc/java/$(APP_PACKAGE_DIR) xbmc/obj
+ @cp -R xbmc/src/* xbmc/java/$(APP_PACKAGE_DIR)/
package: libs python java
@echo "Gradle build..."
diff --git a/tools/android/packaging/build.gradle b/tools/android/packaging/build.gradle
index 440157dff1..6832158cc7 100644
--- a/tools/android/packaging/build.gradle
+++ b/tools/android/packaging/build.gradle
@@ -3,6 +3,7 @@
buildscript {
repositories {
jcenter()
+ google()
}
dependencies {
classpath 'com.android.tools.build:gradle:2.3.3'
@@ -15,9 +16,11 @@ buildscript {
allprojects {
repositories {
jcenter()
+ google()
}
}
task wrapper(type: Wrapper) {
gradleVersion = '4.1' //version required
}
+
diff --git a/tools/android/packaging/xbmc/AndroidManifest.xml.in b/tools/android/packaging/xbmc/AndroidManifest.xml.in
index 7cb99dfa9d..e6342e7eca 100644
--- a/tools/android/packaging/xbmc/AndroidManifest.xml.in
+++ b/tools/android/packaging/xbmc/AndroidManifest.xml.in
@@ -39,6 +39,9 @@
<uses-feature
android:name="android.hardware.wifi"
android:required="false" />
+ <uses-feature
+ android:name="android.software.leanback"
+ android:required="false" />
<application
android:banner="@drawable/banner"
@@ -116,17 +119,26 @@
</receiver>
<provider
- android:name=".XBMCImageContentProvider"
+ android:name=".content.XBMCImageContentProvider"
android:authorities="@APP_PACKAGE@.image"
android:exported="true" />
<provider
- android:name=".XBMCMediaContentProvider"
+ android:name=".content.XBMCMediaContentProvider"
android:authorities="@APP_PACKAGE@.media"
android:exported="true" />
- <activity android:name=".XBMCSearchableActivity">
- <intent-filter>
+ <provider
+ android:name=".content.XBMCFileContentProvider"
+ android:authorities="@APP_PACKAGE@.file"
+ android:exported="true" />
+ <provider
+ android:name=".content.XBMCYTDLContentProvider"
+ android:authorities="@APP_PACKAGE@.ytdl"
+ android:exported="true" />
+
+ <activity android:name=".XBMCSearchableActivity" >
+ <intent-filter>
<action android:name="android.intent.action.SEARCH" />
<category android:name="android.intent.category.DEFAULT" />
@@ -144,6 +156,16 @@
android:resource="@xml/searchable" />
</activity>
+ <service
+ android:name=".channels.SyncChannelJobService"
+ android:exported="false"
+ android:permission="android.permission.BIND_JOB_SERVICE" />
+
+ <service
+ android:name=".channels.SyncProgramsJobService"
+ android:exported="false"
+ android:permission="android.permission.BIND_JOB_SERVICE" />
+
</application>
</manifest><!-- END_INCLUDE(manifest) -->
diff --git a/tools/android/packaging/xbmc/build.gradle.in b/tools/android/packaging/xbmc/build.gradle.in
index ff4af80cc8..c8b21fd0d5 100644
--- a/tools/android/packaging/xbmc/build.gradle.in
+++ b/tools/android/packaging/xbmc/build.gradle.in
@@ -1,7 +1,7 @@
apply plugin: 'com.android.application'
android {
- compileSdkVersion 24
+ compileSdkVersion 26
buildToolsVersion "25.0.3"
defaultConfig {
applicationId "@APP_PACKAGE@"
@@ -39,3 +39,8 @@ project.afterEvaluate {
preBuild.dependsOn
}
+dependencies {
+ // New support library to for channels/programs development.
+ compile 'com.android.support:support-tv-provider:26.0.1'
+ compile 'com.google.code.gson:gson:2.8.0'
+}
diff --git a/tools/android/packaging/xbmc/res/drawable/ic_recommendation_80dp.png b/tools/android/packaging/xbmc/res/drawable/ic_recommendation_80dp.png
new file mode 100644
index 0000000000..1bd52332dd
--- /dev/null
+++ b/tools/android/packaging/xbmc/res/drawable/ic_recommendation_80dp.png
Binary files differ
diff --git a/tools/android/packaging/xbmc/src/Main.java.in b/tools/android/packaging/xbmc/src/Main.java.in
index 6bf0549ed9..e4f5fd72c9 100644
--- a/tools/android/packaging/xbmc/src/Main.java.in
+++ b/tools/android/packaging/xbmc/src/Main.java.in
@@ -1,5 +1,8 @@
package @APP_PACKAGE@;
+import android.content.Context;
+import android.os.Build.VERSION_CODES;
+import android.os.Build;
import android.app.NativeActivity;
import android.content.ComponentName;
import android.content.Intent;
@@ -15,9 +18,11 @@ import android.graphics.Color;
import android.graphics.PixelFormat;
import android.os.Handler;
+import @APP_PACKAGE@.channels.util.TvUtil;
+
public class Main extends NativeActivity implements Choreographer.FrameCallback
{
- private static final String TAG = "@APP_NAME_LC@";
+ private static final String TAG = "@APP_NAME@";
public static Main MainActivity = null;
@@ -107,10 +112,15 @@ public class Main extends NativeActivity implements Choreographer.FrameCallback
if (getPackageManager().hasSystemFeature("android.software.leanback"))
{
- // Leanback
- mJsonRPC = new XBMCJsonRPC();
- handler.removeCallbacks(leanbackUpdateRunnable);
- handler.postDelayed(leanbackUpdateRunnable, 30 * 1000);
+ if (Build.VERSION.SDK_INT >= VERSION_CODES.O)
+ TvUtil.scheduleSyncingChannel(this);
+ else
+ {
+ // Leanback
+ mJsonRPC = new XBMCJsonRPC();
+ handler.removeCallbacks(leanbackUpdateRunnable);
+ handler.postDelayed(leanbackUpdateRunnable, 30 * 1000);
+ }
}
// register the InputDeviceListener implementation
@@ -206,8 +216,7 @@ public class Main extends NativeActivity implements Choreographer.FrameCallback
catch (UnsatisfiedLinkError e)
{
Log.e("Main", "Native not registered");
- }
- finally
+ } finally
{
mNewIntent = null;
}
@@ -220,12 +229,16 @@ public class Main extends NativeActivity implements Choreographer.FrameCallback
public void onPause()
{
super.onPause();
+
+ if (Build.VERSION.SDK_INT >= VERSION_CODES.O)
+ TvUtil.scheduleSyncingChannel(this);
}
@Override
public void onActivityResult(int requestCode, int resultCode,
Intent resultData)
{
+ super.onActivityResult(requestCode, resultCode, resultData);
_onActivityResult(requestCode, resultCode, resultData);
}
diff --git a/tools/android/packaging/xbmc/src/XBMCJsonRPC.java.in b/tools/android/packaging/xbmc/src/XBMCJsonRPC.java.in
index 6fe8992ad6..d3a29a48a1 100644
--- a/tools/android/packaging/xbmc/src/XBMCJsonRPC.java.in
+++ b/tools/android/packaging/xbmc/src/XBMCJsonRPC.java.in
@@ -7,6 +7,8 @@ import java.io.OutputStreamWriter;
import java.net.ConnectException;
import java.net.HttpURLConnection;
import java.net.URL;
+import java.util.List;
+import java.util.ArrayList;
import org.json.JSONArray;
import org.json.JSONObject;
@@ -25,6 +27,18 @@ import android.net.Uri;
import android.provider.BaseColumns;
import android.util.Log;
+import @APP_PACKAGE@.content.XBMCYTDLContentProvider;
+import @APP_PACKAGE@.model.Album;
+import @APP_PACKAGE@.model.Movie;
+import @APP_PACKAGE@.model.MusicVideo;
+import @APP_PACKAGE@.model.Song;
+import @APP_PACKAGE@.model.TVEpisode;
+import @APP_PACKAGE@.model.TVShow;
+import @APP_PACKAGE@.content.XBMCFileContentProvider;
+import @APP_PACKAGE@.model.File;
+import @APP_PACKAGE@.model.Media;
+import @APP_PACKAGE@.content.XBMCImageContentProvider;
+
public class XBMCJsonRPC
{
public final static String APP_NAME = "@APP_NAME@ Search";
@@ -46,30 +60,33 @@ public class XBMCJsonRPC
public final static String REQ_ID_MOVIES_ACTOR = "5";
public final static String REQ_ID_SHOWS_ACTOR = "6";
+ private final static int MAX_ITEMS = 10;
+
private static String TAG = "@APP_NAME@json";
- private String m_jsonURL = "http://localhost:8080";
+ private String m_xbmc_web_url = "http://localhost:8080";
private java.util.HashSet<Integer> mRecomendationIds = new java.util.HashSet<Integer>();
private int MAX_RECOMMENDATIONS = 3;
- // {"jsonrpc": "2.0", "method": "VideoLibrary.GetMovies", "params": { "filter": {"field": "playcount", "operator": "is", "value": "0"}, "limits": { "start" : 0, "end": 3}, "properties" : ["imdbnumber", "title", "tagline", "thumbnail", "fanart"], "sort": { "order": "descending", "method": "dateadded", "ignorearticle": true } }, "id": "1"}
+ private String GET_VERSION =
+ "{ \"jsonrpc\": \"2.0\", \"method\": \"JSONRPC.Version\", \"id\": 1 }";
private String RECOMMENDATION_MOVIES_JSON =
- "{\"jsonrpc\": \"2.0\", \"method\": \"VideoLibrary.GetMovies\", "
+ "{\"jsonrpc\": \"2.0\", \"method\": \"VideoLibrary.GetMovies\", "
+ "\"params\": { \"filter\": {\"field\": \"playcount\", \"operator\": \"is\", \"value\": \"0\"}, "
+ "\"limits\": { \"start\" : 0, \"end\": 10}, "
- + "\"properties\" : [\"imdbnumber\", \"title\", \"tagline\", \"thumbnail\", \"fanart\"], "
+ + "\"properties\" : [\"imdbnumber\", \"title\", \"tagline\", \"thumbnail\", \"fanart\", \"year\", \"runtime\", \"file\", \"plot\"], "
+ "\"sort\": { \"order\": \"descending\", \"method\": \"random\", \"ignorearticle\": true } }, "
+ "\"id\": \"1\"}";
private String RECOMMENDATIONS_SHOWS_JSON =
- "{\"jsonrpc\":\"2.0\",\"method\":\"VideoLibrary.GetTVShows\",\"params\":{\"filter\":{\"and\":[{\"field\":\"playcount\",\"operator\":\"is\",\"value\":\"0\"},{\"field\":\"plot\",\"operator\":\"isnot\",\"value\":\"\"}]},\"limits\":{\"start\":0,\"end\":10},\"properties\":[\"imdbnumber\",\"title\",\"plot\",\"thumbnail\",\"fanart\"],\"sort\":{\"order\":\"descending\",\"method\":\"lastplayed\",\"ignorearticle\":true}},\"id\":\"1\"}";
+ "{\"jsonrpc\":\"2.0\",\"method\":\"VideoLibrary.GetTVShows\",\"params\":{\"filter\":{\"and\":[{\"field\":\"playcount\",\"operator\":\"is\",\"value\":\"0\"},{\"field\":\"plot\",\"operator\":\"isnot\",\"value\":\"\"}]},\"limits\":{\"start\":0,\"end\":10},\"properties\":[\"imdbnumber\",\"title\",\"plot\",\"thumbnail\",\"fanart\", \"studio\"],\"sort\":{\"order\":\"descending\",\"method\":\"lastplayed\",\"ignorearticle\":true}},\"id\":\"1\"}";
private String RECOMMENDATIONS_ALBUMS_JSON =
- "{\"jsonrpc\": \"2.0\", \"method\": \"AudioLibrary.GetAlbums\", \"params\": { \"limits\": { \"start\" : 0, \"end\": 3}, \"properties\" : [\"title\", \"displayartist\", \"thumbnail\", \"fanart\"], \"sort\": { \"order\": \"descending\", \"method\": \"random\", \"ignorearticle\": true } }, \"id\": \"1\"}";
+ "{\"jsonrpc\": \"2.0\", \"method\": \"AudioLibrary.GetAlbums\", \"params\": { \"limits\": { \"start\" : 0, \"end\": 3}, \"properties\" : [\"title\", \"displayartist\", \"thumbnail\", \"fanart\"], \"sort\": { \"order\": \"descending\", \"method\": \"random\", \"ignorearticle\": true } }, \"id\": \"1\"}";
private String SEARCH_MOVIES_JSON =
- "{\"jsonrpc\": \"2.0\", \"method\": \"VideoLibrary.GetMovies\", "
+ "{\"jsonrpc\": \"2.0\", \"method\": \"VideoLibrary.GetMovies\", "
+ "\"params\": { \"filter\": {%s}, "
+ "\"limits\": { \"start\" : 0, \"end\": 10}, "
+ "\"properties\" : [\"imdbnumber\", \"title\", \"tagline\", \"thumbnail\", \"fanart\", \"year\", \"runtime\"], "
@@ -77,20 +94,41 @@ public class XBMCJsonRPC
+ "\"id\": \"%s\"}";
private String SEARCH_SHOWS_JSON =
- "{\"jsonrpc\":\"2.0\",\"method\":\"VideoLibrary.GetTVShows\",\"params\":{\"filter\":{%s},\"limits\":{\"start\":0,\"end\":10},\"properties\":[\"imdbnumber\",\"title\",\"plot\",\"thumbnail\",\"fanart\",\"year\"],\"sort\":{\"order\":\"descending\",\"method\":\"lastplayed\",\"ignorearticle\":true}},\"id\":\"%s\"}";
+ "{\"jsonrpc\":\"2.0\",\"method\":\"VideoLibrary.GetTVShows\",\"params\":{\"filter\":{%s},\"limits\":{\"start\":0,\"end\":10},\"properties\":[\"imdbnumber\",\"title\",\"plot\",\"thumbnail\",\"fanart\",\"year\"],\"sort\":{\"order\":\"descending\",\"method\":\"lastplayed\",\"ignorearticle\":true}},\"id\":\"%s\"}";
private String SEARCH_ALBUMS_JSON =
- "{\"jsonrpc\": \"2.0\", \"method\": \"AudioLibrary.GetAlbums\", \"params\": {\"filter\":{%s},\"limits\": { \"start\" : 0, \"end\": 10}, \"properties\" : [\"title\", \"displayartist\", \"thumbnail\", \"fanart\"], \"sort\": { \"order\": \"descending\", \"method\": \"dateadded\", \"ignorearticle\": true } }, \"id\": \"%s\"}";
+ "{\"jsonrpc\": \"2.0\", \"method\": \"AudioLibrary.GetAlbums\", \"params\": {\"filter\":{%s},\"limits\": { \"start\" : 0, \"end\": 10}, \"properties\" : [\"title\", \"displayartist\", \"thumbnail\", \"fanart\"], \"sort\": { \"order\": \"descending\", \"method\": \"dateadded\", \"ignorearticle\": true } }, \"id\": \"%s\"}";
private String SEARCH_ARTISTS_JSON =
- "{\"jsonrpc\": \"2.0\", \"method\": \"AudioLibrary.GetArtists\", \"params\": {\"filter\":{%s},\"limits\": { \"start\" : 0, \"end\": 10}, \"properties\" : [\"description\", \"thumbnail\", \"fanart\"], \"sort\": { \"order\": \"descending\", \"method\": \"dateadded\", \"ignorearticle\": true } }, \"id\": \"%s\"}";
+ "{\"jsonrpc\": \"2.0\", \"method\": \"AudioLibrary.GetArtists\", \"params\": {\"filter\":{%s},\"limits\": { \"start\" : 0, \"end\": 10}, \"properties\" : [\"description\", \"thumbnail\", \"fanart\"], \"sort\": { \"order\": \"descending\", \"method\": \"dateadded\", \"ignorearticle\": true } }, \"id\": \"%s\"}";
+
+ private String RETRIEVE_MOVIE_DETAILS =
+ "{ \"jsonrpc\": \"2.0\", \"method\": \"VideoLibrary.GetMovieDetails\", \"params\": { \"movieid\" : %s, \"properties\" : [\"imdbnumber\", \"title\", \"tagline\", \"thumbnail\", \"fanart\", \"year\", \"runtime\", \"file\", \"plot\", \"trailer\"] }, \"id\": \"%s\" }";
+
+ private String RETRIEVE_EPISODE_DETAILS =
+ "{ \"jsonrpc\": \"2.0\", \"method\": \"VideoLibrary.GetEpisodeDetails\", \"params\": { \"episodeid\" : %s, \"properties\" : [\"title\", \"tvshowid\", \"showtitle\", \"season\", \"episode\", \"thumbnail\", \"fanart\", \"file\"] }, \"id\": \"%s\" }";
+
+ private String RETRIEVE_TVSHOW_DETAILS =
+ "{ \"jsonrpc\": \"2.0\", \"method\": \"VideoLibrary.GetTVShowDetails\", \"params\": { \"tvshowid\" : %s, \"properties\" : [\"title\", \"studio\", \"thumbnail\", \"fanart\"] }, \"id\": \"%s\" }";
+
+ private String RETRIEVE_ALBUM_DETAILS =
+ "{ \"jsonrpc\": \"2.0\", \"method\": \"AudioLibrary.GetAlbumDetails\", \"params\": { \"albumid\" : %s, \"properties\" : [\"title\", \"displayartist\", \"thumbnail\", \"fanart\", \"artistid\"] }, \"id\": \"%s\" }";
+
+ private String RETRIEVE_SONG_DETAILS =
+ "{ \"jsonrpc\": \"2.0\", \"method\": \"AudioLibrary.GetSongDetails\", \"params\": { \"songid\" : %s, \"properties\" : [\"title\", \"displayartist\", \"thumbnail\", \"fanart\", \"albumid\", \"artistid\", \"file\"] }, \"id\": \"%s\" }";
+
+ private String RETRIEVE_MUSICVIDEO_DETAILS =
+ "{ \"jsonrpc\": \"2.0\", \"method\": \"VideoLibrary.GetMusicVideoDetails\", \"params\": { \"musicvideoid\" : %s, \"properties\" : [\"title\", \"artist\", \"thumbnail\", \"fanart\", \"file\"] }, \"id\": \"%s\" }";
+
+ private String RETRIEVE_FILE_ITEMS =
+ "{ \"jsonrpc\": \"2.0\", \"method\": \"Files.GetDirectory\", \"params\": { \"directory\" : \"%s\" }, \"id\": \"%s\" }";
private NotificationManager mNotificationManager;
public XBMCJsonRPC()
{
String jsonPort = XBMCProperties.getStringProperty("xbmc.jsonPort", "8080");
- m_jsonURL = "http://localhost:" + jsonPort;
+ m_xbmc_web_url = "http://localhost:" + jsonPort;
}
public String request_string(String jsonRequest)
@@ -98,12 +136,12 @@ public class XBMCJsonRPC
try
{
//Log.d(TAG, "JSON in: " + jsonRequest);
- //Log.d(TAG, "JSON url: " + m_jsonURL);
+ //Log.d(TAG, "JSON url: " + m_xbmc_web_url);
String returnStr = null;
StringBuilder strbuilder = new StringBuilder();
- URL url = new URL(m_jsonURL + "/jsonrpc");
+ URL url = new URL(m_xbmc_web_url + "/jsonrpc");
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setDoOutput(true);
connection.setDoInput(true);
@@ -205,14 +243,14 @@ public class XBMCJsonRPC
try
{
JSONObject req = request_object("{\"jsonrpc\": \"2.0\", \"method\": \"Files.PrepareDownload\", \"params\": { \"path\": \""
- + src + "\"}, \"id\": \"1\"}");
+ + src + "\"}, \"id\": \"1\"}");
if (req == null || req.isNull("result"))
return null;
JSONObject result = req.getJSONObject("result");
String surl = result.getJSONObject("details").getString("path");
- URL url = new URL(m_jsonURL + "/" + surl);
+ URL url = new URL(m_xbmc_web_url + "/" + surl);
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
String auth = XBMCProperties.getJsonAuthorization();
if (!auth.isEmpty())
@@ -222,101 +260,112 @@ public class XBMCJsonRPC
InputStream input = connection.getInputStream();
Bitmap myBitmap = BitmapFactory.decodeStream(input);
return myBitmap;
- }
- catch (Exception e)
+ } catch (Exception e)
{
e.printStackTrace();
return null;
}
}
- public String getBitmapUrl(String src)
+ public String getDownloadUrl(String src)
{
try
{
JSONObject req = request_object("{\"jsonrpc\": \"2.0\", \"method\": \"Files.PrepareDownload\", \"params\": { \"path\": \""
- + src + "\"}, \"id\": \"1\"}");
+ + src + "\"}, \"id\": \"1\"}");
if (req == null || req.isNull("result"))
- return null;
+ return "";
JSONObject result = req.getJSONObject("result");
String surl = result.getJSONObject("details").getString("path");
- return (m_jsonURL + "/" + surl);
- }
- catch (Exception e)
+ return (m_xbmc_web_url + "/" + surl);
+ } catch (Exception e)
{
e.printStackTrace();
- return null;
+ return "";
}
}
- public Cursor search(String query)
+ public boolean Ping()
{
- String[] menuCols = new String[]{
- BaseColumns._ID,
- COLUMN_TITLE,
- COLUMN_TAGLINE,
- COLUMN_THUMB,
- COLUMN_FANART,
- };
- MatrixCursor mc = new MatrixCursor(menuCols);
-
try
{
- JSONObject req = request_object(String.format(SEARCH_MOVIES_JSON, /*"\"operator\": \"contains\", \"field\": \"title\", \"value\": \"" + query + "\"", limit));*/
- "\"or\": [" +
- "{\"operator\": \"contains\", \"field\": \"title\", \"value\": \"" + query + "\"}," +
- "{\"operator\": \"contains\", \"field\": \"originaltitle\", \"value\": \"" + query + "\"}," +
- "{\"operator\": \"contains\", \"field\": \"set\", \"value\": \"" + query + "\"}," +
- "{\"operator\": \"contains\", \"field\": \"actor\", \"value\": \"" + query + "\"}," +
- "{\"operator\": \"contains\", \"field\": \"director\", \"value\": \"" + query + "\"}]"));
-
+ JSONObject req = request_object(GET_VERSION);
if (req == null || req.isNull("result"))
- return null;
-
- JSONObject results = req.getJSONObject("result");
- JSONArray movies = results.getJSONArray("movies");
-
- for (int i = 0; i < movies.length(); ++i)
- {
- JSONObject movie = movies.getJSONObject(i);
- mc.addRow(new Object[]{movie.getString("movieid"), movie.getString("title"), movie.getString("tagline"), movie.getString("thumbnail"), movie.getString("fanart")});
- }
+ return false;
}
catch (Exception e)
{
- e.printStackTrace();
- return null;
+ return false;
}
+ return true;
+ }
- try
- {
- JSONObject req = request_object(String.format(SEARCH_SHOWS_JSON, /*"\"operator\": \"contains\", \"field\": \"title\", \"value\": \"" + query + "\"", limit));*/
- "\"or\": [" +
- "{\"operator\": \"contains\", \"field\": \"title\", \"value\": \"" + query + "\"}," +
- "{\"operator\": \"contains\", \"field\": \"actor\", \"value\": \"" + query + "\"}," +
- "{\"operator\": \"contains\", \"field\": \"director\", \"value\": \"" + query + "\"}]"));
+ public Cursor search(String query)
+ {
+ String[] menuCols = new String[] {
+ BaseColumns._ID,
+ COLUMN_TITLE,
+ COLUMN_TAGLINE,
+ COLUMN_THUMB,
+ COLUMN_FANART,
+ };
+ MatrixCursor mc = new MatrixCursor(menuCols);
- if (req == null || req.isNull("result"))
+ try
+ {
+ JSONObject req = request_object(String.format(SEARCH_MOVIES_JSON, /*"\"operator\": \"contains\", \"field\": \"title\", \"value\": \"" + query + "\"", limit));*/
+ "\"or\": [" +
+ "{\"operator\": \"contains\", \"field\": \"title\", \"value\": \"" + query + "\"}," +
+ "{\"operator\": \"contains\", \"field\": \"originaltitle\", \"value\": \"" + query + "\"}," +
+ "{\"operator\": \"contains\", \"field\": \"set\", \"value\": \"" + query + "\"}," +
+ "{\"operator\": \"contains\", \"field\": \"actor\", \"value\": \"" + query + "\"}," +
+ "{\"operator\": \"contains\", \"field\": \"director\", \"value\": \"" + query + "\"}]"));
+
+ if (req == null || req.isNull("result"))
+ return null;
+
+ JSONObject results = req.getJSONObject("result");
+ JSONArray movies = results.getJSONArray("movies");
+
+ for (int i = 0; i < movies.length(); ++i)
+ {
+ JSONObject movie = movies.getJSONObject(i);
+ mc.addRow(new Object[]{movie.getString("movieid"), movie.getString("title"), movie.getString("tagline"), movie.getString("thumbnail"), movie.getString("fanart")});
+ }
+ } catch (Exception e)
+ {
+ e.printStackTrace();
return null;
+ }
- JSONObject results = req.getJSONObject("result");
- JSONArray tvshows = results.getJSONArray("tvshows");
+ try
+ {
+ JSONObject req = request_object(String.format(SEARCH_SHOWS_JSON, /*"\"operator\": \"contains\", \"field\": \"title\", \"value\": \"" + query + "\"", limit));*/
+ "\"or\": [" +
+ "{\"operator\": \"contains\", \"field\": \"title\", \"value\": \"" + query + "\"}," +
+ "{\"operator\": \"contains\", \"field\": \"actor\", \"value\": \"" + query + "\"}," +
+ "{\"operator\": \"contains\", \"field\": \"director\", \"value\": \"" + query + "\"}]"));
+
+ if (req == null || req.isNull("result"))
+ return null;
- for (int i = 0; i < tvshows.length(); ++i)
+ JSONObject results = req.getJSONObject("result");
+ JSONArray tvshows = results.getJSONArray("tvshows");
+
+ for (int i = 0; i < tvshows.length(); ++i)
+ {
+ JSONObject tvshow = tvshows.getJSONObject(i);
+ mc.addRow(new Object[]{tvshow.getString("movieid"), tvshow.getString("title"), tvshow.getString("plot"), tvshow.getString("thumbnail"), tvshow.getString("fanart")});
+ }
+ } catch (Exception e)
{
- JSONObject tvshow = tvshows.getJSONObject(i);
- mc.addRow(new Object[]{tvshow.getString("movieid"), tvshow.getString("title"), tvshow.getString("plot"), tvshow.getString("thumbnail"), tvshow.getString("fanart")});
+ e.printStackTrace();
+ return null;
}
- }
- catch (Exception e)
- {
- e.printStackTrace();
- return null;
- }
- return mc;
+ return mc;
}
public Cursor getSuggestions(String query, int limit)
@@ -325,52 +374,52 @@ public class XBMCJsonRPC
int totCount = 0;
String[] menuCols = new String[]
- {
- BaseColumns._ID,
- SearchManager.SUGGEST_COLUMN_TEXT_1,
- SearchManager.SUGGEST_COLUMN_TEXT_2,
- SearchManager.SUGGEST_COLUMN_ICON_1,
- SearchManager.SUGGEST_COLUMN_RESULT_CARD_IMAGE,
- SearchManager.SUGGEST_COLUMN_INTENT_ACTION,
- SearchManager.SUGGEST_COLUMN_INTENT_DATA,
- SearchManager.SUGGEST_COLUMN_VIDEO_WIDTH,
- SearchManager.SUGGEST_COLUMN_VIDEO_HEIGHT,
- SearchManager.SUGGEST_COLUMN_PRODUCTION_YEAR,
- SearchManager.SUGGEST_COLUMN_DURATION,
- SearchManager.SUGGEST_COLUMN_SHORTCUT_ID
- };
+ {
+ BaseColumns._ID,
+ SearchManager.SUGGEST_COLUMN_TEXT_1,
+ SearchManager.SUGGEST_COLUMN_TEXT_2,
+ SearchManager.SUGGEST_COLUMN_ICON_1,
+ SearchManager.SUGGEST_COLUMN_RESULT_CARD_IMAGE,
+ SearchManager.SUGGEST_COLUMN_INTENT_ACTION,
+ SearchManager.SUGGEST_COLUMN_INTENT_DATA,
+ SearchManager.SUGGEST_COLUMN_VIDEO_WIDTH,
+ SearchManager.SUGGEST_COLUMN_VIDEO_HEIGHT,
+ SearchManager.SUGGEST_COLUMN_PRODUCTION_YEAR,
+ SearchManager.SUGGEST_COLUMN_DURATION,
+ SearchManager.SUGGEST_COLUMN_SHORTCUT_ID
+ };
MatrixCursor mc = new MatrixCursor(menuCols);
String str_req = "[" +
- String.format(SEARCH_MOVIES_JSON,
- "\"or\": [" +
- "{\"operator\": \"contains\", \"field\": \"title\", \"value\": \"" + query + "\"}," +
- "{\"operator\": \"contains\", \"field\": \"originaltitle\", \"value\": \"" + query + "\"}," +
- "{\"operator\": \"contains\", \"field\": \"set\", \"value\": \"" + query + "\"}]", REQ_ID_MOVIES) +
- "," +
- String.format(SEARCH_SHOWS_JSON,
- "\"or\": [" +
- "{\"operator\": \"contains\", \"field\": \"title\", \"value\": \"" + query + "\"}," +
- "{\"operator\": \"contains\", \"field\": \"originaltitle\", \"value\": \"" + query + "\"}]", REQ_ID_SHOWS) +
- "," +
- String.format(SEARCH_ALBUMS_JSON,
- "\"or\": [" +
- "{\"operator\": \"contains\", \"field\": \"album\", \"value\": \"" + query + "\"}," +
- "{\"operator\": \"contains\", \"field\": \"label\", \"value\": \"" + query + "\"}]", REQ_ID_ALBUMS) +
- "," +
- String.format(SEARCH_ARTISTS_JSON,
- "\"operator\": \"contains\", \"field\": \"artist\", \"value\": \"" + query + "\"", REQ_ID_ARTISTS) +
- "," +
- String.format(SEARCH_MOVIES_JSON,
- "\"or\": [" +
- "{\"operator\": \"contains\", \"field\": \"actor\", \"value\": \"" + query + "\"}," +
- "{\"operator\": \"contains\", \"field\": \"director\", \"value\": \"" + query + "\"}]", REQ_ID_MOVIES_ACTOR) +
- "," +
- String.format(SEARCH_SHOWS_JSON,
- "\"or\": [" +
- "{\"operator\": \"contains\", \"field\": \"actor\", \"value\": \"" + query + "\"}," +
- "{\"operator\": \"contains\", \"field\": \"director\", \"value\": \"" + query + "\"}]", REQ_ID_SHOWS_ACTOR) +
- "]";
+ String.format(SEARCH_MOVIES_JSON,
+ "\"or\": [" +
+ "{\"operator\": \"contains\", \"field\": \"title\", \"value\": \"" + query + "\"}," +
+ "{\"operator\": \"contains\", \"field\": \"originaltitle\", \"value\": \"" + query + "\"}," +
+ "{\"operator\": \"contains\", \"field\": \"set\", \"value\": \"" + query + "\"}]", REQ_ID_MOVIES) +
+ "," +
+ String.format(SEARCH_SHOWS_JSON,
+ "\"or\": [" +
+ "{\"operator\": \"contains\", \"field\": \"title\", \"value\": \"" + query + "\"}," +
+ "{\"operator\": \"contains\", \"field\": \"originaltitle\", \"value\": \"" + query + "\"}]", REQ_ID_SHOWS) +
+ "," +
+ String.format(SEARCH_ALBUMS_JSON,
+ "\"or\": [" +
+ "{\"operator\": \"contains\", \"field\": \"album\", \"value\": \"" + query + "\"}," +
+ "{\"operator\": \"contains\", \"field\": \"label\", \"value\": \"" + query + "\"}]", REQ_ID_ALBUMS) +
+ "," +
+ String.format(SEARCH_ARTISTS_JSON,
+ "\"operator\": \"contains\", \"field\": \"artist\", \"value\": \"" + query + "\"", REQ_ID_ARTISTS) +
+ "," +
+ String.format(SEARCH_MOVIES_JSON,
+ "\"or\": [" +
+ "{\"operator\": \"contains\", \"field\": \"actor\", \"value\": \"" + query + "\"}," +
+ "{\"operator\": \"contains\", \"field\": \"director\", \"value\": \"" + query + "\"}]", REQ_ID_MOVIES_ACTOR) +
+ "," +
+ String.format(SEARCH_SHOWS_JSON,
+ "\"or\": [" +
+ "{\"operator\": \"contains\", \"field\": \"actor\", \"value\": \"" + query + "\"}," +
+ "{\"operator\": \"contains\", \"field\": \"director\", \"value\": \"" + query + "\"}]", REQ_ID_SHOWS_ACTOR) +
+ "]";
JSONArray res_array = request_array(str_req);
if (res_array == null)
@@ -398,8 +447,7 @@ public class XBMCJsonRPC
if (id.equals(REQ_ID_MOVIES) || ((nb_movies + nb_shows) < 3 && id.equals(REQ_ID_MOVIES_ACTOR)))
{
- searchmovies:
- try
+ searchmovies: try
{
if (resp.isNull("result"))
break searchmovies;
@@ -428,8 +476,8 @@ public class XBMCJsonRPC
movie.getString("movieid"),
movie.getString("title"),
movie.getString("tagline"),
- XBMCImageContentProvider.GetImageUri(getBitmapUrl(movie.getString("thumbnail"))).toString(),
- XBMCImageContentProvider.GetImageUri(getBitmapUrl(movie.getString("thumbnail"))).toString(),
+ XBMCImageContentProvider.GetImageUri(getDownloadUrl(movie.getString("thumbnail"))).toString(),
+ XBMCImageContentProvider.GetImageUri(getDownloadUrl(movie.getString("thumbnail"))).toString(),
Intent.ACTION_VIEW,
Uri.parse("videodb://movies/titles/" + movie.getString("movieid")),
0,
@@ -441,18 +489,16 @@ public class XBMCJsonRPC
nb_movies++;
totCount++;
}
- }
- catch (Exception e)
+ } catch (Exception e)
{
e.printStackTrace();
}
}
else if (id.equals(REQ_ID_SHOWS) || ((nb_movies + nb_shows) < 3 && id.equals(REQ_ID_SHOWS_ACTOR)))
{
- searchtv:
- try
+ searchtv: try
{
- if (resp.isNull("result"))
+ if(resp.isNull("result"))
break searchtv;
JSONObject results = resp.getJSONObject("result");
if (results == null || results.isNull("tvshows"))
@@ -478,8 +524,8 @@ public class XBMCJsonRPC
tvshow.getString("tvshowid"),
tvshow.getString("title"),
tvshow.getString("plot"),
- XBMCImageContentProvider.GetImageUri(getBitmapUrl(tvshow.getString("thumbnail"))).toString(),
- XBMCImageContentProvider.GetImageUri(getBitmapUrl(tvshow.getString("thumbnail"))).toString(),
+ XBMCImageContentProvider.GetImageUri(getDownloadUrl(tvshow.getString("thumbnail"))).toString(),
+ XBMCImageContentProvider.GetImageUri(getDownloadUrl(tvshow.getString("thumbnail"))).toString(),
Intent.ACTION_GET_CONTENT,
Uri.parse("videodb://tvshows/titles/" + tvshow.getString("tvshowid") + "/"),
0,
@@ -491,16 +537,14 @@ public class XBMCJsonRPC
nb_shows++;
totCount++;
}
- }
- catch (Exception e)
+ } catch (Exception e)
{
e.printStackTrace();
}
}
else if (id.equals(REQ_ID_ALBUMS))
{
- searchalbums:
- try
+ searchalbums: try
{
if (resp.isNull("result"))
break searchalbums;
@@ -513,32 +557,30 @@ public class XBMCJsonRPC
{
JSONObject album = albums.getJSONObject(i);
mc.addRow(new Object[]
- {
- album.getString("albumid"),
- album.getString("title"),
- album.getString("displayartist"),
- XBMCImageContentProvider.GetImageUri(getBitmapUrl(album.getString("thumbnail"))).toString(),
- XBMCImageContentProvider.GetImageUri(getBitmapUrl(album.getString("thumbnail"))).toString(),
- Intent.ACTION_GET_CONTENT,
- Uri.parse("musicdb://albums/" + album.getString("albumid") + "/"),
- 0,
- 0,
- 0,
- 0,
- -1
- });
+ {
+ album.getString("albumid"),
+ album.getString("title"),
+ album.getString("displayartist"),
+ XBMCImageContentProvider.GetImageUri(getDownloadUrl(album.getString("thumbnail"))).toString(),
+ XBMCImageContentProvider.GetImageUri(getDownloadUrl(album.getString("thumbnail"))).toString(),
+ Intent.ACTION_GET_CONTENT,
+ Uri.parse("musicdb://albums/" + album.getString("albumid") + "/"),
+ 0,
+ 0,
+ 0,
+ 0,
+ -1
+ });
totCount++;
}
- }
- catch (Exception e)
+ } catch (Exception e)
{
e.printStackTrace();
}
}
else if (id.equals(REQ_ID_ARTISTS))
{
- searchartists:
- try
+ searchartists: try
{
if (resp.isNull("result"))
break searchartists;
@@ -551,24 +593,23 @@ public class XBMCJsonRPC
{
JSONObject artist = artists.getJSONObject(i);
mc.addRow(new Object[]
- {
- artist.getString("artistid"),
- artist.getString("artist"),
- artist.getString("description"),
- XBMCImageContentProvider.GetImageUri(getBitmapUrl(artist.getString("thumbnail"))).toString(),
- XBMCImageContentProvider.GetImageUri(getBitmapUrl(artist.getString("thumbnail"))).toString(),
- Intent.ACTION_GET_CONTENT,
- Uri.parse("musicdb://artists/" + artist.getString("artistid") + "/"),
- 0,
- 0,
- 0,
- 0,
- -1
- });
+ {
+ artist.getString("artistid"),
+ artist.getString("artist"),
+ artist.getString("description"),
+ XBMCImageContentProvider.GetImageUri(getDownloadUrl(artist.getString("thumbnail"))).toString(),
+ XBMCImageContentProvider.GetImageUri(getDownloadUrl(artist.getString("thumbnail"))).toString(),
+ Intent.ACTION_GET_CONTENT,
+ Uri.parse("musicdb://artists/" + artist.getString("artistid") + "/"),
+ 0,
+ 0,
+ 0,
+ 0,
+ -1
+ });
totCount++;
}
- }
- catch (Exception e)
+ } catch (Exception e)
{
e.printStackTrace();
}
@@ -584,13 +625,13 @@ public class XBMCJsonRPC
{
mNotificationManager = (NotificationManager) ctx.getSystemService(Context.NOTIFICATION_SERVICE);
}
- for (Integer id : mRecomendationIds)
+ for(Integer id : mRecomendationIds)
mNotificationManager.cancel(id);
mRecomendationIds.clear();
XBMCRecommendationBuilder builder = new XBMCRecommendationBuilder()
- .setContext(ctx)
- .setSmallIcon(R.drawable.notif_icon);
+ .setContext(ctx)
+ .setSmallIcon(R.drawable.notif_icon);
JSONObject rep = request_object(RECOMMENDATION_MOVIES_JSON);
if (rep != null && !rep.isNull("result"))
@@ -609,13 +650,13 @@ public class XBMCJsonRPC
int id = Integer.parseInt(movie.getString("movieid")) + 1000000;
final XBMCRecommendationBuilder notificationBuilder = builder
- .setBackground(
- XBMCImageContentProvider.GetImageUri(
- getBitmapUrl(movie.getString("fanart"))).toString())
- .setId(id).setPriority(MAX_RECOMMENDATIONS - count)
- .setTitle(movie.getString("title"))
- .setDescription(movie.getString("tagline"))
- .setIntent(buildPendingMovieIntent(ctx, movie));
+ .setBackground(
+ XBMCImageContentProvider.GetImageUri(
+ getDownloadUrl(movie.getString("fanart"))).toString())
+ .setId(id).setPriority(MAX_RECOMMENDATIONS - count)
+ .setTitle(movie.getString("title"))
+ .setDescription(movie.getString("tagline"))
+ .setIntent(buildPendingMovieIntent(ctx, movie));
Bitmap bitmap = getBitmap(movie.getString("thumbnail"));
notificationBuilder.setBitmap(bitmap);
@@ -623,14 +664,12 @@ public class XBMCJsonRPC
mNotificationManager.notify(id, notification);
mRecomendationIds.add(id);
++count;
- }
- catch (Exception e)
+ } catch (Exception e)
{
continue;
}
}
- }
- catch (Exception e)
+ } catch (Exception e)
{
e.printStackTrace();
}
@@ -653,13 +692,13 @@ public class XBMCJsonRPC
int id = Integer.parseInt(tvshow.getString("tvshowid")) + 2000000;
final XBMCRecommendationBuilder notificationBuilder = builder
- .setBackground(
- XBMCImageContentProvider.GetImageUri(
- getBitmapUrl(tvshow.getString("fanart"))).toString())
- .setId(id).setPriority(MAX_RECOMMENDATIONS - count)
- .setTitle(tvshow.getString("title"))
- .setDescription(tvshow.getString("plot"))
- .setIntent(buildPendingShowIntent(ctx, tvshow));
+ .setBackground(
+ XBMCImageContentProvider.GetImageUri(
+ getDownloadUrl(tvshow.getString("fanart"))).toString())
+ .setId(id).setPriority(MAX_RECOMMENDATIONS - count)
+ .setTitle(tvshow.getString("title"))
+ .setDescription(tvshow.getString("plot"))
+ .setIntent(buildPendingShowIntent(ctx, tvshow));
Bitmap bitmap = getBitmap(tvshow.getString("thumbnail"));
notificationBuilder.setBitmap(bitmap);
@@ -667,15 +706,13 @@ public class XBMCJsonRPC
mNotificationManager.notify(id, notification);
mRecomendationIds.add(id);
++count;
- }
- catch (Exception e)
+ } catch (Exception e)
{
continue;
}
}
- }
- catch (Exception e)
+ } catch (Exception e)
{
e.printStackTrace();
}
@@ -698,13 +735,13 @@ public class XBMCJsonRPC
int id = Integer.parseInt(album.getString("albumid")) + 3000000;
final XBMCRecommendationBuilder notificationBuilder = builder
- .setBackground(
- XBMCImageContentProvider.GetImageUri(
- getBitmapUrl(album.getString("fanart"))).toString())
- .setId(id).setPriority(MAX_RECOMMENDATIONS - count)
- .setTitle(album.getString("title"))
- .setDescription(album.getString("displayartist"))
- .setIntent(buildPendingAlbumIntent(ctx, album));
+ .setBackground(
+ XBMCImageContentProvider.GetImageUri(
+ getDownloadUrl(album.getString("fanart"))).toString())
+ .setId(id).setPriority(MAX_RECOMMENDATIONS - count)
+ .setTitle(album.getString("title"))
+ .setDescription(album.getString("displayartist"))
+ .setIntent(buildPendingAlbumIntent(ctx, album));
Bitmap bitmap = getBitmap(album.getString("thumbnail"));
notificationBuilder.setBitmap(bitmap);
@@ -712,15 +749,13 @@ public class XBMCJsonRPC
mNotificationManager.notify(id, notification);
mRecomendationIds.add(id);
++count;
- }
- catch (Exception e)
+ } catch (Exception e)
{
continue;
}
}
- }
- catch (Exception e)
+ } catch (Exception e)
{
e.printStackTrace();
}
@@ -739,8 +774,7 @@ public class XBMCJsonRPC
//detailsIntent.putExtra(MovieDetailsActivity.NOTIFICATION_ID, id);
return PendingIntent.getActivity(ctx, 0, detailsIntent, PendingIntent.FLAG_CANCEL_CURRENT);
- }
- catch (Exception e)
+ } catch (Exception e)
{
e.printStackTrace();
return null;
@@ -756,8 +790,7 @@ public class XBMCJsonRPC
detailsIntent.setData(Uri.parse("videodb://tvshows/titles/" + tvshow.getString("tvshowid") + "/"));
return PendingIntent.getActivity(ctx, 0, detailsIntent, PendingIntent.FLAG_CANCEL_CURRENT);
- }
- catch (Exception e)
+ } catch (Exception e)
{
e.printStackTrace();
return null;
@@ -773,11 +806,495 @@ public class XBMCJsonRPC
detailsIntent.setData(Uri.parse("musicdb://albums/" + tvshow.getString("albumid") + "/"));
return PendingIntent.getActivity(ctx, 0, detailsIntent, PendingIntent.FLAG_CANCEL_CURRENT);
+ } catch (Exception e)
+ {
+ e.printStackTrace();
+ return null;
}
- catch (Exception e)
+ }
+
+ public List<File> getFiles(String url)
+ {
+ List<File> files = new ArrayList<File>();
+
+ try
+ {
+ JSONObject req = request_object(String.format(RETRIEVE_FILE_ITEMS, url, "1"));
+
+ if (req == null || req.isNull("result"))
+ return files;
+
+ JSONObject results = req.getJSONObject("result");
+ JSONArray filesA = results.getJSONArray("files");
+
+ for (int i = 0; i < filesA.length(); ++i)
+ {
+ JSONObject fileO = filesA.getJSONObject(i);
+ Uri uri = Uri.parse(fileO.getString("file"));
+ File file = File.createFile(fileO.getString("label"), fileO.getString("filetype"), XBMCFileContentProvider.buildUri(uri.getPath()).toString());
+ if (fileO.has("id"))
+ file.setId(fileO.getInt("id"));
+ if (fileO.has("type") && !fileO.getString("type").equals("unknown"))
+ file.setMediatype(fileO.getString("type"));
+ files.add(file);
+ }
+ } catch (Exception e)
{
e.printStackTrace();
+ }
+
+ return files;
+ }
+
+ private Movie createMovieFromJson(JSONObject details)
+ {
+ Movie med = new Movie();
+
+ try
+ {
+ med.setId(details.getInt("movieid"));
+ med.setTitle(details.getString("title"));
+ med.setDescription(details.getString("tagline"));
+ if (details.has("thumbnail") && !details.getString("thumbnail").isEmpty())
+ {
+ String url = getDownloadUrl(details.getString("thumbnail"));
+ if (!url.isEmpty())
+ med.setCardImageUrl(XBMCImageContentProvider.GetImageUri(url).toString());
+ }
+ med.setCardImageAspectRatio("2:3");
+ if (details.has("fanart") && !details.getString("fanart").isEmpty())
+ {
+ String url = getDownloadUrl(details.getString("fanart"));
+ if (!url.isEmpty())
+ med.setBackgroundImageUrl(XBMCImageContentProvider.GetImageUri(url).toString());
+ }
+ med.setXbmcUrl("videodb://movies/titles/" + details.getString("movieid") + "?showinfo=true");
+
+ if (details.has("trailer") && !details.getString("trailer").isEmpty())
+ {
+ String trailer_url = details.getString("trailer");
+ if (trailer_url.startsWith("plugin://plugin.video.youtube"))
+ {
+ Uri u = Uri.parse(trailer_url);
+ String videoid = u.getQueryParameter("videoid");
+ String yturl = Uri.parse(m_xbmc_web_url)
+ .buildUpon()
+ .appendPath("addons").appendPath("webinterface.youtube-dl")
+ .appendQueryParameter("url", "http://www.youtube.com/watch?v=" + videoid)
+ .build()
+ .toString();
+ med.setVideoUrl(XBMCYTDLContentProvider.GetYTDLUri(yturl).toString());
+ Log.d(TAG, "createMovieFromJson: " + med.getVideoUrl());
+ }
+ else
+ med.setVideoUrl(trailer_url);
+ }
+ med.setCategory(Media.MEDIA_TYPE_MOVIE);
+
+ med.setYear(details.getString("year"));
+ med.setPlot(details.getString("plot"));
+ }
+ catch (Exception e)
+ {
+ return null;
+ }
+
+ return med;
+ }
+
+ private TVShow createTVShowFromJson(JSONObject details)
+ {
+ TVShow med = new TVShow();
+
+ try
+ {
+ med.setId(details.getInt("tvshowid"));
+ med.setTitle(details.getString("title"));
+ JSONArray ja = details.getJSONArray("studio");
+ if (ja.length() > 0)
+ med.setDescription(ja.getString(0));
+ if (details.has("thumbnail") && !details.getString("thumbnail").isEmpty())
+ {
+ String url = getDownloadUrl(details.getString("thumbnail"));
+ if (!url.isEmpty())
+ med.setCardImageUrl(XBMCImageContentProvider.GetImageUri(url).toString());
+ }
+ med.setCardImageAspectRatio("2:3");
+ if (details.has("fanart") && !details.getString("fanart").isEmpty())
+ {
+ String url = getDownloadUrl(details.getString("fanart"));
+ if (!url.isEmpty())
+ med.setBackgroundImageUrl(XBMCImageContentProvider.GetImageUri(url).toString());
+ }
+ med.setXbmcUrl("videodb://tvshows/titles/" + details.getInt("tvshowid") + "/");
+ med.setCategory(Media.MEDIA_TYPE_TVSHOW);
+ }
+ catch (Exception e)
+ {
+ return null;
+ }
+
+ return med;
+ }
+
+ private TVEpisode createTVEpisodeFromJson(JSONObject details)
+ {
+ TVEpisode med = new TVEpisode();
+
+ try
+ {
+ med.setId(details.getInt("episodeid"));
+ med.setTitle(details.getString("title"));
+ med.setDescription(details.getString("showtitle"));
+ if (details.has("thumbnail") && !details.getString("thumbnail").isEmpty())
+ {
+ String url = getDownloadUrl(details.getString("thumbnail"));
+ if (!url.isEmpty())
+ med.setCardImageUrl(XBMCImageContentProvider.GetImageUri(url).toString());
+ }
+ med.setCardImageAspectRatio("16:9");
+ if (details.has("fanart") && !details.getString("fanart").isEmpty())
+ {
+ String url = getDownloadUrl(details.getString("fanart"));
+ if (!url.isEmpty())
+ med.setBackgroundImageUrl(XBMCImageContentProvider.GetImageUri(url).toString());
+ }
+ med.setXbmcUrl("videodb://tvshows/titles/" + details.getInt("tvshowid") + "/" + details.getInt("episodeid") + "?showinfo=true");
+/*
+ String url = getDownloadUrl(details.getString("file"));
+ if (!url.isEmpty())
+ med.setVideoUrl(url);
+*/
+ med.setCategory(Media.MEDIA_TYPE_TVEPISODE);
+
+ med.setSeason(details.getInt("season"));
+ med.setEpisode(details.getInt("episode"));
+ }
+ catch (Exception e)
+ {
+ return null;
+ }
+
+ return med;
+ }
+
+ private Album createAlbumFromJson(JSONObject details)
+ {
+ Album med = new Album();
+
+ try
+ {
+ med.setId(details.getInt("albumid"));
+ med.setTitle(details.getString("title"));
+ med.setDescription(details.getString("displayartist"));
+ if (details.has("thumbnail") && !details.getString("thumbnail").isEmpty())
+ {
+ String url = getDownloadUrl(details.getString("thumbnail"));
+ if (!url.isEmpty())
+ med.setCardImageUrl(XBMCImageContentProvider.GetImageUri(url).toString());
+ }
+ med.setCardImageAspectRatio("1:1");
+ if (details.has("fanart") && !details.getString("fanart").isEmpty())
+ {
+ String url = getDownloadUrl(details.getString("fanart"));
+ if (!url.isEmpty())
+ med.setBackgroundImageUrl(XBMCImageContentProvider.GetImageUri(url).toString());
+ }
+ med.setXbmcUrl("musicdb://albums/" + details.getString("albumid") + "/");
+ med.setCategory(Media.MEDIA_TYPE_ALBUM);
+ }
+ catch (Exception e)
+ {
+ return null;
+ }
+
+ return med;
+ }
+
+ private Song createSongFromJson(JSONObject details)
+ {
+ Song med = new Song();
+
+ try
+ {
+ med.setId(details.getInt("songid"));
+ med.setTitle(details.getString("title"));
+ med.setDescription(details.getString("displayartist"));
+ if (details.has("thumbnail") && !details.getString("thumbnail").isEmpty())
+ {
+ String url = getDownloadUrl(details.getString("thumbnail"));
+ if (!url.isEmpty())
+ med.setCardImageUrl(XBMCImageContentProvider.GetImageUri(url).toString());
+ }
+ med.setCardImageAspectRatio("1:1");
+ if (details.has("fanart") && !details.getString("fanart").isEmpty())
+ {
+ String url = getDownloadUrl(details.getString("fanart"));
+ if (!url.isEmpty())
+ med.setBackgroundImageUrl(XBMCImageContentProvider.GetImageUri(url).toString());
+ }
+
+ String extension = "";
+ if (details.has("file") && !details.getString("file").isEmpty())
+ {
+ String file = details.getString("file");
+ extension = file.substring(file.lastIndexOf("."));
+ }
+
+ if (details.has("albumid") && !details.getString("albumid").isEmpty())
+ med.setXbmcUrl("musicdb://albums/" + details.getString("albumid") + "/" + details.getInt("songid") + extension);
+ else
+ med.setXbmcUrl("musicdb://songs/" + details.getInt("songid") + extension);
+
+/*
+ String url = getDownloadUrl(details.getString("file"));
+ if (!url.isEmpty())
+ med.setVideoUrl(url);
+*/
+
+ med.setCategory(Media.MEDIA_TYPE_SONG);
+ }
+ catch (Exception e)
+ {
return null;
}
+
+ return med;
+ }
+
+ private MusicVideo createMusicvideoFromJson(JSONObject details)
+ {
+ MusicVideo med = new MusicVideo();
+
+ try
+ {
+ med.setId(details.getInt("musicvideoid"));
+ med.setTitle(details.getString("title"));
+ JSONArray ja = details.getJSONArray("artist");
+ if (ja.length() > 0)
+ med.setDescription(ja.getString(0));
+ if (details.has("thumbnail") && !details.getString("thumbnail").isEmpty())
+ {
+ String url = getDownloadUrl(details.getString("thumbnail"));
+ if (!url.isEmpty())
+ med.setCardImageUrl(XBMCImageContentProvider.GetImageUri(url).toString());
+ }
+ med.setCardImageAspectRatio("1:1");
+ if (details.has("fanart") && !details.getString("fanart").isEmpty())
+ {
+ String url = getDownloadUrl(details.getString("fanart"));
+ if (!url.isEmpty())
+ med.setBackgroundImageUrl(XBMCImageContentProvider.GetImageUri(url).toString());
+ }
+
+ med.setXbmcUrl("videodb://musicvideos/titles/" + details.getInt("musicvideoid"));
+
+ String url = getDownloadUrl(details.getString("file"));
+ if (!url.isEmpty())
+ med.setVideoUrl(url);
+
+ med.setCategory(Media.MEDIA_TYPE_MUSICVIDEO);
+ }
+ catch (Exception e)
+ {
+ return null;
+ }
+
+ return med;
+ }
+
+ public List<Media> getSuggestions()
+ {
+ List<Media> medias = new ArrayList<Media>();
+
+ JSONObject rep = request_object(RECOMMENDATION_MOVIES_JSON);
+ if (rep != null && !rep.isNull("result"))
+ {
+ try
+ {
+ JSONObject results = rep.getJSONObject("result");
+ JSONArray movies = results.getJSONArray("movies");
+
+ int count = 0;
+ for (int i = 0; i < movies.length() && count < MAX_RECOMMENDATIONS; ++i)
+ {
+ try
+ {
+ JSONObject details = movies.getJSONObject(i);
+ Movie med = createMovieFromJson(details);
+ if (med != null)
+ {
+ medias.add(med);
+ ++count;
+ }
+ } catch (Exception e)
+ {
+ continue;
+ }
+ }
+ } catch (Exception e)
+ {
+ e.printStackTrace();
+ }
+ }
+
+ rep = request_object(RECOMMENDATIONS_SHOWS_JSON);
+ if (rep != null && !rep.isNull("result"))
+ {
+ try
+ {
+ JSONObject results = rep.getJSONObject("result");
+ JSONArray tvshows = results.getJSONArray("tvshows");
+
+ int count = 0;
+ for (int i = 0; i < tvshows.length() && count < MAX_RECOMMENDATIONS; ++i)
+ {
+ try
+ {
+ JSONObject tvshow = tvshows.getJSONObject(i);
+ TVShow med = createTVShowFromJson(tvshow);
+ if (med != null)
+ {
+ medias.add(med);
+ ++count;
+ }
+ } catch (Exception e)
+ {
+ continue;
+ }
+
+ }
+ } catch (Exception e)
+ {
+ e.printStackTrace();
+ }
+ }
+
+ rep = request_object(RECOMMENDATIONS_ALBUMS_JSON);
+ if (rep != null && !rep.isNull("result"))
+ {
+ try
+ {
+ JSONObject results = rep.getJSONObject("result");
+ JSONArray albums = results.getJSONArray("albums");
+
+ int count = 0;
+ for (int i = 0; i < albums.length() && count < MAX_RECOMMENDATIONS; ++i)
+ {
+ try
+ {
+ JSONObject album = albums.getJSONObject(i);
+ Album med = createAlbumFromJson(album);
+ if (med != null)
+ {
+ medias.add(med);
+ ++count;
+ }
+ } catch (Exception e)
+ {
+ continue;
+ }
+
+ }
+ } catch (Exception e)
+ {
+ e.printStackTrace();
+ }
+ }
+
+ return medias;
+ }
+
+ public List<Media> getMedias(List<File> files)
+ {
+ List<Media> medias = new ArrayList<Media>();
+
+ try
+ {
+ int nbItems = 0;
+ for (int i = 0; i < files.size() && nbItems < MAX_ITEMS; ++i)
+ {
+ File file = files.get(i);
+ String mediaType = file.getMediatype();
+ long mediaId = file.getId();
+ if (mediaType.equals("movie"))
+ {
+ JSONObject reqMovie = request_object(String.format(RETRIEVE_MOVIE_DETAILS, mediaId, "1"));
+ if (reqMovie == null || reqMovie.isNull("result"))
+ continue;
+
+ JSONObject details = reqMovie.getJSONObject("result").getJSONObject("moviedetails");
+ Movie med = createMovieFromJson(details);
+ if (med != null)
+ {
+ medias.add(med);
+ nbItems++;
+ }
+ }
+ else if (mediaType.equals("episode"))
+ {
+ JSONObject reqMovie = request_object(String.format(RETRIEVE_EPISODE_DETAILS, mediaId, "1"));
+ if (reqMovie == null || reqMovie.isNull("result"))
+ continue;
+
+ JSONObject details = reqMovie.getJSONObject("result").getJSONObject("episodedetails");
+ TVEpisode med = createTVEpisodeFromJson(details);
+ if (med != null)
+ {
+ medias.add(med);
+ nbItems++;
+ }
+ }
+ else if (mediaType.equals("tvshow"))
+ {
+ JSONObject reqMovie = request_object(String.format(RETRIEVE_TVSHOW_DETAILS, mediaId, "1"));
+ if (reqMovie == null || reqMovie.isNull("result"))
+ continue;
+
+ JSONObject details = reqMovie.getJSONObject("result").getJSONObject("tvshowdetails");
+ TVShow med = createTVShowFromJson(details);
+ medias.add(med);
+ nbItems++;
+ }
+ else if (mediaType.equals("album"))
+ {
+ JSONObject reqMovie = request_object(String.format(RETRIEVE_ALBUM_DETAILS, mediaId, "1"));
+ if (reqMovie == null || reqMovie.isNull("result"))
+ continue;
+
+ JSONObject details = reqMovie.getJSONObject("result").getJSONObject("albumdetails");
+ Album med = createAlbumFromJson(details);
+ medias.add(med);
+ nbItems++;
+ }
+ else if (mediaType.equals("song"))
+ {
+ JSONObject reqMovie = request_object(String.format(RETRIEVE_SONG_DETAILS, mediaId, "1"));
+ if (reqMovie == null || reqMovie.isNull("result"))
+ continue;
+
+ JSONObject details = reqMovie.getJSONObject("result").getJSONObject("songdetails");
+ Song med = createSongFromJson(details);
+ medias.add(med);
+ nbItems++;
+ }
+ else if (mediaType.equals("musicvideo"))
+ {
+ JSONObject reqMovie = request_object(String.format(RETRIEVE_MUSICVIDEO_DETAILS, mediaId, "1"));
+ if (reqMovie == null || reqMovie.isNull("result"))
+ continue;
+
+ JSONObject details = reqMovie.getJSONObject("result").getJSONObject("musicvideodetails");
+ MusicVideo med = createMusicvideoFromJson(details);
+ medias.add(med);
+ nbItems++;
+ }
+ }
+ } catch (Exception e)
+ {
+ e.printStackTrace();
+ }
+
+ return medias;
}
}
diff --git a/tools/android/packaging/xbmc/src/channels/SyncChannelJobService.java.in b/tools/android/packaging/xbmc/src/channels/SyncChannelJobService.java.in
new file mode 100644
index 0000000000..5e4e2e64d5
--- /dev/null
+++ b/tools/android/packaging/xbmc/src/channels/SyncChannelJobService.java.in
@@ -0,0 +1,211 @@
+/*
+ * Copyright (C) 2017 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 @APP_PACKAGE@.channels;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+
+import android.app.job.JobParameters;
+import android.app.job.JobScheduler;
+import android.app.job.JobService;
+import android.content.Context;
+import android.database.Cursor;
+import android.os.AsyncTask;
+import android.support.media.tv.TvContractCompat;
+import android.util.Log;
+
+import @APP_PACKAGE@.R;
+import @APP_PACKAGE@.XBMCJsonRPC;
+import @APP_PACKAGE@.channels.model.Subscription;
+import @APP_PACKAGE@.channels.model.XBMCDatabase;
+import @APP_PACKAGE@.channels.util.TvUtil;
+import @APP_PACKAGE@.content.XBMCFileContentProvider;
+import @APP_PACKAGE@.model.File;
+
+/**
+ * A service that will populate the TV provider with channels that every user should have. Once a
+ * channel is created, it trigger another service to add programs.
+ */
+public class SyncChannelJobService extends JobService
+{
+
+ private static final String TAG = "RecommendChannelJobSvc";
+
+ private SyncChannelTask mSyncChannelTask;
+
+ @Override
+ public boolean onStartJob(final JobParameters jobParameters)
+ {
+ Log.d(TAG, "Starting channel creation job");
+
+ mSyncChannelTask =
+ new SyncChannelTask(getApplicationContext())
+ {
+ @Override
+ protected void onPostExecute(Boolean success)
+ {
+ super.onPostExecute(success);
+ jobFinished(jobParameters, !success);
+ }
+ };
+ mSyncChannelTask.execute();
+ return true;
+ }
+
+ @Override
+ public boolean onStopJob(JobParameters jobParameters)
+ {
+ if (mSyncChannelTask != null)
+ {
+ mSyncChannelTask.cancel(true);
+ }
+ return true;
+ }
+
+ private static class SyncChannelTask extends AsyncTask<Void, Void, Boolean>
+ {
+
+ private final Context mContext;
+
+ SyncChannelTask(Context context)
+ {
+ this.mContext = context;
+ }
+
+ @Override
+ protected Boolean doInBackground(Void... voids)
+ {
+ XBMCJsonRPC json = new XBMCJsonRPC();
+ if (!json.Ping())
+ return false;
+ json = null;
+
+ List<Subscription> subscriptions = XBMCDatabase.getSubscriptions(mContext);
+ List<Subscription> freshsubscriptions = new ArrayList<>();
+ List<File> playlistsContent = new ArrayList<>();
+
+ try (Cursor cursor =
+ mContext.getContentResolver()
+ .query(
+ XBMCFileContentProvider.buildUri("/playlists/video"),
+ null,
+ null,
+ null,
+ null))
+ {
+ if (cursor != null)
+ {
+ while (cursor.moveToNext())
+ playlistsContent.add(File.fromCursor(cursor));
+ }
+ }
+ try (Cursor cursor =
+ mContext.getContentResolver()
+ .query(
+ XBMCFileContentProvider.buildUri("/playlists/mixed"),
+ null,
+ null,
+ null,
+ null))
+ {
+ if (cursor != null)
+ {
+ while (cursor.moveToNext())
+ playlistsContent.add(File.fromCursor(cursor));
+ }
+ }
+ try (Cursor cursor =
+ mContext.getContentResolver()
+ .query(
+ XBMCFileContentProvider.buildUri("/playlists/music"),
+ null,
+ null,
+ null,
+ null))
+ {
+ if (cursor != null)
+ {
+ while (cursor.moveToNext())
+ playlistsContent.add(File.fromCursor(cursor));
+ }
+ }
+
+ Subscription sub = Subscription.createSubscription(mContext.getString(R.string.suggestion_channel), "", R.drawable.ic_recommendation_80dp);
+ freshsubscriptions.add(sub);
+ if (subscriptions.size() == 0) // First-run: Add default channel
+ {
+ long channelId = TvUtil.createChannel(mContext, sub);
+ sub.setChannelId(channelId);
+ subscriptions.add(sub);
+
+ TvContractCompat.requestChannelBrowsable(mContext, channelId);
+ }
+
+ for (File file : playlistsContent)
+ {
+ sub = Subscription.createSubscription(file.getName(), file.getUri(), R.drawable.ic_recommendation_80dp);
+ freshsubscriptions.add(sub);
+
+ int subidx = subscriptions.indexOf(sub);
+ if (subidx != -1)
+ continue;
+
+ long channelId = TvUtil.createChannel(mContext, sub);
+ sub.setChannelId(channelId);
+ subscriptions.add(sub);
+ }
+
+ // Kick off a job to update default programs.
+ // The program job should verify if the channel is visible before updating programs.
+ for (Iterator<Subscription> iterator = subscriptions.iterator(); iterator.hasNext();)
+ {
+ Subscription channel = iterator.next();
+
+ if (freshsubscriptions.indexOf(channel) == -1)
+ {
+ // Channel is gone
+ Long chanid = channel.getChannelId();
+ mContext.getContentResolver()
+ .delete(
+ TvContractCompat.buildPreviewProgramsUriForChannel(chanid),
+ null,
+ null);
+ mContext.getContentResolver()
+ .delete(
+ TvContractCompat.buildChannelUri(chanid),
+ null,
+ null);
+ XBMCDatabase.removeMedias(mContext, chanid);
+ iterator.remove();
+
+ JobScheduler scheduler =
+ (JobScheduler) mContext.getSystemService(Context.JOB_SCHEDULER_SERVICE);
+ if (scheduler.getPendingJob(TvUtil.getTriggeredJobIdForChannelId(chanid)) != null)
+ scheduler.cancel(TvUtil.getTriggeredJobIdForChannelId(chanid));
+ if (scheduler.getPendingJob(TvUtil.getTimedJobIdForChannelId(chanid)) != null)
+ scheduler.cancel(TvUtil.getTimedJobIdForChannelId(chanid));
+
+ continue;
+ }
+ TvUtil.scheduleTriggeredSyncingProgramsForChannel(mContext, channel.getChannelId());
+ TvUtil.scheduleTimedSyncingProgramsForChannel(mContext, channel.getChannelId());
+ }
+ XBMCDatabase.saveSubscriptions(mContext, subscriptions);
+
+ return true;
+ }
+ }
+}
diff --git a/tools/android/packaging/xbmc/src/channels/SyncProgramsJobService.java.in b/tools/android/packaging/xbmc/src/channels/SyncProgramsJobService.java.in
new file mode 100644
index 0000000000..0e85d93846
--- /dev/null
+++ b/tools/android/packaging/xbmc/src/channels/SyncProgramsJobService.java.in
@@ -0,0 +1,305 @@
+/*
+ * Copyright (C) 2017 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 @APP_PACKAGE@.channels;
+
+import android.app.job.JobParameters;
+import android.app.job.JobService;
+import android.content.ContentUris;
+import android.content.Context;
+import android.content.Intent;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.os.PersistableBundle;
+import android.support.annotation.NonNull;
+import android.support.media.tv.Channel;
+import android.support.media.tv.PreviewProgram;
+import android.support.media.tv.TvContractCompat;
+import android.util.Log;
+
+import @APP_PACKAGE@.Splash;
+import @APP_PACKAGE@.XBMCJsonRPC;
+import @APP_PACKAGE@.model.Movie;
+import @APP_PACKAGE@.model.TVEpisode;
+import @APP_PACKAGE@.model.File;
+import @APP_PACKAGE@.model.Media;
+import @APP_PACKAGE@.channels.model.Subscription;
+import @APP_PACKAGE@.channels.model.XBMCDatabase;
+import @APP_PACKAGE@.channels.util.TvUtil;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Syncs programs for a channel. A channel id is required to be passed via the {@link
+ * JobParameters}. This service is scheduled to listen to changes to a channel. Once the job
+ * completes, it will reschedule itself to listen for the next change to the channel. See {@link
+ * TvUtil#scheduleTriggeredSyncingProgramsForChannel(Context, long)} for more details about the scheduling.
+ */
+public class SyncProgramsJobService extends JobService
+{
+
+ private static final String TAG = "SyncProgramsJobService";
+
+ private SyncProgramsTask mSyncProgramsTask;
+
+ @Override
+ public boolean onStartJob(final JobParameters jobParameters)
+ {
+ Log.d(TAG, "onStartJob(): " + jobParameters);
+
+ final long channelId = getChannelId(jobParameters);
+ if (channelId == -1L)
+ {
+ return false;
+ }
+ Log.d(TAG, "onStartJob(): Scheduling syncing for programs for channel " + channelId);
+
+ mSyncProgramsTask =
+ new SyncProgramsTask(getApplicationContext())
+ {
+ @Override
+ protected void onPostExecute(Boolean finished)
+ {
+ super.onPostExecute(finished);
+ mSyncProgramsTask = null;
+ jobFinished(jobParameters, !finished);
+ }
+ };
+ mSyncProgramsTask.execute(channelId);
+
+ return true;
+ }
+
+ @Override
+ public boolean onStopJob(JobParameters jobParameters)
+ {
+ if (mSyncProgramsTask != null)
+ {
+ mSyncProgramsTask.cancel(true);
+ }
+ return true;
+ }
+
+ private long getChannelId(JobParameters jobParameters)
+ {
+ PersistableBundle extras = jobParameters.getExtras();
+ if (extras == null)
+ {
+ return -1L;
+ }
+
+ return extras.getLong(TvContractCompat.EXTRA_CHANNEL_ID, -1L);
+ }
+
+ private class SyncProgramsTask extends AsyncTask<Long, Void, Boolean>
+ {
+
+ private final Context mContext;
+
+ private SyncProgramsTask(Context context)
+ {
+ this.mContext = context;
+ }
+
+ @Override
+ protected Boolean doInBackground(Long... channelIds)
+ {
+ XBMCJsonRPC json = new XBMCJsonRPC();
+ if (!json.Ping())
+ return false;
+ json = null;
+
+ List<Long> params = Arrays.asList(channelIds);
+ if (!params.isEmpty())
+ {
+ for (Long channelId : params)
+ {
+ Subscription subscription =
+ XBMCDatabase.findSubscriptionByChannelId(mContext, channelId);
+ if (subscription != null)
+ {
+ List<Media> cachedMedias = XBMCDatabase.getMedias(mContext, channelId);
+ syncPrograms(channelId, subscription.getUri(), cachedMedias);
+ }
+ }
+ }
+ return true;
+ }
+
+ /*
+ * Syncs programs by querying the given channel id.
+ *
+ * If the channel is not browsable, the programs will be removed to avoid showing
+ * stale programs when the channel becomes browsable in the future.
+ *
+ * If the channel is browsable, then it will check if the channel has any programs.
+ * If the channel does not have any programs, new programs will be added.
+ * If the channel does have programs, then a fresh list of programs will be fetched and the
+ * channel's programs will be updated.
+ */
+ private void syncPrograms(long channelId, String uri, List<Media> initialMedias)
+ {
+ Log.d(TAG, "Sync programs for channel: " + channelId);
+ List<Media> medias = new ArrayList<>(initialMedias);
+
+ try (Cursor cursor =
+ getContentResolver()
+ .query(
+ TvContractCompat.buildChannelUri(channelId),
+ null,
+ null,
+ null,
+ null))
+ {
+ if (cursor != null && cursor.moveToNext())
+ {
+ Channel channel = Channel.fromCursor(cursor);
+ if (!channel.isBrowsable())
+ {
+ Log.d(TAG, "Channel is not browsable: " + channelId);
+ deletePrograms(channelId, medias);
+ }
+ else
+ {
+ XBMCJsonRPC jsonrpc = new XBMCJsonRPC();
+ if (uri.isEmpty())
+ {
+ // Suggestion channel
+ Log.d(TAG, "Suggestion channel is browsable: " + channelId);
+
+ deletePrograms(channelId, medias);
+ medias = createPrograms(channelId, jsonrpc.getSuggestions());
+ }
+ else
+ {
+ Log.d(TAG, "Channel is browsable: " + channelId);
+
+ String path = Uri.parse(uri).getPath();
+ String xbmcURL = "special://profile" + path;
+ List<File> files = jsonrpc.getFiles(xbmcURL);
+
+ deletePrograms(channelId, medias);
+ medias = createPrograms(channelId, jsonrpc.getMedias(files));
+ }
+ jsonrpc = null;
+ }
+ XBMCDatabase.saveMedias(getApplicationContext(), channelId, medias);
+ }
+ }
+ }
+
+ private List<Media> createPrograms(long channelId, List<Media> medias)
+ {
+ List<Media> mediasAdded = new ArrayList<>(medias.size());
+ for (Media media : medias)
+ {
+ PreviewProgram previewProgram = buildProgram(channelId, media);
+
+ Uri programUri =
+ getContentResolver()
+ .insert(
+ TvContractCompat.PreviewPrograms.CONTENT_URI,
+ previewProgram.toContentValues());
+ long programId = ContentUris.parseId(programUri);
+ Log.d(TAG, "Inserted new program: " + programId);
+ media.setProgramId(programId);
+ mediasAdded.add(media);
+ }
+
+ return mediasAdded;
+ }
+
+ private void deletePrograms(long channelId, List<Media> medias)
+ {
+ if (medias.isEmpty())
+ {
+ return;
+ }
+
+ int count = 0;
+ for (Media media : medias)
+ {
+ count +=
+ getContentResolver()
+ .delete(
+ TvContractCompat.buildPreviewProgramUri(media.getProgramId()),
+ null,
+ null);
+ }
+ Log.d(TAG, "Deleted " + count + " programs for channel " + channelId);
+
+ // Remove our local records to stay in sync with the TV Provider.
+ XBMCDatabase.removeMedias(getApplicationContext(), channelId);
+ }
+
+ @NonNull
+ private PreviewProgram buildProgram(long channelId, Media media)
+ {
+ Intent detailsIntent = new Intent(mContext, Splash.class);
+ detailsIntent.setAction(Intent.ACTION_GET_CONTENT);
+ detailsIntent.setData(Uri.parse(media.getXbmcUrl()));
+
+ PreviewProgram.Builder builder = new PreviewProgram.Builder();
+ builder.setChannelId(channelId)
+ .setTitle(media.getTitle())
+ .setDescription(media.getDescription())
+ .setIntent(detailsIntent);
+
+ if(media.getCategory().equals(Media.MEDIA_TYPE_MOVIE))
+ builder.setType(TvContractCompat.PreviewProgramColumns.TYPE_CLIP);
+ else if(media.getCategory().equals(Media.MEDIA_TYPE_TVSHOW))
+ builder.setType(TvContractCompat.PreviewProgramColumns.TYPE_TV_SERIES);
+ else if(media.getCategory().equals(Media.MEDIA_TYPE_TVEPISODE))
+ builder.setType(TvContractCompat.PreviewProgramColumns.TYPE_TV_EPISODE);
+ else if(media.getCategory().equals(Media.MEDIA_TYPE_ALBUM))
+ builder.setType(TvContractCompat.PreviewProgramColumns.TYPE_ALBUM);
+ else if(media.getCategory().equals(Media.MEDIA_TYPE_SONG))
+ builder.setType(TvContractCompat.PreviewProgramColumns.TYPE_TRACK);
+ else if(media.getCategory().equals(Media.MEDIA_TYPE_MUSICVIDEO))
+ builder.setType(TvContractCompat.PreviewProgramColumns.TYPE_CLIP);
+
+ if (media.getCardImageUrl() != null)
+ {
+ builder.setPosterArtUri(Uri.parse(media.getCardImageUrl()));
+ if (media.getCardImageAspectRatio().equals("2:3"))
+ builder.setPosterArtAspectRatio(TvContractCompat.PreviewProgramColumns.ASPECT_RATIO_2_3);
+ else if (media.getCardImageAspectRatio().equals("1:1"))
+ builder.setPosterArtAspectRatio(TvContractCompat.PreviewProgramColumns.ASPECT_RATIO_1_1);
+ else
+ builder.setPosterArtAspectRatio(TvContractCompat.PreviewProgramColumns.ASPECT_RATIO_16_9);
+ }
+ else if (media.getBackgroundImageUrl() != null)
+ {
+ builder.setPosterArtUri(Uri.parse(media.getBackgroundImageUrl()));
+ builder.setPosterArtAspectRatio(TvContractCompat.PreviewProgramColumns.ASPECT_RATIO_16_9);
+ }
+ if (media.getVideoUrl() != null)
+ builder.setPreviewVideoUri(Uri.parse(media.getVideoUrl()));
+
+ if (media instanceof Movie)
+ {
+ builder.setLongDescription(((Movie)media).getPlot());
+ }
+ if (media instanceof TVEpisode)
+ {
+ builder.setSeasonNumber(((TVEpisode)media).getEpisode());
+ builder.setEpisodeNumber(((TVEpisode)media).getEpisode());
+ }
+
+ return builder.build();
+ }
+ }
+}
diff --git a/tools/android/packaging/xbmc/src/channels/model/Subscription.java.in b/tools/android/packaging/xbmc/src/channels/model/Subscription.java.in
new file mode 100644
index 0000000000..0a0c7eb806
--- /dev/null
+++ b/tools/android/packaging/xbmc/src/channels/model/Subscription.java.in
@@ -0,0 +1,105 @@
+/*
+ * Copyright (C) 2017 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 @APP_PACKAGE@.channels.model;
+
+import android.net.Uri;
+
+/**
+ * Contains the data about a channel that will be displayed on the launcher.
+ */
+public class Subscription
+{
+ private long channelId;
+ private String name;
+ private String uri;
+ private int channelLogo;
+
+ /**
+ * Constructor for Gson to use.
+ */
+ public Subscription()
+ {
+ }
+
+ private Subscription(
+ String name, String uri, int channelLogo)
+ {
+ this.name = name;
+ this.uri = uri;
+ this.channelLogo = channelLogo;
+ }
+
+ public static Subscription createSubscription(
+ String name, String uri, int channelLogo)
+ {
+ return new Subscription(name, uri, channelLogo);
+ }
+
+ public long getChannelId()
+ {
+ return channelId;
+ }
+
+ public void setChannelId(long channelId)
+ {
+ this.channelId = channelId;
+ }
+
+ public String getName()
+ {
+ return name;
+ }
+
+ public void setName(String name)
+ {
+ this.name = name;
+ }
+
+ public String getUri()
+ {
+ return uri;
+ }
+
+ public void setUri(String uri)
+ {
+ this.uri = uri;
+ }
+
+ public int getChannelLogo()
+ {
+ return channelLogo;
+ }
+
+ public void setChannelLogo(int channelLogo)
+ {
+ this.channelLogo = channelLogo;
+ }
+
+ @Override
+ public boolean equals(Object o)
+ {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ Subscription that = (Subscription) o;
+
+ return name != null ? name.equals(that.name) : that.name == null;
+ }
+
+ @Override
+ public int hashCode()
+ {
+ return name != null ? name.hashCode() : 0;
+ }
+}
diff --git a/tools/android/packaging/xbmc/src/channels/model/XBMCDatabase.java.in b/tools/android/packaging/xbmc/src/channels/model/XBMCDatabase.java.in
new file mode 100644
index 0000000000..090c8c210c
--- /dev/null
+++ b/tools/android/packaging/xbmc/src/channels/model/XBMCDatabase.java.in
@@ -0,0 +1,255 @@
+/*
+ * Copyright (C) 2017 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 @APP_PACKAGE@.channels.model;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.net.Uri;
+import android.support.annotation.DrawableRes;
+import android.support.annotation.Nullable;
+
+import @APP_PACKAGE@.R;
+import @APP_PACKAGE@.channels.util.SharedPreferencesHelper;
+import @APP_PACKAGE@.model.Media;
+
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Mock database stores data in {@link SharedPreferences}.
+ */
+public final class XBMCDatabase
+{
+
+ private XBMCDatabase()
+ {
+ // Do nothing.
+ }
+
+ public static Subscription getSubscription(Context context, String title, String uri)
+ {
+ return findOrCreateSubscription(
+ context,
+ title,
+ uri,
+ R.drawable.ic_recommendation_80dp);
+ }
+
+ private static Subscription findOrCreateSubscription(
+ Context context,
+ String title,
+ String uri,
+ @DrawableRes int logoResource)
+ {
+ // See if we have already created the channel in the TV Provider.
+ Subscription subscription = findSubscriptionByTitle(context, title);
+ if (subscription != null)
+ {
+ return subscription;
+ }
+
+ return Subscription.createSubscription(
+ title,
+ uri,
+ logoResource);
+ }
+
+ @Nullable
+ private static Subscription findSubscriptionByTitle(Context context, String title)
+ {
+ for (Subscription subscription : getSubscriptions(context))
+ {
+ if (subscription.getName().equals(title))
+ {
+ return subscription;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Overrides the subscriptions stored in {@link SharedPreferences}.
+ *
+ * @param context used for accessing shared preferences.
+ * @param subscriptions stored in shared preferences.
+ */
+ public static void saveSubscriptions(Context context, List<Subscription> subscriptions)
+ {
+ SharedPreferencesHelper.storeSubscriptions(context, subscriptions);
+ }
+
+ /**
+ * Adds the subscription to the list of persisted subscriptions in {@link SharedPreferences}.
+ * Will update the persisted subscription if it already exists.
+ *
+ * @param context used for accessing shared preferences.
+ * @param subscription to be saved.
+ */
+ public static void saveSubscription(Context context, Subscription subscription)
+ {
+ List<Subscription> subscriptions = getSubscriptions(context);
+ int index = subscriptions.indexOf(subscription);
+ if (index == -1)
+ {
+ subscriptions.add(subscription);
+ } else
+ {
+ subscriptions.set(index, subscription);
+ }
+ saveSubscriptions(context, subscriptions);
+ }
+
+ /**
+ * Returns subscriptions stored in {@link SharedPreferences}.
+ *
+ * @param context used for accessing shared preferences.
+ * @return a list of subscriptions or empty list if none exist.
+ */
+ public static List<Subscription> getSubscriptions(Context context)
+ {
+ return SharedPreferencesHelper.readSubscriptions(context);
+ }
+
+ /**
+ * Finds a subscription given a channel id that the subscription is associated with.
+ *
+ * @param context used for accessing shared preferences.
+ * @param channelId of the channel that the subscription is associated with.
+ * @return a subscription or null if none exist.
+ */
+ @Nullable
+ public static Subscription findSubscriptionByChannelId(Context context, long channelId)
+ {
+ for (Subscription subscription : getSubscriptions(context))
+ {
+ if (subscription.getChannelId() == channelId)
+ {
+ return subscription;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Finds a subscription with the given name.
+ *
+ * @param context used for accessing shared preferences.
+ * @param name of the subscription.
+ * @return a subscription or null if none exist.
+ */
+ @Nullable
+ public static Subscription findSubscriptionByName(Context context, String name)
+ {
+ for (Subscription subscription : getSubscriptions(context))
+ {
+ if (subscription.getName().equals(name))
+ {
+ return subscription;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Overrides the Medias stored in {@link SharedPreferences} for a given subscription.
+ *
+ * @param context used for accessing shared preferences.
+ * @param channelId of the channel that the Medias are associated with.
+ * @param Medias to be stored.
+ */
+ public static void saveMedias(Context context, long channelId, List<Media> Medias)
+ {
+ SharedPreferencesHelper.storeMedias(context, channelId, Medias);
+ }
+
+ /**
+ * Removes the list of Medias associated with a channel. Overrides the current list with an
+ * empty list in {@link SharedPreferences}.
+ *
+ * @param context used for accessing shared preferences.
+ * @param channelId of the channel that the Medias are associated with.
+ */
+ public static void removeMedias(Context context, long channelId)
+ {
+ saveMedias(context, channelId, Collections.<Media>emptyList());
+ }
+
+ /**
+ * Finds Media in subscriptions with channel id and updates it. Otherwise will add the new Media
+ * to the subscription.
+ *
+ * @param context to access shared preferences.
+ * @param channelId of the subscription that the Media is associated with.
+ * @param Media to be persisted or updated.
+ */
+ public static void saveMedia(Context context, long channelId, Media Media)
+ {
+ List<Media> Medias = getMedias(context, channelId);
+ int index = findMedia(Medias, Media);
+ if (index == -1)
+ {
+ Medias.add(Media);
+ } else
+ {
+ Medias.set(index, Media);
+ }
+ saveMedias(context, channelId, Medias);
+ }
+
+ private static int findMedia(List<Media> Medias, Media Media)
+ {
+ for (int index = 0; index < Medias.size(); ++index)
+ {
+ Media current = Medias.get(index);
+ if (current.getId() == Media.getId())
+ {
+ return index;
+ }
+ }
+ return -1;
+ }
+
+ /**
+ * Returns Medias stored in {@link SharedPreferences} for a given subscription.
+ *
+ * @param context used for accessing shared preferences.
+ * @param channelId of the subscription that the Media is associated with.
+ * @return a list of Medias for a subscription
+ */
+ public static List<Media> getMedias(Context context, long channelId)
+ {
+ return SharedPreferencesHelper.readMedias(context, channelId);
+ }
+
+ /**
+ * Finds a Media in a subscription by its id.
+ *
+ * @param context to access shared preferences.
+ * @param channelId of the subscription that the Media is associated with.
+ * @param MediaId of the Media.
+ * @return a Media or null if none exist.
+ */
+ @Nullable
+ public static Media findMediaById(Context context, long channelId, long MediaId)
+ {
+ for (Media Media : getMedias(context, channelId))
+ {
+ if (Media.getId() == MediaId)
+ {
+ return Media;
+ }
+ }
+ return null;
+ }
+}
diff --git a/tools/android/packaging/xbmc/src/channels/util/SharedPreferencesHelper.java.in b/tools/android/packaging/xbmc/src/channels/util/SharedPreferencesHelper.java.in
new file mode 100644
index 0000000000..dc817dd071
--- /dev/null
+++ b/tools/android/packaging/xbmc/src/channels/util/SharedPreferencesHelper.java.in
@@ -0,0 +1,145 @@
+/*
+ * Copyright (c) 2017 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 @APP_PACKAGE@.channels.util;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.util.Log;
+import @APP_PACKAGE@.model.Media;
+import @APP_PACKAGE@.channels.model.Subscription;
+import com.google.gson.Gson;
+import com.google.gson.JsonSyntaxException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * Helper class to store {@link Subscription}s and {@link Media}s in {@link SharedPreferences}.
+ *
+ * <p>SharedPreferencesHelper provides static methods to set and get these objects.
+ *
+ * <p>The methods of this class should not be called on the UI thread. Marshalling an object into
+ * JSON can be expensive for large objects.
+ */
+public final class SharedPreferencesHelper {
+
+ private static final String TAG = "SharedPreferencesHelper";
+
+ private static final String PREFS_NAME = "@APP_PACKAGE@";
+ private static final String PREFS_SUBSCRIPTIONS_KEY =
+ "@APP_PACKAGE@.prefs.SUBSCRIPTIONS";
+ private static final String PREFS_SUBSCRIBED_MediaS_PREFIX =
+ "@APP_PACKAGE@.prefs.SUBSCRIBED_MediaS_";
+
+ private static final Gson mGson = new Gson();
+
+ /**
+ * Reads the {@link List<Subscription>} from {@link SharedPreferences}.
+ *
+ * @param context used for getting an instance of shared preferences.
+ * @return a list of subscriptions or an empty list if none exist.
+ */
+ public static List<Subscription> readSubscriptions(Context context) {
+ return getList(context, Subscription.class, PREFS_SUBSCRIPTIONS_KEY);
+ }
+
+ /**
+ * Overrides the subscriptions stored in {@link SharedPreferences}.
+ *
+ * @param context used for getting an instance of shared preferences.
+ * @param subscriptions to be stored in shared preferences.
+ */
+ public static void storeSubscriptions(Context context, List<Subscription> subscriptions) {
+ setList(context, subscriptions, PREFS_SUBSCRIPTIONS_KEY);
+ }
+
+ /**
+ * Reads the {@link List<Media>} from {@link SharedPreferences} for a given channel.
+ *
+ * @param context used for getting an instance of shared preferences.
+ * @param channelId of the channel that the Medias are associated with.
+ * @return a list of Medias or an empty list if none exist.
+ */
+ public static List<Media> readMedias(Context context, long channelId) {
+ return getList(context, Media.class, PREFS_SUBSCRIBED_MediaS_PREFIX + channelId);
+ }
+
+ /**
+ * Overrides the Medias stored in {@link SharedPreferences} for the associated channel id.
+ *
+ * @param context used for getting an instance of shared preferences.
+ * @param channelId of the channel that the Medias are associated with.
+ * @param Medias to be stored.
+ */
+ public static void storeMedias(Context context, long channelId, List<Media> Medias) {
+ setList(context, Medias, PREFS_SUBSCRIBED_MediaS_PREFIX + channelId);
+ }
+
+ /**
+ * Retrieves a set of Strings from {@link SharedPreferences} and returns as a List.
+ *
+ * @param context used for getting an instance of shared preferences.
+ * @param clazz the class that the strings will be unmarshalled into.
+ * @param key the key in shared preferences to access the string set.
+ * @param <T> the type of object that will be in the returned list, should be the same as the
+ * clazz that was supplied.
+ * @return a list of <T> objects that were stored in shared preferences or an empty list if no
+ * objects exists.
+ */
+ private static <T> List<T> getList(Context context, Class<T> clazz, String key) {
+ SharedPreferences sharedPreferences =
+ context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
+ Set<String> stringSet = sharedPreferences.getStringSet(key, new HashSet<String>());
+ if (stringSet.isEmpty()) {
+ // Favoring mutability of the list over Collections.emptyList().
+ return new ArrayList<>();
+ }
+ List<T> list = new ArrayList<>(stringSet.size());
+ try {
+ for (String contactString : stringSet) {
+ list.add(mGson.fromJson(contactString, clazz));
+ }
+ } catch (JsonSyntaxException e) {
+ Log.e(TAG, "Could not parse json.", e);
+ return Collections.emptyList();
+ }
+ return list;
+ }
+
+ /**
+ * Saves a list of Strings into {@link SharedPreferences}.
+ *
+ * @param context used for getting an instance of shared preferences.
+ * @param list of <T> object that need to be persisted.
+ * @param key the key in shared preferences which the string set will be stored.
+ * @param <T> type the of object we will be marshalling and persisting.
+ */
+ private static <T> void setList(Context context, List<T> list, String key) {
+ SharedPreferences sharedPreferences =
+ context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
+ SharedPreferences.Editor editor = sharedPreferences.edit();
+
+ Set<String> strings = new LinkedHashSet<>(list.size());
+ for (T item : list) {
+ strings.add(mGson.toJson(item));
+ }
+ editor.putStringSet(key, strings);
+ editor.apply();
+ }
+}
diff --git a/tools/android/packaging/xbmc/src/channels/util/TvUtil.java.in b/tools/android/packaging/xbmc/src/channels/util/TvUtil.java.in
new file mode 100644
index 0000000000..08f961a082
--- /dev/null
+++ b/tools/android/packaging/xbmc/src/channels/util/TvUtil.java.in
@@ -0,0 +1,297 @@
+/*
+ * Copyright (c) 2017 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 @APP_PACKAGE@.channels.util;
+
+import android.app.job.JobInfo;
+import android.app.job.JobScheduler;
+import android.content.ComponentName;
+import android.content.ContentUris;
+import android.content.Context;
+import android.content.Intent;
+import android.database.Cursor;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Canvas;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.VectorDrawable;
+import android.media.tv.TvContract;
+import android.net.Uri;
+import android.os.PersistableBundle;
+import android.support.annotation.NonNull;
+import android.support.annotation.WorkerThread;
+import android.support.media.tv.Channel;
+import android.support.media.tv.ChannelLogoUtils;
+import android.support.media.tv.TvContractCompat;
+import android.util.Log;
+
+import @APP_PACKAGE@.Splash;
+import @APP_PACKAGE@.channels.SyncChannelJobService;
+import @APP_PACKAGE@.channels.SyncProgramsJobService;
+import @APP_PACKAGE@.channels.model.Subscription;
+
+/**
+ * Manages interactions with the TV Provider.
+ */
+public class TvUtil
+{
+
+ private static final String TAG = "TvUtil";
+ private static final int CHANNEL_JOB_ID = 500;
+ private static final int CHANNEL_TRIGGERED_JOB_ID_OFFSET = 1000;
+ private static final int CHANNEL_TIMED_JOB_ID_OFFSET = 2000;
+ private static final int CHANNEL_IMMEDIATE_JOB_ID_OFFSET = 2000;
+
+ private static final String[] CHANNELS_PROJECTION = {
+ TvContractCompat.Channels._ID,
+ TvContract.Channels.COLUMN_DISPLAY_NAME,
+ TvContractCompat.Channels.COLUMN_BROWSABLE
+ };
+
+ /**
+ * Converts a {@link Subscription} into a {@link Channel} and adds it to the tv provider.
+ *
+ * @param context used for accessing a content resolver.
+ * @param subscription to be converted to a channel and added to the tv provider.
+ * @return the id of the channel that the tv provider returns.
+ */
+ @WorkerThread
+ public static long createChannel(Context context, Subscription subscription)
+ {
+ // Checks if our subscription has been added to the channels before.
+ Cursor cursor =
+ context.getContentResolver()
+ .query(
+ TvContractCompat.Channels.CONTENT_URI,
+ CHANNELS_PROJECTION,
+ null,
+ null,
+ null);
+ if (cursor != null && cursor.moveToFirst())
+ {
+ do
+ {
+ Channel channel = Channel.fromCursor(cursor);
+ if (subscription.getName().equals(channel.getDisplayName()))
+ {
+ Log.d(
+ TAG,
+ "Channel already exists. Returning channel "
+ + channel.getId()
+ + " from TV Provider.");
+ return channel.getId();
+ }
+ } while (cursor.moveToNext());
+ }
+
+ Intent playlistIntent = new Intent(context, Splash.class);
+ if (subscription.getUri().isEmpty())
+ {
+ playlistIntent.setAction(Intent.ACTION_VIEW);
+ }
+ else
+ {
+ playlistIntent.setAction(Intent.ACTION_GET_CONTENT);
+ playlistIntent.setData(Uri.parse("special://profile" + Uri.parse(subscription.getUri()).getPath()));
+ }
+
+ Channel.Builder builder = new Channel.Builder();
+ builder.setType(TvContractCompat.Channels.TYPE_PREVIEW)
+ .setDisplayName(subscription.getName())
+ .setAppLinkIntent(playlistIntent);
+
+ Log.d(TAG, "Creating channel: " + subscription.getName());
+ Uri channelUrl =
+ context.getContentResolver()
+ .insert(
+ TvContractCompat.Channels.CONTENT_URI,
+ builder.build().toContentValues());
+
+ Log.d(TAG, "channel insert at " + channelUrl);
+ long channelId = ContentUris.parseId(channelUrl);
+ Log.d(TAG, "channel id " + channelId);
+
+ Bitmap bitmap = convertToBitmap(context, subscription.getChannelLogo());
+ ChannelLogoUtils.storeChannelLogo(context, channelId, bitmap);
+
+ return channelId;
+ }
+
+ public static int getNumberOfChannels(Context context)
+ {
+ Cursor cursor =
+ context.getContentResolver()
+ .query(
+ TvContractCompat.Channels.CONTENT_URI,
+ CHANNELS_PROJECTION,
+ null,
+ null,
+ null);
+ return cursor != null ? cursor.getCount() : 0;
+ }
+
+ /**
+ * Converts a resource into a {@link Bitmap}. If the resource is a vector drawable, it will be
+ * drawn into a new Bitmap. Otherwise the {@link BitmapFactory} will decode the resource.
+ *
+ * @param context used for getting the drawable from resources.
+ * @param resourceId of the drawable.
+ * @return a bitmap of the resource.
+ */
+ @NonNull
+ public static Bitmap convertToBitmap(Context context, int resourceId)
+ {
+ Drawable drawable = context.getDrawable(resourceId);
+ if (drawable instanceof VectorDrawable)
+ {
+ Bitmap bitmap =
+ Bitmap.createBitmap(
+ drawable.getIntrinsicWidth(),
+ drawable.getIntrinsicHeight(),
+ Bitmap.Config.ARGB_8888);
+ Canvas canvas = new Canvas(bitmap);
+ drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
+ drawable.draw(canvas);
+ return bitmap;
+ }
+
+ return BitmapFactory.decodeResource(context.getResources(), resourceId);
+ }
+
+ /**
+ * Schedules syncing channels via a {@link JobScheduler}.
+ *
+ * @param context for accessing the {@link JobScheduler}.
+ */
+ public static void scheduleSyncingChannel(Context context)
+ {
+ JobScheduler scheduler =
+ (JobScheduler) context.getSystemService(Context.JOB_SCHEDULER_SERVICE);
+ if (scheduler.getPendingJob(CHANNEL_JOB_ID) != null)
+ return;
+
+ ComponentName componentName = new ComponentName(context, SyncChannelJobService.class);
+ JobInfo.Builder builder = new JobInfo.Builder(CHANNEL_JOB_ID, componentName);
+ builder.setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY);
+ builder.setMinimumLatency(10000);
+
+ Log.d(TAG, "Scheduled channel creation.");
+ scheduler.schedule(builder.build());
+ }
+
+ /**
+ * Schedulers syncing programs for a channel. The scheduler will listen to a {@link Uri} for a
+ * particular channel.
+ *
+ * @param context for accessing the {@link JobScheduler}.
+ * @param channelId for the channel to listen for changes.
+ */
+ public static void scheduleTriggeredSyncingProgramsForChannel(Context context, long channelId)
+ {
+ JobScheduler scheduler =
+ (JobScheduler) context.getSystemService(Context.JOB_SCHEDULER_SERVICE);
+ if (scheduler.getPendingJob(getTriggeredJobIdForChannelId(channelId)) != null)
+ return;
+
+ ComponentName componentName = new ComponentName(context, SyncProgramsJobService.class);
+
+ JobInfo.Builder builder =
+ new JobInfo.Builder(getTriggeredJobIdForChannelId(channelId), componentName);
+
+ JobInfo.TriggerContentUri triggerContentUri =
+ new JobInfo.TriggerContentUri(
+ TvContractCompat.buildChannelUri(channelId),
+ JobInfo.TriggerContentUri.FLAG_NOTIFY_FOR_DESCENDANTS);
+ builder.addTriggerContentUri(triggerContentUri);
+ builder.setTriggerContentMaxDelay(0L);
+ builder.setTriggerContentUpdateDelay(0L);
+
+ PersistableBundle bundle = new PersistableBundle();
+ bundle.putLong(TvContractCompat.EXTRA_CHANNEL_ID, channelId);
+ builder.setExtras(bundle);
+
+ scheduler.schedule(builder.build());
+ }
+
+ /**
+ * Schedulers syncing programs for a channel on a time base. The scheduler will listen to a {@link Uri} for a
+ * particular channel.
+ *
+ * @param context for accessing the {@link JobScheduler}.
+ * @param channelId for the channel to listen for changes.
+ */
+ public static void scheduleTimedSyncingProgramsForChannel(Context context, long channelId)
+ {
+ JobScheduler scheduler =
+ (JobScheduler) context.getSystemService(Context.JOB_SCHEDULER_SERVICE);
+ if (scheduler.getPendingJob(getTimedJobIdForChannelId(channelId)) != null)
+ return;
+
+ ComponentName componentName = new ComponentName(context, SyncProgramsJobService.class);
+
+ JobInfo.Builder builder =
+ new JobInfo.Builder(getTimedJobIdForChannelId(channelId), componentName);
+ builder.setPeriodic(1800000);
+
+ PersistableBundle bundle = new PersistableBundle();
+ bundle.putLong(TvContractCompat.EXTRA_CHANNEL_ID, channelId);
+ builder.setExtras(bundle);
+
+ JobInfo job = builder.build();
+ Log.d(TAG, "scheduleTimedSyncingProgramsForChannel: minperiod=" + job.getMinPeriodMillis());
+
+ scheduler.schedule(job);
+ }
+
+ /**
+ * Schedulers syncing programs for a channel on a time base. The scheduler will listen to a {@link Uri} for a
+ * particular channel.
+ *
+ * @param context for accessing the {@link JobScheduler}.
+ * @param channelId for the channel to listen for changes.
+ */
+ public static void scheduleSyncingProgramsForChannel(Context context, long channelId)
+ {
+ JobScheduler scheduler =
+ (JobScheduler) context.getSystemService(Context.JOB_SCHEDULER_SERVICE);
+ if (scheduler.getPendingJob(getImmediateJobIdForChannelId(channelId)) != null)
+ return;
+
+ ComponentName componentName = new ComponentName(context, SyncProgramsJobService.class);
+
+ JobInfo.Builder builder =
+ new JobInfo.Builder(getImmediateJobIdForChannelId(channelId), componentName);
+ builder.setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY);
+
+ PersistableBundle bundle = new PersistableBundle();
+ bundle.putLong(TvContractCompat.EXTRA_CHANNEL_ID, channelId);
+ builder.setExtras(bundle);
+
+ scheduler.schedule(builder.build());
+ }
+
+ public static int getTriggeredJobIdForChannelId(long channelId)
+ {
+ return (int) (CHANNEL_TRIGGERED_JOB_ID_OFFSET + channelId);
+ }
+ public static int getTimedJobIdForChannelId(long channelId)
+ {
+ return (int) (CHANNEL_TIMED_JOB_ID_OFFSET + channelId);
+ }
+ public static int getImmediateJobIdForChannelId(long channelId)
+ {
+ return (int) (CHANNEL_IMMEDIATE_JOB_ID_OFFSET + channelId);
+ }
+}
diff --git a/tools/android/packaging/xbmc/src/content/XBMCContentProvider.java.in b/tools/android/packaging/xbmc/src/content/XBMCContentProvider.java.in
new file mode 100644
index 0000000000..a75c0bedd5
--- /dev/null
+++ b/tools/android/packaging/xbmc/src/content/XBMCContentProvider.java.in
@@ -0,0 +1,12 @@
+package @APP_PACKAGE@.content;
+
+import android.content.ContentProvider;
+
+/**
+ * Created by koyin on 17/12/2017.
+ */
+
+public abstract class XBMCContentProvider extends ContentProvider
+{
+ public static final String AUTHORITY_ROOT = "@APP_PACKAGE@";
+}
diff --git a/tools/android/packaging/xbmc/src/content/XBMCFileContentProvider.java.in b/tools/android/packaging/xbmc/src/content/XBMCFileContentProvider.java.in
new file mode 100644
index 0000000000..9918fd4ac6
--- /dev/null
+++ b/tools/android/packaging/xbmc/src/content/XBMCFileContentProvider.java.in
@@ -0,0 +1,99 @@
+package @APP_PACKAGE@.content;
+
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.database.MatrixCursor;
+import android.net.Uri;
+
+import @APP_PACKAGE@.XBMCJsonRPC;
+import @APP_PACKAGE@.model.File;
+
+import java.util.List;
+
+public class XBMCFileContentProvider extends XBMCContentProvider
+{
+ private static String TAG = "@APP_NAME@_File_Provider";
+
+ public static final String AUTHORITY = AUTHORITY_ROOT + ".file";
+
+ private XBMCJsonRPC mJsonRPC = null;
+
+ public static Uri buildUri(String path)
+ {
+ Uri.Builder builder = new Uri.Builder();
+ builder.scheme("content")
+ .authority(AUTHORITY)
+ .path(path);
+ return builder.build();
+ }
+
+ @Override
+ public int delete(Uri arg0, String arg1, String[] arg2)
+ {
+ // TODO Auto-generated method stub
+ return 0;
+ }
+
+ @Override
+ public String getType(Uri arg0)
+ {
+ return "vnd.android.cursor.dir/xbmc_file";
+ }
+
+ @Override
+ public Uri insert(Uri arg0, ContentValues arg1)
+ {
+ // TODO Auto-generated method stub
+ return null;
+ }
+
+ @Override
+ public boolean onCreate()
+ {
+ mJsonRPC = new XBMCJsonRPC();
+
+ return true;
+ }
+
+ @Override
+ public int update(Uri arg0, ContentValues arg1, String arg2, String[] arg3)
+ {
+ // TODO Auto-generated method stub
+ return 0;
+ }
+
+ @Override
+ public Cursor query(Uri uri, String[] projection, String selection,
+ String[] selectionArgs, String sortOrder)
+ {
+ String path = uri.getPath();
+ String xbmcURL = "special://profile" + path;
+
+ List<File> files = mJsonRPC.getFiles(xbmcURL);
+ if (files.isEmpty())
+ return null;
+
+ String[] fileCols = new String[]
+ {
+ File.NAME,
+ File.CATEGORY,
+ File.URI,
+ File.ID,
+ File.MEDIATYPE
+ };
+ MatrixCursor mc = new MatrixCursor(fileCols);
+
+ for (File file : files)
+ {
+ mc.addRow(new Object[]
+ {
+ file.getName(),
+ file.getCategory(),
+ file.getUri(),
+ file.getId(),
+ file.getMediatype()
+ });
+ }
+ return mc;
+ }
+}
diff --git a/tools/android/packaging/xbmc/src/XBMCImageContentProvider.java.in b/tools/android/packaging/xbmc/src/content/XBMCImageContentProvider.java.in
index f5a302089b..aef96a09b1 100644
--- a/tools/android/packaging/xbmc/src/XBMCImageContentProvider.java.in
+++ b/tools/android/packaging/xbmc/src/content/XBMCImageContentProvider.java.in
@@ -1,4 +1,4 @@
-package @APP_PACKAGE@;
+package @APP_PACKAGE@.content;
import java.io.FileNotFoundException;
import java.io.IOException;
@@ -6,39 +6,39 @@ import java.io.InputStream;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URL;
-
-import android.content.ContentProvider;
import android.content.ContentValues;
import android.database.Cursor;
import android.net.Uri;
import android.os.ParcelFileDescriptor;
import android.util.Log;
+import @APP_PACKAGE@.XBMCProperties;
+
/**
* Provides a background image for Recommendations for a RecommendationCardView.
- * <p>
+ *
* The card view changed to require a content provider be created. It requests
* items via the openFile(Uri uri) method. This is an example of how to retrieve
* an image from a web site and provide a ParcelFileDescriptor back that
* contains the contents.
- * <p>
+ *
* This is based on code from the following stackoverflow description on how to
* populate a ParcelFileDescriptor from any input stream.
- * <p>
+ *
* http://stackoverflow.com/a/14734310/1950264
- * <p>
+ *
* You still need to setup a ContentProvider entry and Authority in the
* AndroidManifest.xml
- * <p>
+ *
* See
* http://developer.android.com/reference/android/content/ContentProvider.html
+ *
*/
-public class XBMCImageContentProvider extends ContentProvider
+public class XBMCImageContentProvider extends XBMCContentProvider
{
- private static String TAG = "@APP_NAME@";
+ private static String TAG = "@APP_NAME@_Image_Provider";
- public static String AUTHORITY = "@APP_PACKAGE@";
- public static String AUTHORITY_IMAGE = AUTHORITY + ".image";
+ public static String AUTHORITY = AUTHORITY_ROOT + ".image";
@Override
public boolean onCreate()
@@ -48,10 +48,15 @@ public class XBMCImageContentProvider extends ContentProvider
public static Uri GetImageUri(String surl)
{
+ if (surl == null)
+ return null;
+ if (surl.isEmpty())
+ return null;
+
Uri.Builder builder = new Uri.Builder();
builder.scheme("content")
- .authority(AUTHORITY_IMAGE)
- .fragment(surl);
+ .authority(AUTHORITY)
+ .fragment(surl);
Uri out = builder.build();
// Log.d(TAG, "GetImageUri: in:" + surl + " out:" + out.toString());
@@ -60,7 +65,7 @@ public class XBMCImageContentProvider extends ContentProvider
@Override
public ParcelFileDescriptor openFile(Uri uri, String mode)
- throws FileNotFoundException
+ throws FileNotFoundException
{
// Log.d(TAG, "openFile: " + uri.toString());
@@ -72,7 +77,7 @@ public class XBMCImageContentProvider extends ContentProvider
// Log.d(TAG, " decodedUrl: " + decodedUrl);
if (decodedUrl == null)
{
- throw new FileNotFoundException("Uri is null");
+ return null;
}
pipe = ParcelFileDescriptor.createPipe();
@@ -85,13 +90,13 @@ public class XBMCImageContentProvider extends ContentProvider
connection.connect();
new TransferThread(connection.getInputStream(),
- new ParcelFileDescriptor.AutoCloseOutputStream(pipe[1])).start();
+ new ParcelFileDescriptor.AutoCloseOutputStream(pipe[1])).start();
}
catch (IOException e)
{
Log.e(getClass().getSimpleName(), "Exception opening pipe", e);
throw new FileNotFoundException("Could not open pipe for: "
- + uri.toString());
+ + uri.toString());
}
return (pipe[0]);
@@ -99,7 +104,7 @@ public class XBMCImageContentProvider extends ContentProvider
@Override
public Cursor query(Uri uri, String[] projection, String selection,
- String[] selectionArgs, String sortOrder)
+ String[] selectionArgs, String sortOrder)
{
return null;
}
@@ -124,7 +129,7 @@ public class XBMCImageContentProvider extends ContentProvider
@Override
public int update(Uri uri, ContentValues values, String selection,
- String[] selectionArgs)
+ String[] selectionArgs)
{
return 0;
}
@@ -148,7 +153,7 @@ public class XBMCImageContentProvider extends ContentProvider
try
{
- while ((len = in.read(buf)) > 0)
+ while ((len = in.read(buf)) >= 0)
{
out.write(buf, 0, len);
}
diff --git a/tools/android/packaging/xbmc/src/XBMCMediaContentProvider.java.in b/tools/android/packaging/xbmc/src/content/XBMCMediaContentProvider.java.in
index 71043cf3d9..98179571e2 100644
--- a/tools/android/packaging/xbmc/src/XBMCMediaContentProvider.java.in
+++ b/tools/android/packaging/xbmc/src/content/XBMCMediaContentProvider.java.in
@@ -1,19 +1,19 @@
-package @APP_PACKAGE@;
+package @APP_PACKAGE@.content;
import android.app.SearchManager;
-import android.content.ContentProvider;
import android.content.ContentValues;
import android.content.UriMatcher;
import android.database.Cursor;
import android.net.Uri;
import android.util.Log;
-public class XBMCMediaContentProvider extends ContentProvider
-{
- private static String TAG = "@APP_NAME@mediaprovider";
+import @APP_PACKAGE@.XBMCJsonRPC;
- public static final String AUTHORITY = "@APP_PACKAGE@";
- public static final String AUTHORITY_MEDIA = AUTHORITY + ".media";
+public class XBMCMediaContentProvider extends XBMCContentProvider
+{
+ private static String TAG = "@APP_NAME@_Media_Provider";
+
+ public static final String AUTHORITY = AUTHORITY_ROOT + ".media";
public static final String SUGGEST_PATH = "suggestions";
// UriMatcher stuff
@@ -26,8 +26,8 @@ public class XBMCMediaContentProvider extends ContentProvider
private static UriMatcher buildUriMatcher()
{
UriMatcher matcher = new UriMatcher(UriMatcher.NO_MATCH);
- matcher.addURI(AUTHORITY_MEDIA, SUGGEST_PATH + "/" + SearchManager.SUGGEST_URI_PATH_QUERY, SEARCH_SUGGEST);
- matcher.addURI(AUTHORITY_MEDIA, SUGGEST_PATH + "/" + SearchManager.SUGGEST_URI_PATH_QUERY + "/*", SEARCH_SUGGEST);
+ matcher.addURI(AUTHORITY, SUGGEST_PATH + "/" + SearchManager.SUGGEST_URI_PATH_QUERY, SEARCH_SUGGEST);
+ matcher.addURI(AUTHORITY, SUGGEST_PATH + "/" + SearchManager.SUGGEST_URI_PATH_QUERY + "/*", SEARCH_SUGGEST);
return matcher;
}
@@ -55,33 +55,29 @@ public class XBMCMediaContentProvider extends ContentProvider
@Override
public boolean onCreate()
{
- mJsonRPC = new XBMCJsonRPC();
+ mJsonRPC = new XBMCJsonRPC();
return true;
}
@Override
public Cursor query(Uri uri, String[] projection, String selection,
- String[] selectionArgs, String sortOrder)
+ String[] selectionArgs, String sortOrder)
{
Log.d(TAG, "query: " + uri.toString());
-
+
switch (URI_MATCHER.match(uri))
{
- case SEARCH_SUGGEST:
- String query = uri.getLastPathSegment().toLowerCase();
- int limit = 10;
- try
- {
- limit = Integer.parseInt(uri.getQueryParameter("limit"));
- }
- catch (Exception e)
- {
- }
- return mJsonRPC.getSuggestions(query, limit);
-
- default:
- throw new IllegalArgumentException("Unknown Uri: " + uri);
+ case SEARCH_SUGGEST:
+ String query = uri.getLastPathSegment().toLowerCase();
+ int limit = 10;
+ try {
+ limit = Integer.parseInt(uri.getQueryParameter("limit"));
+ } catch (Exception e) {}
+ return mJsonRPC.getSuggestions(query, limit);
+
+ default:
+ throw new IllegalArgumentException("Unknown Uri: " + uri);
}
}
diff --git a/tools/android/packaging/xbmc/src/content/XBMCYTDLContentProvider.java.in b/tools/android/packaging/xbmc/src/content/XBMCYTDLContentProvider.java.in
new file mode 100644
index 0000000000..b2f4040d69
--- /dev/null
+++ b/tools/android/packaging/xbmc/src/content/XBMCYTDLContentProvider.java.in
@@ -0,0 +1,188 @@
+package @APP_PACKAGE@.content;
+
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.HttpURLConnection;
+import java.net.URL;
+
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.ParcelFileDescriptor;
+import android.util.Log;
+import android.util.TimingLogger;
+
+import @APP_PACKAGE@.XBMCProperties;
+
+public class XBMCYTDLContentProvider extends XBMCContentProvider
+{
+ private static String TAG = "@APP_NAME@_YTDL_Provider";
+
+ public static String AUTHORITY = AUTHORITY_ROOT + ".ytdl";
+
+ @Override
+ public boolean onCreate()
+ {
+ return true;
+ }
+
+ public static Uri GetYTDLUri(String surl)
+ {
+ if (surl == null)
+ return null;
+ if (surl.isEmpty())
+ return null;
+
+ Uri.Builder builder = new Uri.Builder();
+ builder.scheme("content")
+ .authority(AUTHORITY)
+ .fragment(surl);
+
+ Uri out = builder.build();
+ return out;
+ }
+
+ private static String getFinalURL(String url) throws IOException
+ {
+ TimingLogger timings = new TimingLogger(TAG, "XBMCYTDLContentProvider::getFinalURL");
+
+ HttpURLConnection con = (HttpURLConnection) new URL(url).openConnection();
+ con.setInstanceFollowRedirects(false);
+ con.connect();
+ timings.addSplit("connect");
+
+ con.getInputStream();
+ timings.addSplit("response");
+
+ if (con.getResponseCode() == HttpURLConnection.HTTP_MOVED_PERM || con.getResponseCode() == HttpURLConnection.HTTP_MOVED_TEMP)
+ {
+ String redirectUrl = con.getHeaderField("Location");
+ return getFinalURL(redirectUrl);
+ }
+
+ timings.addSplit("done");
+ timings.dumpToLog();
+
+ return url;
+ }
+
+ @Override
+ public ParcelFileDescriptor openFile(Uri uri, String mode)
+ throws FileNotFoundException
+ {
+ ParcelFileDescriptor[] pipe = null;
+
+ try
+ {
+ String decodedUrl = uri.getFragment();
+ if (decodedUrl == null)
+ {
+ return null;
+ }
+
+ String finalURL = "";
+ try
+ {
+ finalURL = getFinalURL(decodedUrl);
+ }
+ catch (Exception e)
+ {
+ e.printStackTrace();
+ return null;
+ }
+
+ pipe = ParcelFileDescriptor.createPipe();
+
+ URL url = new URL(finalURL);
+ HttpURLConnection connection = (HttpURLConnection) url.openConnection();
+
+ connection.setDoInput(true);
+ connection.connect();
+
+ new TransferThread(connection.getInputStream(),
+ new ParcelFileDescriptor.AutoCloseOutputStream(pipe[1])).start();
+ }
+ catch (IOException e)
+ {
+ Log.e(getClass().getSimpleName(), "Exception opening pipe", e);
+ throw new FileNotFoundException("Could not open pipe for: "
+ + uri.toString());
+ }
+
+ return (pipe[0]);
+ }
+
+ @Override
+ public Cursor query(Uri uri, String[] projection, String selection,
+ String[] selectionArgs, String sortOrder)
+ {
+ return null;
+ }
+
+ @Override
+ public String getType(Uri uri)
+ {
+ return "video/*";
+ }
+
+ @Override
+ public Uri insert(Uri uri, ContentValues values)
+ {
+ return null;
+ }
+
+ @Override
+ public int delete(Uri uri, String selection, String[] selectionArgs)
+ {
+ return 0;
+ }
+
+ @Override
+ public int update(Uri uri, ContentValues values, String selection,
+ String[] selectionArgs)
+ {
+ return 0;
+ }
+
+ static class TransferThread extends Thread
+ {
+ InputStream in;
+ OutputStream out;
+
+ TransferThread(InputStream in, OutputStream out)
+ {
+ this.in = in;
+ this.out = out;
+ }
+
+ @Override
+ public void run()
+ {
+ byte[] buf = new byte[8192];
+ int len;
+
+ try
+ {
+ while ((len = in.read(buf)) >= 0)
+ {
+ out.write(buf, 0, len);
+ }
+
+ out.flush();
+ }
+ catch (IOException e) {}
+ finally
+ {
+ try
+ {
+ in.close();
+ out.close();
+ }
+ catch (IOException e) {}
+ }
+ }
+ }
+
+}
diff --git a/tools/android/packaging/xbmc/src/model/Album.java.in b/tools/android/packaging/xbmc/src/model/Album.java.in
new file mode 100644
index 0000000000..8e518ab640
--- /dev/null
+++ b/tools/android/packaging/xbmc/src/model/Album.java.in
@@ -0,0 +1,11 @@
+package @APP_PACKAGE@.model;
+
+import @APP_PACKAGE@.model.Media;
+
+/**
+ * Created by cbro on 22/12/2017
+ */
+
+public class Album extends Media
+{
+}
diff --git a/tools/android/packaging/xbmc/src/model/File.java.in b/tools/android/packaging/xbmc/src/model/File.java.in
new file mode 100644
index 0000000000..65e8906421
--- /dev/null
+++ b/tools/android/packaging/xbmc/src/model/File.java.in
@@ -0,0 +1,170 @@
+package @APP_PACKAGE@.model;
+
+/*
+ * Copyright (C) 2017 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.
+ */
+
+import android.database.Cursor;
+import android.net.Uri;
+
+import @APP_PACKAGE@.content.XBMCFileContentProvider;
+
+import java.io.Serializable;
+
+/*
+ * Media class represents video entity with title, description, image thumbs and video url.
+ *
+ */
+public class File implements Serializable
+{
+
+ public static final String NAME = "name";
+ public static final String CATEGORY = "category";
+ public static final String URI = "uri";
+ public static final String MEDIATYPE = "mediatype";
+ public static final String ID = "id";
+ private static final String TAG = "File";
+ private String name;
+ private String category;
+ private String mediatype;
+ private long id;
+ private String uri;
+
+ private File()
+ {
+ }
+
+ public File(String name, String category, String uri)
+ {
+ this.name = name;
+ this.category = category;
+ this.setUri(uri);
+
+ this.mediatype = null;
+ this.id = -1;
+ }
+
+ public static File createFile(String name, String category, String uri)
+ {
+ return new File(name, category, uri);
+ }
+
+ public static File fromCursor(Cursor cursor)
+ {
+ int index;
+ File file = new File();
+
+ if ((index = cursor.getColumnIndex(File.NAME)) >= 0 && !cursor.isNull(index))
+ file.setName(cursor.getString(index));
+ if ((index = cursor.getColumnIndex(File.CATEGORY)) >= 0 && !cursor.isNull(index))
+ file.setCategory(cursor.getString(index));
+ if ((index = cursor.getColumnIndex(File.URI)) >= 0 && !cursor.isNull(index))
+ file.setUri(cursor.getString(index));
+ if ((index = cursor.getColumnIndex(File.ID)) >= 0 && !cursor.isNull(index))
+ file.setId(cursor.getLong(index));
+ if ((index = cursor.getColumnIndex(File.MEDIATYPE)) >= 0 && !cursor.isNull(index))
+ file.setMediatype(cursor.getString(index));
+
+ return file;
+ }
+
+ @Override
+ public String toString()
+ {
+ return "File{"
+ + "id="
+ + getId()
+ + ", name='"
+ + name
+ + '\''
+ + ", category='"
+ + category
+ + '\''
+ + ", mediatype='"
+ + mediatype
+ + '\''
+ + '}';
+ }
+
+ public String getName()
+ {
+ return name;
+ }
+
+ public void setName(String name)
+ {
+ this.name = name;
+ }
+
+ public String getCategory()
+ {
+ return category;
+ }
+
+ public void setCategory(String category)
+ {
+ this.category = category;
+ }
+
+ public String getMediatype()
+ {
+ return mediatype;
+ }
+
+ public void setMediatype(String mediatype)
+ {
+ this.mediatype = mediatype;
+ }
+
+ public long getId()
+ {
+ return id;
+ }
+
+ public void setId(long id)
+ {
+ this.id = id;
+ }
+
+ public String getUri()
+ {
+ return uri;
+ }
+
+ public void setUri(String uri)
+ {
+ this.uri = uri;
+ }
+
+ @Override
+ public boolean equals(Object o)
+ {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ File file = (File) o;
+
+ if (!name.equals(file.name)) return false;
+ if (category != null ? !category.equals(file.category) : file.category != null) return false;
+ return uri != null ? uri.equals(file.uri) : file.uri == null;
+ }
+
+ @Override
+ public int hashCode()
+ {
+ int result = name.hashCode();
+ result = 31 * result + (category != null ? category.hashCode() : 0);
+ result = 31 * result + (uri != null ? uri.hashCode() : 0);
+ return result;
+ }
+}
diff --git a/tools/android/packaging/xbmc/src/model/Media.java.in b/tools/android/packaging/xbmc/src/model/Media.java.in
new file mode 100644
index 0000000000..21d0e33ea2
--- /dev/null
+++ b/tools/android/packaging/xbmc/src/model/Media.java.in
@@ -0,0 +1,188 @@
+package @APP_PACKAGE@.model;
+
+/*
+ * Copyright (C) 2017 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.
+ */
+
+import java.io.Serializable;
+
+/*
+ * Media class represents video entity with title, description, image thumbs and video url.
+ *
+ */
+public class Media implements Serializable
+{
+
+ private static final String TAG = "Media";
+
+ private long id;
+ private String title;
+ private String description;
+ private String bgImageUrl = null;
+ private String cardImageUrl = null;
+ private String cardImageAspectRatio = null;
+ private String videoUrl = null;
+ private String xbmcUrl = null;
+ private String category;
+ // Program id / Watch Next id returned from the TV Provider.
+ private long programId;
+ private long watchNextId;
+
+ public static final String MEDIA_TYPE_MOVIE = "movie";
+ public static final String MEDIA_TYPE_TVSHOW = "tvshow";
+ public static final String MEDIA_TYPE_TVEPISODE = "episode";
+ public static final String MEDIA_TYPE_ALBUM = "album";
+ public static final String MEDIA_TYPE_SONG = "song";
+ public static final String MEDIA_TYPE_MUSICVIDEO = "musicvideo";
+
+ public Media()
+ {
+ }
+
+ public long getProgramId()
+ {
+ return programId;
+ }
+
+ public void setProgramId(long programId)
+ {
+ this.programId = programId;
+ }
+
+ public long getWatchNextId()
+ {
+ return watchNextId;
+ }
+
+ public void setWatchNextId(long watchNextId)
+ {
+ this.watchNextId = watchNextId;
+ }
+
+ public long getId()
+ {
+ return id;
+ }
+
+ public void setId(long id)
+ {
+ this.id = id;
+ }
+
+ public String getTitle()
+ {
+ return title;
+ }
+
+ public void setTitle(String title)
+ {
+ this.title = title;
+ }
+
+ public String getDescription()
+ {
+ return description;
+ }
+
+ public void setDescription(String description)
+ {
+ this.description = description;
+ }
+
+ public String getVideoUrl()
+ {
+ return videoUrl;
+ }
+
+ public void setVideoUrl(String videoUrl)
+ {
+ this.videoUrl = videoUrl;
+ }
+
+ public String getXbmcUrl()
+ {
+ return xbmcUrl;
+ }
+
+ public void setXbmcUrl(String xbmcUrl)
+ {
+ this.xbmcUrl = xbmcUrl;
+ }
+
+ public String getBackgroundImageUrl()
+ {
+ return bgImageUrl;
+ }
+
+ public void setBackgroundImageUrl(String bgImageUrl)
+ {
+ this.bgImageUrl = bgImageUrl;
+ }
+
+ public String getCardImageUrl()
+ {
+ return cardImageUrl;
+ }
+
+ public void setCardImageUrl(String cardImageUrl)
+ {
+ this.cardImageUrl = cardImageUrl;
+ }
+
+ public String getCategory()
+ {
+ return category;
+ }
+
+ public void setCategory(String category)
+ {
+ this.category = category;
+ }
+
+ public String getCardImageAspectRatio()
+ {
+ return cardImageAspectRatio;
+ }
+
+ public void setCardImageAspectRatio(String cardImageAspectRatio)
+ {
+ this.cardImageAspectRatio = cardImageAspectRatio;
+ }
+
+ @Override
+ public String toString()
+ {
+ return "Media{"
+ + "id="
+ + id
+ + ", programId='"
+ + programId
+ + '\''
+ + ", watchNextId='"
+ + watchNextId
+ + '\''
+ + ", title='"
+ + title
+ + '\''
+ + ", videoUrl='"
+ + videoUrl
+ + '\''
+ + ", backgroundImageUrl='"
+ + bgImageUrl
+ + '\''
+ + ", cardImageUrl='"
+ + cardImageUrl
+ + '\''
+ + '}';
+ }
+}
diff --git a/tools/android/packaging/xbmc/src/model/Movie.java.in b/tools/android/packaging/xbmc/src/model/Movie.java.in
new file mode 100644
index 0000000000..d8ec1ddb2b
--- /dev/null
+++ b/tools/android/packaging/xbmc/src/model/Movie.java.in
@@ -0,0 +1,33 @@
+package @APP_PACKAGE@.model;
+
+import @APP_PACKAGE@.model.Media;
+
+/**
+ * Created by cbro on 22/12/2017
+ */
+
+public class Movie extends Media
+{
+ private String year;
+ private String plot;
+
+ public String getYear()
+ {
+ return year;
+ }
+
+ public void setYear(String year)
+ {
+ this.year = year;
+ }
+
+ public String getPlot()
+ {
+ return plot;
+ }
+
+ public void setPlot(String plot)
+ {
+ this.plot = plot;
+ }
+}
diff --git a/tools/android/packaging/xbmc/src/model/MusicVideo.java.in b/tools/android/packaging/xbmc/src/model/MusicVideo.java.in
new file mode 100644
index 0000000000..205810d2f8
--- /dev/null
+++ b/tools/android/packaging/xbmc/src/model/MusicVideo.java.in
@@ -0,0 +1,9 @@
+package @APP_PACKAGE@.model;
+
+/**
+ * Created by koyin on 26/12/2017.
+ */
+
+public class MusicVideo extends Media
+{
+}
diff --git a/tools/android/packaging/xbmc/src/model/Song.java.in b/tools/android/packaging/xbmc/src/model/Song.java.in
new file mode 100644
index 0000000000..9fbfa51811
--- /dev/null
+++ b/tools/android/packaging/xbmc/src/model/Song.java.in
@@ -0,0 +1,9 @@
+package @APP_PACKAGE@.model;
+
+/**
+ * Created by koyin on 26/12/2017.
+ */
+
+public class Song extends Media
+{
+}
diff --git a/tools/android/packaging/xbmc/src/model/TVEpisode.java.in b/tools/android/packaging/xbmc/src/model/TVEpisode.java.in
new file mode 100644
index 0000000000..fb73b88817
--- /dev/null
+++ b/tools/android/packaging/xbmc/src/model/TVEpisode.java.in
@@ -0,0 +1,33 @@
+package @APP_PACKAGE@.model;
+
+import @APP_PACKAGE@.model.Media;
+
+/**
+ * Created by cbro on 22/12/2017
+ */
+
+public class TVEpisode extends Media
+{
+ private int season;
+ private int episode;
+
+ public int getSeason()
+ {
+ return season;
+ }
+
+ public void setSeason(int season)
+ {
+ this.season = season;
+ }
+
+ public int getEpisode()
+ {
+ return episode;
+ }
+
+ public void setEpisode(int episode)
+ {
+ this.episode = episode;
+ }
+}
diff --git a/tools/android/packaging/xbmc/src/model/TVShow.java.in b/tools/android/packaging/xbmc/src/model/TVShow.java.in
new file mode 100644
index 0000000000..534993c78f
--- /dev/null
+++ b/tools/android/packaging/xbmc/src/model/TVShow.java.in
@@ -0,0 +1,11 @@
+package @APP_PACKAGE@.model;
+
+import @APP_PACKAGE@.model.Media;
+
+/**
+ * Created by cbro on 22/12/2017
+ */
+
+public class TVShow extends Media
+{
+}
diff --git a/tools/android/packaging/xbmc/strings.xml.in b/tools/android/packaging/xbmc/strings.xml.in
index 6c3bc58342..141a1a3fe1 100644
--- a/tools/android/packaging/xbmc/strings.xml.in
+++ b/tools/android/packaging/xbmc/strings.xml.in
@@ -4,4 +4,5 @@
<string name="app_name">@APP_NAME@</string>
<string name="search_hint">@APP_NAME@</string>
+ <string name="suggestion_channel">Suggestions</string>
</resources>