Compare commits

...

273 Commits

Author SHA1 Message Date
Bill Thornton
a67c486385 Merge pull request #311 from jellyfin/v0.11.1
v0.11.1
2020-02-06 23:40:00 -05:00
Bill Thornton
8aa0c5a18e Disable all subtitle rendering in exoplayer 2020-02-06 23:23:20 -05:00
Florianisme
4c5eab8d62 Fix wrong last position when media player is closed and progress still reported
(cherry picked from commit 8cd11c888c)
2020-02-06 14:58:43 +01:00
Bill Thornton
5e016b38b0 Fix crash on empty server address 2020-02-06 01:09:41 -05:00
Niels van Velzen
5e4e135239 Fix default home sections not used when the user partially changed them 2020-02-06 00:08:45 -05:00
Andreas
8c1d6c30de Fix displaying ACRA as enabled by default in preferences 2020-02-06 00:08:36 -05:00
Niels van Velzen
ec83ec0540 Update version to 0.11.1 2020-02-06 00:06:50 -05:00
Bill Thornton
32dd0ae3ce Merge pull request #289 from nielsvanvelzen/jitpack-finally-passed
Set apiclient version to 0.5.0
2020-02-04 08:22:52 -05:00
Niels van Velzen
e258ae8113 Set apiclient version to 0.5.0 2020-02-04 10:05:01 +01:00
Bill Thornton
c11d26c887 Merge remote-tracking branch 'upstream/master' into release 2020-02-03 17:06:58 -05:00
Bill Thornton
56d9950898 Merge pull request #286 from nielsvanvelzen/took-us-only-3-months
Update app version
2020-02-03 16:55:48 -05:00
Niels van Velzen
60007bf43d Update app version 2020-02-03 22:40:12 +01:00
Bill Thornton
1af15961e8 Merge pull request #279 from thornbill/remove-strings
Remove removed strings
2020-01-30 22:08:46 -05:00
Bill Thornton
e6f82208d2 Remove removed strings 2020-01-27 10:52:15 -05:00
Alexander Brissman
5cec9f1ef3 Translated using Weblate (Norwegian Bokmål)
Currently translated at 100.0% (346 of 346 strings)

Translation: Jellyfin/Jellyfin AndroidTV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-androidtv/nb_NO/
2020-01-26 14:17:26 -05:00
SaddFox
4cd27fc4a4 Translated using Weblate (Slovenian)
Currently translated at 40.4% (138 of 342 strings)

Translation: Jellyfin/Jellyfin AndroidTV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-androidtv/sl/
2020-01-25 10:17:25 -05:00
Niels van Velzen
269a6160b7 Translated using Weblate (Dutch)
Currently translated at 100.0% (342 of 342 strings)

Translation: Jellyfin/Jellyfin AndroidTV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-androidtv/nl/
2020-01-25 10:17:25 -05:00
Niels van Velzen
3a077e4d1c Merge pull request #274 from joshuaboniface/weblate-fail
Fix bad rebase merge from Weblate
2020-01-24 20:44:31 +01:00
Joshua M. Boniface
4b0390efd5 Fix bad rebase merge from Weblate 2020-01-23 13:41:42 -05:00
4d1m
41b52d3616 Translated using Weblate (Romanian)
Currently translated at 100.0% (342 of 342 strings)

Translation: Jellyfin/Jellyfin AndroidTV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-androidtv/ro/
2020-01-23 13:26:24 -05:00
NunzioArdi
182daa3ae4 Translated using Weblate (French)
Currently translated at 100.0% (342 of 342 strings)

Translation: Jellyfin/Jellyfin AndroidTV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-androidtv/fr/
2020-01-23 13:26:09 -05:00
nextlooper42
3f31bd349a Translated using Weblate (Slovak)
Currently translated at 100.0% (342 of 342 strings)

Translation: Jellyfin/Jellyfin AndroidTV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-androidtv/sk/
2020-01-23 13:25:49 -05:00
SaddFox
ab25bd7d81 Translated using Weblate (Slovenian)
Currently translated at 40.1% (137 of 342 strings)

Translation: Jellyfin/Jellyfin AndroidTV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-androidtv/sl/
2020-01-23 13:25:49 -05:00
Slobodan Simić
2c3ae8e6a8 Translated using Weblate (Serbian)
Currently translated at 23.7% (81 of 342 strings)

Translation: Jellyfin/Jellyfin AndroidTV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-androidtv/sr/
2020-01-23 13:25:49 -05:00
Z Yang
4d0bad1533 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (342 of 342 strings)

Translation: Jellyfin/Jellyfin AndroidTV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-androidtv/zh_Hans/
2020-01-23 13:25:49 -05:00
Alexander Brissman
e50c7f2211 Translated using Weblate (Norwegian Bokmål)
Currently translated at 100.0% (342 of 342 strings)

Translation: Jellyfin/Jellyfin AndroidTV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-androidtv/nb_NO/
2020-01-23 13:25:49 -05:00
nextlooper42
128ee79abb Translated using Weblate (Slovak)
Currently translated at 51.8% (177 of 342 strings)

Translation: Jellyfin/Jellyfin AndroidTV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-androidtv/sk/
2020-01-23 13:25:23 -05:00
Philmo67
6a6c6c275a Translated using Weblate (French)
Currently translated at 99.7% (341 of 342 strings)

Translation: Jellyfin/Jellyfin AndroidTV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-androidtv/fr/
2020-01-23 13:25:13 -05:00
Slobodan Simić
04dddff75f Added translation using Weblate (Serbian) 2020-01-23 13:24:54 -05:00
Philmo67
b2cb898534 Translated using Weblate (French)
Currently translated at 99.7% (341 of 342 strings)

Translation: Jellyfin/Jellyfin AndroidTV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-androidtv/fr/
2020-01-23 13:24:54 -05:00
Bakumin
1268a72527 Translated using Weblate (German)
Currently translated at 100.0% (342 of 342 strings)

Translation: Jellyfin/Jellyfin AndroidTV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-androidtv/de/
2020-01-23 13:24:54 -05:00
Florian
49e7c760a2 Translated using Weblate (German)
Currently translated at 100.0% (342 of 342 strings)

Translation: Jellyfin/Jellyfin AndroidTV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-androidtv/de/
2020-01-23 13:24:54 -05:00
spagnoloG
535602b67c Translated using Weblate (Slovenian)
Currently translated at 31.6% (108 of 342 strings)

Translation: Jellyfin/Jellyfin AndroidTV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-androidtv/sl/
2020-01-23 13:24:34 -05:00
tokiedokie
8fee67c2e4 Translated using Weblate (Japanese)
Currently translated at 90.1% (308 of 342 strings)

Translation: Jellyfin/Jellyfin AndroidTV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-androidtv/ja/
2020-01-23 13:24:34 -05:00
Bakumin
89a283440b Translated using Weblate (German)
Currently translated at 100.0% (342 of 342 strings)

Translation: Jellyfin/Jellyfin AndroidTV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-androidtv/de/
2020-01-23 13:24:34 -05:00
Libor Filípek
774bff2b49 Translated using Weblate (Czech)
Currently translated at 100.0% (342 of 342 strings)

Translation: Jellyfin/Jellyfin AndroidTV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-androidtv/cs/
2020-01-23 13:24:34 -05:00
spagnoloG
4d34a30dbf Added translation using Weblate (Slovenian) 2020-01-23 13:24:17 -05:00
shoeflydbm
1320da0797 Translated using Weblate (Norwegian Bokmål)
Currently translated at 100.0% (342 of 342 strings)

Translation: Jellyfin/Jellyfin AndroidTV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-androidtv/nb_NO/
2020-01-23 13:24:17 -05:00
shoeflydbm
b171bdd7ea Translated using Weblate (Norwegian Bokmål)
Currently translated at 100.0% (342 of 342 strings)

Translation: Jellyfin/Jellyfin AndroidTV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-androidtv/nb_NO/
2020-01-23 13:24:17 -05:00
Rotekoppen
c1be6543ab Translated using Weblate (Norwegian Bokmål)
Currently translated at 100.0% (342 of 342 strings)

Translation: Jellyfin/Jellyfin AndroidTV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-androidtv/nb_NO/
2020-01-23 13:24:17 -05:00
pcmester
473905239d Translated using Weblate (Hungarian)
Currently translated at 5.3% (18 of 342 strings)

Translation: Jellyfin/Jellyfin AndroidTV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-androidtv/hu/
2020-01-23 13:24:17 -05:00
Travis Longstocking
20af2f4b8e Translated using Weblate (Spanish)
Currently translated at 97.1% (332 of 342 strings)

Translation: Jellyfin/Jellyfin AndroidTV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-androidtv/es/
2020-01-23 13:24:17 -05:00
Anders Bengtsson
e8c7809b1e Translated using Weblate (Swedish)
Currently translated at 3.8% (13 of 342 strings)

Translation: Jellyfin/Jellyfin AndroidTV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-androidtv/sv/
2020-01-23 13:24:17 -05:00
damnitall
26f96ee332 Translated using Weblate (Portuguese (Portugal))
Currently translated at 73.7% (252 of 342 strings)

Translation: Jellyfin/Jellyfin AndroidTV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-androidtv/pt_PT/
2020-01-23 13:24:17 -05:00
pcmester
dc6d2d9c53 Added translation using Weblate (Hungarian) 2020-01-23 13:24:17 -05:00
DenisDehaty
5e1925c7f1 Translated using Weblate (Slovak)
Currently translated at 9.1% (31 of 342 strings)

Translation: Jellyfin/Jellyfin AndroidTV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-androidtv/sk/
2020-01-23 13:24:17 -05:00
jfin
bf57210edd Translated using Weblate (Portuguese (Portugal))
Currently translated at 68.4% (234 of 342 strings)

Translation: Jellyfin/Jellyfin AndroidTV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-androidtv/pt_PT/
2020-01-23 13:24:17 -05:00
jfin
2f7fed9df6 Translated using Weblate (Portuguese (Brazil))
Currently translated at 98.5% (337 of 342 strings)

Translation: Jellyfin/Jellyfin AndroidTV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-androidtv/pt_BR/
2020-01-23 13:23:59 -05:00
vimo
ce1b6a2228 Translated using Weblate (Dutch)
Currently translated at 100.0% (342 of 342 strings)

Translation: Jellyfin/Jellyfin AndroidTV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-androidtv/nl/
2020-01-23 13:23:59 -05:00
David
9739898bd1 Translated using Weblate (German)
Currently translated at 100.0% (342 of 342 strings)

Translation: Jellyfin/Jellyfin AndroidTV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-androidtv/de/
2020-01-23 13:23:42 -05:00
Daniel De Jesus
3c20fd688d Translated using Weblate (Spanish (Dominican Republic))
Currently translated at 3.2% (11 of 342 strings)

Translation: Jellyfin/Jellyfin AndroidTV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-androidtv/es_DO/
2020-01-23 13:23:42 -05:00
Allan
e9ba3131db Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (342 of 342 strings)

Translation: Jellyfin/Jellyfin AndroidTV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-androidtv/zh_Hans/
2020-01-23 13:23:42 -05:00
David
b93552ecfb Translated using Weblate (German)
Currently translated at 100.0% (342 of 342 strings)

Translation: Jellyfin/Jellyfin AndroidTV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-androidtv/de/
2020-01-23 13:23:42 -05:00
m0d3rnX
7e52bb48b6 Translated using Weblate (German)
Currently translated at 100.0% (342 of 342 strings)

Translation: Jellyfin/Jellyfin AndroidTV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-androidtv/de/
2020-01-23 13:23:42 -05:00
4d1m
193d526583 Translated using Weblate (Romanian)
Currently translated at 39.5% (135 of 342 strings)

Translation: Jellyfin/Jellyfin AndroidTV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-androidtv/ro/
2020-01-23 13:23:10 -05:00
tanto faz
c0100c1c3c Translated using Weblate (Portuguese (Brazil))
Currently translated at 97.7% (334 of 342 strings)

Translation: Jellyfin/Jellyfin AndroidTV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-androidtv/pt_BR/
2020-01-23 13:23:10 -05:00
Davi Carlos de Lira Ferreira
0ffbed66fc Translated using Weblate (Portuguese (Brazil))
Currently translated at 97.7% (334 of 342 strings)

Translation: Jellyfin/Jellyfin AndroidTV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-androidtv/pt_BR/
2020-01-23 13:23:10 -05:00
Odd Stråbø
8665c4a39e Translated using Weblate (Norwegian Bokmål)
Currently translated at 99.4% (340 of 342 strings)

Translation: Jellyfin/Jellyfin AndroidTV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-androidtv/nb_NO/
2020-01-23 13:23:10 -05:00
ridaso
e4db812b82 Translated using Weblate (French)
Currently translated at 99.7% (341 of 342 strings)

Translation: Jellyfin/Jellyfin AndroidTV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-androidtv/fr/
2020-01-23 13:23:10 -05:00
Daniel De Jesus
90d02fa0ed Translated using Weblate (Spanish)
Currently translated at 92.4% (316 of 342 strings)

Translation: Jellyfin/Jellyfin AndroidTV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-androidtv/es/
2020-01-23 13:23:10 -05:00
Marcos Sanmartín
940c15511c Translated using Weblate (Spanish)
Currently translated at 92.4% (316 of 342 strings)

Translation: Jellyfin/Jellyfin AndroidTV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-androidtv/es/
2020-01-23 13:22:48 -05:00
Daniel De Jesus
78a595ff4c Added translation using Weblate (Spanish (Dominican Republic)) 2020-01-23 13:21:13 -05:00
Niels van Velzen
8a2edbcc0a Merge pull request #272 from thornbill/fix-release
Fix invalid id reference causing lintVitalRelease to fail
2020-01-23 18:57:32 +01:00
Bill Thornton
821a53f016 Fix invalid id reference causing lintVitalRelease to fail 2020-01-23 12:21:00 -05:00
Bill Thornton
46c8d8dcee Merge pull request #271 from thornbill/docker-build
Fix docker build
2020-01-23 10:34:52 -05:00
Bill Thornton
16d55baeb9 Fix docker build 2020-01-23 09:54:19 -05:00
Niels van Velzen
52eec32518 Merge pull request #268 from thornbill/add-live-tv-buttons
Add HomeFragmentRow for live tv buttons
2020-01-23 09:41:20 +01:00
Bill Thornton
055893eb2b Remove unneeded variable definitions 2020-01-23 00:30:53 -05:00
Bill Thornton
93302c34f7 Update null safety in HomeFragmentLiveTVRow 2020-01-22 23:56:19 -05:00
Bill Thornton
1f7f486732 Update comments 2020-01-21 14:39:17 -05:00
Bill Thornton
97ad3ab320 Add HomeFragmentRow for live tv buttons 2020-01-21 14:36:14 -05:00
Bill Thornton
3abc2a0d2c Move home fragments to ui package 2020-01-21 10:57:58 -05:00
Niels van Velzen
9ec29a6f15 Merge pull request #267 from Froghut/search_enhancements
Search enhancements
2020-01-20 20:42:34 +01:00
Froghut
6d46bc1084 fix naming 2020-01-20 19:47:35 +01:00
Froghut
65708a5fc5 cleanup 2020-01-19 19:19:36 +01:00
Froghut
2588e7f1f8 make categories in search always show in the same order and split Series + Episodes 2020-01-19 19:17:59 +01:00
Bill Thornton
21453982a3 Merge pull request #266 from nielsvanvelzen/readme
Add downloads to README
2020-01-18 17:00:20 -05:00
Niels van Velzen
8a8831ef84 Add downloads to README
Also adds additional links and some text about translations (copied from web repo)
2020-01-18 13:17:11 +01:00
Bill Thornton
c02b65a5e9 Merge pull request #263 from thornbill/channel-manager-fix
Fix ChannelManager crash on older API levels
2020-01-16 12:40:18 -05:00
Bill Thornton
e5771f4c95 Fix ChannelManager crash on older API levels 2020-01-16 11:07:57 -05:00
Bill Thornton
bf79e8c337 Merge pull request #244 from nielsvanvelzen/exoplayer2
Upgrade to ExoPlayer 2
2020-01-08 15:42:11 -05:00
Bill Thornton
ba3a95fc3d Merge pull request #249 from nielsvanvelzen/player-preferrence
Add option to choose the preferred media player in settings
2020-01-08 15:34:09 -05:00
Niels van Velzen
35c0801f0d Set default value for useVlc to make sure it is always set according to preferences.
Also removes the forced-enabling of VLC in the error handler for requesting stream info
2020-01-07 22:30:11 +01:00
Niels van Velzen
91e7252025 Make sure to use VLC when preferred, even when transcoding 2020-01-07 22:23:33 +01:00
Bill Thornton
2cd9d9e6d3 Merge pull request #232 from nielsvanvelzen/kotlin-channels
Add new and unfinished episodes to "watch next" section on Android TV
2020-01-07 13:33:17 -05:00
Niels van Velzen
7ae58475e5 Merge branch 'master' into player-preferrence
# Conflicts:
#	app/src/main/java/org/jellyfin/androidtv/TvApp.java
2020-01-07 19:26:22 +01:00
Niels van Velzen
87043fcaf2 Merge branch 'master' into kotlin-channels
# Conflicts:
#	app/build.gradle
2020-01-07 19:10:35 +01:00
Bill Thornton
3738563a73 Merge pull request #239 from nielsvanvelzen/search
Refactor search
2020-01-06 13:24:05 -05:00
Niels van Velzen
859f8d107b Properly release exoplayer. fixes potential memory issue 2020-01-04 15:06:13 +01:00
Niels van Velzen
c411f58c3d Remove old strings from other languages as well 2020-01-03 20:06:25 +01:00
Niels van Velzen
1e275345d4 Merge branch 'master' into player-preferrence
# Conflicts:
#	app/src/main/res/values/strings.xml
2020-01-03 20:04:34 +01:00
Niels van Velzen
c2cc1afac1 Merge branch 'master' into search
# Conflicts:
#	app/build.gradle
2020-01-03 20:01:28 +01:00
Niels van Velzen
f5c87878f6 Fix bad import after merge 2020-01-03 20:00:08 +01:00
Niels van Velzen
35021068c2 Merge branch 'master' into kotlin-channels
# Conflicts:
#	app/src/main/java/org/jellyfin/androidtv/browsing/HomeFragment.java
2020-01-03 19:58:21 +01:00
Niels van Velzen
96c0dc0f87 Remove recommendation code (was unused and not working) 2020-01-03 19:55:50 +01:00
Niels van Velzen
1ca6abdd13 Remove build flavors
Uses the amazon fork for exoplayer for all builds. This removes the need for a separate flavor for google/amazon devices 🎉
2020-01-03 19:36:28 +01:00
Niels van Velzen
5049e7d09f Add missing click listener to text search fragment 2020-01-03 19:27:14 +01:00
Bill Thornton
911f00f8b2 Merge pull request #246 from Florianisme/play-button-focus
Focus resume or otherwise play button by default
2020-01-02 14:59:54 -05:00
Bill Thornton
fbfaa3db2e Merge pull request #210 from nielsvanvelzen/homesections
Add home sections
2020-01-02 14:52:42 -05:00
Bill Thornton
dc1e41a510 Add excluded collection types to latest media home section 2020-01-02 14:42:03 -05:00
Niels van Velzen
290ba7bbb4 Fix exception logging 2020-01-02 14:42:03 -05:00
Niels van Velzen
fa9c5621e4 Always go to user selection screen on "logout" 2020-01-02 14:42:03 -05:00
Niels van Velzen
9d7bb0f232 Fix async loading of user views to fix row ordering 2020-01-02 14:41:17 -05:00
Niels van Velzen
722d766c28 Group items 2020-01-02 14:39:30 -05:00
Niels van Velzen
cd26e12bdc Show user preferred libraries in separate rows as "latest" (wip, async) 2020-01-02 14:39:30 -05:00
Niels van Velzen
45caec1460 Fix signout when auto login is off 2020-01-02 14:39:30 -05:00
Niels van Velzen
9755caa71e Fix minimum SDK issue (getContext() was not supported) 2020-01-02 14:39:30 -05:00
Niels van Velzen
5a16dbc77b Add home sections 2020-01-02 14:39:30 -05:00
Niels van Velzen
3897ebbb55 Remove strings for external player option 2020-01-02 19:14:08 +01:00
Niels van Velzen
e36dd6c968 Add option to choose the preferred media player in settings 2020-01-02 19:13:00 +01:00
Bill Thornton
5c4f213383 Increase max jvm heap size 2020-01-02 12:25:59 -05:00
Niels van Velzen
bf33dc60bd Remove showSearchWithPermission method 2020-01-02 17:50:52 +01:00
Niels van Velzen
038caab680 Disable forced speech disabled activity 2020-01-02 17:50:28 +01:00
Florianisme
344130ca7e Focus resume or otherwise play button by default 2020-01-02 13:52:21 +01:00
Niels van Velzen
1704e3df3d Fix channels updating twice on app start 2020-01-02 00:19:10 -05:00
Niels van Velzen
8ca73dd713 Add support for Android Leanback "Watch Next" feature 2020-01-02 00:19:09 -05:00
Bill Thornton
93a23c69bb Merge pull request #231 from nielsvanvelzen/kotlin
Enable Kotlin support
2020-01-02 00:12:09 -05:00
Joshua M. Boniface
cad39f36cc Merge pull request #228 from nielsvanvelzen/build-file
Rename "build" file to "build.sh"
2020-01-01 23:32:14 -05:00
dkanada
ece9324d32 Merge pull request #243 from thornbill/fix-card-margin
Fix top margin on card title
2020-01-02 13:03:05 +09:00
Niels van Velzen
3c7d9c86a9 Single event listener, fix setZoom, disable controls in XML, fix play after pause 2020-01-01 16:20:50 +01:00
Niels van Velzen
cb27fc6309 Fix audio progress not working 2020-01-01 15:56:53 +01:00
Niels van Velzen
b9cec5294e Update VideoManager to exoplayer 2 2020-01-01 15:34:26 +01:00
Niels van Velzen
1b30d78b1a Use ExoPlayer 2 in MediaManager (audio player) 2020-01-01 15:20:37 +01:00
Niels van Velzen
6771bd2d14 Remove exomedia and upgrade exoplayer to the latest amazon revision 2020-01-01 14:45:34 +01:00
Niels van Velzen
229990b50b Add separate search fragment for speech incapable devices (like Fire TV) 2020-01-01 10:53:48 +01:00
Niels van Velzen
de2081bec2 Use more simple approach for disabling speech 2019-12-31 12:04:31 +01:00
Niels van Velzen
900032efa6 Separate SearchFragment and SearchProvider 2019-12-31 11:30:59 +01:00
Niels van Velzen
71e434517b Disable speech when not supported 2019-12-31 11:24:17 +01:00
Bill Thornton
7e4b454a42 Fix top margin on card title 2019-12-31 02:09:46 -05:00
Bill Thornton
6a91c08b55 Merge pull request #237 from nielsvanvelzen/readable-titles
Make title in cards readable
2019-12-31 01:12:56 -05:00
Niels van Velzen
2c2e738222 Remove no-speech search xml files 2019-12-29 18:38:55 +01:00
Niels van Velzen
91bef5afc6 Refactor SearchFragment 2019-12-29 18:07:09 +01:00
Niels van Velzen
ec83261b43 Remove MySearchFragmentNoSpeech and custom speech permission logic (is now handled by androidx SearchSupportFragment) 2019-12-29 17:43:09 +01:00
Niels van Velzen
a352a32bab Make title in cards scroll and increase the element width 2019-12-29 17:24:13 +01:00
Bill Thornton
25edae1563 Merge pull request #217 from AndreasGB/typestring-to-enum
Replace string usages for BaseItemDto.Type with the EBaseItemType
2019-12-24 08:08:31 -05:00
Niels van Velzen
8efe4d23de Switch to kotlin stdlib for jdk 8 2019-12-24 11:19:04 +01:00
Niels van Velzen
cc767ef1cb Fix crash because the saved instance state can be null 2019-12-24 11:15:21 +01:00
Niels van Velzen
4eb2b9db7a Add .editorconfig for Kotlin files 2019-12-24 11:15:20 +01:00
Niels van Velzen
54d9cba938 Convert MainActivity to kotlin fail to demonstrate Kotlin support 2019-12-24 11:15:20 +01:00
Niels van Velzen
9bfb1c7a8e Rename "build" file to "build.sh" because Gradle needs a "build" directory in the project root 2019-12-24 11:15:19 +01:00
Niels van Velzen
3566785030 Add Kotlin support 2019-12-24 11:15:19 +01:00
Bill Thornton
e23fb21fd2 Merge pull request #200 from thornbill/acra
Add ACRA for crash reporting
2019-12-23 19:07:30 -05:00
Bill Thornton
a5a1b8c9d7 Merge pull request #234 from nielsvanvelzen/remove-unused-resources
Remove unused resources
2019-12-23 17:11:12 -05:00
Niels van Velzen
99b164e240 Remove lbl_select_color from strings.xml (string is removed) 2019-12-23 22:13:11 +01:00
Niels van Velzen
6fff8ff642 Restore legacy theme 2019-12-23 22:07:11 +01:00
Niels van Velzen
d0490d318a Manually remove additional file (integers.xml) and comment in themes.xml for removed legacy theme 2019-12-23 22:07:10 +01:00
Niels van Velzen
35823fb11c Remove unused drawables, layouts, strings, arrays and ids 2019-12-23 22:07:10 +01:00
Bill Thornton
b42285f838 Merge pull request #233 from nielsvanvelzen/deprecation-fixes
Deprecation fixes
2019-12-23 15:06:50 -05:00
Niels van Velzen
d65843fb6d Convert remaining activities to FragmentActivity 2019-12-23 19:20:20 +01:00
ridaso
4cf20c4ede Translated using Weblate (French)
Currently translated at 99.7% (341 of 342 strings)

Translation: Jellyfin/Jellyfin AndroidTV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-androidtv/fr/
2019-12-23 13:14:38 -05:00
Andreas Ackermann
d0272cc3fb Undo version bump, point at apiclient library master-SNAPSHOT instead 2019-12-23 11:57:27 -05:00
Andreas Ackermann
29f127e226 Remove E-prefix from EBaseItemType enum 2019-12-23 11:57:26 -05:00
Andreas Ackermann
0d820ebb2c Replace string usages for BaseItemDto.Type with the EBaseItemType
Also fixing dependency substitution now that we use explicit versions
2019-12-23 11:55:09 -05:00
Lars Sørensen
006f4390a7 Translated using Weblate (Danish)
Currently translated at 49.4% (169 of 342 strings)

Translation: Jellyfin/Jellyfin AndroidTV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-androidtv/da/
2019-12-22 09:16:25 -05:00
ridaso
05bebbe67b Translated using Weblate (French)
Currently translated at 99.7% (341 of 342 strings)

Translation: Jellyfin/Jellyfin AndroidTV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-androidtv/fr/
2019-12-21 03:16:18 -05:00
Niels van Velzen
354ef42fcb Replace empty BitMapDrawable with transparent ColorDrawable 2019-12-20 22:55:41 +01:00
Niels van Velzen
bace7f8b91 Fix RecommendationBuilder not using a notification channel 2019-12-20 22:41:27 +01:00
Niels van Velzen
97644e2479 Remove background manager dim layers (androidx does not implement it) 2019-12-20 22:37:30 +01:00
Niels van Velzen
19f3088e62 Update deprecated methods in HorizontalGridPresenter 2019-12-20 22:31:59 +01:00
Niels van Velzen
3a1ca78182 Move to AndroidX fragments 2 2019-12-20 22:31:30 +01:00
Niels van Velzen
0a1d3dbf76 Move to AndroidX fragments
Uses a FragmentActivity for the MainActivity and updates RowsFragment to RowsSupportFragment
2019-12-20 22:23:45 +01:00
ridaso
102a93361c Translated using Weblate (French)
Currently translated at 99.7% (341 of 342 strings)

Translation: Jellyfin/Jellyfin AndroidTV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-androidtv/fr/
2019-12-20 01:16:19 -05:00
tanto faz
27ea77eac7 Translated using Weblate (Portuguese (Brazil))
Currently translated at 97.1% (332 of 342 strings)

Translation: Jellyfin/Jellyfin AndroidTV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-androidtv/pt_BR/
2019-12-14 11:39:55 -05:00
4everfun
e19c50fe3d Translated using Weblate (Dutch)
Currently translated at 95.3% (326 of 342 strings)

Translation: Jellyfin/Jellyfin AndroidTV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-androidtv/nl/
2019-12-14 11:39:55 -05:00
David
18dedd1c77 Translated using Weblate (German)
Currently translated at 100.0% (342 of 342 strings)

Translation: Jellyfin/Jellyfin AndroidTV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-androidtv/de/
2019-12-14 11:39:55 -05:00
Bill Thornton
edc83a77fd Merge pull request #229 from AndreasGB/delete-empty-japanese-strings
Delete empty strings introduced by weblate from translations
2019-12-14 11:27:50 -05:00
Andreas Ackermann
41b91145d6 Delete empty strings from Indonesian translations 2019-12-14 12:48:23 +01:00
Andreas Ackermann
c77e84bbe6 Delete empty strings from Korean translations 2019-12-14 12:42:28 +01:00
Andreas Ackermann
beb8e821ae Delete empty strings from Mongolian translations 2019-12-14 12:35:20 +01:00
Andreas Ackermann
3011b7980e Delete empty strings from Slovak translations 2019-12-14 12:32:03 +01:00
Andreas Ackermann
8779286066 Delete empty strings from japanese translation
This is supposed to fix the issue that empty strings were displayed
in the japanese version instead of the english original if the string
was untranslated.
2019-12-14 11:51:35 +01:00
Niels van Velzen
c5e36251a4 Rename "build" file to "build.sh" because Gradle needs a "build" directory in the project root 2019-12-14 11:40:54 +01:00
Anthony Lavado
d68b43d328 Merge pull request #226 from thornbill/azure-triggers
Add release branch to CI triggers
2019-12-12 13:24:24 -05:00
Bill Thornton
7f2275a11a Add release branch to CI triggers 2019-12-12 13:15:30 -05:00
Federico Antoniazzi
51621715f9 Translated using Weblate (Italian)
Currently translated at 52.6% (180 of 342 strings)

Translation: Jellyfin/Jellyfin AndroidTV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-androidtv/it/
2019-12-12 07:16:08 -05:00
mpblabs
341efc62d2 Translated using Weblate (German)
Currently translated at 99.4% (340 of 342 strings)

Translation: Jellyfin/Jellyfin AndroidTV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-androidtv/de/
2019-12-12 07:16:06 -05:00
Bill Thornton
afa1f918ee Set fixed apiclient version 2019-12-11 00:06:38 -05:00
Bill Thornton
82f5dafb5d Fix unescaped apostrophe 2019-12-10 23:36:45 -05:00
Bill Thornton
2a3ed44fd5 Enable ACRA dialog by default and update text 2019-12-10 09:13:21 -05:00
tanto faz
922d413cd6 Translated using Weblate (Portuguese (Brazil))
Currently translated at 97.1% (332 of 342 strings)

Translation: Jellyfin/Jellyfin AndroidTV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-androidtv/pt_BR/
2019-12-10 08:16:13 -05:00
Matzi24GR
0134b0d792 Translated using Weblate (Greek)
Currently translated at 30.7% (105 of 342 strings)

Translation: Jellyfin/Jellyfin AndroidTV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-androidtv/el/
2019-12-10 08:16:13 -05:00
Bill Thornton
ee0cbf4c10 Merge pull request #225 from nielsvanvelzen/html-descriptions
Support simple HTML elements in descriptions
2019-12-08 14:40:45 -05:00
Niels van Velzen
6b1a59224f Support simple HTML elements in descriptions 2019-12-08 11:06:58 +01:00
kmnb
29d926746e Added translation using Weblate (Indonesian) 2019-12-07 02:33:27 -05:00
Tommy Larsson
120c77ade6 Translated using Weblate (Swedish)
Currently translated at 2.9% (10 of 342 strings)

Translation: Jellyfin/Jellyfin AndroidTV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-androidtv/sv/
2019-12-07 02:32:44 -05:00
tanto faz
7f1d98ecc8 Translated using Weblate (Portuguese (Brazil))
Currently translated at 93.6% (320 of 342 strings)

Translation: Jellyfin/Jellyfin AndroidTV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-androidtv/pt_BR/
2019-12-07 02:32:44 -05:00
WWWesten
e7c08331e1 Translated using Weblate (Kazakh)
Currently translated at 100.0% (342 of 342 strings)

Translation: Jellyfin/Jellyfin AndroidTV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-androidtv/kk/
2019-12-07 02:32:44 -05:00
mirfire
b29f0fd912 Translated using Weblate (French)
Currently translated at 99.7% (341 of 342 strings)

Translation: Jellyfin/Jellyfin AndroidTV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-androidtv/fr/
2019-12-07 02:32:44 -05:00
Søren R K
a36b7c4cb4 Translated using Weblate (Danish)
Currently translated at 44.4% (152 of 342 strings)

Translation: Jellyfin/Jellyfin AndroidTV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-androidtv/da/
2019-12-07 02:32:44 -05:00
Adwin Ying
a815ea0319 Translated using Weblate (Japanese)
Currently translated at 74.6% (255 of 342 strings)

Translation: Jellyfin/Jellyfin AndroidTV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-androidtv/ja/
2019-12-07 02:32:44 -05:00
Bill Thornton
4cfd5383d9 Add AcraLimiter 2019-12-05 12:38:59 -05:00
Bill Thornton
d3458164d6 Remove custom error handling 2019-12-05 12:21:23 -05:00
Bill Thornton
72ed94447a Add ACRA for crash reporting 2019-12-05 12:19:12 -05:00
Bill Thornton
fbd601213f Merge pull request #222 from joern-h/fix-audio-queue-reload-on-shuffle
Fix audio queue reload
2019-12-04 10:55:10 -06:00
dkanada
becb6295a6 Merge pull request #219 from joern-h/fix-musicartist-playall
Fixed Play on MusicArtists
2019-12-05 01:38:53 +09:00
WWWesten
5c40e50351 Translated using Weblate (Kazakh)
Currently translated at 81.6% (279 of 342 strings)

Translation: Jellyfin/Jellyfin AndroidTV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-androidtv/kk/
2019-12-03 16:15:58 -05:00
dkanada
99035900ff Merge pull request #221 from dcyrille18/beyontv-hevc-support
Add HEVC support for BeyondTV
2019-12-04 00:17:33 +09:00
dkanada
76512e3fc6 Merge pull request #218 from joern-h/fix-audio-playlists
Fixed playlist query
2019-12-04 00:17:23 +09:00
dkanada
c54ef94e03 Merge pull request #214 from nielsvanvelzen/encode-ass-subtitles
Force SSA/ASS subtitles to be encoded when not using VLC
2019-12-04 00:17:13 +09:00
dkanada
ac24a6a09e Merge pull request #213 from nielsvanvelzen/switchSubtitleStream-loop
Fix switching to encoded subtitles causing the video player to hang.
2019-12-04 00:17:02 +09:00
Jörn
2b80bac9f1 Fix audio queue reload
'MediaManager.shuffleAudioQueue()' returns while the dialog to replace or
add the queue is showing. This causes the reload of the current queue to
fire before the queue has changed.
I implemented a 'onQueueReplaced' event to fix this.
2019-12-03 10:28:49 +01:00
Jörn
19f280c294 Fix Shuffle-All on Playlists 2019-12-03 09:08:37 +01:00
Niels van Velzen
dc203ec04b Prevent calling the switchSubtitleStream function if the selected subtitle index didn't change when starting playback. 2019-12-02 21:38:02 +01:00
KingsFourze
c8fa914ec7 Translated using Weblate (Chinese (Traditional))
Currently translated at 30.1% (103 of 342 strings)

Translation: Jellyfin/Jellyfin AndroidTV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-androidtv/zh_Hant/
2019-12-02 01:15:55 -05:00
Pan Renzhou
8cf33c16f4 Translated using Weblate (Chinese (Simplified))
Currently translated at 98.8% (338 of 342 strings)

Translation: Jellyfin/Jellyfin AndroidTV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-androidtv/zh_Hans/
2019-12-02 01:15:49 -05:00
Ales Komarek
6d09df1de7 Translated using Weblate (Czech)
Currently translated at 98.8% (338 of 342 strings)

Translation: Jellyfin/Jellyfin AndroidTV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-androidtv/cs/
2019-12-02 01:15:48 -05:00
Cyrille DROMAS
a7eedf25db Add HEVC support for BeyondTV 2019-12-01 17:42:41 +01:00
Jörn
bc2d44c6bb Fixed Play on MusicArtists 2019-11-29 22:39:27 +01:00
TheGoose
3eee4c1db4 Translated using Weblate (English)
Currently translated at 100.0% (342 of 342 strings)

Translation: Jellyfin/Jellyfin AndroidTV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-androidtv/en/
2019-11-29 16:15:42 -05:00
Jörn
9e31e52794 Fixed playlist query 2019-11-29 21:46:16 +01:00
踢低吸
49791c3ea3 Translated using Weblate (Chinese (Traditional))
Currently translated at 14.6% (50 of 342 strings)

Translation: Jellyfin/Jellyfin AndroidTV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-androidtv/zh_Hant/
2019-11-28 13:48:34 -05:00
TheGoose
c421c2a78f Translated using Weblate (English (United Kingdom))
Currently translated at 100.0% (342 of 342 strings)

Translation: Jellyfin/Jellyfin AndroidTV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-androidtv/en_GB/
2019-11-28 13:48:34 -05:00
踢低吸
d90e8e3d29 Translated using Weblate (Chinese (Traditional))
Currently translated at 10.8% (37 of 342 strings)

Translation: Jellyfin/Jellyfin AndroidTV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-androidtv/zh_Hant/
2019-11-28 05:48:31 -05:00
penguinfuko
cc792c93e4 Translated using Weblate (Chinese (Traditional))
Currently translated at 10.8% (37 of 342 strings)

Translation: Jellyfin/Jellyfin AndroidTV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-androidtv/zh_Hant/
2019-11-28 05:48:30 -05:00
Niels van Velzen
4ef5fe8f69 Force SSA/ASS subtitles to be encoded when not using VLC
VLC can render them fine but the default player is unable to render ASS/SSA subtitles that use custom fonts, drawing or other special features. Forcing encoding on those font types fixes this issue.
2019-11-27 22:15:06 +01:00
Niels van Velzen
0dd2a93b21 Fix switching to encoded subtitles causing the video player to hang.
The .switchSubtitleStream() function calls the stop & play functions which would then call the switch function and this ended up looping. Adding an additional check if the to be switched to stream isn't the current already prevents this.
2019-11-27 22:06:47 +01:00
WWWesten
1c7052e5c2 Translated using Weblate (Russian)
Currently translated at 100.0% (342 of 342 strings)

Translation: Jellyfin/Jellyfin AndroidTV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-androidtv/ru/
2019-11-27 13:15:47 -05:00
jeannin matthieu
e7fb835d89 Translated using Weblate (French)
Currently translated at 98.8% (338 of 342 strings)

Translation: Jellyfin/Jellyfin AndroidTV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-androidtv/fr/
2019-11-27 13:15:47 -05:00
Thomas Schwery
0a4b452870 Translated using Weblate (French)
Currently translated at 98.8% (338 of 342 strings)

Translation: Jellyfin/Jellyfin AndroidTV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-androidtv/fr/
2019-11-27 13:15:47 -05:00
Bill Thornton
7cc84de9c0 Merge pull request #212 from jellyfin/dkanada-patch-1
Use new version for apiclient
2019-11-27 09:32:08 -08:00
dkanada
7ac16901e2 use new version for apiclient 2019-11-28 02:22:58 +09:00
Leo Verto
1ce6d1f0f8 Translated using Weblate (German)
Currently translated at 95.6% (327 of 342 strings)

Translation: Jellyfin/Jellyfin AndroidTV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-androidtv/de/
2019-11-25 22:15:38 -05:00
Anthony Lavado
cb0d241ef1 Merge pull request #207 from AndreasGB/gradle-update
Upgrading Gradle Version to 5.4.1
2019-11-25 15:44:19 -05:00
Bill Thornton
e43901c75d Merge pull request #209 from nielsvanvelzen/fix-authorities
Use ${applicationId} in authorities so the ".debug" prefix is applied while developing
2019-11-24 07:02:13 -08:00
Ivan Poletsky
c08fdd3a69 Translated using Weblate (Russian)
Currently translated at 95.6% (327 of 342 strings)

Translation: Jellyfin/Jellyfin AndroidTV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-androidtv/ru/
2019-11-24 08:15:39 -05:00
muldermatthijs
c00d610581 Translated using Weblate (Dutch)
Currently translated at 88.3% (302 of 342 strings)

Translation: Jellyfin/Jellyfin AndroidTV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-androidtv/nl/
2019-11-24 08:15:38 -05:00
Christian Rodríguez Martínez
55a00fdf52 Translated using Weblate (Spanish)
Currently translated at 91.5% (313 of 342 strings)

Translation: Jellyfin/Jellyfin AndroidTV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-androidtv/es/
2019-11-24 08:15:34 -05:00
Niels van Velzen
1295dc604b Use ${applicationId} in authorities so the ".debug" prefix is applied while developing.
Fixes issue where debugging is not possible on a device with the stable Jellyfin release installed.
2019-11-24 12:08:41 +01:00
Andreas Ackermann
0fe28c1133 Conditional composite build with apiclient 2019-11-22 18:44:38 +01:00
zhuzhe1983
b35027525e Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (329 of 329 strings)

Translation: Jellyfin/Jellyfin AndroidTV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-androidtv/zh_Hans/
2019-11-22 04:15:36 -05:00
rokoh
906949476d Translated using Weblate (French)
Currently translated at 99.7% (328 of 329 strings)

Translation: Jellyfin/Jellyfin AndroidTV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-androidtv/fr/
2019-11-22 04:15:36 -05:00
Libor Filípek
f7fee75206 Translated using Weblate (Czech)
Currently translated at 100.0% (329 of 329 strings)

Translation: Jellyfin/Jellyfin AndroidTV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-androidtv/cs/
2019-11-22 04:15:36 -05:00
Andreas Ackermann
101bc5b043 Upgrading Gradle Version to 5.4.1
Also adding myself to CONTRIBUTORS.md now, as this ate way too much time :D
2019-11-20 22:05:02 +01:00
dkanada
7e222f77b4 Merge pull request #198 from thornbill/style-issues
Fix style issues and untranslatable text
2019-11-20 00:56:48 +09:00
dkanada
e89a33df92 Merge pull request #196 from thornbill/fix-search-permissions
Fix search with denied permissions
2019-11-20 00:56:13 +09:00
Bill Thornton
1523dcf803 Update color background label 2019-11-19 09:58:14 -05:00
Bill Thornton
4077af5fca Fix formatting 2019-11-19 00:20:14 -05:00
Thomas Schwery
3e9c76d4f9 Translated using Weblate (French)
Currently translated at 97.6% (320 of 328 strings)

Translation: Jellyfin/Jellyfin AndroidTV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-androidtv/fr/
2019-11-17 12:15:22 -05:00
Bill Thornton
643832c252 Fix style issues and untranslatable text 2019-11-16 02:43:05 -05:00
dkanada
54d5efba3e Merge pull request #203 from thornbill/fix-string-format
Fix string formatting
2019-11-16 16:40:52 +09:00
Bill Thornton
5480c0346d Fix string formatting 2019-11-16 02:18:44 -05:00
Bill Thornton
4c3db801d2 Merge pull request #202 from AndreasGB/more-playback-bitrates
Added more bitrate options
2019-11-16 01:53:53 -05:00
Bill Thornton
17a131648b Merge pull request #201 from AndreasGB/add-latest-tv-shows
Added Latest TV Shows to Home Fragment
2019-11-15 13:17:48 -05:00
swinkworld
814b232746 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (328 of 328 strings)

Translation: Jellyfin/Jellyfin AndroidTV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-androidtv/zh_Hans/
2019-11-15 13:15:26 -05:00
tluciomiranda
c4a7764808 Translated using Weblate (Portuguese (Portugal))
Currently translated at 72.9% (239 of 328 strings)

Translation: Jellyfin/Jellyfin AndroidTV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-androidtv/pt_PT/
2019-11-15 13:15:26 -05:00
Raphael Fuchter
4af56eb389 Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (328 of 328 strings)

Translation: Jellyfin/Jellyfin AndroidTV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-androidtv/pt_BR/
2019-11-15 13:15:26 -05:00
César Cerón Pérez
098fb29f4d Translated using Weblate (Spanish (Mexico))
Currently translated at 99.4% (326 of 328 strings)

Translation: Jellyfin/Jellyfin AndroidTV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-androidtv/es_MX/
2019-11-15 13:15:26 -05:00
Andreas
e028a49441 Translated using Weblate (German)
Currently translated at 99.4% (326 of 328 strings)

Translation: Jellyfin/Jellyfin AndroidTV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-androidtv/de/
2019-11-15 13:15:26 -05:00
Andreas Ackermann
9a367a2266 Privatized methods and implemented requested changes 2019-11-15 18:27:16 +01:00
Andreas Ackermann
0254645eb5 Added more bitrate options 2019-11-15 15:06:01 +01:00
Andreas Ackermann
bad5cc9a80 Added Latest TV Shows to Home Fragment 2019-11-15 12:08:03 +01:00
Bill Thornton
87fdf9d178 Fix search with denied permissions 2019-11-13 17:03:10 -05:00
Anthony Lavado
89122ecfad Merge pull request #195 from thornbill/fix-string-format
Fix string formatting
2019-11-13 17:01:20 -05:00
Bill Thornton
ad1cc403d2 Fix string formatting 2019-11-13 10:51:27 -05:00
Sander Lambrechts
ef46c9bd6f Translated using Weblate (Dutch)
Currently translated at 83.2% (273 of 328 strings)

Translation: Jellyfin/Jellyfin AndroidTV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-androidtv/nl/
2019-11-09 18:15:16 -05:00
DJSweder
081aa8ce16 Translated using Weblate (Czech)
Currently translated at 83.2% (273 of 328 strings)

Translation: Jellyfin/Jellyfin AndroidTV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-androidtv/cs/
2019-11-09 18:15:16 -05:00
dkanada
7607a7c315 Merge pull request #191 from jellyfin/dkanada-patch-2
New release
2019-11-10 02:02:27 +09:00
Budkhuu
4d92f5fc50 Translated using Weblate (Mongolian)
Currently translated at 65.1% (209 of 321 strings)

Translation: Jellyfin/Jellyfin AndroidTV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-androidtv/mn/
2019-11-08 01:15:10 -05:00
dkanada
08f5601449 new release 2019-11-08 15:14:36 +09:00
dkanada
18f1f69000 Merge pull request #190 from thornbill/theme-manager
Cleanup theme manager
2019-11-08 11:42:44 +09:00
Bill Thornton
511afe1c22 Cleanup theme manager 2019-11-07 14:12:39 -05:00
dkanada
25f732c72a Merge pull request #188 from thornbill/add-server-crash
Fix version check when no servers in response
2019-11-08 03:23:21 +09:00
dkanada
fc944cc77d Merge pull request #187 from thornbill/images
Fix incorrect image types and aspect ratios
2019-11-08 03:21:12 +09:00
dkanada
47fbd86278 Merge pull request #189 from thornbill/fix-delayed-message
Fix using DelayedMessage on not the main ui thread
2019-11-07 19:20:37 +09:00
Bill Thornton
b656935ce6 Fix using DelayedMessage on not the main ui thread 2019-11-07 01:17:29 -05:00
Budkhuu
49684b5eac Added translation using Weblate (Mongolian) 2019-11-06 23:31:39 -05:00
Bill Thornton
3b6689f2a3 Fix version check when no servers in response 2019-11-06 23:27:30 -05:00
Bill Thornton
613e9c4e57 Force aspect ratio for LiveTvProgram movies 2019-11-06 21:27:06 -05:00
Bill Thornton
cda5970488 Fix program image type 2019-11-06 19:35:15 -05:00
Bill Thornton
79669bfee1 Fix non-translated strings and formatting 2019-11-06 19:27:22 -05:00
Bill Thornton
6cddce7ada Force aspect ratio for CollectionFolder types 2019-11-06 19:12:19 -05:00
Bill Thornton
dab9bb1d8d Fix formatting issues 2019-11-06 19:12:19 -05:00
jeannin matthieu
a25e380476 Translated using Weblate (French)
Currently translated at 99.1% (318 of 321 strings)

Translation: Jellyfin/Jellyfin AndroidTV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-androidtv/fr/
2019-11-05 14:15:02 -05:00
156 changed files with 5608 additions and 3960 deletions

View File

@@ -1,5 +1,6 @@
trigger:
- master
- release
pool:
vmImage: 'ubuntu-latest'

8
.editorconfig Normal file
View File

@@ -0,0 +1,8 @@
[*]
end_of_line = lf
insert_final_newline = true
[{*.kts, *.kt}]
charset = utf-8
indent_style = tab
tab_width = 4

View File

@@ -6,6 +6,9 @@
- [grafixeyehero](https://github.com/grafixeyehero)
- [BnMcG](https://github.com/bnmcg)
- [Letroll](https://github.com/letroll)
- [AndreasGB](https://github.com/andreasgb)
- [joern-h](https://github.com/joern-h)
- [Niels van Velzen](https://github.com/nielsvanvelzen)
# Emby Contributors

View File

@@ -6,13 +6,45 @@
<p align="center">
<img alt="Logo banner" src="https://raw.githubusercontent.com/jellyfin/jellyfin-ux/master/branding/SVG/banner-logo-solid.svg?sanitize=true"/>
<br/><br/>
<a href="https://github.com/jellyfin/jellyfin-androidtv"><img alt="GPL 2.0 License" src="https://img.shields.io/github/license/jellyfin/jellyfin-androidtv.svg"/></a>
<a href="https://github.com/jellyfin/jellyfin-androidtv/releases"><img alt="Current Release" src="https://img.shields.io/github/release/jellyfin/jellyfin-androidtv.svg"/></a>
<a href="https://translate.jellyfin.org/projects/jellyfin/jellyfin-androidtv/?utm_source=widget"><img src="https://translate.jellyfin.org/widgets/jellyfin/-/jellyfin-androidtv/svg-badge.svg" alt="Translation status" /></a>
<a href="https://github.com/jellyfin/jellyfin-androidtv">
<img alt="GPL 2.0 License" src="https://img.shields.io/github/license/jellyfin/jellyfin-androidtv.svg"/>
</a>
<a href="https://github.com/jellyfin/jellyfin-androidtv/releases">
<img alt="Current Release" src="https://img.shields.io/github/release/jellyfin/jellyfin-androidtv.svg"/>
</a>
<a href="https://translate.jellyfin.org/projects/jellyfin/jellyfin-androidtv/?utm_source=widget">
<img src="https://translate.jellyfin.org/widgets/jellyfin/-/jellyfin-androidtv/svg-badge.svg" alt="Translation status" />
</a>
<br/>
<a href="https://opencollective.com/jellyfin">
<img alt="Donate" src="https://img.shields.io/opencollective/all/jellyfin.svg?label=backers"/>
</a>
<a href="https://features.jellyfin.org">
<img alt="Feature Requests" src="https://img.shields.io/badge/fider-vote%20on%20features-success.svg"/>
</a>
<a href="https://forum.jellyfin.org">
<img alt="Discuss on our Forum" src="https://img.shields.io/discourse/https/forum.jellyfin.org/users.svg"/>
</a>
<a href="https://matrix.to/#/+jellyfin:matrix.org">
<img alt="Chat on Matrix" src="https://img.shields.io/matrix/jellyfin:matrix.org.svg?logo=matrix"/>
</a>
<a href="https://www.reddit.com/r/jellyfin">
<img alt="Join our Subreddit" src="https://img.shields.io/badge/reddit-r%2Fjellyfin-%23FF5700.svg"/>
</a>
<br/>
<a href="https://play.google.com/store/apps/details?id=org.jellyfin.androidtv">
<img width="200" src="https://play.google.com/intl/en_us/badges/static/images/badges/en_badge_web_generic.png" alt="Get it on Google Play"/>
</a>
<br/>
<a href="https://www.amazon.com/gp/aw/d/B07TX7Z725">
<img width="170" src="https://images-na.ssl-images-amazon.com/images/G/01/mobile-apps/devportal2/res/images/amazon-appstore-badge-english-black.png" alt="Available at Amazon Appstore"/>
</a>
<br/>
<a href="https://repo.jellyfin.org/releases/client/androidtv/">Download archive</a>
</p>
---
Jellyfin Android TV is a Jellyfin client for Android TV, nVidia Shield, and Amazon Fire TV devices. We welcome all contributions and pull requests! If you have a larger feature in mind please open an issue so we can discuss the implementation before you start. Translations can be improved very easily from our <a href="https://translate.jellyfin.org/projects/jellyfin/jellyfin-androidtv">Weblate</a> instance. Look through the following graphic to see if your native language could use some work!
<p align="center">
Jellyfin Android TV is a Jellyfin client for Android TV, nVidia Shield, and Amazon Fire TV devices.
</p>
<a href="https://translate.jellyfin.org/engage/jellyfin/?utm_source=widget">
<img src="https://translate.jellyfin.org/widgets/jellyfin/-/jellyfin-androidtv/multi-auto.svg" alt="Detailed Translation Status"/>
</a>

View File

@@ -1,14 +1,21 @@
apply plugin: 'com.android.application'
def version = "0.10.1"
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
def version = "0.11.1"
android {
compileSdkVersion 28
buildToolsVersion "28.0.3"
compileOptions {
sourceCompatibility 1.8
targetCompatibility 1.8
}
defaultConfig {
minSdkVersion 21
targetSdkVersion 28
versionCode 903
versionCode 906
versionName version
}
@@ -22,25 +29,21 @@ android {
debuggable true
}
}
flavorDimensions "version"
productFlavors {
google {
dimension = "version"
versionName = version + "g"
}
amazon {
dimension = "version"
versionName = version + "a"
}
}
}
dependencies {
def acraVersion = '5.4.0'
implementation fileTree(include: ['*.jar'], dir: 'libs')
amazonImplementation 'com.github.jellyfin:exomedia:amazon~2.5.6-SNAPSHOT'
googleImplementation 'com.devbrackets.android:exomedia:2.5.6'
implementation 'com.github.jellyfin:jellyfin-apiclient-java:master-SNAPSHOT'
// This is split like this because this is a modular gradle project on jitpack
// see https://github.com/jitpack/gradle-modular
// The split allows dependency substitution to work in the settings.gradle file,
// at least for the 'library' part. 'android' part is still broken somehow, but this
// is better than nothing.
implementation 'com.github.jellyfin.jellyfin-apiclient-java:android:0.5.0'
implementation 'com.github.jellyfin.jellyfin-apiclient-java:library:0.5.0'
implementation 'com.amazon.android:exoplayer:2.10.6'
implementation 'androidx.recyclerview:recyclerview:1.0.0'
implementation 'androidx.leanback:leanback:1.0.0'
implementation 'androidx.appcompat:appcompat:1.0.2'
@@ -52,5 +55,17 @@ dependencies {
implementation 'com.google.guava:guava:18.0'
implementation 'com.flaviofaria:kenburnsview:1.0.6'
implementation 'org.videolan.android:libvlc-all:3.1.12'
implementation "ch.acra:acra-http:$acraVersion"
implementation "ch.acra:acra-dialog:$acraVersion"
implementation "ch.acra:acra-limiter:$acraVersion"
testImplementation 'junit:junit:4.12'
implementation "androidx.core:core-ktx:1.1.0"
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
implementation 'androidx.tvprovider:tvprovider:1.0.0'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.2'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.2'
}
repositories {
mavenCentral()
}

View File

@@ -11,6 +11,9 @@
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<!-- Needed for ChannelManager on older devices (< API 23) -->
<uses-permission android:name="com.android.providers.tv.permission.WRITE_EPG_DATA" />
<uses-feature
android:name="android.hardware.touchscreen"
android:required="false" />
@@ -81,13 +84,6 @@
</intent-filter>
</receiver>
<provider
android:authorities="org.jellyfin.androidtv.recommendations"
android:name=".integration.RecommendationContentProvider"
android:exported="true"
android:grantUriPermissions="true"
tools:ignore="ExportedContentProvider" />
<activity
android:name=".details.ExpandedTextActivity"
android:label="@string/title_activity_expanded_text" />

View File

@@ -1,42 +1,35 @@
package org.jellyfin.androidtv;
import android.Manifest;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.Application;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.graphics.Bitmap;
import android.graphics.Typeface;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.GradientDrawable;
import android.media.AudioManager;
import android.os.Build;
import android.preference.PreferenceManager;
import androidx.annotation.NonNull;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import androidx.palette.graphics.Palette;
import org.acra.ACRA;
import org.acra.annotation.AcraCore;
import org.acra.annotation.AcraDialog;
import org.acra.annotation.AcraHttpSender;
import org.acra.annotation.AcraLimiter;
import org.acra.sender.HttpSender;
import org.jellyfin.androidtv.base.BaseActivity;
import org.jellyfin.androidtv.livetv.TvManager;
import org.jellyfin.androidtv.model.DisplayPriorityType;
import org.jellyfin.androidtv.model.LogonCredentials;
import org.jellyfin.androidtv.playback.ExternalPlayerActivity;
import org.jellyfin.androidtv.playback.MediaManager;
import org.jellyfin.androidtv.playback.PlaybackController;
import org.jellyfin.androidtv.playback.PlaybackManager;
import org.jellyfin.androidtv.playback.PlaybackOverlayActivity;
import org.jellyfin.androidtv.search.SearchActivity;
import org.jellyfin.androidtv.model.LogonCredentials;
import org.jellyfin.androidtv.util.DeviceUtils;
import org.jellyfin.androidtv.util.Utils;
import java.util.Calendar;
import java.util.HashMap;
import org.jellyfin.apiclient.interaction.ApiClient;
import org.jellyfin.apiclient.interaction.EmptyResponse;
import org.jellyfin.apiclient.interaction.IConnectionManager;
@@ -45,15 +38,34 @@ import org.jellyfin.apiclient.interaction.VolleyHttpClient;
import org.jellyfin.apiclient.logging.AndroidLogger;
import org.jellyfin.apiclient.model.configuration.ServerConfiguration;
import org.jellyfin.apiclient.model.dto.BaseItemDto;
import org.jellyfin.apiclient.model.dto.BaseItemType;
import org.jellyfin.apiclient.model.dto.UserDto;
import org.jellyfin.apiclient.model.entities.DisplayPreferences;
import org.jellyfin.apiclient.model.logging.ILogger;
import org.jellyfin.apiclient.model.serialization.GsonJsonSerializer;
public class TvApp extends Application implements ActivityCompat.OnRequestPermissionsResultCallback {
import java.util.Calendar;
import java.util.HashMap;
import androidx.core.content.ContextCompat;
import androidx.palette.graphics.Palette;
@AcraCore(buildConfigClass = BuildConfig.class)
@AcraHttpSender(
uri = "https://collector.tracepot.com/a2eda9d9",
httpMethod = HttpSender.Method.POST
)
@AcraDialog(
resTitle = R.string.acra_dialog_title,
resText = R.string.acra_dialog_text,
resTheme = R.style.Theme_Jellyfin
)
@AcraLimiter
public class TvApp extends Application {
// The minimum supported server version. Trying to connect to an older server will display an error.
public static final String MINIMUM_SERVER_VERSION = "10.3.0";
public static final String CREDENTIALS_PATH = "org.jellyfin.androidtv.login.json";
public static final int LIVE_TV_GUIDE_OPTION_ID = 1000;
public static final int LIVE_TV_RECORDINGS_OPTION_ID = 2000;
public static final int VIDEO_QUEUE_OPTION_ID = 3000;
@@ -61,7 +73,6 @@ public class TvApp extends Application implements ActivityCompat.OnRequestPermis
public static final int LIVE_TV_SERIES_OPTION_ID = 5000;
private static final String TAG = "Jellyfin-AndroidTV";
private static final int SEARCH_PERMISSION = 0;
private ILogger logger;
private IConnectionManager connectionManager;
@@ -97,8 +108,6 @@ public class TvApp extends Application implements ActivityCompat.OnRequestPermis
private long lastMusicPlayback = System.currentTimeMillis();
private long lastUserInteraction = System.currentTimeMillis();
private boolean searchAllowed = Build.VERSION.SDK_INT < 23;
private GradientDrawable currentBackgroundGradient;
private boolean audioMuted;
@@ -109,6 +118,13 @@ public class TvApp extends Application implements ActivityCompat.OnRequestPermis
private LogonCredentials configuredAutoCredentials;
@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
ACRA.init(this);
}
@Override
public void onCreate() {
super.onCreate();
@@ -119,24 +135,6 @@ public class TvApp extends Application implements ActivityCompat.OnRequestPermis
setCurrentBackgroundGradient(new int[] {ContextCompat.getColor(this, R.color.lb_default_brand_color_dark), ContextCompat.getColor(this, R.color.lb_default_brand_color)});
logger.Info("Application object created");
Thread.setDefaultUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
@Override
public void uncaughtException(Thread thread, Throwable ex) {
logger.FatalException("Uncaught Exception", new Exception(ex));
/*
* If an Exception happens when an Activity is being initialized,
* it seems to hit an infinite loop of trying to re-initialize the Activity.
* To avoid this, we call finish on the current activity.
*/
if (currentActivity != null) {
currentActivity.finish();
}
android.os.Process.killProcess(android.os.Process.myPid());
System.exit(10);
}
});
}
public static TvApp getApplication() {
@@ -229,61 +227,12 @@ public class TvApp extends Application implements ActivityCompat.OnRequestPermis
}
public void showSearch(final Activity activity, boolean musicOnly) {
if (!searchAllowed && ContextCompat.checkSelfPermission(activity,
Manifest.permission.RECORD_AUDIO)
!= PackageManager.PERMISSION_GRANTED) {
//request necessary permission
logger.Info("Requesting search permission...");
if (ActivityCompat.shouldShowRequestPermissionRationale(activity, Manifest.permission.RECORD_AUDIO)) {
//show explanation
logger.Info("Show rationale for permission");
new AlertDialog.Builder(activity)
.setTitle("Search Permission")
.setMessage("Search requires permission to record audio in order to use the microphone for voice search")
.setPositiveButton("Ok", new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int whichButton) {
ActivityCompat.requestPermissions(activity, new String[] {Manifest.permission.RECORD_AUDIO}, SEARCH_PERMISSION);
}
}).show();
} else {
ActivityCompat.requestPermissions(activity, new String[] {Manifest.permission.RECORD_AUDIO}, SEARCH_PERMISSION);
}
} else {
showSearchInternal(activity, musicOnly);
}
}
private void showSearchInternal(Context activity, boolean musicOnly) {
Intent intent = new Intent(activity, SearchActivity.class);
if (musicOnly) intent.putExtra("MusicOnly", true);
intent.putExtra("MusicOnly", musicOnly);
activity.startActivity(intent);
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String permissions[], @NonNull int[] grantResults) {
switch (requestCode) {
case SEARCH_PERMISSION: {
// If request is cancelled, the result arrays are empty.
if (grantResults.length > 0
&& grantResults[0] == PackageManager.PERMISSION_GRANTED) {
// permission was granted, yay!
searchAllowed = true;
showSearchInternal(getCurrentActivity(), false);
} else {
// permission denied, boo! Disable the
// functionality that depends on this permission.
Utils.showToast(this, "Search not allowed");
}
return;
}
// other 'case' lines to check for other
// permissions this app might request
}
}
public void showMessage(String title, String msg) {
if (currentActivity != null) {
currentActivity.showMessage(title, msg);
@@ -320,23 +269,23 @@ public class TvApp extends Application implements ActivityCompat.OnRequestPermis
return getPrefs().getString("pref_login_behavior", "0").equals("1") && getConfiguredAutoCredentials().getServerInfo().getId() != null;
}
public boolean useExternalPlayer(String itemType) {
public boolean useExternalPlayer(BaseItemType itemType) {
switch (itemType) {
case "Movie":
case "Episode":
case "Video":
case "Series":
case "Recording":
return getPrefs().getBoolean("pref_video_use_external", false);
case "TvChannel":
case "Program":
case Movie:
case Episode:
case Video:
case Series:
case Recording:
return getPrefs().getString("pref_video_player", "auto").equals("external");
case TvChannel:
case Program:
return getPrefs().getBoolean("pref_live_tv_use_external", false);
default:
return false;
}
}
public Class getPlaybackActivityClass(String itemType) {
public Class getPlaybackActivityClass(BaseItemType itemType) {
return useExternalPlayer(itemType) ? ExternalPlayerActivity.class : PlaybackOverlayActivity.class;
}
@@ -571,14 +520,6 @@ public class TvApp extends Application implements ActivityCompat.OnRequestPermis
this.httpClient = httpClient;
}
public boolean isSearchAllowed() {
return searchAllowed;
}
public void setSearchAllowed(boolean searchAllowed) {
this.searchAllowed = searchAllowed;
}
public BaseItemDto getLastPlayedItem() {
return lastPlayedItem;
}

View File

@@ -1,6 +1,5 @@
package org.jellyfin.androidtv.base;
import android.app.Activity;
import android.graphics.Rect;
import android.os.Bundle;
import android.os.Handler;
@@ -17,8 +16,10 @@ import org.jellyfin.androidtv.R;
import org.jellyfin.androidtv.TvApp;
import org.jellyfin.androidtv.util.Utils;
public class BaseActivity extends Activity {
import androidx.core.app.ActivityCompat;
import androidx.fragment.app.FragmentActivity;
public abstract class BaseActivity extends FragmentActivity implements ActivityCompat.OnRequestPermissionsResultCallback {
private TvApp app = TvApp.getApplication();
private long timeoutInterval = 3600000;
private Handler handler = new Handler();
@@ -38,7 +39,7 @@ public class BaseActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
timeoutInterval = Long.parseLong(PreferenceManager.getDefaultSharedPreferences(app).getString("pref_auto_logoff_timeout","3600000"));
timeoutInterval = Long.parseLong(PreferenceManager.getDefaultSharedPreferences(app).getString("pref_auto_logoff_timeout", "3600000"));
startAutoLogoffLoop();
TvApp.getApplication().setCurrentActivity(this);
@@ -79,8 +80,12 @@ public class BaseActivity extends Activity {
if (messageUi != null && !isFinishing()) {
messageTitle.setText(title);
messageMessage.setText(msg);
if (icon != null) messageIcon.setImageResource(icon);
if (background != null) messageUi.setBackgroundResource(background);
if (icon != null) {
messageIcon.setImageResource(icon);
}
if (background != null) {
messageUi.setBackgroundResource(background);
}
messageUi.animate().x(msgPos).alpha(1).setDuration(300);
handler.postDelayed(new Runnable() {
@Override
@@ -89,7 +94,7 @@ public class BaseActivity extends Activity {
messageUi.animate().alpha(0).x(farRight).setDuration(300);
}
}
},timeout);
}, timeout);
}
}
@@ -122,7 +127,9 @@ public class BaseActivity extends Activity {
@Override
protected void onDestroy() {
if (handler != null && loop != null) handler.removeCallbacks(loop);
if (handler != null && loop != null) {
handler.removeCallbacks(loop);
}
super.onDestroy();
}
@@ -133,7 +140,9 @@ public class BaseActivity extends Activity {
}
private void startAutoLogoffLoop() {
if (loop != null) return;
if (loop != null) {
return;
}
loop = new Runnable() {
@Override
@@ -165,7 +174,9 @@ public class BaseActivity extends Activity {
}
public void sendMessage(CustomMessage message) {
if (messageListener != null) messageListener.onMessageReceived(message);
if (messageListener != null) {
messageListener.onMessageReceived(message);
}
}
@Override

View File

@@ -6,6 +6,7 @@ import org.jellyfin.androidtv.TvApp;
import org.jellyfin.androidtv.model.ChangeTriggerType;
import org.jellyfin.androidtv.querying.StdItemQuery;
import org.jellyfin.apiclient.model.dto.BaseItemType;
import org.jellyfin.apiclient.model.querying.ArtistsQuery;
import org.jellyfin.apiclient.model.querying.ItemFields;
@@ -30,7 +31,7 @@ public class BrowseGridFragment extends StdGridFragment {
protected void setupQueries(IGridLoader gridLoader) {
StdItemQuery query = new StdItemQuery(new ItemFields[] {ItemFields.PrimaryImageAspectRatio});
query.setParentId(mParentId);
if (mFolder.getType().equals("UserView") || mFolder.getType().equals("CollectionFolder")) {
if (mFolder.getBaseItemType() == BaseItemType.UserView || mFolder.getBaseItemType() == BaseItemType.CollectionFolder) {
String type = mFolder.getCollectionType() != null ? mFolder.getCollectionType().toLowerCase() : "";
switch (type) {
case "movies":

View File

@@ -19,6 +19,7 @@ import java.util.List;
import org.jellyfin.apiclient.interaction.Response;
import org.jellyfin.apiclient.model.dto.BaseItemDto;
import org.jellyfin.apiclient.model.dto.BaseItemType;
import org.jellyfin.apiclient.model.entities.LocationType;
import org.jellyfin.apiclient.model.livetv.RecordingGroupQuery;
import org.jellyfin.apiclient.model.livetv.RecordingQuery;
@@ -119,7 +120,7 @@ public class BrowseRecordingsFragment extends EnhancedBrowseFragment {
programInfo.setChannelName(timer.getChannelName());
programInfo.setName(Utils.getSafeValue(timer.getName(), "Unknown"));
TvApp.getApplication().getLogger().Warn("No program info for timer %s. Creating one...", programInfo.getName());
programInfo.setType("Program");
programInfo.setBaseItemType(BaseItemType.Program);
programInfo.setTimerId(timer.getId());
programInfo.setSeriesTimerId(timer.getSeriesTimerId());
programInfo.setStartDate(timer.getStartDate());

View File

@@ -1,6 +1,5 @@
package org.jellyfin.androidtv.browsing;
import android.os.Bundle;
import androidx.leanback.widget.ArrayObjectAdapter;
import androidx.leanback.widget.HeaderItem;
import androidx.leanback.widget.ListRow;
@@ -21,6 +20,7 @@ import java.util.List;
import org.jellyfin.apiclient.interaction.Response;
import org.jellyfin.apiclient.model.dto.BaseItemDto;
import org.jellyfin.apiclient.model.dto.BaseItemType;
import org.jellyfin.apiclient.model.entities.LocationType;
import org.jellyfin.apiclient.model.entities.SortOrder;
import org.jellyfin.apiclient.model.livetv.LiveTvChannelQuery;
@@ -39,29 +39,13 @@ import org.jellyfin.apiclient.model.querying.LatestItemsQuery;
import org.jellyfin.apiclient.model.querying.NextUpQuery;
import org.jellyfin.apiclient.model.results.TimerInfoDtoResult;
/**
* Created by Eric on 12/4/2014.
*/
public class BrowseViewFragment extends EnhancedBrowseFragment {
boolean isLiveTvLibrary;
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
}
@Override
public void onResume() {
super.onResume();
}
private boolean isLiveTvLibrary;
@Override
protected void setupQueries(final IRowLoader rowLoader) {
String type = mFolder.getCollectionType() != null ? mFolder.getCollectionType().toLowerCase() : "";
switch (type)
{
switch (type) {
case "movies":
itemTypeString = "Movie";
@@ -77,15 +61,15 @@ public class BrowseViewFragment extends EnhancedBrowseFragment {
resumeMovies.setFilters(new ItemFilter[]{ItemFilter.IsResumable});
resumeMovies.setSortBy(new String[]{ItemSortBy.DatePlayed});
resumeMovies.setSortOrder(SortOrder.Descending);
mRows.add(new BrowseRowDef(mApplication.getString(R.string.lbl_continue_watching), resumeMovies, 0, new ChangeTriggerType[] {ChangeTriggerType.MoviePlayback}));
mRows.add(new BrowseRowDef(mApplication.getString(R.string.lbl_continue_watching), resumeMovies, 0, new ChangeTriggerType[]{ChangeTriggerType.MoviePlayback}));
//Latest
LatestItemsQuery latestMovies = new LatestItemsQuery();
latestMovies.setFields(new ItemFields[] {ItemFields.PrimaryImageAspectRatio, ItemFields.Overview});
latestMovies.setFields(new ItemFields[]{ItemFields.PrimaryImageAspectRatio, ItemFields.Overview});
latestMovies.setParentId(mFolder.getId());
latestMovies.setLimit(50);
latestMovies.setImageTypeLimit(1);
mRows.add(new BrowseRowDef(mApplication.getString(R.string.lbl_latest), latestMovies, new ChangeTriggerType[] {ChangeTriggerType.MoviePlayback, ChangeTriggerType.LibraryUpdated}));
mRows.add(new BrowseRowDef(mApplication.getString(R.string.lbl_latest), latestMovies, new ChangeTriggerType[]{ChangeTriggerType.MoviePlayback, ChangeTriggerType.LibraryUpdated}));
//Favorites
StdItemQuery favorites = new StdItemQuery();
@@ -95,7 +79,7 @@ public class BrowseViewFragment extends EnhancedBrowseFragment {
favorites.setImageTypeLimit(1);
favorites.setFilters(new ItemFilter[]{ItemFilter.IsFavorite});
favorites.setSortBy(new String[]{ItemSortBy.SortName});
mRows.add(new BrowseRowDef(mApplication.getString(R.string.lbl_favorites), favorites, 60, new ChangeTriggerType[] {ChangeTriggerType.LibraryUpdated, ChangeTriggerType.FavoriteUpdate}));
mRows.add(new BrowseRowDef(mApplication.getString(R.string.lbl_favorites), favorites, 60, new ChangeTriggerType[]{ChangeTriggerType.LibraryUpdated, ChangeTriggerType.FavoriteUpdate}));
//Collections
StdItemQuery collections = new StdItemQuery();
@@ -104,7 +88,7 @@ public class BrowseViewFragment extends EnhancedBrowseFragment {
collections.setImageTypeLimit(1);
//collections.setParentId(mFolder.getId());
collections.setSortBy(new String[]{ItemSortBy.SortName});
mRows.add(new BrowseRowDef(mApplication.getString(R.string.lbl_collections), collections, 60, new ChangeTriggerType[] {ChangeTriggerType.LibraryUpdated}));
mRows.add(new BrowseRowDef(mApplication.getString(R.string.lbl_collections), collections, 60, new ChangeTriggerType[]{ChangeTriggerType.LibraryUpdated}));
rowLoader.loadRows(mRows);
break;
@@ -117,8 +101,8 @@ public class BrowseViewFragment extends EnhancedBrowseFragment {
nextUpQuery.setLimit(50);
nextUpQuery.setParentId(mFolder.getId());
nextUpQuery.setImageTypeLimit(1);
nextUpQuery.setFields(new ItemFields[] {ItemFields.PrimaryImageAspectRatio, ItemFields.Overview});
mRows.add(new BrowseRowDef(mApplication.getResources().getString(R.string.lbl_next_up), nextUpQuery, new ChangeTriggerType[] {ChangeTriggerType.TvPlayback}));
nextUpQuery.setFields(new ItemFields[]{ItemFields.PrimaryImageAspectRatio, ItemFields.Overview});
mRows.add(new BrowseRowDef(mApplication.getResources().getString(R.string.lbl_next_up), nextUpQuery, new ChangeTriggerType[]{ChangeTriggerType.TvPlayback}));
//Premieres
if (mApplication.getPrefs().getBoolean("pref_enable_premieres", false)) {
@@ -140,13 +124,13 @@ public class BrowseViewFragment extends EnhancedBrowseFragment {
//Latest content added
LatestItemsQuery latestSeries = new LatestItemsQuery();
latestSeries.setFields(new ItemFields[] {ItemFields.PrimaryImageAspectRatio, ItemFields.Overview});
latestSeries.setFields(new ItemFields[]{ItemFields.PrimaryImageAspectRatio, ItemFields.Overview});
latestSeries.setIncludeItemTypes(new String[]{"Episode"});
latestSeries.setGroupItems(true);
latestSeries.setParentId(mFolder.getId());
latestSeries.setLimit(50);
latestSeries.setImageTypeLimit(1);
mRows.add(new BrowseRowDef(mApplication.getString(R.string.lbl_latest), latestSeries, new ChangeTriggerType[] {ChangeTriggerType.LibraryUpdated}));
mRows.add(new BrowseRowDef(mApplication.getString(R.string.lbl_latest), latestSeries, new ChangeTriggerType[]{ChangeTriggerType.LibraryUpdated}));
//Favorites
StdItemQuery tvFavorites = new StdItemQuery();
@@ -156,7 +140,7 @@ public class BrowseViewFragment extends EnhancedBrowseFragment {
tvFavorites.setImageTypeLimit(1);
tvFavorites.setFilters(new ItemFilter[]{ItemFilter.IsFavorite});
tvFavorites.setSortBy(new String[]{ItemSortBy.SortName});
mRows.add(new BrowseRowDef(mApplication.getString(R.string.lbl_favorites), tvFavorites, 60, new ChangeTriggerType[] {ChangeTriggerType.LibraryUpdated, ChangeTriggerType.FavoriteUpdate}));
mRows.add(new BrowseRowDef(mApplication.getString(R.string.lbl_favorites), tvFavorites, 60, new ChangeTriggerType[]{ChangeTriggerType.LibraryUpdated, ChangeTriggerType.FavoriteUpdate}));
rowLoader.loadRows(mRows);
break;
@@ -164,13 +148,13 @@ public class BrowseViewFragment extends EnhancedBrowseFragment {
//Latest
LatestItemsQuery latestAlbums = new LatestItemsQuery();
latestAlbums.setFields(new ItemFields[] {ItemFields.PrimaryImageAspectRatio, ItemFields.Overview});
latestAlbums.setFields(new ItemFields[]{ItemFields.PrimaryImageAspectRatio, ItemFields.Overview});
latestAlbums.setIncludeItemTypes(new String[]{"Audio"});
latestAlbums.setGroupItems(true);
latestAlbums.setImageTypeLimit(1);
latestAlbums.setParentId(mFolder.getId());
latestAlbums.setLimit(50);
mRows.add(new BrowseRowDef(mApplication.getString(R.string.lbl_latest), latestAlbums, new ChangeTriggerType[] {ChangeTriggerType.LibraryUpdated}));
mRows.add(new BrowseRowDef(mApplication.getString(R.string.lbl_latest), latestAlbums, new ChangeTriggerType[]{ChangeTriggerType.LibraryUpdated}));
//Last Played
StdItemQuery lastPlayed = new StdItemQuery();
@@ -183,7 +167,7 @@ public class BrowseViewFragment extends EnhancedBrowseFragment {
lastPlayed.setSortOrder(SortOrder.Descending);
lastPlayed.setEnableTotalRecordCount(false);
lastPlayed.setLimit(50);
mRows.add(new BrowseRowDef(mApplication.getString(R.string.lbl_last_played), lastPlayed, 0, false, true, new ChangeTriggerType[] {ChangeTriggerType.MusicPlayback, ChangeTriggerType.LibraryUpdated}));
mRows.add(new BrowseRowDef(mApplication.getString(R.string.lbl_last_played), lastPlayed, 0, false, true, new ChangeTriggerType[]{ChangeTriggerType.MusicPlayback, ChangeTriggerType.LibraryUpdated}));
//Favorites
StdItemQuery favAlbums = new StdItemQuery();
@@ -193,17 +177,16 @@ public class BrowseViewFragment extends EnhancedBrowseFragment {
favAlbums.setImageTypeLimit(1);
favAlbums.setFilters(new ItemFilter[]{ItemFilter.IsFavorite});
favAlbums.setSortBy(new String[]{ItemSortBy.SortName});
mRows.add(new BrowseRowDef(mApplication.getString(R.string.lbl_favorites), favAlbums, 60, false, true, new ChangeTriggerType[] {ChangeTriggerType.LibraryUpdated, ChangeTriggerType.FavoriteUpdate}));
mRows.add(new BrowseRowDef(mApplication.getString(R.string.lbl_favorites), favAlbums, 60, false, true, new ChangeTriggerType[]{ChangeTriggerType.LibraryUpdated, ChangeTriggerType.FavoriteUpdate}));
//AudioPlaylists
StdItemQuery playlists = new StdItemQuery(new ItemFields[] {ItemFields.PrimaryImageAspectRatio, ItemFields.CumulativeRunTimeTicks});
StdItemQuery playlists = new StdItemQuery(new ItemFields[]{ItemFields.PrimaryImageAspectRatio, ItemFields.CumulativeRunTimeTicks});
playlists.setIncludeItemTypes(new String[]{"Playlist"});
playlists.setMediaTypes(new String[] {"Audio"});
playlists.setImageTypeLimit(1);
playlists.setRecursive(true);
playlists.setSortBy(new String[]{ItemSortBy.DateCreated});
playlists.setSortOrder(SortOrder.Descending);
mRows.add(new BrowseRowDef(mApplication.getString(R.string.lbl_playlists), playlists, 60, false, true, new ChangeTriggerType[] {ChangeTriggerType.LibraryUpdated}, QueryType.AudioPlaylists));
mRows.add(new BrowseRowDef(mApplication.getString(R.string.lbl_playlists), playlists, 60, false, true, new ChangeTriggerType[]{ChangeTriggerType.LibraryUpdated}, QueryType.AudioPlaylists));
rowLoader.loadRows(mRows);
break;
@@ -214,7 +197,7 @@ public class BrowseViewFragment extends EnhancedBrowseFragment {
//On now
RecommendedProgramQuery onNow = new RecommendedProgramQuery();
onNow.setIsAiring(true);
onNow.setFields(new ItemFields[] {ItemFields.Overview, ItemFields.PrimaryImageAspectRatio, ItemFields.ChannelInfo});
onNow.setFields(new ItemFields[]{ItemFields.Overview, ItemFields.PrimaryImageAspectRatio, ItemFields.ChannelInfo});
onNow.setUserId(TvApp.getApplication().getCurrentUser().getId());
onNow.setImageTypeLimit(1);
onNow.setEnableTotalRecordCount(false);
@@ -261,7 +244,7 @@ public class BrowseViewFragment extends EnhancedBrowseFragment {
// Also get scheduled recordings for next 24 hours
final TimerQuery scheduled = new TimerQuery();
TvApp.getApplication().getApiClient().GetLiveTvTimersAsync(scheduled, new Response<TimerInfoDtoResult>(){
TvApp.getApplication().getApiClient().GetLiveTvTimersAsync(scheduled, new Response<TimerInfoDtoResult>() {
@Override
public void onResponse(TimerInfoDtoResult response) {
List<BaseItemDto> nearTimers = new ArrayList<>();
@@ -274,9 +257,9 @@ public class BrowseViewFragment extends EnhancedBrowseFragment {
programInfo = new BaseItemDto();
programInfo.setId(timer.getId());
programInfo.setChannelName(timer.getChannelName());
programInfo.setName(Utils.getSafeValue(timer.getName(),"Unknown"));
programInfo.setName(Utils.getSafeValue(timer.getName(), "Unknown"));
TvApp.getApplication().getLogger().Warn("No program info for timer %s. Creating one...", programInfo.getName());
programInfo.setType("Program");
programInfo.setBaseItemType(BaseItemType.Program);
programInfo.setTimerId(timer.getId());
programInfo.setSeriesTimerId(timer.getSeriesTimerId());
programInfo.setStartDate(timer.getStartDate());
@@ -309,11 +292,11 @@ public class BrowseViewFragment extends EnhancedBrowseFragment {
recordings.setFields(new ItemFields[]{ItemFields.Overview, ItemFields.PrimaryImageAspectRatio});
recordings.setUserId(TvApp.getApplication().getCurrentUser().getId());
recordings.setEnableImages(true);
mRows.add(new BrowseRowDef("Recent Recordings", recordings, 50));
mRows.add(new BrowseRowDef(mActivity.getString(R.string.lbl_recent_recordings), recordings, 50));
//All Recordings by group - will only be there for non-internal TV
RecordingGroupQuery recordingGroups = new RecordingGroupQuery();
recordingGroups.setUserId(TvApp.getApplication().getCurrentUser().getId());
mRows.add(new BrowseRowDef("All Recordings", recordingGroups));
mRows.add(new BrowseRowDef(mActivity.getString(R.string.lbl_all_recordings), recordingGroups));
rowLoader.loadRows(mRows);
//Now insert our smart rows
@@ -384,7 +367,7 @@ public class BrowseViewFragment extends EnhancedBrowseFragment {
ItemQuery rowQuery = new StdItemQuery();
rowQuery.setParentId(item.getId());
rowQuery.setUserId(userId);
rows.add(new BrowseRowDef(item.getName(), rowQuery, 60, new ChangeTriggerType[] {ChangeTriggerType.LibraryUpdated}));
rows.add(new BrowseRowDef(item.getName(), rowQuery, 60, new ChangeTriggerType[]{ChangeTriggerType.LibraryUpdated}));
}
}
@@ -398,8 +381,6 @@ public class BrowseViewFragment extends EnhancedBrowseFragment {
});
break;
}
}
@Override

View File

@@ -1,11 +1,11 @@
package org.jellyfin.androidtv.browsing;
import android.app.Fragment;
import androidx.fragment.app.Fragment;
import android.graphics.Bitmap;
import android.os.Bundle;
import android.os.Handler;
import androidx.leanback.app.BackgroundManager;
import androidx.leanback.app.RowsFragment;
import androidx.leanback.app.RowsSupportFragment;
import androidx.leanback.widget.ArrayObjectAdapter;
import androidx.leanback.widget.HeaderItem;
import androidx.leanback.widget.ListRow;
@@ -40,7 +40,7 @@ import java.util.Timer;
import java.util.TimerTask;
public class CustomBrowseFragment extends Fragment implements IRowLoader {
private RowsFragment mRowsFragment;
private RowsSupportFragment mRowsFragment;
private static final int BACKGROUND_UPDATE_DELAY = 100;
protected String MainTitle;
@@ -65,13 +65,13 @@ public class CustomBrowseFragment extends Fragment implements IRowLoader {
View root = inflater.inflate(R.layout.fragment_custom_browse, container, false);
// Inject the RowsFragment in the results container
// Inject the RowsSupportFragment in the results container
if (getChildFragmentManager().findFragmentById(R.id.rows_area) == null) {
mRowsFragment = new RowsFragment();
mRowsFragment = new RowsSupportFragment();
getChildFragmentManager().beginTransaction()
.replace(R.id.rows_area, mRowsFragment).commit();
} else {
mRowsFragment = (RowsFragment) getChildFragmentManager()
mRowsFragment = (RowsSupportFragment) getChildFragmentManager()
.findFragmentById(R.id.rows_area);
}

View File

@@ -1,6 +1,6 @@
package org.jellyfin.androidtv.browsing;
import android.app.Fragment;
import androidx.fragment.app.Fragment;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.Color;
@@ -9,7 +9,7 @@ import android.os.Bundle;
import android.os.Handler;
import androidx.annotation.Nullable;
import androidx.leanback.app.BackgroundManager;
import androidx.leanback.app.RowsFragment;
import androidx.leanback.app.RowsSupportFragment;
import androidx.leanback.widget.ArrayObjectAdapter;
import androidx.leanback.widget.ClassPresenterSelector;
import androidx.leanback.widget.HeaderItem;
@@ -60,6 +60,7 @@ import java.util.TimerTask;
import org.jellyfin.apiclient.interaction.EmptyResponse;
import org.jellyfin.apiclient.interaction.Response;
import org.jellyfin.apiclient.model.dto.BaseItemDto;
import org.jellyfin.apiclient.model.dto.BaseItemType;
import org.jellyfin.apiclient.model.entities.DisplayPreferences;
public class EnhancedBrowseFragment extends Fragment implements IRowLoader {
@@ -95,7 +96,7 @@ public class EnhancedBrowseFragment extends Fragment implements IRowLoader {
private SimpleTarget<Bitmap> mBackgroundTarget;
private DisplayMetrics mMetrics;
RowsFragment mRowsFragment;
RowsSupportFragment mRowsFragment;
protected CompositeClickedListener mClickedListener = new CompositeClickedListener();
protected CompositeSelectedListener mSelectedListener = new CompositeSelectedListener();
protected ArrayObjectAdapter mRowsAdapter;
@@ -112,7 +113,7 @@ public class EnhancedBrowseFragment extends Fragment implements IRowLoader {
super.onCreate(savedInstanceState);
BaseItemDto item = new BaseItemDto();
item.setId(ItemListActivity.FAV_SONGS);
item.setType("Playlist");
item.setBaseItemType(BaseItemType.Playlist);
item.setIsFolder(true);
favSongsRowItem = new BaseRowItem(0, item);
@@ -135,13 +136,13 @@ public class EnhancedBrowseFragment extends Fragment implements IRowLoader {
mSummary.setTypeface(roboto);
mSummary.setShadowLayer(5, 5, 5, Color.BLACK);
// Inject the RowsFragment in the results container
// Inject the RowsSupportFragment in the results container
if (getChildFragmentManager().findFragmentById(R.id.rowsFragment) == null) {
mRowsFragment = new RowsFragment();
mRowsFragment = new RowsSupportFragment();
getChildFragmentManager().beginTransaction()
.replace(R.id.rowsFragment, mRowsFragment).commit();
} else {
mRowsFragment = (RowsFragment) getChildFragmentManager()
mRowsFragment = (RowsSupportFragment) getChildFragmentManager()
.findFragmentById(R.id.rowsFragment);
}
@@ -388,8 +389,8 @@ public class EnhancedBrowseFragment extends Fragment implements IRowLoader {
}
private void refreshCurrentItem() {
if (mCurrentItem != null && !"Photo".equals(mCurrentItem.getType()) && !"MusicArtist".equals(mCurrentItem.getType())
&& !"MusicAlbum".equals(mCurrentItem.getType()) && !"Playlist".equals(mCurrentItem.getType())) {
if (mCurrentItem != null && mCurrentItem.getBaseItemType() != BaseItemType.Photo && mCurrentItem.getBaseItemType() != BaseItemType.MusicArtist
&& mCurrentItem.getBaseItemType() != BaseItemType.MusicAlbum && mCurrentItem.getBaseItemType() != BaseItemType.Playlist) {
mCurrentItem.refresh(new EmptyResponse() {
@Override
public void onResponse() {

View File

@@ -9,6 +9,7 @@ import org.jellyfin.androidtv.querying.StdItemQuery;
import java.util.Arrays;
import org.jellyfin.androidtv.util.Utils;
import org.jellyfin.apiclient.model.dto.BaseItemType;
import org.jellyfin.apiclient.model.entities.SortOrder;
import org.jellyfin.apiclient.model.livetv.RecordingQuery;
import org.jellyfin.apiclient.model.querying.ItemFields;
@@ -23,12 +24,12 @@ public class GenericFolderFragment extends EnhancedBrowseFragment {
}
private static String[] showSpecialViewTypes = new String[] {"CollectionFolder", "Folder", "UserView", "ChannelFolderItem"};
private static BaseItemType[] showSpecialViewTypes = new BaseItemType[] {BaseItemType.CollectionFolder, BaseItemType.Folder, BaseItemType.UserView, BaseItemType.ChannelFolderItem };
@Override
protected void setupQueries(IRowLoader rowLoader) {
if (mFolder.getType().equals("RecordingGroup")){
if (mFolder.getBaseItemType() == BaseItemType.RecordingGroup){
RecordingQuery query = new RecordingQuery();
query.setUserId(TvApp.getApplication().getCurrentUser().getId());
query.setGroupId(mFolder.getId());
@@ -38,14 +39,14 @@ public class GenericFolderFragment extends EnhancedBrowseFragment {
} else {
if (Utils.getSafeValue(mFolder.getChildCount(), 0) > 0 ||
mFolder.getType().equals("Channel") ||
mFolder.getType().equals("ChannelFolderItem") ||
mFolder.getType().equals("UserView") ||
mFolder.getType().equals("CollectionFolder")) {
boolean showSpecialViews = Arrays.asList(showSpecialViewTypes).contains(mFolder.getType()) && !"channels".equals(mFolder.getCollectionType());
mFolder.getBaseItemType() == BaseItemType.Channel ||
mFolder.getBaseItemType() == BaseItemType.ChannelFolderItem ||
mFolder.getBaseItemType() == BaseItemType.UserView ||
mFolder.getBaseItemType() == BaseItemType.CollectionFolder) {
boolean showSpecialViews = Arrays.asList(showSpecialViewTypes).contains(mFolder.getBaseItemType()) && !"channels".equals(mFolder.getCollectionType());
if (showSpecialViews) {
if (!mFolder.getType().equals("ChannelFolderItem")) {
if (mFolder.getBaseItemType() != BaseItemType.ChannelFolderItem) {
StdItemQuery resume = new StdItemQuery();
resume.setParentId(mFolder.getId());
resume.setLimit(50);

View File

@@ -1,401 +0,0 @@
package org.jellyfin.androidtv.browsing;
import android.app.AlertDialog;
import android.content.DialogInterface;
import android.content.Intent;
import android.os.Bundle;
import android.os.Handler;
import androidx.leanback.widget.ArrayObjectAdapter;
import androidx.leanback.widget.HeaderItem;
import androidx.leanback.widget.ListRow;
import androidx.leanback.widget.OnItemViewClickedListener;
import androidx.leanback.widget.Presenter;
import androidx.leanback.widget.Row;
import androidx.leanback.widget.RowPresenter;
import android.widget.Toast;
import org.jellyfin.androidtv.R;
import org.jellyfin.androidtv.TvApp;
import org.jellyfin.androidtv.base.CustomMessage;
import org.jellyfin.androidtv.base.IMessageListener;
import org.jellyfin.androidtv.integration.RecommendationManager;
import org.jellyfin.androidtv.itemhandling.ItemRowAdapter;
import org.jellyfin.androidtv.livetv.LiveTvGuideActivity;
import org.jellyfin.androidtv.model.ChangeTriggerType;
import org.jellyfin.androidtv.model.DisplayPriorityType;
import org.jellyfin.androidtv.playback.AudioEventListener;
import org.jellyfin.androidtv.playback.MediaManager;
import org.jellyfin.androidtv.presentation.GridButtonPresenter;
import org.jellyfin.androidtv.presentation.ThemeManager;
import org.jellyfin.androidtv.querying.QueryType;
import org.jellyfin.androidtv.querying.StdItemQuery;
import org.jellyfin.androidtv.querying.ViewQuery;
import org.jellyfin.androidtv.settings.SettingsActivity;
import org.jellyfin.androidtv.model.LogonCredentials;
import org.jellyfin.androidtv.startup.SelectUserActivity;
import org.jellyfin.androidtv.ui.GridButton;
import org.jellyfin.androidtv.util.apiclient.AuthenticationHelper;
import java.io.IOException;
import org.jellyfin.apiclient.interaction.EmptyResponse;
import org.jellyfin.apiclient.interaction.Response;
import org.jellyfin.apiclient.model.entities.LocationType;
import org.jellyfin.apiclient.model.entities.SortOrder;
import org.jellyfin.apiclient.model.livetv.RecommendedProgramQuery;
import org.jellyfin.apiclient.model.livetv.RecordingQuery;
import org.jellyfin.apiclient.model.querying.ItemFields;
import org.jellyfin.apiclient.model.querying.ItemFilter;
import org.jellyfin.apiclient.model.querying.ItemSortBy;
import org.jellyfin.apiclient.model.querying.ItemsResult;
import org.jellyfin.apiclient.model.querying.LatestItemsQuery;
import org.jellyfin.apiclient.model.querying.NextUpQuery;
public class HomeFragment extends StdBrowseFragment {
private static final int LOGOUT = 0;
private static final int SETTINGS = 1;
@Override
public void onActivityCreated(Bundle savedInstanceState) {
MainTitle = this.getString(R.string.home_title);
super.onActivityCreated(savedInstanceState);
//Save last login so we can get back proper context on entry
try {
AuthenticationHelper.saveLoginCredentials(new LogonCredentials(TvApp.getApplication().getApiClient().getServerInfo(), TvApp.getApplication().getCurrentUser()), TvApp.CREDENTIALS_PATH);
} catch (IOException e) {
TvApp.getApplication().getLogger().ErrorException("Unable to save login creds", e);
}
//Init recommendations
RecommendationManager.init();
//Get auto bitrate
TvApp.getApplication().determineAutoBitrate();
//First time audio message
if (!mApplication.getSystemPrefs().getBoolean("syspref_audio_warned", false)) {
mApplication.getSystemPrefs().edit().putBoolean("syspref_audio_warned",true).apply();
new AlertDialog.Builder(mActivity)
.setTitle(mApplication.getString(R.string.lbl_audio_capabilitites))
.setMessage(mApplication.getString(R.string.msg_audio_warning))
.setPositiveButton(mApplication.getString(R.string.btn_got_it), null)
.setNegativeButton(mApplication.getString(R.string.btn_set_compatible_audio), new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
mApplication.getPrefs().edit().putString("pref_audio_option", "1").apply();
}
})
.setCancelable(false)
.show();
}
ThemeManager.showWelcomeMessage();
//Subscribe to Audio messages
MediaManager.addAudioEventListener(audioEventListener);
//Setup activity messages
mActivity.registerMessageListener(new IMessageListener() {
@Override
public void onMessageReceived(CustomMessage message) {
switch (message) {
case RefreshRows:
if (hasResumeRow()) {
refreshRows();
} else {
addContinueWatching();
}
break;
}
}
});
if (mApplication.getPrefs().getBoolean("pref_live_tv_mode",false)) {
//open guide activity and tell it to start last channel
Intent guide = new Intent(getActivity(), LiveTvGuideActivity.class);
guide.putExtra("loadLast", true);
startActivity(guide);
}
}
@Override
public void onResume() {
super.onResume();
//make sure rows have had a chance to be created
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
addNowPlaying();
//check for resume row and add if not there
if (!hasResumeRow()) addContinueWatching();
}
}, 750);
}
@Override
public void onDestroy() {
super.onDestroy();
MediaManager.removeAudioEventListener(audioEventListener);
}
@Override
protected void setupQueries(final IRowLoader rowLoader) {
//Peek at the first library item to determine our order
TvApp.getApplication().getApiClient().GetUserViews(TvApp.getApplication().getCurrentUser().getId(), new Response<ItemsResult>() {
@Override
public void onResponse(ItemsResult response) {
//First library and in-progress
mRows.add(new BrowseRowDef(mApplication.getString(R.string.lbl_library), new ViewQuery()));
//Special suggestions
String[] specialGenres = ThemeManager.getSpecialGenres();
if (specialGenres != null) {
StdItemQuery suggestions = new StdItemQuery();
suggestions.setIncludeItemTypes(new String[]{"Movie", "Series"});
suggestions.setGenres(specialGenres);
suggestions.setRecursive(true);
suggestions.setLimit(40);
suggestions.setEnableTotalRecordCount(false);
suggestions.setSortBy(new String[]{ItemSortBy.DatePlayed});
suggestions.setSortOrder(SortOrder.Ascending);
mRows.add(new BrowseRowDef(ThemeManager.getSuggestionTitle(), suggestions, 0, true, true, new ChangeTriggerType[]{}));
}
mRows.add(new BrowseRowDef(mApplication.getString(R.string.lbl_continue_watching), getResumeQuery(), 0, true, true, new ChangeTriggerType[]{ChangeTriggerType.MoviePlayback, ChangeTriggerType.TvPlayback, ChangeTriggerType.VideoQueueChange}, QueryType.ContinueWatching));
//Now others based on first library type
if (response.getTotalRecordCount() > 0) {
mApplication.setDisplayPriority("tvshows".equals(response.getItems()[0].getCollectionType()) ? DisplayPriorityType.TvShows : ("livetv".equals(response.getItems()[0].getCollectionType()) ? DisplayPriorityType.LiveTv : DisplayPriorityType.Movies));
switch (mApplication.getDisplayPriority()) {
case TvShows:
addNextUp();
addPremieres();
addLatestMovies();
addOnNow();
break;
case LiveTv:
addOnNow();
addNextUp();
addPremieres();
addLatestMovies();
break;
default:
addLatestMovies();
addNextUp();
addPremieres();
addOnNow();
}
}
// StdItemQuery latestMusic = new StdItemQuery();
// latestMusic.setIncludeItemTypes(new String[]{"MusicAlbum"});
// latestMusic.setRecursive(true);
// latestMusic.setLimit(50);
// latestMusic.setSortBy(new String[]{ItemSortBy.DateCreated});
// latestMusic.setSortOrder(SortOrder.Descending);
// mRowDef.add(new BrowseRowDef("Latest Albums", latestMusic, 0));
rowLoader.loadRows(mRows);
}
});
}
protected StdItemQuery getResumeQuery() {
StdItemQuery resumeItems = new StdItemQuery();
resumeItems.setMediaTypes(new String[] {"Video"});
resumeItems.setRecursive(true);
resumeItems.setImageTypeLimit(1);
resumeItems.setEnableTotalRecordCount(false);
resumeItems.setCollapseBoxSetItems(false);
resumeItems.setExcludeLocationTypes(new LocationType[] {LocationType.Virtual});
resumeItems.setLimit(50);
resumeItems.setFilters(new ItemFilter[]{ItemFilter.IsResumable});
resumeItems.setSortBy(new String[]{ItemSortBy.DatePlayed});
resumeItems.setSortOrder(SortOrder.Descending);
return resumeItems;
}
protected void addLatestMovies() {
LatestItemsQuery latestMovies = new LatestItemsQuery();
latestMovies.setFields(new ItemFields[] {ItemFields.PrimaryImageAspectRatio, ItemFields.Overview});
latestMovies.setIncludeItemTypes(new String[]{"Movie"});
latestMovies.setImageTypeLimit(1);
latestMovies.setLimit(50);
mRows.add(new BrowseRowDef(mApplication.getString(R.string.lbl_latest_movies), latestMovies, new ChangeTriggerType[]{ChangeTriggerType.LibraryUpdated, ChangeTriggerType.MoviePlayback}));
}
protected void addNextUp() {
NextUpQuery nextUpQuery = new NextUpQuery();
nextUpQuery.setUserId(TvApp.getApplication().getCurrentUser().getId());
nextUpQuery.setImageTypeLimit(1);
nextUpQuery.setLimit(50);
nextUpQuery.setFields(new ItemFields[]{ItemFields.PrimaryImageAspectRatio, ItemFields.Overview});
mRows.add(new BrowseRowDef(mApplication.getString(R.string.lbl_next_up_tv), nextUpQuery, new ChangeTriggerType[] {ChangeTriggerType.TvPlayback}));
}
protected void addPremieres() {
if (mApplication.getPrefs().getBoolean("pref_enable_premieres", false)) {
StdItemQuery newQuery = new StdItemQuery(new ItemFields[]{ItemFields.DateCreated, ItemFields.PrimaryImageAspectRatio, ItemFields.Overview});
newQuery.setUserId(TvApp.getApplication().getCurrentUser().getId());
newQuery.setIncludeItemTypes(new String[]{"Episode"});
newQuery.setRecursive(true);
newQuery.setIsVirtualUnaired(false);
newQuery.setIsMissing(false);
newQuery.setImageTypeLimit(1);
newQuery.setFilters(new ItemFilter[]{ItemFilter.IsUnplayed});
newQuery.setSortBy(new String[]{ItemSortBy.DateCreated});
newQuery.setSortOrder(SortOrder.Descending);
newQuery.setEnableTotalRecordCount(false);
newQuery.setLimit(200);
mRows.add(new BrowseRowDef(mApplication.getString(R.string.lbl_new_premieres), newQuery, 0, true, true, new ChangeTriggerType[] {ChangeTriggerType.TvPlayback}, QueryType.Premieres));
}
}
protected void addOnNow() {
if (TvApp.getApplication().getCurrentUser().getPolicy().getEnableLiveTvAccess()) {
RecommendedProgramQuery onNow = new RecommendedProgramQuery();
onNow.setIsAiring(true);
onNow.setFields(new ItemFields[] {ItemFields.Overview, ItemFields.PrimaryImageAspectRatio, ItemFields.ChannelInfo});
onNow.setUserId(TvApp.getApplication().getCurrentUser().getId());
onNow.setImageTypeLimit(1);
onNow.setEnableTotalRecordCount(false);
onNow.setLimit(20);
mRows.add(new BrowseRowDef(mApplication.getString(R.string.lbl_on_now), onNow));
//Latest Recordings
RecordingQuery recordings = new RecordingQuery();
recordings.setFields(new ItemFields[]{ItemFields.Overview, ItemFields.PrimaryImageAspectRatio});
recordings.setUserId(TvApp.getApplication().getCurrentUser().getId());
recordings.setEnableImages(true);
recordings.setLimit(40);
mRows.add(new BrowseRowDef(mActivity.getString(R.string.lbl_recordings), recordings));
}
}
protected boolean hasResumeRow() {
if (mRowsAdapter == null) return true;
for (int i = 0; i < mRowsAdapter.size(); i++) {
ListRow row = (ListRow)mRowsAdapter.get(i);
if (row.getAdapter() instanceof ItemRowAdapter && ((ItemRowAdapter)row.getAdapter()).getQueryType().equals(QueryType.ContinueWatching)) return true;
}
return false;
}
protected void addContinueWatching() {
//create the row and retrieve it to see if there are any before adding
final ItemRowAdapter resume = new ItemRowAdapter(getResumeQuery(), 0, true, true, mCardPresenter, mRowsAdapter, QueryType.ContinueWatching);
resume.setReRetrieveTriggers(new ChangeTriggerType[] {ChangeTriggerType.VideoQueueChange, ChangeTriggerType.TvPlayback, ChangeTriggerType.MoviePlayback});
final ListRow row = new ListRow(new HeaderItem(mApplication.getString(R.string.lbl_continue_watching)), resume);
resume.setRow(row);
resume.setRetrieveFinishedListener(new EmptyResponse() {
@Override
public void onResponse() {
mApplication.getLogger().Info("*** Continue watching retrieve finished: "+resume.size());
if (resume.size() > 0) {
mRowsAdapter.add(1, row);
}
}
});
resume.Retrieve();
}
protected AudioEventListener audioEventListener = new AudioEventListener() {
@Override
public void onQueueStatusChanged(boolean hasQueue) {
//remove on any change - it will re-add on resume
if (nowPlayingRow != null) {
mRowsAdapter.remove(nowPlayingRow);
nowPlayingRow = null;
}
}
};
protected ListRow nowPlayingRow;
protected void addNowPlaying() {
if (MediaManager.isPlayingAudio()) {
if (nowPlayingRow == null) {
nowPlayingRow = new ListRow(new HeaderItem(getString(R.string.lbl_now_playing)), MediaManager.getManagedAudioQueue());
mRowsAdapter.add(1, nowPlayingRow);
}
} else {
if (nowPlayingRow != null) {
mRowsAdapter.remove(nowPlayingRow);
nowPlayingRow = null;
}
}
}
@Override
protected void addAdditionalRows(ArrayObjectAdapter rowAdapter) {
super.addAdditionalRows(rowAdapter);
HeaderItem gridHeader = new HeaderItem(rowAdapter.size(), mApplication.getString(R.string.lbl_settings));
GridButtonPresenter mGridPresenter = new GridButtonPresenter();
ArrayObjectAdapter toolsRow = new ArrayObjectAdapter(mGridPresenter);
toolsRow.add(new GridButton(SETTINGS, mApplication.getString(R.string.lbl_settings), R.drawable.tile_settings));
toolsRow.add(new GridButton(LOGOUT, mApplication.getString(R.string.lbl_logout), R.drawable.tile_logout));
rowAdapter.add(new ListRow(gridHeader, toolsRow));
}
@Override
protected void setupEventListeners() {
super.setupEventListeners();
mClickedListener.registerListener(new ItemViewClickedListener());
}
private final class ItemViewClickedListener implements OnItemViewClickedListener {
@Override
public void onItemClicked(Presenter.ViewHolder itemViewHolder, Object item,
RowPresenter.ViewHolder rowViewHolder, Row row) {
if (item instanceof GridButton) {
switch (((GridButton) item).getId()) {
case LOGOUT:
TvApp app = TvApp.getApplication();
if (app.getIsAutoLoginConfigured()) {
// Present user selection
app.setLoginApiClient(app.getApiClient());
Intent userIntent = new Intent(getActivity(), SelectUserActivity.class);
userIntent.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY);
getActivity().startActivity(userIntent);
}
getActivity().finish(); //don't actually log out because we handle it ourselves
break;
case SETTINGS:
Intent settings = new Intent(getActivity(), SettingsActivity.class);
getActivity().startActivity(settings);
break;
default:
Toast.makeText(getActivity(), item.toString(), Toast.LENGTH_SHORT)
.show();
break;
}
}
}
}
}

View File

@@ -1,15 +0,0 @@
package org.jellyfin.androidtv.browsing;
import android.os.Bundle;
import org.jellyfin.androidtv.R;
import org.jellyfin.androidtv.base.BaseActivity;
public class MainActivity extends BaseActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_home);
}
}

View File

@@ -0,0 +1,12 @@
package org.jellyfin.androidtv.browsing
import android.os.Bundle
import org.jellyfin.androidtv.R
import org.jellyfin.androidtv.base.BaseActivity
class MainActivity : BaseActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_home)
}
}

View File

@@ -18,7 +18,7 @@ import android.graphics.Bitmap;
import android.os.Bundle;
import android.os.Handler;
import androidx.leanback.app.BackgroundManager;
import androidx.leanback.app.BrowseFragment;
import androidx.leanback.app.BrowseSupportFragment;
import androidx.leanback.widget.ArrayObjectAdapter;
import androidx.leanback.widget.HeaderItem;
import androidx.leanback.widget.ListRow;
@@ -67,8 +67,9 @@ import java.util.Timer;
import java.util.TimerTask;
import org.jellyfin.apiclient.interaction.EmptyResponse;
import org.jellyfin.apiclient.model.dto.BaseItemType;
public class StdBrowseFragment extends BrowseFragment implements IRowLoader {
public class StdBrowseFragment extends BrowseSupportFragment implements IRowLoader {
private static final String TAG = "StdBrowseFragment";
private static final int BACKGROUND_UPDATE_DELAY = 100;
@@ -90,7 +91,7 @@ public class StdBrowseFragment extends BrowseFragment implements IRowLoader {
private final Handler mHandler = new Handler();
private String mBackgroundUrl;
protected ArrayList<BrowseRowDef> mRows = new ArrayList<>();
CardPresenter mCardPresenter;
protected CardPresenter mCardPresenter;
protected boolean justLoaded = true;
@@ -400,7 +401,7 @@ public class StdBrowseFragment extends BrowseFragment implements IRowLoader {
}
private void refreshCurrentItem() {
if (mCurrentItem != null && !mCurrentItem.getType().equals("UserView") && !mCurrentItem.getType().equals("CollectionFolder")) {
if (mCurrentItem != null && mCurrentItem.getBaseItemType() != BaseItemType.UserView && mCurrentItem.getBaseItemType() != BaseItemType.CollectionFolder) {
TvApp.getApplication().getLogger().Debug("Refresh item "+mCurrentItem.getFullName());
mCurrentItem.refresh(new EmptyResponse() {
@Override
@@ -452,9 +453,11 @@ public class StdBrowseFragment extends BrowseFragment implements IRowLoader {
mCurrentRow = (ListRow) row;
BaseRowItem rowItem = (BaseRowItem) item;
//mApplication.getLogger().Debug("Selected Item "+rowItem.getIndex() + " type: "+ (rowItem.getItemType().equals(BaseRowItem.ItemType.BaseItem) ? rowItem.getBaseItem().getType() : "other"));
ItemRowAdapter adapter = (ItemRowAdapter) ((ListRow)row).getAdapter();
adapter.loadMoreItemsIfNeeded(rowItem.getIndex());
if (((ListRow) row).getAdapter() instanceof ItemRowAdapter) {
//mApplication.getLogger().Debug("Selected Item "+rowItem.getIndex() + " type: "+ (rowItem.getItemType().equals(BaseRowItem.ItemType.BaseItem) ? rowItem.getBaseItem().getType() : "other"));
ItemRowAdapter adapter = (ItemRowAdapter) ((ListRow) row).getAdapter();
adapter.loadMoreItemsIfNeeded(rowItem.getIndex());
}
if (ShowFanart) {
mBackgroundUrl = rowItem.getBackdropImageUrl();

View File

@@ -17,7 +17,8 @@ package org.jellyfin.androidtv.browsing;
import android.app.Activity;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.Color;
import android.graphics.drawable.ColorDrawable;
import android.os.Bundle;
import android.os.Handler;
import androidx.leanback.app.BackgroundManager;
@@ -73,6 +74,7 @@ import java.util.TimerTask;
import org.jellyfin.apiclient.interaction.EmptyResponse;
import org.jellyfin.apiclient.interaction.Response;
import org.jellyfin.apiclient.model.dto.BaseItemDto;
import org.jellyfin.apiclient.model.dto.BaseItemType;
import org.jellyfin.apiclient.model.entities.DisplayPreferences;
public class StdGridFragment extends HorizontalGridFragment implements IGridLoader {
@@ -450,7 +452,7 @@ public class StdGridFragment extends HorizontalGridFragment implements IGridLoad
toolBar.addView(new ImageButton(getActivity(), R.drawable.ic_search, size, new View.OnClickListener() {
@Override
public void onClick(View v) {
TvApp.getApplication().showSearch(getActivity(), "music".equals(mFolder.getCollectionType()) || "MusicAlbum".equals(mFolder.getType()) || "MusicArtist".equals(mFolder.getType()));
TvApp.getApplication().showSearch(getActivity(), "music".equals(mFolder.getCollectionType()) || mFolder.getBaseItemType() == BaseItemType.MusicAlbum || mFolder.getBaseItemType() == BaseItemType.MusicArtist);
}
}));
@@ -481,7 +483,7 @@ public class StdGridFragment extends HorizontalGridFragment implements IGridLoad
mPopup = new PopupWindow(layout, WIDTH, HEIGHT);
mPopup.setFocusable(true);
mPopup.setOutsideTouchable(true);
mPopup.setBackgroundDrawable(new BitmapDrawable()); // necessary for popup to dismiss
mPopup.setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT)); // necessary for popup to dismiss
mPopup.setAnimationStyle(R.style.PopupSlideInTop);
mJumplist = new JumpList(activity, new CharSelectedListener() {
@@ -588,8 +590,8 @@ public class StdGridFragment extends HorizontalGridFragment implements IGridLoad
getGridPresenter().setPosition(MediaManager.getCurrentMediaPosition());
MediaManager.setCurrentMediaPosition(-1); // re-set so it doesn't mess with parent views
}
if (mCurrentItem != null && !"Photo".equals(mCurrentItem.getType()) && !"PhotoAlbum".equals(mCurrentItem.getType())
&& !"MusicArtist".equals(mCurrentItem.getType()) && !"MusicAlbum".equals(mCurrentItem.getType())) {
if (mCurrentItem != null && mCurrentItem.getBaseItemType() != BaseItemType.Photo && mCurrentItem.getBaseItemType() != BaseItemType.PhotoAlbum
&& mCurrentItem.getBaseItemType() != BaseItemType.MusicArtist && mCurrentItem.getBaseItemType() != BaseItemType.MusicAlbum) {
TvApp.getApplication().getLogger().Debug("Refresh item "+mCurrentItem.getFullName());
mCurrentItem.refresh(new EmptyResponse() {
@Override

View File

@@ -0,0 +1,126 @@
package org.jellyfin.androidtv.channels
import android.content.Intent
import android.net.Uri
import android.os.Build
import androidx.tvprovider.media.tv.TvContractCompat.WatchNextPrograms
import androidx.tvprovider.media.tv.WatchNextProgram
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.jellyfin.androidtv.TvApp
import org.jellyfin.androidtv.startup.StartupActivity
import org.jellyfin.androidtv.util.apiclient.getNextUpEpisodes
import org.jellyfin.apiclient.model.drawing.ImageFormat
import org.jellyfin.apiclient.model.dto.BaseItemDto
import org.jellyfin.apiclient.model.dto.ImageOptions
import org.jellyfin.apiclient.model.querying.ItemFields
import org.jellyfin.apiclient.model.querying.NextUpQuery
/**
* Manages channels on the android tv home screen
*
* More info: https://developer.android.com/training/tv/discovery/recommendations-channel
*/
class ChannelManager {
private companion object {
/**
* Amount of ticks found in a millisecond, used for calculation
*/
private const val TICKS_IN_MILLISECOND = 10000
}
private val application = TvApp.getApplication()
/**
* Check if the app can use Leanback features and is API level 26 or higher
*/
private val isSupported = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O
&& application.packageManager.hasSystemFeature("android.software.leanback")
/**
* Update all channels for the currently authenticated user
*/
fun update() {
// Don't do anything if not supported
if (!isSupported) return
// Launch in separate coroutine scope
GlobalScope.launch {
updateWatchNext()
}
}
/**
* Updates the "watch next" row with new and unfinished episodes
* does not include movies, music or other types of media
*/
private suspend fun updateWatchNext() = withContext(Dispatchers.Default) {
// Delete current items
application.contentResolver.delete(WatchNextPrograms.CONTENT_URI, null, null)
// Get user or return if no user is found (not authenticated)
val user = application.currentUser ?: return@withContext
// Get new items
val response = application.apiClient.getNextUpEpisodes(NextUpQuery().apply {
userId = user.id
imageTypeLimit = 1
limit = 10
fields = arrayOf(ItemFields.DateCreated)
})
// Add new items
response?.items?.forEach { item ->
application.contentResolver.insert(
WatchNextPrograms.CONTENT_URI,
getBaseItemAsWatchNextProgram(item).toContentValues()
)
}
}
/**
* Convert [BaseItemDto] to [WatchNextProgram]
*
* Assumes the item type is "episode"
*/
private fun getBaseItemAsWatchNextProgram(item: BaseItemDto) = WatchNextProgram.Builder().apply {
setInternalProviderId(item.id)
setType(WatchNextPrograms.TYPE_TV_EPISODE)
setTitle("${item.seriesName} - ${item.name}")
// Poster image
setPosterArtAspectRatio(WatchNextPrograms.ASPECT_RATIO_16_9)
setPosterArtUri(Uri.parse(application.apiClient.GetImageUrl(item, ImageOptions().apply {
format = ImageFormat.Png
height = 288
width = 512
})))
// Use date created or fallback to current time if unavailable
setLastEngagementTimeUtcMillis(item.dateCreated?.time ?: System.currentTimeMillis())
when {
// User has started playing the episode
item.canResume -> {
setWatchNextType(WatchNextPrograms.WATCH_NEXT_TYPE_CONTINUE)
setLastPlaybackPositionMillis((item.resumePositionTicks / TICKS_IN_MILLISECOND).toInt())
setDurationMillis((item.runTimeTicks / TICKS_IN_MILLISECOND).toInt())
}
// First episode of the season
item.indexNumber == 0 -> {
setWatchNextType(WatchNextPrograms.WATCH_NEXT_TYPE_NEW)
}
// Default
else -> {
setWatchNextType(WatchNextPrograms.WATCH_NEXT_TYPE_NEXT)
}
}
// Set intent to open the episode
setIntent(Intent(application, StartupActivity::class.java).apply {
putExtra("ItemId", item.id)
})
}.build()
}

View File

@@ -0,0 +1,30 @@
package org.jellyfin.androidtv.constants;
// Names used in jellyfin-web
// Only uses those that are configurable in web settings
public enum HomeSectionType {
LATEST_MEDIA("latestmedia"),
LIBRARY_TILES_SMALL("smalllibrarytiles"),
LIBRARY_BUTTONS("librarybuttons"),
RESUME("resume"),
RESUME_AUDIO("resumeaudio"),
ACTIVE_RECORDINGS("activerecordings"),
NEXT_UP("nextup"),
LIVE_TV("livetv"),
NONE("none");
public static HomeSectionType getByName(String name) {
for (HomeSectionType type : HomeSectionType.values()) {
if (type.name.equalsIgnoreCase(name)) return type;
}
return null;
}
public final String name;
HomeSectionType(String name) {
this.name = name;
}
}

View File

@@ -1,13 +1,14 @@
package org.jellyfin.androidtv.details;
import android.os.Bundle;
import android.app.Activity;
import android.widget.TextView;
import org.jellyfin.androidtv.R;
import org.jellyfin.androidtv.TvApp;
public class ExpandedTextActivity extends Activity {
import androidx.fragment.app.FragmentActivity;
public class ExpandedTextActivity extends FragmentActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {

View File

@@ -12,7 +12,7 @@ import android.os.AsyncTask;
import android.os.Bundle;
import android.os.Handler;
import androidx.leanback.app.BackgroundManager;
import androidx.leanback.app.RowsFragment;
import androidx.leanback.app.RowsSupportFragment;
import androidx.leanback.widget.ArrayObjectAdapter;
import androidx.leanback.widget.ClassPresenterSelector;
import androidx.leanback.widget.HeaderItem;
@@ -77,6 +77,7 @@ import org.jellyfin.apiclient.interaction.EmptyResponse;
import org.jellyfin.apiclient.interaction.Response;
import org.jellyfin.apiclient.model.dto.BaseItemDto;
import org.jellyfin.apiclient.model.dto.BaseItemPerson;
import org.jellyfin.apiclient.model.dto.BaseItemType;
import org.jellyfin.apiclient.model.dto.ImageOptions;
import org.jellyfin.apiclient.model.dto.MediaSourceInfo;
import org.jellyfin.apiclient.model.dto.UserItemDataDto;
@@ -117,7 +118,7 @@ public class FullDetailsActivity extends BaseActivity implements IRecordingIndic
private Calendar mLastUpdated;
private String mPrevItemId;
private RowsFragment mRowsFragment;
private RowsSupportFragment mRowsFragment;
private ArrayObjectAdapter mRowsAdapter;
private MyDetailsOverviewRowPresenter mDorPresenter;
@@ -145,14 +146,13 @@ public class FullDetailsActivity extends BaseActivity implements IRecordingIndic
mActivity = this;
BackgroundManager backgroundManager = BackgroundManager.getInstance(this);
backgroundManager.attach(getWindow());
backgroundManager.setDimLayer(getDrawable(R.drawable.left_fade));
mBackgroundTarget = new MyPicassoBackgroundManagerTarget(backgroundManager);
mMetrics = new DisplayMetrics();
getWindowManager().getDefaultDisplay().getMetrics(mMetrics);
mRowsFragment = new RowsFragment();
getFragmentManager().beginTransaction().add(R.id.rowsFragment, mRowsFragment).commit();
mRowsFragment = new RowsSupportFragment();
getSupportFragmentManager().beginTransaction().add(R.id.rowsFragment, mRowsFragment).commit();
mRowsFragment.setOnItemViewClickedListener(new ItemViewClickedListener());
mRowsFragment.setOnItemViewSelectedListener(new ItemViewSelectedListener());
@@ -169,7 +169,7 @@ public class FullDetailsActivity extends BaseActivity implements IRecordingIndic
registerMessageListener(new IMessageListener() {
@Override
public void onMessageReceived(CustomMessage message) {
if (message == CustomMessage.ActionComplete && mSeriesTimerInfo != null && "SeriesTimer".equals(mBaseItem.getType())) {
if (message == CustomMessage.ActionComplete && mSeriesTimerInfo != null && mBaseItem.getBaseItemType() == BaseItemType.SeriesTimer) {
//update info
mApplication.getApiClient().GetLiveTvSeriesTimerAsync(mSeriesTimerInfo.getId(), new Response<SeriesTimerInfoDto>() {
@Override
@@ -206,11 +206,11 @@ public class FullDetailsActivity extends BaseActivity implements IRecordingIndic
rotateBackdrops();
//Update information that may have changed - delay slightly to allow changes to take on the server
if (mApplication.getLastPlayback().after(mLastUpdated) && !"MusicArtist".equals(mBaseItem.getType())) {
if (mApplication.getLastPlayback().after(mLastUpdated) && mBaseItem.getBaseItemType() != BaseItemType.MusicArtist) {
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
if ("Episode".equals(mBaseItem.getType()) && mApplication.getLastPlayback().after(mLastUpdated) && mApplication.getLastPlayedItem() != null && !mBaseItem.getId().equals(mApplication.getLastPlayedItem().getId()) && "Episode".equals(mApplication.getLastPlayedItem().getType())) {
if (mBaseItem.getBaseItemType() == BaseItemType.Episode && mApplication.getLastPlayback().after(mLastUpdated) && mApplication.getLastPlayedItem() != null && !mBaseItem.getId().equals(mApplication.getLastPlayedItem().getId()) && mApplication.getLastPlayedItem().getBaseItemType() == BaseItemType.Episode) {
mApplication.getLogger().Info("Re-loading after new episode playback");
loadItem(mApplication.getLastPlayedItem().getId());
mApplication.setLastPlayedItem(null); //blank this out so a detail screen we back up to doesn't also do this
@@ -222,7 +222,7 @@ public class FullDetailsActivity extends BaseActivity implements IRecordingIndic
if (!isFinishing()) {
mBaseItem = response;
if (mResumeButton != null) {
mResumeButton.setVisibility(("Series".equals(mBaseItem.getType()) && ! mBaseItem.getUserData().getPlayed()) || response.getCanResume() ? View.VISIBLE : View.GONE);
mResumeButton.setVisibility((mBaseItem.getBaseItemType() == BaseItemType.Series && ! mBaseItem.getUserData().getPlayed()) || response.getCanResume() ? View.VISIBLE : View.GONE);
if (response.getCanResume()){
mResumeButton.setText(String.format(getString(R.string.lbl_resume_from), TimeUtils.formatMillis((response.getUserData().getPlaybackPositionTicks()/10000) - mApplication.getResumePreroll())));
}
@@ -293,8 +293,25 @@ public class FullDetailsActivity extends BaseActivity implements IRecordingIndic
}
}
private static String[] buttonTypes = new String[] {"Episode","Movie","Series","Season","Folder","Video","Recording","Program","ChannelVideoItem","Trailer","MusicArtist","Person","MusicVideo","SeriesTimer"};
private static List<String> buttonTypeList = Arrays.asList(buttonTypes);
private static BaseItemType[] buttonTypes = new BaseItemType[] {
BaseItemType.Episode,
BaseItemType.Movie,
BaseItemType.Series,
BaseItemType.Season,
BaseItemType.Folder,
BaseItemType.Video,
BaseItemType.Recording,
BaseItemType.Program,
BaseItemType.ChannelVideoItem,
BaseItemType.Trailer,
BaseItemType.MusicArtist,
BaseItemType.Person,
BaseItemType.MusicVideo,
BaseItemType.SeriesTimer
};
private static List<BaseItemType> buttonTypeList = Arrays.asList(buttonTypes);
private static String[] directPlayableTypes = new String[] {"Episode","Movie","Video","Recording","Program"};
private static List<String> directPlayableTypeList = Arrays.asList(directPlayableTypes);
@@ -332,7 +349,7 @@ public class FullDetailsActivity extends BaseActivity implements IRecordingIndic
BaseItemDto item = new BaseItemDto();
item.setId(mSeriesTimerInfo.getId());
item.setSeriesTimerId(mSeriesTimerInfo.getId());
item.setType("SeriesTimer");
item.setBaseItemType(BaseItemType.SeriesTimer);
item.setName(mSeriesTimerInfo.getName());
item.setOverview(BaseItemUtils.getSeriesOverview(mSeriesTimerInfo));
@@ -369,7 +386,7 @@ public class FullDetailsActivity extends BaseActivity implements IRecordingIndic
// Figure image size
Double aspect = ImageUtils.getImageAspectRatio(item, false);
posterHeight = aspect > 1 ? Utils.convertDpToPixel(mActivity, 160) : Utils.convertDpToPixel(mActivity, "Person".equals(item.getType()) || "MusicArtist".equals(item.getType()) ? 300 : 200);
posterHeight = aspect > 1 ? Utils.convertDpToPixel(mActivity, 160) : Utils.convertDpToPixel(mActivity, item.getBaseItemType() == BaseItemType.Person || item.getBaseItemType() == BaseItemType.MusicArtist ? 300 : 200);
posterWidth = (int)((aspect) * posterHeight);
if (posterHeight < 10) posterWidth = Utils.convertDpToPixel(mActivity, 150); //Guard against zero size images causing picasso to barf
@@ -378,9 +395,9 @@ public class FullDetailsActivity extends BaseActivity implements IRecordingIndic
mDetailsOverviewRow = new MyDetailsOverviewRow(item);
mDetailsOverviewRow.setSummary(item.getOverview());
switch (item.getType()) {
case "Person":
case "MusicArtist":
switch (item.getBaseItemType()) {
case Person:
case MusicArtist:
mDetailsOverviewRow.setSummarySubTitle("");
break;
default:
@@ -388,7 +405,7 @@ public class FullDetailsActivity extends BaseActivity implements IRecordingIndic
BaseItemPerson director = BaseItemUtils.getFirstPerson(item, PersonType.Director);
InfoItem firstRow;
if ("Series".equals(item.getType())) {
if (item.getBaseItemType() == BaseItemType.Series) {
firstRow = new InfoItem(
getString(R.string.lbl_seasons),
Utils.getSafeValue(item.getChildCount(), 0).toString());
@@ -488,9 +505,9 @@ public class FullDetailsActivity extends BaseActivity implements IRecordingIndic
}
protected void addAdditionalRows(ArrayObjectAdapter adapter) {
TvApp.getApplication().getLogger().Debug("Item type: "+mBaseItem.getType());
switch (mBaseItem.getType()) {
case "Movie":
TvApp.getApplication().getLogger().Debug("Item type: " + mBaseItem.getBaseItemType());
switch (mBaseItem.getBaseItemType()) {
case Movie:
//Cast/Crew
if (mBaseItem.getPeople() != null && mBaseItem.getPeople().length > 0) {
@@ -527,7 +544,7 @@ public class FullDetailsActivity extends BaseActivity implements IRecordingIndic
addInfoRows(adapter);
break;
case "Trailer":
case Trailer:
//Cast/Crew
if (mBaseItem.getPeople() != null && mBaseItem.getPeople().length > 0) {
@@ -546,7 +563,7 @@ public class FullDetailsActivity extends BaseActivity implements IRecordingIndic
addItemRow(adapter, similarTrailerAdapter, 4, getString(R.string.lbl_more_like_this));
addInfoRows(adapter);
break;
case "Person":
case Person:
ItemQuery personMovies = new ItemQuery();
personMovies.setFields(new ItemFields[]{ItemFields.PrimaryImageAspectRatio});
@@ -567,7 +584,7 @@ public class FullDetailsActivity extends BaseActivity implements IRecordingIndic
addItemRow(adapter, personSeriesAdapter, 1, mApplication.getString(R.string.lbl_tv_series));
break;
case "MusicArtist":
case MusicArtist:
ItemQuery artistAlbums = new ItemQuery();
artistAlbums.setFields(new ItemFields[]{ItemFields.PrimaryImageAspectRatio});
@@ -579,7 +596,7 @@ public class FullDetailsActivity extends BaseActivity implements IRecordingIndic
addItemRow(adapter, artistAlbumsAdapter, 0, mApplication.getString(R.string.lbl_albums));
break;
case "Series":
case Series:
NextUpQuery nextUpQuery = new NextUpQuery();
nextUpQuery.setUserId(TvApp.getApplication().getCurrentUser().getId());
nextUpQuery.setSeriesId(mBaseItem.getId());
@@ -616,7 +633,7 @@ public class FullDetailsActivity extends BaseActivity implements IRecordingIndic
addItemRow(adapter, similarAdapter, 4, getString(R.string.lbl_more_like_this));
break;
case "Episode":
case Episode:
if (mBaseItem.getSeasonId() != null && mBaseItem.getIndexNumber() != null) {
StdItemQuery nextEpisodes = new StdItemQuery();
nextEpisodes.setParentId(mBaseItem.getSeasonId());
@@ -649,7 +666,7 @@ public class FullDetailsActivity extends BaseActivity implements IRecordingIndic
addInfoRows(adapter);
break;
case "SeriesTimer":
case SeriesTimer:
TimerQuery scheduled = new TimerQuery();
scheduled.setSeriesTimerId(mSeriesTimerInfo.getId());
TvManager.getScheduleRowsAsync(scheduled, new CardPresenter(true), adapter, new Response<Integer>());
@@ -677,7 +694,7 @@ public class FullDetailsActivity extends BaseActivity implements IRecordingIndic
}
private void updateInfo(BaseItemDto item) {
if (buttonTypeList.contains(item.getType())) addButtons(BUTTON_SIZE);
if (buttonTypeList.contains(item.getBaseItemType())) addButtons(BUTTON_SIZE);
// updatePlayedDate();
//
updateBackground(ImageUtils.getBackdropImageUrl(item, TvApp.getApplication().getApiClient(), true));
@@ -706,10 +723,10 @@ public class FullDetailsActivity extends BaseActivity implements IRecordingIndic
}
private String getEndTime() {
if (mBaseItem != null && !"MusicArtist".equals(mBaseItem.getType()) && !"Person".equals(mBaseItem.getType())) {
if (mBaseItem != null && mBaseItem.getBaseItemType() != BaseItemType.MusicArtist && mBaseItem.getBaseItemType() != BaseItemType.Person) {
Long runtime = Utils.getSafeValue(mBaseItem.getRunTimeTicks(), mBaseItem.getOriginalRunTimeTicks());
if (runtime != null && runtime > 0) {
long endTimeTicks = "Program".equals(mBaseItem.getType()) && mBaseItem.getEndDate() != null ? TimeUtils.convertToLocalDate(mBaseItem.getEndDate()).getTime() : System.currentTimeMillis() + runtime / 10000;
long endTimeTicks = mBaseItem.getBaseItemType() == BaseItemType.Program && mBaseItem.getEndDate() != null ? TimeUtils.convertToLocalDate(mBaseItem.getEndDate()).getTime() : System.currentTimeMillis() + runtime / 10000;
if (mBaseItem.getCanResume()) {
endTimeTicks = System.currentTimeMillis() + ((runtime - mBaseItem.getUserData().getPlaybackPositionTicks()) / 10000);
return android.text.format.DateFormat.getTimeFormat(this).format(new Date(endTimeTicks));
@@ -723,7 +740,7 @@ public class FullDetailsActivity extends BaseActivity implements IRecordingIndic
}
private void addItemToQueue() {
if ("Audio".equals(mBaseItem.getType())) {
if (mBaseItem.getBaseItemType() == BaseItemType.Audio) {
MediaManager.addToAudioQueue(Arrays.asList(mBaseItem));
} else {
@@ -792,7 +809,7 @@ public class FullDetailsActivity extends BaseActivity implements IRecordingIndic
private void addButtons(int buttonSize) {
String buttonLabel;
if ("Series".equals(mBaseItem.getType()) || "SeriesTimer".equals(mBaseItem.getType())) {
if (mBaseItem.getBaseItemType() == BaseItemType.Series || mBaseItem.getBaseItemType() == BaseItemType.SeriesTimer) {
buttonLabel = getString(R.string.lbl_play_next_up);
} else {
long startPos = 0;
@@ -804,7 +821,7 @@ public class FullDetailsActivity extends BaseActivity implements IRecordingIndic
mResumeButton = new TextUnderButton(this, R.drawable.ic_resume, buttonSize, 2, buttonLabel, new View.OnClickListener() {
@Override
public void onClick(View v) {
if ("Series".equals(mBaseItem.getType())) {
if (mBaseItem.getBaseItemType() == BaseItemType.Series) {
//play next up
NextUpQuery nextUpQuery = new NextUpQuery();
nextUpQuery.setUserId(TvApp.getApplication().getCurrentUser().getId());
@@ -836,7 +853,8 @@ public class FullDetailsActivity extends BaseActivity implements IRecordingIndic
if (BaseItemUtils.canPlay(mBaseItem)) {
mDetailsOverviewRow.addAction(mResumeButton);
mResumeButton.setVisibility(("Series".equals(mBaseItem.getType()) && ! mBaseItem.getUserData().getPlayed()) || (mBaseItem.getCanResume() ) ? View.VISIBLE : View.GONE);
boolean resumeButtonVisible = (mBaseItem.getBaseItemType() == BaseItemType.Series && !mBaseItem.getUserData().getPlayed()) || (mBaseItem.getCanResume());
mResumeButton.setVisibility(resumeButtonVisible ? View.VISIBLE : View.GONE);
TextUnderButton play = new TextUnderButton(this, R.drawable.ic_play, buttonSize, 2, getString(BaseItemUtils.isLiveTv(mBaseItem) ? R.string.lbl_tune_to_channel : mBaseItem.getIsFolderItem() ? R.string.lbl_play_all : R.string.lbl_play), new View.OnClickListener() {
@Override
@@ -845,6 +863,11 @@ public class FullDetailsActivity extends BaseActivity implements IRecordingIndic
}
});
mDetailsOverviewRow.addAction(play);
if (resumeButtonVisible) {
mResumeButton.requestFocus();
} else {
play.requestFocus();
}
if (!mBaseItem.getIsFolderItem() && !BaseItemUtils.isLiveTv(mBaseItem)) {
queueButton = new TextUnderButton(this, R.drawable.ic_add, buttonSize, 2, getString(R.string.lbl_add_to_queue), new View.OnClickListener() {
@@ -866,7 +889,7 @@ public class FullDetailsActivity extends BaseActivity implements IRecordingIndic
mDetailsOverviewRow.addAction(shuffle);
}
if ("MusicArtist".equals(mBaseItem.getType())) {
if (mBaseItem.getBaseItemType() == BaseItemType.MusicArtist) {
TextUnderButton imix = new TextUnderButton(this, R.drawable.ic_mix, buttonSize, getString(R.string.lbl_instant_mix), new View.OnClickListener() {
@Override
public void onClick(View v) {
@@ -1050,7 +1073,7 @@ public class FullDetailsActivity extends BaseActivity implements IRecordingIndic
UserItemDataDto userData = mBaseItem.getUserData();
if (userData != null && mProgramInfo == null) {
if (!"MusicArtist".equals(mBaseItem.getType()) && !"Person".equals(mBaseItem.getType())) {
if (mBaseItem.getBaseItemType() != BaseItemType.MusicArtist && mBaseItem.getBaseItemType() != BaseItemType.Person) {
mWatchedToggleButton = new TextUnderButton(this, userData.getPlayed() ? R.drawable.ic_watch_red : R.drawable.ic_watch, buttonSize, getString(R.string.lbl_watched), markWatchedListener);
mDetailsOverviewRow.addAction(mWatchedToggleButton);
}
@@ -1065,7 +1088,7 @@ public class FullDetailsActivity extends BaseActivity implements IRecordingIndic
mDetailsOverviewRow.addAction(favButton);
}
if ("Episode".equals(mBaseItem.getType()) && mBaseItem.getSeriesId() != null) {
if (mBaseItem.getBaseItemType() == BaseItemType.Episode && mBaseItem.getSeriesId() != null) {
//add the prev button first so it will be there in proper position - we'll show it later if needed
mPrevButton = new TextUnderButton(this, R.drawable.ic_previous_episode, buttonSize, 3, getString(R.string.lbl_previous_episode), new View.OnClickListener() {
@Override
@@ -1110,8 +1133,8 @@ public class FullDetailsActivity extends BaseActivity implements IRecordingIndic
mDetailsOverviewRow.addAction(goToSeriesButton);
}
if (("Recording".equals(mBaseItem.getType()) && TvApp.getApplication().getCurrentUser().getPolicy().getEnableLiveTvManagement() && mBaseItem.getCanDelete()) ||
(("Movie".equals(mBaseItem.getType()) || "Episode".equals(mBaseItem.getType()) || "Video".equals(mBaseItem.getType())) && TvApp.getApplication().getCurrentUser().getPolicy().getEnableContentDeletion())) {
if ((mBaseItem.getBaseItemType() == BaseItemType.Recording && TvApp.getApplication().getCurrentUser().getPolicy().getEnableLiveTvManagement() && mBaseItem.getCanDelete()) ||
((mBaseItem.getBaseItemType() == BaseItemType.Movie || mBaseItem.getBaseItemType() == BaseItemType.Episode || mBaseItem.getBaseItemType() == BaseItemType.Video) && TvApp.getApplication().getCurrentUser().getPolicy().getEnableContentDeletion())) {
deleteButton = new TextUnderButton(this, R.drawable.ic_trash, buttonSize, getString(R.string.lbl_delete), new View.OnClickListener() {
@Override
public void onClick(View v) {
@@ -1121,7 +1144,7 @@ public class FullDetailsActivity extends BaseActivity implements IRecordingIndic
mDetailsOverviewRow.addAction(deleteButton);
}
if (mSeriesTimerInfo != null && "SeriesTimer".equals(mBaseItem.getType())) {
if (mSeriesTimerInfo != null && mBaseItem.getBaseItemType() == BaseItemType.SeriesTimer) {
//Settings
mDetailsOverviewRow.addAction(new TextUnderButton(this, R.drawable.ic_settings, buttonSize, getString(R.string.lbl_series_settings), new View.OnClickListener() {
@Override
@@ -1207,7 +1230,7 @@ public class FullDetailsActivity extends BaseActivity implements IRecordingIndic
moreButton.setVisibility(View.GONE);
mDetailsOverviewRow.addAction(moreButton);
if (!"Episode".equals(mBaseItem.getType())) showMoreButtonIfNeeded(); //Episodes check for previous and then call this above
if (mBaseItem.getBaseItemType() != BaseItemType.Episode) showMoreButtonIfNeeded(); //Episodes check for previous and then call this above
}
@@ -1401,13 +1424,13 @@ public class FullDetailsActivity extends BaseActivity implements IRecordingIndic
protected void play(final BaseItemDto item, final int pos, final boolean shuffle) {
final Activity activity = this;
PlaybackHelper.getItemsToPlay(item, pos == 0 && item.getType().equals("Movie"), shuffle, new Response<List<BaseItemDto>>() {
PlaybackHelper.getItemsToPlay(item, pos == 0 && item.getBaseItemType() == BaseItemType.Movie, shuffle, new Response<List<BaseItemDto>>() {
@Override
public void onResponse(List<BaseItemDto> response) {
if ("MusicArtist".equals(item.getType())) {
if (item.getBaseItemType() == BaseItemType.MusicArtist) {
MediaManager.playNow(response);
} else {
Intent intent = new Intent(activity, mApplication.getPlaybackActivityClass(item.getType()));
Intent intent = new Intent(activity, mApplication.getPlaybackActivityClass(item.getBaseItemType()));
MediaManager.setCurrentVideoQueue(response);
intent.putExtra("Position", pos);
startActivity(intent);
@@ -1419,7 +1442,7 @@ public class FullDetailsActivity extends BaseActivity implements IRecordingIndic
protected void play(final BaseItemDto[] items, final int pos, final boolean shuffle) {
List<BaseItemDto> itemsToPlay = Arrays.asList(items);
Intent intent = new Intent(this, mApplication.getPlaybackActivityClass(items[0].getType()));
Intent intent = new Intent(this, mApplication.getPlaybackActivityClass(items[0].getBaseItemType()));
if (shuffle) Collections.shuffle(itemsToPlay);
MediaManager.setCurrentVideoQueue(itemsToPlay);
intent.putExtra("Position", pos);

View File

@@ -57,6 +57,7 @@ import java.util.List;
import org.jellyfin.apiclient.interaction.EmptyResponse;
import org.jellyfin.apiclient.interaction.Response;
import org.jellyfin.apiclient.model.dto.BaseItemDto;
import org.jellyfin.apiclient.model.dto.BaseItemType;
import org.jellyfin.apiclient.model.dto.UserItemDataDto;
import org.jellyfin.apiclient.model.library.PlayAccess;
import org.jellyfin.apiclient.model.playlists.PlaylistItemQuery;
@@ -153,7 +154,7 @@ public class ItemListActivity extends BaseActivity {
mItemList.setRowClickedListener(new ItemRowView.RowClickedListener() {
@Override
public void onRowClicked(ItemRowView row) {
showMenu(row, !"Audio".equals(row.getItem().getType()));
showMenu(row, row.getItem().getBaseItemType() != BaseItemType.Audio);
}
});
@@ -194,7 +195,6 @@ public class ItemListActivity extends BaseActivity {
BackgroundManager backgroundManager = BackgroundManager.getInstance(this);
backgroundManager.attach(getWindow());
backgroundManager.setDimLayer(getDrawable(R.drawable.left_fade));
mBackgroundTarget = new PicassoBackgroundManagerTarget(backgroundManager);
mItemId = getIntent().getStringExtra("ItemId");
@@ -331,7 +331,7 @@ public class ItemListActivity extends BaseActivity {
return true;
}
});
if ("Audio".equals(row.getItem().getType())) {
if (row.getItem().getBaseItemType() == BaseItemType.Audio) {
MenuItem mix = menu.getMenu().add(0, 1, order++, R.string.lbl_instant_mix);
mix.setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() {
@Override
@@ -357,7 +357,7 @@ public class ItemListActivity extends BaseActivity {
item.setOverview(getString(R.string.desc_automatic_fav_songs));
item.setPlayAccess(PlayAccess.Full);
item.setMediaType("Audio");
item.setType("Playlist");
item.setBaseItemType(BaseItemType.Playlist);
item.setIsFolder(true);
setBaseItem(item);
break;
@@ -368,7 +368,7 @@ public class ItemListActivity extends BaseActivity {
queue.setOverview(getString(R.string.desc_current_video_queue));
queue.setPlayAccess(PlayAccess.Full);
queue.setMediaType("Video");
queue.setType("Playlist");
queue.setBaseItemType(BaseItemType.Playlist);
queue.setIsFolder(true);
if (MediaManager.getCurrentVideoQueue() != null) {
long runtime = 0;
@@ -406,7 +406,7 @@ public class ItemListActivity extends BaseActivity {
updatePoster(mBaseItem);
//get items
if ("Playlist".equals(mBaseItem.getType())) {
if (mBaseItem.getBaseItemType() == BaseItemType.Playlist) {
// Have to use different query here
switch (mItemId) {
case FAV_SONGS:
@@ -516,7 +516,7 @@ public class ItemListActivity extends BaseActivity {
for (String genre : mBaseItem.getGenres()) {
if (!first) InfoLayoutHelper.addSpacer(this, layout, " / ", 14);
first = false;
layout.addView(new GenreButton(this, roboto, 16, genre, mBaseItem.getType()));
layout.addView(new GenreButton(this, roboto, 16, genre, mBaseItem.getBaseItemType()));
}
}
}
@@ -541,7 +541,7 @@ public class ItemListActivity extends BaseActivity {
private void play(List<BaseItemDto> items) {
if ("Video".equals(mBaseItem.getMediaType())) {
Intent intent = new Intent(mActivity, mApplication.getPlaybackActivityClass(mBaseItem.getType()));
Intent intent = new Intent(mActivity, mApplication.getPlaybackActivityClass(mBaseItem.getBaseItemType()));
//Resume first item if needed
BaseItemDto first = items.size() > 0 ? items.get(0) : null;
if (first != null && first.getUserData() != null) {
@@ -578,7 +578,7 @@ public class ItemListActivity extends BaseActivity {
@Override
public void onClick(View v) {
if (mItems.size() > 0) {
if (mBaseItem.getId().equals(VIDEO_QUEUE) || mBaseItem.getId().equals(FAV_SONGS)) {
if (mBaseItem.getId().equals(VIDEO_QUEUE) || mBaseItem.getId().equals(FAV_SONGS) || "Playlist".equals(mBaseItem.getType())) {
List<BaseItemDto> shuffled = new ArrayList<>(mItems);
Collections.shuffle(shuffled);
play(shuffled);
@@ -597,7 +597,7 @@ public class ItemListActivity extends BaseActivity {
}
}
if ("MusicAlbum".equals(mBaseItem.getType())) {
if (mBaseItem.getBaseItemType() == BaseItemType.MusicAlbum) {
TextUnderButton mix = new TextUnderButton(this, R.drawable.ic_mix, buttonSize, 2, getString(R.string.lbl_instant_mix), new View.OnClickListener() {
@Override
public void onClick(final View v) {
@@ -631,7 +631,7 @@ public class ItemListActivity extends BaseActivity {
}
if ("Playlist".equals(mBaseItem.getType())) {
if (mBaseItem.getBaseItemType() == BaseItemType.Playlist) {
if (VIDEO_QUEUE.equals(mBaseItem.getId())) {
mButtonRow.addView(new TextUnderButton(this, R.drawable.ic_save, buttonSize, 2, getString(R.string.lbl_save_as_playlist), new View.OnClickListener() {
@Override

View File

@@ -3,7 +3,7 @@ package org.jellyfin.androidtv.details;
import android.animation.Animator;
import android.os.Bundle;
import android.os.Handler;
import androidx.leanback.app.RowsFragment;
import androidx.leanback.app.RowsSupportFragment;
import androidx.leanback.widget.ArrayObjectAdapter;
import androidx.leanback.widget.HeaderItem;
import androidx.leanback.widget.ListRow;
@@ -56,7 +56,7 @@ public class PhotoPlayerActivity extends BaseActivity {
boolean isPlaying;
FrameLayout mPopupArea;
RowsFragment mPopupRowsFragment;
RowsSupportFragment mPopupRowsFragment;
ArrayObjectAdapter mPopupRowAdapter;
ListRow mThumbRow;
PositionableListRowPresenter mPopupRowPresenter;
@@ -95,13 +95,13 @@ public class PhotoPlayerActivity extends BaseActivity {
loadNext();
loadPrev();
// Inject the RowsFragment in the popup container
// Inject the RowsSupportFragment in the popup container
if (getFragmentManager().findFragmentById(R.id.rows_area) == null) {
mPopupRowsFragment = new RowsFragment();
getFragmentManager().beginTransaction()
mPopupRowsFragment = new RowsSupportFragment();
getSupportFragmentManager().beginTransaction()
.replace(R.id.rows_area, mPopupRowsFragment).commit();
} else {
mPopupRowsFragment = (RowsFragment) getFragmentManager()
mPopupRowsFragment = (RowsSupportFragment) getSupportFragmentManager()
.findFragmentById(R.id.rows_area);
}

View File

@@ -154,7 +154,7 @@ public class TvApiEventListener extends ApiEventListener {
switch (response.getItems()[0].getMediaType()) {
case "Video":
MediaManager.setCurrentVideoQueue(Arrays.asList(response.getItems()));
Intent intent = new Intent(TvApp.getApplication().getCurrentActivity(), TvApp.getApplication().getPlaybackActivityClass(response.getItems()[0].getType()));
Intent intent = new Intent(TvApp.getApplication().getCurrentActivity(), TvApp.getApplication().getPlaybackActivityClass(response.getItems()[0].getBaseItemType()));
TvApp.getApplication().getCurrentActivity().startActivity(intent);
break;
case "Audio":

View File

@@ -1,51 +0,0 @@
package org.jellyfin.androidtv.integration;
public class Recommendation {
private RecommendationType type;
private String itemId;
private Integer recId;
private long dateAdded;
public Recommendation(RecommendationType type, String id) {
itemId = id;
this.type = type;
this.dateAdded = System.currentTimeMillis();
}
public String getItemId() {
return itemId;
}
public void setItemId(String mItemId) {
this.itemId = mItemId;
}
public Integer getRecId() {
return recId;
}
public void setRecId(Integer mRecId) {
this.recId = mRecId;
}
public long getDateAdded() {
return dateAdded;
}
public RecommendationType getType() {
return type;
}
public void setType(RecommendationType mType) {
this.type = mType;
}
@Override
public boolean equals(Object o) {
if (o instanceof Recommendation) {
Recommendation compare = (Recommendation)o;
return compare.getType() == this.getType() && compare.getItemId().equals(this.getItemId());
}
return false;
}
}

View File

@@ -1,126 +0,0 @@
package org.jellyfin.androidtv.integration;
import android.app.Notification;
import android.app.PendingIntent;
import android.content.Context;
import android.graphics.Bitmap;
import android.net.Uri;
import android.os.Bundle;
import androidx.core.app.NotificationCompat;
import android.util.Log;
import org.jellyfin.androidtv.util.Utils;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
/**
* Created by Eric on 3/1/2015.
*/
public class RecommendationBuilder {
private static final String TAG = "RecommendationBuilder";
private Context mContext;
private int mId;
private int mPriority;
private int mSmallIcon;
private String mTitle;
private String mDescription;
private Bitmap mBitmap;
private String mBackgroundUri;
private PendingIntent mIntent;
public RecommendationBuilder() {
}
public RecommendationBuilder setContext(Context context) {
mContext = context;
return this;
}
public RecommendationBuilder setId(int id) {
mId = id;
return this;
}
public RecommendationBuilder setPriority(int priority) {
mPriority = priority;
return this;
}
public RecommendationBuilder setTitle(String title) {
mTitle = title;
return this;
}
public RecommendationBuilder setDescription(String description) {
mDescription = description;
return this;
}
public RecommendationBuilder setBitmap(Bitmap bitmap) {
mBitmap = bitmap;
return this;
}
public RecommendationBuilder setBackground(String uri) {
mBackgroundUri = uri;
return this;
}
public RecommendationBuilder setIntent(PendingIntent intent) {
mIntent = intent;
return this;
}
public RecommendationBuilder setSmallIcon(int resourceId) {
mSmallIcon = resourceId;
return this;
}
public Notification build() {
Log.d(TAG, "Building notification - " + this.toString());
Bundle extras = new Bundle();
if (mBackgroundUri != null) {
try {
Log.d(TAG, "Background - " + Uri.parse(RecommendationContentProvider.CONTENT_URI + URLEncoder.encode(mBackgroundUri, "UTF-8") + "/0").toString());
extras.putString(Notification.EXTRA_BACKGROUND_IMAGE_URI, Uri.parse(RecommendationContentProvider.CONTENT_URI + URLEncoder.encode(mBackgroundUri, "UTF-8")).toString());
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
}
Notification notification = new NotificationCompat.BigPictureStyle(
new NotificationCompat.Builder(mContext)
.setContentTitle(mTitle)
.setContentText(mDescription)
.setPriority(mPriority)
.setLocalOnly(true)
.setOngoing(true)
.setColor(Utils.getBrandColor(mContext))
.setCategory(Notification.CATEGORY_RECOMMENDATION)
.setLargeIcon(mBitmap)
.setSmallIcon(mSmallIcon)
.setContentIntent(mIntent)
.setExtras(extras))
.build();
return notification;
}
@Override
public String toString() {
return "RecommendationBuilder{" +
", mId=" + mId +
", mPriority=" + mPriority +
", mSmallIcon=" + mSmallIcon +
", mTitle='" + mTitle + '\'' +
", mDescription='" + mDescription + '\'' +
", mBitmap='" + mBitmap + '\'' +
", mBackgroundUri='" + mBackgroundUri + '\'' +
", mIntent=" + mIntent +
'}';
}}

View File

@@ -1,157 +0,0 @@
package org.jellyfin.androidtv.integration;
/**
* The MIT License (MIT)
* Copyright (c) 2014 David Carver
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be included
* in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
* OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS
* OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF
* OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
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 com.squareup.okhttp.OkHttpClient;
import com.squareup.okhttp.OkUrlFactory;
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 java.net.URLDecoder;
/**
* 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 RecommendationContentProvider extends ContentProvider {
public static String AUTHORITY = "org.jellyfin.androidtv.recommendations";
public static String CONTENT_URI = "content://" + AUTHORITY + "/";
@Override
public boolean onCreate() {
return true;
}
@Override
public ParcelFileDescriptor openFile(Uri uri, String mode)
throws FileNotFoundException {
ParcelFileDescriptor[] pipe = null;
String url = uri.getPath();
try {
String decodedUrl = URLDecoder.decode(url.replaceFirst("/", ""),
"UTF-8");
pipe = ParcelFileDescriptor.createPipe();
// This uses OKHttp to
OkHttpClient httpClient = new OkHttpClient();
HttpURLConnection connection = new OkUrlFactory(httpClient).open(new URL(decodedUrl));
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 "image/*";
}
@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);
}
in.close();
out.flush();
out.close();
} catch (IOException e) {
Log.e(getClass().getSimpleName(),
"Exception transferring file", e);
}
}
}
}

View File

@@ -1,332 +0,0 @@
package org.jellyfin.androidtv.integration;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.TaskStackBuilder;
import android.content.Context;
import android.content.Intent;
import android.os.AsyncTask;
import android.os.Build;
import org.jellyfin.androidtv.R;
import org.jellyfin.androidtv.TvApp;
import org.jellyfin.androidtv.browsing.MainActivity;
import org.jellyfin.androidtv.querying.StdItemQuery;
import org.jellyfin.androidtv.startup.StartupActivity;
import org.jellyfin.androidtv.util.ImageUtils;
import org.jellyfin.androidtv.util.Utils;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import org.jellyfin.apiclient.interaction.Response;
import org.jellyfin.apiclient.model.dto.BaseItemDto;
import org.jellyfin.apiclient.model.entities.LocationType;
import org.jellyfin.apiclient.model.entities.SortOrder;
import org.jellyfin.apiclient.model.querying.ItemFields;
import org.jellyfin.apiclient.model.querying.ItemFilter;
import org.jellyfin.apiclient.model.querying.ItemSortBy;
import org.jellyfin.apiclient.model.querying.ItemsResult;
import org.jellyfin.apiclient.model.querying.NextUpQuery;
import org.jellyfin.apiclient.model.querying.SimilarItemsQuery;
/**
* Created by Eric on 3/1/2015.
*/
public class RecommendationManager {
private final String REC_FILE_NAME = "reccomendations.json";
private final Integer MAX_TV_RECS = 3;
private final Integer MAX_MOVIE_RECS = 4;
private boolean isEnabled;
private static RecommendationManager instance;
private Recommendations mRecommendations;
public RecommendationManager() {
isEnabled = Build.VERSION.SDK_INT >= 21;
mRecommendations = loadRecs();
if (isEnabled) {
validate();
} else {
TvApp.getApplication().getLogger().Info("Recommendations not enabled on this device");
}
}
public static RecommendationManager getInstance() {
if (instance == null) instance = new RecommendationManager();
return instance;
}
public static void init() {
if (instance == null) {
getInstance();
} else {
instance.validate();
}
}
private Recommendations loadRecs() {
if (isEnabled) {
try {
InputStream recFile = TvApp.getApplication().openFileInput(REC_FILE_NAME);
String json = Utils.readStringFromStream(recFile);
recFile.close();
return (Recommendations) TvApp.getApplication().getSerializer().DeserializeFromString(json, Recommendations.class);
} catch (IOException e) {
// none saved
return new Recommendations(TvApp.getApplication().getApiClient().getServerInfo().getId(), TvApp.getApplication().getCurrentUser().getId());
}
} else {
return new Recommendations(TvApp.getApplication().getApiClient().getServerInfo().getId(), TvApp.getApplication().getCurrentUser().getId());
}
}
public boolean validate() {
if (isEnabled) {
//Now validate that these are for this server and user.
if (mRecommendations == null || mRecommendations.getServerId() == null || TvApp.getApplication().getApiClient().getServerInfo() == null || !mRecommendations.getServerId().equals(TvApp.getApplication().getApiClient().getServerInfo().getId())
|| mRecommendations.getUserId() == null || TvApp.getApplication().getCurrentUser() == null || !mRecommendations.getUserId().equals(TvApp.getApplication().getCurrentUser().getId())) {
//Nope - clear them out and start over for this user
clearAll();
createAll();
TvApp.getApplication().getLogger().Info("Recommendations re-set for user "+TvApp.getApplication().getCurrentUser().getName());
return false;
}
createAll();
TvApp.getApplication().getLogger().Info("Recommendations re-created for user "+TvApp.getApplication().getCurrentUser().getName());
}
return true;
}
private void saveRecs() {
if (isEnabled) {
try {
OutputStream recFile = TvApp.getApplication().openFileOutput(REC_FILE_NAME, Context.MODE_PRIVATE);
recFile.write(TvApp.getApplication().getSerializer().SerializeToString(mRecommendations).getBytes());
recFile.close();
} catch (IOException e) {
TvApp.getApplication().getLogger().ErrorException("Error saving recommendations",e);
}
}
}
public void clearAll() {
NotificationManager nm = (NotificationManager)
TvApp.getApplication().getSystemService(Context.NOTIFICATION_SERVICE);
nm.cancelAll();
mRecommendations = new Recommendations(TvApp.getApplication().getApiClient().getServerInfo().getId(), TvApp.getApplication().getCurrentUser().getId());
saveRecs();
}
public void createAll() {
if (isEnabled) {
//Create recs for next up tv and movies
NextUpQuery nextUpQuery = new NextUpQuery();
nextUpQuery.setUserId(TvApp.getApplication().getCurrentUser().getId());
nextUpQuery.setLimit(MAX_TV_RECS);
nextUpQuery.setFields(new ItemFields[] {ItemFields.PrimaryImageAspectRatio});
TvApp.getApplication().getApiClient().GetNextUpEpisodesAsync(nextUpQuery, new Response<ItemsResult>() {
@Override
public void onResponse(ItemsResult response) {
if (response.getTotalRecordCount() > 0) {
for (BaseItemDto episode : response.getItems()) {
if (episode.getLocationType() != LocationType.Virtual) addRecommendation(episode, RecommendationType.Tv);
}
}
}
});
//First try for resumables
StdItemQuery resumeMovies = new StdItemQuery();
resumeMovies.setIncludeItemTypes(new String[]{"Movie"});
resumeMovies.setRecursive(true);
resumeMovies.setLimit(2);
resumeMovies.setFilters(new ItemFilter[]{ItemFilter.IsResumable});
resumeMovies.setSortBy(new String[]{ItemSortBy.DatePlayed});
resumeMovies.setSortOrder(SortOrder.Descending);
TvApp.getApplication().getApiClient().GetItemsAsync(resumeMovies, new Response<ItemsResult>() {
@Override
public void onResponse(ItemsResult response) {
int movieItems = 0;
if (response.getTotalRecordCount() > 0) {
for (BaseItemDto movie : response.getItems()) {
recommend(movie.getId());
movieItems++;
}
}
if (movieItems < MAX_MOVIE_RECS) {
//Now fill in with latest movies
StdItemQuery suggMovies = new StdItemQuery();
suggMovies.setIncludeItemTypes(new String[]{"Movie"});
suggMovies.setRecursive(true);
suggMovies.setLimit(MAX_MOVIE_RECS - movieItems);
suggMovies.setFilters(new ItemFilter[]{ItemFilter.IsUnplayed});
suggMovies.setSortBy(new String[]{ItemSortBy.DateCreated});
suggMovies.setSortOrder(SortOrder.Descending);
TvApp.getApplication().getApiClient().GetItemsAsync(suggMovies, new Response<ItemsResult>() {
@Override
public void onResponse(ItemsResult suggResponse) {
if (suggResponse.getTotalRecordCount() > 0) {
for (BaseItemDto movie : suggResponse.getItems()) {
addRecommendation(movie, RecommendationType.Movie);
}
}
}
});
}
}
});
}
}
public void recommend(final String itemId) {
if (isEnabled) {
if (itemId == null) {
TvApp.getApplication().getLogger().Error("Attempt to recommend null Item");
return;
}
//No matter what it is, if it is resumable, recommend this item (need to re-retrieve for current user data)
TvApp.getApplication().getApiClient().GetItemAsync(itemId, TvApp.getApplication().getCurrentUser().getId(), new Response<BaseItemDto>() {
@Override
public void onResponse(BaseItemDto response) {
if (response == null) {
TvApp.getApplication().getLogger().Error("No item found with ID: "+itemId);
return;
}
if (response.getCanResume()) {
addRecommendation(response, RecommendationType.Movie);
} else {
switch (response.getType()) {
case "Movie":
//First remove us if we were a recommendation
mRecommendations.remove(RecommendationType.Movie, response.getId());
//Suggest a similar movie
SimilarItemsQuery similar = new SimilarItemsQuery();
similar.setId(response.getId());
similar.setLimit(1);
similar.setUserId(TvApp.getApplication().getCurrentUser().getId());
TvApp.getApplication().getApiClient().GetSimilarItems(similar, new Response<ItemsResult>() {
@Override
public void onResponse(ItemsResult similarResponse) {
if (similarResponse.getTotalRecordCount() > 0) {
addRecommendation(similarResponse.getItems()[0], RecommendationType.Movie);
}
}
@Override
public void onError(Exception exception) {
TvApp.getApplication().getLogger().ErrorException("Error retrieving item for recommendation", exception);
}
});
break;
case "Episode":
//First remove us if we were a recommendation
mRecommendations.remove(RecommendationType.Tv, response.getId());
//Suggest the next up episode
NextUpQuery next = new NextUpQuery();
next.setSeriesId(response.getSeriesId());
next.setUserId(TvApp.getApplication().getCurrentUser().getId());
next.setLimit(1);
TvApp.getApplication().getApiClient().GetNextUpEpisodesAsync(next, new Response<ItemsResult>() {
@Override
public void onResponse(ItemsResult nextResponse) {
if (nextResponse.getTotalRecordCount() > 0 && nextResponse.getItems()[0].getLocationType() != LocationType.Virtual) {
addRecommendation(nextResponse.getItems()[0], RecommendationType.Tv);
}
}
@Override
public void onError(Exception exception) {
TvApp.getApplication().getLogger().ErrorException("Error retrieving item for recommendation", exception);
}
});
break;
}
}
}
@Override
public void onError(Exception exception) {
TvApp.getApplication().getLogger().ErrorException("Error retrieving item for recommendation", exception);
}
});
}
}
public boolean addRecommendation(BaseItemDto item, RecommendationType type) {
if (isEnabled) {
//No need if we are already there
if (mRecommendations.get(type, item.getId()) != null) return false;
//Not so build one
new AsyncRunner().execute(item, type);
return true;
} else {
return false;
}
}
private class AsyncRunner extends AsyncTask<Object, Integer, Boolean> {
@Override
protected Boolean doInBackground(Object... params) {
BaseItemDto item = (BaseItemDto) params[0];
RecommendationType type = (RecommendationType) params[1];
Recommendation rec = new Recommendation(type, item.getId());
rec.setRecId(mRecommendations.getRecId(type, type == RecommendationType.Movie ? MAX_MOVIE_RECS : MAX_TV_RECS));
RecommendationBuilder builder = new RecommendationBuilder()
.setContext(TvApp.getApplication())
.setSmallIcon(R.drawable.app_icon_medium);
Notification recommendation = builder
.setId(rec.getRecId())
.setPriority(0)
.setTitle(item.getName())
.setDescription(item.getOverview())
.setBitmap(Utils.getBitmapFromURL(ImageUtils.getPrimaryImageUrl(item, TvApp.getApplication().getApiClient(), true, true, false, 300)))
.setBackground(ImageUtils.getBackdropImageUrl(item, TvApp.getApplication().getApiClient(), true))
.setIntent(buildPendingIntent(item))
.build();
mRecommendations.add(rec);
saveRecs();
NotificationManager notificationManager = (NotificationManager)
TvApp.getApplication().getSystemService(Context.NOTIFICATION_SERVICE);
notificationManager.notify(rec.getRecId(), recommendation);
return true;
}
}
private PendingIntent buildPendingIntent(BaseItemDto item) {
Intent directIntent = new Intent(TvApp.getApplication(), StartupActivity.class);
directIntent.putExtra("ItemId", item.getId());
directIntent.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY);
TaskStackBuilder stackBuilder = TaskStackBuilder.create(TvApp.getApplication());
stackBuilder.addParentStack(MainActivity.class);
stackBuilder.addNextIntent(directIntent);
// Ensure a unique PendingIntents, otherwise all recommendations end up with the same
// PendingIntent
directIntent.setAction(item.getId());
return stackBuilder.getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT);
}
}

View File

@@ -1,6 +0,0 @@
package org.jellyfin.androidtv.integration;
public enum RecommendationType {
Movie,
Tv
}

View File

@@ -1,129 +0,0 @@
package org.jellyfin.androidtv.integration;
import android.app.NotificationManager;
import android.content.Context;
import org.jellyfin.androidtv.TvApp;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
/**
* Created by Eric on 3/1/2015.
*/
public class Recommendations {
private String serverId;
private String userId;
private List<Recommendation> tvRecommendations = new ArrayList<>();
private List<Recommendation> movieRecommendations = new ArrayList<>();
public Recommendations(String serverId, String userId) {
this.userId = userId;
this.serverId = serverId;
}
public List<Recommendation> getTvRecommendations() {
return tvRecommendations;
}
public void setTvRecommendations(List<Recommendation> tvRecommendations) {
this.tvRecommendations = tvRecommendations;
}
public List<Recommendation> getMovieRecommendations() {
return movieRecommendations;
}
public void setMovieRecommendations(List<Recommendation> movieRecommendations) {
this.movieRecommendations = movieRecommendations;
}
public Recommendation get(RecommendationType type, String id) {
Recommendation compare = new Recommendation(type, id);
switch (type) {
case Movie:
return movieRecommendations.contains(compare) ? movieRecommendations.get(movieRecommendations.indexOf(compare)) : null;
case Tv:
return tvRecommendations.contains(compare) ? tvRecommendations.get(tvRecommendations.indexOf(compare)) : null;
}
return null;
}
public boolean add(Recommendation rec) {
switch (rec.getType()) {
case Movie:
movieRecommendations.add(rec);
break;
case Tv:
tvRecommendations.add(rec);
break;
}
return true;
}
public Integer getRecId(RecommendationType type, int max) {
switch (type) {
case Movie:
if (movieRecommendations.size() < max) return 100 + movieRecommendations.size()+1;
return replaceOldest(type);
case Tv:
if (tvRecommendations.size() < max) return 200 + tvRecommendations.size()+1;
return replaceOldest(type);
}
throw new IllegalArgumentException("type");
}
private Integer replaceOldest(RecommendationType type) {
List<Recommendation> list = type == RecommendationType.Movie ? movieRecommendations : tvRecommendations;
Recommendation oldest = Collections.min(list, new Comparator<Recommendation>() {
@Override
public int compare(Recommendation lhs, Recommendation rhs) {
return Long.compare(lhs.getDateAdded(), rhs.getDateAdded());
}
});
list.remove(oldest);
((NotificationManager) TvApp.getApplication().getSystemService(Context.NOTIFICATION_SERVICE)).cancel(oldest.getRecId());
return oldest.getRecId();
}
public boolean remove(RecommendationType type, String id) {
List<Recommendation> list = type == RecommendationType.Movie ? movieRecommendations : tvRecommendations;
Recommendation existing = get(type, id);
if (existing != null) {
list.remove(existing);
((NotificationManager) TvApp.getApplication().getSystemService(Context.NOTIFICATION_SERVICE)).cancel(existing.getRecId());
return true;
}
return false;
}
public String getUserId() {
return userId;
}
public void setUserId(String userId) {
this.userId = userId;
}
public String getServerId() {
return serverId;
}
public void setServerId(String serverId) {
this.serverId = serverId;
}
}

View File

@@ -13,6 +13,7 @@ import org.jellyfin.androidtv.util.Utils;
import org.jellyfin.androidtv.util.apiclient.BaseItemUtils;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Date;
import org.jellyfin.apiclient.interaction.EmptyResponse;
@@ -20,6 +21,7 @@ import org.jellyfin.apiclient.interaction.Response;
import org.jellyfin.apiclient.model.apiclient.ServerInfo;
import org.jellyfin.apiclient.model.dto.BaseItemDto;
import org.jellyfin.apiclient.model.dto.BaseItemPerson;
import org.jellyfin.apiclient.model.dto.BaseItemType;
import org.jellyfin.apiclient.model.dto.UserDto;
import org.jellyfin.apiclient.model.entities.ImageType;
import org.jellyfin.apiclient.model.livetv.ChannelInfoDto;
@@ -54,7 +56,7 @@ public class BaseRowItem {
public BaseRowItem(int index, BaseItemDto item, boolean preferParentThumb, boolean staticHeight, SelectAction selectAction) {
this.index = index;
this.baseItem = item;
this.type = item.getType().equals("Program") ? ItemType.LiveTvProgram : item.getType().equals("Recording") ? ItemType.LiveTvRecording : ItemType.BaseItem;
this.type = item.getBaseItemType() == BaseItemType.Program ? ItemType.LiveTvProgram : item.getBaseItemType() == BaseItemType.Recording ? ItemType.LiveTvRecording : ItemType.BaseItem;
this.preferParentThumb = preferParentThumb;
this.staticHeight = staticHeight;
this.selectAction = selectAction;
@@ -124,31 +126,76 @@ public class BaseRowItem {
return baseItem;
}
public BaseItemPerson getPerson() { return person; }
public ChapterItemInfo getChapterInfo() { return chapterInfo; }
public ServerInfo getServerInfo() { return serverInfo; }
public UserDto getUser() { return user; }
public SearchHint getSearchHint() { return searchHint; }
public ChannelInfoDto getChannelInfo() { return channelInfo; }
public BaseItemDto getProgramInfo() { return baseItem; }
public BaseItemDto getRecordingInfo() { return baseItem; }
public SeriesTimerInfoDto getSeriesTimerInfo() { return seriesTimerInfo; }
public GridButton getGridButton() { return gridButton; }
public boolean isChapter() { return type == ItemType.Chapter; }
public boolean isPerson() { return type == ItemType.Person; }
public boolean isBaseItem() { return type == ItemType.BaseItem; }
public boolean getPreferParentThumb() { return preferParentThumb; }
public ItemType getItemType() { return type; }
public BaseItemPerson getPerson() {
return person;
}
public ChapterItemInfo getChapterInfo() {
return chapterInfo;
}
public ServerInfo getServerInfo() {
return serverInfo;
}
public UserDto getUser() {
return user;
}
public SearchHint getSearchHint() {
return searchHint;
}
public ChannelInfoDto getChannelInfo() {
return channelInfo;
}
public BaseItemDto getProgramInfo() {
return baseItem;
}
public BaseItemDto getRecordingInfo() {
return baseItem;
}
public SeriesTimerInfoDto getSeriesTimerInfo() {
return seriesTimerInfo;
}
public GridButton getGridButton() {
return gridButton;
}
public boolean isChapter() {
return type == ItemType.Chapter;
}
public boolean isPerson() {
return type == ItemType.Person;
}
public boolean isBaseItem() {
return type == ItemType.BaseItem;
}
public boolean getPreferParentThumb() {
return preferParentThumb;
}
public ItemType getItemType() {
return type;
}
public boolean isFolder() {
return type == ItemType.BaseItem && baseItem != null && baseItem.getIsFolderItem();
}
public boolean showCardInfoOverlay() {return type == ItemType.BaseItem && baseItem != null
&& ("Folder".equals(baseItem.getType()) || "PhotoAlbum".equals(baseItem.getType()) || "RecordingGroup".equals(baseItem.getType())
|| "UserView".equals(baseItem.getType()) || "CollectionFolder".equals(baseItem.getType()) || "Photo".equals(baseItem.getType())
|| "Video".equals(baseItem.getType()) || "Person".equals(baseItem.getType()) || "Playlist".equals(baseItem.getType())
|| "MusicArtist".equals(baseItem.getType()));
public boolean showCardInfoOverlay() {
return type == ItemType.BaseItem && baseItem != null
&& Arrays.asList(BaseItemType.Folder, BaseItemType.PhotoAlbum, BaseItemType.RecordingGroup,
BaseItemType.UserView, BaseItemType.CollectionFolder, BaseItemType.Photo,
BaseItemType.Video, BaseItemType.Person, BaseItemType.Playlist,
BaseItemType.MusicArtist).contains(baseItem.getBaseItemType());
}
public boolean isValid() {
@@ -180,8 +227,8 @@ public class BaseRowItem {
default:
return getPrimaryImageUrl(maxHeight);
}
default:
return getPrimaryImageUrl(maxHeight);
default:
return getPrimaryImageUrl(maxHeight);
}
}
@@ -206,8 +253,11 @@ public class BaseRowItem {
case SeriesTimer:
return ImageUtils.getResourceUrl(R.drawable.tile_land_series_timer);
case SearchHint:
return Utils.isNonEmpty(searchHint.getPrimaryImageTag()) ? ImageUtils.getImageUrl(searchHint.getItemId(), ImageType.Primary, searchHint.getPrimaryImageTag(), TvApp.getApplication().getApiClient()) :
Utils.isNonEmpty(searchHint.getThumbImageItemId()) ? ImageUtils.getImageUrl(searchHint.getThumbImageItemId(), ImageType.Thumb, searchHint.getThumbImageTag(), TvApp.getApplication().getApiClient()) : null;
if (Utils.isNonEmpty(searchHint.getPrimaryImageTag())) {
return ImageUtils.getImageUrl(searchHint.getItemId(), ImageType.Primary, searchHint.getPrimaryImageTag(), TvApp.getApplication().getApiClient());
} else if (Utils.isNonEmpty(searchHint.getThumbImageItemId())) {
return ImageUtils.getImageUrl(searchHint.getThumbImageItemId(), ImageType.Thumb, searchHint.getThumbImageTag(), TvApp.getApplication().getApiClient());
}
}
return null;
}
@@ -253,7 +303,9 @@ public class BaseRowItem {
public String getCardName() {
switch (type) {
case BaseItem:
if ("Audio".equals(baseItem.getType())) return baseItem.getAlbumArtist() != null ? baseItem.getAlbumArtist() : baseItem.getAlbum() != null ? baseItem.getAlbum() : "<Unknown>";
if (baseItem.getBaseItemType() == BaseItemType.Audio) {
return baseItem.getAlbumArtist() != null ? baseItem.getAlbumArtist() : baseItem.getAlbum() != null ? baseItem.getAlbum() : "<Unknown>";
}
default:
return getFullName();
}
@@ -291,7 +343,7 @@ public class BaseRowItem {
case BaseItem:
case LiveTvRecording:
case LiveTvProgram:
return "Audio".equals(baseItem.getType())? getFullName() : baseItem.getName();
return baseItem.getBaseItemType() == BaseItemType.Audio ? getFullName() : baseItem.getName();
case Person:
return person.getName();
case Server:
@@ -372,29 +424,11 @@ public class BaseRowItem {
return "";
}
public String getType() {
switch (type) {
case BaseItem:
case LiveTvRecording:
case LiveTvProgram:
return baseItem.getType();
case Person:
return person.getType();
case Server:
case User:
case Chapter:
break;
case SearchHint:
return searchHint.getType();
case LiveTvChannel:
return channelInfo.getType();
case GridButton:
return "GridButton";
case SeriesTimer:
return "SeriesTimer";
}
return "";
public BaseItemType getBaseItemType() {
if (baseItem != null)
return baseItem.getBaseItemType();
else
return null;
}
public String getSummary() {
@@ -441,7 +475,7 @@ public class BaseRowItem {
public int getChildCount() {
switch (type) {
case BaseItem:
return isFolder() && !"MusicArtist".equals(baseItem.getType()) && baseItem.getChildCount() != null ? baseItem.getChildCount() : -1;
return isFolder() && baseItem.getBaseItemType() != BaseItemType.MusicArtist && baseItem.getChildCount() != null ? baseItem.getChildCount() : -1;
case Person:
case Server:
case User:
@@ -458,7 +492,7 @@ public class BaseRowItem {
}
public String getChildCountStr() {
if (baseItem != null && "Playlist".equals(baseItem.getType()) && baseItem.getCumulativeRunTimeTicks() != null) {
if (baseItem != null && baseItem.getBaseItemType() == BaseItemType.Playlist && baseItem.getCumulativeRunTimeTicks() != null) {
return TimeUtils.formatMillis(baseItem.getCumulativeRunTimeTicks() / 10000);
} else {
Integer count = getChildCount();
@@ -478,9 +512,9 @@ public class BaseRowItem {
public Drawable getBadgeImage() {
switch (type) {
case BaseItem:
if (baseItem.getType().equals("Movie") && baseItem.getCriticRating() != null) {
if (baseItem.getBaseItemType() == BaseItemType.Movie && baseItem.getCriticRating() != null) {
return baseItem.getCriticRating() > 59 ? TvApp.getApplication().getDrawableCompat(R.drawable.fresh) : TvApp.getApplication().getDrawableCompat(R.drawable.rotten);
} else if (baseItem.getType().equals("Program") && baseItem.getTimerId() != null) {
} else if (baseItem.getBaseItemType() == BaseItemType.Program && baseItem.getTimerId() != null) {
return baseItem.getSeriesTimerId() != null ? TvApp.getApplication().getDrawableCompat(R.drawable.ic_record_series_red) : TvApp.getApplication().getDrawableCompat(R.drawable.ic_record_red);
}
break;
@@ -554,7 +588,8 @@ public class BaseRowItem {
LiveTvRecording,
GridButton,
SeriesTimer,
LiveTvProgram
LiveTvProgram,
}
public enum SelectAction {

View File

@@ -30,6 +30,7 @@ import java.util.List;
import org.jellyfin.apiclient.interaction.Response;
import org.jellyfin.apiclient.model.dto.BaseItemDto;
import org.jellyfin.apiclient.model.dto.BaseItemType;
import org.jellyfin.apiclient.model.dto.UserDto;
import org.jellyfin.apiclient.model.entities.DisplayPreferences;
import org.jellyfin.apiclient.model.library.PlayAccess;
@@ -50,15 +51,15 @@ public class ItemLauncher {
case BaseItem:
final BaseItemDto baseItem = rowItem.getBaseItem();
try {
TvApp.getApplication().getLogger().Debug("Item selected: " + rowItem.getIndex() + " - " + baseItem.getName() + " (" + baseItem.getType() + ")");
TvApp.getApplication().getLogger().Debug("Item selected: " + rowItem.getIndex() + " - " + baseItem.getName() + " (" + baseItem.getBaseItemType() + ")");
} catch (Exception e) {
//swallow it
}
//specialized type handling
switch (baseItem.getType()) {
case "UserView":
case "CollectionFolder":
switch (baseItem.getBaseItemType()) {
case UserView:
case CollectionFolder:
//We need to get display prefs...
TvApp.getApplication().getDisplayPrefsAsync(baseItem.getDisplayPreferencesId(), new Response<DisplayPreferences>() {
@Override
@@ -104,8 +105,8 @@ public class ItemLauncher {
}
});
return;
case "Series":
case "MusicArtist":
case Series:
case MusicArtist:
//Start activity for details display
Intent intent = new Intent(activity, FullDetailsActivity.class);
intent.putExtra("ItemId", baseItem.getId());
@@ -117,8 +118,8 @@ public class ItemLauncher {
return;
case "MusicAlbum":
case "Playlist":
case MusicAlbum:
case Playlist:
//Start activity for song list display
Intent songListIntent = new Intent(activity, ItemListActivity.class);
songListIntent.putExtra("ItemId", baseItem.getId());
@@ -130,13 +131,13 @@ public class ItemLauncher {
return;
case "Audio":
case Audio:
//produce item menu
KeyProcessor.HandleKey(KeyEvent.KEYCODE_MENU, rowItem, (BaseActivity) activity);
return;
case "Season":
case "RecordingGroup":
case Season:
case RecordingGroup:
//Start activity for enhanced browse
Intent seasonIntent = new Intent(activity, GenericFolderActivity.class);
seasonIntent.putExtra("Folder", TvApp.getApplication().getSerializer().SerializeToString(baseItem));
@@ -148,7 +149,7 @@ public class ItemLauncher {
return;
case "BoxSet":
case BoxSet:
// open collection browsing
Intent collectionIntent = new Intent(activity, CollectionActivity.class);
collectionIntent.putExtra("Folder", TvApp.getApplication().getSerializer().SerializeToString(baseItem));
@@ -159,7 +160,7 @@ public class ItemLauncher {
activity.startActivity(collectionIntent);
return;
case "Photo":
case Photo:
// open photo player
MediaManager.setCurrentMediaPosition(pos);
Intent photoIntent = new Intent(activity, PhotoPlayerActivity.class);
@@ -200,10 +201,10 @@ public class ItemLauncher {
case Play:
if (baseItem.getPlayAccess() == PlayAccess.Full) {
//Just play it directly
PlaybackHelper.getItemsToPlay(baseItem, baseItem.getType().equals("Movie"), false, new Response<List<BaseItemDto>>() {
PlaybackHelper.getItemsToPlay(baseItem, baseItem.getBaseItemType() == BaseItemType.Movie, false, new Response<List<BaseItemDto>>() {
@Override
public void onResponse(List<BaseItemDto> response) {
Intent intent = new Intent(activity, application.getPlaybackActivityClass(baseItem.getType()));
Intent intent = new Intent(activity, application.getPlaybackActivityClass(baseItem.getBaseItemType()));
MediaManager.setCurrentVideoQueue(response);
intent.putExtra("Position", 0);
activity.startActivity(intent);
@@ -236,7 +237,7 @@ public class ItemLauncher {
List<BaseItemDto> items = new ArrayList<>();
items.add(response);
MediaManager.setCurrentVideoQueue(items);
Intent intent = new Intent(activity, application.getPlaybackActivityClass(response.getType()));
Intent intent = new Intent(activity, application.getPlaybackActivityClass(response.getBaseItemType()));
Long start = chapter.getStartPositionTicks() / 10000;
intent.putExtra("Position", start.intValue());
activity.startActivity(intent);
@@ -265,14 +266,14 @@ public class ItemLauncher {
application.getApiClient().GetItemAsync(hint.getItemId(), application.getCurrentUser().getId(), new Response<BaseItemDto>() {
@Override
public void onResponse(BaseItemDto response) {
if ((response.getIsFolderItem() && !"Series".equals(response.getType())) || "MusicArtist".equals(response.getType())) {
if ((response.getIsFolderItem() && response.getBaseItemType() != BaseItemType.Series) || response.getBaseItemType() == BaseItemType.MusicArtist) {
// open generic folder browsing
Intent intent = new Intent(activity, GenericGridActivity.class);
intent.putExtra("Folder", TvApp.getApplication().getSerializer().SerializeToString(response));
activity.startActivity(intent);
} else if ("Audio".equals(response.getType())) {
} else if (response.getBaseItemType() == BaseItemType.Audio) {
PlaybackHelper.retrieveAndPlay(response.getId(), false, activity);
//produce item menu
// KeyProcessor.HandleKey(KeyEvent.KEYCODE_MENU, rowItem, (BaseActivity) activity);
@@ -281,8 +282,10 @@ public class ItemLauncher {
} else {
Intent intent = new Intent(activity, FullDetailsActivity.class);
intent.putExtra("ItemId", response.getId());
if (response.getType().equals("Program")) {
intent.putExtra("ItemType", response.getType());
if (response.getBaseItemType() == BaseItemType.Program) {
// TODO: Seems like this is never used...
intent.putExtra("ItemType", response.getBaseItemType().name());
intent.putExtra("ChannelId", response.getChannelId());
intent.putExtra("ProgramInfo", TvApp.getApplication().getSerializer().SerializeToString(response));
}
@@ -305,7 +308,10 @@ public class ItemLauncher {
//Start details fragment for display and playback
Intent programIntent = new Intent(activity, FullDetailsActivity.class);
programIntent.putExtra("ItemId", program.getId());
programIntent.putExtra("ItemType", program.getType());
// TODO Seems unused
programIntent.putExtra("ItemType", program.getBaseItemType().name());
programIntent.putExtra("ChannelId", program.getChannelId());
programIntent.putExtra("ProgramInfo", TvApp.getApplication().getSerializer().SerializeToString(program));
@@ -319,7 +325,7 @@ public class ItemLauncher {
public void onResponse(BaseItemDto response) {
List<BaseItemDto> items = new ArrayList<>();
items.add(response);
Intent intent = new Intent(activity, TvApp.getApplication().getPlaybackActivityClass(response.getType()));
Intent intent = new Intent(activity, TvApp.getApplication().getPlaybackActivityClass(response.getBaseItemType()));
MediaManager.setCurrentVideoQueue(items);
intent.putExtra("Position", 0);
activity.startActivity(intent);
@@ -341,7 +347,8 @@ public class ItemLauncher {
PlaybackHelper.getItemsToPlay(response, false, false, new Response<List<BaseItemDto>>() {
@Override
public void onResponse(List<BaseItemDto> response) {
Intent intent = new Intent(activity, application.getPlaybackActivityClass(channel.getType()));
// TODO Check whether this usage of BaseItemType.valueOf is okay.
Intent intent = new Intent(activity, application.getPlaybackActivityClass(BaseItemType.valueOf(channel.getType())));
MediaManager.setCurrentVideoQueue(response);
intent.putExtra("Position", 0);
activity.startActivity(intent);
@@ -368,7 +375,7 @@ public class ItemLauncher {
TvApp.getApplication().getApiClient().GetItemAsync(rowItem.getRecordingInfo().getId(), TvApp.getApplication().getCurrentUser().getId(), new Response<BaseItemDto>() {
@Override
public void onResponse(BaseItemDto response) {
Intent intent = new Intent(activity, application.getPlaybackActivityClass(rowItem.getType()));
Intent intent = new Intent(activity, application.getPlaybackActivityClass(rowItem.getBaseItemType()));
List<BaseItemDto> items = new ArrayList<>();
items.add(response);
MediaManager.setCurrentVideoQueue(items);

View File

@@ -38,6 +38,7 @@ import org.jellyfin.apiclient.interaction.Response;
import org.jellyfin.apiclient.model.apiclient.ServerInfo;
import org.jellyfin.apiclient.model.dto.BaseItemDto;
import org.jellyfin.apiclient.model.dto.BaseItemPerson;
import org.jellyfin.apiclient.model.dto.BaseItemType;
import org.jellyfin.apiclient.model.dto.UserDto;
import org.jellyfin.apiclient.model.livetv.ChannelInfoDto;
import org.jellyfin.apiclient.model.livetv.LiveTvChannelQuery;
@@ -812,7 +813,7 @@ public class ItemRowAdapter extends ArrayObjectAdapter {
for (BaseItemDto item : response.getItems()) {
//re-map the display prefs id to our actual id
item.setDisplayPreferencesId(item.getId());
if (!ignoreTypeList.contains(item.getCollectionType()) && !ignoreTypeList.contains(item.getType())) {
if (!ignoreTypeList.contains(item.getCollectionType())) {
adapter.add(new BaseRowItem(i++, item, preferParentThumb, staticHeight));
}
}
@@ -860,12 +861,10 @@ public class ItemRowAdapter extends ArrayObjectAdapter {
}
totalItems = response.getTotalRecordCount();
setItemsLoaded(itemsLoaded + i);
if (itemsLoaded > 0 && mParent != null) {
mParent.add(mRow);
}
}
currentlyRetrieving = false;
notifyRetrieveFinished();
}
@Override
@@ -873,11 +872,18 @@ public class ItemRowAdapter extends ArrayObjectAdapter {
TvApp.getApplication().getLogger().ErrorException("Error retrieving search results", exception);
Utils.showToast(TvApp.getApplication(), exception.getLocalizedMessage());
currentlyRetrieving = false;
notifyRetrieveFinished();
}
});
}
public void addToParentIfResultsReceived() {
if (itemsLoaded > 0 && mParent != null) {
mParent.add(mRow);
}
}
public void GetResultSizeAsync(final Response<Integer> outerResponse) {
switch (queryType) {
case AlbumArtists:
@@ -1267,11 +1273,6 @@ public class ItemRowAdapter extends ArrayObjectAdapter {
if (response.getItems() != null && response.getItems().length > 0) {
int i = 0;
int prevItems = adapter.size() > 0 ? adapter.size() : 0;
if (query.getIsAiring()) {
// show guide option as first item
adapter.add(new BaseRowItem(new GridButton(TvApp.LIVE_TV_GUIDE_OPTION_ID, TvApp.getApplication().getResources().getString(R.string.lbl_live_tv_guide), R.drawable.tile_port_guide)));
i++;
}
for (BaseItemDto item : response.getItems()) {
adapter.add(new BaseRowItem(item, staticHeight));
i++;
@@ -1314,7 +1315,7 @@ public class ItemRowAdapter extends ArrayObjectAdapter {
int i = 0;
int prevItems = adapter.size() > 0 ? adapter.size() : 0;
for (BaseItemDto item : response.getItems()) {
item.setType("RecordingGroup"); // the API does not fill this in
item.setBaseItemType(BaseItemType.RecordingGroup); // the API does not fill this in
item.setIsFolder(true); // nor this
adapter.add(new BaseRowItem(item));
i++;

View File

@@ -5,7 +5,7 @@ import android.content.Context;
import android.content.DialogInterface;
import android.graphics.Color;
import android.graphics.Typeface;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.ColorDrawable;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.Handler;
@@ -425,7 +425,7 @@ public class LiveTvGuideActivity extends BaseActivity implements ILiveTvGuide {
mPopup = new PopupWindow(layout, WIDTH, HEIGHT);
mPopup.setFocusable(true);
mPopup.setOutsideTouchable(true);
mPopup.setBackgroundDrawable(new BitmapDrawable()); // necessary for popup to dismiss
mPopup.setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT)); // necessary for popup to dismiss
mPopup.setAnimationStyle(R.style.PopupSlideInRight);
mMovies = layout.findViewById(R.id.movies);
mSeries = layout.findViewById(R.id.series);
@@ -511,7 +511,7 @@ public class LiveTvGuideActivity extends BaseActivity implements ILiveTvGuide {
mPopup = new PopupWindow(layout, WIDTH, HEIGHT);
mPopup.setFocusable(true);
mPopup.setOutsideTouchable(true);
mPopup.setBackgroundDrawable(new BitmapDrawable()); // necessary for popup to dismiss
mPopup.setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT)); // necessary for popup to dismiss
mPopup.setAnimationStyle(R.style.PopupSlideInRight);
mHd = layout.findViewById(R.id.hd);
mRepeat = layout.findViewById(R.id.repeat);

View File

@@ -32,6 +32,7 @@ import java.util.TimeZone;
import org.jellyfin.apiclient.interaction.EmptyResponse;
import org.jellyfin.apiclient.interaction.Response;
import org.jellyfin.apiclient.model.dto.BaseItemDto;
import org.jellyfin.apiclient.model.dto.BaseItemType;
import org.jellyfin.apiclient.model.entities.DisplayPreferences;
import org.jellyfin.apiclient.model.entities.LocationType;
import org.jellyfin.apiclient.model.entities.SortOrder;
@@ -405,7 +406,7 @@ public class TvManager {
programInfo.setChannelName(timer.getChannelName());
programInfo.setName(Utils.getSafeValue(timer.getName(), "Unknown"));
TvApp.getApplication().getLogger().Warn("No program info for timer %s. Creating one...", programInfo.getName());
programInfo.setType("Program");
programInfo.setBaseItemType(BaseItemType.Program);
programInfo.setTimerId(timer.getId());
programInfo.setSeriesTimerId(timer.getSeriesTimerId());
programInfo.setStartDate(timer.getStartDate());

View File

@@ -9,4 +9,5 @@ public class AudioEventListener {
public void onPlaybackStateChange(PlaybackController.PlaybackState newState, BaseItemDto currentItem) {}
public void onProgress(long pos) {}
public void onQueueStatusChanged(boolean hasQueue) {}
public void onQueueReplaced(){}
}

View File

@@ -9,7 +9,7 @@ import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.os.Handler;
import androidx.leanback.app.BackgroundManager;
import androidx.leanback.app.RowsFragment;
import androidx.leanback.app.RowsSupportFragment;
import androidx.leanback.widget.ArrayObjectAdapter;
import androidx.leanback.widget.HeaderItem;
import androidx.leanback.widget.ListRow;
@@ -88,7 +88,7 @@ public class AudioNowPlayingActivity extends BaseActivity {
private TextView mCurrentPos;
private TextView mRemainingTime;
private int mCurrentDuration;
private RowsFragment mRowsFragment;
private RowsSupportFragment mRowsFragment;
private ArrayObjectAdapter mRowsAdapter;
private static PositionableListRowPresenter mAudioQueuePresenter;
@@ -203,8 +203,6 @@ public class AudioNowPlayingActivity extends BaseActivity {
@Override
public void onClick(DialogInterface dialog, int which) {
MediaManager.shuffleAudioQueue();
mRowsAdapter.remove(mQueueRow);
addQueue();
}
})
.setNegativeButton(mActivity.getString(R.string.lbl_no), null)
@@ -254,13 +252,12 @@ public class AudioNowPlayingActivity extends BaseActivity {
BackgroundManager backgroundManager = BackgroundManager.getInstance(this);
backgroundManager.attach(getWindow());
backgroundManager.setDimLayer(getDrawable(R.drawable.left_fade));
mBackgroundTarget = new PicassoBackgroundManagerTarget(backgroundManager);
mMetrics = new DisplayMetrics();
getWindowManager().getDefaultDisplay().getMetrics(mMetrics);
mRowsFragment = new RowsFragment();
getFragmentManager().beginTransaction().add(R.id.rowsFragment, mRowsFragment).commit();
mRowsFragment = new RowsSupportFragment();
getSupportFragmentManager().beginTransaction().add(R.id.rowsFragment, mRowsFragment).commit();
//create list background gradient
mListBackground = mApplication.getCurrentBackgroundGradient();
@@ -382,6 +379,12 @@ public class AudioNowPlayingActivity extends BaseActivity {
finish(); // entire queue removed nothing to do here
}
}
@Override
public void onQueueReplaced() {
mRowsAdapter.remove(mQueueRow);
addQueue();
}
};
private GotFocusEvent mainAreaFocusListener = new GotFocusEvent() {
@@ -486,7 +489,7 @@ public class AudioNowPlayingActivity extends BaseActivity {
for (String genre : mBaseItem.getGenres()) {
if (!first) InfoLayoutHelper.addSpacer(this, layout, " / ", 14);
first = false;
layout.addView(new GenreButton(this, roboto, 16, genre, mBaseItem.getType()));
layout.addView(new GenreButton(this, roboto, 16, genre, mBaseItem.getBaseItemType()));
}
}
}

View File

@@ -1,7 +1,6 @@
package org.jellyfin.androidtv.playback;
import android.app.AlertDialog;
import android.app.Fragment;
import android.content.ComponentName;
import android.content.Context;
import android.content.DialogInterface;
@@ -12,15 +11,6 @@ import android.media.AudioManager;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.Handler;
import androidx.annotation.Nullable;
import androidx.leanback.app.RowsFragment;
import androidx.leanback.widget.ArrayObjectAdapter;
import androidx.leanback.widget.HeaderItem;
import androidx.leanback.widget.ListRow;
import androidx.leanback.widget.OnItemViewClickedListener;
import androidx.leanback.widget.Presenter;
import androidx.leanback.widget.Row;
import androidx.leanback.widget.RowPresenter;
import android.text.Html;
import android.text.Spannable;
import android.text.SpannableString;
@@ -53,7 +43,6 @@ import org.jellyfin.androidtv.R;
import org.jellyfin.androidtv.TvApp;
import org.jellyfin.androidtv.base.CustomMessage;
import org.jellyfin.androidtv.base.IMessageListener;
import org.jellyfin.androidtv.integration.RecommendationManager;
import org.jellyfin.androidtv.itemhandling.BaseRowItem;
import org.jellyfin.androidtv.itemhandling.ItemRowAdapter;
import org.jellyfin.androidtv.livetv.ILiveTvGuide;
@@ -83,14 +72,6 @@ import org.jellyfin.androidtv.util.TimeUtils;
import org.jellyfin.androidtv.util.Utils;
import org.jellyfin.androidtv.util.apiclient.BaseItemUtils;
import org.jellyfin.androidtv.util.apiclient.StreamHelper;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.List;
import org.jellyfin.apiclient.interaction.EmptyResponse;
import org.jellyfin.apiclient.interaction.Response;
import org.jellyfin.apiclient.model.dto.BaseItemDto;
@@ -103,6 +84,24 @@ import org.jellyfin.apiclient.model.livetv.SeriesTimerInfoDto;
import org.jellyfin.apiclient.model.mediainfo.SubtitleTrackEvent;
import org.jellyfin.apiclient.model.mediainfo.SubtitleTrackInfo;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.List;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.leanback.app.RowsSupportFragment;
import androidx.leanback.widget.ArrayObjectAdapter;
import androidx.leanback.widget.HeaderItem;
import androidx.leanback.widget.ListRow;
import androidx.leanback.widget.OnItemViewClickedListener;
import androidx.leanback.widget.Presenter;
import androidx.leanback.widget.Row;
import androidx.leanback.widget.RowPresenter;
public class CustomPlaybackOverlayFragment extends Fragment implements IPlaybackOverlayFragment, ILiveTvGuide {
ImageView mPoster;
ImageView mStudioImage;
@@ -118,7 +117,7 @@ public class CustomPlaybackOverlayFragment extends Fragment implements IPlayback
LinearLayout mInfoRow;
LinearLayout mButtonRow;
FrameLayout mPopupArea;
RowsFragment mPopupRowsFragment;
RowsSupportFragment mPopupRowsFragment;
ArrayObjectAdapter mPopupRowAdapter;
ListRow mChapterRow;
PositionableListRowPresenter mPopupRowPresenter;
@@ -246,13 +245,13 @@ public class CustomPlaybackOverlayFragment extends Fragment implements IPlayback
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
ViewGroup root = (ViewGroup) inflater.inflate(R.layout.vlc_player_interface, container);
// inject the RowsFragment in the popup container
// inject the RowsSupportFragment in the popup container
if (getChildFragmentManager().findFragmentById(R.id.rows_area) == null) {
mPopupRowsFragment = new RowsFragment();
mPopupRowsFragment = new RowsSupportFragment();
getChildFragmentManager().beginTransaction()
.replace(R.id.rows_area, mPopupRowsFragment).commit();
} else {
mPopupRowsFragment = (RowsFragment) getChildFragmentManager()
mPopupRowsFragment = (RowsSupportFragment) getChildFragmentManager()
.findFragmentById(R.id.rows_area);
}
@@ -754,7 +753,6 @@ public class CustomPlaybackOverlayFragment extends Fragment implements IPlayback
public void onDestroy() {
super.onDestroy();
if (mPlaybackController != null) {
if (mPlaybackController.getCurrentlyPlayingItem() != null) RecommendationManager.getInstance().recommend(mPlaybackController.getCurrentlyPlayingItem().getId());
mPlaybackController.removePreviousQueueItems();
}
}

View File

@@ -1,6 +1,5 @@
package org.jellyfin.androidtv.playback;
import android.app.Activity;
import android.app.AlertDialog;
import android.content.ActivityNotFoundException;
import android.content.DialogInterface;
@@ -9,7 +8,6 @@ import android.content.SharedPreferences;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import androidx.leanback.app.BackgroundManager;
import org.jellyfin.androidtv.R;
import org.jellyfin.androidtv.TvApp;
@@ -19,15 +17,18 @@ import org.jellyfin.androidtv.model.compat.VideoOptions;
import org.jellyfin.androidtv.util.ProfileHelper;
import org.jellyfin.androidtv.util.Utils;
import org.jellyfin.androidtv.util.apiclient.ReportingHelper;
import java.util.List;
import org.jellyfin.apiclient.interaction.Response;
import org.jellyfin.apiclient.model.dto.BaseItemDto;
import org.jellyfin.apiclient.model.dto.BaseItemType;
import org.jellyfin.apiclient.model.dto.UserItemDataDto;
import org.jellyfin.apiclient.model.session.PlayMethod;
public class ExternalPlayerActivity extends Activity {
import java.util.List;
import androidx.fragment.app.FragmentActivity;
import androidx.leanback.app.BackgroundManager;
public class ExternalPlayerActivity extends FragmentActivity {
List<BaseItemDto> mItemsToPlay;
TvApp mApplication = TvApp.getApplication();
@@ -133,7 +134,7 @@ public class ExternalPlayerActivity extends Activity {
@Override
public void onClick(DialogInterface dialog, int which) {
SharedPreferences prefs = mApplication.getPrefs();
prefs.edit().putBoolean("pref_video_use_external", false).apply();
prefs.edit().putString("pref_video_player", "auto").apply();
prefs.edit().putBoolean("pref_live_tv_use_external", false).apply();
}
})
@@ -205,7 +206,7 @@ public class ExternalPlayerActivity extends Activity {
//Get playback info for current item
mCurrentNdx = ndx;
final BaseItemDto item = mItemsToPlay.get(mCurrentNdx);
isLiveTv = item.getType().equals("TvChannel");
isLiveTv = item.getBaseItemType() == BaseItemType.TvChannel;
if (!isLiveTv && mApplication.getPrefs().getBoolean("pref_send_path_external", false)) {
// Just pass the path directly

View File

@@ -7,14 +7,18 @@ import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.media.AudioManager;
import android.media.MediaPlayer;
import android.net.Uri;
import android.os.Handler;
import android.text.InputType;
import android.widget.EditText;
import com.devbrackets.android.exomedia.EMAudioPlayer;
import com.devbrackets.android.exomedia.event.EMMediaProgressEvent;
import com.devbrackets.android.exomedia.listener.EMProgressCallback;
import com.google.android.exoplayer2.ExoPlaybackException;
import com.google.android.exoplayer2.ExoPlayerFactory;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.SimpleExoPlayer;
import com.google.android.exoplayer2.source.ProgressiveMediaSource;
import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
import org.jellyfin.androidtv.R;
import org.jellyfin.androidtv.TvApp;
@@ -32,6 +36,12 @@ import org.jellyfin.androidtv.util.RemoteControlReceiver;
import org.jellyfin.androidtv.util.Utils;
import org.jellyfin.androidtv.util.apiclient.BaseItemUtils;
import org.jellyfin.androidtv.util.apiclient.ReportingHelper;
import org.jellyfin.apiclient.interaction.ApiClient;
import org.jellyfin.apiclient.interaction.Response;
import org.jellyfin.apiclient.model.dlna.DeviceProfile;
import org.jellyfin.apiclient.model.dto.BaseItemDto;
import org.jellyfin.apiclient.model.playlists.PlaylistCreationRequest;
import org.jellyfin.apiclient.model.playlists.PlaylistCreationResult;
import org.videolan.libvlc.LibVLC;
import org.videolan.libvlc.Media;
@@ -41,13 +51,6 @@ import java.util.Collections;
import java.util.Date;
import java.util.List;
import org.jellyfin.apiclient.interaction.ApiClient;
import org.jellyfin.apiclient.interaction.Response;
import org.jellyfin.apiclient.model.dlna.DeviceProfile;
import org.jellyfin.apiclient.model.dto.BaseItemDto;
import org.jellyfin.apiclient.model.playlists.PlaylistCreationRequest;
import org.jellyfin.apiclient.model.playlists.PlaylistCreationResult;
public class MediaManager {
private static ItemRowAdapter mCurrentMediaAdapter;
private static int mCurrentMediaPosition = -1;
@@ -63,7 +66,7 @@ public class MediaManager {
private static LibVLC mLibVLC;
private static org.videolan.libvlc.MediaPlayer mVlcPlayer;
private static VlcEventHandler mVlcHandler = new VlcEventHandler();
private static EMAudioPlayer mExoplayer;
private static SimpleExoPlayer mExoPlayer;
private static AudioManager mAudioManager;
private static boolean audioInitialized;
private static boolean nativeMode = false;
@@ -157,7 +160,7 @@ public class MediaManager {
}
private static boolean isPaused() {
return nativeMode ? !mExoplayer.isPlaying() : !mVlcPlayer.isPlaying();
return nativeMode ? !mExoPlayer.isPlaying() : !mVlcPlayer.isPlaying();
}
private static void reportProgress() {
@@ -165,7 +168,7 @@ public class MediaManager {
if (System.currentTimeMillis() < lastProgressEvent + 750) return;
lastProgressEvent = System.currentTimeMillis();
mCurrentAudioPosition = nativeMode ? mExoplayer.getCurrentPosition() : mVlcPlayer.getTime();
mCurrentAudioPosition = nativeMode ? mExoPlayer.getCurrentPosition() : mVlcPlayer.getTime();
//fire external listeners if there
for (AudioEventListener listener : mAudioEventListeners) {
@@ -199,19 +202,21 @@ public class MediaManager {
// Create a new media player based on platform
if (DeviceUtils.is60()) {
nativeMode = true;
mExoplayer = new EMAudioPlayer(TvApp.getApplication());
mExoplayer.setProgressCallback(new EMProgressCallback() {
mExoPlayer = ExoPlayerFactory.newSimpleInstance(TvApp.getApplication());
mExoPlayer.addListener(new Player.EventListener() {
@Override
public boolean onProgressUpdated(EMMediaProgressEvent progressEvent) {
reportProgress();
return false;
public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {
if (playbackState == Player.STATE_READY) {
startProgressLoop();
} else if (playbackState == Player.STATE_ENDED) {
onComplete();
stopProgressLoop();
}
}
});
mExoplayer.setOnCompletionListener(new MediaPlayer.OnCompletionListener() {
@Override
public void onCompletion(MediaPlayer mp) {
onComplete();
public void onPlayerError(ExoPlaybackException error) {
stopProgressLoop();
}
});
} else {
@@ -253,6 +258,25 @@ public class MediaManager {
return true;
}
private static Runnable progressLoop;
private static Handler mHandler = new Handler();
private static void startProgressLoop() {
progressLoop = new Runnable() {
@Override
public void run() {
reportProgress();
mHandler.postDelayed(this, 500);
}
};
mHandler.post(progressLoop);
}
private static void stopProgressLoop() {
if (progressLoop != null) {
mHandler.removeCallbacks(progressLoop);
}
}
private static AudioManager.OnAudioFocusChangeListener mAudioFocusChanged = new AudioManager.OnAudioFocusChangeListener() {
@Override
public void onAudioFocusChange(int focusChange) {
@@ -271,6 +295,13 @@ public class MediaManager {
}
};
private static void fireQueueReplaced(){
for (AudioEventListener listener : mAudioEventListeners) {
TvApp.getApplication().getLogger().Info("Firing queue replaced listener. ");
listener.onQueueReplaced();
}
}
private static void fireQueueStatusChange() {
for (AudioEventListener listener : mAudioEventListeners) {
TvApp.getApplication().getLogger().Info("Firing queue state change listener. "+ hasAudioQueueItems());
@@ -447,7 +478,7 @@ public class MediaManager {
}
}
public static boolean isPlayingAudio() { return audioInitialized && (nativeMode ? mExoplayer.isPlaying() : mVlcPlayer.isPlaying()); }
public static boolean isPlayingAudio() { return audioInitialized && (nativeMode ? mExoPlayer.isPlaying() : mVlcPlayer.isPlaying()); }
private static boolean ensureInitialized() {
if (!audioInitialized) {
@@ -472,6 +503,7 @@ public class MediaManager {
@Override
public void onClick(DialogInterface dialog, int which) {
playNowInternal(items);
fireQueueReplaced();
}
})
.setNeutralButton(R.string.lbl_add_to_queue, new DialogInterface.OnClickListener() {
@@ -485,7 +517,6 @@ public class MediaManager {
} else {
playNowInternal(items);
}
}
private static void playNowInternal(List<BaseItemDto> items) {
@@ -557,8 +588,10 @@ public class MediaManager {
mCurrentAudioQueuePosition = pos;
mCurrentAudioPosition = 0;
if (nativeMode) {
mExoplayer.setDataSource(TvApp.getApplication(), Uri.parse(response.ToUrl(apiClient.getApiUrl(), apiClient.getAccessToken())));
mExoplayer.start();
DataSource.Factory dataSourceFactory = new DefaultDataSourceFactory(TvApp.getApplication(), "ATV/ExoPlayer");
mExoPlayer.setPlayWhenReady(true);
mExoPlayer.prepare(new ProgressiveMediaSource.Factory(dataSourceFactory).createMediaSource(Uri.parse(response.ToUrl(apiClient.getApiUrl(), apiClient.getAccessToken()))));
} else {
TvApp.getApplication().getLogger().Info("Playback attempt via VLC of " + response.getMediaUrl());
Media media = new Media(mLibVLC, Uri.parse(response.getMediaUrl()));
@@ -660,7 +693,7 @@ public class MediaManager {
if (mCurrentAudioQueue == null || (!mRepeat && mCurrentAudioQueue.size() == 0)) return -1;
if (isPlayingAudio() && mCurrentAudioPosition > 10000) {
//just back up to the beginning of current item
if (nativeMode) mExoplayer.seekTo(0);
if (nativeMode) mExoPlayer.seekTo(0);
else mVlcPlayer.setTime(0);
return mCurrentAudioQueuePosition;
}
@@ -682,7 +715,7 @@ public class MediaManager {
}
private static void stop() {
if (nativeMode) mExoplayer.stopPlayback();
if (nativeMode) mExoPlayer.stop(true);
else mVlcPlayer.stop();
}
@@ -701,7 +734,7 @@ public class MediaManager {
}
private static void pause() {
if (nativeMode) mExoplayer.pause();
if (nativeMode) mExoPlayer.setPlayWhenReady(false);
else mVlcPlayer.pause();
}
@@ -723,7 +756,7 @@ public class MediaManager {
public static void resumeAudio() {
if (mCurrentAudioItem != null) {
ensureAudioFocus();
if (nativeMode) mExoplayer.start();
if (nativeMode) mExoPlayer.setPlayWhenReady(true);
else mVlcPlayer.play();
updateCurrentAudioItemPlaying(true);
ReportingHelper.reportStart(mCurrentAudioItem, mCurrentAudioPosition * 10000);

View File

@@ -24,15 +24,13 @@ import org.jellyfin.androidtv.util.Utils;
import org.jellyfin.androidtv.util.apiclient.PlaybackHelper;
import org.jellyfin.androidtv.util.apiclient.ReportingHelper;
import org.jellyfin.androidtv.util.apiclient.StreamHelper;
import java.util.List;
import org.jellyfin.apiclient.interaction.ApiClient;
import org.jellyfin.apiclient.interaction.Response;
import org.jellyfin.apiclient.model.dlna.DeviceProfile;
import org.jellyfin.apiclient.model.dlna.DirectPlayProfile;
import org.jellyfin.apiclient.model.dlna.SubtitleDeliveryMethod;
import org.jellyfin.apiclient.model.dto.BaseItemDto;
import org.jellyfin.apiclient.model.dto.BaseItemType;
import org.jellyfin.apiclient.model.dto.MediaSourceInfo;
import org.jellyfin.apiclient.model.entities.LocationType;
import org.jellyfin.apiclient.model.entities.MediaStream;
@@ -42,6 +40,8 @@ import org.jellyfin.apiclient.model.livetv.ChannelInfoDto;
import org.jellyfin.apiclient.model.mediainfo.SubtitleTrackInfo;
import org.jellyfin.apiclient.model.session.PlayMethod;
import java.util.List;
public class PlaybackController {
// Frequency to report playback progress
private final static long PROGRESS_REPORTING_INTERVAL = TimeUtils.secondsToMillis(3);
@@ -90,7 +90,7 @@ public class PlaybackController {
private boolean isLiveTv;
private boolean directStreamLiveTv;
private String liveTvChannelName = "";
private boolean useVlc = false;
private boolean useVlc;
private boolean vlcErrorEncountered;
private boolean exoErrorEncountered;
@@ -111,6 +111,9 @@ public class PlaybackController {
refreshRateSwitchingEnabled = DeviceUtils.is60() && mApplication.getPrefs().getBoolean("pref_refresh_switching", false);
if (refreshRateSwitchingEnabled) getDisplayModes();
// Set default value for useVlc field
// when set to auto the default will be exoplayer
useVlc = mApplication.getPrefs().getString("pref_video_player", "auto").equals("vlc");
}
public void init(VideoManager mgr, View spinner) {
@@ -331,7 +334,7 @@ public class PlaybackController {
return;
}
isLiveTv = item.getType().equals("TvChannel");
isLiveTv = item.getBaseItemType() == BaseItemType.TvChannel;
startSpinner();
//Build options for each player
@@ -473,26 +476,38 @@ public class PlaybackController {
vlcResponse.getMediaSource().getVideoStream().getWidth() > 1200);
mApplication.getLogger().Info(useDeinterlacing ? "Explicit deinterlacing will be used" : "Explicit deinterlacing will NOT be used");
// TODO: Clean up this logic
// Now look at both responses and choose the one that direct plays or bitstreams - favor VLC
useVlc = !vlcErrorEncountered &&
!vlcResponse.getPlayMethod().equals(PlayMethod.Transcode) &&
(DeviceUtils.is60() ||
!mApplication.getPrefs().getBoolean("pref_bitstream_ac3", false) ||
vlcResponse.getMediaSource() == null ||
vlcResponse.getMediaSource().getDefaultAudioStream() == null ||
(!"ac3".equals(vlcResponse.getMediaSource().getDefaultAudioStream().getCodec()) &&
!"truehd".equals(vlcResponse.getMediaSource().getDefaultAudioStream().getCodec()))) &&
(Utils.downMixAudio() ||
!DeviceUtils.is60() ||
internalResponse.getPlayMethod().equals(PlayMethod.Transcode) ||
!mApplication.getPrefs().getBoolean("pref_bitstream_dts", false) ||
internalResponse.getMediaSource() == null ||
internalResponse.getMediaSource().getDefaultAudioStream() == null ||
(!internalResponse.getMediaSource().getDefaultAudioStream().getCodec().equals("dca") &&
!internalResponse.getMediaSource().getDefaultAudioStream().getCodec().equals("dts"))) &&
(!DeviceUtils.isFireTvStick() ||
(vlcResponse.getMediaSource().getVideoStream() != null && vlcResponse.getMediaSource().getVideoStream().getWidth() < 1000));
String preferredVideoPlayer = mApplication.getPrefs().getString("pref_video_player", "auto");
mApplication.getLogger().Info("User preferred player is: " + preferredVideoPlayer);
if (preferredVideoPlayer.equals("vlc")) {
// Force VLC
useVlc = true;
} else if (preferredVideoPlayer.equals("exoplayer")) {
// Make sure to not use VLC
useVlc = false;
} else if (preferredVideoPlayer.equals("auto")) {
// TODO: Clean up this logic
// Now look at both responses and choose the one that direct plays or bitstreams - favor VLC
useVlc = !vlcErrorEncountered &&
!vlcResponse.getPlayMethod().equals(PlayMethod.Transcode) &&
(DeviceUtils.is60() ||
!mApplication.getPrefs().getBoolean("pref_bitstream_ac3", false) ||
vlcResponse.getMediaSource() == null ||
vlcResponse.getMediaSource().getDefaultAudioStream() == null ||
(!"ac3".equals(vlcResponse.getMediaSource().getDefaultAudioStream().getCodec()) &&
!"truehd".equals(vlcResponse.getMediaSource().getDefaultAudioStream().getCodec()))) &&
(Utils.downMixAudio() ||
!DeviceUtils.is60() ||
internalResponse.getPlayMethod().equals(PlayMethod.Transcode) ||
!mApplication.getPrefs().getBoolean("pref_bitstream_dts", false) ||
internalResponse.getMediaSource() == null ||
internalResponse.getMediaSource().getDefaultAudioStream() == null ||
(!internalResponse.getMediaSource().getDefaultAudioStream().getCodec().equals("dca") &&
!internalResponse.getMediaSource().getDefaultAudioStream().getCodec().equals("dts"))) &&
(!DeviceUtils.isFireTvStick() ||
(vlcResponse.getMediaSource().getVideoStream() != null && vlcResponse.getMediaSource().getVideoStream().getWidth() < 1000));
}
mApplication.getLogger().Info(useVlc ? "Preferring VLC" : "Will use internal player");
mVideoManager.init(getBufferAmount(), useDeinterlacing);
@@ -524,7 +539,6 @@ public class PlaybackController {
@Override
public void onError(Exception exception) {
mApplication.getLogger().ErrorException("Unable to get internal stream info", exception);
useVlc = true;
mCurrentOptions = vlcOptions;
startItem(item, position, vlcResponse);
}
@@ -568,7 +582,10 @@ public class PlaybackController {
setPlaybackMethod(response.getPlayMethod());
if (useVlc && (!getPlaybackMethod().equals(PlayMethod.Transcode) || isLiveTv)) {
// Force VLC when media is not live TV and the preferred player is VLC
boolean forceVlc = !isLiveTv && mApplication.getPrefs().getString("pref_video_player", "auto").equals("vlc");
if (forceVlc || (useVlc && (!getPlaybackMethod().equals(PlayMethod.Transcode) || isLiveTv))) {
TvApp.getApplication().getLogger().Info("Playing back in VLC.");
mVideoManager.setNativeMode(false);
} else {
@@ -920,7 +937,7 @@ public class PlaybackController {
public void updateTvProgramInfo() {
// Get the current program info when playing a live TV channel
final BaseItemDto channel = getCurrentlyPlayingItem();
if (channel.getType().equals("TvChannel")) {
if (channel.getBaseItemType() == BaseItemType.TvChannel) {
TvApp.getApplication().getApiClient().GetLiveTvChannelAsync(channel.getId(), TvApp.getApplication().getCurrentUser().getId(), new Response<ChannelInfoDto>() {
@Override
public void onResponse(ChannelInfoDto response) {
@@ -1101,8 +1118,14 @@ public class PlaybackController {
} else {
if (mDefaultSubIndex >= 0) {
//Default subs requested select them
mApplication.getLogger().Info("Selecting default sub stream: " + mDefaultSubIndex);
switchSubtitleStream(mDefaultSubIndex);
Integer currentIndex = mCurrentOptions.getSubtitleStreamIndex();
if (currentIndex != null && currentIndex == mDefaultSubIndex) {
mApplication.getLogger().Info("Not selecting default subtitle stream because it is already selected");
} else {
mApplication.getLogger().Info("Selecting default sub stream: " + mDefaultSubIndex);
switchSubtitleStream(mDefaultSubIndex);
}
} else {
TvApp.getApplication().getLogger().Info("Turning off subs by default");
mVideoManager.disableSubs();

View File

@@ -1,23 +1,38 @@
package org.jellyfin.androidtv.playback;
import android.app.Activity;
import android.content.Context;
import android.content.res.Configuration;
import android.graphics.PixelFormat;
import android.media.MediaPlayer;
import android.net.Uri;
import android.os.Handler;
import android.os.Looper;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;
import com.devbrackets.android.exomedia.EMVideoView;
import com.google.android.exoplayer2.DefaultRenderersFactory;
import com.google.android.exoplayer2.ExoPlaybackException;
import com.google.android.exoplayer2.ExoPlayerFactory;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.Renderer;
import com.google.android.exoplayer2.SimpleExoPlayer;
import com.google.android.exoplayer2.source.ProgressiveMediaSource;
import com.google.android.exoplayer2.text.TextOutput;
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
import com.google.android.exoplayer2.ui.PlayerView;
import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
import org.jellyfin.androidtv.R;
import org.jellyfin.androidtv.TvApp;
import org.jellyfin.androidtv.util.DeviceUtils;
import org.jellyfin.androidtv.util.Utils;
import org.jellyfin.apiclient.model.dto.MediaSourceInfo;
import org.jellyfin.apiclient.model.entities.MediaStream;
import org.jellyfin.apiclient.model.entities.MediaStreamType;
import org.videolan.libvlc.IVLCVout;
import org.videolan.libvlc.LibVLC;
import org.videolan.libvlc.Media;
@@ -25,10 +40,6 @@ import org.videolan.libvlc.Media;
import java.util.ArrayList;
import java.util.List;
import org.jellyfin.apiclient.model.dto.MediaSourceInfo;
import org.jellyfin.apiclient.model.entities.MediaStream;
import org.jellyfin.apiclient.model.entities.MediaStreamType;
public class VideoManager implements IVLCVout.OnNewVideoLayoutListener {
public final static int ZOOM_NORMAL = 0;
@@ -43,7 +54,8 @@ public class VideoManager implements IVLCVout.OnNewVideoLayoutListener {
private SurfaceView mSurfaceView;
private SurfaceView mSubtitlesSurface;
private FrameLayout mSurfaceFrame;
private EMVideoView mVideoView;
private SimpleExoPlayer mExoPlayer;
private PlayerView mExoPlayerView;
private LibVLC mLibVLC;
private org.videolan.libvlc.MediaPlayer mVlcPlayer;
private String mCurrentVideoPath;
@@ -63,11 +75,15 @@ public class VideoManager implements IVLCVout.OnNewVideoLayoutListener {
private long mForcedTime = -1;
private long mLastTime = -1;
private long mMetaDuration = -1;
private long lastExoPlayerPosition = -1;
private boolean nativeMode = false;
private boolean mSurfaceReady = false;
public boolean isContracted = false;
private boolean hasSubtitlesSurface = false;
private PlaybackListener errorListener;
private PlaybackListener completionListener;
private PlaybackListener preparedListener;
public VideoManager(PlaybackOverlayActivity activity, View view) {
mActivity = activity;
@@ -83,7 +99,41 @@ public class VideoManager implements IVLCVout.OnNewVideoLayoutListener {
} else {
mSubtitlesSurface.setVisibility(View.GONE);
}
mVideoView = view.findViewById(R.id.videoView);
mExoPlayer = ExoPlayerFactory.newSimpleInstance(
TvApp.getApplication(),
new DefaultRenderersFactory(TvApp.getApplication()) {
@Override
protected void buildTextRenderers(Context context,
TextOutput output,
Looper outputLooper, int extensionRendererMode,
ArrayList<Renderer> out) {
// Do not add text renderers since we handle subtitles
}
},
new DefaultTrackSelector());
mExoPlayerView = view.findViewById(R.id.exoPlayerView);
mExoPlayerView.setPlayer(mExoPlayer);
mExoPlayer.addListener(new Player.EventListener() {
@Override
public void onPlayerError(ExoPlaybackException error) {
TvApp.getApplication().getLogger().Error("***** Got error from player");
if (errorListener != null) errorListener.onEvent();
stopProgressLoop();
}
@Override
public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {
// Do not call listener when paused
if (playbackState == Player.STATE_READY && playWhenReady) {
if (preparedListener != null) preparedListener.onEvent();
startProgressLoop();
} else if (playbackState == Player.STATE_ENDED) {
if (completionListener != null) completionListener.onEvent();
stopProgressLoop();
}
}
});
}
public void init(int buffer, boolean isInterlaced) {
@@ -93,9 +143,9 @@ public class VideoManager implements IVLCVout.OnNewVideoLayoutListener {
public void setNativeMode(boolean value) {
nativeMode = value;
if (nativeMode) {
mVideoView.setVisibility(View.VISIBLE);
mExoPlayerView.setVisibility(View.VISIBLE);
} else {
mVideoView.setVisibility(View.GONE);
mExoPlayerView.setVisibility(View.GONE);
}
}
@@ -106,20 +156,20 @@ public class VideoManager implements IVLCVout.OnNewVideoLayoutListener {
mZoomMode = mode;
switch (mode) {
case ZOOM_NORMAL:
mVideoView.setScaleY(1);
mVideoView.setScaleX(1);
mExoPlayerView.setScaleY(1);
mExoPlayerView.setScaleX(1);
break;
case ZOOM_VERTICAL:
mVideoView.setScaleX(1);
mVideoView.setScaleY(1.33f);
mExoPlayerView.setScaleX(1);
mExoPlayerView.setScaleY(1.33f);
break;
case ZOOM_HORIZONTAL:
mVideoView.setScaleY(1);
mVideoView.setScaleX(1.33f);
mExoPlayerView.setScaleY(1);
mExoPlayerView.setScaleX(1.33f);
break;
case ZOOM_FULL:
mVideoView.setScaleX(1.33f);
mVideoView.setScaleY(1.33f);
mExoPlayerView.setScaleX(1.33f);
mExoPlayerView.setScaleY(1.33f);
break;
}
@@ -131,14 +181,22 @@ public class VideoManager implements IVLCVout.OnNewVideoLayoutListener {
public long getDuration() {
if (nativeMode){
return mVideoView.getDuration() > 0 ? mVideoView.getDuration() : mMetaDuration;
return mExoPlayer.getDuration() > 0 ? mExoPlayer.getDuration() : mMetaDuration;
} else {
return mVlcPlayer.getLength() > 0 ? mVlcPlayer.getLength() : mMetaDuration;
}
}
public long getCurrentPosition() {
if (nativeMode) return mVideoView.getCurrentPosition();
if (nativeMode) {
if (mExoPlayer == null) {
return lastExoPlayerPosition;
} else {
long mExoPlayerCurrentPosition = mExoPlayer.getCurrentPosition();
lastExoPlayerPosition = mExoPlayerCurrentPosition;
return mExoPlayerCurrentPosition;
}
}
if (mVlcPlayer == null) return 0;
@@ -162,17 +220,17 @@ public class VideoManager implements IVLCVout.OnNewVideoLayoutListener {
}
public boolean isPlaying() {
return nativeMode ? mVideoView.isPlaying() : mVlcPlayer != null && mVlcPlayer.isPlaying();
return nativeMode ? mExoPlayer.isPlaying() : mVlcPlayer != null && mVlcPlayer.isPlaying();
}
public boolean canSeek() { return nativeMode || mVlcPlayer.isSeekable(); }
public void start() {
if (nativeMode) {
mVideoView.start();
mVideoView.setKeepScreenOn(true);
normalWidth = mVideoView.getLayoutParams().width;
normalHeight = mVideoView.getLayoutParams().height;
mExoPlayer.setPlayWhenReady(true);
mExoPlayerView.setKeepScreenOn(true);
normalWidth = mExoPlayerView.getLayoutParams().width;
normalHeight = mExoPlayerView.getLayoutParams().height;
} else {
if (!mSurfaceReady) {
TvApp.getApplication().getLogger().Error("Attempt to play before surface ready");
@@ -188,8 +246,8 @@ public class VideoManager implements IVLCVout.OnNewVideoLayoutListener {
public void play() {
if (nativeMode) {
mVideoView.start();
mVideoView.setKeepScreenOn(true);
mExoPlayer.setPlayWhenReady(true);
mExoPlayerView.setKeepScreenOn(true);
} else {
mVlcPlayer.play();
mSurfaceView.setKeepScreenOn(true);
@@ -198,8 +256,8 @@ public class VideoManager implements IVLCVout.OnNewVideoLayoutListener {
public void pause() {
if (nativeMode) {
mVideoView.pause();
mVideoView.setKeepScreenOn(false);
mExoPlayer.setPlayWhenReady(false);
mExoPlayerView.setKeepScreenOn(false);
} else {
mVlcPlayer.pause();
mSurfaceView.setKeepScreenOn(false);
@@ -213,7 +271,7 @@ public class VideoManager implements IVLCVout.OnNewVideoLayoutListener {
public void stopPlayback() {
if (nativeMode) {
mVideoView.stopPlayback();
mExoPlayer.stop();
} else {
mVlcPlayer.stop();
}
@@ -223,8 +281,8 @@ public class VideoManager implements IVLCVout.OnNewVideoLayoutListener {
public long seekTo(long pos) {
if (nativeMode) {
Long intPos = pos;
TvApp.getApplication().getLogger().Info("Exo length in seek is: " + mVideoView.getDuration());
mVideoView.seekTo(intPos.intValue());
TvApp.getApplication().getLogger().Info("Exo length in seek is: " + mExoPlayer.getDuration());
mExoPlayer.seekTo(intPos.intValue());
return pos;
} else {
if (mVlcPlayer == null || !mVlcPlayer.isSeekable()) return -1;
@@ -255,7 +313,9 @@ public class VideoManager implements IVLCVout.OnNewVideoLayoutListener {
if (nativeMode) {
try {
mVideoView.setVideoPath(path);
DataSource.Factory dataSourceFactory = new DefaultDataSourceFactory(TvApp.getApplication(), "ATV/ExoPlayer");
mExoPlayer.prepare(new ProgressiveMediaSource.Factory(dataSourceFactory).createMediaSource(Uri.parse(path)));
} catch (IllegalStateException e) {
TvApp.getApplication().getLogger().ErrorException("Unable to set video path. Probably backing out.", e);
}
@@ -273,7 +333,7 @@ public class VideoManager implements IVLCVout.OnNewVideoLayoutListener {
public void hideSurface() {
if (nativeMode) {
mVideoView.setVisibility(View.INVISIBLE);
mExoPlayerView.setVisibility(View.INVISIBLE);
} else {
mSurfaceView.setVisibility(View.INVISIBLE);
}
@@ -281,7 +341,7 @@ public class VideoManager implements IVLCVout.OnNewVideoLayoutListener {
public void showSurface() {
if (nativeMode) {
mVideoView.setVisibility(View.VISIBLE);
mExoPlayerView.setVisibility(View.VISIBLE);
} else {
mSurfaceView.setVisibility(View.VISIBLE);
}
@@ -470,15 +530,22 @@ public class VideoManager implements IVLCVout.OnNewVideoLayoutListener {
}
private void releasePlayer() {
if (mVlcPlayer == null) return;
if (mVlcPlayer != null) {
mVlcPlayer.setEventListener(null);
mVlcPlayer.stop();
mVlcPlayer.getVLCVout().detachViews();
mVlcPlayer.release();
mLibVLC.release();
mLibVLC = null;
mVlcPlayer = null;
}
if (mExoPlayer != null) {
mExoPlayerView.setPlayer(null);
mExoPlayer.release();
mExoPlayer = null;
}
mVlcPlayer.setEventListener(null);
mVlcPlayer.stop();
mVlcPlayer.getVLCVout().detachViews();
mVlcPlayer.release();
mLibVLC.release();
mLibVLC = null;
mVlcPlayer = null;
mSurfaceView.setKeepScreenOn(false);
}
@@ -486,7 +553,7 @@ public class VideoManager implements IVLCVout.OnNewVideoLayoutListener {
int normalHeight;
public void contractVideo(int height) {
FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) (nativeMode ? mVideoView.getLayoutParams() : mSurfaceView.getLayoutParams());
FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) (nativeMode ? mExoPlayerView.getLayoutParams() : mSurfaceView.getLayoutParams());
if (isContracted) return;
Activity activity = TvApp.getApplication().getCurrentActivity();
@@ -499,8 +566,8 @@ public class VideoManager implements IVLCVout.OnNewVideoLayoutListener {
lp.bottomMargin = ((lp.height - normalHeight) / 2) - 50;
if (nativeMode) {
mVideoView.setLayoutParams(lp);
mVideoView.invalidate();
mExoPlayerView.setLayoutParams(lp);
mExoPlayerView.invalidate();
} else mSurfaceView.setLayoutParams(lp);
isContracted = true;
@@ -509,14 +576,14 @@ public class VideoManager implements IVLCVout.OnNewVideoLayoutListener {
public void setVideoFullSize() {
if (normalHeight == 0) return;
FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) (nativeMode ? mVideoView.getLayoutParams() : mSurfaceView.getLayoutParams());
FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) (nativeMode ? mExoPlayerView.getLayoutParams() : mSurfaceView.getLayoutParams());
lp.height = normalHeight;
lp.width = normalWidth;
if (nativeMode) {
lp.rightMargin = 0;
lp.bottomMargin = 0;
mVideoView.setLayoutParams(lp);
mVideoView.invalidate();
mExoPlayerView.setLayoutParams(lp);
mExoPlayerView.invalidate();
} else mSurfaceView.setLayoutParams(lp);
isContracted = false;
@@ -592,19 +659,7 @@ public class VideoManager implements IVLCVout.OnNewVideoLayoutListener {
if (hasSubtitlesSurface) mSubtitlesSurface.invalidate();
}
PlaybackListener errorListener;
public void setOnErrorListener(final PlaybackListener listener) {
mVideoView.setOnErrorListener(new MediaPlayer.OnErrorListener() {
@Override
public boolean onError(MediaPlayer mp, int what, int extra) {
TvApp.getApplication().getLogger().Error("***** Got error from player");
listener.onEvent();
stopProgressLoop();
return true;
}
});
mVlcHandler.setOnErrorListener(listener);
errorListener = listener;
}
@@ -614,28 +669,12 @@ public class VideoManager implements IVLCVout.OnNewVideoLayoutListener {
}
public void setOnCompletionListener(final PlaybackListener listener) {
mVideoView.setOnCompletionListener(new MediaPlayer.OnCompletionListener() {
@Override
public void onCompletion(MediaPlayer mp) {
listener.onEvent();
stopProgressLoop();
}
});
completionListener = listener;
mVlcHandler.setOnCompletionListener(listener);
}
private MediaPlayer mNativeMediaPlayer;
public void setOnPreparedListener(final PlaybackListener listener) {
mVideoView.setOnPreparedListener(new MediaPlayer.OnPreparedListener() {
@Override
public void onPrepared(MediaPlayer mp) {
mNativeMediaPlayer = mp;
listener.onEvent();
startProgressLoop();
}
});
preparedListener = listener;
mVlcHandler.setOnPreparedListener(listener);
}

View File

@@ -3,9 +3,11 @@ package org.jellyfin.androidtv.presentation;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.drawable.Drawable;
import androidx.leanback.widget.BaseCardView;
import androidx.leanback.widget.Presenter;
import androidx.palette.graphics.Palette;
import android.view.View;
import android.view.ViewGroup;
@@ -92,30 +94,41 @@ public class CardPresenter extends Presenter {
BaseItemDto itemDto = mItem.getBaseItem();
boolean showWatched = true;
boolean showProgress = false;
double aspect = imageType.equals(ImageType.BANNER) ? ASPECT_RATIO_BANNER : imageType.equals(ImageType.THUMB) ? ImageUtils.ASPECT_RATIO_16_9 : Utils.getSafeValue(ImageUtils.getImageAspectRatio(itemDto, m.getPreferParentThumb()), ImageUtils.ASPECT_RATIO_7_9);
switch (itemDto.getType()) {
case "Audio":
case "MusicAlbum":
double aspect;
if (imageType.equals(ImageType.BANNER)) {
aspect = ASPECT_RATIO_BANNER;
} else if (imageType.equals(ImageType.THUMB)) {
aspect = ImageUtils.ASPECT_RATIO_16_9;
} else {
aspect = Utils.getSafeValue(ImageUtils.getImageAspectRatio(itemDto, m.getPreferParentThumb()), ImageUtils.ASPECT_RATIO_7_9);
}
switch (itemDto.getBaseItemType()) {
case Audio:
case MusicAlbum:
mDefaultCardImage = TvApp.getApplication().getDrawableCompat(R.drawable.tile_audio);
if (aspect < 0.8) aspect = 1.0;
if (aspect < 0.8) {
aspect = 1.0;
}
showWatched = false;
break;
case "Person":
case Person:
mDefaultCardImage = TvApp.getApplication().getDrawableCompat(R.drawable.tile_port_person);
break;
case "MusicArtist":
case MusicArtist:
mDefaultCardImage = TvApp.getApplication().getDrawableCompat(R.drawable.tile_port_person);
if (aspect <.8) aspect = 1.0;
if (aspect < .8) {
aspect = 1.0;
}
showWatched = false;
break;
case "RecordingGroup":
case RecordingGroup:
mDefaultCardImage = TvApp.getApplication().getDrawableCompat(R.drawable.tile_port_record);
break;
case "Season":
case "Series":
case Season:
case Series:
mDefaultCardImage = TvApp.getApplication().getDrawableCompat(R.drawable.tile_port_tv);
break;
case "Episode":
case Episode:
//TvApp.getApplication().getLogger().Debug("**** Image width: "+ cardWidth + " Aspect: " + Utils.getImageAspectRatio(itemDto, m.getPreferParentThumb()) + " Item: "+itemDto.getName());
mDefaultCardImage = TvApp.getApplication().getDrawableCompat(R.drawable.tile_land_tv);
switch (itemDto.getLocationType()) {
@@ -124,7 +137,7 @@ public class CardPresenter extends Presenter {
case Remote:
break;
case Virtual:
mCardView.setBanner((itemDto.getPremiereDate() != null ? TimeUtils.convertToLocalDate(itemDto.getPremiereDate()) : new Date(System.currentTimeMillis()+1)).getTime() > System.currentTimeMillis() ? R.drawable.banner_edge_future : R.drawable.banner_edge_missing);
mCardView.setBanner((itemDto.getPremiereDate() != null ? TimeUtils.convertToLocalDate(itemDto.getPremiereDate()) : new Date(System.currentTimeMillis() + 1)).getTime() > System.currentTimeMillis() ? R.drawable.banner_edge_future : R.drawable.banner_edge_missing);
break;
case Offline:
mCardView.setBanner(R.drawable.banner_edge_offline);
@@ -134,27 +147,31 @@ public class CardPresenter extends Presenter {
//Always show info for episodes
mCardView.setCardType(BaseCardView.CARD_TYPE_INFO_UNDER);
break;
case "CollectionFolder":
case "Folder":
case "MovieGenreFolder":
case "MusicGenreFolder":
case "MovieGenre":
case "Genre":
case "MusicGenre":
case "UserView":
case CollectionFolder:
// Force the aspect ratio to 16x9 because the server is returning the wrong value of 1
aspect = ImageUtils.ASPECT_RATIO_16_9;
mDefaultCardImage = TvApp.getApplication().getDrawableCompat(R.drawable.tile_port_folder);
break;
case "Photo":
case Folder:
case MovieGenreFolder:
case MusicGenreFolder:
case MovieGenre:
case Genre:
case MusicGenre:
case UserView:
mDefaultCardImage = TvApp.getApplication().getDrawableCompat(R.drawable.tile_port_folder);
break;
case Photo:
mDefaultCardImage = TvApp.getApplication().getDrawableCompat(R.drawable.tile_land_photo);
showWatched = false;
break;
case "PhotoAlbum":
case "Playlist":
case PhotoAlbum:
case Playlist:
showWatched = false;
mDefaultCardImage = TvApp.getApplication().getDrawableCompat(R.drawable.tile_port_folder);
break;
case "Movie":
case "Video":
case Movie:
case Video:
mDefaultCardImage = TvApp.getApplication().getDrawableCompat(R.drawable.tile_port_video);
showProgress = true;
break;
@@ -163,10 +180,16 @@ public class CardPresenter extends Presenter {
break;
}
cardHeight = !m.isStaticHeight() ? (aspect > 1 ? lHeight : pHeight) : sHeight;
cardWidth = (int)(aspect * cardHeight);
if (cardWidth < 10) cardWidth = 230; //Guard against zero size images causing picasso to barf
if (itemDto.getLocationType() == LocationType.Offline) mCardView.setBanner(R.drawable.banner_edge_offline);
if (itemDto.getIsPlaceHolder() != null && itemDto.getIsPlaceHolder()) mCardView.setBanner(R.drawable.banner_edge_disc);
cardWidth = (int) (aspect * cardHeight);
if (cardWidth < 10) {
cardWidth = 230; //Guard against zero size images causing picasso to barf
}
if (itemDto.getLocationType() == LocationType.Offline) {
mCardView.setBanner(R.drawable.banner_edge_offline);
}
if (itemDto.getIsPlaceHolder() != null && itemDto.getIsPlaceHolder()) {
mCardView.setBanner(R.drawable.banner_edge_disc);
}
UserItemDataDto userData = itemDto.getUserData();
if (showWatched && userData != null) {
if (userData.getPlayed()) {
@@ -176,8 +199,8 @@ public class CardPresenter extends Presenter {
}
}
if(showProgress && itemDto.getRunTimeTicks() != null && itemDto.getRunTimeTicks() > 0 && userData != null && userData.getPlaybackPositionTicks() > 0) {
mCardView.setProgress(((int)(userData.getPlaybackPositionTicks() * 100.0 / itemDto.getRunTimeTicks()))); // force floating pt math with 100.0
if (showProgress && itemDto.getRunTimeTicks() != null && itemDto.getRunTimeTicks() > 0 && userData != null && userData.getPlaybackPositionTicks() > 0) {
mCardView.setProgress(((int) (userData.getPlaybackPositionTicks() * 100.0 / itemDto.getRunTimeTicks()))); // force floating pt math with 100.0
} else {
mCardView.setProgress(0);
}
@@ -187,25 +210,36 @@ public class CardPresenter extends Presenter {
ChannelInfoDto channel = mItem.getChannelInfo();
double tvAspect = imageType.equals(ImageType.BANNER) ? ASPECT_RATIO_BANNER : imageType.equals(ImageType.THUMB) ? ImageUtils.ASPECT_RATIO_16_9 : Utils.getSafeValue(channel.getPrimaryImageAspectRatio(), ImageUtils.ASPECT_RATIO_7_9);
cardHeight = !m.isStaticHeight() ? tvAspect > 1 ? lHeight : pHeight : sHeight;
cardWidth = (int)((tvAspect) * cardHeight);
if (cardWidth < 10) cardWidth = 230; //Guard against zero size images causing picasso to barf
cardWidth = (int) ((tvAspect) * cardHeight);
if (cardWidth < 10) {
cardWidth = 230; //Guard against zero size images causing picasso to barf
}
mCardView.setMainImageDimensions(cardWidth, cardHeight);
mDefaultCardImage = TvApp.getApplication().getDrawableCompat(R.drawable.tile_port_tv);
break;
case LiveTvProgram:
BaseItemDto program = mItem.getProgramInfo();
Double programAspect = program.getPrimaryImageAspectRatio();
if (programAspect == null) programAspect = Utils.isTrue(program.getIsMovie()) ? ImageUtils.ASPECT_RATIO_2_3 : ImageUtils.ASPECT_RATIO_16_9;
if (Utils.isTrue(program.getIsMovie())) {
// The server reports the incorrect image aspect ratio for movies, so we are overriding it here
programAspect = ImageUtils.ASPECT_RATIO_2_3;
} else if (programAspect == null) {
programAspect = ImageUtils.ASPECT_RATIO_16_9;
}
cardHeight = !m.isStaticHeight() ? programAspect > 1 ? lHeight : pHeight : sHeight;
cardWidth = (int)((programAspect) * cardHeight);
if (cardWidth < 10) cardWidth = 230; //Guard against zero size images causing picasso to barf
cardWidth = (int) ((programAspect) * cardHeight);
if (cardWidth < 10) {
cardWidth = 230; //Guard against zero size images causing picasso to barf
}
switch (program.getLocationType()) {
case FileSystem:
case Remote:
case Offline:
break;
case Virtual:
if (program.getStartDate() != null && TimeUtils.convertToLocalDate(program.getStartDate()).getTime() > System.currentTimeMillis()) mCardView.setBanner(R.drawable.banner_edge_future);
if (program.getStartDate() != null && TimeUtils.convertToLocalDate(program.getStartDate()).getTime() > System.currentTimeMillis()) {
mCardView.setBanner(R.drawable.banner_edge_future);
}
break;
}
mCardView.setMainImageDimensions(cardWidth, cardHeight);
@@ -217,47 +251,49 @@ public class CardPresenter extends Presenter {
BaseItemDto recording = mItem.getRecordingInfo();
double recordingAspect = imageType.equals(ImageType.BANNER) ? ASPECT_RATIO_BANNER : (imageType.equals(ImageType.THUMB) ? ImageUtils.ASPECT_RATIO_16_9 : Utils.getSafeValue(recording.getPrimaryImageAspectRatio(), ImageUtils.ASPECT_RATIO_7_9));
cardHeight = !m.isStaticHeight() ? recordingAspect > 1 ? lHeight : pHeight : sHeight;
cardWidth = (int)((recordingAspect) * cardHeight);
if (cardWidth < 10) cardWidth = 230; //Guard against zero size images causing picasso to barf
cardWidth = (int) ((recordingAspect) * cardHeight);
if (cardWidth < 10) {
cardWidth = 230; //Guard against zero size images causing picasso to barf
}
mCardView.setMainImageDimensions(cardWidth, cardHeight);
mDefaultCardImage = TvApp.getApplication().getDrawableCompat(R.drawable.tile_port_tv);
break;
case Server:
cardWidth = (int)(ImageUtils.ASPECT_RATIO_7_9 * cardHeight);
cardWidth = (int) (ImageUtils.ASPECT_RATIO_7_9 * cardHeight);
mCardView.setMainImageDimensions(cardWidth, cardHeight);
mDefaultCardImage = TvApp.getApplication().getDrawableCompat(R.drawable.tile_port_server);
break;
case Person:
cardHeight = !m.isStaticHeight() ? pHeight : sHeight;
cardWidth = (int)(ImageUtils.ASPECT_RATIO_7_9 * cardHeight);
cardWidth = (int) (ImageUtils.ASPECT_RATIO_7_9 * cardHeight);
mCardView.setMainImageDimensions(cardWidth, cardHeight);
mDefaultCardImage = TvApp.getApplication().getDrawableCompat(R.drawable.tile_port_person);
break;
case User:
cardWidth = (int)(ImageUtils.ASPECT_RATIO_7_9 * cardHeight);
cardWidth = (int) (ImageUtils.ASPECT_RATIO_7_9 * cardHeight);
mCardView.setMainImageDimensions(cardWidth, cardHeight);
mDefaultCardImage = TvApp.getApplication().getDrawableCompat(R.drawable.tile_port_person);
break;
case Chapter:
cardHeight = !m.isStaticHeight() ? pHeight : sHeight;
cardWidth = (int)(ImageUtils.ASPECT_RATIO_16_9 * cardHeight);
cardWidth = (int) (ImageUtils.ASPECT_RATIO_16_9 * cardHeight);
mCardView.setMainImageDimensions(cardWidth, cardHeight);
mDefaultCardImage = TvApp.getApplication().getDrawableCompat(R.drawable.tile_chapter);
break;
case SearchHint:
switch (mItem.getSearchHint().getType()) {
case "Episode":
cardWidth = (int)(ImageUtils.ASPECT_RATIO_16_9 * cardHeight);
cardWidth = (int) (ImageUtils.ASPECT_RATIO_16_9 * cardHeight);
mCardView.setMainImageDimensions(cardWidth, cardHeight);
mDefaultCardImage = TvApp.getApplication().getDrawableCompat(R.drawable.tile_port_tv);
break;
case "Person":
cardWidth = (int)(ImageUtils.ASPECT_RATIO_7_9 * cardHeight);
cardWidth = (int) (ImageUtils.ASPECT_RATIO_7_9 * cardHeight);
mCardView.setMainImageDimensions(cardWidth, cardHeight);
mDefaultCardImage = TvApp.getApplication().getDrawableCompat(R.drawable.tile_port_person);
break;
default:
cardWidth = (int)(ImageUtils.ASPECT_RATIO_7_9 * cardHeight);
cardWidth = (int) (ImageUtils.ASPECT_RATIO_7_9 * cardHeight);
mCardView.setMainImageDimensions(cardWidth, cardHeight);
mDefaultCardImage = TvApp.getApplication().getDrawableCompat(R.drawable.tile_port_video);
break;
@@ -265,13 +301,13 @@ public class CardPresenter extends Presenter {
break;
case GridButton:
cardHeight = !m.isStaticHeight() ? pHeight : sHeight;
cardWidth = (int)(ImageUtils.ASPECT_RATIO_7_9 * cardHeight);
cardWidth = (int) (ImageUtils.ASPECT_RATIO_7_9 * cardHeight);
mCardView.setMainImageDimensions(cardWidth, cardHeight);
mDefaultCardImage = TvApp.getApplication().getDrawableCompat(R.drawable.tile_port_video);
break;
case SeriesTimer:
cardHeight = !m.isStaticHeight() ? pHeight : sHeight;
cardWidth = (int)(ImageUtils.ASPECT_RATIO_16_9 * cardHeight);
cardWidth = (int) (ImageUtils.ASPECT_RATIO_16_9 * cardHeight);
mCardView.setMainImageDimensions(cardWidth, cardHeight);
mDefaultCardImage = TvApp.getApplication().getDrawableCompat(R.drawable.tile_land_series_timer);
//Always show info for timers
@@ -293,7 +329,9 @@ public class CardPresenter extends Presenter {
}
protected void updateCardViewImage(String url) {
if (!validContext()) return;
if (!validContext()) {
return;
}
try {
if (url == null) {
@@ -325,7 +363,9 @@ public class CardPresenter extends Presenter {
mCardView.clearBanner();
mCardView.setUnwatchedCount(-1);
mCardView.setProgress(0);
if (!validContext()) return;
if (!validContext()) {
return;
}
try {
Glide.with(getContext())
.load(TvApp.getApplication().getDrawable(R.drawable.loading))
@@ -351,16 +391,22 @@ public class CardPresenter extends Presenter {
@Override
public void onBindViewHolder(Presenter.ViewHolder viewHolder, Object item) {
if (!(item instanceof BaseRowItem)) return;
if (!(item instanceof BaseRowItem)) {
return;
}
BaseRowItem rowItem = (BaseRowItem) item;
if (!rowItem.isValid()) return;
if (!rowItem.isValid()) {
return;
}
ViewHolder holder = (ViewHolder) viewHolder;
holder.setItem(rowItem, mImageType, 260, 300, mStaticHeight);
holder.mCardView.setTitleText(rowItem.getCardName());
holder.mCardView.setContentText(rowItem.getSubText());
if (ImageType.DEFAULT.equals(mImageType)) holder.mCardView.setOverlayInfo(rowItem);
if (ImageType.DEFAULT.equals(mImageType)) {
holder.mCardView.setOverlayInfo(rowItem);
}
holder.mCardView.showFavIcon(rowItem.isFavorite());
if (rowItem.isPlaying()) {
holder.mCardView.setPlayingIndicator(true);

View File

@@ -9,11 +9,13 @@ import androidx.leanback.widget.FocusHighlightHelper;
import androidx.leanback.widget.HorizontalGridView;
import androidx.leanback.widget.ItemBridgeAdapter;
import androidx.leanback.widget.ObjectAdapter;
import androidx.leanback.widget.OnChildSelectedListener;
import androidx.leanback.widget.OnChildViewHolderSelectedListener;
import androidx.leanback.widget.OnItemViewClickedListener;
import androidx.leanback.widget.OnItemViewSelectedListener;
import androidx.leanback.widget.Presenter;
import androidx.leanback.widget.ShadowOverlayContainer;
import androidx.recyclerview.widget.RecyclerView;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
@@ -187,7 +189,7 @@ public class HorizontalGridPresenter extends Presenter {
vh.getGridView().setNumRows(mNumRows);
vh.getGridView().setFadingRightEdge(true);
vh.getGridView().setFadingRightEdgeLength(100);
vh.getGridView().setItemMargin(10);
vh.getGridView().setItemSpacing(10);
vh.mInitialized = true;
vh.mItemBridgeAdapter.setWrapper(mWrapper);
@@ -200,10 +202,10 @@ public class HorizontalGridPresenter extends Presenter {
mZoomFactor, true);
final ViewHolder gridViewHolder = vh;
vh.getGridView().setOnChildSelectedListener(new OnChildSelectedListener() {
vh.getGridView().setOnChildViewHolderSelectedListener(new OnChildViewHolderSelectedListener() {
@Override
public void onChildSelected(ViewGroup parent, View view, int position, long id) {
selectChildView(gridViewHolder, view);
public void onChildViewHolderSelected(RecyclerView parent, RecyclerView.ViewHolder child, int position, int subposition) {
selectChildView(gridViewHolder, child.itemView);
}
});

View File

@@ -3,6 +3,10 @@ package org.jellyfin.androidtv.presentation;
import android.graphics.Color;
import android.graphics.Typeface;
import androidx.leanback.widget.RowPresenter;
import android.os.Build;
import android.text.Html;
import android.text.Spanned;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
@@ -112,10 +116,23 @@ public class MyDetailsOverviewRowPresenter extends RowPresenter {
vh.mPoster.setImageDrawable(row.getImageDrawable());
//vh.mStudioImage.setImageDrawable(row.getStudioDrawable());
vh.mSummary.setText(row.getSummary());
//vh.mSummaryTitle.setText(row.getSummaryTitle());
switch (row.getItem().getType()) {
case "Person":
// Support simple HTML elements
String summaryRaw = row.getSummary();
if (summaryRaw != null) {
Spanned summarySpanned;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
summarySpanned = Html.fromHtml(summaryRaw, Html.FROM_HTML_MODE_COMPACT);
} else {
summarySpanned = Html.fromHtml(summaryRaw);
}
vh.mSummary.setText(summarySpanned);
}
switch (row.getItem().getBaseItemType()) {
case Person:
RelativeLayout.LayoutParams params = (RelativeLayout.LayoutParams) vh.mSummary.getLayoutParams();
params.topMargin = 10;
params.height = Utils.convertDpToPixel(TvApp.getApplication(), 185);
@@ -143,7 +160,7 @@ public class MyDetailsOverviewRowPresenter extends RowPresenter {
for (String genre : item.getGenres()) {
if (!first) InfoLayoutHelper.addSpacer(TvApp.getApplication().getCurrentActivity(), layout, " / ", 12);
first = false;
layout.addView(new GenreButton(TvApp.getApplication().getCurrentActivity(), TvApp.getApplication().getDefaultFont(), 14, genre, item.getType()));
layout.addView(new GenreButton(TvApp.getApplication().getCurrentActivity(), TvApp.getApplication().getDefaultFont(), 14, genre, item.getBaseItemType()));
}
}
}

View File

@@ -237,22 +237,22 @@ public class MyImageCardView extends BaseCardView {
if (mOverlayName == null) return;
if (getCardType() == BaseCardView.CARD_TYPE_MAIN_ONLY && item.showCardInfoOverlay()) {
switch (item.getType()) {
case "Photo":
switch (item.getBaseItemType()) {
case Photo:
mOverlayName.setText(item.getBaseItem().getPremiereDate() != null ? android.text.format.DateFormat.getDateFormat(TvApp.getApplication()).format(TimeUtils.convertToLocalDate(item.getBaseItem().getPremiereDate())) : item.getFullName());
mOverlayIcon.setImageResource(R.drawable.ic_camera);
break;
case "PhotoAlbum":
case PhotoAlbum:
mOverlayName.setText(item.getFullName());
mOverlayIcon.setImageResource(R.drawable.ic_photos);
break;
case "Video":
case Video:
mOverlayName.setText(item.getFullName());
mOverlayIcon.setImageResource(R.drawable.ic_movie);
break;
case "Playlist":
case "MusicArtist":
case "Person":
case Playlist:
case MusicArtist:
case Person:
mOverlayName.setText(item.getFullName());
hideIcon();
break;

View File

@@ -1,88 +1,90 @@
package org.jellyfin.androidtv.presentation;
import android.media.MediaPlayer;
import android.os.Handler;
import org.jellyfin.androidtv.R;
import org.jellyfin.androidtv.TvApp;
import org.jellyfin.androidtv.base.BaseActivity;
import org.jellyfin.androidtv.util.Utils;
import java.util.Calendar;
import java.util.GregorianCalendar;
/**
* Created by Eric on 10/24/2015.
*/
public class ThemeManager {
private static boolean isEnabled() {
return TvApp.getApplication().getPrefs().getBoolean("pref_enable_themes",true);
return TvApp.getApplication().getPrefs().getBoolean("pref_enable_themes", true);
}
private static boolean isHalloween() {
if (!isEnabled()) return false;
if (!isEnabled()) {
return false;
}
Calendar today = new GregorianCalendar();
return today.get(Calendar.MONTH) == 9 && (today.get(Calendar.DAY_OF_MONTH) == 30 || today.get(Calendar.DAY_OF_MONTH) == 31);
return today.get(Calendar.MONTH) == Calendar.OCTOBER &&
(today.get(Calendar.DAY_OF_MONTH) == 30 || today.get(Calendar.DAY_OF_MONTH) == 31);
}
private static boolean isHolidays() {
if (!isEnabled()) return false;
if (!isEnabled()) {
return false;
}
Calendar today = new GregorianCalendar();
return today.get(Calendar.MONTH) == 11 && (today.get(Calendar.DAY_OF_MONTH) > 20 && today.get(Calendar.DAY_OF_MONTH) < 27);
return today.get(Calendar.MONTH) == Calendar.DECEMBER &&
(today.get(Calendar.DAY_OF_MONTH) > 20 && today.get(Calendar.DAY_OF_MONTH) < 27);
}
public static int getBrandColor() {
return isHalloween() ?
TvApp.getApplication().getResources().getColor(R.color.halloween_end) :
isHolidays() ?
TvApp.getApplication().getResources().getColor(R.color.holiday_end) :
Utils.getBrandColor();
private static void showHolidayMessage(final BaseActivity activity, final String title, final String message, final Integer icon, final Integer background) {
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
// make sure they haven't moved away from that activity
if (TvApp.getApplication().getCurrentActivity() == null ||
TvApp.getApplication().getCurrentActivity() != activity) {
return;
}
activity.showMessage(title, message, 10000, icon, background);
}
}, 2000);
}
public static void showWelcomeMessage() {
final BaseActivity currentActivity = TvApp.getApplication().getCurrentActivity();
if (isHalloween() && currentActivity != null) {
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
// make sure they haven't moved away from that activity
if (TvApp.getApplication().getCurrentActivity() == null || TvApp.getApplication().getCurrentActivity() != currentActivity) return;
MediaPlayer mp = MediaPlayer.create(TvApp.getApplication(), R.raw.howl);
mp.start();
currentActivity.showMessage("Happy Halloween!", "Try some of our spooky suggestions", 10000, R.drawable.ic_ghost, R.drawable.orange_gradient);
}
}, 2000);
} else
if (isHolidays() && currentActivity != null) {
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
// make sure they haven't moved away from that activity
if (TvApp.getApplication().getCurrentActivity() == null || TvApp.getApplication().getCurrentActivity() != currentActivity) return;
MediaPlayer mp = MediaPlayer.create(TvApp.getApplication(), R.raw.sleighbells);
mp.start();
currentActivity.showMessage("Happy Holidays!", "Try some of our holiday suggestions", 10000, R.drawable.ic_snowflake, R.drawable.holiday_gradient);
}
}, 2000);
if (currentActivity != null) {
if (isHalloween()) {
showHolidayMessage(currentActivity,
currentActivity.getString(R.string.title_suggestions_halloween),
currentActivity.getString(R.string.desc_suggestions_halloween),
R.drawable.ic_ghost,
R.drawable.orange_gradient);
} else if (isHolidays()) {
showHolidayMessage(currentActivity,
currentActivity.getString(R.string.title_suggestions_holiday),
currentActivity.getString(R.string.desc_suggestions_holiday),
R.drawable.ic_snowflake,
R.drawable.holiday_gradient);
}
}
}
public static String[] getSpecialGenres() {
if (isHalloween()) {
return new String[] {"Halloween","Horror"};
return new String[]{"Halloween", "Horror"};
}
if (isHolidays()) {
return new String[] {"Holiday","Holidays","Christmas"};
return new String[]{"Holiday", "Holidays", "Christmas"};
}
return null;
}
public static String getSuggestionTitle() {
return isHalloween() ? "Spooky Suggestions" : isHolidays() ? "Holiday Suggestions" : "Suggestions";
int labelId = R.string.lbl_suggestions;
if (isHalloween()) {
labelId = R.string.lbl_suggestions_halloween;
} else if (isHolidays()) {
labelId = R.string.lbl_suggestions_holiday;
}
return TvApp.getApplication().getCurrentActivity().getString(labelId);
}
}

View File

@@ -12,5 +12,20 @@ public enum QueryType {
StaticChapters,
StaticServers,
Users,
Search, Specials, Trailers, LiveTvChannel, LiveTvProgram, LiveTvRecording, LiveTvRecordingGroup, StaticItems, Persons, StaticAudioQueueItems, AlbumArtists, AudioPlaylists, ContinueWatching, LatestItems, SeriesTimer, Premieres
Search,
Specials,
Trailers,
LiveTvChannel,
LiveTvProgram,
LiveTvRecording,
LiveTvRecordingGroup,
StaticItems,
Persons,
StaticAudioQueueItems,
AlbumArtists,
AudioPlaylists,
ContinueWatching,
LatestItems,
SeriesTimer,
Premieres
}

View File

@@ -0,0 +1,29 @@
package org.jellyfin.androidtv.search;
import android.os.Bundle;
import org.jellyfin.androidtv.itemhandling.BaseRowItem;
import org.jellyfin.androidtv.itemhandling.ItemLauncher;
import org.jellyfin.androidtv.itemhandling.ItemRowAdapter;
import androidx.leanback.app.SearchSupportFragment;
import androidx.leanback.widget.ListRow;
public class LeanbackSearchFragment extends SearchSupportFragment {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Create provider
SearchProvider searchProvider = new SearchProvider(getContext(), getActivity().getIntent().getBooleanExtra("MusicOnly", false));
setSearchResultProvider(searchProvider);
// Create click listener
setOnItemViewClickedListener((itemViewHolder, item, rowViewHolder, row) -> {
if (!(item instanceof BaseRowItem)) return;
ItemLauncher.launch((BaseRowItem) item, (ItemRowAdapter) ((ListRow) row).getAdapter(), ((BaseRowItem) item).getIndex(), getActivity());
});
}
}

View File

@@ -1,112 +0,0 @@
package org.jellyfin.androidtv.search;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.os.Handler;
import androidx.leanback.app.BackgroundManager;
import androidx.leanback.app.SearchFragment;
import androidx.leanback.widget.ArrayObjectAdapter;
import androidx.leanback.widget.ListRow;
import androidx.leanback.widget.ListRowPresenter;
import androidx.leanback.widget.ObjectAdapter;
import androidx.leanback.widget.OnItemViewClickedListener;
import androidx.leanback.widget.Presenter;
import androidx.leanback.widget.Row;
import androidx.leanback.widget.RowPresenter;
import android.util.DisplayMetrics;
import com.squareup.picasso.Target;
import org.jellyfin.androidtv.imagehandling.PicassoBackgroundManagerTarget;
import org.jellyfin.androidtv.itemhandling.BaseRowItem;
import org.jellyfin.androidtv.itemhandling.ItemLauncher;
import org.jellyfin.androidtv.itemhandling.ItemRowAdapter;
import org.jellyfin.androidtv.util.Utils;
import java.util.Timer;
/**
* Created by Eric on 1/26/2015.
*/
public class MySearchFragment extends SearchFragment
implements SearchFragment.SearchResultProvider {
private static final int SEARCH_DELAY_MS = 1500;
private final Handler mHandler = new Handler();
private ArrayObjectAdapter mRowsAdapter;
private SearchRunnable mDelayedLoad;
private Drawable mDefaultBackground;
private Target mBackgroundTarget;
private DisplayMetrics mMetrics;
private Timer mBackgroundTimer;
private String mBackgroundUrl;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mRowsAdapter = new ArrayObjectAdapter(new ListRowPresenter());
setSearchResultProvider(this);
setOnItemViewClickedListener(new OnItemViewClickedListener() {
@Override
public void onItemClicked(Presenter.ViewHolder itemViewHolder, Object item, RowPresenter.ViewHolder rowViewHolder, Row row) {
if (!(item instanceof BaseRowItem)) return;
ItemLauncher.launch((BaseRowItem) item, (ItemRowAdapter) ((ListRow)row).getAdapter(),((BaseRowItem) item).getIndex(), getActivity());
}
});
mDelayedLoad = new SearchRunnable(getActivity(), mRowsAdapter, getActivity().getIntent().getBooleanExtra("MusicOnly",false));
prepareBackgroundManager();
}
@Override
public ObjectAdapter getResultsAdapter() {
return mRowsAdapter;
}
private boolean textEdited = false;
@Override
public boolean onQueryTextChange(String newQuery) {
textEdited = true;
if (Utils.isNonEmpty(newQuery)) {
mHandler.removeCallbacks(mDelayedLoad);
mDelayedLoad.setQueryString(newQuery);
mHandler.postDelayed(mDelayedLoad, SEARCH_DELAY_MS);
} else {
mRowsAdapter.clear();
}
return true;
}
@Override
public boolean onQueryTextSubmit(String query) {
if (!textEdited) {
if (Utils.isNonEmpty(query)) {
mHandler.removeCallbacks(mDelayedLoad);
mDelayedLoad.setQueryString(query);
mHandler.postDelayed(mDelayedLoad, SEARCH_DELAY_MS);
} else {
mRowsAdapter.clear();
}
}
return true;
}
private void prepareBackgroundManager() {
BackgroundManager backgroundManager = BackgroundManager.getInstance(getActivity());
backgroundManager.attach(getActivity().getWindow());
mBackgroundTarget = new PicassoBackgroundManagerTarget(backgroundManager);
mDefaultBackground = null;
mMetrics = new DisplayMetrics();
getActivity().getWindowManager().getDefaultDisplay().getMetrics(mMetrics);
}
}

View File

@@ -1,108 +0,0 @@
package org.jellyfin.androidtv.search;
import android.app.Fragment;
import android.os.Bundle;
import android.os.Handler;
import androidx.leanback.app.RowsFragment;
import androidx.leanback.widget.ArrayObjectAdapter;
import androidx.leanback.widget.ListRow;
import androidx.leanback.widget.ListRowPresenter;
import androidx.leanback.widget.OnItemViewClickedListener;
import androidx.leanback.widget.Presenter;
import androidx.leanback.widget.Row;
import androidx.leanback.widget.RowPresenter;
import android.text.Editable;
import android.text.TextWatcher;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.EditText;
import org.jellyfin.androidtv.R;
import org.jellyfin.androidtv.itemhandling.BaseRowItem;
import org.jellyfin.androidtv.itemhandling.ItemLauncher;
import org.jellyfin.androidtv.itemhandling.ItemRowAdapter;
/**
* Created by Eric on 4/8/2015.
*/
public class MySearchFragmentNoSpeech extends Fragment {
private static final int SEARCH_DELAY_MS = 1500;
private RowsFragment mRowsFragment;
private EditText mSearchField;
private ArrayObjectAdapter mRowsAdapter;
private SearchRunnable mDelayedLoad;
private Handler mHandler = new Handler();
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View root = inflater.inflate(R.layout.fragment_empty_frame, container, false);
// Inject the RowsFragment in the results container
if (getChildFragmentManager().findFragmentById(R.id.emptyFrame) == null) {
mRowsFragment = new RowsFragment();
getChildFragmentManager().beginTransaction()
.replace(R.id.emptyFrame, mRowsFragment).commit();
} else {
mRowsFragment = (RowsFragment) getChildFragmentManager()
.findFragmentById(R.id.emptyFrame);
}
mRowsAdapter = new ArrayObjectAdapter(new ListRowPresenter());
mRowsFragment.setAdapter(mRowsAdapter);
// Setup item selection
mRowsFragment.setOnItemViewClickedListener(new OnItemViewClickedListener() {
@Override
public void onItemClicked(Presenter.ViewHolder itemViewHolder, Object item, RowPresenter.ViewHolder rowViewHolder, Row row) {
if (!(item instanceof BaseRowItem)) return;
ItemLauncher.launch((BaseRowItem) item, (ItemRowAdapter) ((ListRow)row).getAdapter(), ((BaseRowItem) item).getIndex(), getActivity());
}
});
mDelayedLoad = new SearchRunnable(getActivity(), mRowsAdapter, getActivity().getIntent().getBooleanExtra("MusicOnly",false));
return root;
}
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
// Setup search field listening
mSearchField = (EditText) getActivity().findViewById(R.id.searchField);
mSearchField.addTextChangedListener(new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
}
@Override
public void afterTextChanged(Editable s) {
if (s.length() > 0) {
mHandler.removeCallbacks(mDelayedLoad);
mDelayedLoad.setQueryString(s.toString());
mHandler.postDelayed(mDelayedLoad, SEARCH_DELAY_MS);
} else {
mRowsAdapter.clear();
}
}
});
}
}

View File

@@ -3,26 +3,34 @@ package org.jellyfin.androidtv.search;
import android.os.Bundle;
import android.speech.SpeechRecognizer;
import org.jellyfin.androidtv.R;
import org.jellyfin.androidtv.TvApp;
import org.jellyfin.androidtv.base.BaseActivity;
public class SearchActivity extends BaseActivity {
import androidx.fragment.app.Fragment;
public class SearchActivity extends BaseActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (SpeechRecognizer.isRecognitionAvailable(TvApp.getApplication())) {
setContentView(R.layout.activity_search);
} else {
setContentView(R.layout.activity_search_no_speech);
}
boolean isSpeechEnabled = SpeechRecognizer.isRecognitionAvailable(this);
// Determine fragment to use
Fragment searchFragment = isSpeechEnabled
? new LeanbackSearchFragment()
: new TextSearchFragment();
// Add fragment
getSupportFragmentManager()
.beginTransaction()
.replace(android.R.id.content, searchFragment)
.commit();
}
@Override
public boolean onSearchRequested() {
//re-start us
this.recreate();
// Reset layout
recreate();
return true;
}
}

View File

@@ -0,0 +1,67 @@
package org.jellyfin.androidtv.search;
import android.content.Context;
import android.os.Handler;
import org.jellyfin.androidtv.util.Utils;
import androidx.leanback.app.SearchSupportFragment;
import androidx.leanback.widget.ArrayObjectAdapter;
import androidx.leanback.widget.ListRowPresenter;
import androidx.leanback.widget.ObjectAdapter;
public class SearchProvider implements SearchSupportFragment.SearchResultProvider {
private static final int SEARCH_DELAY_MS = 1500;
private final Handler mHandler = new Handler();
private ArrayObjectAdapter mRowsAdapter;
private SearchRunnable mDelayedLoad;
SearchProvider(Context context, boolean musicOnly) {
mRowsAdapter = new ArrayObjectAdapter(new ListRowPresenter());
mDelayedLoad = new SearchRunnable(context, mRowsAdapter, musicOnly);
}
@Override
public ObjectAdapter getResultsAdapter() {
return mRowsAdapter;
}
@Override
public boolean onQueryTextChange(String query) {
search(query, true);
return true;
}
@Override
public boolean onQueryTextSubmit(String query) {
search(query, false);
return true;
}
/**
* Update search results
*
* @param query String to search for
* @param delayed When true the search is delayed by [SEARCH_DELAY_MS] milliseconds
*/
private void search(String query, boolean delayed) {
// Clear results when query is empty
if (Utils.isEmpty(query)) {
mRowsAdapter.clear();
return;
}
// Remove current delayed search (if any)
mHandler.removeCallbacks(mDelayedLoad);
// Update search string
mDelayedLoad.setQueryString(query);
// Schedule search depending on [delayed]
if (delayed) mHandler.postDelayed(mDelayedLoad, SEARCH_DELAY_MS);
else mHandler.post(mDelayedLoad);
}
}

View File

@@ -1,98 +1,128 @@
package org.jellyfin.androidtv.search;
import android.app.Activity;
import androidx.leanback.widget.ArrayObjectAdapter;
import androidx.leanback.widget.HeaderItem;
import androidx.leanback.widget.ListRow;
import android.content.Context;
import org.jellyfin.androidtv.R;
import org.jellyfin.androidtv.itemhandling.ItemRowAdapter;
import org.jellyfin.androidtv.presentation.CardPresenter;
import org.jellyfin.apiclient.interaction.EmptyResponse;
import org.jellyfin.apiclient.model.search.SearchQuery;
import androidx.leanback.widget.ArrayObjectAdapter;
import androidx.leanback.widget.HeaderItem;
import androidx.leanback.widget.ListRow;
import java.util.ArrayList;
/**
* Created by Eric on 4/8/2015.
*/
public class SearchRunnable implements Runnable {
private String searchString;
private Activity activity;
private Context context;
private ArrayObjectAdapter mRowsAdapter;
private boolean musicOnly;
private int searchesReceived;
private ArrayList<ItemRowAdapter> searchItemRows;
public void setQueryString(String value) {
searchString = value;
}
public SearchRunnable(Activity activity, ArrayObjectAdapter adapter, boolean musicOnly) {
this.activity = activity;
public SearchRunnable(Context context, ArrayObjectAdapter adapter, boolean musicOnly) {
this.context = context;
this.mRowsAdapter = adapter;
this.musicOnly = musicOnly;
this.searchItemRows = new ArrayList<>();
}
@Override
public void run() {
mRowsAdapter.clear();
searchItemRows.clear();
searchesReceived = 0;
if (!musicOnly) {
//Get search results by type
SearchQuery people = getSearchQuery(new String[] {"Person","People"});
ItemRowAdapter peopleAdapter = new ItemRowAdapter(people, new CardPresenter(), mRowsAdapter);
ListRow peopleRow = new ListRow(new HeaderItem(activity.getString(R.string.lbl_people)), peopleAdapter);
peopleAdapter.setRow(peopleRow);
peopleAdapter.Retrieve();
SearchQuery movies = getSearchQuery(new String[] {"Movie", "BoxSet"});
ItemRowAdapter movieAdapter = new ItemRowAdapter(movies, new CardPresenter(), mRowsAdapter);
ListRow movieRow = new ListRow(new HeaderItem(activity.getString(R.string.lbl_movies)), movieAdapter);
ListRow movieRow = new ListRow(new HeaderItem(context.getString(R.string.lbl_movies)), movieAdapter);
movieAdapter.setRow(movieRow);
movieAdapter.Retrieve();
retrieveSearchResult(movieAdapter);
SearchQuery tv = getSearchQuery(new String[] {"Series","Episode"});
SearchQuery tvSeries = getSearchQuery(new String[] {"Series"});
ItemRowAdapter tvSeriesAdapter = new ItemRowAdapter(tvSeries, new CardPresenter(), mRowsAdapter);
ListRow tvSeriesRow = new ListRow(new HeaderItem(context.getString(R.string.lbl_series)), tvSeriesAdapter);
tvSeriesAdapter.setRow(tvSeriesRow);
retrieveSearchResult(tvSeriesAdapter);
SearchQuery tv = getSearchQuery(new String[] {"Episode"});
ItemRowAdapter tvAdapter = new ItemRowAdapter(tv, new CardPresenter(), mRowsAdapter);
ListRow tvRow = new ListRow(new HeaderItem(activity.getString(R.string.lbl_tv)), tvAdapter);
ListRow tvRow = new ListRow(new HeaderItem(context.getString(R.string.lbl_episodes)), tvAdapter);
tvAdapter.setRow(tvRow);
tvAdapter.Retrieve();
retrieveSearchResult(tvAdapter);
SearchQuery people = getSearchQuery(new String[] {"Person","People"});
ItemRowAdapter peopleAdapter = new ItemRowAdapter(people, new CardPresenter(), mRowsAdapter);
ListRow peopleRow = new ListRow(new HeaderItem(context.getString(R.string.lbl_people)), peopleAdapter);
peopleAdapter.setRow(peopleRow);
retrieveSearchResult(peopleAdapter);
SearchQuery videos = getSearchQuery(new String[] {"Video"});
ItemRowAdapter videoAdapter = new ItemRowAdapter(videos, new CardPresenter(), mRowsAdapter);
ListRow videoRow = new ListRow(new HeaderItem(activity.getString(R.string.lbl_videos)), videoAdapter);
ListRow videoRow = new ListRow(new HeaderItem(context.getString(R.string.lbl_videos)), videoAdapter);
videoAdapter.setRow(videoRow);
videoAdapter.Retrieve();
retrieveSearchResult(videoAdapter);
SearchQuery recordings = getSearchQuery(new String[] {"Recording"});
ItemRowAdapter recordingAdapter = new ItemRowAdapter(recordings, new CardPresenter(), mRowsAdapter);
ListRow recordingRow = new ListRow(new HeaderItem(activity.getString(R.string.lbl_recordings)), recordingAdapter);
ListRow recordingRow = new ListRow(new HeaderItem(context.getString(R.string.lbl_recordings)), recordingAdapter);
recordingAdapter.setRow(recordingRow);
recordingAdapter.Retrieve();
retrieveSearchResult(recordingAdapter);
SearchQuery programs = getSearchQuery(new String[] {"Program"});
ItemRowAdapter programAdapter = new ItemRowAdapter(programs, new CardPresenter(), mRowsAdapter);
ListRow programRow = new ListRow(new HeaderItem(activity.getString(R.string.lbl_programs)), programAdapter);
ListRow programRow = new ListRow(new HeaderItem(context.getString(R.string.lbl_programs)), programAdapter);
programAdapter.setRow(programRow);
programAdapter.Retrieve();
retrieveSearchResult(programAdapter);
}
SearchQuery artists = getSearchQuery(new String[] {"MusicArtist"});
ItemRowAdapter artistAdapter = new ItemRowAdapter(artists, new CardPresenter(), mRowsAdapter);
ListRow artistRow = new ListRow(new HeaderItem(activity.getString(R.string.lbl_artists)), artistAdapter);
ListRow artistRow = new ListRow(new HeaderItem(context.getString(R.string.lbl_artists)), artistAdapter);
artistAdapter.setRow(artistRow);
artistAdapter.Retrieve();
retrieveSearchResult(artistAdapter);
SearchQuery albums = getSearchQuery(new String[] {"MusicAlbum"});
ItemRowAdapter albumAdapter = new ItemRowAdapter(albums, new CardPresenter(), mRowsAdapter);
ListRow albumRow = new ListRow(new HeaderItem(activity.getString(R.string.lbl_albums)), albumAdapter);
ListRow albumRow = new ListRow(new HeaderItem(context.getString(R.string.lbl_albums)), albumAdapter);
albumAdapter.setRow(albumRow);
albumAdapter.Retrieve();
retrieveSearchResult(albumAdapter);
SearchQuery songs = getSearchQuery(new String[] {"Audio"});
ItemRowAdapter songAdapter = new ItemRowAdapter(songs, new CardPresenter(), mRowsAdapter);
ListRow songRow = new ListRow(new HeaderItem(activity.getString(R.string.lbl_songs)), songAdapter);
ListRow songRow = new ListRow(new HeaderItem(context.getString(R.string.lbl_songs)), songAdapter);
songAdapter.setRow(songRow);
songAdapter.Retrieve();
retrieveSearchResult(songAdapter);
}
private void retrieveSearchResult(ItemRowAdapter itemRow) {
searchItemRows.add(itemRow);
itemRow.setRetrieveFinishedListener(new EmptyResponse() {
@Override
public void onResponse() {
searchesReceived++;
if (searchesReceived == searchItemRows.size()){
for (ItemRowAdapter itemRowAdapter : searchItemRows) {
itemRowAdapter.addToParentIfResultsReceived();
}
}
}
});
itemRow.Retrieve();
}
private SearchQuery getSearchQuery(String[] itemTypes) {
SearchQuery query = new SearchQuery();
query.setLimit(50);

View File

@@ -0,0 +1,84 @@
package org.jellyfin.androidtv.search;
import android.os.Bundle;
import android.text.Editable;
import android.text.TextWatcher;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.inputmethod.EditorInfo;
import android.widget.EditText;
import android.widget.TextView;
import org.jellyfin.androidtv.R;
import org.jellyfin.androidtv.itemhandling.BaseRowItem;
import org.jellyfin.androidtv.itemhandling.ItemLauncher;
import org.jellyfin.androidtv.itemhandling.ItemRowAdapter;
import androidx.fragment.app.Fragment;
import androidx.leanback.app.RowsSupportFragment;
import androidx.leanback.widget.ListRow;
public class TextSearchFragment extends Fragment implements TextWatcher, TextView.OnEditorActionListener {
private SearchProvider searchProvider;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
searchProvider = new SearchProvider(getContext(), getActivity().getIntent().getBooleanExtra("MusicOnly", false));
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
return inflater.inflate(R.layout.fragment_search_text, container, false);
}
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
// Add event listeners
EditText searchBar = getActivity().findViewById(R.id.search_bar);
searchBar.addTextChangedListener(this);
searchBar.setOnEditorActionListener(this);
// Set up result fragment
RowsSupportFragment rowsSupportFragment = (RowsSupportFragment) getChildFragmentManager().findFragmentById(R.id.results_frame);
rowsSupportFragment.setAdapter(searchProvider.getResultsAdapter());
// Create click listener
rowsSupportFragment.setOnItemViewClickedListener((itemViewHolder, item, rowViewHolder, row) -> {
if (!(item instanceof BaseRowItem)) return;
ItemLauncher.launch((BaseRowItem) item, (ItemRowAdapter) ((ListRow) row).getAdapter(), ((BaseRowItem) item).getIndex(), getActivity());
});
}
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
// No implementation
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
// No implementation
}
@Override
public void afterTextChanged(Editable s) {
searchProvider.onQueryTextChange(s.toString());
}
@Override
public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
// Detect keyboard "submit" actions
if (actionId == EditorInfo.IME_ACTION_SEARCH)
searchProvider.onQueryTextSubmit(v.getText().toString());
// Return "false" to automatically close keyboard
return false;
}
}

View File

@@ -113,7 +113,7 @@ public class SettingsFragment extends PreferenceFragment implements SharedPrefer
if (!TvApp.getApplication().getConfiguredAutoCredentials().getUserDto().getId().equals(TvApp.getApplication().getCurrentUser().getId())) {
listPreference.setEnabled(false);
pwPrompt.setEnabled(false);
extra = getActivity().getString(R.string.lbl_paren_login_as)+TvApp.getApplication().getConfiguredAutoCredentials().getUserDto().getName()+getActivity().getString(R.string.lbl_to_change_paren);
extra = getActivity().getString(R.string.lbl_paren_login_as) + TvApp.getApplication().getConfiguredAutoCredentials().getUserDto().getName() + getActivity().getString(R.string.lbl_to_change_paren);
}
listPreference.setSummary(getActivity().getString(R.string.lbl_login_as) + TvApp.getApplication().getConfiguredAutoCredentials().getUserDto().getName() + extra);
} else {
@@ -121,6 +121,18 @@ public class SettingsFragment extends PreferenceFragment implements SharedPrefer
pwPrompt.setEnabled(false);
}
} else {
if (listPreference.getKey().equals("pref_video_player")) {
boolean isExternal = listPreference.getValue().equals("external");
// enable/disable other related items
Preference direct = findPreference("pref_send_path_external");
if (direct != null) direct.setEnabled(isExternal);
for (String key : extPlayerVideoDep) {
Preference pref = findPreference(key);
if (pref != null) pref.setEnabled(!isExternal);
}
}
listPreference.setSummary(listPreference.getEntry());
}
}
@@ -131,14 +143,6 @@ public class SettingsFragment extends PreferenceFragment implements SharedPrefer
// enable other live tv direct only options
Preference live = findPreference("pref_enable_vlc_livetv");
if (live != null) live.setEnabled(cb.isChecked());
} else if (cb.getKey().equals("pref_video_use_external")) {
// enable/disable other related items
Preference direct = findPreference("pref_send_path_external");
if (direct != null) direct.setEnabled(cb.isChecked());
for (String key: extPlayerVideoDep) {
Preference pref = findPreference(key);
if (pref != null) pref.setEnabled(!cb.isChecked());
}
} else if (cb.getKey().equals("pref_live_tv_use_external")) {
// enable/disable other related items
for (String key: extPlayerLiveTvDep) {

View File

@@ -14,11 +14,12 @@ import org.jellyfin.androidtv.R;
import org.jellyfin.androidtv.TvApp;
import org.jellyfin.androidtv.util.Utils;
import org.jellyfin.androidtv.util.apiclient.AuthenticationHelper;
import org.jellyfin.apiclient.model.dto.UserDto;
import androidx.fragment.app.FragmentActivity;
public class DpadPwActivity extends Activity {
public class DpadPwActivity extends FragmentActivity {
private long lastKeyDown = Long.MAX_VALUE;
private long lastKeyUp = 0;

View File

@@ -1,12 +1,13 @@
package org.jellyfin.androidtv.startup;
import android.app.Activity;
import android.os.Bundle;
import org.jellyfin.androidtv.R;
import androidx.fragment.app.FragmentActivity;
public class SelectUserActivity extends Activity {
public class SelectUserActivity extends FragmentActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {

View File

@@ -8,9 +8,6 @@ import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.preference.PreferenceManager;
import androidx.annotation.NonNull;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import org.jellyfin.androidtv.BuildConfig;
import org.jellyfin.androidtv.R;
@@ -24,15 +21,12 @@ import org.jellyfin.androidtv.playback.PlaybackManager;
import org.jellyfin.androidtv.util.ProfileHelper;
import org.jellyfin.androidtv.util.Utils;
import org.jellyfin.androidtv.util.apiclient.AuthenticationHelper;
import java.util.ArrayList;
import org.jellyfin.apiclient.interaction.AndroidConnectionManager;
import org.jellyfin.apiclient.interaction.AndroidDevice;
import org.jellyfin.apiclient.interaction.ApiEventListener;
import org.jellyfin.apiclient.interaction.ConnectionResult;
import org.jellyfin.apiclient.interaction.IConnectionManager;
import org.jellyfin.apiclient.interaction.Response;
import org.jellyfin.apiclient.interaction.AndroidConnectionManager;
import org.jellyfin.apiclient.interaction.AndroidDevice;
import org.jellyfin.apiclient.interaction.VolleyHttpClient;
import org.jellyfin.apiclient.model.apiclient.ConnectionState;
import org.jellyfin.apiclient.model.dto.UserDto;
@@ -41,7 +35,14 @@ import org.jellyfin.apiclient.model.serialization.GsonJsonSerializer;
import org.jellyfin.apiclient.model.session.ClientCapabilities;
import org.jellyfin.apiclient.model.session.GeneralCommandType;
public class StartupActivity extends Activity {
import java.util.ArrayList;
import androidx.annotation.NonNull;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import androidx.fragment.app.FragmentActivity;
public class StartupActivity extends FragmentActivity {
private static final int NETWORK_PERMISSION = 1;
private TvApp application;
@@ -58,7 +59,7 @@ public class StartupActivity extends Activity {
//Migrate prefs
if (Integer.parseInt(application.getConfigVersion()) < 2) {
application.getSystemPrefs().edit().putString("sys_pref_config_version", "2").commit();
application.getSystemPrefs().edit().putString("sys_pref_config_version", "2").apply();
}
if (Integer.parseInt(application.getConfigVersion()) < 3) {
application.getPrefs().edit().putString("pref_max_bitrate", "0").apply();
@@ -69,6 +70,12 @@ public class StartupActivity extends Activity {
application.getPrefs().edit().putBoolean("pref_enable_info_panel", false).apply();
application.getSystemPrefs().edit().putString("sys_pref_config_version", "4").apply();
}
if (Integer.parseInt(application.getConfigVersion()) < 5) {
boolean useExternal = application.getPrefs().getBoolean("pref_video_use_external", false);
application.getPrefs().edit().putString("pref_video_player", useExternal ? "external" : "auto").apply();
application.getSystemPrefs().edit().putString("sys_pref_config_version", "5").apply();
}
//Ensure we have prefs
PreferenceManager.setDefaultValues(this, R.xml.preferences, false);
@@ -181,7 +188,8 @@ public class StartupActivity extends Activity {
}
// Check the server version
if (!AuthenticationHelper.isSupportedServerVersion(response.getServers().get(0))) {
if (!response.getServers().isEmpty() &&
!AuthenticationHelper.isSupportedServerVersion(response.getServers().get(0))) {
Utils.showToast(activity, activity.getString(R.string.msg_error_server_version, TvApp.MINIMUM_SERVER_VERSION));
AuthenticationHelper.automaticSignIn(connectionManager, activity);
return;

View File

@@ -2,7 +2,8 @@ package org.jellyfin.androidtv.ui;
import android.app.Activity;
import android.content.Context;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.Color;
import android.graphics.drawable.ColorDrawable;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.View;
@@ -32,7 +33,7 @@ public class AudioDelayPopup {
mPopup = new PopupWindow(layout, WIDTH, HEIGHT);
mPopup.setFocusable(true);
mPopup.setOutsideTouchable(true);
mPopup.setBackgroundDrawable(new BitmapDrawable()); // necessary for popup to dismiss
mPopup.setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT)); // necessary for popup to dismiss
mAnchor = anchor;

View File

@@ -2,7 +2,8 @@ package org.jellyfin.androidtv.ui;
import android.app.Activity;
import android.content.Context;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.Color;
import android.graphics.drawable.ColorDrawable;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.View;
@@ -45,7 +46,7 @@ public class DisplayPrefsPopup {
mPopup = new PopupWindow(layout, WIDTH, HEIGHT);
mPopup.setFocusable(true);
mPopup.setOutsideTouchable(true);
mPopup.setBackgroundDrawable(new BitmapDrawable()); // necessary for popup to dismiss
mPopup.setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT)); // necessary for popup to dismiss
mPopup.setAnimationStyle(R.style.PopupSlideInRight);
mAnchor = anchor;

View File

@@ -4,11 +4,13 @@ import android.content.Context;
import android.graphics.Typeface;
import androidx.appcompat.widget.AppCompatTextView;
import org.jellyfin.apiclient.model.dto.BaseItemType;
/**
* Created by Eric on 2/21/2015.
*/
public class GenreButton extends AppCompatTextView {
public GenreButton(Context context, Typeface font, int size, String text, String itemType) {
public GenreButton(Context context, Typeface font, int size, String text, BaseItemType itemType) {
super(context);
setTypeface(font);
setTextSize(size);

View File

@@ -1,6 +1,6 @@
package org.jellyfin.androidtv.ui;
import android.app.Fragment;
import androidx.fragment.app.Fragment;
import android.os.Bundle;
import androidx.leanback.widget.ObjectAdapter;
import androidx.leanback.widget.OnItemViewClickedListener;

View File

@@ -13,6 +13,7 @@ import org.jellyfin.androidtv.R;
import org.jellyfin.androidtv.TvApp;
import org.jellyfin.androidtv.itemhandling.BaseRowItem;
import org.jellyfin.androidtv.util.InfoLayoutHelper;
import org.jellyfin.apiclient.model.dto.BaseItemType;
/**
* Created by Eric on 7/27/2015.
@@ -49,7 +50,7 @@ public class ItemPanel extends RelativeLayout {
public void setItem(BaseRowItem item) {
if (item != null) {
title.setText(item.getFullName() + (item.getItemType() == BaseRowItem.ItemType.BaseItem && "Episode".equals(item.getBaseItem().getType()) ? " - " + item.getName() : ""));
title.setText(item.getFullName() + (item.getItemType() == BaseRowItem.ItemType.BaseItem && item.getBaseItemType() == BaseItemType.Episode ? " - " + item.getName() : ""));
if (TvApp.getApplication().getCurrentActivity() != null) InfoLayoutHelper.addInfoRow(TvApp.getApplication().getCurrentActivity(), item, infoRow, true, true);
summary.setText(item.getSummary());
}

View File

@@ -95,8 +95,8 @@ public class ItemRowView extends FrameLayout {
mBaseItem = item;
ourIndex = ndx + 1;
mIndexNo.setText(Integer.toString(ourIndex));
switch (item.getType()) {
case "Audio":
switch (item.getBaseItemType()) {
case Audio:
mItemName.setText(item.getName());
String artist = item.getArtists() != null && item.getArtists().size() > 0 ? item.getArtists().get(0) : !TextUtils.isEmpty(item.getAlbumArtist()) ? item.getAlbumArtist() : null;
if (!TextUtils.isEmpty(artist)) {

View File

@@ -5,7 +5,7 @@ import android.content.Context;
import android.content.DialogInterface;
import android.graphics.Color;
import android.graphics.Typeface;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.ColorDrawable;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.View;
@@ -62,7 +62,7 @@ public class LiveProgramDetailPopup {
mPopup = new PopupWindow(layout, width, NORMAL_HEIGHT);
mPopup.setFocusable(true);
mPopup.setOutsideTouchable(true);
mPopup.setBackgroundDrawable(new BitmapDrawable()); // necessary for popup to dismiss
mPopup.setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT)); // necessary for popup to dismiss
mPopup.setAnimationStyle(R.style.PopupSlideInTop);
mDTitle = layout.findViewById(R.id.title);
mDTitle.setTypeface(roboto);

View File

@@ -1,8 +1,9 @@
package org.jellyfin.androidtv.ui;
import android.content.Context;
import android.graphics.Color;
import android.graphics.Typeface;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.ColorDrawable;
import android.text.format.DateUtils;
import android.view.Gravity;
import android.view.LayoutInflater;
@@ -79,7 +80,7 @@ public class RecordPopup {
mPopup = new PopupWindow(layout, width, NORMAL_HEIGHT);
mPopup.setFocusable(true);
mPopup.setOutsideTouchable(true);
mPopup.setBackgroundDrawable(new BitmapDrawable()); // necessary for popup to dismiss
mPopup.setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT)); // necessary for popup to dismiss
mDTitle = (TextView)layout.findViewById(R.id.title);
mDTitle.setTypeface(roboto);

View File

@@ -0,0 +1,381 @@
package org.jellyfin.androidtv.ui.home;
import android.app.AlertDialog;
import android.content.DialogInterface;
import android.content.Intent;
import android.os.Bundle;
import android.os.Handler;
import android.util.ArrayMap;
import org.jellyfin.androidtv.R;
import org.jellyfin.androidtv.TvApp;
import org.jellyfin.androidtv.base.CustomMessage;
import org.jellyfin.androidtv.base.IMessageListener;
import org.jellyfin.androidtv.browsing.BrowseRowDef;
import org.jellyfin.androidtv.browsing.IRowLoader;
import org.jellyfin.androidtv.browsing.StdBrowseFragment;
import org.jellyfin.androidtv.channels.ChannelManager;
import org.jellyfin.androidtv.constants.HomeSectionType;
import org.jellyfin.androidtv.itemhandling.ItemRowAdapter;
import org.jellyfin.androidtv.livetv.LiveTvGuideActivity;
import org.jellyfin.androidtv.model.ChangeTriggerType;
import org.jellyfin.androidtv.model.LogonCredentials;
import org.jellyfin.androidtv.playback.AudioEventListener;
import org.jellyfin.androidtv.playback.MediaManager;
import org.jellyfin.androidtv.presentation.CardPresenter;
import org.jellyfin.androidtv.presentation.PositionableListRowPresenter;
import org.jellyfin.androidtv.presentation.ThemeManager;
import org.jellyfin.androidtv.querying.QueryType;
import org.jellyfin.androidtv.querying.StdItemQuery;
import org.jellyfin.androidtv.querying.ViewQuery;
import org.jellyfin.androidtv.util.apiclient.AuthenticationHelper;
import org.jellyfin.apiclient.interaction.Response;
import org.jellyfin.apiclient.model.entities.DisplayPreferences;
import org.jellyfin.apiclient.model.entities.LocationType;
import org.jellyfin.apiclient.model.entities.MediaType;
import org.jellyfin.apiclient.model.entities.SortOrder;
import org.jellyfin.apiclient.model.livetv.RecommendedProgramQuery;
import org.jellyfin.apiclient.model.livetv.RecordingQuery;
import org.jellyfin.apiclient.model.querying.ItemFields;
import org.jellyfin.apiclient.model.querying.ItemFilter;
import org.jellyfin.apiclient.model.querying.ItemSortBy;
import org.jellyfin.apiclient.model.querying.ItemsResult;
import org.jellyfin.apiclient.model.querying.NextUpQuery;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import androidx.leanback.widget.ArrayObjectAdapter;
import androidx.leanback.widget.ListRow;
public class HomeFragment extends StdBrowseFragment {
// Copied from jellyfin-web (homesections.js#getDefaultSection)
private static final HomeSectionType[] DEFAULT_SECTIONS = new HomeSectionType[]{
HomeSectionType.LIBRARY_TILES_SMALL,
HomeSectionType.RESUME,
HomeSectionType.RESUME_AUDIO,
HomeSectionType.LIVE_TV,
HomeSectionType.NEXT_UP,
HomeSectionType.LATEST_MEDIA,
HomeSectionType.NONE
};
private List<HomeFragmentRow> rows = new ArrayList<>();
private ItemsResult views;
private HomeFragmentNowPlayingRow nowPlaying;
private HomeFragmentLiveTVRow liveTVRow;
private HomeFragmentFooterRow footer;
private ChannelManager channelManager;
private AudioEventListener audioEventListener = new AudioEventListener() {
@Override
public void onQueueStatusChanged(boolean hasQueue) {
nowPlaying.update(mRowsAdapter);
}
};
@Override
public void onActivityCreated(Bundle savedInstanceState) {
MainTitle = this.getString(R.string.home_title);
super.onActivityCreated(savedInstanceState);
// Save last login so we can get back proper context on entry
try {
AuthenticationHelper.saveLoginCredentials(new LogonCredentials(TvApp.getApplication().getApiClient().getServerInfo(), TvApp.getApplication().getCurrentUser()), TvApp.CREDENTIALS_PATH);
} catch (IOException e) {
TvApp.getApplication().getLogger().ErrorException("Unable to save login credentials", e);
}
// Init leanback home channels;
channelManager = new ChannelManager();
// Get auto bitrate
TvApp.getApplication().determineAutoBitrate();
//First time audio message
if (!mApplication.getSystemPrefs().getBoolean("syspref_audio_warned", false)) {
mApplication.getSystemPrefs().edit().putBoolean("syspref_audio_warned", true).apply();
new AlertDialog.Builder(mActivity)
.setTitle(mApplication.getString(R.string.lbl_audio_capabilitites))
.setMessage(mApplication.getString(R.string.msg_audio_warning))
.setPositiveButton(mApplication.getString(R.string.btn_got_it), null)
.setNegativeButton(mApplication.getString(R.string.btn_set_compatible_audio), new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
mApplication.getPrefs().edit().putString("pref_audio_option", "1").apply();
}
})
.setCancelable(false)
.show();
}
ThemeManager.showWelcomeMessage();
//Subscribe to Audio messages
MediaManager.addAudioEventListener(audioEventListener);
// Setup activity messages
mActivity.registerMessageListener(new IMessageListener() {
@Override
public void onMessageReceived(CustomMessage message) {
if (message == CustomMessage.RefreshRows) {
if (hasResumeRow()) {
refreshRows();
}
}
}
});
if (mApplication.getPrefs().getBoolean("pref_live_tv_mode", false)) {
// Open guide activity and tell it to start last channel
Intent guide = new Intent(getActivity(), LiveTvGuideActivity.class);
guide.putExtra("loadLast", true);
startActivity(guide);
}
nowPlaying = new HomeFragmentNowPlayingRow(getActivity());
liveTVRow = new HomeFragmentLiveTVRow(getActivity());
footer = new HomeFragmentFooterRow(getActivity());
}
@Override
public void onResume() {
super.onResume();
// Update leanback channels
channelManager.update();
//make sure rows have had a chance to be created
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
nowPlaying.update(mRowsAdapter);
}
}, 750);
}
@Override
public void onDestroy() {
super.onDestroy();
MediaManager.removeAudioEventListener(audioEventListener);
}
@Override
protected void setupEventListeners() {
super.setupEventListeners();
mClickedListener.registerListener((itemViewHolder, item, rowViewHolder, row) -> {
liveTVRow.onItemClicked(itemViewHolder, item, rowViewHolder, row);
footer.onItemClicked(itemViewHolder, item, rowViewHolder, row);
});
}
public void addSection(HomeSectionType type) {
switch (type) {
case LATEST_MEDIA:
rows.add(loadRecentlyAdded());
break;
case LIBRARY_TILES_SMALL:
rows.add(loadLibraryTiles());
break;
case LIBRARY_BUTTONS:
rows.add(loadLibraryButtons());
break;
case RESUME:
rows.add(loadResumeVideo());
break;
case RESUME_AUDIO:
rows.add(loadResumeAudio());
break;
case ACTIVE_RECORDINGS:
rows.add(loadLatestLiveTvRecordings());
break;
case NEXT_UP:
rows.add(loadNextUp());
break;
case LIVE_TV:
if (TvApp.getApplication().getCurrentUser().getPolicy().getEnableLiveTvAccess()) {
rows.add(liveTVRow);
rows.add(loadOnNow());
}
break;
}
}
@Override
protected void setupQueries(final IRowLoader rowLoader) {
TvApp application = TvApp.getApplication();
// Update the views before creating rows
application.getApiClient().GetUserViews(application.getCurrentUser().getId(), new Response<ItemsResult>() {
@Override
public void onResponse(ItemsResult response) {
views = response;
// Use "emby" as app because jellyfin-web version uses the same
TvApp.getApplication().getDisplayPrefsAsync("usersettings", "emby", new Response<DisplayPreferences>() {
@Override
public void onResponse(DisplayPreferences response) {
HashMap<String, String> prefs = response.getCustomPrefs();
// Section key pattern
Pattern pattern = Pattern.compile("^homesection(\\d+)$");
// Add sections to map first to make sure they stay in the correct order
ArrayMap<Integer, HomeSectionType> sections = new ArrayMap<>();
// Set defaults
for (int i = 0; i < DEFAULT_SECTIONS.length; i++) {
sections.put(i, DEFAULT_SECTIONS[i]);
}
// Overwrite with user-preferred
for (String key : prefs.keySet()) {
Matcher matcher = pattern.matcher(key);
if (!matcher.matches()) continue;
int index = Integer.parseInt(matcher.group(1));
HomeSectionType sectionType = HomeSectionType.getByName(prefs.get(key));
if (sectionType != null)
sections.put(index, sectionType);
}
// Fallback when no customization is done by the user
rows.clear();
// Actually add the sections
for (HomeSectionType section : sections.values()) {
if (section != HomeSectionType.NONE)
addSection(section);
}
loadRows();
}
@Override
public void onError(Exception exception) {
mApplication.getLogger().ErrorException("Unable to retrieve home sections", exception);
// Fallback to default sections
for (HomeSectionType section : DEFAULT_SECTIONS) {
addSection(section);
}
loadRows();
}
});
}
});
}
@Override
public void loadRows(List<BrowseRowDef> rows) {
// Override to make sure it is ignored because we have our custom row management
}
private void loadRows() {
// Add sections to layout
mRowsAdapter = new ArrayObjectAdapter(new PositionableListRowPresenter());
mCardPresenter = new CardPresenter();
nowPlaying.addToRowsAdapter(mCardPresenter, mRowsAdapter);
for (HomeFragmentRow row : rows)
row.addToRowsAdapter(mCardPresenter, mRowsAdapter);
footer.addToRowsAdapter(mCardPresenter, mRowsAdapter);
setAdapter(mRowsAdapter);
}
private HomeFragmentRow loadRecentlyAdded() {
return new HomeFragmentLatestRow(this.views);
}
private HomeFragmentRow loadLibraryTiles() {
ViewQuery query = new ViewQuery();
return new HomeFragmentBrowseRowDefRow(new BrowseRowDef(mApplication.getString(R.string.lbl_my_media), query));
}
private HomeFragmentRow loadLibraryButtons() {
// Currently not implemented, fallback to large "library tiles" until this gets implemented
return loadLibraryTiles();
}
private HomeFragmentRow loadResume(String title, String[] mediaTypes) {
StdItemQuery query = new StdItemQuery();
query.setMediaTypes(mediaTypes);
query.setRecursive(true);
query.setImageTypeLimit(1);
query.setEnableTotalRecordCount(false);
query.setCollapseBoxSetItems(false);
query.setExcludeLocationTypes(new LocationType[]{LocationType.Virtual});
query.setLimit(50);
query.setFilters(new ItemFilter[]{ItemFilter.IsResumable});
query.setSortBy(new String[]{ItemSortBy.DatePlayed});
query.setSortOrder(SortOrder.Descending);
return new HomeFragmentBrowseRowDefRow(new BrowseRowDef(title, query, 0, new ChangeTriggerType[]{ChangeTriggerType.VideoQueueChange, ChangeTriggerType.TvPlayback, ChangeTriggerType.MoviePlayback}));
}
private HomeFragmentRow loadResumeVideo() {
return loadResume(mApplication.getString(R.string.lbl_continue_watching), new String[]{MediaType.Video});
}
private HomeFragmentRow loadResumeAudio() {
return loadResume(mApplication.getString(R.string.lbl_continue_watching), new String[]{MediaType.Audio});
}
private HomeFragmentRow loadLatestLiveTvRecordings() {
RecordingQuery query = new RecordingQuery();
query.setFields(new ItemFields[]{ItemFields.Overview, ItemFields.PrimaryImageAspectRatio});
query.setUserId(TvApp.getApplication().getCurrentUser().getId());
query.setEnableImages(true);
query.setLimit(40);
return new HomeFragmentBrowseRowDefRow(new BrowseRowDef(mActivity.getString(R.string.lbl_recordings), query));
}
private HomeFragmentRow loadNextUp() {
NextUpQuery query = new NextUpQuery();
query.setUserId(TvApp.getApplication().getCurrentUser().getId());
query.setImageTypeLimit(1);
query.setLimit(50);
query.setFields(new ItemFields[]{ItemFields.PrimaryImageAspectRatio, ItemFields.Overview});
return new HomeFragmentBrowseRowDefRow(new BrowseRowDef(mApplication.getString(R.string.lbl_next_up), query, new ChangeTriggerType[]{ChangeTriggerType.TvPlayback}));
}
private HomeFragmentRow loadOnNow() {
RecommendedProgramQuery query = new RecommendedProgramQuery();
query.setIsAiring(true);
query.setFields(new ItemFields[]{ItemFields.Overview, ItemFields.PrimaryImageAspectRatio, ItemFields.ChannelInfo});
query.setUserId(TvApp.getApplication().getCurrentUser().getId());
query.setImageTypeLimit(1);
query.setEnableTotalRecordCount(false);
query.setLimit(20);
return new HomeFragmentBrowseRowDefRow(new BrowseRowDef(mApplication.getString(R.string.lbl_on_now), query));
}
private boolean hasResumeRow() {
if (mRowsAdapter == null) return true;
for (int i = 0; i < mRowsAdapter.size(); i++) {
ListRow row = (ListRow) mRowsAdapter.get(i);
if (row.getAdapter() instanceof ItemRowAdapter && ((ItemRowAdapter) row.getAdapter()).getQueryType().equals(QueryType.ContinueWatching))
return true;
}
return false;
}
}

View File

@@ -0,0 +1,75 @@
package org.jellyfin.androidtv.ui.home;
import org.jellyfin.androidtv.browsing.BrowseRowDef;
import org.jellyfin.androidtv.itemhandling.ItemRowAdapter;
import org.jellyfin.androidtv.presentation.CardPresenter;
import org.jellyfin.androidtv.querying.QueryType;
import org.jellyfin.androidtv.querying.ViewQuery;
import androidx.leanback.widget.ArrayObjectAdapter;
import androidx.leanback.widget.HeaderItem;
import androidx.leanback.widget.ListRow;
public class HomeFragmentBrowseRowDefRow extends HomeFragmentRow {
private BrowseRowDef browseRowDef;
public HomeFragmentBrowseRowDefRow(BrowseRowDef browseRowDef) {
this.browseRowDef = browseRowDef;
}
@Override
public void addToRowsAdapter(CardPresenter cardPresenter, ArrayObjectAdapter rowsAdapter) {
HeaderItem header = new HeaderItem(browseRowDef.getHeaderText());
ItemRowAdapter rowAdapter;
switch (browseRowDef.getQueryType()) {
case NextUp:
rowAdapter = new ItemRowAdapter(browseRowDef.getNextUpQuery(), true, cardPresenter, rowsAdapter);
break;
case LatestItems:
rowAdapter = new ItemRowAdapter(browseRowDef.getLatestItemsQuery(), true, cardPresenter, rowsAdapter);
break;
case Season:
rowAdapter = new ItemRowAdapter(browseRowDef.getSeasonQuery(), cardPresenter, rowsAdapter);
break;
case Upcoming:
rowAdapter = new ItemRowAdapter(browseRowDef.getUpcomingQuery(), cardPresenter, rowsAdapter);
break;
case Views:
rowAdapter = new ItemRowAdapter(new ViewQuery(), cardPresenter, rowsAdapter);
break;
case SimilarSeries:
rowAdapter = new ItemRowAdapter(browseRowDef.getSimilarQuery(), QueryType.SimilarSeries, cardPresenter, rowsAdapter);
break;
case SimilarMovies:
rowAdapter = new ItemRowAdapter(browseRowDef.getSimilarQuery(), QueryType.SimilarMovies, cardPresenter, rowsAdapter);
break;
case Persons:
rowAdapter = new ItemRowAdapter(browseRowDef.getPersonsQuery(), browseRowDef.getChunkSize(), cardPresenter, rowsAdapter);
break;
case LiveTvChannel:
rowAdapter = new ItemRowAdapter(browseRowDef.getTvChannelQuery(), 40, cardPresenter, rowsAdapter);
break;
case LiveTvProgram:
rowAdapter = new ItemRowAdapter(browseRowDef.getProgramQuery(), cardPresenter, rowsAdapter);
break;
case LiveTvRecording:
rowAdapter = new ItemRowAdapter(browseRowDef.getRecordingQuery(), browseRowDef.getChunkSize(), cardPresenter, rowsAdapter);
break;
case LiveTvRecordingGroup:
rowAdapter = new ItemRowAdapter(browseRowDef.getRecordingGroupQuery(), cardPresenter, rowsAdapter);
break;
default:
rowAdapter = new ItemRowAdapter(browseRowDef.getQuery(), browseRowDef.getChunkSize(), browseRowDef.getPreferParentThumb(), browseRowDef.isStaticHeight(), cardPresenter, rowsAdapter, browseRowDef.getQueryType());
break;
}
rowAdapter.setReRetrieveTriggers(browseRowDef.getChangeTriggers());
ListRow row = new ListRow(header, rowAdapter);
rowAdapter.setRow(row);
rowAdapter.Retrieve();
rowsAdapter.add(row);
}
}

View File

@@ -0,0 +1,69 @@
package org.jellyfin.androidtv.ui.home;
import android.app.Activity;
import android.content.Intent;
import org.jellyfin.androidtv.R;
import org.jellyfin.androidtv.TvApp;
import org.jellyfin.androidtv.presentation.CardPresenter;
import org.jellyfin.androidtv.presentation.GridButtonPresenter;
import org.jellyfin.androidtv.settings.SettingsActivity;
import org.jellyfin.androidtv.startup.SelectUserActivity;
import org.jellyfin.androidtv.ui.GridButton;
import androidx.leanback.widget.ArrayObjectAdapter;
import androidx.leanback.widget.HeaderItem;
import androidx.leanback.widget.ListRow;
import androidx.leanback.widget.OnItemViewClickedListener;
import androidx.leanback.widget.Presenter;
import androidx.leanback.widget.Row;
import androidx.leanback.widget.RowPresenter;
public class HomeFragmentFooterRow extends HomeFragmentRow implements OnItemViewClickedListener {
private static final int LOGOUT = 0;
private static final int SETTINGS = 1;
private Activity activity;
public HomeFragmentFooterRow(Activity activity) {
this.activity = activity;
}
@Override
public void addToRowsAdapter(CardPresenter cardPresenter, ArrayObjectAdapter rowsAdapter) {
HeaderItem header = new HeaderItem(rowsAdapter.size(), activity.getString(R.string.lbl_settings));
GridButtonPresenter presenter = new GridButtonPresenter();
ArrayObjectAdapter adapter = new ArrayObjectAdapter(presenter);
adapter.add(new GridButton(SETTINGS, activity.getString(R.string.lbl_settings), R.drawable.tile_settings));
adapter.add(new GridButton(LOGOUT, activity.getString(R.string.lbl_logout), R.drawable.tile_logout));
rowsAdapter.add(new ListRow(header, adapter));
}
public void onItemClicked(Presenter.ViewHolder itemViewHolder, Object item, RowPresenter.ViewHolder rowViewHolder, Row row) {
if (!(item instanceof GridButton)) return;
switch (((GridButton) item).getId()) {
case LOGOUT:
TvApp app = TvApp.getApplication();
// Present user selection
app.setLoginApiClient(app.getApiClient());
// Open login activity
Intent selectUserIntent = new Intent(activity, SelectUserActivity.class);
selectUserIntent.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY); // Disallow back button
activity.startActivity(selectUserIntent);
activity.finish();
break;
case SETTINGS:
Intent settingsIntent = new Intent(activity, SettingsActivity.class);
activity.startActivity(settingsIntent);
break;
}
}
}

View File

@@ -0,0 +1,57 @@
package org.jellyfin.androidtv.ui.home;
import org.jellyfin.androidtv.R;
import org.jellyfin.androidtv.TvApp;
import org.jellyfin.androidtv.browsing.BrowseRowDef;
import org.jellyfin.androidtv.model.ChangeTriggerType;
import org.jellyfin.androidtv.presentation.CardPresenter;
import org.jellyfin.apiclient.model.configuration.UserConfiguration;
import org.jellyfin.apiclient.model.dto.BaseItemDto;
import org.jellyfin.apiclient.model.querying.ItemFields;
import org.jellyfin.apiclient.model.querying.ItemsResult;
import org.jellyfin.apiclient.model.querying.LatestItemsQuery;
import java.util.Arrays;
import java.util.List;
import androidx.leanback.widget.ArrayObjectAdapter;
class HomeFragmentLatestRow extends HomeFragmentRow {
// See: https://github.com/jellyfin/jellyfin-web/blob/bbf1f8d5df66a58c29f07969caa476852d86ab4a/src/components/homesections/homesections.js#L292
private static final List<String> EXCLUDED_COLLECTION_TYPES = Arrays.asList("playlists", "livetv", "boxsets", "channels");
private final ItemsResult views;
public HomeFragmentLatestRow(ItemsResult views) {
this.views = views;
}
@Override
public void addToRowsAdapter(CardPresenter cardPresenter, ArrayObjectAdapter rowsAdapter) {
TvApp application = TvApp.getApplication();
// Get configuration (to find excluded items)
UserConfiguration configuration = application.getCurrentUser().getConfiguration();
// Create a list of views to include
List<String> latestItemsExcludes = Arrays.asList(configuration.getLatestItemsExcludes());
for (BaseItemDto item : views.getItems()) {
// Skip excluded collection types
if (EXCLUDED_COLLECTION_TYPES.contains(item.getCollectionType())) continue;
// Skip user configured excluded items
if (latestItemsExcludes.contains(item.getId())) continue;
// Create query and add row
LatestItemsQuery query = new LatestItemsQuery();
query.setFields(new ItemFields[]{ItemFields.PrimaryImageAspectRatio, ItemFields.Overview});
query.setImageTypeLimit(1);
query.setParentId(item.getId());
query.setGroupItems(true);
query.setLimit(50);
HomeFragmentBrowseRowDefRow row = new HomeFragmentBrowseRowDefRow(new BrowseRowDef(String.format("%s %s", application.getString(R.string.lbl_latest), item.getName()), query, new ChangeTriggerType[]{ChangeTriggerType.LibraryUpdated}));
row.addToRowsAdapter(cardPresenter, rowsAdapter);
}
}
}

View File

@@ -0,0 +1,90 @@
package org.jellyfin.androidtv.ui.home
import android.app.Activity
import android.content.Intent
import androidx.leanback.widget.ArrayObjectAdapter
import androidx.leanback.widget.HeaderItem
import androidx.leanback.widget.ListRow
import androidx.leanback.widget.OnItemViewClickedListener
import androidx.leanback.widget.Presenter
import androidx.leanback.widget.Row
import androidx.leanback.widget.RowPresenter
import org.jellyfin.androidtv.R
import org.jellyfin.androidtv.TvApp
import org.jellyfin.androidtv.browsing.BrowseRecordingsActivity
import org.jellyfin.androidtv.browsing.BrowseScheduleActivity
import org.jellyfin.androidtv.browsing.UserViewActivity
import org.jellyfin.androidtv.livetv.LiveTvGuideActivity
import org.jellyfin.androidtv.presentation.CardPresenter
import org.jellyfin.androidtv.presentation.GridButtonPresenter
import org.jellyfin.androidtv.ui.GridButton
import org.jellyfin.apiclient.model.dto.BaseItemDto
class HomeFragmentLiveTVRow(val activity: Activity) : HomeFragmentRow(), OnItemViewClickedListener {
override fun addToRowsAdapter(cardPresenter: CardPresenter?, rowsAdapter: ArrayObjectAdapter) {
val header = HeaderItem(rowsAdapter.size().toLong(), activity.getString(R.string.pref_live_tv_cat))
val adapter = ArrayObjectAdapter(GridButtonPresenter())
// Live TV Guide button
adapter.add(GridButton(TvApp.LIVE_TV_GUIDE_OPTION_ID, activity.getString(R.string.lbl_live_tv_guide), R.drawable.tile_port_guide))
// Live TV Recordings button
adapter.add(GridButton(TvApp.LIVE_TV_RECORDINGS_OPTION_ID, activity.getString(R.string.lbl_recorded_tv), R.drawable.tile_port_record))
if (TvApp.getApplication().canManageRecordings()) {
// Recording Schedule button
adapter.add(GridButton(TvApp.LIVE_TV_SCHEDULE_OPTION_ID, activity.getString(R.string.lbl_schedule), R.drawable.tile_port_time))
// Recording Series button
adapter.add(GridButton(TvApp.LIVE_TV_SERIES_OPTION_ID, activity.getString(R.string.lbl_series), R.drawable.tile_port_series_timer))
}
rowsAdapter.add(ListRow(header, adapter))
}
override fun onItemClicked(itemViewHolder: Presenter.ViewHolder?, item: Any?, rowViewHolder: RowPresenter.ViewHolder?, row: Row?) {
if (item !is GridButton) return
when (item.id) {
TvApp.LIVE_TV_GUIDE_OPTION_ID -> {
activity.startActivity(
Intent(activity, LiveTvGuideActivity::class.java)
)
}
TvApp.LIVE_TV_RECORDINGS_OPTION_ID -> {
activity.startActivity(
Intent(activity, BrowseRecordingsActivity::class.java).apply {
putExtra(
"Folder",
TvApp.getApplication().serializer.SerializeToString(
BaseItemDto().apply {
id = ""
name = activity.getString(R.string.lbl_recorded_tv)
}
))
}
)
}
TvApp.LIVE_TV_SCHEDULE_OPTION_ID -> {
activity.startActivity(
Intent(activity, BrowseScheduleActivity::class.java)
)
}
TvApp.LIVE_TV_SERIES_OPTION_ID -> {
activity.startActivity(
Intent(activity, UserViewActivity::class.java).apply {
putExtra(
"Folder",
TvApp.getApplication().serializer.SerializeToString(
BaseItemDto().apply {
id = "SERIESTIMERS"
collectionType = "SeriesTimers"
name = activity.getString(R.string.lbl_series_recordings)
}
)
)
}
)
}
}
}
}

View File

@@ -0,0 +1,48 @@
package org.jellyfin.androidtv.ui.home;
import android.content.Context;
import org.jellyfin.androidtv.R;
import org.jellyfin.androidtv.playback.MediaManager;
import org.jellyfin.androidtv.presentation.CardPresenter;
import androidx.leanback.widget.ArrayObjectAdapter;
import androidx.leanback.widget.HeaderItem;
import androidx.leanback.widget.ListRow;
class HomeFragmentNowPlayingRow extends HomeFragmentRow {
private final Context context;
private ListRow row;
public HomeFragmentNowPlayingRow(Context context) {
this.context = context;
}
@Override
public void addToRowsAdapter(CardPresenter cardPresenter, ArrayObjectAdapter rowsAdapter) {
update(rowsAdapter);
}
private void add(ArrayObjectAdapter rowsAdapter) {
if (row == null) {
row = new ListRow(new HeaderItem(context.getString(R.string.lbl_now_playing)), MediaManager.getManagedAudioQueue());
rowsAdapter.add(0, row);
}
}
private void remove(ArrayObjectAdapter rowsAdapter) {
if (row != null) {
rowsAdapter.remove(row);
row = null;
}
}
public void update(ArrayObjectAdapter rowsAdapter) {
if (MediaManager.isPlayingAudio()) {
add(rowsAdapter);
} else {
remove(rowsAdapter);
}
}
}

View File

@@ -0,0 +1,10 @@
package org.jellyfin.androidtv.ui.home;
import org.jellyfin.androidtv.presentation.CardPresenter;
import androidx.leanback.widget.ArrayObjectAdapter;
public abstract class HomeFragmentRow {
abstract public void addToRowsAdapter(CardPresenter cardPresenter, ArrayObjectAdapter rowsAdapter);
}

View File

@@ -4,28 +4,31 @@ import android.app.ProgressDialog;
import android.content.Context;
import android.os.Handler;
import androidx.annotation.NonNull;
import org.jellyfin.androidtv.R;
import org.jellyfin.androidtv.TvApp;
/**
* Created by Eric on 12/28/2014.
*/
public class DelayedMessage {
private String title = TvApp.getApplication().getString(R.string.lbl_please_wait);
private String message = TvApp.getApplication().getString(R.string.msg_little_longer);
private Runnable runnable;
private final String title;
private final String message;
private final Handler handler;
private final Runnable runnable;
private ProgressDialog dialog;
private Handler handler;
public DelayedMessage(final Context activity) {
public DelayedMessage(@NonNull final Context activity) {
this(activity, 750);
}
public DelayedMessage(final Context activity, int delay) {
handler = new Handler();
public DelayedMessage(@NonNull final Context activity, int delay) {
title = TvApp.getApplication().getString(R.string.lbl_please_wait);
message = TvApp.getApplication().getString(R.string.msg_little_longer);
handler = new Handler(activity.getMainLooper());
runnable = new Runnable() {
@Override
public void run() {
if (activity != null) dialog = ProgressDialog.show(activity, title, message);
dialog = ProgressDialog.show(activity, title, message);
}
};
handler.postDelayed(runnable, delay);
@@ -33,6 +36,8 @@ public class DelayedMessage {
public void Cancel() {
handler.removeCallbacks(runnable);
if (dialog != null) dialog.dismiss();
if (dialog != null) {
dialog.dismiss();
}
}
}

View File

@@ -7,6 +7,7 @@ public class DeviceUtils {
private static final String FIRE_STICK_MODEL = "AFTM";
private static final String NEXUS_MODEL = "Nexus Player";
private static final String SHIELD_MODEL = "SHIELD Android TV";
private static final String BEYONDTV_MODEL = "BeyondTV";
public static boolean isFireTv() {
return Build.MODEL.startsWith(FIRE_TV_PREFIX);
@@ -24,6 +25,10 @@ public class DeviceUtils {
return Build.MODEL.equals(NEXUS_MODEL);
}
public static boolean isBeyondTv() {
return Build.MODEL.equals(BEYONDTV_MODEL);
}
public static boolean is50() {
return Build.VERSION.SDK_INT >= 21;
}

View File

@@ -11,6 +11,7 @@ import java.util.List;
import org.jellyfin.apiclient.interaction.ApiClient;
import org.jellyfin.apiclient.model.dto.BaseItemDto;
import org.jellyfin.apiclient.model.dto.BaseItemPerson;
import org.jellyfin.apiclient.model.dto.BaseItemType;
import org.jellyfin.apiclient.model.dto.ImageOptions;
import org.jellyfin.apiclient.model.dto.StudioDto;
import org.jellyfin.apiclient.model.dto.UserDto;
@@ -26,8 +27,8 @@ public class ImageUtils {
private static final int MAX_PRIMARY_IMAGE_HEIGHT = 370;
private static final List<String> THUMB_FALLBACK_TYPES = Collections.singletonList("Episode");
private static final List<String> PROGRESS_INDICATOR_TYPES = Arrays.asList("Episode", "Movie", "MusicVideo", "Video");
private static final List<BaseItemType> THUMB_FALLBACK_TYPES = Collections.singletonList(BaseItemType.Episode);
private static final List<BaseItemType> PROGRESS_INDICATOR_TYPES = Arrays.asList(BaseItemType.Episode, BaseItemType.Movie, BaseItemType.MusicVideo, BaseItemType.Video);
public static Double getImageAspectRatio(BaseItemDto item, boolean preferParentThumb) {
if (preferParentThumb &&
@@ -35,7 +36,7 @@ public class ImageUtils {
return ASPECT_RATIO_16_9;
}
if (THUMB_FALLBACK_TYPES.contains(item.getType())) {
if (THUMB_FALLBACK_TYPES.contains(item.getBaseItemType())) {
if (item.getPrimaryImageAspectRatio() != null) {
return item.getPrimaryImageAspectRatio();
}
@@ -134,8 +135,8 @@ public class ImageUtils {
options.setImageType(ImageType.Banner);
UserItemDataDto userData = item.getUserData();
if (userData != null && !"MusicArtist".equals(item.getType()) && !"MusicAlbum".equals(item.getType())) {
if (PROGRESS_INDICATOR_TYPES.contains(item.getType()) &&
if (userData != null && item.getBaseItemType() != BaseItemType.MusicArtist && item.getBaseItemType() != BaseItemType.MusicAlbum) {
if (PROGRESS_INDICATOR_TYPES.contains(item.getBaseItemType()) &&
userData.getPlayedPercentage() != null &&
userData.getPlayedPercentage() > 0 &&
userData.getPlayedPercentage() < 99) {
@@ -163,7 +164,7 @@ public class ImageUtils {
}
public static String getPrimaryImageUrl(BaseItemDto item, ApiClient apiClient, boolean showProgress, boolean preferParentThumb, boolean preferSeriesPoster, int maxHeight) {
if (item.getType().equals("SeriesTimer")) {
if (item.getBaseItemType() == BaseItemType.SeriesTimer) {
return getResourceUrl(R.drawable.tile_land_series_timer);
}
@@ -172,7 +173,7 @@ public class ImageUtils {
String imageTag = item.getImageTags() != null ? item.getImageTags().get(ImageType.Primary) : null;
ImageType imageType = ImageType.Primary;
if (preferSeriesPoster && item.getType().equals("Episode")) {
if (preferSeriesPoster && item.getBaseItemType() == BaseItemType.Episode) {
if (item.getSeasonId() != null) {
imageTag = null;
itemId = item.getSeasonId();
@@ -180,7 +181,7 @@ public class ImageUtils {
imageTag = item.getSeriesPrimaryImageTag();
itemId = item.getSeriesId();
}
} else if (preferParentThumb || (item.getType().equals("Episode") && imageTag == null)) {
} else if (preferParentThumb || (item.getBaseItemType() == BaseItemType.Episode && imageTag == null)) {
//try for thumb of season or series
if (item.getParentThumbImageTag() != null) {
imageTag = item.getParentThumbImageTag();
@@ -192,13 +193,16 @@ public class ImageUtils {
imageType = ImageType.Thumb;
}
} else {
if (item.getType().equals("Season") && imageTag == null) {
if (item.getBaseItemType() == BaseItemType.Season && imageTag == null) {
imageTag = item.getSeriesPrimaryImageTag();
itemId = item.getSeriesId();
} else if (item.getBaseItemType() == BaseItemType.Program && item.getHasThumb()) {
imageTag = item.getImageTags().get(ImageType.Thumb);
imageType = ImageType.Thumb;
}
}
if ("Audio".equals(item.getType()) && !item.getHasPrimaryImage()) {
if (item.getBaseItemType() == BaseItemType.Audio && !item.getHasPrimaryImage()) {
//Try the album or artist
if (item.getAlbumId() != null && item.getAlbumPrimaryImageTag() != null) {
imageTag = item.getAlbumPrimaryImageTag();
@@ -215,7 +219,7 @@ public class ImageUtils {
UserItemDataDto userData = item.getUserData();
if (userData != null) {
if (showProgress && PROGRESS_INDICATOR_TYPES.contains(item.getType()) &&
if (showProgress && PROGRESS_INDICATOR_TYPES.contains(item.getBaseItemType()) &&
userData.getPlayedPercentage() != null &&
userData.getPlayedPercentage() > 0 &&
userData.getPlayedPercentage() < 99) {

View File

@@ -18,6 +18,7 @@ import java.util.Calendar;
import java.util.Date;
import org.jellyfin.apiclient.model.dto.BaseItemDto;
import org.jellyfin.apiclient.model.dto.BaseItemType;
import org.jellyfin.apiclient.model.entities.MediaStream;
import org.jellyfin.apiclient.model.entities.SeriesStatus;
@@ -47,31 +48,31 @@ public class InfoLayoutHelper {
public static void addInfoRow(Activity activity, BaseItemDto item, LinearLayout layout, boolean includeRuntime, boolean includeEndTime, MediaStream audioStream) {
layout.removeAllViews();
addCriticInfo(activity, item, layout);
switch (item.getType()) {
case "Episode":
switch (item.getBaseItemType()) {
case Episode:
addSeasonEpisode(activity, item, layout);
addDate(activity, item, layout);
break;
case "BoxSet":
case BoxSet:
addBoxSetCounts(activity, item, layout);
break;
case "Series":
case Series:
//addSeasonCount(activity, item, layout);
addSeriesAirs(activity, item, layout);
addDate(activity, item, layout);
includeEndTime = false;
break;
case "Program":
case Program:
addProgramInfo(activity, item, layout);
break;
case "RecordingGroup":
case RecordingGroup:
addRecordingCount(activity, item, layout);
break;
case "MusicArtist":
case MusicArtist:
Integer artistAlbums = item.getAlbumCount() != null ? item.getAlbumCount() : item.getChildCount();
addCount(activity, artistAlbums, layout, artistAlbums != null && artistAlbums == 1 ? activity.getResources().getString(R.string.lbl_album) : activity.getResources().getString(R.string.lbl_albums));
return;
case "MusicAlbum":
case MusicAlbum:
String artist = item.getAlbumArtist() != null ? item.getAlbumArtist() : item.getArtists() != null && item.getAlbumArtists().size() > 0 ? item.getArtists().get(0) : null;
if (artist != null) {
addText(activity, artist+" ", layout, 500);
@@ -80,7 +81,7 @@ public class InfoLayoutHelper {
Integer songCount = item.getSongCount() != null ? item.getSongCount() : item.getChildCount();
addCount(activity, songCount, layout, songCount == 1 ? activity.getResources().getString(R.string.lbl_song) : activity.getResources().getString(R.string.lbl_songs));
return;
case "Playlist":
case Playlist:
if (item.getChildCount() != null) addCount(activity, item.getChildCount(), layout, item.getChildCount() == 1 ? activity.getResources().getString(R.string.lbl_item) : activity.getResources().getString(R.string.lbl_items));
if (item.getCumulativeRunTimeTicks() != null) addText(activity, " ("+ TimeUtils.formatMillis(item.getCumulativeRunTimeTicks() / 10000)+")", layout, 300);
break;
@@ -263,8 +264,8 @@ public class InfoLayoutHelper {
private static void addDate(Activity activity, BaseItemDto item, LinearLayout layout) {
TextView date = new TextView(activity);
date.setTextSize(textSize);
switch (item.getType()) {
case "Person":
switch (item.getBaseItemType()) {
case Person:
StringBuilder sb = new StringBuilder();
if (item.getPremiereDate() != null) {
sb.append(TvApp.getApplication().getString(R.string.lbl_born));
@@ -287,8 +288,8 @@ public class InfoLayoutHelper {
layout.addView(date);
break;
case "Program":
case "TvChannel":
case Program:
case TvChannel:
if (item.getStartDate() != null && item.getEndDate() != null) {
date.setText(android.text.format.DateFormat.getTimeFormat(TvApp.getApplication()).format(TimeUtils.convertToLocalDate(item.getStartDate()))
+ "-"+ android.text.format.DateFormat.getTimeFormat(TvApp.getApplication()).format(TimeUtils.convertToLocalDate(item.getEndDate())));
@@ -296,7 +297,7 @@ public class InfoLayoutHelper {
addSpacer(activity, layout, " ");
}
break;
case "Series":
case Series:
if (item.getProductionYear() != null && item.getProductionYear() > 0) {
date.setText(item.getProductionYear().toString());
layout.addView(date);
@@ -343,7 +344,7 @@ public class InfoLayoutHelper {
}
private static void addSeriesStatus(Activity activity, BaseItemDto item, LinearLayout layout) {
if ("Series".equals(item.getType()) && item.getSeriesStatus() != null) {
if (item.getBaseItemType() == BaseItemType.Series && item.getSeriesStatus() != null) {
boolean continuing = item.getSeriesStatus() == SeriesStatus.Continuing;
String status = continuing ? activity.getString(R.string.lbl__continuing) : activity.getString(R.string.lbl_ended);
addBlockText(activity, layout, status, textSize-4, Color.LTGRAY, continuing ? R.drawable.green_gradient : R.drawable.red_gradient);

View File

@@ -27,6 +27,7 @@ import java.util.List;
import org.jellyfin.apiclient.interaction.EmptyResponse;
import org.jellyfin.apiclient.interaction.Response;
import org.jellyfin.apiclient.model.dto.BaseItemDto;
import org.jellyfin.apiclient.model.dto.BaseItemType;
import org.jellyfin.apiclient.model.dto.UserItemDataDto;
import org.jellyfin.apiclient.model.entities.SortOrder;
import org.jellyfin.apiclient.model.querying.ItemFilter;
@@ -64,7 +65,7 @@ public class KeyProcessor {
switch (key) {
case KeyEvent.KEYCODE_MEDIA_PLAY:
case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE:
if (MediaManager.isPlayingAudio() && (!rowItem.isBaseItem() || !rowItem.getBaseItem().getType().equals("Photo"))) {
if (MediaManager.isPlayingAudio() && (!rowItem.isBaseItem() || rowItem.getBaseItemType() != BaseItemType.Photo)) {
MediaManager.pauseAudio();
return true;
}
@@ -74,38 +75,38 @@ public class KeyProcessor {
case BaseItem:
BaseItemDto item = rowItem.getBaseItem();
if (!BaseItemUtils.canPlay(item)) return false;
switch (item.getType()) {
case "Audio":
switch (item.getBaseItemType()) {
case Audio:
if (rowItem instanceof AudioQueueItem) {
createItemMenu(rowItem, item.getUserData(), activity);
return true;
}
//fall through...
case "Movie":
case "Episode":
case "TvChannel":
case "Video":
case "Program":
case "ChannelVideoItem":
case "Trailer":
case Movie:
case Episode:
case TvChannel:
case Video:
case Program:
case ChannelVideoItem:
case Trailer:
// give some audible feedback
Utils.beep();
// retrieve full item and play
PlaybackHelper.retrieveAndPlay(item.getId(), false, activity);
return true;
case "Series":
case "Season":
case "BoxSet":
case Series:
case Season:
case BoxSet:
createPlayMenu(rowItem.getBaseItem(), true, false, activity);
return true;
case "MusicAlbum":
case "MusicArtist":
case MusicAlbum:
case MusicArtist:
createPlayMenu(rowItem.getBaseItem(), true, true, activity);
return true;
case "Playlist":
case Playlist:
createPlayMenu(rowItem.getBaseItem(), true, "Audio".equals(rowItem.getBaseItem().getMediaType()), activity);
return true;
case "Photo":
case Photo:
// open photo player
Utils.beep();
Intent photoIntent = new Intent(activity, PhotoPlayerActivity.class);
@@ -158,7 +159,7 @@ public class KeyProcessor {
if (rowItem.getGridButton().getId() == TvApp.VIDEO_QUEUE_OPTION_ID) {
//Queue already there - just kick off playback
Utils.beep();
String itemType = MediaManager.getCurrentVideoQueue().size() > 0 ? MediaManager.getCurrentVideoQueue().get(0).getType() : "";
BaseItemType itemType = MediaManager.getCurrentVideoQueue().size() > 0 ? MediaManager.getCurrentVideoQueue().get(0).getBaseItemType() : null;
Intent intent = new Intent(activity, TvApp.getApplication().getPlaybackActivityClass(itemType));
activity.startActivity(intent);
}
@@ -180,21 +181,21 @@ public class KeyProcessor {
case BaseItem:
BaseItemDto item = rowItem.getBaseItem();
switch (item.getType()) {
case "Movie":
case "Episode":
case "TvChannel":
case "Video":
case "Program":
case "ChannelVideoItem":
case "Series":
case "Season":
case "BoxSet":
case "MusicAlbum":
case "MusicArtist":
case "Playlist":
case "Audio":
case "Trailer":
switch (item.getBaseItemType()) {
case Movie:
case Episode:
case TvChannel:
case Video:
case Program:
case ChannelVideoItem:
case Series:
case Season:
case BoxSet:
case MusicAlbum:
case MusicArtist:
case Playlist:
case Audio:
case Trailer:
// generate a standard item menu
createItemMenu(rowItem, item.getUserData(), activity);
break;
@@ -268,9 +269,9 @@ public class KeyProcessor {
} else {
if (BaseItemUtils.canPlay(item)) {
if (item.getIsFolderItem()
&& !"MusicAlbum".equals(item.getType())
&& !"Playlist".equals(item.getType())
&& !"MusicArtist".equals(item.getType())
&& item.getBaseItemType() != BaseItemType.MusicAlbum
&& item.getBaseItemType() != BaseItemType.Playlist
&& item.getBaseItemType() != BaseItemType.MusicArtist
&& userData!= null
&& userData.getUnplayedItemCount() !=null
&& userData.getUnplayedItemCount() > 0) {
@@ -282,17 +283,17 @@ public class KeyProcessor {
}
}
isMusic = "MusicAlbum".equals(item.getType())
|| "MusicArtist".equals(item.getType())
|| "Audio".equals(item.getType())
|| ("Playlist".equals(item.getType()) && "Audio".equals(item.getMediaType()));
isMusic = item.getBaseItemType() == BaseItemType.MusicAlbum
|| item.getBaseItemType() == BaseItemType.MusicArtist
|| item.getBaseItemType() == BaseItemType.Audio
|| (item.getBaseItemType() == BaseItemType.Playlist && "Audio".equals(item.getMediaType()));
if (isMusic || !item.getIsFolderItem()) {
menu.getMenu().add(0, MENU_ADD_QUEUE, order++, R.string.lbl_add_to_queue);
}
if (isMusic) {
if (!"Playlist".equals(item.getType())) {
if (item.getBaseItemType() != BaseItemType.Playlist) {
menu.getMenu().add(0, MENU_INSTANT_MIX, order++, R.string.lbl_instant_mix);
}
} else {
@@ -338,7 +339,7 @@ public class KeyProcessor {
private static void createPlayMenu(BaseItemDto item, boolean isFolder, boolean isMusic, BaseActivity activity) {
PopupMenu menu = Utils.createPopupMenu(activity, activity.getCurrentFocus(), Gravity.RIGHT);
int order = 0;
if (!isMusic && !"Playlist".equals(item.getType())) {
if (!isMusic && item.getBaseItemType() != BaseItemType.Playlist) {
menu.getMenu().add(0, MENU_PLAY_FIRST_UNWATCHED, order++, R.string.lbl_play_first_unwatched);
}
menu.getMenu().add(0, MENU_PLAY, order++, R.string.lbl_play_all);

View File

@@ -69,8 +69,8 @@ public class ProfileHelper {
profile.setSubtitleProfiles(new SubtitleProfile[] {
getSubtitleProfile("srt", SubtitleDeliveryMethod.External),
getSubtitleProfile("subrip", SubtitleDeliveryMethod.External),
getSubtitleProfile("ass", SubtitleDeliveryMethod.External),
getSubtitleProfile("ssa", SubtitleDeliveryMethod.External),
getSubtitleProfile("ass", SubtitleDeliveryMethod.Encode),
getSubtitleProfile("ssa", SubtitleDeliveryMethod.Encode),
getSubtitleProfile("pgs", SubtitleDeliveryMethod.Encode),
getSubtitleProfile("pgssub", SubtitleDeliveryMethod.Encode),
getSubtitleProfile("dvdsub", SubtitleDeliveryMethod.External),
@@ -139,8 +139,8 @@ public class ProfileHelper {
profile.setSubtitleProfiles(new SubtitleProfile[] {
getSubtitleProfile("srt", SubtitleDeliveryMethod.Embed),
getSubtitleProfile("subrip", SubtitleDeliveryMethod.Embed),
getSubtitleProfile("ass", SubtitleDeliveryMethod.Embed),
getSubtitleProfile("ssa", SubtitleDeliveryMethod.Embed),
getSubtitleProfile("ass", SubtitleDeliveryMethod.Encode),
getSubtitleProfile("ssa", SubtitleDeliveryMethod.Encode),
getSubtitleProfile("pgs", SubtitleDeliveryMethod.Embed),
getSubtitleProfile("pgssub", SubtitleDeliveryMethod.Embed),
getSubtitleProfile("dvdsub", SubtitleDeliveryMethod.Embed),
@@ -292,7 +292,7 @@ public class ProfileHelper {
));
videoDirectPlayProfile.setContainer(Utils.join(",", containers));
List<String> videoCodecs;
if (DeviceUtils.isShield() || DeviceUtils.isNexus()) {
if (DeviceUtils.isShield() || DeviceUtils.isNexus() || DeviceUtils.isBeyondTv()) {
videoCodecs = Arrays.asList(
CodecTypes.H264,
CodecTypes.HEVC,
@@ -400,8 +400,8 @@ public class ProfileHelper {
getSubtitleProfile("srt", SubtitleDeliveryMethod.External),
getSubtitleProfile("srt", SubtitleDeliveryMethod.Embed),
getSubtitleProfile("subrip", SubtitleDeliveryMethod.Embed),
getSubtitleProfile("ass", SubtitleDeliveryMethod.Embed),
getSubtitleProfile("ssa", SubtitleDeliveryMethod.Embed),
getSubtitleProfile("ass", SubtitleDeliveryMethod.Encode),
getSubtitleProfile("ssa", SubtitleDeliveryMethod.Encode),
getSubtitleProfile("pgs", SubtitleDeliveryMethod.Encode),
getSubtitleProfile("pgssub", SubtitleDeliveryMethod.Encode),
getSubtitleProfile("dvdsub", SubtitleDeliveryMethod.Embed),

View File

@@ -0,0 +1,18 @@
package org.jellyfin.androidtv.util.apiclient
import org.jellyfin.apiclient.interaction.ApiClient
import org.jellyfin.apiclient.interaction.Response
import org.jellyfin.apiclient.model.querying.ItemsResult
import org.jellyfin.apiclient.model.querying.NextUpQuery
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
/**
* Adds a coroutine capable version of the "getNextUpEpisodes" function
*/
suspend fun ApiClient.getNextUpEpisodes(query: NextUpQuery): ItemsResult? = suspendCoroutine { continuation ->
GetNextUpEpisodesAsync(query, object : Response<ItemsResult>() {
override fun onResponse(response: ItemsResult?) = continuation.resume(response!!)
override fun onError(exception: Exception?) = continuation.resume(null)
})
}

View File

@@ -51,7 +51,9 @@ public class AuthenticationHelper {
public void onClick(DialogInterface dialog, int whichButton) {
String addressValue = address.getText().toString();
TvApp.getApplication().getLogger().Debug("Entered address: " + addressValue);
signInToServer(TvApp.getApplication().getConnectionManager(), addressValue, activity);
if (!addressValue.isEmpty()) {
signInToServer(TvApp.getApplication().getConnectionManager(), addressValue, activity);
}
}
}).show();
}
@@ -90,15 +92,16 @@ public class AuthenticationHelper {
private static Response<ConnectionResult> getSignInResponse(final Activity activity, final String address) {
// This is taking longer than expected message
// final DelayedMessage message = new DelayedMessage(activity);
final DelayedMessage message = new DelayedMessage(activity);
return new Response<ConnectionResult>() {
@Override
public void onResponse(ConnectionResult serverResult) {
// message.Cancel();
message.Cancel();
// Check the server version
if (!isSupportedServerVersion(serverResult.getServers().get(0))) {
if (!serverResult.getServers().isEmpty() &&
!isSupportedServerVersion(serverResult.getServers().get(0))) {
Utils.showToast(activity, activity.getString(R.string.msg_error_server_version, TvApp.MINIMUM_SERVER_VERSION));
return;
}
@@ -124,7 +127,7 @@ public class AuthenticationHelper {
@Override
public void onError(Exception exception) {
// message.Cancel();
message.Cancel();
TvApp.getApplication().getLogger().ErrorException("Error trying to sign in to specific server " + address, exception);
Utils.showToast(activity, activity.getString(R.string.msg_error_connecting_server));
}

View File

@@ -15,6 +15,7 @@ import java.util.List;
import org.jellyfin.apiclient.model.dto.BaseItemDto;
import org.jellyfin.apiclient.model.dto.BaseItemPerson;
import org.jellyfin.apiclient.model.dto.BaseItemType;
import org.jellyfin.apiclient.model.dto.ChapterInfoDto;
import org.jellyfin.apiclient.model.dto.ImageOptions;
import org.jellyfin.apiclient.model.entities.ImageType;
@@ -23,25 +24,27 @@ import org.jellyfin.apiclient.model.library.PlayAccess;
import org.jellyfin.apiclient.model.livetv.SeriesTimerInfoDto;
public class BaseItemUtils {
// TODO Feature Envy!!! Wants to live in BaseItemDto.
public static boolean isLiveTv(BaseItemDto item) {
return "Program".equals(item.getType()) || "LiveTvChannel".equals(item.getType());
return item.getBaseItemType() == BaseItemType.Program
|| item.getBaseItemType() == BaseItemType.LiveTvChannel;
}
public static boolean canPlay(BaseItemDto item) {
return item.getPlayAccess().equals(PlayAccess.Full)
&& ((item.getIsPlaceHolder() == null || !item.getIsPlaceHolder())
&& (!item.getType().equals("Episode") || !item.getLocationType().equals(LocationType.Virtual)))
&& (!item.getType().equals("Person"))
&& (!item.getType().equals("SeriesTimer"))
&& (item.getBaseItemType() != BaseItemType.Episode || !item.getLocationType().equals(LocationType.Virtual)))
&& (item.getBaseItemType() != BaseItemType.Person)
&& (item.getBaseItemType() != BaseItemType.SeriesTimer)
&& (!item.getIsFolderItem() || item.getChildCount() == null || item.getChildCount() > 0);
}
public static String getFullName(BaseItemDto item) {
switch (item.getType()) {
case "Episode":
switch (item.getBaseItemType()) {
case Episode:
return item.getSeriesName() + (item.getParentIndexNumber() != null ? " S" + item.getParentIndexNumber() : "") + (item.getIndexNumber() != null ? " E" + item.getIndexNumber() : "") + (item.getIndexNumberEnd() != null ? "-" + item.getIndexNumberEnd() : "");
case "Audio":
case "MusicAlbum":
case Audio:
case MusicAlbum:
// we actually want the artist name if available
return (item.getAlbumArtist() != null ? item.getAlbumArtist() + " - " : "") + item.getName();
default:
@@ -50,15 +53,15 @@ public class BaseItemUtils {
}
public static String getSubName(BaseItemDto item) {
switch (item.getType()) {
case "Episode":
switch (item.getBaseItemType()) {
case Episode:
String addendum = item.getLocationType().equals(LocationType.Virtual) && item.getPremiereDate() != null ? " (" + TimeUtils.getFriendlyDate(TimeUtils.convertToLocalDate(item.getPremiereDate())) + ")" : "";
return item.getName() + addendum;
case "Season":
case Season:
return item.getChildCount() != null && item.getChildCount() > 0 ? item.getChildCount() + " " + TvApp.getApplication().getString(R.string.lbl_episodes) : "";
case "MusicAlbum":
case MusicAlbum:
return item.getChildCount() != null && item.getChildCount() > 0 ? item.getChildCount() + " " + TvApp.getApplication().getString(item.getChildCount() > 1 ? R.string.lbl_songs : R.string.lbl_song) : "";
case "Audio":
case Audio:
return item.getName();
default:
return item.getOfficialRating();

View File

@@ -17,6 +17,7 @@ import java.util.List;
import org.jellyfin.apiclient.interaction.Response;
import org.jellyfin.apiclient.model.dto.BaseItemDto;
import org.jellyfin.apiclient.model.dto.BaseItemType;
import org.jellyfin.apiclient.model.entities.LocationType;
import org.jellyfin.apiclient.model.livetv.ChannelInfoDto;
import org.jellyfin.apiclient.model.querying.ItemFields;
@@ -32,8 +33,8 @@ public class PlaybackHelper {
ItemQuery query = new ItemQuery();
TvApp.getApplication().setPlayingIntros(false);
switch (mainItem.getType()) {
case "Episode":
switch (mainItem.getBaseItemType()) {
case Episode:
items.add(mainItem);
if (TvApp.getApplication().getPrefs().getBoolean("pref_enable_tv_queuing", true)) {
MediaManager.setVideoQueueModified(false); // we are automatically creating new queue
@@ -72,10 +73,10 @@ public class PlaybackHelper {
outerResponse.onResponse(items);
}
break;
case "Series":
case "Season":
case "BoxSet":
case "Folder":
case Series:
case Season:
case BoxSet:
case Folder:
//get all videos
query.setParentId(mainItem.getId());
query.setIsMissing(false);
@@ -94,18 +95,18 @@ public class PlaybackHelper {
}
});
break;
case "MusicAlbum":
case "MusicArtist":
case MusicAlbum:
case MusicArtist:
//get all songs
query.setIsMissing(false);
query.setIsVirtualUnaired(false);
query.setIncludeItemTypes(new String[]{"Audio"});
query.setSortBy(shuffle ? new String[] {ItemSortBy.Random} : "MusicArtist".equals(mainItem.getType()) ? new String[] {ItemSortBy.Album} : new String[] {ItemSortBy.SortName});
query.setMediaTypes(new String[]{"Audio"});
query.setSortBy(shuffle ? new String[] {ItemSortBy.Random} : mainItem.getBaseItemType() == BaseItemType.MusicArtist ? new String[] {ItemSortBy.Album} : new String[] {ItemSortBy.SortName});
query.setRecursive(true);
query.setLimit(150); // guard against too many items
query.setFields(new ItemFields[] {ItemFields.PrimaryImageAspectRatio, ItemFields.Genres});
query.setUserId(TvApp.getApplication().getCurrentUser().getId());
query.setParentId(mainItem.getId());
query.setArtistIds(new String[]{mainItem.getId()});
TvApp.getApplication().getApiClient().GetItemsAsync(query, new Response<ItemsResult>() {
@Override
public void onResponse(ItemsResult response) {
@@ -113,7 +114,7 @@ public class PlaybackHelper {
}
});
break;
case "Playlist":
case Playlist:
if (mainItem.getId().equals(ItemListActivity.FAV_SONGS)) {
query.setFilters(new ItemFilter[] {ItemFilter.IsFavoriteOrLikes});
} else {
@@ -134,7 +135,7 @@ public class PlaybackHelper {
});
break;
case "Program":
case Program:
if (mainItem.getParentId() == null) {
outerResponse.onError(new Exception("No Channel ID"));
return;
@@ -160,7 +161,7 @@ public class PlaybackHelper {
});
break;
case "TvChannel":
case TvChannel:
// Retrieve full channel info for display
TvApp.getApplication().getApiClient().GetLiveTvChannelAsync(mainItem.getId(), TvApp.getApplication().getCurrentUser().getId(), new Response<ChannelInfoDto>() {
@Override
@@ -179,7 +180,7 @@ public class PlaybackHelper {
break;
default:
if (allowIntros && !TvApp.getApplication().useExternalPlayer(mainItem.getType()) && TvApp.getApplication().getPrefs().getBoolean("pref_enable_cinema_mode", true)) {
if (allowIntros && !TvApp.getApplication().useExternalPlayer(mainItem.getBaseItemType()) && TvApp.getApplication().getPrefs().getBoolean("pref_enable_cinema_mode", true)) {
//Intros
TvApp.getApplication().getApiClient().GetIntrosAsync(mainItem.getId(), TvApp.getApplication().getCurrentUser().getId(), new Response<ItemsResult>() {
@Override
@@ -210,20 +211,20 @@ public class PlaybackHelper {
}
public static void play(final BaseItemDto item, final int pos, final boolean shuffle, final Context activity) {
getItemsToPlay(item, pos == 0 && item.getType().equals("Movie"), shuffle, new Response<List<BaseItemDto>>() {
getItemsToPlay(item, pos == 0 && item.getBaseItemType() == BaseItemType.Movie, shuffle, new Response<List<BaseItemDto>>() {
@Override
public void onResponse(List<BaseItemDto> response) {
switch (item.getType()) {
case "MusicAlbum":
case "MusicArtist":
switch (item.getBaseItemType()) {
case MusicAlbum:
case MusicArtist:
MediaManager.playNow(response);
break;
case "Playlist":
case Playlist:
if ("Audio".equals(item.getMediaType())) {
MediaManager.playNow(response);
} else {
String itemType = response.size() > 0 ? response.get(0).getType() : "";
BaseItemType itemType = response.size() > 0 ? response.get(0).getBaseItemType() : null;
Intent intent = new Intent(activity, TvApp.getApplication().getPlaybackActivityClass(itemType));
MediaManager.setCurrentVideoQueue(response);
intent.putExtra("Position", pos);
@@ -232,14 +233,14 @@ public class PlaybackHelper {
activity.startActivity(intent);
}
break;
case "Audio":
case Audio:
if (response.size() > 0) {
MediaManager.playNow(response.get(0));
}
break;
default:
Intent intent = new Intent(activity, TvApp.getApplication().getPlaybackActivityClass(item.getType()));
Intent intent = new Intent(activity, TvApp.getApplication().getPlaybackActivityClass(item.getBaseItemType()));
MediaManager.setCurrentVideoQueue(response);
intent.putExtra("Position", pos);
if (!(activity instanceof Activity))

View File

@@ -22,11 +22,11 @@ public class ReportingHelper {
TvApp.getApplication().getPlaybackManager().reportPlaybackStopped(info, streamInfo, apiClient.getServerInfo().getId(), TvApp.getApplication().getCurrentUser().getId(), false, apiClient, new EmptyResponse());
TvApp.getApplication().setLastPlayback(Calendar.getInstance());
switch (item.getType()) {
case "Movie":
switch (item.getBaseItemType()) {
case Movie:
TvApp.getApplication().setLastMoviePlayback(Calendar.getInstance());
break;
case "Episode":
case Episode:
TvApp.getApplication().setLastTvPlayback(Calendar.getInstance());
break;
}

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -20,7 +20,6 @@
android:layout_height="wrap_content"
android:textAppearance="?android:attr/textAppearanceMedium"
android:text="@string/dpad_entry_desc"
android:id="@+id/textView7"
android:paddingLeft="30sp"
android:paddingRight="30sp"
android:layout_marginTop="37dp"
@@ -32,7 +31,6 @@
android:layout_height="wrap_content"
android:textAppearance="?android:attr/textAppearanceMedium"
android:text="@string/dpad_pw_cancel"
android:id="@+id/textView8"
android:layout_alignParentBottom="true"
android:layout_alignStart="@+id/imageView8"
android:layout_marginBottom="30sp"

View File

@@ -1,7 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/activity_expanded_text"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="@dimen/activity_vertical_margin"

View File

@@ -2,7 +2,7 @@
<fragment xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/main_browse_fragment"
android:name="org.jellyfin.androidtv.browsing.HomeFragment"
android:name="org.jellyfin.androidtv.ui.home.HomeFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity"

View File

@@ -1,11 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<fragment xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/search_fragment"
android:name="org.jellyfin.androidtv.search.MySearchFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".ByLetterActivity"
tools:deviceIds="tv"
tools:ignore="MergeRootFrame"
tools:layout="@layout/activity_search" />

View File

@@ -1,39 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<EditText
android:id="@+id/searchField"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:layout_centerHorizontal="true"
android:layout_marginTop="45dp"
android:enabled="true"
android:focusable="true"
android:gravity="center_vertical"
android:hint="@string/lbl_search_hint"
android:minWidth="400sp"
android:minHeight="30sp"
android:textSize="18sp" />
<fragment
android:id="@+id/fragment_search_no_speech"
android:name="org.jellyfin.androidtv.search.MySearchFragmentNoSpeech"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="120sp"
tools:layout="@layout/fragment_empty_frame" />
<ImageView
android:id="@+id/imageView10"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignTop="@+id/searchField"
android:layout_alignBottom="@+id/searchField"
android:layout_marginEnd="@dimen/activity_horizontal_margin"
android:layout_toStartOf="@+id/searchField"
android:src="@drawable/lb_ic_in_app_search" />
</RelativeLayout>

View File

@@ -1,17 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent" android:layout_height="match_parent"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?attr/popupWindowBackground"
android:padding="20dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="?android:attr/textAppearanceMedium"
android:text="Audio Delay"
android:text="@string/lbl_audio_delay"
android:id="@+id/textView18"
android:layout_alignParentTop="true"
android:layout_centerHorizontal="true"
android:textColor="?attr/defaultBrandColor" />
android:layout_centerHorizontal="true" />
<org.jellyfin.androidtv.ui.NumberSpinner
android:layout_width="wrap_content"

View File

@@ -1,6 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent" android:layout_height="match_parent"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="2sp"
android:background="@drawable/light_border">

View File

@@ -1,11 +0,0 @@
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="16dp">
<GridView android:id="@+id/color_grid"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:stretchMode="columnWidth" />
</FrameLayout>

View File

@@ -1,28 +0,0 @@
<!--
Copyright 2013 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.
-->
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="8dp">
<ImageView
android:id="@+id/color_view"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_gravity="center"
android:scaleType="fitXY" />
</FrameLayout>

View File

@@ -2,7 +2,8 @@
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="40dp">
android:padding="40dp"
android:background="?attr/popupWindowBackground">
<TextView
android:layout_width="wrap_content"
@@ -10,8 +11,7 @@
android:textAppearance="?android:attr/textAppearanceLarge"
android:text="@string/lbl_display_preferences"
android:id="@+id/textView17"
android:layout_centerHorizontal="true"
android:textColor="?attr/defaultBrandColor" />
android:layout_centerHorizontal="true" />
<RelativeLayout
android:layout_width="wrap_content"
@@ -31,8 +31,7 @@
<TextView
android:layout_width="90dp"
android:layout_height="wrap_content"
android:text="Image Size "
android:id="@+id/textView14"
android:text="@string/lbl_image_size"
android:textSize="16sp" />
<Spinner
@@ -55,8 +54,7 @@
<TextView
android:layout_width="90dp"
android:layout_height="wrap_content"
android:text="Image Type"
android:id="@+id/textView16"
android:text="@string/lbl_image_type"
android:textSize="16sp" />
<Spinner
@@ -79,7 +77,7 @@
<TextView
android:layout_width="90dp"
android:layout_height="wrap_content"
android:text="Default View"
android:text="@string/lbl_default_view"
android:id="@+id/defaultViewLbl"
android:textSize="16sp" />

View File

@@ -1,9 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent" android:layout_height="match_parent">
<FrameLayout
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:id="@+id/emptyFrame"/>
</FrameLayout>

Some files were not shown because too many files have changed in this diff Show More