aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--addons/resource.language.en_gb/resources/strings.po13
-rw-r--r--addons/skin.estuary/xml/Includes_PVR.xml10
-rw-r--r--cmake/scripts/android/Install.cmake28
-rwxr-xr-xmedia/icon80x80.pngbin0 -> 1799 bytes
-rw-r--r--system/shaders/GL/4.0/gl_yuv2rgb_filter4.glsl33
-rw-r--r--tools/android/packaging/Makefile.in7
-rw-r--r--tools/android/packaging/build.gradle3
-rw-r--r--tools/android/packaging/xbmc/AndroidManifest.xml.in99
-rw-r--r--tools/android/packaging/xbmc/build.gradle.in7
-rw-r--r--tools/android/packaging/xbmc/res/drawable/ic_recommendation_80dp.pngbin0 -> 2239 bytes
-rw-r--r--tools/android/packaging/xbmc/res/layout/activity_main.xml (renamed from tools/android/packaging/xbmc/activity_main.xml.in)0
-rw-r--r--tools/android/packaging/xbmc/src/Main.java.in129
-rw-r--r--tools/android/packaging/xbmc/src/Splash.java.in402
-rw-r--r--tools/android/packaging/xbmc/src/XBMCBroadcastReceiver.java.in7
-rw-r--r--tools/android/packaging/xbmc/src/XBMCInputDeviceListener.java.in2
-rw-r--r--tools/android/packaging/xbmc/src/XBMCJsonRPC.java.in634
-rw-r--r--tools/android/packaging/xbmc/src/XBMCMediaSession.java.in9
-rw-r--r--tools/android/packaging/xbmc/src/XBMCProperties.java.in8
-rw-r--r--tools/android/packaging/xbmc/src/XBMCRecommendationBuilder.java.in36
-rw-r--r--tools/android/packaging/xbmc/src/XBMCSearchableActivity.java.in18
-rw-r--r--tools/android/packaging/xbmc/src/XBMCSettingsContentObserver.java.in10
-rw-r--r--tools/android/packaging/xbmc/src/XBMCVideoView.java.in14
-rw-r--r--tools/android/packaging/xbmc/src/channels/SyncChannelJobService.java.in211
-rw-r--r--tools/android/packaging/xbmc/src/channels/SyncProgramsJobService.java.in305
-rw-r--r--tools/android/packaging/xbmc/src/channels/model/Subscription.java.in105
-rw-r--r--tools/android/packaging/xbmc/src/channels/model/XBMCDatabase.java.in255
-rw-r--r--tools/android/packaging/xbmc/src/channels/util/SharedPreferencesHelper.java.in145
-rw-r--r--tools/android/packaging/xbmc/src/channels/util/TvUtil.java.in297
-rw-r--r--tools/android/packaging/xbmc/src/content/XBMCContentProvider.java.in12
-rw-r--r--tools/android/packaging/xbmc/src/content/XBMCFileContentProvider.java.in99
-rw-r--r--tools/android/packaging/xbmc/src/content/XBMCImageContentProvider.java.in (renamed from tools/android/packaging/xbmc/src/XBMCImageContentProvider.java.in)51
-rw-r--r--tools/android/packaging/xbmc/src/content/XBMCMediaContentProvider.java.in (renamed from tools/android/packaging/xbmc/src/XBMCMediaContentProvider.java.in)16
-rw-r--r--tools/android/packaging/xbmc/src/content/XBMCYTDLContentProvider.java.in188
-rw-r--r--tools/android/packaging/xbmc/src/interfaces/XBMCNsdManagerDiscoveryListener.java.in37
-rw-r--r--tools/android/packaging/xbmc/src/interfaces/XBMCNsdManagerRegistrationListener.java.in23
-rw-r--r--tools/android/packaging/xbmc/src/interfaces/XBMCNsdManagerResolveListener.java.in7
-rw-r--r--tools/android/packaging/xbmc/src/interfaces/XBMCSurfaceTextureOnFrameAvailableListener.java.in2
-rw-r--r--tools/android/packaging/xbmc/src/model/Album.java.in11
-rw-r--r--tools/android/packaging/xbmc/src/model/File.java.in170
-rw-r--r--tools/android/packaging/xbmc/src/model/Media.java.in188
-rw-r--r--tools/android/packaging/xbmc/src/model/Movie.java.in33
-rw-r--r--tools/android/packaging/xbmc/src/model/MusicVideo.java.in9
-rw-r--r--tools/android/packaging/xbmc/src/model/Song.java.in9
-rw-r--r--tools/android/packaging/xbmc/src/model/TVEpisode.java.in33
-rw-r--r--tools/android/packaging/xbmc/src/model/TVShow.java.in11
-rw-r--r--tools/android/packaging/xbmc/strings.xml.in1
-rw-r--r--xbmc/FileItem.cpp8
-rw-r--r--xbmc/cores/VideoPlayer/VideoRenderers/LinuxRendererGL.cpp10
-rw-r--r--xbmc/cores/VideoPlayer/VideoRenderers/VideoShaders/YUV2RGBShaderGL.cpp12
-rw-r--r--xbmc/cores/VideoPlayer/VideoRenderers/VideoShaders/YUV2RGBShaderGL.h10
-rw-r--r--xbmc/dialogs/GUIDialogContextMenu.h1
-rw-r--r--xbmc/guiinfo/GUIInfoLabels.h2
-rw-r--r--xbmc/network/GUIDialogNetworkSetup.cpp3
-rw-r--r--xbmc/platform/android/activity/XBMCApp.cpp35
-rw-r--r--xbmc/pvr/PVRGUIInfo.cpp17
-rw-r--r--xbmc/pvr/PVRGUIInfo.h1
-rw-r--r--xbmc/pvr/windows/GUIEPGGridContainer.cpp12
-rw-r--r--xbmc/pvr/windows/GUIEPGGridContainer.h3
-rw-r--r--xbmc/pvr/windows/GUIEPGGridContainerModel.cpp14
-rw-r--r--xbmc/pvr/windows/GUIEPGGridContainerModel.h5
-rw-r--r--xbmc/pvr/windows/GUIWindowPVRGuide.cpp85
-rw-r--r--xbmc/pvr/windows/GUIWindowPVRGuide.h7
-rw-r--r--xbmc/rendering/gl/RenderSystemGL.cpp9
-rw-r--r--xbmc/windows/GUIMediaWindow.cpp1
64 files changed, 3434 insertions, 488 deletions
diff --git a/addons/resource.language.en_gb/resources/strings.po b/addons/resource.language.en_gb/resources/strings.po
index 377078ce93..3cf8c8b208 100644
--- a/addons/resource.language.en_gb/resources/strings.po
+++ b/addons/resource.language.en_gb/resources/strings.po
@@ -9924,7 +9924,7 @@ msgctxt "#19101"
msgid "Provider"
msgstr ""
-#. message box text prompting user to switch to nother channel
+#. message box text prompting user to switch to another channel
#: xbmc/pvr/channels/PVRChannelGroupInternal.cpp
msgctxt "#19102"
msgid "Please switch to another channel."
@@ -11017,7 +11017,11 @@ msgctxt "#19287"
msgid "All channels"
msgstr ""
-#empty string with id 19288
+#. Label for epg grid window "go to date" context menu entry
+#: xbmc/pvr/windows/GUIWindowPVRGuide.cpp
+msgctxt "#19288"
+msgid "Go to date"
+msgstr ""
#. Label for button to hide a group in the group manager
#: addons/skin.estuary/xml/DialogPVRGroupManager.xml
@@ -12436,7 +12440,10 @@ msgctxt "#20173"
msgid "FTP server"
msgstr ""
-#empty string with id 20174
+#: xbmc/network/GUIDialogNetworkSetup.cpp
+msgctxt "#20174"
+msgid "FTPS server"
+msgstr ""
#: xbmc/network/GUIDialogNetworkSetup.cpp
msgctxt "#20175"
diff --git a/addons/skin.estuary/xml/Includes_PVR.xml b/addons/skin.estuary/xml/Includes_PVR.xml
index 6059b23c5b..b85dea2a37 100644
--- a/addons/skin.estuary/xml/Includes_PVR.xml
+++ b/addons/skin.estuary/xml/Includes_PVR.xml
@@ -105,17 +105,17 @@
<control type="group">
<visible>!ListItem.IsFolder</visible>
<control type="image">
- <left>0</left>
- <top>120</top>
+ <top>135</top>
+ <left>630</left>
<width>200</width>
<height>200</height>
- <aspectratio align="right">keep</aspectratio>
- <texture fallback="DefaultTVShows.png">$INFO[Listitem.Icon]</texture>
+ <aspectratio align="center" aligny="top">keep</aspectratio>
+ <texture>$INFO[Listitem.Icon]</texture>
<fadetime>200</fadetime>
</control>
<control type="group">
<top>120</top>
- <left>240</left>
+ <left>0</left>
<width>590</width>
<control type="label">
<height>262</height>
diff --git a/cmake/scripts/android/Install.cmake b/cmake/scripts/android/Install.cmake
index cc52d51c55..a965b1ab75 100644
--- a/cmake/scripts/android/Install.cmake
+++ b/cmake/scripts/android/Install.cmake
@@ -53,29 +53,45 @@ endif()
unset(patch)
set(package_files strings.xml
- activity_main.xml
colors.xml
searchable.xml
AndroidManifest.xml
build.gradle
- src/Main.java
src/Splash.java
+ src/Main.java
src/XBMCBroadcastReceiver.java
- src/XBMCImageContentProvider.java
src/XBMCInputDeviceListener.java
src/XBMCJsonRPC.java
- src/XBMCMediaContentProvider.java
src/XBMCMediaSession.java
src/XBMCRecommendationBuilder.java
src/XBMCSearchableActivity.java
src/XBMCSettingsContentObserver.java
src/XBMCProperties.java
src/XBMCVideoView.java
+ src/channels/SyncChannelJobService.java
+ src/channels/SyncProgramsJobService.java
+ src/channels/model/XBMCDatabase.java
+ src/channels/model/Subscription.java
+ src/channels/util/SharedPreferencesHelper.java
+ src/channels/util/TvUtil.java
src/interfaces/XBMCAudioManagerOnAudioFocusChangeListener.java
src/interfaces/XBMCSurfaceTextureOnFrameAvailableListener.java
- src/interfaces/XBMCNsdManagerDiscoveryListener.java
- src/interfaces/XBMCNsdManagerRegistrationListener.java
src/interfaces/XBMCNsdManagerResolveListener.java
+ src/interfaces/XBMCNsdManagerRegistrationListener.java
+ src/interfaces/XBMCNsdManagerDiscoveryListener.java
+ src/model/TVEpisode.java
+ src/model/Movie.java
+ src/model/TVShow.java
+ src/model/File.java
+ src/model/Album.java
+ src/model/Song.java
+ src/model/MusicVideo.java
+ src/model/Media.java
+ src/content/XBMCFileContentProvider.java
+ src/content/XBMCMediaContentProvider.java
+ src/content/XBMCContentProvider.java
+ src/content/XBMCImageContentProvider.java
+ src/content/XBMCYTDLContentProvider.java
)
foreach(file IN LISTS package_files)
configure_file(${CMAKE_SOURCE_DIR}/tools/android/packaging/xbmc/${file}.in
diff --git a/media/icon80x80.png b/media/icon80x80.png
new file mode 100755
index 0000000000..247a9505f6
--- /dev/null
+++ b/media/icon80x80.png
Binary files differ
diff --git a/system/shaders/GL/4.0/gl_yuv2rgb_filter4.glsl b/system/shaders/GL/4.0/gl_yuv2rgb_filter4.glsl
index a65260c157..814814549b 100644
--- a/system/shaders/GL/4.0/gl_yuv2rgb_filter4.glsl
+++ b/system/shaders/GL/4.0/gl_yuv2rgb_filter4.glsl
@@ -20,8 +20,20 @@ out vec4 fragColor;
vec2 stretch(vec2 pos)
{
- float x = pos.x - 0.5;
- return vec2(mix(x * abs(x) * 2.0, x, m_stretch) + 0.5, pos.y);
+#if (XBMC_STRETCH)
+ // our transform should map [0..1] to itself, with f(0) = 0, f(1) = 1, f(0.5) = 0.5, and f'(0.5) = b.
+ // a simple curve to do this is g(x) = b(x-0.5) + (1-b)2^(n-1)(x-0.5)^n + 0.5
+ // where the power preserves sign. n = 2 is the simplest non-linear case (required when b != 1)
+ #if(XBMC_texture_rectangle)
+ float x = (pos.x * m_step.x) - 0.5;
+ return vec2((mix(2.0 * x * abs(x), x, m_stretch) + 0.5) / m_step.x, pos.y);
+ #else
+ float x = pos.x - 0.5;
+ return vec2(mix(2.0 * x * abs(x), x, m_stretch) + 0.5, pos.y);
+ #endif
+#else
+ return pos;
+#endif
}
vec4[4] load4x4_0(sampler2D sampler, vec2 pos)
@@ -44,16 +56,13 @@ vec4[4] load4x4_0(sampler2D sampler, vec2 pos)
float filter_0(sampler2D sampler, vec2 coord)
{
-#if (XBMC_STRETCH)
- vec2 pos = stretch(coord) + m_step * 0.5;
-#else
vec2 pos = coord + m_step * 0.5;
-#endif
-
vec2 f = fract(pos / m_step);
vec4 linetaps = texture(m_kernelTex, 1.0 - f.x);
vec4 coltaps = texture(m_kernelTex, 1.0 - f.y);
+ linetaps /= linetaps.r + linetaps.g + linetaps.b + linetaps.a;
+ columntaps /= columntaps.r + columntaps.g + columntaps.b + columntaps.a;
mat4 conv;
conv[0] = linetaps * coltaps.x;
conv[1] = linetaps * coltaps.y;
@@ -78,9 +87,9 @@ vec4 process()
vec4 rgb;
#if defined(XBMC_YV12)
- vec4 yuv = vec4(filter_0(m_sampY, m_cordY),
- texture(m_sampU, m_cordU).r,
- texture(m_sampV, m_cordV).r,
+ vec4 yuv = vec4(filter_0(m_sampY, stretch(m_cordY)),
+ texture(m_sampU, stretch(m_cordU)).r,
+ texture(m_sampV, stretch(m_cordV)).r,
1.0);
rgb = m_yuvmat * yuv;
@@ -88,8 +97,8 @@ vec4 process()
#elif defined(XBMC_NV12)
- vec4 yuv = vec4(filter_0(m_sampY, m_cordY),
- texture(m_sampU, m_cordU).rg,
+ vec4 yuv = vec4(filter_0(m_sampY, stretch(m_cordY)),
+ texture(m_sampU, stretch(m_cordU)).rg,
1.0);
rgb = m_yuvmat * yuv;
rgb.a = m_alpha;
diff --git a/tools/android/packaging/Makefile.in b/tools/android/packaging/Makefile.in
index cc0b66989e..cf21dca28d 100644
--- a/tools/android/packaging/Makefile.in
+++ b/tools/android/packaging/Makefile.in
@@ -128,9 +128,9 @@ 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/activity_main.xml xbmc/res/layout/
cp xbmc/searchable.xml xbmc/res/xml/
libs: $(PREFIX)/lib/@APP_NAME_LC@/lib@APP_NAME_LC@.so
@@ -154,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 165833c2ae..e6342e7eca 100644
--- a/tools/android/packaging/xbmc/AndroidManifest.xml.in
+++ b/tools/android/packaging/xbmc/AndroidManifest.xml.in
@@ -1,12 +1,13 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- BEGIN_INCLUDE(manifest) -->
+<?xml version="1.0" encoding="utf-8"?><!-- BEGIN_INCLUDE(manifest) -->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="@APP_PACKAGE@"
android:installLocation="auto"
android:versionCode="@APP_VERSION_CODE_ANDROID@"
- android:versionName="@APP_VERSION@" >
+ android:versionName="@APP_VERSION@">
- <uses-sdk android:minSdkVersion="21" android:targetSdkVersion="22" />
+ <uses-sdk
+ android:minSdkVersion="21"
+ android:targetSdkVersion="22" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.INTERNET" />
@@ -17,30 +18,48 @@
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
- <uses-feature android:name="android.hardware.bluetooth" android:required="false" />
- <uses-feature android:name="android.hardware.screen.landscape" android:required="true" />
- <uses-feature android:name="android.hardware.touchscreen" android:required="false" />
- <uses-feature android:name="android.hardware.touchscreen.multitouch" android:required="false" />
- <uses-feature android:name="android.hardware.type.television" android:required="false" />
- <uses-feature android:name="android.hardware.usb.host" android:required="false" />
- <uses-feature android:name="android.hardware.wifi" android:required="false" />
-
- <application android:icon="@drawable/ic_launcher"
- android:banner="@drawable/banner"
- android:logo="@drawable/banner"
- android:label="@string/app_name"
- android:hasCode="true"
- android:debuggable="@ANDROID_DEBUGGABLE@">
+ <uses-feature
+ android:name="android.hardware.bluetooth"
+ android:required="false" />
+ <uses-feature
+ android:name="android.hardware.screen.landscape"
+ android:required="true" />
+ <uses-feature
+ android:name="android.hardware.touchscreen"
+ android:required="false" />
+ <uses-feature
+ android:name="android.hardware.touchscreen.multitouch"
+ android:required="false" />
+ <uses-feature
+ android:name="android.hardware.type.television"
+ android:required="false" />
+ <uses-feature
+ android:name="android.hardware.usb.host"
+ android:required="false" />
+ <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"
+ android:debuggable="true"
+ android:hasCode="true"
+ android:icon="@drawable/ic_launcher"
+ android:label="@string/app_name"
+ android:logo="@drawable/banner">
<activity
android:name=".Splash"
android:configChanges="orientation|keyboard|keyboardHidden|navigation|touchscreen|screenLayout|screenSize"
android:finishOnTaskLaunch="true"
android:launchMode="singleInstance"
android:screenOrientation="sensorLandscape"
- android:theme="@style/AppTheme"
- >
+ android:theme="@style/AppTheme">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
+
<category android:name="android.intent.category.LAUNCHER" />
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
</intent-filter>
@@ -68,7 +87,7 @@
<data android:scheme="ssh" />
<data android:scheme="sftp" />
<data android:scheme="smb" />
- </intent-filter>
+ </intent-filter>
</activity>
<!--
@@ -82,8 +101,7 @@
android:label="@string/app_name"
android:launchMode="singleInstance"
android:screenOrientation="sensorLandscape"
- android:theme="@style/AppTheme"
- >
+ android:theme="@style/AppTheme">
<!-- Tell NativeActivity the name of or .so -->
<meta-data
@@ -91,7 +109,7 @@
android:value="@APP_NAME_LC@" />
</activity>
- <receiver android:name=".XBMCBroadcastReceiver" >
+ <receiver android:name=".XBMCBroadcastReceiver">
<intent-filter>
<action android:name="android.intent.action.DREAMING_STOPPED" />
<action android:name="android.bluetooth.a2dp.profile.action.CONNECTION_STATE_CHANGED" />
@@ -101,30 +119,53 @@
</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" />
+ <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" />
<category android:name="android.intent.category.BROWSABLE" />
</intent-filter>
<intent-filter>
- <action android:name="com.google.android.gms.actions.SEARCH_ACTION"/>
+ <action android:name="com.google.android.gms.actions.SEARCH_ACTION" />
+
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
</intent-filter>
- <meta-data android:name="android.app.searchable"
- android:resource="@xml/searchable"/>
+
+ <meta-data
+ android:name="android.app.searchable"
+ android:resource="@xml/searchable" />
</activity>
+ <service
+ android:name=".channels.SyncChannelJobService"
+ android:exported="false"
+ android:permission="android.permission.BIND_JOB_SERVICE" />
+
+ <service
+ android:name=".channels.SyncProgramsJobService"
+ android:exported="false"
+ android:permission="android.permission.BIND_JOB_SERVICE" />
+
</application>
</manifest><!-- END_INCLUDE(manifest) -->
diff --git a/tools/android/packaging/xbmc/build.gradle.in b/tools/android/packaging/xbmc/build.gradle.in
index ff4af80cc8..c8b21fd0d5 100644
--- a/tools/android/packaging/xbmc/build.gradle.in
+++ b/tools/android/packaging/xbmc/build.gradle.in
@@ -1,7 +1,7 @@
apply plugin: 'com.android.application'
android {
- compileSdkVersion 24
+ compileSdkVersion 26
buildToolsVersion "25.0.3"
defaultConfig {
applicationId "@APP_PACKAGE@"
@@ -39,3 +39,8 @@ project.afterEvaluate {
preBuild.dependsOn
}
+dependencies {
+ // New support library to for channels/programs development.
+ compile 'com.android.support:support-tv-provider:26.0.1'
+ compile 'com.google.code.gson:gson:2.8.0'
+}
diff --git a/tools/android/packaging/xbmc/res/drawable/ic_recommendation_80dp.png b/tools/android/packaging/xbmc/res/drawable/ic_recommendation_80dp.png
new file mode 100644
index 0000000000..1bd52332dd
--- /dev/null
+++ b/tools/android/packaging/xbmc/res/drawable/ic_recommendation_80dp.png
Binary files differ
diff --git a/tools/android/packaging/xbmc/activity_main.xml.in b/tools/android/packaging/xbmc/res/layout/activity_main.xml
index 1345cb0f6f..1345cb0f6f 100644
--- a/tools/android/packaging/xbmc/activity_main.xml.in
+++ b/tools/android/packaging/xbmc/res/layout/activity_main.xml
diff --git a/tools/android/packaging/xbmc/src/Main.java.in b/tools/android/packaging/xbmc/src/Main.java.in
index 205e9d2618..f5fdcbbed6 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;
@@ -27,12 +32,16 @@ public class Main extends NativeActivity implements Choreographer.FrameCallback
private View thisView = null;
private Handler handler = new Handler();
private Intent mNewIntent = null;
+ private int mNewIntentDelay = 0;
native void _onNewIntent(Intent intent);
+
native void _onActivityResult(int requestCode, int resultCode, Intent resultData);
+
native void _doFrame(long frameTimeNanos);
+
native void _onVisibleBehindCanceled();
-
+
private Runnable leanbackUpdateRunnable = new Runnable()
{
@Override
@@ -46,7 +55,7 @@ public class Main extends NativeActivity implements Choreographer.FrameCallback
mJsonRPC.updateLeanback(Main.this);
}
}.start();
- handler.postDelayed(this, XBMCProperties.getIntProperty("xbmc.leanbackrefresh", 60*60) * 1000);
+ handler.postDelayed(this, XBMCProperties.getIntProperty("xbmc.leanbackrefresh", 60 * 60) * 1000);
}
};
@@ -69,7 +78,9 @@ public class Main extends NativeActivity implements Choreographer.FrameCallback
ret.right = thisView.getRootView().getWidth();
ret.bottom = thisView.getRootView().getHeight();
}
- catch (Exception e) {}
+ catch (Exception e)
+ {
+ }
return ret;
}
@@ -98,14 +109,27 @@ public class Main extends NativeActivity implements Choreographer.FrameCallback
setVolumeControlStream(AudioManager.STREAM_MUSIC);
mSettingsContentObserver = new XBMCSettingsContentObserver(this, handler);
- getApplicationContext().getContentResolver().registerContentObserver(android.provider.Settings.System.CONTENT_URI, true, mSettingsContentObserver );
+ getApplicationContext().getContentResolver().registerContentObserver(android.provider.Settings.System.CONTENT_URI, true, mSettingsContentObserver);
+
+ // Delayed Intent
+ if (getIntent().getData() != null)
+ {
+ mNewIntent = new Intent(getIntent());
+ mNewIntentDelay = 5000;
+ getIntent().setData(null);
+ }
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
@@ -115,35 +139,36 @@ public class Main extends NativeActivity implements Choreographer.FrameCallback
thisView = getWindow().getDecorView();
thisView.setOnSystemUiVisibilityChangeListener(new View.OnSystemUiVisibilityChangeListener()
+ {
+ @Override
+ public void onSystemUiVisibilityChange(int visibility)
+ {
+ if ((visibility & View.SYSTEM_UI_FLAG_HIDE_NAVIGATION) == 0)
+ {
+ handler.post(new Runnable()
{
- @Override
- public void onSystemUiVisibilityChange(int visibility)
+ public void run()
{
- if ((visibility & View.SYSTEM_UI_FLAG_HIDE_NAVIGATION) == 0)
+ if (android.os.Build.VERSION.SDK_INT >= 19)
{
- handler.post(new Runnable()
- {
- public void run()
- {
- if (android.os.Build.VERSION.SDK_INT >= 19) {
- // Immersive mode
-
- // Constants from API > 17
- final int API_SYSTEM_UI_FLAG_IMMERSIVE_STICKY = 0x00001000;
-
- thisView.setSystemUiVisibility(
- View.SYSTEM_UI_FLAG_LAYOUT_STABLE
- | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
- | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
- | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
- | View.SYSTEM_UI_FLAG_FULLSCREEN
- | API_SYSTEM_UI_FLAG_IMMERSIVE_STICKY);
- }
- }
- });
+ // Immersive mode
+
+ // Constants from API > 17
+ final int API_SYSTEM_UI_FLAG_IMMERSIVE_STICKY = 0x00001000;
+
+ thisView.setSystemUiVisibility(
+ View.SYSTEM_UI_FLAG_LAYOUT_STABLE
+ | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
+ | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
+ | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
+ | View.SYSTEM_UI_FLAG_FULLSCREEN
+ | API_SYSTEM_UI_FLAG_IMMERSIVE_STICKY);
}
}
});
+ }
+ }
+ });
}
@Override
@@ -152,6 +177,7 @@ public class Main extends NativeActivity implements Choreographer.FrameCallback
super.onNewIntent(intent);
// Delay until after Resume
mNewIntent = intent;
+ mNewIntentDelay = 500;
}
@Override
@@ -169,19 +195,20 @@ public class Main extends NativeActivity implements Choreographer.FrameCallback
{
super.onResume();
- if (android.os.Build.VERSION.SDK_INT >= 19) {
+ if (android.os.Build.VERSION.SDK_INT >= 19)
+ {
// Immersive mode
// Constants from API > 17
final int API_SYSTEM_UI_FLAG_IMMERSIVE_STICKY = 0x00001000;
thisView.setSystemUiVisibility(
- View.SYSTEM_UI_FLAG_LAYOUT_STABLE
- | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
- | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
- | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
- | View.SYSTEM_UI_FLAG_FULLSCREEN
- | API_SYSTEM_UI_FLAG_IMMERSIVE_STICKY);
+ View.SYSTEM_UI_FLAG_LAYOUT_STABLE
+ | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
+ | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
+ | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
+ | View.SYSTEM_UI_FLAG_FULLSCREEN
+ | API_SYSTEM_UI_FLAG_IMMERSIVE_STICKY);
}
// New intent ?
@@ -192,15 +219,19 @@ public class Main extends NativeActivity implements Choreographer.FrameCallback
@Override
public void run()
{
- try {
+ try
+ {
_onNewIntent(mNewIntent);
- } catch (UnsatisfiedLinkError e) {
+ }
+ catch (UnsatisfiedLinkError e)
+ {
Log.e("Main", "Native not registered");
- } finally {
+ } finally
+ {
mNewIntent = null;
}
}
- }, 500);
+ }, mNewIntentDelay);
}
}
@@ -208,12 +239,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)
+ Intent resultData)
{
+ super.onActivityResult(requestCode, resultCode, resultData);
_onActivityResult(requestCode, resultCode, resultData);
}
@@ -227,14 +262,14 @@ public class Main extends NativeActivity implements Choreographer.FrameCallback
getApplicationContext().getContentResolver().unregisterContentObserver(mSettingsContentObserver);
super.onDestroy();
}
-
+
@Override
public void onVisibleBehindCanceled()
{
_onVisibleBehindCanceled();
super.onVisibleBehindCanceled();
}
-
+
@Override
public void doFrame(long frameTimeNanos)
{
diff --git a/tools/android/packaging/xbmc/src/Splash.java.in b/tools/android/packaging/xbmc/src/Splash.java.in
index e42657e908..b5b999429f 100644
--- a/tools/android/packaging/xbmc/src/Splash.java.in
+++ b/tools/android/packaging/xbmc/src/Splash.java.in
@@ -49,7 +49,8 @@ import android.content.BroadcastReceiver;
import android.content.IntentFilter;
import android.os.Environment;
-public class Splash extends Activity {
+public class Splash extends Activity
+{
private static final int Uninitialized = 0;
private static final int InError = 1;
@@ -87,19 +88,23 @@ public class Splash extends Activity {
private boolean mCachingDone = false;
private boolean mInstallLibs = false;
- private class StateMachine extends Handler {
+ private class StateMachine extends Handler
+ {
private Splash mSplash = null;
- StateMachine(Splash a) {
+ StateMachine(Splash a)
+ {
this.mSplash = a;
}
@Override
- public void handleMessage(Message msg) {
+ public void handleMessage(Message msg)
+ {
mSplash.mState = msg.what;
- switch(mSplash.mState) {
+ switch (mSplash.mState)
+ {
case InError:
showErrorDialog(mSplash, "Error", mErrorMsg);
break;
@@ -130,17 +135,22 @@ public class Splash extends Activity {
mSplash.stopWatchingExternalStorage();
if (mSplash.mCachingDone)
sendEmptyMessage(StartingXBMC);
- else {
+ else
+ {
SetupEnvironment();
- if (mState == InError) {
+ if (mState == InError)
+ {
sendEmptyMessage(InError);
}
- if (fXbmcHome.exists() && fXbmcHome.lastModified() >= fPackagePath.lastModified() && !mInstallLibs) {
+ if (fXbmcHome.exists() && fXbmcHome.lastModified() >= fPackagePath.lastModified() && !mInstallLibs)
+ {
mState = CachingDone;
mCachingDone = true;
sendEmptyMessage(StartingXBMC);
- } else {
+ }
+ else
+ {
new FillCache(mSplash).execute();
}
}
@@ -156,6 +166,7 @@ public class Splash extends Activity {
}
}
}
+
private StateMachine mStateMachine = new StateMachine(this);
private class DownloadObb extends AsyncTask<String, Integer, Integer>
@@ -163,12 +174,14 @@ public class Splash extends Activity {
private Splash mSplash = null;
private int mProgressStatus = 0;
- public DownloadObb(Splash splash) {
+ public DownloadObb(Splash splash)
+ {
this.mSplash = splash;
}
@Override
- protected Integer doInBackground(String... sUrl) {
+ protected Integer doInBackground(String... sUrl)
+ {
InputStream input = null;
OutputStream output = null;
HttpURLConnection connection = null;
@@ -179,21 +192,24 @@ public class Splash extends Activity {
Log.d(TAG, "Downloading " + src + " to " + dest);
- if (!fObb.getParentFile().exists() && !fObb.getParentFile().mkdirs()) {
+ if (!fObb.getParentFile().exists() && !fObb.getParentFile().mkdirs())
+ {
Log.e(TAG, "Error creating directory " + fObb.getParentFile().getAbsolutePath());
return -1;
}
int ret = 0;
- try {
+ try
+ {
URL url = new URL(src);
connection = (HttpURLConnection) url.openConnection();
connection.connect();
// expect HTTP 200 OK, so we don't mistakenly save error report
// instead of the file
- if (connection.getResponseCode() != HttpURLConnection.HTTP_OK) {
- return -1;
+ if (connection.getResponseCode() != HttpURLConnection.HTTP_OK)
+ {
+ return -1;
}
// this will be useful to display download percentage
@@ -209,9 +225,11 @@ public class Splash extends Activity {
int count;
mProgress.setProgress(0);
mProgress.setMax(fileLength);
- while ((count = input.read(data)) != -1) {
+ while ((count = input.read(data)) != -1)
+ {
// allow canceling with back button
- if (isCancelled()) {
+ if (isCancelled())
+ {
ret = -1;
break;
}
@@ -221,15 +239,22 @@ public class Splash extends Activity {
publishProgress((int) total);
output.write(data, 0, count);
}
- } catch (Exception e) {
+ }
+ catch (Exception e)
+ {
return -1;
- } finally {
- try {
+ }
+ finally
+ {
+ try
+ {
if (output != null)
output.close();
if (input != null)
input.close();
- } catch (IOException ignored) {
+ }
+ catch (IOException ignored)
+ {
}
if (connection != null)
@@ -245,23 +270,27 @@ public class Splash extends Activity {
}
@Override
- protected void onProgressUpdate(Integer... values) {
- switch (mState) {
- case DownloadingObb:
- mSplash.mTextView.setText("Downloading OBB...");
- mSplash.mProgress.setVisibility(View.VISIBLE);
- mSplash.mProgress.setProgress(values[0]);
- break;
- case DownloadObbDone:
- mSplash.mProgress.setVisibility(View.INVISIBLE);
- break;
+ protected void onProgressUpdate(Integer... values)
+ {
+ switch (mState)
+ {
+ case DownloadingObb:
+ mSplash.mTextView.setText("Downloading OBB...");
+ mSplash.mProgress.setVisibility(View.VISIBLE);
+ mSplash.mProgress.setProgress(values[0]);
+ break;
+ case DownloadObbDone:
+ mSplash.mProgress.setVisibility(View.INVISIBLE);
+ break;
}
}
@Override
- protected void onPostExecute(Integer result) {
+ protected void onPostExecute(Integer result)
+ {
super.onPostExecute(result);
- if (result < 0) {
+ if (result < 0)
+ {
mState = InError;
mErrorMsg = "Cannot download obb.";
}
@@ -270,16 +299,19 @@ public class Splash extends Activity {
}
}
- private class FillCache extends AsyncTask<Void, Integer, Integer> {
+ private class FillCache extends AsyncTask<Void, Integer, Integer>
+ {
private Splash mSplash = null;
private int mProgressStatus = 0;
- public FillCache(Splash splash) {
+ public FillCache(Splash splash)
+ {
this.mSplash = splash;
}
- void DeleteRecursive(File fileOrDirectory) {
+ void DeleteRecursive(File fileOrDirectory)
+ {
if (fileOrDirectory.isDirectory())
for (File child : fileOrDirectory.listFiles())
DeleteRecursive(child);
@@ -288,8 +320,10 @@ public class Splash extends Activity {
}
@Override
- protected Integer doInBackground(Void... param) {
- if (fXbmcHome.exists()) {
+ protected Integer doInBackground(Void... param)
+ {
+ if (fXbmcHome.exists())
+ {
// Remove existing files
mStateMachine.sendEmptyMessage(Clearing);
Log.d(TAG, "Removing existing " + fXbmcHome.toString());
@@ -303,7 +337,8 @@ public class Splash extends Activity {
ZipFile zip;
byte[] buf = new byte[4096];
int n;
- try {
+ try
+ {
zip = new ZipFile(sPackagePath);
Enumeration<? extends ZipEntry> entries = zip.entries();
mProgress.setProgress(0);
@@ -311,14 +346,15 @@ public class Splash extends Activity {
mState = Caching;
publishProgress(mProgressStatus);
- while (entries.hasMoreElements()) {
+ while (entries.hasMoreElements())
+ {
// Update the progress bar
publishProgress(++mProgressStatus);
ZipEntry e = (ZipEntry) entries.nextElement();
String sName = e.getName();
- if (! (sName.startsWith("assets/") || (mInstallLibs && sName.startsWith("lib/"))) )
+ if (!(sName.startsWith("assets/") || (mInstallLibs && sName.startsWith("lib/"))))
continue;
if (sName.startsWith("assets/python2.7"))
continue;
@@ -334,23 +370,27 @@ public class Splash extends Activity {
{
sFullPath = sXbmcHome + "/" + sName;
File fFullPath = new File(sFullPath);
- if (e.isDirectory()) {
+ if (e.isDirectory())
+ {
fFullPath.mkdirs();
continue;
}
fFullPath.getParentFile().mkdirs();
- }
+ }
- try {
+ try
+ {
InputStream in = zip.getInputStream(e);
BufferedOutputStream out = new BufferedOutputStream(
- new FileOutputStream(sFullPath));
+ new FileOutputStream(sFullPath));
while ((n = in.read(buf, 0, 4096)) > -1)
out.write(buf, 0, n);
in.close();
out.close();
- } catch (IOException e1) {
+ }
+ catch (IOException e1)
+ {
e1.printStackTrace();
}
}
@@ -359,11 +399,15 @@ public class Splash extends Activity {
fXbmcHome.setLastModified(fPackagePath.lastModified());
- } catch (FileNotFoundException e1) {
+ }
+ catch (FileNotFoundException e1)
+ {
e1.printStackTrace();
mErrorMsg = "Cannot find package.";
return -1;
- } catch (IOException e) {
+ }
+ catch (IOException e)
+ {
e.printStackTrace();
mErrorMsg = "Cannot read package.";
File obb = new File(sPackagePath);
@@ -378,23 +422,27 @@ public class Splash extends Activity {
}
@Override
- protected void onProgressUpdate(Integer... values) {
- switch (mState) {
- case Caching:
- mSplash.mTextView.setText("Preparing for first run. Please wait...");
- mSplash.mProgress.setVisibility(View.VISIBLE);
- mSplash.mProgress.setProgress(values[0]);
- break;
- case CachingDone:
- mSplash.mProgress.setVisibility(View.INVISIBLE);
- break;
+ protected void onProgressUpdate(Integer... values)
+ {
+ switch (mState)
+ {
+ case Caching:
+ mSplash.mTextView.setText("Preparing for first run. Please wait...");
+ mSplash.mProgress.setVisibility(View.VISIBLE);
+ mSplash.mProgress.setProgress(values[0]);
+ break;
+ case CachingDone:
+ mSplash.mProgress.setVisibility(View.INVISIBLE);
+ break;
}
}
@Override
- protected void onPostExecute(Integer result) {
+ protected void onPostExecute(Integer result)
+ {
super.onPostExecute(result);
- if (result < 0) {
+ if (result < 0)
+ {
mState = InError;
}
@@ -402,7 +450,8 @@ public class Splash extends Activity {
}
}
- public void showErrorDialog(final Activity act, final String title, final String message) {
+ public void showErrorDialog(final Activity act, final String title, final String message)
+ {
if (myAlertDialog != null && myAlertDialog.isShowing())
return;
@@ -411,19 +460,21 @@ public class Splash extends Activity {
builder.setIcon(android.R.drawable.ic_dialog_alert);
builder.setMessage(Html.fromHtml(message));
builder.setPositiveButton("Exit",
- new DialogInterface.OnClickListener() {
- public void onClick(DialogInterface dialog, int arg1) {
- dialog.dismiss();
- act.finish();
- }
- });
+ new DialogInterface.OnClickListener()
+ {
+ public void onClick(DialogInterface dialog, int arg1)
+ {
+ dialog.dismiss();
+ act.finish();
+ }
+ });
builder.setCancelable(false);
myAlertDialog = builder.create();
myAlertDialog.show();
// Make links actually clickable
((TextView) myAlertDialog.findViewById(android.R.id.message))
- .setMovementMethod(LinkMovementMethod.getInstance());
+ .setMovementMethod(LinkMovementMethod.getInstance());
}
private void SetupEnvironment()
@@ -439,7 +490,8 @@ public class Splash extends Activity {
try
{
Thread.sleep(1000);
- } catch (InterruptedException e)
+ }
+ catch (InterruptedException e)
{
continue;
}
@@ -451,7 +503,8 @@ public class Splash extends Activity {
sXbmcHome = "";
}
}
- if (sXbmcHome.isEmpty()) {
+ if (sXbmcHome.isEmpty())
+ {
File fCacheDir = getCacheDir();
sXbmcHome = fCacheDir.getAbsolutePath() + "/apk";
fXbmcHome = new File(sXbmcHome);
@@ -468,7 +521,8 @@ public class Splash extends Activity {
try
{
Thread.sleep(1000);
- } catch (InterruptedException e)
+ }
+ catch (InterruptedException e)
{
continue;
}
@@ -494,8 +548,11 @@ public class Splash extends Activity {
{
obbfn = "main." + getPackageManager().getPackageInfo(getPackageName(), 0).versionCode + "." + getPackageName() + ".obb";
sPackagePath = Environment.getExternalStorageDirectory()
- + "/Android/obb/" + getPackageName() + "/" + obbfn;
- } catch (Exception e) {}
+ + "/Android/obb/" + getPackageName() + "/" + obbfn;
+ }
+ catch (Exception e)
+ {
+ }
}
fPackagePath = new File(sPackagePath);
@@ -504,63 +561,80 @@ public class Splash extends Activity {
if (!fPackagePath.exists())
{
mState = DownloadingObb;
- new DownloadObb(this).execute("http://mirrors.kodi.tv/releases/android/obb/" + obbfn, sPackagePath);
+ new DownloadObb(this).execute("http://mirrors.@APP_NAME_LC@.tv/releases/android/obb/" + obbfn, sPackagePath);
}
}
}
- private void MigrateUserData() {
- String sOldUserDir;
- File fOldUserDir;
- try {
- sOldUserDir = getExternalFilesDir(null).getParentFile().getParentFile() + "/org.xbmc.xbmc/files/.xbmc";
- fOldUserDir = new File(sOldUserDir);
- if (!fOldUserDir.exists())
- return;
- } catch (Exception e) {
+ private void MigrateUserData()
+ {
+ String sOldUserDir;
+ File fOldUserDir;
+ try
+ {
+ sOldUserDir = getExternalFilesDir(null).getParentFile().getParentFile() + "/org.xbmc.xbmc/files/.xbmc";
+ fOldUserDir = new File(sOldUserDir);
+ if (!fOldUserDir.exists())
return;
- }
+ }
+ catch (Exception e)
+ {
+ return;
+ }
- File fNewUserDir = new File(getExternalFilesDir(null), ".@APP_NAME_LC@");
- String sKodiMigrated = fNewUserDir.getAbsolutePath() + "/.kodi_data_was_migrated";
- File fKodiMigrated = new File(sKodiMigrated);
+ File fNewUserDir = new File(getExternalFilesDir(null), ".@APP_NAME_LC@");
+ String s@APP_NAME@Migrated = fNewUserDir.getAbsolutePath() + "/.@APP_NAME_LC@_data_was_migrated";
+ File f@APP_NAME@Migrated = new File(s@APP_NAME@Migrated);
- Log.d(TAG, "External_dir = " + fOldUserDir);
- if (fOldUserDir.exists() && !fNewUserDir.exists()) {
- Log.d(TAG, "XBMC user data detected at " + fOldUserDir.getAbsolutePath() + ", migrating to " + fNewUserDir.getAbsolutePath());
- if (!fNewUserDir.getParentFile().exists() && !fNewUserDir.getParentFile().mkdirs()) {
- Log.d(TAG, "Error creating " + fNewUserDir.getParentFile().getAbsolutePath());
- return;
+ Log.d(TAG, "External_dir = " + fOldUserDir);
+ if (fOldUserDir.exists() && !fNewUserDir.exists())
+ {
+ Log.d(TAG, "XBMC user data detected at " + fOldUserDir.getAbsolutePath() + ", migrating to " + fNewUserDir.getAbsolutePath());
+ if (!fNewUserDir.getParentFile().exists() && !fNewUserDir.getParentFile().mkdirs())
+ {
+ Log.d(TAG, "Error creating " + fNewUserDir.getParentFile().getAbsolutePath());
+ return;
+ }
+ if (fOldUserDir.renameTo(fNewUserDir))
+ {
+ try
+ {
+ new FileOutputStream(f@APP_NAME@Migrated).close();
}
- if (fOldUserDir.renameTo(fNewUserDir)) {
- try {
- new FileOutputStream(fKodiMigrated).close();
- } catch (IOException e1) {
- e1.printStackTrace();
- }
- Log.d(TAG, "XBMC user data migrated to @APP_NAME@ successfully");
- } else {
- Log.d(TAG, "Error migrating XBMC user data");
+ catch (IOException e1)
+ {
+ e1.printStackTrace();
}
+ Log.d(TAG, "XBMC user data migrated to @APP_NAME@ successfully");
+ }
+ else
+ {
+ Log.d(TAG, "Error migrating XBMC user data");
}
+ }
}
- private boolean ParseCpuFeature() {
+ private boolean ParseCpuFeature()
+ {
ProcessBuilder cmd;
- try {
- String[] args = { "/system/bin/cat", "/proc/cpuinfo" };
+ try
+ {
+ String[] args = {"/system/bin/cat", "/proc/cpuinfo"};
cmd = new ProcessBuilder(args);
Process process = cmd.start();
InputStream in = process.getInputStream();
byte[] re = new byte[1024];
- while (in.read(re) != -1) {
+ while (in.read(re) != -1)
+ {
mCpuinfo = mCpuinfo + new String(re);
}
in.close();
- } catch (IOException ex) {
+ }
+ catch (IOException ex)
+ {
ex.printStackTrace();
return false;
}
@@ -574,33 +648,41 @@ public class Splash extends Activity {
//
// ParseMounts() was part of the attempts to solve the issue and is not in use currently,
// but kept for possible future use.
- private boolean ParseMounts() {
+ private boolean ParseMounts()
+ {
ProcessBuilder cmd;
final Pattern reMount = Pattern.compile("^(.+?)\\s+(.+?)\\s+(.+?)\\s");
String strMounts = "";
- try {
- String[] args = { "/system/bin/cat", "/proc/mounts" };
+ try
+ {
+ String[] args = {"/system/bin/cat", "/proc/mounts"};
cmd = new ProcessBuilder(args);
Process process = cmd.start();
InputStream in = process.getInputStream();
byte[] re = new byte[1024];
- while (in.read(re) != -1) {
+ while (in.read(re) != -1)
+ {
strMounts = strMounts + new String(re);
}
in.close();
- } catch (IOException ex) {
+ }
+ catch (IOException ex)
+ {
ex.printStackTrace();
return false;
}
String[] Mounts = strMounts.split("\n");
- for (int i=0; i<Mounts.length; ++i) {
+ for (int i = 0; i < Mounts.length; ++i)
+ {
Log.d(TAG, "mount: " + Mounts[i]);
Matcher m = reMount.matcher(Mounts[i]);
- if (m.find()) {
- if (m.group(1).startsWith("/dev/block/vold") && !m.group(2).startsWith("/mnt/secure/asec")) {
+ if (m.find())
+ {
+ if (m.group(1).startsWith("/dev/block/vold") && !m.group(2).startsWith("/mnt/secure/asec"))
+ {
Log.d(TAG, "adding mount: " + m.group(2));
mMounts.add(m.group(2));
}
@@ -609,26 +691,34 @@ public class Splash extends Activity {
return true;
}
- private boolean CheckCpuFeature(String feat) {
+ private boolean CheckCpuFeature(String feat)
+ {
final Pattern FeaturePattern = Pattern.compile("(?i):.*?\\s" + feat + "(?:\\s|$)");
Matcher m = FeaturePattern.matcher(mCpuinfo);
return m.find();
}
- void updateExternalStorageState() {
+ void updateExternalStorageState()
+ {
String state = Environment.getExternalStorageState();
Log.d(TAG, "External storage = " + Environment.getExternalStorageDirectory().getAbsolutePath() + "; state = " + state);
- if (Environment.MEDIA_MOUNTED.equals(state)) {
+ if (Environment.MEDIA_MOUNTED.equals(state))
+ {
mStateMachine.sendEmptyMessage(StorageChecked);
- } else {
+ }
+ else
+ {
mExternalStorageChecked = false;
}
}
- void startWatchingExternalStorage() {
- mExternalStorageReceiver = new BroadcastReceiver() {
+ void startWatchingExternalStorage()
+ {
+ mExternalStorageReceiver = new BroadcastReceiver()
+ {
@Override
- public void onReceive(Context context, Intent intent) {
+ public void onReceive(Context context, Intent intent)
+ {
Log.i(TAG, "Storage: " + intent.getData());
updateExternalStorageState();
}
@@ -643,12 +733,14 @@ public class Splash extends Activity {
registerReceiver(mExternalStorageReceiver, filter);
}
- void stopWatchingExternalStorage() {
+ void stopWatchingExternalStorage()
+ {
if (mExternalStorageReceiver != null)
unregisterReceiver(mExternalStorageReceiver);
}
- protected void startXBMC() {
+ protected void startXBMC()
+ {
// Run @APP_NAME@
Intent intent = getIntent();
intent.setClass(this, @APP_PACKAGE@.Main.class);
@@ -658,7 +750,8 @@ public class Splash extends Activity {
}
@Override
- public void onCreate(Bundle savedInstanceState) {
+ public void onCreate(Bundle savedInstanceState)
+ {
super.onCreate(savedInstanceState);
// Be sure properties are initialized for native
@@ -666,12 +759,13 @@ public class Splash extends Activity {
// Check if @APP_NAME@ is not already running
ActivityManager activityManager = (ActivityManager) getBaseContext()
- .getSystemService(Context.ACTIVITY_SERVICE);
+ .getSystemService(Context.ACTIVITY_SERVICE);
List<RunningTaskInfo> tasks = activityManager
- .getRunningTasks(Integer.MAX_VALUE);
+ .getRunningTasks(Integer.MAX_VALUE);
for (RunningTaskInfo task : tasks)
if (task.topActivity.toString().equalsIgnoreCase(
- "ComponentInfo{@APP_PACKAGE@/@APP_PACKAGE@.Main}")) {
+ "ComponentInfo{@APP_PACKAGE@/@APP_PACKAGE@.Main}"))
+ {
// @APP_NAME@ already running; just activate it
startXBMC();
return;
@@ -681,17 +775,22 @@ public class Splash extends Activity {
String pkg_arch = "";
// Read the properties
- try {
+ try
+ {
Resources resources = this.getResources();
InputStream xbmcprop = resources.openRawResource(R.raw.xbmc);
Properties properties = new Properties();
properties.load(xbmcprop);
pkg_arch = properties.getProperty("native_arch");
- } catch (NotFoundException e) {
+ }
+ catch (NotFoundException e)
+ {
mErrorMsg = "Cannot find properties file";
Log.e(TAG, mErrorMsg);
mState = InError;
- } catch (IOException e) {
+ }
+ catch (IOException e)
+ {
mErrorMsg = "Failed to open properties file";
Log.e(TAG, mErrorMsg);
mState = InError;
@@ -699,7 +798,7 @@ public class Splash extends Activity {
boolean arch_ok = false;
String[] abis = Build.SUPPORTED_ABIS;
- for (int i=0; i<abis.length; ++i)
+ for (int i = 0; i < abis.length; ++i)
{
Log.i(TAG, "ABI: " + abis[i]);
if (abis[i].equalsIgnoreCase(pkg_arch))
@@ -711,24 +810,30 @@ public class Splash extends Activity {
if (!arch_ok)
{
- mErrorMsg = "This package is not compatible with your device (" + pkg_arch +").\nPlease check the <a href=\"http://wiki.kodi.tv/index.php?title=XBMC_for_Android_specific_FAQ\">Kodi Android wiki</a> for more information.";
+ mErrorMsg = "This package is not compatible with your device (" + pkg_arch + ").\nPlease check the <a href=\"http://wiki.@APP_NAME_LC@.tv/index.php?title=XBMC_for_Android_specific_FAQ\">@APP_NAME@ Android wiki</a> for more information.";
Log.e(TAG, mErrorMsg);
mState = InError;
}
- if (mState != InError) {
- if (pkg_arch.equalsIgnoreCase("arm")) {
+ if (mState != InError)
+ {
+ if (pkg_arch.equalsIgnoreCase("arm"))
+ {
// arm arch: check if the cpu supports neon
boolean ret = ParseCpuFeature();
//Log.d(TAG, "/proc/cpuinfo = " + mCpuinfo);
- if (!ret) {
+ if (!ret)
+ {
mErrorMsg = "Error! Cannot parse CPU features.";
Log.e(TAG, mErrorMsg);
mState = InError;
- } else {
+ }
+ else
+ {
ret = CheckCpuFeature("neon") || CheckCpuFeature("aarch64") || CheckCpuFeature("asimd"); // aarch64 is always neon; asimd feature also represents neon
- if (!ret) {
- mErrorMsg = "This @APP_NAME@ package is not compatible with your device (NEON).\nPlease check the <a href=\"http://wiki.kodi.tv/index.php?title=XBMC_for_Android_specific_FAQ\">Kodi Android wiki</a> for more information.";
+ if (!ret)
+ {
+ mErrorMsg = "This @APP_NAME@ package is not compatible with your device (NEON).\nPlease check the <a href=\"http://wiki.@APP_NAME_LC@.tv/index.php?title=XBMC_for_Android_specific_FAQ\">@APP_NAME@ Android wiki</a> for more information.";
Log.e(TAG, mErrorMsg);
mState = InError;
}
@@ -740,19 +845,22 @@ public class Splash extends Activity {
if (Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState()))
mExternalStorageChecked = true;
- if (mState != InError && mExternalStorageChecked) {
+ if (mState != InError && mExternalStorageChecked)
+ {
mState = ChecksDone;
SetupEnvironment();
MigrateUserData();
- if ((mState != DownloadingObb && mState != InError) && fXbmcHome.exists() && fXbmcHome.lastModified() >= fPackagePath.lastModified() && !mInstallLibs) {
+ if ((mState != DownloadingObb && mState != InError) && fXbmcHome.exists() && fXbmcHome.lastModified() >= fPackagePath.lastModified() && !mInstallLibs)
+ {
mState = CachingDone;
mCachingDone = true;
}
}
- if ((mState != DownloadingObb && mState != InError) && mCachingDone && mExternalStorageChecked) {
+ if ((mState != DownloadingObb && mState != InError) && mCachingDone && mExternalStorageChecked)
+ {
startXBMC();
return;
}
@@ -761,15 +869,19 @@ public class Splash extends Activity {
mProgress = (ProgressBar) findViewById(R.id.progressBar1);
mTextView = (TextView) findViewById(R.id.textView1);
- if (mState == DownloadingObb || mState == InError) {
+ if (mState == DownloadingObb || mState == InError)
+ {
mStateMachine.sendEmptyMessage(mState);
return;
}
- if (!mExternalStorageChecked) {
+ if (!mExternalStorageChecked)
+ {
startWatchingExternalStorage();
mStateMachine.sendEmptyMessage(WaitingStorageChecked);
- } else {
+ }
+ else
+ {
if (!mCachingDone)
new FillCache(this).execute();
else
diff --git a/tools/android/packaging/xbmc/src/XBMCBroadcastReceiver.java.in b/tools/android/packaging/xbmc/src/XBMCBroadcastReceiver.java.in
index acbc270d9b..4174c64508 100644
--- a/tools/android/packaging/xbmc/src/XBMCBroadcastReceiver.java.in
+++ b/tools/android/packaging/xbmc/src/XBMCBroadcastReceiver.java.in
@@ -30,9 +30,12 @@ public class XBMCBroadcastReceiver extends BroadcastReceiver
}
else
{
- try {
+ try
+ {
_onReceive(intent);
- } catch (UnsatisfiedLinkError e) {
+ }
+ catch (UnsatisfiedLinkError e)
+ {
Log.e(TAG, "Native not registered");
}
}
diff --git a/tools/android/packaging/xbmc/src/XBMCInputDeviceListener.java.in b/tools/android/packaging/xbmc/src/XBMCInputDeviceListener.java.in
index 2890ecda4d..101f2eaacb 100644
--- a/tools/android/packaging/xbmc/src/XBMCInputDeviceListener.java.in
+++ b/tools/android/packaging/xbmc/src/XBMCInputDeviceListener.java.in
@@ -6,7 +6,9 @@ import android.util.Log;
public class XBMCInputDeviceListener implements InputDeviceListener
{
native void _onInputDeviceAdded(int deviceId);
+
native void _onInputDeviceChanged(int deviceId);
+
native void _onInputDeviceRemoved(int deviceId);
@Override
diff --git a/tools/android/packaging/xbmc/src/XBMCJsonRPC.java.in b/tools/android/packaging/xbmc/src/XBMCJsonRPC.java.in
index 0187c9937c..fa7f5e3dad 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,24 +60,27 @@ 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\", "
+ "\"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\"}";
@@ -85,12 +102,33 @@ public class XBMCJsonRPC
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\"}";
+ 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);
@@ -212,7 +250,7 @@ public class XBMCJsonRPC
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())
@@ -229,24 +267,39 @@ public class XBMCJsonRPC
}
}
- 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\"}");
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);
+ return (m_xbmc_web_url + "/" + surl);
} catch (Exception e)
{
e.printStackTrace();
- return null;
+ return "";
+ }
+ }
+
+ public boolean Ping()
+ {
+ try
+ {
+ JSONObject req = request_object(GET_VERSION);
+ if (req == null || req.isNull("result"))
+ return false;
}
+ catch (Exception e)
+ {
+ return false;
+ }
+ return true;
}
public Cursor search(String query)
@@ -419,20 +472,20 @@ public class XBMCJsonRPC
e.printStackTrace();
}
mc.addRow(new Object[]
- {
- movie.getString("movieid"),
- movie.getString("title"),
- movie.getString("tagline"),
- XBMCImageContentProvider.GetImageUri(getBitmapUrl(movie.getString("thumbnail"))).toString(),
- XBMCImageContentProvider.GetImageUri(getBitmapUrl(movie.getString("thumbnail"))).toString(),
- Intent.ACTION_VIEW,
- Uri.parse("videodb://movies/titles/" + movie.getString("movieid")),
- 0,
- 0,
- rYear,
- rDur,
- -1
- });
+ {
+ movie.getString("movieid"),
+ movie.getString("title"),
+ movie.getString("tagline"),
+ 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,
+ 0,
+ rYear,
+ rDur,
+ -1
+ });
nb_movies++;
totCount++;
}
@@ -467,20 +520,20 @@ public class XBMCJsonRPC
e.printStackTrace();
}
mc.addRow(new Object[]
- {
- tvshow.getString("tvshowid"),
- tvshow.getString("title"),
- tvshow.getString("plot"),
- XBMCImageContentProvider.GetImageUri(getBitmapUrl(tvshow.getString("thumbnail"))).toString(),
- XBMCImageContentProvider.GetImageUri(getBitmapUrl(tvshow.getString("thumbnail"))).toString(),
- Intent.ACTION_GET_CONTENT,
- Uri.parse("videodb://tvshows/titles/" + tvshow.getString("tvshowid") + "/"),
- 0,
- 0,
- rYear,
- 45*60*1000, // HACK: we don't get show duration via JSON: hardcode one to have search working
- -1
- });
+ {
+ tvshow.getString("tvshowid"),
+ tvshow.getString("title"),
+ tvshow.getString("plot"),
+ 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,
+ 0,
+ rYear,
+ 45 * 60 * 1000, // HACK: we don't get show duration via JSON: hardcode one to have search working
+ -1
+ });
nb_shows++;
totCount++;
}
@@ -508,8 +561,8 @@ public class XBMCJsonRPC
album.getString("albumid"),
album.getString("title"),
album.getString("displayartist"),
- XBMCImageContentProvider.GetImageUri(getBitmapUrl(album.getString("thumbnail"))).toString(),
- XBMCImageContentProvider.GetImageUri(getBitmapUrl(album.getString("thumbnail"))).toString(),
+ 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,
@@ -544,8 +597,8 @@ public class XBMCJsonRPC
artist.getString("artistid"),
artist.getString("artist"),
artist.getString("description"),
- XBMCImageContentProvider.GetImageUri(getBitmapUrl(artist.getString("thumbnail"))).toString(),
- XBMCImageContentProvider.GetImageUri(getBitmapUrl(artist.getString("thumbnail"))).toString(),
+ 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,
@@ -599,7 +652,7 @@ public class XBMCJsonRPC
final XBMCRecommendationBuilder notificationBuilder = builder
.setBackground(
XBMCImageContentProvider.GetImageUri(
- getBitmapUrl(movie.getString("fanart"))).toString())
+ getDownloadUrl(movie.getString("fanart"))).toString())
.setId(id).setPriority(MAX_RECOMMENDATIONS - count)
.setTitle(movie.getString("title"))
.setDescription(movie.getString("tagline"))
@@ -641,7 +694,7 @@ public class XBMCJsonRPC
final XBMCRecommendationBuilder notificationBuilder = builder
.setBackground(
XBMCImageContentProvider.GetImageUri(
- getBitmapUrl(tvshow.getString("fanart"))).toString())
+ getDownloadUrl(tvshow.getString("fanart"))).toString())
.setId(id).setPriority(MAX_RECOMMENDATIONS - count)
.setTitle(tvshow.getString("title"))
.setDescription(tvshow.getString("plot"))
@@ -684,7 +737,7 @@ public class XBMCJsonRPC
final XBMCRecommendationBuilder notificationBuilder = builder
.setBackground(
XBMCImageContentProvider.GetImageUri(
- getBitmapUrl(album.getString("fanart"))).toString())
+ getDownloadUrl(album.getString("fanart"))).toString())
.setId(id).setPriority(MAX_RECOMMENDATIONS - count)
.setTitle(album.getString("title"))
.setDescription(album.getString("displayartist"))
@@ -759,4 +812,491 @@ public class XBMCJsonRPC
return null;
}
}
+
+ 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/XBMCMediaSession.java.in b/tools/android/packaging/xbmc/src/XBMCMediaSession.java.in
index 0a78996e59..8e0758af2f 100644
--- a/tools/android/packaging/xbmc/src/XBMCMediaSession.java.in
+++ b/tools/android/packaging/xbmc/src/XBMCMediaSession.java.in
@@ -18,12 +18,19 @@ import java.util.concurrent.FutureTask;
public class XBMCMediaSession
{
native void _onPlayRequested();
+
native void _onPauseRequested();
+
native void _onNextRequested();
+
native void _onPreviousRequested();
+
native void _onForwardRequested();
+
native void _onRewindRequested();
+
native void _onStopRequested();
+
native void _onSeekRequested(long pos);
private static final String TAG = "XBMCMediaSession";
@@ -136,7 +143,7 @@ public class XBMCMediaSession
private void updateIntent(Intent intent)
{
PendingIntent pi = PendingIntent.getActivity(Main.MainActivity, 99 /*request code*/,
- intent, PendingIntent.FLAG_UPDATE_CURRENT);
+ intent, PendingIntent.FLAG_UPDATE_CURRENT);
mSession.setSessionActivity(pi);
}
diff --git a/tools/android/packaging/xbmc/src/XBMCProperties.java.in b/tools/android/packaging/xbmc/src/XBMCProperties.java.in
index 21983423cb..8f86a12d07 100644
--- a/tools/android/packaging/xbmc/src/XBMCProperties.java.in
+++ b/tools/android/packaging/xbmc/src/XBMCProperties.java.in
@@ -3,12 +3,13 @@ package @APP_PACKAGE@;
import java.io.File;
import java.io.FileInputStream;
import java.util.Properties;
+
import android.os.Environment;
import android.util.Base64;
import android.util.Log;
public class XBMCProperties
-{
+{
private static final String TAG = "@APP_NAME@properties";
private static boolean isInitialized()
@@ -30,7 +31,8 @@ public class XBMCProperties
FileInputStream xbmcenvprop = new FileInputStream(fProp);
sysProp.load(xbmcenvprop);
System.setProperties(sysProp);
- } catch (Exception e)
+ }
+ catch (Exception e)
{
Log.e(TAG, "Error loading " + propfn + " (" + e.getMessage() + ")");
}
@@ -71,7 +73,7 @@ public class XBMCProperties
String jsonPwd = System.getProperty("xbmc.jsonPwd", "");
if (!jsonPwd.isEmpty())
- return "Basic " + Base64.encodeToString((jsonUser+":"+jsonPwd).getBytes(), Base64.NO_WRAP);
+ return "Basic " + Base64.encodeToString((jsonUser + ":" + jsonPwd).getBytes(), Base64.NO_WRAP);
return "";
}
diff --git a/tools/android/packaging/xbmc/src/XBMCRecommendationBuilder.java.in b/tools/android/packaging/xbmc/src/XBMCRecommendationBuilder.java.in
index c69ba61bae..c3d71e905a 100644
--- a/tools/android/packaging/xbmc/src/XBMCRecommendationBuilder.java.in
+++ b/tools/android/packaging/xbmc/src/XBMCRecommendationBuilder.java.in
@@ -102,20 +102,20 @@ public class XBMCRecommendationBuilder
}
Notification notification = new Notification.BigPictureStyle(
- new Notification.Builder(mContext)
- .setContentTitle(mTitle)
- .setContentText(mDescription)
- .setPriority(mPriority)
- .setLocalOnly(true)
- .setOngoing(true)
- .setColor(mContext.getResources().getColor(R.color.recommendation_color))
- .setCategory(API_NOTIFICATION_CATEGORY_RECOMMENDATION)
- .setLargeIcon(mBitmap)
- .setSmallIcon(mSmallIcon)
- .setContentIntent(mIntent)
- .setExtras(extras)
- .setAutoCancel(false)
- ).build();
+ new Notification.Builder(mContext)
+ .setContentTitle(mTitle)
+ .setContentText(mDescription)
+ .setPriority(mPriority)
+ .setLocalOnly(true)
+ .setOngoing(true)
+ .setColor(mContext.getResources().getColor(R.color.recommendation_color))
+ .setCategory(API_NOTIFICATION_CATEGORY_RECOMMENDATION)
+ .setLargeIcon(mBitmap)
+ .setSmallIcon(mSmallIcon)
+ .setContentIntent(mIntent)
+ .setExtras(extras)
+ .setAutoCancel(false)
+ ).build();
return notification;
}
@@ -124,9 +124,9 @@ public class XBMCRecommendationBuilder
public String toString()
{
return "RecommendationBuilder{" + ", mId=" + mId + ", mPriority="
- + mPriority + ", mSmallIcon=" + mSmallIcon + ", mTitle='" + mTitle
- + '\'' + ", mDescription='" + mDescription + '\'' + ", mBitmap='"
- + mBitmap + '\'' + ", mBackgroundUri='" + mBackgroundUri + '\''
- + ", mIntent=" + mIntent + '}';
+ + mPriority + ", mSmallIcon=" + mSmallIcon + ", mTitle='" + mTitle
+ + '\'' + ", mDescription='" + mDescription + '\'' + ", mBitmap='"
+ + mBitmap + '\'' + ", mBackgroundUri='" + mBackgroundUri + '\''
+ + ", mIntent=" + mIntent + '}';
}
}
diff --git a/tools/android/packaging/xbmc/src/XBMCSearchableActivity.java.in b/tools/android/packaging/xbmc/src/XBMCSearchableActivity.java.in
index 15a13fd2aa..0058c5ad1b 100644
--- a/tools/android/packaging/xbmc/src/XBMCSearchableActivity.java.in
+++ b/tools/android/packaging/xbmc/src/XBMCSearchableActivity.java.in
@@ -16,7 +16,7 @@ public class XBMCSearchableActivity extends Activity
private static final String TAG = "@APP_NAME@Search";
private ListView mListView;
-
+
public void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
@@ -36,24 +36,24 @@ public class XBMCSearchableActivity extends Activity
private void search(String query)
{
Cursor c = getContentResolver().query(
- Uri.parse("content://@APP_PACKAGE@.media/search/" + query), null, null,
- null, null);
+ Uri.parse("content://@APP_PACKAGE@.media/search/" + query), null, null,
+ null, null);
// Specify the columns we want to display in the result
String[] from = new String[]
- { XBMCJsonRPC.COLUMN_TITLE, XBMCJsonRPC.COLUMN_TAGLINE };
+ {XBMCJsonRPC.COLUMN_TITLE, XBMCJsonRPC.COLUMN_TAGLINE};
// Specify the corresponding layout elements where we want the columns to go
int[] to = new int[]
- { R.id.title, R.id.tagline };
+ {R.id.title, R.id.tagline};
// Create a simple cursor adapter for the definitions and apply them to the
// ListView
SimpleCursorAdapter words = new SimpleCursorAdapter(this, R.layout.result,
- c, from, to);
+ c, from, to);
mListView.setAdapter(words);
}
-
+
private void doAction(Intent origIntent)
{
Uri data = origIntent.getData();
@@ -65,13 +65,13 @@ public class XBMCSearchableActivity extends Activity
newIntent.addFlags(Intent.FLAG_ACTIVITY_PREVIOUS_IS_TOP);
startActivity(newIntent);
finish();
-}
+ }
private void handleIntent(Intent intent)
{
Log.d(TAG, "NEW INTENT: " + intent.getAction() + "; DATA=" + intent.getData().toString());
- if (Intent.ACTION_SEARCH.equals(intent.getAction()))
+ if (Intent.ACTION_SEARCH.equals(intent.getAction()))
{
search(intent.getStringExtra(SearchManager.QUERY));
}
diff --git a/tools/android/packaging/xbmc/src/XBMCSettingsContentObserver.java.in b/tools/android/packaging/xbmc/src/XBMCSettingsContentObserver.java.in
index fc90a853e1..6992aa3e6a 100644
--- a/tools/android/packaging/xbmc/src/XBMCSettingsContentObserver.java.in
+++ b/tools/android/packaging/xbmc/src/XBMCSettingsContentObserver.java.in
@@ -31,7 +31,7 @@ public class XBMCSettingsContentObserver extends ContentObserver
@Override
public void onChange(boolean selfChange)
{
- onChange(selfChange, null);
+ onChange(selfChange, null);
}
// Implement the onChange(boolean, Uri) method to take advantage of the new Uri argument.
@@ -42,12 +42,12 @@ public class XBMCSettingsContentObserver extends ContentObserver
Log.d(TAG, "Setting changed: " + uri.toString());
if (
- uri.compareTo(Uri.parse("content://settings/system/volume_music_speaker")) == 0 ||
- uri.compareTo(Uri.parse("content://settings/system/volume_music_hdmi")) == 0
- )
+ uri.compareTo(Uri.parse("content://settings/system/volume_music_speaker")) == 0 ||
+ uri.compareTo(Uri.parse("content://settings/system/volume_music_hdmi")) == 0
+ )
{
AudioManager audio = (AudioManager) context
- .getSystemService(Context.AUDIO_SERVICE);
+ .getSystemService(Context.AUDIO_SERVICE);
int currentVolume = audio.getStreamVolume(AudioManager.STREAM_MUSIC);
if (currentVolume != previousVolume)
diff --git a/tools/android/packaging/xbmc/src/XBMCVideoView.java.in b/tools/android/packaging/xbmc/src/XBMCVideoView.java.in
index d86f521462..9957f1e524 100644
--- a/tools/android/packaging/xbmc/src/XBMCVideoView.java.in
+++ b/tools/android/packaging/xbmc/src/XBMCVideoView.java.in
@@ -17,10 +17,12 @@ import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;
public class XBMCVideoView extends SurfaceView implements
- SurfaceHolder.Callback
+ SurfaceHolder.Callback
{
native void _surfaceChanged(SurfaceHolder holder, int format, int width, int height);
+
native void _surfaceCreated(SurfaceHolder holder);
+
native void _surfaceDestroyed(SurfaceHolder holder);
private static final String TAG = "XBMCVideoPlayView";
@@ -30,7 +32,8 @@ public class XBMCVideoView extends SurfaceView implements
public static XBMCVideoView createVideoView()
{
- FutureTask<XBMCVideoView> futureResult = new FutureTask<XBMCVideoView>(new Callable<XBMCVideoView>() {
+ FutureTask<XBMCVideoView> futureResult = new FutureTask<XBMCVideoView>(new Callable<XBMCVideoView>()
+ {
@Override
public XBMCVideoView call() throws Exception
{
@@ -91,7 +94,8 @@ public class XBMCVideoView extends SurfaceView implements
if (!mIsCreated)
{
return null;
- } else
+ }
+ else
{
Log.d(TAG, "getSurface() = " + getHolder().getSurface());
return getHolder().getSurface();
@@ -135,7 +139,7 @@ public class XBMCVideoView extends SurfaceView implements
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width,
- int height)
+ int height)
{
if (holder != getHolder())
return;
@@ -143,7 +147,7 @@ public class XBMCVideoView extends SurfaceView implements
_surfaceChanged(holder, format, width, height);
Log.d(TAG, "Changed, format:" + format + ", width:" + width
- + ", height:" + height);
+ + ", height:" + height);
}
@Override
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 97ae0318cb..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,40 +6,40 @@ 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.
- *
+ *
* 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.
- *
+ *
* This is based on code from the following stackoverflow description on how to
* populate a ParcelFileDescriptor from any input stream.
- *
+ *
* http://stackoverflow.com/a/14734310/1950264
- *
+ *
* You still need to setup a ContentProvider entry and Authority in the
* AndroidManifest.xml
- *
+ *
* 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@";
-
- public static String AUTHORITY = "@APP_PACKAGE@";
- public static String AUTHORITY_IMAGE = AUTHORITY + ".image";
-
+ private static String TAG = "@APP_NAME@_Image_Provider";
+
+ public static String AUTHORITY = AUTHORITY_ROOT + ".image";
+
@Override
public boolean onCreate()
{
@@ -48,22 +48,27 @@ 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)
+ .authority(AUTHORITY)
.fragment(surl);
-
+
Uri out = builder.build();
// Log.d(TAG, "GetImageUri: in:" + surl + " out:" + out.toString());
- return out;
+ return out;
}
-
+
@Override
public ParcelFileDescriptor openFile(Uri uri, String mode)
throws FileNotFoundException
{
// Log.d(TAG, "openFile: " + uri.toString());
-
+
ParcelFileDescriptor[] pipe = null;
try
@@ -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();
@@ -86,7 +91,7 @@ public class XBMCImageContentProvider extends ContentProvider
new TransferThread(connection.getInputStream(),
new ParcelFileDescriptor.AutoCloseOutputStream(pipe[1])).start();
- }
+ }
catch (IOException e)
{
Log.e(getClass().getSimpleName(), "Exception opening pipe", e);
@@ -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);
}
@@ -156,7 +161,7 @@ public class XBMCImageContentProvider extends ContentProvider
in.close();
out.flush();
out.close();
- }
+ }
catch (IOException e)
{
Log.e(getClass().getSimpleName(), "Exception transferring file", e);
diff --git a/tools/android/packaging/xbmc/src/XBMCMediaContentProvider.java.in b/tools/android/packaging/xbmc/src/content/XBMCMediaContentProvider.java.in
index e6ca7f22fc..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
+import @APP_PACKAGE@.XBMCJsonRPC;
+
+public class XBMCMediaContentProvider extends XBMCContentProvider
{
- private static String TAG = "@APP_NAME@mediaprovider";
+ private static String TAG = "@APP_NAME@_Media_Provider";
- public static final String AUTHORITY = "@APP_PACKAGE@";
- public static final String AUTHORITY_MEDIA = AUTHORITY + ".media";
+ 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;
}
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/interfaces/XBMCNsdManagerDiscoveryListener.java.in b/tools/android/packaging/xbmc/src/interfaces/XBMCNsdManagerDiscoveryListener.java.in
index 8457769dca..5e5d96eaa8 100644
--- a/tools/android/packaging/xbmc/src/interfaces/XBMCNsdManagerDiscoveryListener.java.in
+++ b/tools/android/packaging/xbmc/src/interfaces/XBMCNsdManagerDiscoveryListener.java.in
@@ -6,45 +6,50 @@ import android.util.Log;
public class XBMCNsdManagerDiscoveryListener implements NsdManager.DiscoveryListener
{
- native void _onDiscoveryStarted (String serviceType);
- native void _onDiscoveryStopped (String serviceType);
- native void _onServiceFound (NsdServiceInfo serviceInfo);
- native void _onServiceLost (NsdServiceInfo serviceInfo);
- native void _onStartDiscoveryFailed (String serviceType, int errorCode);
- native void _onStopDiscoveryFailed (String serviceType, int errorCode);
+ native void _onDiscoveryStarted(String serviceType);
+
+ native void _onDiscoveryStopped(String serviceType);
+
+ native void _onServiceFound(NsdServiceInfo serviceInfo);
+
+ native void _onServiceLost(NsdServiceInfo serviceInfo);
+
+ native void _onStartDiscoveryFailed(String serviceType, int errorCode);
+
+ native void _onStopDiscoveryFailed(String serviceType, int errorCode);
@Override
- public void onDiscoveryStarted (String serviceType)
+ public void onDiscoveryStarted(String serviceType)
{
_onDiscoveryStarted(serviceType);
}
@Override
- public void onDiscoveryStopped (String serviceType)
+ public void onDiscoveryStopped(String serviceType)
{
_onDiscoveryStopped(serviceType);
}
-
+
@Override
- public void onServiceFound (NsdServiceInfo serviceInfo)
+ public void onServiceFound(NsdServiceInfo serviceInfo)
{
- _onServiceFound (serviceInfo);
+ _onServiceFound(serviceInfo);
}
@Override
- public void onServiceLost (NsdServiceInfo serviceInfo)
+ public void onServiceLost(NsdServiceInfo serviceInfo)
{
- _onServiceLost (serviceInfo);
+ _onServiceLost(serviceInfo);
}
@Override
- public void onStartDiscoveryFailed (String serviceType, int errorCode)
+ public void onStartDiscoveryFailed(String serviceType, int errorCode)
{
_onStartDiscoveryFailed(serviceType, errorCode);
}
-
+
@Override
- public void onStopDiscoveryFailed (String serviceType, int errorCode)
+ public void onStopDiscoveryFailed(String serviceType, int errorCode)
{
_onStopDiscoveryFailed(serviceType, errorCode);
}
diff --git a/tools/android/packaging/xbmc/src/interfaces/XBMCNsdManagerRegistrationListener.java.in b/tools/android/packaging/xbmc/src/interfaces/XBMCNsdManagerRegistrationListener.java.in
index 0e82a6d140..7a6f0eb238 100644
--- a/tools/android/packaging/xbmc/src/interfaces/XBMCNsdManagerRegistrationListener.java.in
+++ b/tools/android/packaging/xbmc/src/interfaces/XBMCNsdManagerRegistrationListener.java.in
@@ -6,34 +6,37 @@ import android.util.Log;
public class XBMCNsdManagerRegistrationListener implements NsdManager.RegistrationListener
{
- native void _onRegistrationFailed (NsdServiceInfo serviceInfo, int errorCode);
- native void _onServiceRegistered (NsdServiceInfo serviceInfo);
- native void _onServiceUnregistered (NsdServiceInfo serviceInfo);
- native void _onUnregistrationFailed (NsdServiceInfo serviceInfo, int errorCode);
+ native void _onRegistrationFailed(NsdServiceInfo serviceInfo, int errorCode);
+
+ native void _onServiceRegistered(NsdServiceInfo serviceInfo);
+
+ native void _onServiceUnregistered(NsdServiceInfo serviceInfo);
+
+ native void _onUnregistrationFailed(NsdServiceInfo serviceInfo, int errorCode);
@Override
- public void onRegistrationFailed (NsdServiceInfo serviceInfo, int errorCode)
+ public void onRegistrationFailed(NsdServiceInfo serviceInfo, int errorCode)
{
_onRegistrationFailed(serviceInfo, errorCode);
}
@Override
- public void onServiceRegistered (NsdServiceInfo serviceInfo)
+ public void onServiceRegistered(NsdServiceInfo serviceInfo)
{
- _onServiceRegistered (serviceInfo);
+ _onServiceRegistered(serviceInfo);
}
@Override
- public void onServiceUnregistered (NsdServiceInfo serviceInfo)
+ public void onServiceUnregistered(NsdServiceInfo serviceInfo)
{
- _onServiceUnregistered (serviceInfo);
+ _onServiceUnregistered(serviceInfo);
}
@Override
- public void onUnregistrationFailed (NsdServiceInfo serviceInfo, int errorCode)
+ public void onUnregistrationFailed(NsdServiceInfo serviceInfo, int errorCode)
{
_onUnregistrationFailed(serviceInfo, errorCode);
diff --git a/tools/android/packaging/xbmc/src/interfaces/XBMCNsdManagerResolveListener.java.in b/tools/android/packaging/xbmc/src/interfaces/XBMCNsdManagerResolveListener.java.in
index 64eb6201c2..4af13c9bd9 100644
--- a/tools/android/packaging/xbmc/src/interfaces/XBMCNsdManagerResolveListener.java.in
+++ b/tools/android/packaging/xbmc/src/interfaces/XBMCNsdManagerResolveListener.java.in
@@ -7,19 +7,20 @@ import android.util.Log;
public class XBMCNsdManagerResolveListener implements NsdManager.ResolveListener
{
native void _onResolveFailed(NsdServiceInfo serviceInfo, int errorCode);
+
native void _onServiceResolved(NsdServiceInfo serviceInfo);
@Override
- public void onResolveFailed (NsdServiceInfo serviceInfo, int errorCode)
+ public void onResolveFailed(NsdServiceInfo serviceInfo, int errorCode)
{
_onResolveFailed(serviceInfo, errorCode);
}
@Override
- public void onServiceResolved (NsdServiceInfo serviceInfo)
+ public void onServiceResolved(NsdServiceInfo serviceInfo)
{
- _onServiceResolved (serviceInfo);
+ _onServiceResolved(serviceInfo);
}
}
diff --git a/tools/android/packaging/xbmc/src/interfaces/XBMCSurfaceTextureOnFrameAvailableListener.java.in b/tools/android/packaging/xbmc/src/interfaces/XBMCSurfaceTextureOnFrameAvailableListener.java.in
index 9869733781..de94a64b1f 100644
--- a/tools/android/packaging/xbmc/src/interfaces/XBMCSurfaceTextureOnFrameAvailableListener.java.in
+++ b/tools/android/packaging/xbmc/src/interfaces/XBMCSurfaceTextureOnFrameAvailableListener.java.in
@@ -10,6 +10,6 @@ public class XBMCSurfaceTextureOnFrameAvailableListener implements OnFrameAvaila
@Override
public void onFrameAvailable(SurfaceTexture surfaceTexture)
{
- _onFrameAvailable(surfaceTexture);
+ _onFrameAvailable(surfaceTexture);
}
}
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>
diff --git a/xbmc/FileItem.cpp b/xbmc/FileItem.cpp
index 57b4f8c9ca..6215b771fc 100644
--- a/xbmc/FileItem.cpp
+++ b/xbmc/FileItem.cpp
@@ -185,6 +185,10 @@ CFileItem::CFileItem(const CPVRChannelPtr& channel)
if (!channel->IconPath().empty())
SetIconImage(channel->IconPath());
+ else if (channel->IsRadio())
+ SetIconImage("DefaultAudio.png");
+ else
+ SetIconImage("DefaultTVShows.png");
SetProperty("channelid", channel->ChannelID());
SetProperty("path", channel->Path());
@@ -1300,12 +1304,12 @@ void CFileItem::FillInDefaultIcon()
if (GetPVRChannelInfoTag()->IsRadio())
SetIconImage("DefaultAudio.png");
else
- SetIconImage("DefaultVideo.png");
+ SetIconImage("DefaultTVShows.png");
}
else if ( IsLiveTV() )
{
// Live TV Channel
- SetIconImage("DefaultVideo.png");
+ SetIconImage("DefaultTVShows.png");
}
else if ( URIUtils::IsArchive(m_strPath) )
{ // archive
diff --git a/xbmc/cores/VideoPlayer/VideoRenderers/LinuxRendererGL.cpp b/xbmc/cores/VideoPlayer/VideoRenderers/LinuxRendererGL.cpp
index 2348592383..255c5ffb41 100644
--- a/xbmc/cores/VideoPlayer/VideoRenderers/LinuxRendererGL.cpp
+++ b/xbmc/cores/VideoPlayer/VideoRenderers/LinuxRendererGL.cpp
@@ -896,13 +896,13 @@ void CLinuxRendererGL::LoadShaders(int field)
// create regular progressive scan shader
// if single pass, create GLSLOutput helper and pass it to YUV2RGB shader
EShaderFormat shaderFormat = GetShaderFormat();
- GLSLOutput *out = nullptr;
+ std::shared_ptr<GLSLOutput> out;
if (m_renderQuality == RQ_SINGLEPASS)
{
- out = new GLSLOutput(4, m_useDithering, m_ditherDepth,
- m_cmsOn ? m_fullRange : false,
- m_cmsOn ? m_tCLUTTex : 0,
- m_CLUTsize);
+ out = std::make_shared<GLSLOutput>(GLSLOutput(4, m_useDithering, m_ditherDepth,
+ m_cmsOn ? m_fullRange : false,
+ m_cmsOn ? m_tCLUTTex : 0,
+ m_CLUTsize));
if (m_scalingMethod == VS_SCALINGMETHOD_LANCZOS3_FAST || m_scalingMethod == VS_SCALINGMETHOD_SPLINE36_FAST)
{
diff --git a/xbmc/cores/VideoPlayer/VideoRenderers/VideoShaders/YUV2RGBShaderGL.cpp b/xbmc/cores/VideoPlayer/VideoRenderers/VideoShaders/YUV2RGBShaderGL.cpp
index 6c1428f6d6..41ce2cf4bb 100644
--- a/xbmc/cores/VideoPlayer/VideoRenderers/VideoShaders/YUV2RGBShaderGL.cpp
+++ b/xbmc/cores/VideoPlayer/VideoRenderers/VideoShaders/YUV2RGBShaderGL.cpp
@@ -59,7 +59,7 @@ static void CalculateYUVMatrixGL(GLfloat res[4][4]
//////////////////////////////////////////////////////////////////////
BaseYUV2RGBGLSLShader::BaseYUV2RGBGLSLShader(bool rect, unsigned flags, EShaderFormat format, bool stretch,
- GLSLOutput *output)
+ std::shared_ptr<GLSLOutput> output)
{
m_width = 1;
m_height = 1;
@@ -118,7 +118,7 @@ BaseYUV2RGBGLSLShader::BaseYUV2RGBGLSLShader(bool rect, unsigned flags, EShaderF
BaseYUV2RGBGLSLShader::~BaseYUV2RGBGLSLShader()
{
Free();
- delete m_glslOutput;
+ m_glslOutput.reset();
}
void BaseYUV2RGBGLSLShader::OnCompiledAndLinked()
@@ -184,7 +184,7 @@ void BaseYUV2RGBGLSLShader::Free()
//////////////////////////////////////////////////////////////////////
YUV2RGBProgressiveShader::YUV2RGBProgressiveShader(bool rect, unsigned flags, EShaderFormat format, bool stretch,
- GLSLOutput *output)
+ std::shared_ptr<GLSLOutput> output)
: BaseYUV2RGBGLSLShader(rect, flags, format, stretch, output)
{
PixelShader()->LoadSource("gl_yuv2rgb_basic.glsl", m_defines);
@@ -199,7 +199,7 @@ YUV2RGBFilterShader4::YUV2RGBFilterShader4(bool rect, unsigned flags,
EShaderFormat format,
bool stretch,
ESCALINGMETHOD method,
- GLSLOutput *output)
+ std::shared_ptr<GLSLOutput> output)
: BaseYUV2RGBGLSLShader(rect, flags, format, stretch, output)
{
m_scaling = method;
@@ -238,8 +238,8 @@ void YUV2RGBFilterShader4::OnCompiledAndLinked()
//TEXTARGET is set to GL_TEXTURE_1D or GL_TEXTURE_2D
glActiveTexture(GL_TEXTURE3);
glBindTexture(GL_TEXTURE_1D, m_kernelTex);
- glTexParameteri(GL_TEXTURE_1D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
- glTexParameteri(GL_TEXTURE_1D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
+ glTexParameteri(GL_TEXTURE_1D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
+ glTexParameteri(GL_TEXTURE_1D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
GLvoid* data = (GLvoid*)kernel.GetFloatPixels();
glTexImage1D(GL_TEXTURE_1D, 0, GL_RGBA32F, kernel.GetSize(), 0, GL_RGBA, GL_FLOAT, data);
diff --git a/xbmc/cores/VideoPlayer/VideoRenderers/VideoShaders/YUV2RGBShaderGL.h b/xbmc/cores/VideoPlayer/VideoRenderers/VideoShaders/YUV2RGBShaderGL.h
index e677c396c3..0d98f4714b 100644
--- a/xbmc/cores/VideoPlayer/VideoRenderers/VideoShaders/YUV2RGBShaderGL.h
+++ b/xbmc/cores/VideoPlayer/VideoRenderers/VideoShaders/YUV2RGBShaderGL.h
@@ -25,6 +25,8 @@
#include "guilib/Shader.h"
#include "cores/VideoSettings.h"
+#include <memory>
+
void CalculateYUVMatrix(TransformMatrix &matrix
, unsigned int flags
, EShaderFormat format
@@ -37,7 +39,7 @@ namespace Shaders {
class BaseYUV2RGBGLSLShader : public CGLSLShaderProgram
{
public:
- BaseYUV2RGBGLSLShader(bool rect, unsigned flags, EShaderFormat format, bool stretch, GLSLOutput *output=nullptr);
+ BaseYUV2RGBGLSLShader(bool rect, unsigned flags, EShaderFormat format, bool stretch, std::shared_ptr<GLSLOutput> output);
virtual ~BaseYUV2RGBGLSLShader();
void SetField(int field) { m_field = field; }
@@ -82,7 +84,7 @@ protected:
std::string m_defines;
- Shaders::GLSLOutput *m_glslOutput = nullptr;
+ std::shared_ptr<Shaders::GLSLOutput> m_glslOutput;
// pixel shader attribute handles
GLint m_hYTex = -1;
@@ -109,7 +111,7 @@ public:
unsigned flags,
EShaderFormat format,
bool stretch,
- GLSLOutput *output);
+ std::shared_ptr<GLSLOutput> output);
};
class YUV2RGBFilterShader4 : public BaseYUV2RGBGLSLShader
@@ -120,7 +122,7 @@ public:
EShaderFormat format,
bool stretch,
ESCALINGMETHOD method,
- GLSLOutput *output);
+ std::shared_ptr<GLSLOutput> output);
~YUV2RGBFilterShader4() override;
protected:
diff --git a/xbmc/dialogs/GUIDialogContextMenu.h b/xbmc/dialogs/GUIDialogContextMenu.h
index 7a7968fbea..57d8684d82 100644
--- a/xbmc/dialogs/GUIDialogContextMenu.h
+++ b/xbmc/dialogs/GUIDialogContextMenu.h
@@ -92,6 +92,7 @@ enum CONTEXT_BUTTON { CONTEXT_BUTTON_CANCELLED = 0,
CONTEXT_BUTTON_BEGIN,
CONTEXT_BUTTON_END,
CONTEXT_BUTTON_NOW,
+ CONTEXT_BUTTON_DATE,
CONTEXT_BUTTON_PLAY_AND_QUEUE,
CONTEXT_BUTTON_PLAY_ONLY_THIS,
CONTEXT_BUTTON_UPDATE_EPG,
diff --git a/xbmc/guiinfo/GUIInfoLabels.h b/xbmc/guiinfo/GUIInfoLabels.h
index 50b0bc83c1..70ce455928 100644
--- a/xbmc/guiinfo/GUIInfoLabels.h
+++ b/xbmc/guiinfo/GUIInfoLabels.h
@@ -529,7 +529,7 @@
#define PVR_CHANNEL_NUMBER_INPUT (PVR_STRINGS_START + 59)
#define PVR_EPG_EVENT_REMAINING_TIME (PVR_STRINGS_START + 60)
#define PVR_EPG_EVENT_FINISH_TIME (PVR_STRINGS_START + 61)
-#define PVR_STRINGS_END PVR_PLAYING_FINISH_TIME
+#define PVR_STRINGS_END PVR_EPG_EVENT_FINISH_TIME
#define ADSP_CONDITIONS_START 1300
#define ADSP_IS_ACTIVE (ADSP_CONDITIONS_START)
diff --git a/xbmc/network/GUIDialogNetworkSetup.cpp b/xbmc/network/GUIDialogNetworkSetup.cpp
index bf6ec3360c..3bc8e463f3 100644
--- a/xbmc/network/GUIDialogNetworkSetup.cpp
+++ b/xbmc/network/GUIDialogNetworkSetup.cpp
@@ -215,6 +215,7 @@ void CGUIDialogNetworkSetup::InitializeSettings()
{ true, true, true, true, false, 443, "davs", 20254},
{ true, true, true, true, false, 80, "dav", 20253},
{ true, true, true, true, false, 21, "ftp", 20173},
+ { true, true, true, true, false, 990, "ftps", 20174},
{false, false, false, false, true, 0, "upnp", 20175},
{ true, true, true, true, false, 80, "rss", 20304}};
@@ -377,7 +378,7 @@ void CGUIDialogNetworkSetup::UpdateButtons()
SendMessage(GUI_MSG_SET_TYPE, passControlID, CGUIEditControl::INPUT_TYPE_PASSWORD, 12326);
}
- // server browse should be disabled if we are in FTP, HTTP, HTTPS, RSS, DAV or DAVS
+ // server browse should be disabled if we are in FTP, FTPS, HTTP, HTTPS, RSS, DAV or DAVS
BaseSettingControlPtr browseControl = GetSettingControl(SETTING_SERVER_BROWSE);
if (browseControl != NULL && browseControl->GetControl() != NULL)
{
diff --git a/xbmc/platform/android/activity/XBMCApp.cpp b/xbmc/platform/android/activity/XBMCApp.cpp
index 911f9346a4..77486fcdd9 100644
--- a/xbmc/platform/android/activity/XBMCApp.cpp
+++ b/xbmc/platform/android/activity/XBMCApp.cpp
@@ -459,31 +459,11 @@ void CXBMCApp::run()
SetupEnv();
XBMC::Context context;
- CJNIIntent startIntent = getIntent();
-
- android_printf("%s Started with action: %s\n", CCompileInfo::GetAppName(), startIntent.getAction().c_str());
-
- CAppParamParser appParamParser;
- std::string filenameToPlay = GetFilenameFromIntent(startIntent);
- if (!filenameToPlay.empty())
- {
- android_printf("-- filename: %s", filenameToPlay.c_str());
- int argc = 2;
- const char** argv = (const char**) malloc(argc*sizeof(char*));
-
- std::string exe_name(CCompileInfo::GetAppName());
- argv[0] = exe_name.c_str();
- argv[1] = filenameToPlay.c_str();
-
- appParamParser.Parse(argv, argc);
-
- free(argv);
- }
-
m_firstrun=false;
android_printf(" => running XBMC_Run...");
try
{
+ CAppParamParser appParamParser;
status = XBMC_Run(true, appParamParser);
android_printf(" => XBMC_Run finished with %d", status);
}
@@ -1047,21 +1027,26 @@ void CXBMCApp::onNewIntent(CJNIIntent intent)
std::string action = intent.getAction();
CLog::Log(LOGDEBUG, "CXBMCApp::onNewIntent - Got intent. Action: %s", action.c_str());
std::string targetFile = GetFilenameFromIntent(intent);
- CLog::Log(LOGDEBUG, "-- targetFile: %s", targetFile.c_str());
- if (action == "android.intent.action.VIEW" || action == "android.intent.action.GET_CONTENT")
+ if (!targetFile.empty() && (action == "android.intent.action.VIEW" || action == "android.intent.action.GET_CONTENT"))
{
+ CLog::Log(LOGDEBUG, "-- targetFile: %s", targetFile.c_str());
+
CURL targeturl(targetFile);
std::string value;
if (action == "android.intent.action.GET_CONTENT" || (targeturl.GetOption("showinfo", value) && value == "true"))
{
- if (targeturl.IsProtocol("videodb"))
+ if (targeturl.IsProtocol("videodb")
+ || (targeturl.IsProtocol("special") && targetFile.find("playlists/video") != std::string::npos)
+ || (targeturl.IsProtocol("special") && targetFile.find("playlists/mixed") != std::string::npos)
+ )
{
std::vector<std::string> params;
params.push_back(targeturl.Get());
params.push_back("return");
CApplicationMessenger::GetInstance().PostMsg(TMSG_GUI_ACTIVATE_WINDOW, WINDOW_VIDEO_NAV, 0, nullptr, "", params);
}
- else if (targeturl.IsProtocol("musicdb"))
+ else if (targeturl.IsProtocol("musicdb")
+ || (targeturl.IsProtocol("special") && targetFile.find("playlists/music") != std::string::npos))
{
std::vector<std::string> params;
params.push_back(targeturl.Get());
diff --git a/xbmc/pvr/PVRGUIInfo.cpp b/xbmc/pvr/PVRGUIInfo.cpp
index 4e18343eac..3e86520604 100644
--- a/xbmc/pvr/PVRGUIInfo.cpp
+++ b/xbmc/pvr/PVRGUIInfo.cpp
@@ -81,6 +81,7 @@ void CPVRGUIInfo::ResetProperties(void)
m_bCanRecordPlayingChannel = false;
m_bHasTVChannels = false;
m_bHasRadioChannels = false;
+ m_bHasTimeshiftData = false;
m_bIsTimeshifting = false;
m_iStartTime = time_t(0);
m_iTimeshiftStartTime = time_t(0);
@@ -246,8 +247,20 @@ void CPVRGUIInfo::UpdateTimeshift(void)
{
if (!CServiceBroker::GetPVRManager().IsPlayingTV() && !CServiceBroker::GetPVRManager().IsPlayingRadio())
{
+ // If nothing is playing (anymore), there is no need to poll the timeshift values from the clients.
CSingleLock lock(m_critSection);
- m_iStartTime = 0;
+ if (m_bHasTimeshiftData)
+ {
+ m_bHasTimeshiftData = false;
+ m_bIsTimeshifting = false;
+ m_iStartTime = 0;
+ m_iTimeshiftStartTime = 0;
+ m_iTimeshiftEndTime = 0;
+ m_iTimeshiftPlayTime = 0;
+ m_strTimeshiftStartTime.clear();
+ m_strTimeshiftEndTime.clear();
+ m_strTimeshiftPlayTime.clear();
+ }
return;
}
@@ -282,6 +295,8 @@ void CPVRGUIInfo::UpdateTimeshift(void)
tmp.SetFromUTCDateTime(m_iTimeshiftPlayTime);
m_strTimeshiftPlayTime = tmp.GetAsLocalizedTime("", true);
+
+ m_bHasTimeshiftData = true;
}
bool CPVRGUIInfo::TranslateCharInfo(DWORD dwInfo, std::string &strValue) const
diff --git a/xbmc/pvr/PVRGUIInfo.h b/xbmc/pvr/PVRGUIInfo.h
index 8af9629c0b..2a04e5f071 100644
--- a/xbmc/pvr/PVRGUIInfo.h
+++ b/xbmc/pvr/PVRGUIInfo.h
@@ -261,6 +261,7 @@ namespace PVR
CPVREpgInfoTagPtr m_playingEpgTag;
std::vector<SBackend> m_backendProperties;
+ bool m_bHasTimeshiftData;
bool m_bIsTimeshifting;
time_t m_iStartTime;
time_t m_iTimeshiftStartTime;
diff --git a/xbmc/pvr/windows/GUIEPGGridContainer.cpp b/xbmc/pvr/windows/GUIEPGGridContainer.cpp
index 931ad062b5..b2b72b32a6 100644
--- a/xbmc/pvr/windows/GUIEPGGridContainer.cpp
+++ b/xbmc/pvr/windows/GUIEPGGridContainer.cpp
@@ -1302,6 +1302,11 @@ CPVRChannelPtr CGUIEPGGridContainer::GetSelectedChannel() const
return CPVRChannelPtr();
}
+CDateTime CGUIEPGGridContainer::GetSelectedDate() const
+{
+ return m_gridModel->GetStartTimeForBlock(m_blockOffset + m_blockCursor);
+}
+
int CGUIEPGGridContainer::GetSelectedItem() const
{
if (!m_gridModel->HasGridItems() ||
@@ -1651,6 +1656,13 @@ void CGUIEPGGridContainer::GoToNow()
SetBlock(m_gridModel->GetPageNowOffset());
}
+void CGUIEPGGridContainer::GoToDate(const CDateTime &date)
+{
+ unsigned int offset = m_gridModel->GetPageNowOffset();
+ ScrollToBlockOffset(m_gridModel->GetBlock(date) - offset);
+ SetBlock(offset);
+}
+
void CGUIEPGGridContainer::SetTimelineItems(const std::unique_ptr<CFileItemList> &items, const CDateTime &gridStart, const CDateTime &gridEnd)
{
int iRulerUnit;
diff --git a/xbmc/pvr/windows/GUIEPGGridContainer.h b/xbmc/pvr/windows/GUIEPGGridContainer.h
index 7232896c68..1b30c8f1b6 100644
--- a/xbmc/pvr/windows/GUIEPGGridContainer.h
+++ b/xbmc/pvr/windows/GUIEPGGridContainer.h
@@ -71,6 +71,7 @@ namespace PVR
CFileItemPtr GetSelectedChannelItem() const;
PVR::CPVRChannelPtr GetSelectedChannel() const;
+ CDateTime GetSelectedDate() const;
void LoadLayout(TiXmlElement *layout);
void SetPageControl(int id);
@@ -85,6 +86,8 @@ namespace PVR
void GoToBegin();
void GoToEnd();
void GoToNow();
+ void GoToDate(const CDateTime &date);
+
void SetTimelineItems(const std::unique_ptr<CFileItemList> &items, const CDateTime &gridStart, const CDateTime &gridEnd);
/*!
* @brief Set the control's selection to the given channel and set the control's view port to show the channel.
diff --git a/xbmc/pvr/windows/GUIEPGGridContainerModel.cpp b/xbmc/pvr/windows/GUIEPGGridContainerModel.cpp
index 30d794b822..ab1bc30afb 100644
--- a/xbmc/pvr/windows/GUIEPGGridContainerModel.cpp
+++ b/xbmc/pvr/windows/GUIEPGGridContainerModel.cpp
@@ -432,6 +432,16 @@ unsigned int CGUIEPGGridContainerModel::GetPageNowOffset() const
return GetGridStartPadding() / MINSPERBLOCK; // this is the 'now' block relative to page start
}
+CDateTime CGUIEPGGridContainerModel::GetStartTimeForBlock(int block) const
+{
+ if (block < 0)
+ block = 0;
+ else if (block >= m_blocks)
+ block = m_blocks - 1;
+
+ return m_gridStart + CDateTimeSpan(0, 0 , block * MINSPERBLOCK, 0);
+}
+
int CGUIEPGGridContainerModel::GetBlock(const CDateTime &datetime) const
{
int diff;
@@ -451,7 +461,7 @@ int CGUIEPGGridContainerModel::GetNowBlock() const
return GetBlock(CDateTime::GetUTCDateTime()) - GetPageNowOffset();
}
-int CGUIEPGGridContainerModel::GetFirstEventBlock(const CPVREpgInfoTagPtr event) const
+int CGUIEPGGridContainerModel::GetFirstEventBlock(const CPVREpgInfoTagPtr &event) const
{
const CDateTime eventStart = event->StartAsUTC();
int diff;
@@ -469,7 +479,7 @@ int CGUIEPGGridContainerModel::GetFirstEventBlock(const CPVREpgInfoTagPtr event)
return std::ceil(fBlockIndex);
}
-int CGUIEPGGridContainerModel::GetLastEventBlock(const CPVREpgInfoTagPtr event) const
+int CGUIEPGGridContainerModel::GetLastEventBlock(const CPVREpgInfoTagPtr &event) const
{
// Last block of a tag is always the block calculated using event's end time, not rounded up.
// Refer to CGUIEPGGridContainerModel::Refresh, where the model is created, for details!
diff --git a/xbmc/pvr/windows/GUIEPGGridContainerModel.h b/xbmc/pvr/windows/GUIEPGGridContainerModel.h
index 255ae3d40b..6efc152b7c 100644
--- a/xbmc/pvr/windows/GUIEPGGridContainerModel.h
+++ b/xbmc/pvr/windows/GUIEPGGridContainerModel.h
@@ -90,9 +90,10 @@ namespace PVR
unsigned int GetPageNowOffset() const;
int GetNowBlock() const;
+ CDateTime GetStartTimeForBlock(int block) const;
int GetBlock(const CDateTime &datetime) const;
- int GetFirstEventBlock(const CPVREpgInfoTagPtr event) const;
- int GetLastEventBlock(const CPVREpgInfoTagPtr event) const;
+ int GetFirstEventBlock(const CPVREpgInfoTagPtr &event) const;
+ int GetLastEventBlock(const CPVREpgInfoTagPtr &event) const;
private:
void FreeItemsMemory();
diff --git a/xbmc/pvr/windows/GUIWindowPVRGuide.cpp b/xbmc/pvr/windows/GUIWindowPVRGuide.cpp
index eedf471b4b..d20a325c9c 100644
--- a/xbmc/pvr/windows/GUIWindowPVRGuide.cpp
+++ b/xbmc/pvr/windows/GUIWindowPVRGuide.cpp
@@ -24,6 +24,7 @@
#include "GUIUserMessages.h"
#include "ServiceBroker.h"
#include "dialogs/GUIDialogBusy.h"
+#include "dialogs/GUIDialogNumeric.h"
#include "input/Key.h"
#include "messaging/ApplicationMessenger.h"
#include "settings/Settings.h"
@@ -161,11 +162,9 @@ void CGUIWindowPVRGuideBase::SetInvalid()
void CGUIWindowPVRGuideBase::GetContextButtons(int itemNumber, CContextButtons &buttons)
{
- if (itemNumber < 0 || itemNumber >= m_vecItems->Size())
- return;
-
buttons.Add(CONTEXT_BUTTON_BEGIN, 19063); /* Go to begin */
buttons.Add(CONTEXT_BUTTON_NOW, 19070); /* Go to now */
+ buttons.Add(CONTEXT_BUTTON_DATE, 19288); /* Go to date */
buttons.Add(CONTEXT_BUTTON_END, 19064); /* Go to end */
CGUIWindowPVRBase::GetContextButtons(itemNumber, buttons);
@@ -391,6 +390,19 @@ bool CGUIWindowPVRGuideBase::OnMessage(CGUIMessage& message)
}
break;
}
+ case ACTION_CONTEXT_MENU:
+ {
+ // EPG "gap" selected => create and process special context menu with item independent entries.
+ CContextButtons buttons;
+ GetContextButtons(-1, buttons);
+
+ int iButton = CGUIDialogContextMenu::ShowAndGetChoice(buttons);
+ if (iButton >= 0)
+ {
+ bReturn = OnContextButton(-1, static_cast<CONTEXT_BUTTON>(iButton));
+ }
+ break;
+ }
}
}
}
@@ -452,14 +464,28 @@ bool CGUIWindowPVRGuideBase::OnMessage(CGUIMessage& message)
bool CGUIWindowPVRGuideBase::OnContextButton(int itemNumber, CONTEXT_BUTTON button)
{
+ switch (button)
+ {
+ case CONTEXT_BUTTON_BEGIN:
+ return OnContextButtonBegin();
+
+ case CONTEXT_BUTTON_NOW:
+ return OnContextButtonNow();
+
+ case CONTEXT_BUTTON_DATE:
+ return OnContextButtonDate();
+
+ case CONTEXT_BUTTON_END:
+ return OnContextButtonEnd();
+
+ default:
+ break;
+ }
+
if (itemNumber < 0 || itemNumber >= m_vecItems->Size())
return false;
- CFileItemPtr pItem = m_vecItems->Get(itemNumber);
- return OnContextButtonBegin(pItem.get(), button) ||
- OnContextButtonEnd(pItem.get(), button) ||
- OnContextButtonNow(pItem.get(), button) ||
- CGUIMediaWindow::OnContextButton(itemNumber, button);
+ return CGUIMediaWindow::OnContextButton(itemNumber, button);
}
bool CGUIWindowPVRGuideBase::RefreshTimelineItems()
@@ -517,42 +543,35 @@ bool CGUIWindowPVRGuideBase::RefreshTimelineItems()
return false;
}
-bool CGUIWindowPVRGuideBase::OnContextButtonBegin(CFileItem *item, CONTEXT_BUTTON button)
+bool CGUIWindowPVRGuideBase::OnContextButtonBegin()
{
- bool bReturn = false;
-
- if (button == CONTEXT_BUTTON_BEGIN)
- {
- CGUIEPGGridContainer* epgGridContainer = GetGridControl();
- epgGridContainer->GoToBegin();
- bReturn = true;
- }
-
- return bReturn;
+ GetGridControl()->GoToBegin();
+ return true;
}
-bool CGUIWindowPVRGuideBase::OnContextButtonEnd(CFileItem *item, CONTEXT_BUTTON button)
+bool CGUIWindowPVRGuideBase::OnContextButtonEnd()
{
- bool bReturn = false;
-
- if (button == CONTEXT_BUTTON_END)
- {
- CGUIEPGGridContainer* epgGridContainer = GetGridControl();
- epgGridContainer->GoToEnd();
- bReturn = true;
- }
+ GetGridControl()->GoToEnd();
+ return true;
+}
- return bReturn;
+bool CGUIWindowPVRGuideBase::OnContextButtonNow()
+{
+ GetGridControl()->GoToNow();
+ return true;
}
-bool CGUIWindowPVRGuideBase::OnContextButtonNow(CFileItem *item, CONTEXT_BUTTON button)
+bool CGUIWindowPVRGuideBase::OnContextButtonDate()
{
bool bReturn = false;
- if (button == CONTEXT_BUTTON_NOW)
+ SYSTEMTIME date;
+ CGUIEPGGridContainer* epgGridContainer = GetGridControl();
+ epgGridContainer->GetSelectedDate().GetAsSystemTime(date);
+
+ if (CGUIDialogNumeric::ShowAndGetDate(date, g_localizeStrings.Get(19288))) /* Go to date */
{
- CGUIEPGGridContainer* epgGridContainer = GetGridControl();
- epgGridContainer->GoToNow();
+ epgGridContainer->GoToDate(CDateTime(date));
bReturn = true;
}
diff --git a/xbmc/pvr/windows/GUIWindowPVRGuide.h b/xbmc/pvr/windows/GUIWindowPVRGuide.h
index e7f116ff13..1cfab0f528 100644
--- a/xbmc/pvr/windows/GUIWindowPVRGuide.h
+++ b/xbmc/pvr/windows/GUIWindowPVRGuide.h
@@ -69,9 +69,10 @@ namespace PVR
bool SelectPlayingFile(void);
- bool OnContextButtonBegin(CFileItem *item, CONTEXT_BUTTON button);
- bool OnContextButtonEnd(CFileItem *item, CONTEXT_BUTTON button);
- bool OnContextButtonNow(CFileItem *item, CONTEXT_BUTTON button);
+ bool OnContextButtonBegin();
+ bool OnContextButtonEnd();
+ bool OnContextButtonNow();
+ bool OnContextButtonDate();
void StartRefreshTimelineItemsThread();
void StopRefreshTimelineItemsThread();
diff --git a/xbmc/rendering/gl/RenderSystemGL.cpp b/xbmc/rendering/gl/RenderSystemGL.cpp
index b50b8fc338..e944ac2975 100644
--- a/xbmc/rendering/gl/RenderSystemGL.cpp
+++ b/xbmc/rendering/gl/RenderSystemGL.cpp
@@ -195,6 +195,15 @@ bool CRenderSystemGL::ResetRenderSystem(int width, int height)
m_width = width;
m_height = height;
+ if (m_RenderVersionMajor > 3 ||
+ (m_RenderVersionMajor == 3 && m_RenderVersionMinor >= 2))
+ {
+ glBindVertexArray(0);
+ glDeleteVertexArrays(1, &m_vertexArray);
+ glGenVertexArrays(1, &m_vertexArray);
+ glBindVertexArray(m_vertexArray);
+ }
+
glClearColor( 0.0f, 0.0f, 0.0f, 0.0f );
CalculateMaxTexturesize();
diff --git a/xbmc/windows/GUIMediaWindow.cpp b/xbmc/windows/GUIMediaWindow.cpp
index 7a7d3f9dc9..44d5a84b8a 100644
--- a/xbmc/windows/GUIMediaWindow.cpp
+++ b/xbmc/windows/GUIMediaWindow.cpp
@@ -475,6 +475,7 @@ bool CGUIMediaWindow::OnMessage(CGUIMessage& message)
std::string path, fileName;
std::string dir = message.GetStringParam(0);
URIUtils::Split(dir, path, fileName);
+ URIUtils::RemoveExtension(fileName);
if (StringUtils::IsInteger(fileName))
dir = path;
const std::string &ret = message.GetStringParam(1);