Compare commits
273 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a67c486385 | ||
|
|
8aa0c5a18e | ||
|
|
4c5eab8d62 | ||
|
|
5e016b38b0 | ||
|
|
5e4e135239 | ||
|
|
8c1d6c30de | ||
|
|
ec83ec0540 | ||
|
|
32dd0ae3ce | ||
|
|
e258ae8113 | ||
|
|
c11d26c887 | ||
|
|
56d9950898 | ||
|
|
60007bf43d | ||
|
|
1af15961e8 | ||
|
|
e6f82208d2 | ||
|
|
5cec9f1ef3 | ||
|
|
4cd27fc4a4 | ||
|
|
269a6160b7 | ||
|
|
3a077e4d1c | ||
|
|
4b0390efd5 | ||
|
|
41b52d3616 | ||
|
|
182daa3ae4 | ||
|
|
3f31bd349a | ||
|
|
ab25bd7d81 | ||
|
|
2c3ae8e6a8 | ||
|
|
4d0bad1533 | ||
|
|
e50c7f2211 | ||
|
|
128ee79abb | ||
|
|
6a6c6c275a | ||
|
|
04dddff75f | ||
|
|
b2cb898534 | ||
|
|
1268a72527 | ||
|
|
49e7c760a2 | ||
|
|
535602b67c | ||
|
|
8fee67c2e4 | ||
|
|
89a283440b | ||
|
|
774bff2b49 | ||
|
|
4d34a30dbf | ||
|
|
1320da0797 | ||
|
|
b171bdd7ea | ||
|
|
c1be6543ab | ||
|
|
473905239d | ||
|
|
20af2f4b8e | ||
|
|
e8c7809b1e | ||
|
|
26f96ee332 | ||
|
|
dc6d2d9c53 | ||
|
|
5e1925c7f1 | ||
|
|
bf57210edd | ||
|
|
2f7fed9df6 | ||
|
|
ce1b6a2228 | ||
|
|
9739898bd1 | ||
|
|
3c20fd688d | ||
|
|
e9ba3131db | ||
|
|
b93552ecfb | ||
|
|
7e52bb48b6 | ||
|
|
193d526583 | ||
|
|
c0100c1c3c | ||
|
|
0ffbed66fc | ||
|
|
8665c4a39e | ||
|
|
e4db812b82 | ||
|
|
90d02fa0ed | ||
|
|
940c15511c | ||
|
|
78a595ff4c | ||
|
|
8a2edbcc0a | ||
|
|
821a53f016 | ||
|
|
46c8d8dcee | ||
|
|
16d55baeb9 | ||
|
|
52eec32518 | ||
|
|
055893eb2b | ||
|
|
93302c34f7 | ||
|
|
1f7f486732 | ||
|
|
97ad3ab320 | ||
|
|
3abc2a0d2c | ||
|
|
9ec29a6f15 | ||
|
|
6d46bc1084 | ||
|
|
65708a5fc5 | ||
|
|
2588e7f1f8 | ||
|
|
21453982a3 | ||
|
|
8a8831ef84 | ||
|
|
c02b65a5e9 | ||
|
|
e5771f4c95 | ||
|
|
bf79e8c337 | ||
|
|
ba3a95fc3d | ||
|
|
35c0801f0d | ||
|
|
91e7252025 | ||
|
|
2cd9d9e6d3 | ||
|
|
7ae58475e5 | ||
|
|
87043fcaf2 | ||
|
|
3738563a73 | ||
|
|
859f8d107b | ||
|
|
c411f58c3d | ||
|
|
1e275345d4 | ||
|
|
c2cc1afac1 | ||
|
|
f5c87878f6 | ||
|
|
35021068c2 | ||
|
|
96c0dc0f87 | ||
|
|
1ca6abdd13 | ||
|
|
5049e7d09f | ||
|
|
911f00f8b2 | ||
|
|
fbfaa3db2e | ||
|
|
dc1e41a510 | ||
|
|
290ba7bbb4 | ||
|
|
fa9c5621e4 | ||
|
|
9d7bb0f232 | ||
|
|
722d766c28 | ||
|
|
cd26e12bdc | ||
|
|
45caec1460 | ||
|
|
9755caa71e | ||
|
|
5a16dbc77b | ||
|
|
3897ebbb55 | ||
|
|
e36dd6c968 | ||
|
|
5c4f213383 | ||
|
|
bf33dc60bd | ||
|
|
038caab680 | ||
|
|
344130ca7e | ||
|
|
1704e3df3d | ||
|
|
8ca73dd713 | ||
|
|
93a23c69bb | ||
|
|
cad39f36cc | ||
|
|
ece9324d32 | ||
|
|
3c7d9c86a9 | ||
|
|
cb27fc6309 | ||
|
|
b9cec5294e | ||
|
|
1b30d78b1a | ||
|
|
6771bd2d14 | ||
|
|
229990b50b | ||
|
|
de2081bec2 | ||
|
|
900032efa6 | ||
|
|
71e434517b | ||
|
|
7e4b454a42 | ||
|
|
6a91c08b55 | ||
|
|
2c2e738222 | ||
|
|
91bef5afc6 | ||
|
|
ec83261b43 | ||
|
|
a352a32bab | ||
|
|
25edae1563 | ||
|
|
8efe4d23de | ||
|
|
cc767ef1cb | ||
|
|
4eb2b9db7a | ||
|
|
54d9cba938 | ||
|
|
9bfb1c7a8e | ||
|
|
3566785030 | ||
|
|
e23fb21fd2 | ||
|
|
a5a1b8c9d7 | ||
|
|
99b164e240 | ||
|
|
6fff8ff642 | ||
|
|
d0490d318a | ||
|
|
35823fb11c | ||
|
|
b42285f838 | ||
|
|
d65843fb6d | ||
|
|
4cf20c4ede | ||
|
|
d0272cc3fb | ||
|
|
29f127e226 | ||
|
|
0d820ebb2c | ||
|
|
006f4390a7 | ||
|
|
05bebbe67b | ||
|
|
354ef42fcb | ||
|
|
bace7f8b91 | ||
|
|
97644e2479 | ||
|
|
19f3088e62 | ||
|
|
3a1ca78182 | ||
|
|
0a1d3dbf76 | ||
|
|
102a93361c | ||
|
|
27ea77eac7 | ||
|
|
e19c50fe3d | ||
|
|
18dedd1c77 | ||
|
|
edc83a77fd | ||
|
|
41b91145d6 | ||
|
|
c77e84bbe6 | ||
|
|
beb8e821ae | ||
|
|
3011b7980e | ||
|
|
8779286066 | ||
|
|
c5e36251a4 | ||
|
|
d68b43d328 | ||
|
|
7f2275a11a | ||
|
|
51621715f9 | ||
|
|
341efc62d2 | ||
|
|
afa1f918ee | ||
|
|
82f5dafb5d | ||
|
|
2a3ed44fd5 | ||
|
|
922d413cd6 | ||
|
|
0134b0d792 | ||
|
|
ee0cbf4c10 | ||
|
|
6b1a59224f | ||
|
|
29d926746e | ||
|
|
120c77ade6 | ||
|
|
7f1d98ecc8 | ||
|
|
e7c08331e1 | ||
|
|
b29f0fd912 | ||
|
|
a36b7c4cb4 | ||
|
|
a815ea0319 | ||
|
|
4cfd5383d9 | ||
|
|
d3458164d6 | ||
|
|
72ed94447a | ||
|
|
fbd601213f | ||
|
|
becb6295a6 | ||
|
|
5c40e50351 | ||
|
|
99035900ff | ||
|
|
76512e3fc6 | ||
|
|
c54ef94e03 | ||
|
|
ac24a6a09e | ||
|
|
2b80bac9f1 | ||
|
|
19f280c294 | ||
|
|
dc203ec04b | ||
|
|
c8fa914ec7 | ||
|
|
8cf33c16f4 | ||
|
|
6d09df1de7 | ||
|
|
a7eedf25db | ||
|
|
bc2d44c6bb | ||
|
|
3eee4c1db4 | ||
|
|
9e31e52794 | ||
|
|
49791c3ea3 | ||
|
|
c421c2a78f | ||
|
|
d90e8e3d29 | ||
|
|
cc792c93e4 | ||
|
|
4ef5fe8f69 | ||
|
|
0dd2a93b21 | ||
|
|
1c7052e5c2 | ||
|
|
e7fb835d89 | ||
|
|
0a4b452870 | ||
|
|
7cc84de9c0 | ||
|
|
7ac16901e2 | ||
|
|
1ce6d1f0f8 | ||
|
|
cb0d241ef1 | ||
|
|
e43901c75d | ||
|
|
c08fdd3a69 | ||
|
|
c00d610581 | ||
|
|
55a00fdf52 | ||
|
|
1295dc604b | ||
|
|
0fe28c1133 | ||
|
|
b35027525e | ||
|
|
906949476d | ||
|
|
f7fee75206 | ||
|
|
101bc5b043 | ||
|
|
7e222f77b4 | ||
|
|
e89a33df92 | ||
|
|
1523dcf803 | ||
|
|
4077af5fca | ||
|
|
3e9c76d4f9 | ||
|
|
643832c252 | ||
|
|
54d5efba3e | ||
|
|
5480c0346d | ||
|
|
4c3db801d2 | ||
|
|
17a131648b | ||
|
|
814b232746 | ||
|
|
c4a7764808 | ||
|
|
4af56eb389 | ||
|
|
098fb29f4d | ||
|
|
e028a49441 | ||
|
|
9a367a2266 | ||
|
|
0254645eb5 | ||
|
|
bad5cc9a80 | ||
|
|
87fdf9d178 | ||
|
|
89122ecfad | ||
|
|
ad1cc403d2 | ||
|
|
ef46c9bd6f | ||
|
|
081aa8ce16 | ||
|
|
7607a7c315 | ||
|
|
4d92f5fc50 | ||
|
|
08f5601449 | ||
|
|
18f1f69000 | ||
|
|
511afe1c22 | ||
|
|
25f732c72a | ||
|
|
fc944cc77d | ||
|
|
47fbd86278 | ||
|
|
b656935ce6 | ||
|
|
49684b5eac | ||
|
|
3b6689f2a3 | ||
|
|
613e9c4e57 | ||
|
|
cda5970488 | ||
|
|
79669bfee1 | ||
|
|
6cddce7ada | ||
|
|
dab9bb1d8d | ||
|
|
a25e380476 |
@@ -1,5 +1,6 @@
|
||||
trigger:
|
||||
- master
|
||||
- release
|
||||
|
||||
pool:
|
||||
vmImage: 'ubuntu-latest'
|
||||
|
||||
8
.editorconfig
Normal file
8
.editorconfig
Normal file
@@ -0,0 +1,8 @@
|
||||
[*]
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
|
||||
[{*.kts, *.kt}]
|
||||
charset = utf-8
|
||||
indent_style = tab
|
||||
tab_width = 4
|
||||
@@ -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
|
||||
|
||||
|
||||
46
README.md
46
README.md
@@ -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>
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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":
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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":
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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 +
|
||||
'}';
|
||||
}}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
package org.jellyfin.androidtv.integration;
|
||||
|
||||
public enum RecommendationType {
|
||||
Movie,
|
||||
Tv
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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++;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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(){}
|
||||
}
|
||||
|
||||
@@ -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()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -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()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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 |
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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" />
|
||||
@@ -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>
|
||||
@@ -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"
|
||||
|
||||
@@ -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">
|
||||
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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" />
|
||||
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user