diff options
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 Binary files differnew file mode 100755 index 0000000000..247a9505f6 --- /dev/null +++ b/media/icon80x80.png 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 Binary files differnew file mode 100644 index 0000000000..1bd52332dd --- /dev/null +++ b/tools/android/packaging/xbmc/res/drawable/ic_recommendation_80dp.png diff --git a/tools/android/packaging/xbmc/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); |