diff options
author | Chris "Koying" Browet <cbro@semperpax.com> | 2017-12-29 13:09:55 +0100 |
---|---|---|
committer | Chris "Koying" Browet <cbro@semperpax.com> | 2018-01-01 21:05:28 +0100 |
commit | 65a6d875ac209432538fcfc0969f699c364a29d4 (patch) | |
tree | 5fe0d2a309a1cec75f1d4f8b4ced5890622138c5 /tools | |
parent | 75861455685f537305fb27ad4bacf52bb3d48194 (diff) |
ADD: [droid] Oreo leanback support
Diffstat (limited to 'tools')
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 Binary files differnew file mode 100644 index 0000000000..1bd52332dd --- /dev/null +++ b/tools/android/packaging/xbmc/res/drawable/ic_recommendation_80dp.png 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> |