Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a67c486385 | ||
|
|
8aa0c5a18e | ||
|
|
4c5eab8d62 | ||
|
|
5e016b38b0 | ||
|
|
5e4e135239 | ||
|
|
8c1d6c30de | ||
|
|
ec83ec0540 | ||
|
|
32dd0ae3ce | ||
|
|
e258ae8113 | ||
|
|
c11d26c887 | ||
|
|
afa1f918ee |
16
.ci/azure-pipelines.yml
Normal file
16
.ci/azure-pipelines.yml
Normal file
@@ -0,0 +1,16 @@
|
||||
trigger:
|
||||
- master
|
||||
- release
|
||||
|
||||
pool:
|
||||
vmImage: 'ubuntu-latest'
|
||||
|
||||
steps:
|
||||
- task: Gradle@2
|
||||
inputs:
|
||||
gradleWrapperFile: 'gradlew'
|
||||
tasks: 'build'
|
||||
publishJUnitResults: true
|
||||
testResultsFiles: '**/TEST-*.xml'
|
||||
javaHomeOption: 'JDKVersion'
|
||||
sonarQubeRunAnalysis: false
|
||||
7
.dockerignore
Normal file
7
.dockerignore
Normal file
@@ -0,0 +1,7 @@
|
||||
.git*
|
||||
.dockerignore
|
||||
Dockerfile
|
||||
build
|
||||
new-file-header.txt
|
||||
CONTRIBUTORS.md
|
||||
README.md
|
||||
@@ -1,6 +1,5 @@
|
||||
root = true
|
||||
|
||||
[*]
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
|
||||
[{*.kts, *.kt}]
|
||||
|
||||
6
.gitattributes
vendored
6
.gitattributes
vendored
@@ -1,7 +1 @@
|
||||
*.kt text eol=lf
|
||||
*.kts text eol=lf
|
||||
*.java text eol=lf
|
||||
*.xml text eol=lf
|
||||
|
||||
CONTRIBUTORS.md merge=union
|
||||
app/src/main/res/values*/strings.xml merge=union
|
||||
|
||||
36
.github/ISSUE_TEMPLATE/bug_report.md
vendored
36
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -1,36 +0,0 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a bug report
|
||||
title: ''
|
||||
labels: bug
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
<!-- A clear and concise description of what the bug is. -->
|
||||
|
||||
**To Reproduce**
|
||||
<!-- Steps to reproduce the behavior: -->
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
**Expected behavior**
|
||||
<!-- A clear and concise description of what you expected to happen. -->
|
||||
|
||||
**Logs**
|
||||
<!-- Please paste any log errors. -->
|
||||
|
||||
**Screenshots**
|
||||
<!-- If applicable, add screenshots to help explain your problem. -->
|
||||
|
||||
**System (please complete the following information):**
|
||||
- Android TV or Fire TV version: [e.g. Android 9 or Fire OS 7]
|
||||
- Device manufacturer: [e.g. Amazon, Nvidia, Sony]
|
||||
- Device model: [e.g. Fire TV Stick (2nd Gen) or Shield TV]
|
||||
- Jellyfin server version: [e.g. 10.5.4]
|
||||
|
||||
**Additional context**
|
||||
<!-- Add any other context about the problem here. -->
|
||||
8
.github/ISSUE_TEMPLATE/config.yml
vendored
8
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -1,8 +0,0 @@
|
||||
blank_issues_enabled: true
|
||||
contact_links:
|
||||
- name: Jellyfin documentation
|
||||
url: https://jellyfin.org/docs/general/getting-help.html
|
||||
about: Our documentation contains lots of help for common issues
|
||||
- name: Jellyfin contact
|
||||
url: https://jellyfin.org/contact/
|
||||
about: Our community is ready to help you get started with Jellyfin
|
||||
14
.github/ISSUE_TEMPLATE/feature_request.md
vendored
14
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@@ -1,14 +0,0 @@
|
||||
---
|
||||
name: Feature Request
|
||||
about: Request a new feature
|
||||
title: ''
|
||||
labels: enhancement
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Describe the feature you'd like**
|
||||
<!-- A clear and concise description of what you want to happen. -->
|
||||
|
||||
**Additional context**
|
||||
<!-- Add any other context or screenshots about the feature request here. -->
|
||||
29
.github/ISSUE_TEMPLATE/media_playback.md
vendored
29
.github/ISSUE_TEMPLATE/media_playback.md
vendored
@@ -1,29 +0,0 @@
|
||||
---
|
||||
name: Media playback issue
|
||||
about: Create a media playback issue report
|
||||
title: ''
|
||||
labels: playback
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Checks**
|
||||
- [ ] Issue does not happen in the browser (jellyfin-web)
|
||||
|
||||
**Media Info of the file**
|
||||
<!-- Use the Media Info tool (set to text format, download here: https://mediaarea.net/en/MediaInfo) or copy the info from the web ui for the file with the playback issue. -->
|
||||
|
||||
**Logs**
|
||||
<!-- Please paste any log messages from during the playback issue. -->
|
||||
|
||||
**FFmpeg Logs**
|
||||
<!-- Please paste any FFmpeg logs if remuxing or transcoding appears to be part of the issue. -->
|
||||
|
||||
**System (please complete the following information):**
|
||||
- Android TV or Fire TV version: [e.g. Android 9 or Fire OS 7]
|
||||
- Device manufacturer: [e.g. Amazon, Nvidia, Sony]
|
||||
- Device model: [e.g. Fire TV Stick (2nd Gen) or Shield TV]
|
||||
- Jellyfin server version: [e.g. 10.5.4]
|
||||
|
||||
**Additional context**
|
||||
<!-- Add any other context about the problem here. -->
|
||||
10
.github/dependabot.yml
vendored
10
.github/dependabot.yml
vendored
@@ -1,10 +0,0 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: github-actions
|
||||
directory: /
|
||||
schedule:
|
||||
interval: weekly
|
||||
labels:
|
||||
- ci
|
||||
- github-actions
|
||||
- dependencies
|
||||
11
.github/pull_request_template.md
vendored
11
.github/pull_request_template.md
vendored
@@ -1,11 +0,0 @@
|
||||
<!--
|
||||
Ensure your title is short, descriptive, and in the imperative mood (Fix X, Change Y, instead of Fixed X, Changed Y).
|
||||
For a good inspiration of what to write in commit messages and PRs please review https://chris.beams.io/posts/git-commit/ and our https://docs.jellyfin.org/general/contributing/issues.html page.
|
||||
-->
|
||||
|
||||
**Changes**
|
||||
<!-- Describe your changes here in 1-5 sentences. -->
|
||||
|
||||
**Issues**
|
||||
<!-- Tag any issues that this PR solves here.
|
||||
ex. Fixes # -->
|
||||
19
.github/stale.yml
vendored
19
.github/stale.yml
vendored
@@ -1,19 +0,0 @@
|
||||
# Number of days of inactivity before an issue becomes stale
|
||||
daysUntilStale: 120
|
||||
# Number of days of inactivity before a stale issue is closed
|
||||
daysUntilClose: 21
|
||||
# Issues with these labels will never be considered stale
|
||||
exemptLabels:
|
||||
- enhancement
|
||||
- confirmed
|
||||
# Label to use when marking an issue as stale
|
||||
staleLabel: stale
|
||||
# Comment to post when marking an issue as stale. Set to `false` to disable
|
||||
markComment: >
|
||||
This issue has gone 120 days without comment. To avoid abandoned issues, it will be closed in 21 days if there are no new comments.
|
||||
|
||||
If you're the original submitter of this issue, please comment confirming if this issue still affects you in the latest release or master branch, or close the issue if it has been fixed. If you're another user also affected by this bug, please comment confirming so. Either action will remove the stale label.
|
||||
|
||||
This bot exists to prevent issues from becoming stale and forgotten. Jellyfin is always moving forward, and bugs are often fixed as side effects of other changes. We therefore ask that bug report authors remain vigilant about their issues to ensure they are closed if fixed, or re-confirmed - perhaps with fresh logs or reproduction examples - regularly. If you have any questions you can reach us on [Matrix or Social Media](https://docs.jellyfin.org/general/getting-help.html).
|
||||
# Comment to post when closing a stale issue. Set to `false` to disable
|
||||
closeComment: false
|
||||
29
.github/workflows/app-build.yaml
vendored
29
.github/workflows/app-build.yaml
vendored
@@ -1,29 +0,0 @@
|
||||
name: App Build
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
- name: Setup Java
|
||||
uses: actions/setup-java@v2
|
||||
with:
|
||||
distribution: adopt
|
||||
java-version: 11
|
||||
cache: gradle
|
||||
- name: Assemble debug APKs
|
||||
run: ./gradlew --build-cache --no-daemon --info assembleDebug
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: build-artifacts
|
||||
retention-days: 14
|
||||
if-no-files-found: error
|
||||
path: app/build/outputs/apk/debug/*.apk
|
||||
28
.github/workflows/app-lint.yaml
vendored
28
.github/workflows/app-lint.yaml
vendored
@@ -1,28 +0,0 @@
|
||||
name: App Lint
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- release-*
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
- name: Setup Java
|
||||
uses: actions/setup-java@v2
|
||||
with:
|
||||
distribution: adopt
|
||||
java-version: 11
|
||||
cache: gradle
|
||||
- name: Run detekt and lint task
|
||||
run: ./gradlew --build-cache --no-daemon --info detekt lint
|
||||
- name: Upload SARIF files
|
||||
uses: github/codeql-action/upload-sarif@v1
|
||||
if: ${{ always() }}
|
||||
with:
|
||||
sarif_file: .
|
||||
67
.github/workflows/app-publish.yaml
vendored
67
.github/workflows/app-publish.yaml
vendored
@@ -1,67 +0,0 @@
|
||||
name: App Publish
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- v*
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-20.04
|
||||
if: ${{ github.repository == 'jellyfin/jellyfin-androidtv' }}
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
- name: Setup Java
|
||||
uses: actions/setup-java@v2
|
||||
with:
|
||||
distribution: adopt
|
||||
java-version: 11
|
||||
- name: Set JELLYFIN_VERSION
|
||||
run: echo "JELLYFIN_VERSION=$(echo ${GITHUB_REF#refs/tags/v} | tr / -)" >> $GITHUB_ENV
|
||||
- name: Assemble release files
|
||||
run: ./gradlew --no-daemon --info assemble versionTxt
|
||||
- name: Sign proprietary APK
|
||||
id: sign
|
||||
uses: r0adkll/sign-android-release@v1
|
||||
with:
|
||||
releaseDirectory: app/build/outputs/apk/release
|
||||
signingKeyBase64: ${{ secrets.KEYSTORE }}
|
||||
keyStorePassword: ${{ secrets.KEYSTORE_PASSWORD }}
|
||||
alias: ${{ secrets.KEY_ALIAS }}
|
||||
keyPassword: ${{ secrets.KEY_PASSWORD }}
|
||||
- name: Prepare release archive
|
||||
run: |
|
||||
mkdir -p build/jellyfin-publish
|
||||
mv app/build/outputs/apk/*/jellyfin-androidtv-*-debug.apk build/jellyfin-publish/
|
||||
mv app/build/outputs/apk/*/jellyfin-androidtv-*-release-unsigned.apk build/jellyfin-publish/
|
||||
mv ${{ steps.sign.outputs.signedReleaseFile }} build/jellyfin-publish/jellyfin-androidtv-v${{ env.JELLYFIN_VERSION }}-release.apk
|
||||
mv app/build/version.txt build/jellyfin-publish/
|
||||
- name: Upload release archive to GitHub release
|
||||
uses: alexellis/upload-assets@0.3.0
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
with:
|
||||
asset_paths: '["build/jellyfin-publish/*"]'
|
||||
- name: Upload release archive to repo.jellyfin.org
|
||||
uses: burnett01/rsync-deployments@5.1
|
||||
with:
|
||||
switches: -vrptz
|
||||
path: build/jellyfin-publish/
|
||||
remote_path: /srv/repository/releases/client/androidtv/versions/v${{ env.JELLYFIN_VERSION }}
|
||||
remote_host: ${{ secrets.DEPLOY_HOST }}
|
||||
remote_user: ${{ secrets.DEPLOY_USER }}
|
||||
remote_key: ${{ secrets.DEPLOY_KEY }}
|
||||
- name: Update repo.jellyfin.org symlinks
|
||||
uses: appleboy/ssh-action@v0.1.4
|
||||
with:
|
||||
host: ${{ secrets.DEPLOY_HOST }}
|
||||
username: ${{ secrets.DEPLOY_USER }}
|
||||
key: ${{ secrets.DEPLOY_KEY }}
|
||||
envs: JELLYFIN_VERSION
|
||||
script_stop: true
|
||||
script: |
|
||||
cd /srv/repository/releases/client/androidtv;
|
||||
rm -rf *.apk version.txt;
|
||||
ln -s versions/v${JELLYFIN_VERSION}/jellyfin-androidtv-v${JELLYFIN_VERSION}-*.apk .;
|
||||
ln -s versions/v${JELLYFIN_VERSION}/version.txt .;
|
||||
23
.github/workflows/app-test.yaml
vendored
23
.github/workflows/app-test.yaml
vendored
@@ -1,23 +0,0 @@
|
||||
name: App Test
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- release-*
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
- name: Setup Java
|
||||
uses: actions/setup-java@v2
|
||||
with:
|
||||
distribution: adopt
|
||||
java-version: 11
|
||||
cache: gradle
|
||||
- name: Run test task
|
||||
run: ./gradlew --build-cache --no-daemon --info test
|
||||
17
.github/workflows/gradlew-update.yaml
vendored
17
.github/workflows/gradlew-update.yaml
vendored
@@ -1,17 +0,0 @@
|
||||
name: Gradle update
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 4 * * *'
|
||||
|
||||
jobs:
|
||||
update:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.repository == 'jellyfin/jellyfin-androidtv'
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
- name: Update Gradle Wrapper
|
||||
uses: gradle-update/update-gradle-wrapper-action@v1
|
||||
with:
|
||||
repo-token: ${{ secrets.JF_BOT_TOKEN }}
|
||||
18
.github/workflows/gradlew-validate.yaml
vendored
18
.github/workflows/gradlew-validate.yaml
vendored
@@ -1,18 +0,0 @@
|
||||
name: Gradle validate
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
pull_request:
|
||||
paths:
|
||||
- '**/gradlе-wrapper.jar'
|
||||
|
||||
jobs:
|
||||
validate:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
- name: Validate Gradle Wrapper
|
||||
uses: gradle/wrapper-validation-action@v1
|
||||
17
.github/workflows/merge-conflict-labeler.yaml
vendored
17
.github/workflows/merge-conflict-labeler.yaml
vendored
@@ -1,17 +0,0 @@
|
||||
name: Merge conflict labeler
|
||||
|
||||
on:
|
||||
push:
|
||||
pull_request_target:
|
||||
types:
|
||||
- synchronize
|
||||
|
||||
jobs:
|
||||
triage:
|
||||
runs-on: ubuntu-20.04
|
||||
if: github.repository == 'jellyfin/jellyfin-androidtv'
|
||||
steps:
|
||||
- uses: eps1lon/actions-label-merge-conflict@v2.0.1
|
||||
with:
|
||||
dirtyLabel: merge conflict
|
||||
repoToken: ${{ secrets.JF_BOT_TOKEN }}
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -3,6 +3,5 @@
|
||||
local.properties
|
||||
|
||||
/.idea
|
||||
.gradle
|
||||
/.gradle
|
||||
build/
|
||||
captures/
|
||||
|
||||
15
CODEOWNERS
15
CODEOWNERS
@@ -1,15 +0,0 @@
|
||||
# Build files
|
||||
/.github/workflows/ @nielsvanvelzen
|
||||
/.github/dependabot.yml @nielsvanvelzen
|
||||
/gradle/ @nielsvanvelzen
|
||||
/buildSrc/ @nielsvanvelzen
|
||||
settings.gradle.ts @nielsvanvelzen
|
||||
build.gradle.kts @nielsvanvelzen
|
||||
gradle.properties @nielsvanvelzen
|
||||
|
||||
# Linting
|
||||
android-lint.xml @nielsvanvelzen
|
||||
detekt.yaml @nielsvanvelzen
|
||||
|
||||
# Playback code
|
||||
/playback/ @nielsvanvelzen
|
||||
@@ -9,7 +9,6 @@
|
||||
- [AndreasGB](https://github.com/andreasgb)
|
||||
- [joern-h](https://github.com/joern-h)
|
||||
- [Niels van Velzen](https://github.com/nielsvanvelzen)
|
||||
- [GodTamIt](https://github.com/GodTamIt)
|
||||
|
||||
# Emby Contributors
|
||||
|
||||
|
||||
38
Dockerfile
Normal file
38
Dockerfile
Normal file
@@ -0,0 +1,38 @@
|
||||
FROM debian:9
|
||||
# Docker build arguments
|
||||
ARG SOURCE_DIR=/repo
|
||||
ARG ARTIFACT_DIR=/dist
|
||||
ARG ANDROID_DIR=/usr/lib/android-sdk
|
||||
# Docker run environment
|
||||
ENV SOURCE_DIR=/repo
|
||||
ENV ARTIFACT_DIR=/dist
|
||||
ENV ANDROID_DIR=/usr/lib/android-sdk
|
||||
|
||||
# Prepare Debian build environment
|
||||
RUN echo "deb http://deb.debian.org/debian stretch-backports main" >> /etc/apt/sources.list.d/backports.list \
|
||||
&& apt-get update \
|
||||
&& apt-get install -y mmv wget unzip git \
|
||||
&& apt-get install -t stretch-backports -y android-sdk openjdk-8-jdk
|
||||
|
||||
# Install Android repository tools
|
||||
RUN rm -rf ${ANDROID_DIR}/tools \
|
||||
&& wget http://dl-ssl.google.com/android/repository/tools_r25.2.3-linux.zip -O tools.zip \
|
||||
&& unzip tools.zip -d ${ANDROID_DIR}/ \
|
||||
&& rm -f tools.zip
|
||||
|
||||
# Install SDK tools
|
||||
# `yes` is required to accept licenses:
|
||||
# https://stackoverflow.com/questions/38096225/automatically-accept-all-sdk-licences/42125740#42125740
|
||||
# There will be no output to the screen while this runs - expect a delay of up to several minutes on the license
|
||||
RUN yes | ${ANDROID_DIR}/tools/bin/sdkmanager "platform-tools" "platforms;android-27" "build-tools;27.0.3" "extras;android;m2repository" "extras;google;m2repository"
|
||||
|
||||
RUN ln -sf ${SOURCE_DIR}/docker-build.sh /docker-build.sh
|
||||
|
||||
# Prepare artifact volume
|
||||
VOLUME ${ARTIFACT_DIR}/
|
||||
|
||||
# Copy repository
|
||||
COPY . ${SOURCE_DIR}/
|
||||
|
||||
# Set docker-build entrypoint
|
||||
ENTRYPOINT ["/docker-build.sh"]
|
||||
11
LICENSE
11
LICENSE
@@ -1,7 +1,7 @@
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
Version 2, June 1991
|
||||
|
||||
Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
|
||||
Copyright (C) 1989, 1991 Free Software Foundation, Inc., <http://fsf.org/>
|
||||
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
@@ -290,8 +290,8 @@ to attach them to the start of each source file to most effectively
|
||||
convey the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
<one line to give the program's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
{description}
|
||||
Copyright (C) {year} {fullname}
|
||||
|
||||
This program is free software; you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
@@ -329,7 +329,7 @@ necessary. Here is a sample; alter the names:
|
||||
Yoyodyne, Inc., hereby disclaims all copyright interest in the program
|
||||
`Gnomovision' (which makes passes at compilers) written by James Hacker.
|
||||
|
||||
<signature of Ty Coon>, 1 April 1989
|
||||
{signature of Ty Coon}, 1 April 1989
|
||||
Ty Coon, President of Vice
|
||||
|
||||
This General Public License does not permit incorporating your program into
|
||||
@@ -337,3 +337,4 @@ proprietary programs. If your program is a subroutine library, you may
|
||||
consider it more useful to permit linking proprietary applications with the
|
||||
library. If this is what you want to do, use the GNU Lesser General
|
||||
Public License instead of this License.
|
||||
|
||||
|
||||
60
README.md
60
README.md
@@ -1,5 +1,5 @@
|
||||
<h1 align="center">Jellyfin Android TV</h1>
|
||||
<h3 align="center">Part of the <a href="https://jellyfin.org">Jellyfin Project</a></h3>
|
||||
<h3 align="center">Part of the <a href="https://jellyfin.media">Jellyfin Project</a></h3>
|
||||
|
||||
---
|
||||
|
||||
@@ -12,8 +12,8 @@
|
||||
<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-android/jellyfin-androidtv/">
|
||||
<img alt="Translation Status" src="https://translate.jellyfin.org/widgets/jellyfin-android/-/jellyfin-androidtv/svg-badge.svg"/>
|
||||
<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">
|
||||
@@ -22,6 +22,9 @@
|
||||
<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>
|
||||
@@ -30,57 +33,18 @@
|
||||
</a>
|
||||
<br/>
|
||||
<a href="https://play.google.com/store/apps/details?id=org.jellyfin.androidtv">
|
||||
<img width="153" alt="Jellyfin on Google Play" src="https://jellyfin.org/images/store-icons/google-play.png"/>
|
||||
<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="153" alt="Jellyfin on Amazon Appstore" src="https://jellyfin.org/images/store-icons/amazon.png"/>
|
||||
<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.
|
||||
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!
|
||||
|
||||
## Translating
|
||||
|
||||
Translations can be improved very easily from our
|
||||
[Weblate](https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv) instance.
|
||||
Look through the following graphic to see if your native language could use some work!
|
||||
|
||||
<a href="https://translate.jellyfin.org/engage/jellyfin-android/">
|
||||
<img alt="Detailed Translation Status" src="https://translate.jellyfin.org/widgets/jellyfin-android/-/jellyfin-androidtv/multi-auto.svg"/>
|
||||
<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>
|
||||
|
||||
## Build Process
|
||||
|
||||
### Dependencies
|
||||
|
||||
- Android Studio
|
||||
|
||||
### Build
|
||||
|
||||
1. Clone or download this repository
|
||||
|
||||
```sh
|
||||
git clone https://github.com/jellyfin/jellyfin-androidtv.git
|
||||
cd jellyfin-androidtv
|
||||
```
|
||||
|
||||
2. Open the project in Android Studio and run it from there or build an APK directly through Gradle:
|
||||
|
||||
```sh
|
||||
./gradlew assembleDebug
|
||||
```
|
||||
|
||||
Add the Android SDK to you PATH environment variable or create the ANDROID_SDK_ROOT variable for
|
||||
this to work.
|
||||
|
||||
### Deploy to device/emulator
|
||||
|
||||
```sh
|
||||
./gradlew installDebug
|
||||
```
|
||||
|
||||
*You can also replace the "Debug" with "Release" to get an optimized release binary.*
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<lint>
|
||||
<issue id="BinaryOperationInTimber" severity="error" />
|
||||
<issue id="ConvertToWebp" severity="warning" />
|
||||
<issue id="DuplicateStrings" severity="ignore" />
|
||||
<issue id="EasterEgg" severity="fatal" />
|
||||
<issue id="ExtraTranslation" severity="warning" />
|
||||
<issue id="FragmentTagUsage" severity="error" />
|
||||
<issue id="GradleDependency" severity="ignore" />
|
||||
<issue id="KotlinPropertyAccess" severity="warning" />
|
||||
<issue id="LambdaLast" severity="warning" />
|
||||
<issue id="LogNotTimber" severity="error" />
|
||||
<issue id="MangledCRLF" severity="warning" />
|
||||
<issue id="NegativeMargin" severity="warning" />
|
||||
<issue id="NoHardKeywords" severity="warning" />
|
||||
<issue id="Registered" severity="warning" />
|
||||
<issue id="RequiredSize" severity="error" />
|
||||
<issue id="Typos" severity="ignore" />
|
||||
<issue id="UnknownNullness" severity="informational" />
|
||||
<issue id="UnusedIds" severity="ignore" />
|
||||
</lint>
|
||||
71
app/build.gradle
Normal file
71
app/build.gradle
Normal file
@@ -0,0 +1,71 @@
|
||||
apply plugin: 'com.android.application'
|
||||
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 906
|
||||
versionName version
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled false
|
||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
debug {
|
||||
applicationIdSuffix ".debug"
|
||||
debuggable true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
def acraVersion = '5.4.0'
|
||||
|
||||
implementation fileTree(include: ['*.jar'], dir: 'libs')
|
||||
|
||||
// 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'
|
||||
implementation 'androidx.palette:palette:1.0.0'
|
||||
implementation 'com.squareup.picasso:picasso:2.3.2'
|
||||
implementation 'com.github.bumptech.glide:glide:3.7.0'
|
||||
implementation 'com.squareup.okhttp:okhttp:2.7.0'
|
||||
implementation 'com.squareup.okhttp:okhttp-urlconnection:2.7.0'
|
||||
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()
|
||||
}
|
||||
@@ -1,143 +0,0 @@
|
||||
plugins {
|
||||
id("com.android.application")
|
||||
kotlin("android")
|
||||
kotlin("plugin.serialization") version Plugins.Versions.kotlin
|
||||
kotlin("kapt")
|
||||
}
|
||||
|
||||
android {
|
||||
compileSdkVersion(30)
|
||||
|
||||
defaultConfig {
|
||||
// Android version targets
|
||||
minSdkVersion(21)
|
||||
targetSdkVersion(30)
|
||||
|
||||
// Release version
|
||||
versionName = project.getVersionName()
|
||||
versionCode = getVersionCode(versionName!!)
|
||||
setProperty("archivesBaseName", "jellyfin-androidtv-v$versionName")
|
||||
}
|
||||
|
||||
sourceSets["main"].java.srcDirs("src/main/kotlin")
|
||||
sourceSets["test"].java.srcDirs("src/test/kotlin")
|
||||
|
||||
buildFeatures {
|
||||
viewBinding = true
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
isCoreLibraryDesugaringEnabled = true
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
val release by getting {
|
||||
isMinifyEnabled = false
|
||||
|
||||
// Add applicationId as string for XML resources
|
||||
resValue("string", "app_id", "org.jellyfin.androidtv")
|
||||
|
||||
// Set flavored application name
|
||||
resValue("string", "app_name", "@string/app_name_release")
|
||||
|
||||
buildConfigField("boolean", "DEVELOPMENT", "false")
|
||||
}
|
||||
|
||||
val debug by getting {
|
||||
// Use different application id to run release and debug at the same time
|
||||
applicationIdSuffix = ".debug"
|
||||
|
||||
// Add applicationId as string for XML resources
|
||||
resValue("string", "app_id", "org.jellyfin.androidtv.debug")
|
||||
|
||||
// Set flavored application name
|
||||
resValue("string", "app_name", "@string/app_name_debug")
|
||||
|
||||
buildConfigField("boolean", "DEVELOPMENT", (defaultConfig.versionCode!! < 100).toString())
|
||||
}
|
||||
}
|
||||
|
||||
lintOptions {
|
||||
lintConfig = file("$rootDir/android-lint.xml")
|
||||
isAbortOnError = false
|
||||
sarifReport = true
|
||||
}
|
||||
}
|
||||
|
||||
val versionTxt by tasks.registering {
|
||||
val path = buildDir.resolve("version.txt")
|
||||
|
||||
doLast {
|
||||
val versionString = "v${android.defaultConfig.versionName}=${android.defaultConfig.versionCode}"
|
||||
logger.info("Writing [$versionString] to $path")
|
||||
path.writeText("$versionString\n")
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
// Jellyfin
|
||||
implementation(projects.playback)
|
||||
implementation(libs.jellyfin.apiclient)
|
||||
implementation(libs.jellyfin.sdk) {
|
||||
// Change version if desired
|
||||
val sdkVersion = findProperty("sdk.version")?.toString()
|
||||
when (sdkVersion) {
|
||||
"local" -> version { strictly("latest-SNAPSHOT") }
|
||||
"snapshot" -> version { strictly("master-SNAPSHOT") }
|
||||
"unstable-snapshot" -> version { strictly("openapi-unstable-SNAPSHOT") }
|
||||
}
|
||||
}
|
||||
|
||||
// Kotlin
|
||||
implementation(libs.kotlinx.coroutines)
|
||||
implementation(libs.kotlinx.serialization.json)
|
||||
|
||||
// Android(x)
|
||||
implementation(libs.androidx.core)
|
||||
implementation(libs.androidx.activity)
|
||||
implementation(libs.androidx.fragment)
|
||||
implementation(libs.androidx.leanback.core)
|
||||
implementation(libs.androidx.leanback.preference)
|
||||
implementation(libs.androidx.preference)
|
||||
implementation(libs.androidx.appcompat)
|
||||
implementation(libs.androidx.tvprovider)
|
||||
implementation(libs.androidx.constraintlayout)
|
||||
implementation(libs.androidx.recyclerview)
|
||||
implementation(libs.androidx.work.runtime)
|
||||
implementation(libs.bundles.androidx.lifecycle)
|
||||
implementation(libs.androidx.window)
|
||||
implementation(libs.androidx.cardview)
|
||||
|
||||
// Dependency Injection
|
||||
implementation(libs.bundles.koin)
|
||||
|
||||
// GSON
|
||||
implementation(libs.gson)
|
||||
|
||||
// Media players
|
||||
implementation(libs.exoplayer)
|
||||
implementation(libs.libvlc)
|
||||
|
||||
// Image utility
|
||||
implementation(libs.glide.core)
|
||||
kapt(libs.glide.compiler)
|
||||
implementation(libs.kenburnsview)
|
||||
|
||||
// Crash Reporting
|
||||
implementation(libs.bundles.acra)
|
||||
|
||||
// Logging
|
||||
implementation(libs.timber)
|
||||
implementation(libs.slf4j.android)
|
||||
|
||||
// Debugging
|
||||
if (getProperty("leakcanary.enable")?.toBoolean() == true)
|
||||
debugImplementation(libs.leakcanary)
|
||||
|
||||
// Compatibility (desugaring)
|
||||
coreLibraryDesugaring(libs.android.desugar)
|
||||
|
||||
// Testing
|
||||
testImplementation(libs.junit)
|
||||
testImplementation(libs.mockito)
|
||||
}
|
||||
17
app/proguard-rules.pro
vendored
Normal file
17
app/proguard-rules.pro
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
# Add project specific ProGuard rules here.
|
||||
# By default, the flags in this file are appended to flags specified
|
||||
# in C:\Users\Eric\AppData\Local\Android\android-sdk/tools/proguard/proguard-android.txt
|
||||
# You can edit the include path and order by changing the proguardFiles
|
||||
# directive in build.gradle.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# Add any project specific keep options here:
|
||||
|
||||
# If your project uses WebView with JS, uncomment the following
|
||||
# and specify the fully qualified class name to the JavaScript interface
|
||||
# class:
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
@@ -1,7 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="logo_gradient_start">#F2364D</color>
|
||||
<color name="logo_gradient_stop">#FDC92F</color>
|
||||
<color name="logo_background">#0A0A0A</color>
|
||||
<color name="logo_text">#FFFFFF</color>
|
||||
</resources>
|
||||
@@ -1,33 +1,19 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
package="org.jellyfin.androidtv">
|
||||
package="org.jellyfin.androidtv" >
|
||||
|
||||
<!-- Android TV Integration -->
|
||||
<uses-permission android:name="com.android.providers.tv.permission.WRITE_EPG_DATA" />
|
||||
|
||||
<!-- AccountManager permissions -->
|
||||
<uses-permission
|
||||
android:name="android.permission.AUTHENTICATE_ACCOUNTS"
|
||||
android:maxSdkVersion="22" />
|
||||
<uses-permission
|
||||
android:name="android.permission.GET_ACCOUNTS"
|
||||
android:maxSdkVersion="22" />
|
||||
<uses-permission
|
||||
android:name="android.permission.MANAGE_ACCOUNTS"
|
||||
android:maxSdkVersion="22" />
|
||||
|
||||
<!-- Generic permissions -->
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
|
||||
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
||||
|
||||
<!-- Device feature requirements -->
|
||||
<uses-feature
|
||||
android:name="android.software.leanback"
|
||||
android:required="false" />
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<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" />
|
||||
@@ -36,28 +22,61 @@
|
||||
android:required="false" />
|
||||
|
||||
<application
|
||||
android:name=".JellyfinApplication"
|
||||
android:allowBackup="true"
|
||||
android:banner="@drawable/app_banner"
|
||||
android:fullBackupContent="@xml/backup_content"
|
||||
android:icon="@drawable/app_icon"
|
||||
android:name=".TvApp"
|
||||
android:banner="@drawable/app_logo"
|
||||
android:icon="@drawable/app_icon_large"
|
||||
android:label="@string/app_name"
|
||||
android:largeHeap="true"
|
||||
android:roundIcon="@drawable/app_icon_round"
|
||||
android:theme="@style/Theme.Jellyfin"
|
||||
android:allowBackup="false"
|
||||
android:usesCleartextTraffic="true"
|
||||
tools:ignore="GoogleAppIndexingWarning">
|
||||
|
||||
<service
|
||||
android:name=".auth.AuthenticatorService"
|
||||
android:exported="false">
|
||||
<activity
|
||||
android:name=".browsing.MainActivity"
|
||||
android:label="@string/app_name"
|
||||
android:logo="@drawable/app_logo"
|
||||
android:screenOrientation="landscape" />
|
||||
<activity
|
||||
android:name=".startup.StartupActivity"
|
||||
android:label="@string/app_name"
|
||||
android:logo="@drawable/app_logo"
|
||||
android:noHistory="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.accounts.AccountAuthenticator" />
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
|
||||
</intent-filter>
|
||||
<meta-data
|
||||
android:name="android.accounts.AccountAuthenticator"
|
||||
android:resource="@xml/authenticator" />
|
||||
</service>
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".startup.SelectUserActivity"
|
||||
android:label="@string/title_activity_select_user" />
|
||||
<activity
|
||||
android:name=".playback.PlaybackOverlayActivity"
|
||||
android:label="PlaybackOverlayActivity" />
|
||||
<activity android:name=".browsing.GenericFolderActivity" />
|
||||
<activity android:name=".browsing.UserViewActivity" />
|
||||
<activity android:name=".startup.SelectServerActivity" />
|
||||
<activity android:name=".browsing.CollectionActivity" />
|
||||
<activity android:name=".browsing.ByLetterActivity" />
|
||||
<activity android:name=".browsing.ByGenreActivity" />
|
||||
<activity android:name=".settings.SettingsActivity" />
|
||||
<activity android:name=".browsing.SuggestedMoviesActivity" />
|
||||
<activity android:name=".browsing.BrowsePersonsActivity" />
|
||||
<activity android:name=".search.SearchActivity" />
|
||||
<activity android:name=".details.FullDetailsActivity" />
|
||||
<activity android:name=".livetv.LiveTvGuideActivity" />
|
||||
<activity android:name=".browsing.GenericGridActivity" />
|
||||
<activity android:name=".browsing.BrowseRecordingsActivity" />
|
||||
<activity android:name=".browsing.BrowseScheduleActivity" />
|
||||
<activity android:name=".details.PhotoPlayerActivity" />
|
||||
<activity android:name=".details.ItemListActivity" />
|
||||
<activity android:name=".playback.AudioNowPlayingActivity" />
|
||||
<activity android:name=".playback.ExternalPlayerActivity" />
|
||||
<activity
|
||||
android:name=".startup.DpadPwActivity"
|
||||
android:label="@string/title_activity_dpad_pw" >
|
||||
</activity>
|
||||
|
||||
<receiver android:name=".util.RemoteControlReceiver">
|
||||
<intent-filter>
|
||||
@@ -66,109 +85,8 @@
|
||||
</receiver>
|
||||
|
||||
<activity
|
||||
android:name=".ui.browsing.MainActivity"
|
||||
android:screenOrientation="landscape" />
|
||||
<activity
|
||||
android:name=".ui.startup.StartupActivity"
|
||||
android:noHistory="true"
|
||||
android:screenOrientation="landscape"
|
||||
android:windowSoftInputMode="adjustResize">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity-alias
|
||||
android:name=".startup.StartupActivity"
|
||||
android:targetActivity=".ui.startup.StartupActivity" />
|
||||
|
||||
<activity
|
||||
android:name=".ui.playback.PlaybackOverlayActivity"
|
||||
android:screenOrientation="landscape" />
|
||||
|
||||
<activity
|
||||
android:name=".ui.browsing.GenericFolderActivity"
|
||||
android:screenOrientation="landscape" />
|
||||
|
||||
<activity
|
||||
android:name=".ui.browsing.UserViewActivity"
|
||||
android:screenOrientation="landscape" />
|
||||
|
||||
<activity
|
||||
android:name=".ui.browsing.CollectionActivity"
|
||||
android:screenOrientation="landscape" />
|
||||
|
||||
<activity
|
||||
android:name=".ui.browsing.ByLetterActivity"
|
||||
android:screenOrientation="landscape" />
|
||||
|
||||
<activity
|
||||
android:name=".ui.browsing.ByGenreActivity"
|
||||
android:screenOrientation="landscape" />
|
||||
|
||||
<activity
|
||||
android:name=".ui.preference.PreferencesActivity"
|
||||
android:theme="@style/Theme.Jellyfin.Preferences" />
|
||||
|
||||
<activity
|
||||
android:name=".ui.browsing.SuggestedMoviesActivity"
|
||||
android:screenOrientation="landscape" />
|
||||
|
||||
<activity
|
||||
android:name=".ui.browsing.BrowsePersonsActivity"
|
||||
android:screenOrientation="landscape" />
|
||||
|
||||
<activity
|
||||
android:name=".ui.search.SearchActivity"
|
||||
android:screenOrientation="landscape">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.SEARCH" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name=".ui.itemdetail.FullDetailsActivity"
|
||||
android:screenOrientation="landscape" />
|
||||
|
||||
<activity
|
||||
android:name=".ui.livetv.LiveTvGuideActivity"
|
||||
android:screenOrientation="landscape" />
|
||||
|
||||
<activity
|
||||
android:name=".ui.browsing.GenericGridActivity"
|
||||
android:screenOrientation="landscape" />
|
||||
|
||||
<activity
|
||||
android:name=".ui.browsing.BrowseRecordingsActivity"
|
||||
android:screenOrientation="landscape" />
|
||||
|
||||
<activity
|
||||
android:name=".ui.browsing.BrowseScheduleActivity"
|
||||
android:screenOrientation="landscape" />
|
||||
|
||||
<activity
|
||||
android:name=".ui.itemdetail.PhotoPlayerActivity"
|
||||
android:screenOrientation="landscape" />
|
||||
|
||||
<activity
|
||||
android:name=".ui.itemdetail.ItemListActivity"
|
||||
android:screenOrientation="landscape" />
|
||||
|
||||
<activity
|
||||
android:name=".ui.playback.AudioNowPlayingActivity"
|
||||
android:screenOrientation="landscape" />
|
||||
|
||||
<activity android:name=".ui.playback.ExternalPlayerActivity" />
|
||||
|
||||
<activity
|
||||
android:name=".ui.playback.nextup.NextUpActivity"
|
||||
android:noHistory="true"
|
||||
android:screenOrientation="landscape" />
|
||||
|
||||
<!-- Playback Rewrite - Temporary -->
|
||||
<activity android:name=".ui.playback.rewrite.PlaybackForwardingActivity" />
|
||||
android:name=".details.ExpandedTextActivity"
|
||||
android:label="@string/title_activity_expanded_text" />
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
|
||||
BIN
app/src/main/assets/fonts/Roboto-Light.ttf
Normal file
BIN
app/src/main/assets/fonts/Roboto-Light.ttf
Normal file
Binary file not shown.
@@ -1,134 +0,0 @@
|
||||
package org.jellyfin.androidtv
|
||||
|
||||
import android.content.Context
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.LifecycleObserver
|
||||
import androidx.lifecycle.OnLifecycleEvent
|
||||
import androidx.lifecycle.ProcessLifecycleOwner
|
||||
import androidx.work.*
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import org.acra.config.dialog
|
||||
import org.acra.config.httpSender
|
||||
import org.acra.config.limiter
|
||||
import org.acra.data.StringFormat
|
||||
import org.acra.ktx.initAcra
|
||||
import org.acra.sender.HttpSender
|
||||
import org.jellyfin.androidtv.auth.SessionRepository
|
||||
import org.jellyfin.androidtv.di.*
|
||||
import org.jellyfin.androidtv.integration.LeanbackChannelWorker
|
||||
import org.jellyfin.androidtv.util.AutoBitrate
|
||||
import org.koin.android.ext.android.get
|
||||
import org.koin.android.ext.android.getKoin
|
||||
import org.koin.android.ext.android.inject
|
||||
import org.koin.android.ext.koin.androidContext
|
||||
import org.koin.core.KoinExperimentalAPI
|
||||
import org.koin.core.context.startKoin
|
||||
import timber.log.Timber
|
||||
import timber.log.Timber.DebugTree
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
@Suppress("unused")
|
||||
class JellyfinApplication : TvApp(), LifecycleObserver {
|
||||
@OptIn(KoinExperimentalAPI::class)
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
|
||||
// Enable improved logging for leaking resources
|
||||
// https://wh0.github.io/2020/08/12/closeguard.html
|
||||
if (BuildConfig.DEBUG) {
|
||||
try {
|
||||
Class.forName("dalvik.system.CloseGuard")
|
||||
.getMethod("setEnabled", Boolean::class.javaPrimitiveType)
|
||||
.invoke(null, true)
|
||||
} catch (e: ReflectiveOperationException) {
|
||||
@Suppress("TooGenericExceptionThrown")
|
||||
throw RuntimeException(e)
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize the logging library
|
||||
Timber.plant(DebugTree())
|
||||
Timber.i("Application object created")
|
||||
|
||||
// Dependency Injection
|
||||
startKoin {
|
||||
androidContext(this@JellyfinApplication)
|
||||
|
||||
modules(
|
||||
appModule,
|
||||
authModule,
|
||||
activityLifecycleCallbacksModule,
|
||||
playbackModule,
|
||||
preferenceModule,
|
||||
utilsModule
|
||||
)
|
||||
}
|
||||
|
||||
// Register lifecycle callbacks
|
||||
getKoin().getAll<ActivityLifecycleCallbacks>().forEach(::registerActivityLifecycleCallbacks)
|
||||
|
||||
ProcessLifecycleOwner.get().lifecycle.addObserver(this)
|
||||
|
||||
// Restore system session
|
||||
get<SessionRepository>().restoreDefaultSystemSession()
|
||||
}
|
||||
|
||||
/**
|
||||
* Called by the Process Lifecycle when the app is activated in the foreground (activity opened).
|
||||
*/
|
||||
@OnLifecycleEvent(Lifecycle.Event.ON_START)
|
||||
fun onActivate() {
|
||||
Timber.i("Process Lifecycle started")
|
||||
|
||||
get<SessionRepository>().restoreDefaultSession()
|
||||
}
|
||||
|
||||
/**
|
||||
* Called from the StartupActivity when the user session is started.
|
||||
*/
|
||||
@DelicateCoroutinesApi
|
||||
suspend fun onSessionStart() {
|
||||
val workManager by inject<WorkManager>()
|
||||
val autoBitrate by inject<AutoBitrate>()
|
||||
|
||||
// Cancel all current workers
|
||||
workManager.cancelAllWork().await()
|
||||
|
||||
// Recreate periodic workers
|
||||
workManager.enqueueUniquePeriodicWork(
|
||||
LeanbackChannelWorker.PERIODIC_UPDATE_REQUEST_NAME,
|
||||
ExistingPeriodicWorkPolicy.REPLACE,
|
||||
PeriodicWorkRequestBuilder<LeanbackChannelWorker>(1, TimeUnit.HOURS)
|
||||
.setBackoffCriteria(BackoffPolicy.LINEAR, 10, TimeUnit.MINUTES)
|
||||
.build()
|
||||
).await()
|
||||
|
||||
// Detect auto bitrate
|
||||
GlobalScope.launch(Dispatchers.IO) { autoBitrate.detect() }
|
||||
}
|
||||
|
||||
override fun attachBaseContext(base: Context?) {
|
||||
super.attachBaseContext(base)
|
||||
|
||||
initAcra {
|
||||
buildConfigClass = BuildConfig::class.java
|
||||
reportFormat = StringFormat.JSON
|
||||
|
||||
httpSender {
|
||||
uri = "https://collector.tracepot.com/a2eda9d9"
|
||||
httpMethod = HttpSender.Method.POST
|
||||
}
|
||||
|
||||
dialog {
|
||||
withResTitle(R.string.acra_dialog_title)
|
||||
withResText(R.string.acra_dialog_text)
|
||||
withResTheme(R.style.Theme_Jellyfin)
|
||||
}
|
||||
|
||||
limiter {}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
package org.jellyfin.androidtv
|
||||
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import com.bumptech.glide.GlideBuilder
|
||||
import com.bumptech.glide.annotation.GlideModule
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
||||
import com.bumptech.glide.module.AppGlideModule
|
||||
import com.bumptech.glide.request.RequestOptions
|
||||
|
||||
@GlideModule
|
||||
class JellyfinGlideModule : AppGlideModule() {
|
||||
override fun applyOptions(context: Context, builder: GlideBuilder): Unit = with(builder) {
|
||||
setDefaultRequestOptions(
|
||||
// Set default disk cache strategy
|
||||
RequestOptions().diskCacheStrategy(DiskCacheStrategy.RESOURCE)
|
||||
)
|
||||
|
||||
// Silence image load errors
|
||||
setLogLevel(Log.ERROR)
|
||||
}
|
||||
}
|
||||
@@ -1,31 +1,70 @@
|
||||
package org.jellyfin.androidtv;
|
||||
|
||||
import static org.koin.java.KoinJavaComponent.inject;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.Application;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Typeface;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.graphics.drawable.GradientDrawable;
|
||||
import android.media.AudioManager;
|
||||
import android.preference.PreferenceManager;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.MediatorLiveData;
|
||||
|
||||
import org.jellyfin.androidtv.ui.livetv.TvManager;
|
||||
import org.jellyfin.androidtv.ui.playback.PlaybackController;
|
||||
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.util.DeviceUtils;
|
||||
import org.jellyfin.androidtv.util.Utils;
|
||||
import org.jellyfin.apiclient.interaction.ApiClient;
|
||||
import org.jellyfin.apiclient.interaction.EmptyResponse;
|
||||
import org.jellyfin.apiclient.interaction.IConnectionManager;
|
||||
import org.jellyfin.apiclient.interaction.Response;
|
||||
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;
|
||||
|
||||
import java.util.Calendar;
|
||||
import java.util.HashMap;
|
||||
|
||||
import kotlin.Lazy;
|
||||
import timber.log.Timber;
|
||||
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 {
|
||||
public static final String DISPLAY_PREFS_APP_NAME = "ATV";
|
||||
// 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;
|
||||
@@ -33,61 +72,152 @@ public class TvApp extends Application {
|
||||
public static final int LIVE_TV_SCHEDULE_OPTION_ID = 4000;
|
||||
public static final int LIVE_TV_SERIES_OPTION_ID = 5000;
|
||||
|
||||
private static final String TAG = "Jellyfin-AndroidTV";
|
||||
|
||||
private ILogger logger;
|
||||
private IConnectionManager connectionManager;
|
||||
private PlaybackManager playbackManager;
|
||||
private GsonJsonSerializer serializer;
|
||||
private static TvApp app;
|
||||
private MediatorLiveData<UserDto> currentUser = new MediatorLiveData<UserDto>();
|
||||
private UserDto currentUser;
|
||||
private BaseItemDto currentPlayingItem;
|
||||
private BaseItemDto lastPlayedItem;
|
||||
private PlaybackController playbackController;
|
||||
private ApiClient loginApiClient;
|
||||
private AudioManager audioManager;
|
||||
private VolleyHttpClient httpClient;
|
||||
|
||||
private int autoBitrate;
|
||||
private String directItemId;
|
||||
private Typeface roboto;
|
||||
|
||||
private HashMap<String, DisplayPreferences> displayPrefsCache = new HashMap<>();
|
||||
|
||||
private Activity currentActivity;
|
||||
private String lastDeletedItemId = "";
|
||||
|
||||
private Lazy<ApiClient> apiClient = inject(ApiClient.class);
|
||||
private ServerConfiguration serverConfiguration;
|
||||
|
||||
private int maxRemoteBitrate = -1;
|
||||
|
||||
private Calendar lastPlayback = Calendar.getInstance();
|
||||
private Calendar lastMoviePlayback = Calendar.getInstance();
|
||||
private Calendar lastTvPlayback = Calendar.getInstance();
|
||||
private Calendar lastLibraryChange = Calendar.getInstance();
|
||||
private long lastVideoQueueChange = System.currentTimeMillis();
|
||||
private long lastFavoriteUpdate = System.currentTimeMillis();
|
||||
private long lastMusicPlayback = System.currentTimeMillis();
|
||||
private long lastUserInteraction = System.currentTimeMillis();
|
||||
|
||||
private GradientDrawable currentBackgroundGradient;
|
||||
|
||||
private boolean audioMuted;
|
||||
private boolean playingIntros;
|
||||
private DisplayPriorityType displayPriority = DisplayPriorityType.Movies;
|
||||
|
||||
private BaseActivity currentActivity;
|
||||
|
||||
private LogonCredentials configuredAutoCredentials;
|
||||
|
||||
@Override
|
||||
protected void attachBaseContext(Context base) {
|
||||
super.attachBaseContext(base);
|
||||
|
||||
ACRA.init(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
logger = new AndroidLogger(TAG);
|
||||
app = (TvApp) getApplicationContext();
|
||||
audioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
|
||||
roboto = Typeface.createFromAsset(getAssets(), "fonts/Roboto-Light.ttf");
|
||||
setCurrentBackgroundGradient(new int[] {ContextCompat.getColor(this, R.color.lb_default_brand_color_dark), ContextCompat.getColor(this, R.color.lb_default_brand_color)});
|
||||
|
||||
app = this;
|
||||
logger.Info("Application object created");
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static TvApp getApplication() {
|
||||
return app;
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
@Nullable
|
||||
public UserDto getCurrentUser() {
|
||||
return currentUser.getValue();
|
||||
public ILogger getLogger() {
|
||||
return logger;
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
public LiveData<UserDto> getCurrentUserLiveData() {
|
||||
public void setLogger(ILogger value) {
|
||||
logger = value;
|
||||
}
|
||||
|
||||
public Typeface getDefaultFont() { return roboto; }
|
||||
|
||||
public IConnectionManager getConnectionManager() {
|
||||
return connectionManager;
|
||||
}
|
||||
|
||||
public void setConnectionManager(IConnectionManager connectionManager) {
|
||||
this.connectionManager = connectionManager;
|
||||
}
|
||||
|
||||
public UserDto getCurrentUser() {
|
||||
return currentUser;
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
public void setCurrentUser(UserDto currentUser) {
|
||||
this.currentUser.postValue(currentUser);
|
||||
this.currentUser = currentUser;
|
||||
TvManager.clearCache();
|
||||
this.displayPrefsCache = new HashMap<>();
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated This function is causing a **lot** of issues because not all activities will set their self as "currentactivity". Try to receive a Context instance instead.
|
||||
*/
|
||||
@Deprecated
|
||||
@Nullable
|
||||
public Activity getCurrentActivity() {
|
||||
public GsonJsonSerializer getSerializer() {
|
||||
return serializer;
|
||||
}
|
||||
|
||||
public void setSerializer(GsonJsonSerializer serializer) {
|
||||
this.serializer = serializer;
|
||||
}
|
||||
|
||||
public ApiClient getApiClient() {
|
||||
return currentUser != null ? connectionManager.GetApiClient(currentUser) : null;
|
||||
}
|
||||
|
||||
public BaseItemDto getCurrentPlayingItem() {
|
||||
return currentPlayingItem;
|
||||
}
|
||||
|
||||
public BaseActivity getCurrentActivity() {
|
||||
return currentActivity;
|
||||
}
|
||||
|
||||
public void setCurrentActivity(Activity activity) {
|
||||
public void setCurrentActivity(BaseActivity activity) {
|
||||
currentActivity = activity;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public void setCurrentPlayingItem(BaseItemDto currentPlayingItem) {
|
||||
this.currentPlayingItem = currentPlayingItem;
|
||||
}
|
||||
|
||||
public ApiClient getLoginApiClient() {
|
||||
return loginApiClient;
|
||||
}
|
||||
|
||||
public void setLoginApiClient(ApiClient loginApiClient) {
|
||||
this.loginApiClient = loginApiClient;
|
||||
}
|
||||
|
||||
public void setAudioMuted(boolean value) {
|
||||
audioMuted = value;
|
||||
getLogger().Info("Setting mute state to: "+audioMuted);
|
||||
if (DeviceUtils.is60()) {
|
||||
audioManager.adjustVolume(audioMuted ? AudioManager.ADJUST_MUTE : AudioManager.ADJUST_UNMUTE, 0);
|
||||
|
||||
} else {
|
||||
audioManager.setStreamMute(AudioManager.STREAM_MUSIC, audioMuted);
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isAudioMuted() { return audioMuted; }
|
||||
|
||||
public PlaybackController getPlaybackController() {
|
||||
return playbackController;
|
||||
}
|
||||
@@ -96,21 +226,214 @@ public class TvApp extends Application {
|
||||
this.playbackController = playbackController;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public void showSearch(final Activity activity, boolean musicOnly) {
|
||||
Intent intent = new Intent(activity, SearchActivity.class);
|
||||
intent.putExtra("MusicOnly", musicOnly);
|
||||
|
||||
activity.startActivity(intent);
|
||||
}
|
||||
|
||||
public void showMessage(String title, String msg) {
|
||||
if (currentActivity != null) {
|
||||
currentActivity.showMessage(title, msg);
|
||||
}
|
||||
}
|
||||
|
||||
public void showMessage(String title, String msg, int timeout, int iconResource) {
|
||||
if (currentActivity != null) {
|
||||
currentActivity.showMessage(title, msg, timeout, iconResource, null);
|
||||
}
|
||||
}
|
||||
|
||||
public LogonCredentials getConfiguredAutoCredentials() {
|
||||
return configuredAutoCredentials;
|
||||
}
|
||||
|
||||
public void setConfiguredAutoCredentials(LogonCredentials configuredAutoCredentials) {
|
||||
this.configuredAutoCredentials = configuredAutoCredentials;
|
||||
}
|
||||
|
||||
public SharedPreferences getPrefs() {
|
||||
return PreferenceManager.getDefaultSharedPreferences(this);
|
||||
}
|
||||
|
||||
public SharedPreferences getSystemPrefs() {
|
||||
return getSharedPreferences("systemprefs", MODE_PRIVATE);
|
||||
}
|
||||
|
||||
public String getConfigVersion() {
|
||||
return getSystemPrefs().getString("sys_pref_config_version", "2");
|
||||
}
|
||||
|
||||
public boolean getIsAutoLoginConfigured() {
|
||||
return getPrefs().getString("pref_login_behavior", "0").equals("1") && getConfiguredAutoCredentials().getServerInfo().getId() != null;
|
||||
}
|
||||
|
||||
public boolean useExternalPlayer(BaseItemType itemType) {
|
||||
switch (itemType) {
|
||||
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(BaseItemType itemType) {
|
||||
return useExternalPlayer(itemType) ? ExternalPlayerActivity.class : PlaybackOverlayActivity.class;
|
||||
}
|
||||
|
||||
public Calendar getLastMoviePlayback() {
|
||||
return lastMoviePlayback.after(lastPlayback) ? lastMoviePlayback : lastPlayback;
|
||||
}
|
||||
|
||||
public void setLastMoviePlayback(Calendar lastMoviePlayback) {
|
||||
this.lastMoviePlayback = lastMoviePlayback;
|
||||
this.lastPlayback = lastMoviePlayback;
|
||||
}
|
||||
|
||||
public void setLastFavoriteUpdate(long time) { lastFavoriteUpdate = time; }
|
||||
public long getLastFavoriteUpdate() { return lastFavoriteUpdate; }
|
||||
|
||||
public void setLastMusicPlayback(long time) { lastMusicPlayback = time; }
|
||||
public long getLastMusicPlayback() { return lastMusicPlayback; }
|
||||
|
||||
public boolean directStreamLiveTv() { return getPrefs().getBoolean("pref_live_direct", true); }
|
||||
|
||||
public void setDirectStreamLiveTv(boolean value) { getPrefs().edit().putBoolean("pref_live_direct", value).commit(); }
|
||||
|
||||
public boolean useVlcForLiveTv() { return getPrefs().getBoolean("pref_enable_vlc_livetv", false); }
|
||||
|
||||
public int getResumePreroll() {
|
||||
try {
|
||||
return Integer.parseInt(getPrefs().getString("pref_resume_preroll","0")) * 1000;
|
||||
} catch (Exception e) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
public Calendar getLastTvPlayback() {
|
||||
return lastTvPlayback.after(lastPlayback) ? lastTvPlayback : lastPlayback;
|
||||
}
|
||||
|
||||
public void setLastTvPlayback(Calendar lastTvPlayback) {
|
||||
this.lastTvPlayback = lastTvPlayback;
|
||||
this.lastPlayback = lastTvPlayback;
|
||||
}
|
||||
|
||||
public Calendar getLastLibraryChange() {
|
||||
return lastLibraryChange;
|
||||
}
|
||||
|
||||
public void setLastLibraryChange(Calendar lastLibraryChange) {
|
||||
this.lastLibraryChange = lastLibraryChange;
|
||||
}
|
||||
|
||||
public Calendar getLastPlayback() {
|
||||
return lastPlayback;
|
||||
}
|
||||
|
||||
public void setLastPlayback(Calendar lastPlayback) {
|
||||
this.lastPlayback = lastPlayback;
|
||||
}
|
||||
|
||||
public long getLastUserInteraction() {
|
||||
return lastUserInteraction;
|
||||
}
|
||||
|
||||
public void setLastUserInteraction(long lastUserInteraction) {
|
||||
this.lastUserInteraction = lastUserInteraction;
|
||||
}
|
||||
|
||||
public boolean canManageRecordings() {
|
||||
UserDto currentUser = getCurrentUser();
|
||||
return currentUser != null && currentUser.getPolicy().getEnableLiveTvManagement();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public DisplayPreferences getCachedDisplayPrefs(String key) {
|
||||
return getCachedDisplayPrefs(key, DISPLAY_PREFS_APP_NAME);
|
||||
public PlaybackManager getPlaybackManager() {
|
||||
return playbackManager;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public DisplayPreferences getCachedDisplayPrefs(String key, String app) {
|
||||
String cacheKey = String.format("%s.%s", app, key);
|
||||
return displayPrefsCache.containsKey(cacheKey) ? displayPrefsCache.get(cacheKey) : new DisplayPreferences();
|
||||
public void setPlaybackManager(PlaybackManager playbackManager) {
|
||||
this.playbackManager = playbackManager;
|
||||
}
|
||||
|
||||
public Drawable getDrawableCompat(int id) {
|
||||
// if (Build.VERSION.SDK_INT >= 21) {
|
||||
// return getDrawable(id);
|
||||
// }
|
||||
|
||||
return getResources().getDrawable(id);
|
||||
}
|
||||
|
||||
public boolean isPlayingVideo() {
|
||||
return playbackController != null && currentActivity != null && currentActivity instanceof PlaybackOverlayActivity;
|
||||
}
|
||||
|
||||
public void stopPlayback() {
|
||||
if (isPlayingVideo()) {
|
||||
currentActivity.finish();
|
||||
} else if (MediaManager.isPlayingAudio()) {
|
||||
MediaManager.stopAudio();
|
||||
}
|
||||
}
|
||||
|
||||
public void pausePlayback() {
|
||||
if (MediaManager.isPlayingAudio()) {
|
||||
MediaManager.pauseAudio();
|
||||
} else if (isPlayingVideo()) {
|
||||
playbackController.playPause();
|
||||
}
|
||||
}
|
||||
public void unPausePlayback() {
|
||||
if (isPlayingVideo()) {
|
||||
playbackController.playPause();
|
||||
} else if (MediaManager.hasAudioQueueItems()) {
|
||||
MediaManager.resumeAudio();
|
||||
}
|
||||
}
|
||||
|
||||
public void playbackNext() {
|
||||
if (isPlayingVideo()) {
|
||||
playbackController.next();
|
||||
} else if (MediaManager.hasAudioQueueItems()) {
|
||||
MediaManager.nextAudioItem();
|
||||
}
|
||||
}
|
||||
|
||||
public void playbackPrev() {
|
||||
if (isPlayingVideo()) {
|
||||
playbackController.prev();
|
||||
} else if (MediaManager.hasAudioQueueItems()) {
|
||||
MediaManager.prevAudioItem();
|
||||
}
|
||||
}
|
||||
|
||||
public void playbackSeek(int pos) {
|
||||
if (isPlayingVideo()) {
|
||||
playbackController.seek(pos);
|
||||
}
|
||||
}
|
||||
|
||||
public void playbackJump() {
|
||||
if (isPlayingVideo()) {
|
||||
playbackController.skip(30000);
|
||||
}
|
||||
}
|
||||
|
||||
public void playbackJumpBack() {
|
||||
if (playbackController != null) {
|
||||
playbackController.skip(-11000);
|
||||
}
|
||||
}
|
||||
|
||||
public DisplayPreferences getCachedDisplayPrefs(String key) {
|
||||
return displayPrefsCache.containsKey(key) ? displayPrefsCache.get(key) : new DisplayPreferences();
|
||||
}
|
||||
|
||||
public void updateDisplayPrefs(DisplayPreferences preferences) {
|
||||
@@ -118,38 +441,34 @@ public class TvApp extends Application {
|
||||
}
|
||||
|
||||
public void updateDisplayPrefs(String app, DisplayPreferences preferences) {
|
||||
displayPrefsCache.put(String.format("%s.%s", app, preferences.getId()), preferences);
|
||||
apiClient.getValue().UpdateDisplayPreferencesAsync(preferences, getCurrentUser().getId(), app, new EmptyResponse());
|
||||
Timber.d("Display prefs updated for %s isFavorite: %s", preferences.getId(), preferences.getCustomPrefs().get("FavoriteOnly"));
|
||||
displayPrefsCache.put(preferences.getId(), preferences);
|
||||
getApiClient().UpdateDisplayPreferencesAsync(preferences, getCurrentUser().getId(), app, new EmptyResponse());
|
||||
logger.Debug("Display prefs updated for "+preferences.getId()+" isFavorite: "+preferences.getCustomPrefs().get("FavoriteOnly"));
|
||||
}
|
||||
|
||||
public void getDisplayPrefsAsync(String key, Response<DisplayPreferences> response) {
|
||||
getDisplayPrefsAsync(key, DISPLAY_PREFS_APP_NAME, response);
|
||||
getDisplayPrefsAsync(key, "ATV", response);
|
||||
}
|
||||
|
||||
public void getDisplayPrefsAsync(final String key, String app, final Response<DisplayPreferences> outerResponse) {
|
||||
String cacheKey = String.format("%s.%s", app, key);
|
||||
|
||||
if (displayPrefsCache.containsKey(cacheKey)) {
|
||||
Timber.d("Display prefs loaded from cache %s", cacheKey);
|
||||
outerResponse.onResponse(displayPrefsCache.get(cacheKey));
|
||||
if (displayPrefsCache.containsKey(key)) {
|
||||
logger.Debug("Display prefs loaded from cache "+key);
|
||||
outerResponse.onResponse(displayPrefsCache.get(key));
|
||||
} else {
|
||||
apiClient.getValue().GetDisplayPreferencesAsync(key, getCurrentUser().getId(), app, new Response<DisplayPreferences>() {
|
||||
getApiClient().GetDisplayPreferencesAsync(key, getCurrentUser().getId(), app, new Response<DisplayPreferences>(){
|
||||
@Override
|
||||
public void onResponse(DisplayPreferences response) {
|
||||
if (response.getSortBy() == null) response.setSortBy("SortName");
|
||||
if (response.getCustomPrefs() == null)
|
||||
response.setCustomPrefs(new HashMap<String, String>());
|
||||
|
||||
displayPrefsCache.put(cacheKey, response);
|
||||
Timber.d("Display prefs loaded and saved in cache %s", cacheKey);
|
||||
if (response.getCustomPrefs() == null) response.setCustomPrefs(new HashMap<String, String>());
|
||||
displayPrefsCache.put(key, response);
|
||||
logger.Debug("Display prefs loaded and saved in cache " + key);
|
||||
outerResponse.onResponse(response);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(Exception exception) {
|
||||
//Continue with defaults
|
||||
Timber.e(exception, "Unable to load display prefs ");
|
||||
logger.ErrorException("Unable to load display prefs ", exception);
|
||||
DisplayPreferences prefs = new DisplayPreferences();
|
||||
prefs.setId(key);
|
||||
prefs.setSortBy("SortName");
|
||||
@@ -160,7 +479,47 @@ public class TvApp extends Application {
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
|
||||
public int getAutoBitrate() {
|
||||
return autoBitrate;
|
||||
}
|
||||
|
||||
public void determineAutoBitrate() {
|
||||
if (getApiClient() == null) return;
|
||||
getApiClient().detectBitrate(new Response<Long>() {
|
||||
@Override
|
||||
public void onResponse(Long response) {
|
||||
autoBitrate = response.intValue();
|
||||
logger.Info("Auto bitrate set to: "+autoBitrate);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
public String getDirectItemId() {
|
||||
return directItemId;
|
||||
}
|
||||
|
||||
public void setDirectItemId(String directItemId) {
|
||||
this.directItemId = directItemId;
|
||||
}
|
||||
|
||||
public String getLastDeletedItemId() {
|
||||
return lastDeletedItemId;
|
||||
}
|
||||
|
||||
public void setLastDeletedItemId(String lastDeletedItemId) {
|
||||
this.lastDeletedItemId = lastDeletedItemId;
|
||||
}
|
||||
|
||||
public VolleyHttpClient getHttpClient() {
|
||||
return httpClient;
|
||||
}
|
||||
|
||||
public void setHttpClient(VolleyHttpClient httpClient) {
|
||||
this.httpClient = httpClient;
|
||||
}
|
||||
|
||||
public BaseItemDto getLastPlayedItem() {
|
||||
return lastPlayedItem;
|
||||
}
|
||||
@@ -168,4 +527,52 @@ public class TvApp extends Application {
|
||||
public void setLastPlayedItem(BaseItemDto lastPlayedItem) {
|
||||
this.lastPlayedItem = lastPlayedItem;
|
||||
}
|
||||
|
||||
public long getLastVideoQueueChange() {
|
||||
return lastVideoQueueChange;
|
||||
}
|
||||
|
||||
public void setLastVideoQueueChange(long lastVideoQueueChange) {
|
||||
this.lastVideoQueueChange = lastVideoQueueChange;
|
||||
}
|
||||
|
||||
public boolean isPlayingIntros() {
|
||||
return playingIntros;
|
||||
}
|
||||
|
||||
public void setPlayingIntros(boolean playingIntros) {
|
||||
this.playingIntros = playingIntros;
|
||||
}
|
||||
|
||||
public ServerConfiguration getServerConfiguration() {
|
||||
return serverConfiguration;
|
||||
}
|
||||
|
||||
public int getServerBitrateLimit() { return maxRemoteBitrate > 0 ? maxRemoteBitrate : 100000000; }
|
||||
|
||||
public DisplayPriorityType getDisplayPriority() {
|
||||
return displayPriority;
|
||||
}
|
||||
|
||||
public void setDisplayPriority(DisplayPriorityType displayPriority) {
|
||||
this.displayPriority = displayPriority;
|
||||
}
|
||||
|
||||
public GradientDrawable getCurrentBackgroundGradient() {
|
||||
return currentBackgroundGradient;
|
||||
}
|
||||
|
||||
public void setCurrentBackground(Bitmap currentBackground) {
|
||||
int[] colors = new int[2];
|
||||
colors[0] = Utils.darker(Palette.from(currentBackground).generate().getMutedColor(ContextCompat.getColor(this, R.color.black_transparent)), .6f);
|
||||
colors[1] = Utils.darker(colors[0], .1f);
|
||||
setCurrentBackgroundGradient(colors);
|
||||
}
|
||||
|
||||
private void setCurrentBackgroundGradient(int[] colors) {
|
||||
currentBackgroundGradient = new GradientDrawable(GradientDrawable.Orientation.RIGHT_LEFT, colors);
|
||||
currentBackgroundGradient.setCornerRadius(0f);
|
||||
currentBackgroundGradient.setGradientCenter(.6f, .5f);
|
||||
currentBackgroundGradient.setAlpha(200);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,81 +0,0 @@
|
||||
package org.jellyfin.androidtv.auth
|
||||
|
||||
import android.accounts.Account
|
||||
import android.accounts.AccountManager
|
||||
import android.os.Bundle
|
||||
import org.jellyfin.androidtv.BuildConfig
|
||||
import org.jellyfin.androidtv.auth.model.AccountManagerAccount
|
||||
import org.jellyfin.sdk.model.serializer.toUUID
|
||||
import org.jellyfin.sdk.model.serializer.toUUIDOrNull
|
||||
import java.util.*
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.suspendCoroutine
|
||||
|
||||
class AccountManagerHelper(
|
||||
private val accountManager: AccountManager
|
||||
) {
|
||||
companion object {
|
||||
const val ACCOUNT_TYPE = BuildConfig.APPLICATION_ID
|
||||
const val ACCOUNT_DATA_ID = "$ACCOUNT_TYPE.id"
|
||||
const val ACCOUNT_DATA_SERVER = "$ACCOUNT_TYPE.server"
|
||||
const val ACCOUNT_DATA_NAME = "$ACCOUNT_TYPE.name"
|
||||
const val ACCOUNT_ACCESS_TOKEN_TYPE = "$ACCOUNT_TYPE.access_token"
|
||||
}
|
||||
|
||||
private fun getAccountData(account: Account): AccountManagerAccount = AccountManagerAccount(
|
||||
id = accountManager.getUserData(account, ACCOUNT_DATA_ID).toUUID(),
|
||||
server = accountManager.getUserData(account, ACCOUNT_DATA_SERVER).toUUID(),
|
||||
name = accountManager.getUserData(account, ACCOUNT_DATA_NAME),
|
||||
accessToken = accountManager.peekAuthToken(account, ACCOUNT_ACCESS_TOKEN_TYPE)
|
||||
)
|
||||
|
||||
suspend fun putAccount(accountManagerAccount: AccountManagerAccount) {
|
||||
var androidAccount = accountManager.getAccountsByType(ACCOUNT_TYPE)
|
||||
.firstOrNull { accountManager.getUserData(it, ACCOUNT_DATA_ID)?.toUUIDOrNull() == accountManagerAccount.id }
|
||||
|
||||
// Update credentials
|
||||
if (androidAccount == null) {
|
||||
androidAccount = Account(accountManagerAccount.name, ACCOUNT_TYPE)
|
||||
|
||||
accountManager.addAccountExplicitly(
|
||||
androidAccount,
|
||||
"", // Leave password empty
|
||||
Bundle()
|
||||
)
|
||||
accountManager.setUserData(androidAccount, ACCOUNT_DATA_ID, accountManagerAccount.id.toString())
|
||||
}
|
||||
|
||||
// Update name
|
||||
if (androidAccount.name != accountManagerAccount.name) {
|
||||
androidAccount = suspendCoroutine { continuation ->
|
||||
accountManager.renameAccount(androidAccount, accountManagerAccount.name, { continuation.resume(it.result) }, null)
|
||||
}
|
||||
}
|
||||
|
||||
accountManager.setUserData(androidAccount, ACCOUNT_DATA_NAME, accountManagerAccount.name)
|
||||
accountManager.setUserData(androidAccount, ACCOUNT_DATA_SERVER, accountManagerAccount.server.toString())
|
||||
accountManager.setAuthToken(androidAccount, ACCOUNT_ACCESS_TOKEN_TYPE, accountManagerAccount.accessToken)
|
||||
}
|
||||
|
||||
fun removeAccount(accountManagerAccount: AccountManagerAccount) {
|
||||
val androidAccount = accountManager.getAccountsByType(ACCOUNT_TYPE)
|
||||
.first { accountManager.getUserData(it, ACCOUNT_DATA_ID)?.toUUIDOrNull() == accountManagerAccount.id }
|
||||
|
||||
// Remove current account info
|
||||
@Suppress("DEPRECATION")
|
||||
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP_MR1)
|
||||
accountManager.removeAccountExplicitly(androidAccount)
|
||||
else
|
||||
accountManager.removeAccount(androidAccount, null, null)
|
||||
}
|
||||
|
||||
fun getAccounts() = accountManager.getAccountsByType(ACCOUNT_TYPE).map(::getAccountData)
|
||||
|
||||
fun getAccountsByServer(server: UUID) = accountManager.getAccountsByType(ACCOUNT_TYPE).filter { account ->
|
||||
accountManager.getUserData(account, ACCOUNT_DATA_SERVER)?.toUUIDOrNull() == server
|
||||
}.map(::getAccountData)
|
||||
|
||||
fun getAccount(id: UUID) = accountManager.getAccountsByType(ACCOUNT_TYPE).firstOrNull { account ->
|
||||
accountManager.getUserData(account, ACCOUNT_DATA_ID)?.toUUIDOrNull() == id
|
||||
}?.let(::getAccountData)
|
||||
}
|
||||
@@ -1,80 +0,0 @@
|
||||
package org.jellyfin.androidtv.auth
|
||||
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import org.jellyfin.androidtv.JellyfinApplication
|
||||
import org.jellyfin.androidtv.util.apiclient.callApi
|
||||
import org.jellyfin.androidtv.util.apiclient.callApiEmpty
|
||||
import org.jellyfin.apiclient.interaction.ApiClient
|
||||
import org.jellyfin.apiclient.interaction.device.IDevice
|
||||
import org.jellyfin.apiclient.model.apiclient.ServerInfo
|
||||
import org.jellyfin.apiclient.model.dto.UserDto
|
||||
import org.jellyfin.apiclient.model.entities.MediaType
|
||||
import org.jellyfin.apiclient.model.session.ClientCapabilities
|
||||
import org.jellyfin.apiclient.model.session.GeneralCommandType
|
||||
import timber.log.Timber
|
||||
|
||||
class ApiBinder(
|
||||
private val application: JellyfinApplication,
|
||||
private val api: ApiClient,
|
||||
private val device: IDevice,
|
||||
private val authenticationStore: AuthenticationStore,
|
||||
) {
|
||||
fun updateSession(session: Session?, resultCallback: (Boolean) -> Unit) {
|
||||
GlobalScope.launch(Dispatchers.IO) {
|
||||
@Suppress("TooGenericExceptionCaught")
|
||||
try {
|
||||
val success = updateSessionInternal(session)
|
||||
resultCallback(success)
|
||||
} catch (throwable: Throwable) {
|
||||
Timber.e(throwable, "Unable to update legacy API session.")
|
||||
resultCallback(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun updateSessionInternal(session: Session?): Boolean {
|
||||
if (session == null) {
|
||||
application.currentUser = null
|
||||
return true
|
||||
}
|
||||
|
||||
val server = authenticationStore.getServer(session.serverId)
|
||||
if (server == null) {
|
||||
Timber.e("Could not bind API because server ${session.serverId} was not found in the store.")
|
||||
return false
|
||||
}
|
||||
|
||||
api.device = AuthenticationDevice(device, session.userId.toString())
|
||||
api.SetAuthenticationInfo(session.accessToken, session.userId.toString())
|
||||
api.EnableAutomaticNetworking(ServerInfo().apply {
|
||||
id = session.serverId.toString()
|
||||
name = server.name
|
||||
address = server.address.removeSuffix("/")
|
||||
userId = session.userId.toString()
|
||||
accessToken = session.accessToken
|
||||
})
|
||||
|
||||
// Update currentUser DTO
|
||||
val user = callApi<UserDto> { callback -> api.GetUserAsync(session.userId.toString(), callback) }
|
||||
application.currentUser = user
|
||||
|
||||
callApiEmpty { callback ->
|
||||
api.ReportCapabilities(ClientCapabilities().apply {
|
||||
playableMediaTypes = arrayListOf(MediaType.Video, MediaType.Audio)
|
||||
supportsMediaControl = true
|
||||
supportedCommands = arrayListOf(
|
||||
GeneralCommandType.DisplayContent.toString(),
|
||||
GeneralCommandType.DisplayMessage.toString(),
|
||||
)
|
||||
}, callback)
|
||||
}
|
||||
|
||||
// Connect to WebSocket AFTER HTTP connection confirmed working
|
||||
// to catch exceptions not catchable with the legacy websocket client
|
||||
api.ensureWebSocket()
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
package org.jellyfin.androidtv.auth
|
||||
|
||||
import org.jellyfin.apiclient.interaction.device.IDevice
|
||||
import org.jellyfin.sdk.model.DeviceInfo
|
||||
import java.security.MessageDigest
|
||||
|
||||
/**
|
||||
* Wrapper for [IDevice] that changes the device id to a combination of the original device id
|
||||
* and the username. This is neccasary to prevent the server from trashing other sessions with
|
||||
* the same device id.
|
||||
*/
|
||||
class AuthenticationDevice(
|
||||
private val parent: IDevice,
|
||||
private val username: String
|
||||
) : IDevice {
|
||||
// Hash the deviceId because we can't use special characters as the device id is send
|
||||
// via HTTP headers
|
||||
override val deviceId = MessageDigest.getInstance("SHA-1").run {
|
||||
update("${parent.deviceId}+${username}".toByteArray())
|
||||
digest().joinToString("") { "%02x".format(it) }
|
||||
}
|
||||
|
||||
override val deviceName = parent.deviceName
|
||||
|
||||
fun toDeviceInfo() = DeviceInfo(
|
||||
id = deviceId,
|
||||
name = deviceName
|
||||
)
|
||||
}
|
||||
@@ -1,267 +0,0 @@
|
||||
package org.jellyfin.androidtv.auth
|
||||
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.emitAll
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import org.jellyfin.androidtv.auth.model.*
|
||||
import org.jellyfin.androidtv.preference.AuthenticationPreferences
|
||||
import org.jellyfin.androidtv.util.ImageUtils
|
||||
import org.jellyfin.apiclient.interaction.device.IDevice
|
||||
import org.jellyfin.sdk.Jellyfin
|
||||
import org.jellyfin.sdk.api.client.KtorClient
|
||||
import org.jellyfin.sdk.api.client.exception.ApiClientException
|
||||
import org.jellyfin.sdk.api.client.extensions.authenticateUserByName
|
||||
import org.jellyfin.sdk.api.operations.ImageApi
|
||||
import org.jellyfin.sdk.api.operations.UserApi
|
||||
import org.jellyfin.sdk.model.api.ImageType
|
||||
import timber.log.Timber
|
||||
import java.util.*
|
||||
|
||||
interface AuthenticationRepository {
|
||||
fun getServers(): List<Server>
|
||||
fun getUsers(server: UUID): List<PrivateUser>?
|
||||
fun getUser(server: UUID, user: UUID): PrivateUser?
|
||||
fun saveServer(id: UUID, name: String, address: String, version: String?, loginDisclaimer: String?)
|
||||
fun authenticateUser(user: User): Flow<LoginState>
|
||||
fun authenticateUser(user: User, server: Server): Flow<LoginState>
|
||||
fun login(server: Server, username: String, password: String = ""): Flow<LoginState>
|
||||
fun logout(user: User)
|
||||
fun removeUser(user: User)
|
||||
fun getUserImageUrl(server: Server, user: User): String?
|
||||
}
|
||||
|
||||
class AuthenticationRepositoryImpl(
|
||||
private val jellyfin: Jellyfin,
|
||||
private val sessionRepository: SessionRepository,
|
||||
private val device: IDevice,
|
||||
private val accountManagerHelper: AccountManagerHelper,
|
||||
private val authenticationStore: AuthenticationStore,
|
||||
private val userApiClient: KtorClient,
|
||||
private val authenticationPreferences: AuthenticationPreferences,
|
||||
) : AuthenticationRepository {
|
||||
private val serverComparator = compareByDescending<Server> { it.dateLastAccessed }.thenBy { it.name }
|
||||
private val userComparator = compareByDescending<PrivateUser> { it.lastUsed }.thenBy { it.name }
|
||||
|
||||
override fun getServers() = authenticationStore.getServers().map { (id, info) ->
|
||||
Server(
|
||||
id = id,
|
||||
name = info.name,
|
||||
address = info.address,
|
||||
version = info.version,
|
||||
loginDisclaimer = info.loginDisclaimer,
|
||||
dateLastAccessed = Date(info.lastUsed),
|
||||
)
|
||||
}.sortedWith(serverComparator)
|
||||
|
||||
private fun mapUser(
|
||||
serverId: UUID,
|
||||
userId: UUID,
|
||||
userInfo: AuthenticationStoreUser,
|
||||
authInfo: AccountManagerAccount?,
|
||||
) = PrivateUser(
|
||||
id = authInfo?.id ?: userId,
|
||||
serverId = authInfo?.server ?: serverId,
|
||||
name = userInfo.name,
|
||||
accessToken = authInfo?.accessToken,
|
||||
requirePassword = userInfo.requirePassword,
|
||||
imageTag = userInfo.imageTag,
|
||||
lastUsed = userInfo.lastUsed,
|
||||
)
|
||||
|
||||
override fun getUsers(server: UUID): List<PrivateUser>? =
|
||||
authenticationStore.getUsers(server)?.mapNotNull { (userId, userInfo) ->
|
||||
accountManagerHelper.getAccount(userId).let { authInfo ->
|
||||
mapUser(server, userId, userInfo, authInfo)
|
||||
}
|
||||
}?.sortedWith(userComparator)
|
||||
|
||||
override fun getUser(server: UUID, user: UUID): PrivateUser? {
|
||||
val userInfo = authenticationStore.getUser(server, user) ?: return null
|
||||
val authInfo = accountManagerHelper.getAccount(user) ?: return null
|
||||
|
||||
return mapUser(server, user, userInfo, authInfo)
|
||||
}
|
||||
|
||||
override fun saveServer(id: UUID, name: String, address: String, version: String?, loginDisclaimer: String?) {
|
||||
val current = authenticationStore.getServer(id)
|
||||
val server = if (current != null) current.copy(name = name, address = address, version = version, loginDisclaimer = loginDisclaimer, lastUsed = Date().time)
|
||||
else AuthenticationStoreServer(name, address, version, loginDisclaimer)
|
||||
|
||||
authenticationStore.putServer(id, server)
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the active session to the information in [user] and [server].
|
||||
* Connects to the server and requests the info of the currently authenticated user.
|
||||
*
|
||||
* @return Whether the user information can be retrieved.
|
||||
*/
|
||||
private suspend fun setActiveSession(user: User, server: Server): Boolean {
|
||||
val authenticated = sessionRepository.switchCurrentSession(user.id)
|
||||
|
||||
if (authenticated) {
|
||||
// Update last use in store
|
||||
authenticationStore.getServer(server.id)?.let { storedServer ->
|
||||
authenticationStore.putServer(server.id, storedServer.copy(lastUsed = Date().time))
|
||||
}
|
||||
|
||||
authenticationStore.getUser(server.id, user.id)?.let { storedUser ->
|
||||
authenticationStore.putUser(server.id, user.id, storedUser.copy(lastUsed = Date().time))
|
||||
}
|
||||
}
|
||||
|
||||
return authenticated
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
override fun authenticateUser(user: User): Flow<LoginState> = flow {
|
||||
Timber.d("Authenticating serverless user %s", user)
|
||||
emit(AuthenticatingState)
|
||||
|
||||
val server = authenticationStore.getServer(user.serverId)?.let {
|
||||
Server(
|
||||
id = user.serverId,
|
||||
name = it.name,
|
||||
address = it.address,
|
||||
version = it.version,
|
||||
loginDisclaimer = it.loginDisclaimer,
|
||||
dateLastAccessed = Date(it.lastUsed),
|
||||
)
|
||||
}
|
||||
|
||||
if (server == null) emit(RequireSignInState)
|
||||
else if (!server.versionSupported) emit(ServerVersionNotSupported(server))
|
||||
else emitAll(authenticateUser(user, server))
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
override fun authenticateUser(user: User, server: Server): Flow<LoginState> = flow {
|
||||
Timber.d("Authenticating user %s", user)
|
||||
emit(AuthenticatingState)
|
||||
|
||||
val account = accountManagerHelper.getAccount(user.id)
|
||||
when {
|
||||
!server.versionSupported -> emit(ServerVersionNotSupported(server))
|
||||
// Access token found, proceed with sign in
|
||||
!authenticationPreferences[AuthenticationPreferences.alwaysAuthenticate] && account?.accessToken != null -> when {
|
||||
// Update session
|
||||
setActiveSession(user, server) -> {
|
||||
// Update stored user
|
||||
try {
|
||||
val userInfo by UserApi(userApiClient).getCurrentUser()
|
||||
val currentUser = authenticationStore.getUser(server.id, user.id)
|
||||
|
||||
val updatedUser = currentUser?.copy(
|
||||
name = userInfo.name!!,
|
||||
requirePassword = userInfo.hasPassword,
|
||||
imageTag = userInfo.primaryImageTag
|
||||
) ?: AuthenticationStoreUser(
|
||||
name = userInfo.name!!,
|
||||
requirePassword = userInfo.hasPassword,
|
||||
imageTag = userInfo.primaryImageTag
|
||||
)
|
||||
authenticationStore.putUser(server.id, user.id, updatedUser)
|
||||
accountManagerHelper.putAccount(AccountManagerAccount(user.id, server.id, updatedUser.name, account.accessToken))
|
||||
|
||||
emit(AuthenticatedState)
|
||||
} catch(err: ApiClientException) {
|
||||
Timber.e(err, "Unable to get current user data")
|
||||
emit(RequireSignInState)
|
||||
}
|
||||
}
|
||||
// Login failed
|
||||
else -> when {
|
||||
// No password required - try login
|
||||
!user.requirePassword -> emitAll(login(server, user.name))
|
||||
// Require new sign in
|
||||
else -> emit(RequireSignInState)
|
||||
}
|
||||
}
|
||||
// User is known to not require a password, try a sign in
|
||||
!user.requirePassword -> emitAll(login(server, user.name))
|
||||
// Account found without access token, require sign in
|
||||
else -> emit(RequireSignInState)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
override fun login(server: Server, username: String, password: String) = flow {
|
||||
if (!server.versionSupported) {
|
||||
emit(ServerVersionNotSupported(server))
|
||||
return@flow
|
||||
}
|
||||
|
||||
val api = jellyfin.createApi(server.address, deviceInfo = AuthenticationDevice(device, username).toDeviceInfo())
|
||||
val userApi = UserApi(api)
|
||||
val result = try {
|
||||
val response = userApi.authenticateUserByName(username, password)
|
||||
response.content
|
||||
} catch (err: ApiClientException) {
|
||||
Timber.e(err, "Unable to sign in as $username")
|
||||
emit(RequireSignInState)
|
||||
return@flow
|
||||
}
|
||||
|
||||
val userInfo = result.user ?: return@flow emit(RequireSignInState)
|
||||
|
||||
val userId = userInfo.id
|
||||
val currentUser = authenticationStore.getUser(server.id, userId)
|
||||
val updatedUser = currentUser?.copy(
|
||||
name = userInfo.name!!,
|
||||
lastUsed = Date().time,
|
||||
requirePassword = userInfo.hasPassword,
|
||||
imageTag = userInfo.primaryImageTag
|
||||
) ?: AuthenticationStoreUser(
|
||||
name = userInfo.name!!,
|
||||
requirePassword = userInfo.hasPassword,
|
||||
imageTag = userInfo.primaryImageTag
|
||||
)
|
||||
|
||||
authenticationStore.putUser(server.id, userId, updatedUser)
|
||||
val accountManagerAccount = AccountManagerAccount(userId, server.id, updatedUser.name, result.accessToken)
|
||||
accountManagerHelper.putAccount(accountManagerAccount)
|
||||
|
||||
val user = PrivateUser(
|
||||
id = userId,
|
||||
serverId = server.id,
|
||||
name = updatedUser.name,
|
||||
accessToken = result.accessToken,
|
||||
requirePassword = userInfo.hasPassword,
|
||||
imageTag = userInfo.primaryImageTag,
|
||||
lastUsed = Date().time,
|
||||
)
|
||||
|
||||
// We just added the account so it should activate properly although in rare cases it doesn't.
|
||||
// this is often caused by issues in the platforms account manager
|
||||
if (setActiveSession(user, server)) emit(AuthenticatedState)
|
||||
else {
|
||||
// Try to remove the account and ask for sign in
|
||||
accountManagerHelper.removeAccount(accountManagerAccount)
|
||||
emit(RequireSignInState)
|
||||
}
|
||||
}
|
||||
|
||||
override fun logout(user: User) {
|
||||
accountManagerHelper.getAccount(user.id)?.let(accountManagerHelper::removeAccount)
|
||||
}
|
||||
|
||||
override fun removeUser(user: User) {
|
||||
authenticationStore.removeUser(user.serverId, user.id)
|
||||
logout(user)
|
||||
}
|
||||
|
||||
override fun getUserImageUrl(server: Server, user: User): String? {
|
||||
val api = jellyfin.createApi(baseUrl = server.address)
|
||||
val imageApi = ImageApi(api)
|
||||
|
||||
return user.imageTag?.let { imageTag ->
|
||||
imageApi.getUserImageUrl(
|
||||
userId = user.id,
|
||||
tag = imageTag,
|
||||
imageType = ImageType.PRIMARY,
|
||||
maxHeight = ImageUtils.MAX_PRIMARY_IMAGE_HEIGHT
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
package org.jellyfin.androidtv.auth
|
||||
|
||||
import org.jellyfin.androidtv.R
|
||||
import org.jellyfin.androidtv.ui.preference.dsl.EnumDisplayOptions
|
||||
|
||||
enum class AuthenticationSortBy {
|
||||
@EnumDisplayOptions(name = R.string.last_use)
|
||||
LAST_USE,
|
||||
@EnumDisplayOptions(name = R.string.alphabetical)
|
||||
ALPHABETICAL
|
||||
}
|
||||
@@ -1,109 +0,0 @@
|
||||
package org.jellyfin.androidtv.auth
|
||||
|
||||
import android.content.Context
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.*
|
||||
import kotlinx.serialization.modules.SerializersModule
|
||||
import kotlinx.serialization.modules.contextual
|
||||
import org.jellyfin.androidtv.auth.model.AuthenticationStoreServer
|
||||
import org.jellyfin.androidtv.auth.model.AuthenticationStoreUser
|
||||
import org.jellyfin.androidtv.util.serializer.UUIDSerializer
|
||||
import timber.log.Timber
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* Storage for authentication related entities. Stores servers with users inside. Should be used in
|
||||
* combination with the [AccountManagerHelper] to store access tokens and integrate with the
|
||||
* operating system.
|
||||
*
|
||||
* The data is stored in a JSON file located in the applications data directory.
|
||||
*/
|
||||
class AuthenticationStore(
|
||||
private val context: Context
|
||||
) {
|
||||
private val storePath
|
||||
get() = context.filesDir.resolve("authentication_store.json")
|
||||
|
||||
private val json = Json {
|
||||
encodeDefaults = true
|
||||
serializersModule = SerializersModule {
|
||||
contextual(UUIDSerializer)
|
||||
}
|
||||
ignoreUnknownKeys = true
|
||||
}
|
||||
|
||||
private val store by lazy {
|
||||
load().toMutableMap()
|
||||
}
|
||||
|
||||
private fun load(): Map<UUID, AuthenticationStoreServer> {
|
||||
// No store found
|
||||
if (!storePath.exists()) return emptyMap()
|
||||
|
||||
// Check for version
|
||||
val root = json.parseToJsonElement(storePath.readText()).jsonObject
|
||||
return when (root["version"]?.jsonPrimitive?.intOrNull) {
|
||||
1 -> json.decodeFromJsonElement<Map<UUID, AuthenticationStoreServer>>(root["servers"]!!)
|
||||
null -> {
|
||||
Timber.e("Authentication Store is corrupt!")
|
||||
emptyMap()
|
||||
}
|
||||
else -> {
|
||||
Timber.e("Authentication Store is using an unknown version!")
|
||||
emptyMap()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun save(): Boolean {
|
||||
val root = JsonObject(mapOf(
|
||||
"version" to JsonPrimitive(1),
|
||||
"servers" to json.encodeToJsonElement(store)
|
||||
))
|
||||
|
||||
storePath.writeText(json.encodeToString(root))
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
fun getServers(): Map<UUID, AuthenticationStoreServer> = store
|
||||
|
||||
fun getUsers(server: UUID): Map<UUID, AuthenticationStoreUser>? = getServer(server)?.users
|
||||
|
||||
fun getServer(serverId: UUID) = store[serverId]
|
||||
|
||||
fun getUser(serverId: UUID, userId: UUID) = getUsers(serverId)?.get(userId)
|
||||
|
||||
fun putServer(id: UUID, server: AuthenticationStoreServer): Boolean {
|
||||
store[id] = server
|
||||
return save()
|
||||
}
|
||||
|
||||
fun putUser(server: UUID, userId: UUID, userInfo: AuthenticationStoreUser): Boolean {
|
||||
val serverInfo = store[server] ?: return false
|
||||
|
||||
store[server] = serverInfo.copy(users = serverInfo.users.toMutableMap().apply { put(userId, userInfo) })
|
||||
|
||||
return save()
|
||||
}
|
||||
|
||||
fun containsServer(server: UUID): Boolean = server in store
|
||||
|
||||
fun containsUser(server: UUID, user: UUID): Boolean = server in store && user in store[server]!!.users
|
||||
|
||||
/**
|
||||
* Removes the server and stored users from the credential store.
|
||||
*/
|
||||
fun removeServer(server: UUID): Boolean {
|
||||
store.remove(server)
|
||||
return save()
|
||||
}
|
||||
|
||||
fun removeUser(server: UUID, user: UUID): Boolean {
|
||||
val serverInfo = store[server] ?: return false
|
||||
|
||||
store[server] = serverInfo.copy(users = serverInfo.users.toMutableMap().apply { remove(user) })
|
||||
|
||||
return save()
|
||||
}
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
package org.jellyfin.androidtv.auth
|
||||
|
||||
import android.accounts.AbstractAccountAuthenticator
|
||||
import android.accounts.Account
|
||||
import android.accounts.AccountAuthenticatorResponse
|
||||
import android.accounts.AccountManager
|
||||
import android.app.Service
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.os.IBinder
|
||||
import androidx.core.os.bundleOf
|
||||
import org.jellyfin.androidtv.ui.preference.PreferencesActivity
|
||||
|
||||
class AuthenticatorService : Service() {
|
||||
private val authenticator by lazy {
|
||||
Authenticator(this)
|
||||
}
|
||||
|
||||
override fun onBind(intent: Intent): IBinder? = authenticator.iBinder
|
||||
|
||||
private inner class Authenticator(
|
||||
private val service: AuthenticatorService
|
||||
) : AbstractAccountAuthenticator(service) {
|
||||
private val unsupportedOperationBundle = bundleOf(
|
||||
AccountManager.KEY_ERROR_CODE to 1,
|
||||
AccountManager.KEY_ERROR_MESSAGE to "Unsupported operation"
|
||||
)
|
||||
|
||||
override fun addAccount(response: AccountAuthenticatorResponse, accountType: String, authTokenType: String?, requiredFeatures: Array<String>?, options: Bundle): Bundle = unsupportedOperationBundle
|
||||
|
||||
override fun confirmCredentials(response: AccountAuthenticatorResponse, account: Account, options: Bundle?): Bundle = unsupportedOperationBundle
|
||||
|
||||
override fun editProperties(response: AccountAuthenticatorResponse, accountType: String): Bundle = bundleOf(
|
||||
AccountManager.KEY_INTENT to Intent(service, PreferencesActivity::class.java)
|
||||
)
|
||||
|
||||
override fun getAccountRemovalAllowed(response: AccountAuthenticatorResponse, account: Account): Bundle = bundleOf(
|
||||
AccountManager.KEY_BOOLEAN_RESULT to true
|
||||
)
|
||||
|
||||
override fun getAuthToken(response: AccountAuthenticatorResponse, account: Account, authTokenType: String, options: Bundle): Bundle = unsupportedOperationBundle
|
||||
|
||||
override fun getAuthTokenLabel(authTokenType: String): String? = null
|
||||
|
||||
override fun hasFeatures(response: AccountAuthenticatorResponse, account: Account, features: Array<String>): Bundle = unsupportedOperationBundle
|
||||
|
||||
override fun updateCredentials(response: AccountAuthenticatorResponse, account: Account, authTokenType: String?, options: Bundle?): Bundle = unsupportedOperationBundle
|
||||
}
|
||||
}
|
||||
@@ -1,97 +0,0 @@
|
||||
package org.jellyfin.androidtv.auth
|
||||
|
||||
import android.content.Context
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.booleanOrNull
|
||||
import kotlinx.serialization.json.jsonObject
|
||||
import kotlinx.serialization.json.jsonPrimitive
|
||||
import kotlinx.serialization.modules.SerializersModule
|
||||
import kotlinx.serialization.modules.contextual
|
||||
import org.jellyfin.androidtv.auth.model.AccountManagerAccount
|
||||
import org.jellyfin.androidtv.auth.model.AuthenticationStoreServer
|
||||
import org.jellyfin.androidtv.auth.model.AuthenticationStoreUser
|
||||
import org.jellyfin.androidtv.preference.SystemPreferences
|
||||
import org.jellyfin.androidtv.util.serializer.UUIDSerializer
|
||||
import org.jellyfin.sdk.model.serializer.toUUIDOrNull
|
||||
import timber.log.Timber
|
||||
|
||||
class LegacyAccountMigration(
|
||||
private val context: Context,
|
||||
private val authenticationStore: AuthenticationStore,
|
||||
private val accountManagerHelper: AccountManagerHelper,
|
||||
private val systemPreferences: SystemPreferences,
|
||||
) {
|
||||
companion object {
|
||||
const val LEGACY_CREDENTIAL_FILE = "org.jellyfin.androidtv.login.json"
|
||||
}
|
||||
|
||||
suspend fun migrate() {
|
||||
val path = context.filesDir.resolve(LEGACY_CREDENTIAL_FILE)
|
||||
|
||||
if (path.exists() && !systemPreferences[SystemPreferences.legacyCredentialsMigrated]) {
|
||||
Timber.d("Starting migration of legacy credentials from $path")
|
||||
val json = Json {
|
||||
serializersModule = SerializersModule {
|
||||
contextual(UUIDSerializer)
|
||||
}
|
||||
ignoreUnknownKeys = true
|
||||
}
|
||||
val root = json.parseToJsonElement(path.readText()).jsonObject
|
||||
val server = root["serverInfo"]?.jsonObject
|
||||
val user = root["userDto"]?.jsonObject
|
||||
|
||||
val serverId = server?.get("Id")?.jsonPrimitive?.content?.toUUIDOrNull()
|
||||
if (serverId != null) {
|
||||
val serverName = server["Name"]?.jsonPrimitive?.content
|
||||
val serverAddress = server["Address"]?.jsonPrimitive?.content
|
||||
val serverVersion = server["Version"]?.jsonPrimitive?.content
|
||||
|
||||
if (!authenticationStore.containsServer(serverId)) {
|
||||
Timber.i("Migrating server $serverId")
|
||||
authenticationStore.putServer(
|
||||
id = serverId,
|
||||
server = AuthenticationStoreServer(
|
||||
name = serverName ?: "",
|
||||
address = serverAddress ?: "",
|
||||
loginDisclaimer = null,
|
||||
version = serverVersion,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
val userId = user?.get("Id")?.jsonPrimitive?.content?.toUUIDOrNull()
|
||||
if (userId != null && !authenticationStore.containsUser(serverId, userId)) {
|
||||
Timber.i("Migrating user $userId")
|
||||
val name = user["Name"]?.jsonPrimitive?.content ?: userId.toString()
|
||||
val requirePassword = user["HasPassword"]?.jsonPrimitive?.booleanOrNull ?: true
|
||||
val imageTag = user["PrimaryImageTag"]?.jsonPrimitive?.content
|
||||
val accessToken = user["AccessToken"]?.jsonPrimitive?.content ?: server["AccessToken"]?.jsonPrimitive?.content
|
||||
|
||||
authenticationStore.putUser(
|
||||
server = serverId,
|
||||
userId = userId,
|
||||
userInfo = AuthenticationStoreUser(
|
||||
name = name,
|
||||
requirePassword = requirePassword,
|
||||
imageTag = imageTag
|
||||
)
|
||||
)
|
||||
accountManagerHelper.putAccount(AccountManagerAccount(
|
||||
id = userId,
|
||||
server = serverId,
|
||||
name = name,
|
||||
accessToken = accessToken?.let {
|
||||
// Convert empty string to null
|
||||
if (it.isBlank()) null
|
||||
else it
|
||||
}
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
systemPreferences[SystemPreferences.legacyCredentialsMigrated] = true
|
||||
} else {
|
||||
Timber.d("Skipping migration of legacy credentials from $path (file does not exist)")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,186 +0,0 @@
|
||||
package org.jellyfin.androidtv.auth
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.liveData
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.*
|
||||
import org.jellyfin.androidtv.auth.model.*
|
||||
import org.jellyfin.androidtv.util.sdk.toPublicUser
|
||||
import org.jellyfin.androidtv.util.sdk.toServer
|
||||
import org.jellyfin.sdk.Jellyfin
|
||||
import org.jellyfin.sdk.api.client.exception.ApiClientException
|
||||
import org.jellyfin.sdk.api.operations.BrandingApi
|
||||
import org.jellyfin.sdk.api.operations.SystemApi
|
||||
import org.jellyfin.sdk.api.operations.UserApi
|
||||
import org.jellyfin.sdk.discovery.RecommendedServerInfo
|
||||
import org.jellyfin.sdk.discovery.RecommendedServerInfoScore
|
||||
import org.jellyfin.sdk.model.api.ServerDiscoveryInfo
|
||||
import org.jellyfin.sdk.model.api.UserDto
|
||||
import org.jellyfin.sdk.model.serializer.toUUID
|
||||
import timber.log.Timber
|
||||
import java.util.*
|
||||
|
||||
interface ServerRepository {
|
||||
fun getStoredServers(): List<Server>
|
||||
fun getDiscoveryServers(): Flow<Server>
|
||||
suspend fun migrateLegacyCredentials()
|
||||
|
||||
fun getServerUsers(server: Server): LiveData<List<User>>
|
||||
fun removeServer(serverId: UUID): Boolean
|
||||
fun addServer(address: String): Flow<ServerAdditionState>
|
||||
suspend fun refreshServerInfo(server: Server): Boolean
|
||||
|
||||
companion object {
|
||||
val minimumServerVersion = Jellyfin.minimumVersion.copy(build = null)
|
||||
}
|
||||
}
|
||||
|
||||
class ServerRepositoryImpl(
|
||||
private val jellyfin: Jellyfin,
|
||||
private val authenticationRepository: AuthenticationRepository,
|
||||
private val authenticationStore: AuthenticationStore,
|
||||
private val legacyAccountMigration: LegacyAccountMigration,
|
||||
) : ServerRepository {
|
||||
override fun getStoredServers() = authenticationRepository.getServers()
|
||||
|
||||
override fun getDiscoveryServers() = flow {
|
||||
val servers = jellyfin.discovery.discoverLocalServers().mapNotNull(ServerDiscoveryInfo::toServer)
|
||||
emitAll(servers)
|
||||
}.flowOn(Dispatchers.IO)
|
||||
|
||||
private suspend fun getServerPublicUsers(server: Server): List<PublicUser> {
|
||||
val client = jellyfin.createApi(server.address)
|
||||
val userApi = UserApi(client)
|
||||
|
||||
return try {
|
||||
val users by userApi.getPublicUsers()
|
||||
users.mapNotNull(UserDto::toPublicUser)
|
||||
} catch (err: ApiClientException) {
|
||||
Timber.e(err, "Unable to retrieve public users")
|
||||
|
||||
emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
public override suspend fun refreshServerInfo(server: Server): Boolean {
|
||||
// Only update existing servers
|
||||
val serverInfo = authenticationStore.getServer(server.id) ?: return false
|
||||
val now = Date().time
|
||||
|
||||
// Only update every 10 minutes
|
||||
if (now - serverInfo.lastRefreshed < 600000 && serverInfo.version != null) return false
|
||||
|
||||
return try {
|
||||
val client = jellyfin.createApi(server.address)
|
||||
// Get login disclaimer
|
||||
val branding by BrandingApi(client).getBrandingOptions()
|
||||
val systemInfo by SystemApi(client).getPublicSystemInfo()
|
||||
|
||||
authenticationStore.putServer(server.id, serverInfo.copy(
|
||||
name = systemInfo.serverName ?: serverInfo.name,
|
||||
version = systemInfo.version ?: serverInfo.version,
|
||||
loginDisclaimer = branding.loginDisclaimer ?: serverInfo.loginDisclaimer,
|
||||
lastRefreshed = now
|
||||
))
|
||||
} catch (err: ApiClientException) {
|
||||
Timber.e(err, "Unable to update server")
|
||||
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
private fun getServerStoredUsers(server: Server): List<PrivateUser> = authenticationRepository
|
||||
.getUsers(server.id)
|
||||
.orEmpty()
|
||||
|
||||
override fun getServerUsers(server: Server): LiveData<List<User>> = liveData {
|
||||
val users = mutableListOf<User>()
|
||||
|
||||
users.addAll(getServerStoredUsers(server))
|
||||
emit(users.toList())
|
||||
|
||||
getServerPublicUsers(server)
|
||||
.sortedBy { it.name }
|
||||
.forEach { user ->
|
||||
val index = users.indexOfFirst { it.id == user.id }
|
||||
if (index == -1) users.add(user)
|
||||
else {
|
||||
users[index] = when(val currentUser = users[index]) {
|
||||
is PublicUser -> user
|
||||
is PrivateUser -> currentUser.copy(
|
||||
name = user.name,
|
||||
requirePassword = user.requirePassword,
|
||||
imageTag = user.imageTag
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
emit(users.toList())
|
||||
}
|
||||
|
||||
override suspend fun migrateLegacyCredentials() = legacyAccountMigration.migrate()
|
||||
|
||||
override fun removeServer(serverId: UUID) = authenticationStore.removeServer(serverId)
|
||||
|
||||
override fun addServer(address: String): Flow<ServerAdditionState> = flow {
|
||||
Timber.d("Adding server %s", address)
|
||||
|
||||
emit(ConnectingState(address))
|
||||
|
||||
val addressCandidates = jellyfin.discovery.getAddressCandidates(address)
|
||||
Timber.d("Found ${addressCandidates.size} candidates")
|
||||
|
||||
val goodRecommendations = mutableListOf<RecommendedServerInfo>()
|
||||
val badRecommendations = mutableListOf<RecommendedServerInfo>()
|
||||
val greatRecommendaton = jellyfin.discovery.getRecommendedServers(addressCandidates)
|
||||
.firstOrNull { recommendedServer ->
|
||||
when (recommendedServer.score) {
|
||||
RecommendedServerInfoScore.GREAT -> true
|
||||
RecommendedServerInfoScore.GOOD -> {
|
||||
goodRecommendations += recommendedServer
|
||||
false
|
||||
}
|
||||
else -> {
|
||||
badRecommendations += recommendedServer
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Timber.d(buildString {
|
||||
append("Recommendations: ")
|
||||
if (greatRecommendaton == null) append(0)
|
||||
else append(1)
|
||||
append(" great, ")
|
||||
append(goodRecommendations.size)
|
||||
append(" good, ")
|
||||
append(badRecommendations.size)
|
||||
append(" bad")
|
||||
})
|
||||
|
||||
val chosenRecommendation = greatRecommendaton ?: goodRecommendations.firstOrNull()
|
||||
if (chosenRecommendation != null) {
|
||||
// Get system info
|
||||
val systemInfo = chosenRecommendation.systemInfo!!
|
||||
|
||||
// Get branding info
|
||||
val api = BrandingApi(jellyfin.createApi(chosenRecommendation.address))
|
||||
val branding by api.getBrandingOptions()
|
||||
|
||||
val id = systemInfo.id!!.toUUID()
|
||||
authenticationRepository.saveServer(
|
||||
id = id,
|
||||
name = systemInfo.serverName ?: "Jellyfin Server",
|
||||
address = chosenRecommendation.address,
|
||||
version = systemInfo.version,
|
||||
loginDisclaimer = branding.loginDisclaimer,
|
||||
)
|
||||
|
||||
emit(ConnectedState(id, systemInfo))
|
||||
} else {
|
||||
// No great or good recommendations, only add bad recommendations
|
||||
emit(UnableToConnectState(addressCandidates))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,167 +0,0 @@
|
||||
package org.jellyfin.androidtv.auth
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import org.jellyfin.androidtv.preference.AuthenticationPreferences
|
||||
import org.jellyfin.androidtv.preference.constant.UserSelectBehavior.*
|
||||
import org.jellyfin.sdk.api.client.KtorClient
|
||||
import org.jellyfin.sdk.model.serializer.toUUIDOrNull
|
||||
import timber.log.Timber
|
||||
import java.util.*
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.suspendCoroutine
|
||||
|
||||
data class Session(
|
||||
val userId: UUID,
|
||||
val serverId: UUID,
|
||||
val accessToken: String,
|
||||
)
|
||||
|
||||
interface SessionRepository {
|
||||
val currentSession: LiveData<Session?>
|
||||
val currentSystemSession: LiveData<Session?>
|
||||
|
||||
fun restoreDefaultSession()
|
||||
fun restoreDefaultSystemSession()
|
||||
|
||||
suspend fun switchCurrentSession(userId: UUID): Boolean
|
||||
fun destroyCurrentSession()
|
||||
}
|
||||
|
||||
class SessionRepositoryImpl(
|
||||
private val authenticationPreferences: AuthenticationPreferences,
|
||||
private val accountManagerHelper: AccountManagerHelper,
|
||||
private val apiBinder: ApiBinder,
|
||||
private val authenticationStore: AuthenticationStore,
|
||||
private val userApiClient: KtorClient,
|
||||
private val systemApiClient: KtorClient,
|
||||
) : SessionRepository {
|
||||
private val _currentSession = MutableLiveData<Session?>()
|
||||
override val currentSession: LiveData<Session?> get() = _currentSession
|
||||
|
||||
private val _currentSystemSession = MutableLiveData<Session?>()
|
||||
override val currentSystemSession: LiveData<Session?> get() = _currentSystemSession
|
||||
|
||||
override fun restoreDefaultSession() {
|
||||
Timber.d("Restoring default session")
|
||||
|
||||
if (authenticationPreferences[AuthenticationPreferences.alwaysAuthenticate]) return destroyCurrentSession()
|
||||
|
||||
val behavior = authenticationPreferences[AuthenticationPreferences.autoLoginUserBehavior]
|
||||
val userId = authenticationPreferences[AuthenticationPreferences.autoLoginUserId].toUUIDOrNull()
|
||||
|
||||
when (behavior) {
|
||||
DISABLED -> destroyCurrentSession()
|
||||
LAST_USER -> setCurrentSession(createLastUserSession(), false)
|
||||
SPECIFIC_USER -> setCurrentSession(createUserSession(userId), false)
|
||||
}
|
||||
}
|
||||
|
||||
override fun restoreDefaultSystemSession() {
|
||||
Timber.d("Restoring default system session")
|
||||
|
||||
if (authenticationPreferences[AuthenticationPreferences.alwaysAuthenticate]) return setCurrentSession(null, false)
|
||||
|
||||
val behavior = authenticationPreferences[AuthenticationPreferences.systemUserBehavior]
|
||||
val userId = authenticationPreferences[AuthenticationPreferences.systemUserId].toUUIDOrNull()
|
||||
|
||||
when (behavior) {
|
||||
DISABLED -> setCurrentSystemSession(null)
|
||||
LAST_USER -> setCurrentSystemSession(createLastUserSession())
|
||||
SPECIFIC_USER -> setCurrentSystemSession(createUserSession(userId))
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun switchCurrentSession(userId: UUID): Boolean {
|
||||
// No change in user - don't switch
|
||||
if (currentSession.value?.userId == userId) return false
|
||||
|
||||
Timber.d("Switching current session to user $userId")
|
||||
|
||||
val session = createUserSession(userId)
|
||||
if (session == null) {
|
||||
Timber.d("Could not switch to non-existing session for user $userId")
|
||||
return false
|
||||
}
|
||||
|
||||
return suspendCoroutine { continuation ->
|
||||
setCurrentSession(session, true) { success ->
|
||||
continuation.resume(success)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun destroyCurrentSession() {
|
||||
Timber.d("Destroying current session")
|
||||
|
||||
_currentSession.value = null
|
||||
userApiClient.applySession(null)
|
||||
apiBinder.updateSession(null) { }
|
||||
}
|
||||
|
||||
private fun setCurrentSession(session: Session?, includeSystemUser: Boolean, callback: ((Boolean) -> Unit)? = null) {
|
||||
// No change in session - don't switch
|
||||
if (session != null && currentSession.value?.userId == session.userId) return
|
||||
|
||||
if (session != null) authenticationPreferences[AuthenticationPreferences.lastUserId] = session.userId.toString()
|
||||
|
||||
val systemUserBehavior = authenticationPreferences[AuthenticationPreferences.systemUserBehavior]
|
||||
if (includeSystemUser && systemUserBehavior == LAST_USER) setCurrentSystemSession(session)
|
||||
|
||||
// Update session after binding the apiclient settings
|
||||
apiBinder.updateSession(session) { success ->
|
||||
Timber.d("Updating current session. userId=${session?.userId} apiBindingSuccess=${success}")
|
||||
|
||||
if (success) {
|
||||
userApiClient.applySession(session)
|
||||
_currentSession.postValue(session)
|
||||
} else {
|
||||
userApiClient.applySession(null)
|
||||
_currentSession.postValue(null)
|
||||
}
|
||||
|
||||
callback?.invoke(success)
|
||||
}
|
||||
}
|
||||
|
||||
private fun setCurrentSystemSession(session: Session?) {
|
||||
// No change in session - don't switch
|
||||
if (session != null && currentSession.value?.userId == session.userId) return
|
||||
|
||||
_currentSystemSession.postValue(session)
|
||||
|
||||
systemApiClient.applySession(session)
|
||||
}
|
||||
|
||||
private fun createLastUserSession(): Session? {
|
||||
val lastUserId = authenticationPreferences[AuthenticationPreferences.lastUserId].toUUIDOrNull()
|
||||
return createUserSession(lastUserId)
|
||||
}
|
||||
|
||||
private fun createUserSession(userId: UUID?): Session? {
|
||||
if (userId == null) return null
|
||||
|
||||
val account = accountManagerHelper.getAccount(userId)
|
||||
if (account?.accessToken == null) return null
|
||||
|
||||
return Session(
|
||||
userId = account.id,
|
||||
serverId = account.server,
|
||||
accessToken = account.accessToken
|
||||
)
|
||||
}
|
||||
|
||||
private fun KtorClient.applySession(session: Session?) {
|
||||
if (session == null) {
|
||||
baseUrl = null
|
||||
accessToken = null
|
||||
userId = null
|
||||
} else {
|
||||
val server = authenticationStore.getServer(session.serverId)
|
||||
?: return applySession(null)
|
||||
baseUrl = server.address
|
||||
accessToken = session.accessToken
|
||||
userId = session.userId
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
package org.jellyfin.androidtv.auth.model
|
||||
|
||||
import java.util.*
|
||||
|
||||
data class AccountManagerAccount(
|
||||
val id: UUID,
|
||||
val server: UUID,
|
||||
val name: String,
|
||||
val accessToken: String? = null,
|
||||
)
|
||||
@@ -1,23 +0,0 @@
|
||||
@file:UseSerializers(UUIDSerializer::class)
|
||||
|
||||
package org.jellyfin.androidtv.auth.model
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.UseSerializers
|
||||
import org.jellyfin.androidtv.util.serializer.UUIDSerializer
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* Locally stored server information. New properties require default values or deserialization will fail.
|
||||
*/
|
||||
@Serializable
|
||||
data class AuthenticationStoreServer(
|
||||
val name: String,
|
||||
val address: String,
|
||||
val version: String? = null,
|
||||
@SerialName("login_disclaimer") val loginDisclaimer: String? = null,
|
||||
@SerialName("last_used") val lastUsed: Long = Date().time,
|
||||
@SerialName("last_refreshed") val lastRefreshed: Long = Date().time,
|
||||
val users: Map<UUID, AuthenticationStoreUser> = emptyMap(),
|
||||
)
|
||||
@@ -1,16 +0,0 @@
|
||||
package org.jellyfin.androidtv.auth.model
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* Locally stored user information. New properties require default values or deserialization will fail.
|
||||
*/
|
||||
@Serializable
|
||||
data class AuthenticationStoreUser(
|
||||
val name: String,
|
||||
@SerialName("last_used") val lastUsed: Long = Date().time,
|
||||
@SerialName("require_password") val requirePassword: Boolean = true,
|
||||
@SerialName("image_tag") val imageTag: String? = null
|
||||
)
|
||||
@@ -1,23 +0,0 @@
|
||||
package org.jellyfin.androidtv.auth.model
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
|
||||
data class LoadingState(
|
||||
val status: Status,
|
||||
@StringRes val messageRes: Int? = null
|
||||
) {
|
||||
companion object {
|
||||
val PENDING = LoadingState(Status.PENDING)
|
||||
val LOADING = LoadingState(Status.LOADING)
|
||||
val SUCCESS = LoadingState(Status.SUCCESS)
|
||||
val ERROR = LoadingState(Status.ERROR)
|
||||
fun error(@StringRes messageRes: Int? = null) = LoadingState(Status.ERROR, messageRes)
|
||||
}
|
||||
|
||||
enum class Status {
|
||||
PENDING,
|
||||
LOADING,
|
||||
SUCCESS,
|
||||
ERROR
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
package org.jellyfin.androidtv.auth.model
|
||||
|
||||
sealed class LoginState
|
||||
object AuthenticatingState : LoginState()
|
||||
object RequireSignInState : LoginState()
|
||||
object ServerUnavailableState : LoginState()
|
||||
data class ServerVersionNotSupported(val server: Server) : LoginState()
|
||||
object AuthenticatedState : LoginState()
|
||||
@@ -1,30 +0,0 @@
|
||||
package org.jellyfin.androidtv.auth.model
|
||||
|
||||
import org.jellyfin.androidtv.auth.ServerRepository
|
||||
import org.jellyfin.sdk.model.ServerVersion
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* Server model to use locally in place of ServerInfo model in ApiClient.
|
||||
*/
|
||||
data class Server(
|
||||
var id: UUID,
|
||||
var name: String,
|
||||
var address: String,
|
||||
val version: String? = null,
|
||||
val loginDisclaimer: String? = null,
|
||||
var dateLastAccessed: Date = Date(0),
|
||||
) {
|
||||
private val serverVersion = version?.let(ServerVersion::fromString)
|
||||
val versionSupported = serverVersion != null && serverVersion >= ServerRepository.minimumServerVersion
|
||||
|
||||
override fun equals(other: Any?) = other is Server
|
||||
&& id == other.id
|
||||
&& address == other.address
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = id.hashCode()
|
||||
result = 31 * result + address.hashCode()
|
||||
return result
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
package org.jellyfin.androidtv.auth.model
|
||||
|
||||
import org.jellyfin.sdk.model.api.PublicSystemInfo
|
||||
import java.util.*
|
||||
|
||||
sealed class ServerAdditionState
|
||||
data class ConnectingState(val address: String) : ServerAdditionState()
|
||||
data class UnableToConnectState(val addressCandidates: List<String>) : ServerAdditionState()
|
||||
data class ConnectedState(val id: UUID, val publicInfo: PublicSystemInfo) : ServerAdditionState()
|
||||
@@ -1,50 +0,0 @@
|
||||
package org.jellyfin.androidtv.auth.model
|
||||
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* User model used locally.
|
||||
*/
|
||||
sealed class User {
|
||||
abstract val id: UUID
|
||||
abstract val serverId: UUID
|
||||
abstract val name: String
|
||||
abstract val accessToken: String?
|
||||
abstract val requirePassword: Boolean
|
||||
abstract val imageTag: String?
|
||||
|
||||
override fun equals(other: Any?) = other is User
|
||||
&& serverId == other.serverId
|
||||
&& id == other.id
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = id.hashCode()
|
||||
result = 31 * result + serverId.hashCode()
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a user stored client side.
|
||||
*/
|
||||
data class PrivateUser(
|
||||
override val id: UUID,
|
||||
override val serverId: UUID,
|
||||
override val name: String,
|
||||
override val accessToken: String?,
|
||||
override val requirePassword: Boolean,
|
||||
override val imageTag: String?,
|
||||
val lastUsed: Long,
|
||||
) : User()
|
||||
|
||||
/**
|
||||
* Represents a user stored server side. Found using the Public User endpoint.
|
||||
*/
|
||||
data class PublicUser(
|
||||
override val id: UUID,
|
||||
override val serverId: UUID,
|
||||
override val name: String,
|
||||
override val accessToken: String?,
|
||||
override val requirePassword: Boolean,
|
||||
override val imageTag: String?,
|
||||
) : User()
|
||||
192
app/src/main/java/org/jellyfin/androidtv/base/BaseActivity.java
Normal file
192
app/src/main/java/org/jellyfin/androidtv/base/BaseActivity.java
Normal file
@@ -0,0 +1,192 @@
|
||||
package org.jellyfin.androidtv.base;
|
||||
|
||||
import android.graphics.Rect;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.view.Gravity;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.jellyfin.androidtv.R;
|
||||
import org.jellyfin.androidtv.TvApp;
|
||||
import org.jellyfin.androidtv.util.Utils;
|
||||
|
||||
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();
|
||||
private Runnable loop;
|
||||
private IKeyListener keyListener;
|
||||
private IMessageListener messageListener;
|
||||
|
||||
private View messageUi;
|
||||
private TextView messageTitle;
|
||||
private TextView messageMessage;
|
||||
private ImageView messageIcon;
|
||||
private int messageTimeout = 6000;
|
||||
private int farRight;
|
||||
private int msgPos;
|
||||
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
timeoutInterval = Long.parseLong(PreferenceManager.getDefaultSharedPreferences(app).getString("pref_auto_logoff_timeout", "3600000"));
|
||||
startAutoLogoffLoop();
|
||||
TvApp.getApplication().setCurrentActivity(this);
|
||||
|
||||
//Add message UI - delay to ensure on top of other views
|
||||
handler.postDelayed(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (!isFinishing() && app.getCurrentActivity() != null) {
|
||||
FrameLayout root = findViewById(android.R.id.content);
|
||||
messageUi = View.inflate(app.getCurrentActivity(), R.layout.message, null);
|
||||
messageTitle = messageUi.findViewById(R.id.msgTitle);
|
||||
messageMessage = messageUi.findViewById(R.id.message);
|
||||
messageIcon = messageUi.findViewById(R.id.msgIcon);
|
||||
messageUi.setAlpha(0);
|
||||
FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT, Gravity.RIGHT | Gravity.BOTTOM);
|
||||
params.bottomMargin = Utils.convertDpToPixel(TvApp.getApplication().getCurrentActivity(), 50);
|
||||
Rect windowSize = new Rect();
|
||||
getWindow().getDecorView().getWindowVisibleDisplayFrame(windowSize);
|
||||
farRight = windowSize.right;
|
||||
msgPos = farRight - Utils.convertDpToPixel(TvApp.getApplication().getCurrentActivity(), 500);
|
||||
messageUi.setLeft(farRight);
|
||||
root.addView(messageUi, params);
|
||||
}
|
||||
}
|
||||
}, 500);
|
||||
|
||||
}
|
||||
|
||||
public void showMessage(String title, String msg) {
|
||||
showMessage(title, msg, messageTimeout, null, null);
|
||||
}
|
||||
|
||||
public void showMessage(String title, String msg, int timeout) {
|
||||
showMessage(title, msg, timeout, null, null);
|
||||
}
|
||||
|
||||
public void showMessage(String title, String msg, int timeout, Integer icon, Integer background) {
|
||||
if (messageUi != null && !isFinishing()) {
|
||||
messageTitle.setText(title);
|
||||
messageMessage.setText(msg);
|
||||
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
|
||||
public void run() {
|
||||
if (messageUi != null && !isFinishing()) {
|
||||
messageUi.animate().alpha(0).x(farRight).setDuration(300);
|
||||
}
|
||||
}
|
||||
}, timeout);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
//Banish task bars and navigation controls on non-TV devices
|
||||
@Override
|
||||
public void onWindowFocusChanged(boolean hasFocus) {
|
||||
super.onWindowFocusChanged(hasFocus);
|
||||
if (hasFocus && android.os.Build.VERSION.SDK_INT >= 19) {
|
||||
getWindow().getDecorView().setSystemUiVisibility(
|
||||
View.SYSTEM_UI_FLAG_LAYOUT_STABLE
|
||||
| View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
|
||||
| View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
|
||||
| View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
|
||||
| View.SYSTEM_UI_FLAG_FULLSCREEN
|
||||
| View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY);}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
TvApp.getApplication().setCurrentActivity(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPause() {
|
||||
super.onPause();
|
||||
TvApp.getApplication().setCurrentActivity(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
if (handler != null && loop != null) {
|
||||
handler.removeCallbacks(loop);
|
||||
}
|
||||
super.onDestroy();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUserInteraction() {
|
||||
super.onUserInteraction();
|
||||
app.setLastUserInteraction(System.currentTimeMillis());
|
||||
}
|
||||
|
||||
private void startAutoLogoffLoop() {
|
||||
if (loop != null) {
|
||||
return;
|
||||
}
|
||||
|
||||
loop = new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (app != null && System.currentTimeMillis() > app.getLastUserInteraction() + timeoutInterval) {
|
||||
app.getLogger().Info("Logging off due to inactivity %d", app.getLastUserInteraction());
|
||||
Utils.showToast(app, "Jellyfin Logging off due to inactivity...");
|
||||
if (app.getPlaybackController() != null && app.getPlaybackController().isPaused()) {
|
||||
app.getLogger().Info("Playback was paused, stopping gracefully...");
|
||||
app.getPlaybackController().stop();
|
||||
}
|
||||
handler.removeCallbacks(this);
|
||||
finish();
|
||||
} else {
|
||||
handler.postDelayed(this, 60000);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
handler.postDelayed(loop, 60000);
|
||||
}
|
||||
|
||||
public void registerKeyListener(IKeyListener listener) {
|
||||
keyListener = listener;
|
||||
}
|
||||
|
||||
public void registerMessageListener(IMessageListener listener) {
|
||||
messageListener = listener;
|
||||
}
|
||||
|
||||
public void sendMessage(CustomMessage message) {
|
||||
if (messageListener != null) {
|
||||
messageListener.onMessageReceived(message);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onKeyUp(int keyCode, KeyEvent event) {
|
||||
return keyListener != null ? keyListener.onKeyUp(keyCode, event) || super.onKeyUp(keyCode, event) : super.onKeyUp(keyCode, event);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onSearchRequested() {
|
||||
TvApp.getApplication().showSearch(this, false);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package org.jellyfin.androidtv.base;
|
||||
|
||||
/**
|
||||
* Created by Eric on 4/20/2015.
|
||||
*/
|
||||
public enum CustomMessage {
|
||||
RefreshCurrentItem,
|
||||
RemoveCurrentItem, ActionComplete, RefreshRows
|
||||
}
|
||||
@@ -1,7 +1,10 @@
|
||||
package org.jellyfin.androidtv.ui.shared;
|
||||
package org.jellyfin.androidtv.base;
|
||||
|
||||
import android.view.KeyEvent;
|
||||
|
||||
/**
|
||||
* Created by Eric on 4/16/2015.
|
||||
*/
|
||||
public interface IKeyListener {
|
||||
public boolean onKeyUp(int key, KeyEvent event);
|
||||
}
|
||||
@@ -1,7 +1,8 @@
|
||||
package org.jellyfin.androidtv.ui.shared;
|
||||
|
||||
import org.jellyfin.androidtv.constant.CustomMessage;
|
||||
package org.jellyfin.androidtv.base;
|
||||
|
||||
/**
|
||||
* Created by Eric on 4/20/2015.
|
||||
*/
|
||||
public interface IMessageListener {
|
||||
public void onMessageReceived(CustomMessage message);
|
||||
}
|
||||
@@ -1,9 +1,7 @@
|
||||
package org.jellyfin.androidtv.ui.browsing;
|
||||
package org.jellyfin.androidtv.browsing;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.leanback.widget.ArrayObjectAdapter;
|
||||
import androidx.leanback.widget.HeaderItem;
|
||||
import androidx.leanback.widget.ListRow;
|
||||
@@ -11,19 +9,18 @@ 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.constant.Extras;
|
||||
import org.jellyfin.androidtv.presentation.GridButtonPresenter;
|
||||
import org.jellyfin.androidtv.ui.GridButton;
|
||||
import org.jellyfin.androidtv.ui.presentation.GridButtonPresenter;
|
||||
|
||||
import org.jellyfin.apiclient.model.dto.BaseItemDto;
|
||||
import org.jellyfin.apiclient.serialization.GsonJsonSerializer;
|
||||
|
||||
import kotlin.Lazy;
|
||||
|
||||
import static org.koin.java.KoinJavaComponent.inject;
|
||||
|
||||
/**
|
||||
* Created by Eric on 12/4/2014.
|
||||
*/
|
||||
public class BrowseFolderFragment extends StdBrowseFragment {
|
||||
|
||||
protected static final int BY_LETTER = 0;
|
||||
@@ -35,11 +32,9 @@ public class BrowseFolderFragment extends StdBrowseFragment {
|
||||
protected String itemTypeString;
|
||||
protected boolean showViews = true;
|
||||
|
||||
private Lazy<GsonJsonSerializer> serializer = inject(GsonJsonSerializer.class);
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
mFolder = serializer.getValue().DeserializeFromString(getActivity().getIntent().getStringExtra(Extras.Folder), BaseItemDto.class);
|
||||
public void onActivityCreated(Bundle savedInstanceState) {
|
||||
mFolder = TvApp.getApplication().getSerializer().DeserializeFromString(getActivity().getIntent().getStringExtra("Folder"),BaseItemDto.class);
|
||||
if (MainTitle == null) MainTitle = mFolder.getName();
|
||||
ShowBadge = false;
|
||||
if (mFolder.getCollectionType() != null) {
|
||||
@@ -60,22 +55,22 @@ public class BrowseFolderFragment extends StdBrowseFragment {
|
||||
showViews = false;
|
||||
}
|
||||
|
||||
super.onCreate(savedInstanceState);
|
||||
super.onActivityCreated(savedInstanceState);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void addAdditionalRows(ArrayObjectAdapter rowAdapter) {
|
||||
super.addAdditionalRows(rowAdapter);
|
||||
if (showViews) {
|
||||
HeaderItem gridHeader = new HeaderItem(rowAdapter.size(), TvApp.getApplication().getString(R.string.lbl_views));
|
||||
HeaderItem gridHeader = new HeaderItem(rowAdapter.size(), mApplication.getString(R.string.lbl_views));
|
||||
|
||||
GridButtonPresenter mGridPresenter = new GridButtonPresenter();
|
||||
ArrayObjectAdapter gridRowAdapter = new ArrayObjectAdapter(mGridPresenter);
|
||||
gridRowAdapter.add(new GridButton(BY_LETTER, TvApp.getApplication().getString(R.string.lbl_by_letter), R.drawable.tile_letters, null));
|
||||
gridRowAdapter.add(new GridButton(BY_LETTER, mApplication.getString(R.string.lbl_by_letter), R.drawable.tile_letters));
|
||||
if (itemTypeString != null && itemTypeString.equals("Movie"))
|
||||
gridRowAdapter.add(new GridButton(SUGGESTED, TvApp.getApplication().getString(R.string.lbl_suggested), R.drawable.tile_suggestions, null));
|
||||
gridRowAdapter.add(new GridButton(GENRES, TvApp.getApplication().getString(R.string.lbl_genres), R.drawable.tile_genres, null));
|
||||
gridRowAdapter.add(new GridButton(PERSONS, TvApp.getApplication().getString(R.string.lbl_performers), R.drawable.tile_actors, null));
|
||||
gridRowAdapter.add(new GridButton(SUGGESTED, mApplication.getString(R.string.lbl_suggested), R.drawable.tile_suggestions));
|
||||
gridRowAdapter.add(new GridButton(GENRES, mApplication.getString(R.string.lbl_genres), R.drawable.tile_genres));
|
||||
gridRowAdapter.add(new GridButton(PERSONS, mApplication.getString(R.string.lbl_performers), R.drawable.tile_actors));
|
||||
rowAdapter.add(new ListRow(gridHeader, gridRowAdapter));
|
||||
}
|
||||
}
|
||||
@@ -94,34 +89,34 @@ public class BrowseFolderFragment extends StdBrowseFragment {
|
||||
switch (((GridButton) item).getId()) {
|
||||
case BY_LETTER:
|
||||
Intent intent = new Intent(getActivity(), ByLetterActivity.class);
|
||||
intent.putExtra(Extras.Folder, serializer.getValue().SerializeToString(mFolder));
|
||||
intent.putExtra(Extras.IncludeType, itemTypeString);
|
||||
intent.putExtra("Folder", TvApp.getApplication().getSerializer().SerializeToString(mFolder));
|
||||
intent.putExtra("IncludeType", itemTypeString);
|
||||
|
||||
getActivity().startActivity(intent);
|
||||
break;
|
||||
case GENRES:
|
||||
Intent genreIntent = new Intent(getActivity(), ByGenreActivity.class);
|
||||
genreIntent.putExtra(Extras.Folder, serializer.getValue().SerializeToString(mFolder));
|
||||
genreIntent.putExtra(Extras.IncludeType, itemTypeString);
|
||||
genreIntent.putExtra("Folder", TvApp.getApplication().getSerializer().SerializeToString(mFolder));
|
||||
genreIntent.putExtra("IncludeType", itemTypeString);
|
||||
|
||||
getActivity().startActivity(genreIntent);
|
||||
break;
|
||||
case SUGGESTED:
|
||||
Intent suggIntent = new Intent(getActivity(), SuggestedMoviesActivity.class);
|
||||
suggIntent.putExtra(Extras.Folder, serializer.getValue().SerializeToString(mFolder));
|
||||
suggIntent.putExtra(Extras.IncludeType, itemTypeString);
|
||||
suggIntent.putExtra("Folder", TvApp.getApplication().getSerializer().SerializeToString(mFolder));
|
||||
suggIntent.putExtra("IncludeType", itemTypeString);
|
||||
|
||||
getActivity().startActivity(suggIntent);
|
||||
break;
|
||||
case PERSONS:
|
||||
Intent personIntent = new Intent(getActivity(), BrowsePersonsActivity.class);
|
||||
personIntent.putExtra(Extras.Folder, serializer.getValue().SerializeToString(mFolder));
|
||||
personIntent.putExtra(Extras.IncludeType, itemTypeString);
|
||||
personIntent.putExtra("Folder", TvApp.getApplication().getSerializer().SerializeToString(mFolder));
|
||||
personIntent.putExtra("IncludeType", itemTypeString);
|
||||
|
||||
getActivity().startActivity(personIntent);
|
||||
break;
|
||||
default:
|
||||
Toast.makeText(getActivity(), item.toString() + TvApp.getApplication().getString(R.string.msg_not_implemented), Toast.LENGTH_SHORT).show();
|
||||
Toast.makeText(getActivity(), item.toString() + mApplication.getString(R.string.msg_not_implemented), Toast.LENGTH_SHORT).show();
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -1,18 +1,20 @@
|
||||
package org.jellyfin.androidtv.ui.browsing;
|
||||
package org.jellyfin.androidtv.browsing;
|
||||
|
||||
import android.os.Bundle;
|
||||
|
||||
import org.jellyfin.androidtv.TvApp;
|
||||
import org.jellyfin.androidtv.constant.Extras;
|
||||
import org.jellyfin.androidtv.constant.ChangeTriggerType;
|
||||
import org.jellyfin.androidtv.data.querying.StdItemQuery;
|
||||
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;
|
||||
|
||||
/**
|
||||
* Created by Eric on 8/16/2015.
|
||||
*/
|
||||
public class BrowseGridFragment extends StdGridFragment {
|
||||
private final static int CHUNK_SIZE = 50;
|
||||
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
@@ -22,16 +24,12 @@ public class BrowseGridFragment extends StdGridFragment {
|
||||
@Override
|
||||
public void onActivityCreated(Bundle savedInstanceState) {
|
||||
super.onActivityCreated(savedInstanceState);
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void setupQueries(IGridLoader gridLoader) {
|
||||
StdItemQuery query = new StdItemQuery(new ItemFields[] {
|
||||
ItemFields.PrimaryImageAspectRatio,
|
||||
ItemFields.ChildCount,
|
||||
ItemFields.MediaSources,
|
||||
ItemFields.MediaStreams
|
||||
});
|
||||
StdItemQuery query = new StdItemQuery(new ItemFields[] {ItemFields.PrimaryImageAspectRatio});
|
||||
query.setParentId(mParentId);
|
||||
if (mFolder.getBaseItemType() == BaseItemType.UserView || mFolder.getBaseItemType() == BaseItemType.CollectionFolder) {
|
||||
String type = mFolder.getCollectionType() != null ? mFolder.getCollectionType().toLowerCase() : "";
|
||||
@@ -52,17 +50,13 @@ public class BrowseGridFragment extends StdGridFragment {
|
||||
case "music":
|
||||
mAllowViewSelection = false;
|
||||
//Special queries needed for album artists
|
||||
String includeType = getActivity().getIntent().getStringExtra(Extras.IncludeType);
|
||||
String includeType = getActivity().getIntent().getStringExtra("IncludeType");
|
||||
if ("AlbumArtist".equals(includeType)) {
|
||||
ArtistsQuery albumArtists = new ArtistsQuery();
|
||||
albumArtists.setUserId(TvApp.getApplication().getCurrentUser().getId());
|
||||
albumArtists.setFields(new ItemFields[]{
|
||||
ItemFields.PrimaryImageAspectRatio,
|
||||
ItemFields.ItemCounts,
|
||||
ItemFields.ChildCount
|
||||
});
|
||||
albumArtists.setFields(new ItemFields[]{ItemFields.PrimaryImageAspectRatio, ItemFields.ItemCounts});
|
||||
albumArtists.setParentId(mParentId);
|
||||
mRowDef = new BrowseRowDef("", albumArtists, CHUNK_SIZE, new ChangeTriggerType[] {});
|
||||
mRowDef = new BrowseRowDef("", albumArtists, 150, new ChangeTriggerType[] {});
|
||||
gridLoader.loadGrid(mRowDef);
|
||||
return;
|
||||
}
|
||||
@@ -72,8 +66,9 @@ public class BrowseGridFragment extends StdGridFragment {
|
||||
}
|
||||
}
|
||||
|
||||
mRowDef = new BrowseRowDef("", query, CHUNK_SIZE, false, true);
|
||||
mRowDef = new BrowseRowDef("", query, 150, false, true);
|
||||
|
||||
gridLoader.loadGrid(mRowDef);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package org.jellyfin.androidtv.browsing;
|
||||
|
||||
import android.os.Bundle;
|
||||
|
||||
import org.jellyfin.androidtv.R;
|
||||
import org.jellyfin.androidtv.base.BaseActivity;
|
||||
|
||||
public class BrowsePersonsActivity extends BaseActivity {
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_browse_persons);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.jellyfin.androidtv.ui.browsing;
|
||||
package org.jellyfin.androidtv.browsing;
|
||||
|
||||
import org.jellyfin.androidtv.R;
|
||||
import org.jellyfin.androidtv.TvApp;
|
||||
@@ -0,0 +1,15 @@
|
||||
package org.jellyfin.androidtv.browsing;
|
||||
|
||||
import android.os.Bundle;
|
||||
|
||||
import org.jellyfin.androidtv.R;
|
||||
import org.jellyfin.androidtv.base.BaseActivity;
|
||||
|
||||
public class BrowseRecordingsActivity extends BaseActivity {
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_recordings);
|
||||
}
|
||||
}
|
||||
@@ -1,19 +1,22 @@
|
||||
package org.jellyfin.androidtv.ui.browsing;
|
||||
package org.jellyfin.androidtv.browsing;
|
||||
|
||||
import android.os.Handler;
|
||||
|
||||
import androidx.leanback.widget.ArrayObjectAdapter;
|
||||
import androidx.leanback.widget.HeaderItem;
|
||||
import androidx.leanback.widget.ListRow;
|
||||
|
||||
import org.jellyfin.androidtv.R;
|
||||
import org.jellyfin.androidtv.TvApp;
|
||||
import org.jellyfin.androidtv.itemhandling.ItemRowAdapter;
|
||||
import org.jellyfin.androidtv.model.DisplayPriorityType;
|
||||
import org.jellyfin.androidtv.presentation.GridButtonPresenter;
|
||||
import org.jellyfin.androidtv.ui.GridButton;
|
||||
import org.jellyfin.androidtv.ui.itemhandling.ItemRowAdapter;
|
||||
import org.jellyfin.androidtv.ui.presentation.GridButtonPresenter;
|
||||
import org.jellyfin.androidtv.util.TimeUtils;
|
||||
import org.jellyfin.androidtv.util.Utils;
|
||||
import org.jellyfin.apiclient.interaction.ApiClient;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import org.jellyfin.apiclient.interaction.Response;
|
||||
import org.jellyfin.apiclient.model.dto.BaseItemDto;
|
||||
import org.jellyfin.apiclient.model.dto.BaseItemType;
|
||||
@@ -25,30 +28,25 @@ import org.jellyfin.apiclient.model.livetv.TimerQuery;
|
||||
import org.jellyfin.apiclient.model.querying.ItemFields;
|
||||
import org.jellyfin.apiclient.model.results.TimerInfoDtoResult;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import timber.log.Timber;
|
||||
|
||||
import static org.koin.java.KoinJavaComponent.get;
|
||||
|
||||
/**
|
||||
* Created by Eric on 9/3/2015.
|
||||
*/
|
||||
public class BrowseRecordingsFragment extends EnhancedBrowseFragment {
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void setupQueries(final IRowLoader rowLoader) {
|
||||
|
||||
showViews = true;
|
||||
mTitle.setText(TvApp.getApplication().getResources().getString(R.string.lbl_loading_elipses));
|
||||
//Latest Recordings
|
||||
RecordingQuery recordings = new RecordingQuery();
|
||||
recordings.setFields(new ItemFields[]{
|
||||
ItemFields.Overview,
|
||||
ItemFields.PrimaryImageAspectRatio,
|
||||
ItemFields.ChildCount
|
||||
});
|
||||
recordings.setFields(new ItemFields[]{ItemFields.Overview, ItemFields.PrimaryImageAspectRatio});
|
||||
recordings.setUserId(TvApp.getApplication().getCurrentUser().getId());
|
||||
recordings.setEnableImages(true);
|
||||
recordings.setLimit(40);
|
||||
@@ -56,11 +54,7 @@ public class BrowseRecordingsFragment extends EnhancedBrowseFragment {
|
||||
|
||||
//Movies
|
||||
RecordingQuery movies = new RecordingQuery();
|
||||
movies.setFields(new ItemFields[]{
|
||||
ItemFields.Overview,
|
||||
ItemFields.PrimaryImageAspectRatio,
|
||||
ItemFields.ChildCount
|
||||
});
|
||||
movies.setFields(new ItemFields[]{ItemFields.Overview, ItemFields.PrimaryImageAspectRatio});
|
||||
movies.setUserId(TvApp.getApplication().getCurrentUser().getId());
|
||||
movies.setEnableImages(true);
|
||||
movies.setIsMovie(true);
|
||||
@@ -68,26 +62,24 @@ public class BrowseRecordingsFragment extends EnhancedBrowseFragment {
|
||||
|
||||
//Shows
|
||||
RecordingQuery shows = new RecordingQuery();
|
||||
shows.setFields(new ItemFields[]{
|
||||
ItemFields.Overview,
|
||||
ItemFields.PrimaryImageAspectRatio,
|
||||
ItemFields.ChildCount
|
||||
});
|
||||
shows.setFields(new ItemFields[]{ItemFields.Overview, ItemFields.PrimaryImageAspectRatio});
|
||||
shows.setUserId(TvApp.getApplication().getCurrentUser().getId());
|
||||
shows.setEnableImages(true);
|
||||
shows.setIsSeries(true);
|
||||
BrowseRowDef showsDef = new BrowseRowDef(mActivity.getString(R.string.lbl_tv_series), shows, 60);
|
||||
|
||||
mRows.add(showsDef);
|
||||
mRows.add(moviesDef);
|
||||
//Insert order based on pref
|
||||
if (mApplication.getDisplayPriority() == DisplayPriorityType.Movies) {
|
||||
mRows.add(moviesDef);
|
||||
mRows.add(showsDef);
|
||||
} else {
|
||||
mRows.add(showsDef);
|
||||
mRows.add(moviesDef);
|
||||
}
|
||||
|
||||
//Sports
|
||||
RecordingQuery sports = new RecordingQuery();
|
||||
sports.setFields(new ItemFields[]{
|
||||
ItemFields.Overview,
|
||||
ItemFields.PrimaryImageAspectRatio,
|
||||
ItemFields.ChildCount
|
||||
});
|
||||
sports.setFields(new ItemFields[]{ItemFields.Overview, ItemFields.PrimaryImageAspectRatio});
|
||||
sports.setUserId(TvApp.getApplication().getCurrentUser().getId());
|
||||
sports.setEnableImages(true);
|
||||
sports.setIsSports(true);
|
||||
@@ -95,11 +87,7 @@ public class BrowseRecordingsFragment extends EnhancedBrowseFragment {
|
||||
|
||||
//Kids
|
||||
RecordingQuery kids = new RecordingQuery();
|
||||
kids.setFields(new ItemFields[]{
|
||||
ItemFields.Overview,
|
||||
ItemFields.PrimaryImageAspectRatio,
|
||||
ItemFields.ChildCount
|
||||
});
|
||||
kids.setFields(new ItemFields[]{ItemFields.Overview, ItemFields.PrimaryImageAspectRatio});
|
||||
kids.setUserId(TvApp.getApplication().getCurrentUser().getId());
|
||||
kids.setEnableImages(true);
|
||||
kids.setIsKids(true);
|
||||
@@ -117,7 +105,7 @@ public class BrowseRecordingsFragment extends EnhancedBrowseFragment {
|
||||
private void addNext24Timers() {
|
||||
final TimerQuery scheduled = new TimerQuery();
|
||||
final long ticks24 = 1000 * 60 * 60 * 24;
|
||||
get(ApiClient.class).GetLiveTvTimersAsync(scheduled, new Response<TimerInfoDtoResult>() {
|
||||
TvApp.getApplication().getApiClient().GetLiveTvTimersAsync(scheduled, new Response<TimerInfoDtoResult>() {
|
||||
@Override
|
||||
public void onResponse(TimerInfoDtoResult response) {
|
||||
List<BaseItemDto> nearTimers = new ArrayList<>();
|
||||
@@ -131,7 +119,7 @@ public class BrowseRecordingsFragment extends EnhancedBrowseFragment {
|
||||
programInfo.setId(timer.getId());
|
||||
programInfo.setChannelName(timer.getChannelName());
|
||||
programInfo.setName(Utils.getSafeValue(timer.getName(), "Unknown"));
|
||||
Timber.w("No program info for timer %s. Creating one...", programInfo.getName());
|
||||
TvApp.getApplication().getLogger().Warn("No program info for timer %s. Creating one...", programInfo.getName());
|
||||
programInfo.setBaseItemType(BaseItemType.Program);
|
||||
programInfo.setTimerId(timer.getId());
|
||||
programInfo.setSeriesTimerId(timer.getSeriesTimerId());
|
||||
@@ -158,20 +146,22 @@ public class BrowseRecordingsFragment extends EnhancedBrowseFragment {
|
||||
|
||||
@Override
|
||||
public void onError(Exception exception) {
|
||||
Utils.showToast(TvApp.getApplication(), exception.getLocalizedMessage());
|
||||
Utils.showToast(mApplication, exception.getLocalizedMessage());
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void addAdditionalRows(ArrayObjectAdapter rowAdapter) {
|
||||
HeaderItem gridHeader = new HeaderItem(rowAdapter.size(), TvApp.getApplication().getString(R.string.lbl_views));
|
||||
HeaderItem gridHeader = new HeaderItem(rowAdapter.size(), mApplication.getString(R.string.lbl_views));
|
||||
|
||||
GridButtonPresenter mGridPresenter = new GridButtonPresenter();
|
||||
ArrayObjectAdapter gridRowAdapter = new ArrayObjectAdapter(mGridPresenter);
|
||||
gridRowAdapter.add(new GridButton(SCHEDULE, TvApp.getApplication().getString(R.string.lbl_schedule), R.drawable.tile_port_time, null));
|
||||
gridRowAdapter.add(new GridButton(SERIES, mActivity.getString(R.string.lbl_series_recordings), R.drawable.tile_port_series_timer, null));
|
||||
gridRowAdapter.add(new GridButton(SCHEDULE, TvApp.getApplication().getString(R.string.lbl_schedule), R.drawable.tile_port_time));
|
||||
gridRowAdapter.add(new GridButton(SERIES, mActivity.getString(R.string.lbl_series_recordings), R.drawable.tile_port_series_timer));
|
||||
rowAdapter.add(new ListRow(gridHeader, gridRowAdapter));
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,9 @@
|
||||
package org.jellyfin.androidtv.ui.browsing;
|
||||
package org.jellyfin.androidtv.browsing;
|
||||
|
||||
import org.jellyfin.androidtv.model.ChangeTriggerType;
|
||||
import org.jellyfin.androidtv.querying.QueryType;
|
||||
import org.jellyfin.androidtv.querying.ViewQuery;
|
||||
|
||||
import org.jellyfin.androidtv.constant.ChangeTriggerType;
|
||||
import org.jellyfin.androidtv.constant.QueryType;
|
||||
import org.jellyfin.androidtv.data.querying.ViewQuery;
|
||||
import org.jellyfin.apiclient.model.livetv.LiveTvChannelQuery;
|
||||
import org.jellyfin.apiclient.model.livetv.RecommendedProgramQuery;
|
||||
import org.jellyfin.apiclient.model.livetv.RecordingGroupQuery;
|
||||
@@ -17,6 +18,9 @@ import org.jellyfin.apiclient.model.querying.SeasonQuery;
|
||||
import org.jellyfin.apiclient.model.querying.SimilarItemsQuery;
|
||||
import org.jellyfin.apiclient.model.querying.UpcomingEpisodesQuery;
|
||||
|
||||
/**
|
||||
* Created by Eric on 12/4/2014.
|
||||
*/
|
||||
public class BrowseRowDef {
|
||||
private String headerText;
|
||||
private ItemQuery query;
|
||||
@@ -111,7 +115,6 @@ public class BrowseRowDef {
|
||||
headerText = header;
|
||||
this.latestItemsQuery = query;
|
||||
this.queryType = QueryType.LatestItems;
|
||||
this.staticHeight = true;
|
||||
this.changeTriggers = changeTriggers;
|
||||
}
|
||||
|
||||
@@ -246,3 +249,4 @@ public class BrowseRowDef {
|
||||
return recordingGroupQuery;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
package org.jellyfin.androidtv.browsing;
|
||||
|
||||
import android.os.Bundle;
|
||||
|
||||
import org.jellyfin.androidtv.R;
|
||||
import org.jellyfin.androidtv.base.BaseActivity;
|
||||
|
||||
public class BrowseScheduleActivity extends BaseActivity {
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_schedule);
|
||||
}
|
||||
}
|
||||
@@ -1,24 +1,32 @@
|
||||
package org.jellyfin.androidtv.ui.browsing;
|
||||
package org.jellyfin.androidtv.browsing;
|
||||
|
||||
import org.jellyfin.androidtv.livetv.TvManager;
|
||||
import org.jellyfin.androidtv.presentation.CardPresenter;
|
||||
|
||||
import org.jellyfin.androidtv.ui.livetv.TvManager;
|
||||
import org.jellyfin.androidtv.ui.presentation.CardPresenter;
|
||||
import org.jellyfin.apiclient.interaction.Response;
|
||||
import org.jellyfin.apiclient.model.livetv.TimerQuery;
|
||||
|
||||
/**
|
||||
* Created by Eric on 9/3/2015.
|
||||
*/
|
||||
public class BrowseScheduleFragment extends EnhancedBrowseFragment {
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void setupQueries(final IRowLoader rowLoader) {
|
||||
TvManager.getScheduleRowsAsync(requireContext(), new TimerQuery(), new CardPresenter(true), mRowsAdapter, new Response<Integer>() {
|
||||
TvManager.getScheduleRowsAsync(new TimerQuery(), new CardPresenter(true), mRowsAdapter, new Response<Integer>() {
|
||||
@Override
|
||||
public void onResponse(Integer response) {
|
||||
if (response == 0) mActivity.setTitle("No Scheduled Recordings");
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.jellyfin.androidtv.ui.browsing;
|
||||
package org.jellyfin.androidtv.browsing;
|
||||
|
||||
import androidx.leanback.widget.ArrayObjectAdapter;
|
||||
import androidx.leanback.widget.HeaderItem;
|
||||
@@ -6,16 +6,18 @@ import androidx.leanback.widget.ListRow;
|
||||
|
||||
import org.jellyfin.androidtv.R;
|
||||
import org.jellyfin.androidtv.TvApp;
|
||||
import org.jellyfin.androidtv.constant.ChangeTriggerType;
|
||||
import org.jellyfin.androidtv.constant.QueryType;
|
||||
import org.jellyfin.androidtv.data.querying.StdItemQuery;
|
||||
import org.jellyfin.androidtv.preference.UserPreferences;
|
||||
import org.jellyfin.androidtv.itemhandling.ItemRowAdapter;
|
||||
import org.jellyfin.androidtv.model.ChangeTriggerType;
|
||||
import org.jellyfin.androidtv.presentation.GridButtonPresenter;
|
||||
import org.jellyfin.androidtv.querying.QueryType;
|
||||
import org.jellyfin.androidtv.querying.StdItemQuery;
|
||||
import org.jellyfin.androidtv.ui.GridButton;
|
||||
import org.jellyfin.androidtv.ui.itemhandling.ItemRowAdapter;
|
||||
import org.jellyfin.androidtv.ui.presentation.GridButtonPresenter;
|
||||
import org.jellyfin.androidtv.util.TimeUtils;
|
||||
import org.jellyfin.androidtv.util.Utils;
|
||||
import org.jellyfin.apiclient.interaction.ApiClient;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import org.jellyfin.apiclient.interaction.Response;
|
||||
import org.jellyfin.apiclient.model.dto.BaseItemDto;
|
||||
import org.jellyfin.apiclient.model.dto.BaseItemType;
|
||||
@@ -37,20 +39,9 @@ import org.jellyfin.apiclient.model.querying.LatestItemsQuery;
|
||||
import org.jellyfin.apiclient.model.querying.NextUpQuery;
|
||||
import org.jellyfin.apiclient.model.results.TimerInfoDtoResult;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import kotlin.Lazy;
|
||||
import timber.log.Timber;
|
||||
|
||||
import static org.koin.java.KoinJavaComponent.get;
|
||||
import static org.koin.java.KoinJavaComponent.inject;
|
||||
|
||||
public class BrowseViewFragment extends EnhancedBrowseFragment {
|
||||
private boolean isLiveTvLibrary;
|
||||
|
||||
private Lazy<ApiClient> apiClient = inject(ApiClient.class);
|
||||
|
||||
@Override
|
||||
protected void setupQueries(final IRowLoader rowLoader) {
|
||||
String type = mFolder.getCollectionType() != null ? mFolder.getCollectionType().toLowerCase() : "";
|
||||
@@ -59,15 +50,7 @@ public class BrowseViewFragment extends EnhancedBrowseFragment {
|
||||
itemTypeString = "Movie";
|
||||
|
||||
//Resume
|
||||
StdItemQuery resumeMovies = new StdItemQuery(new ItemFields[]{
|
||||
ItemFields.PrimaryImageAspectRatio,
|
||||
ItemFields.Overview,
|
||||
ItemFields.ItemCounts,
|
||||
ItemFields.DisplayPreferencesId,
|
||||
ItemFields.ChildCount,
|
||||
ItemFields.MediaStreams,
|
||||
ItemFields.MediaSources
|
||||
});
|
||||
StdItemQuery resumeMovies = new StdItemQuery();
|
||||
resumeMovies.setIncludeItemTypes(new String[]{"Movie"});
|
||||
resumeMovies.setRecursive(true);
|
||||
resumeMovies.setParentId(mFolder.getId());
|
||||
@@ -78,51 +61,34 @@ 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(TvApp.getApplication().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,
|
||||
ItemFields.ChildCount,
|
||||
ItemFields.MediaSources,
|
||||
ItemFields.MediaStreams
|
||||
});
|
||||
latestMovies.setFields(new ItemFields[]{ItemFields.PrimaryImageAspectRatio, ItemFields.Overview});
|
||||
latestMovies.setParentId(mFolder.getId());
|
||||
latestMovies.setLimit(50);
|
||||
latestMovies.setImageTypeLimit(1);
|
||||
mRows.add(new BrowseRowDef(TvApp.getApplication().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(new ItemFields[]{
|
||||
ItemFields.PrimaryImageAspectRatio,
|
||||
ItemFields.Overview,
|
||||
ItemFields.ItemCounts,
|
||||
ItemFields.DisplayPreferencesId,
|
||||
ItemFields.ChildCount,
|
||||
ItemFields.MediaStreams,
|
||||
ItemFields.MediaSources
|
||||
});
|
||||
StdItemQuery favorites = new StdItemQuery();
|
||||
favorites.setIncludeItemTypes(new String[]{"Movie"});
|
||||
favorites.setRecursive(true);
|
||||
favorites.setParentId(mFolder.getId());
|
||||
favorites.setImageTypeLimit(1);
|
||||
favorites.setFilters(new ItemFilter[]{ItemFilter.IsFavorite});
|
||||
favorites.setSortBy(new String[]{ItemSortBy.SortName});
|
||||
mRows.add(new BrowseRowDef(TvApp.getApplication().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();
|
||||
collections.setFields(new ItemFields[]{
|
||||
ItemFields.ChildCount
|
||||
});
|
||||
collections.setIncludeItemTypes(new String[]{"BoxSet"});
|
||||
collections.setRecursive(true);
|
||||
collections.setImageTypeLimit(1);
|
||||
//collections.setParentId(mFolder.getId());
|
||||
collections.setSortBy(new String[]{ItemSortBy.SortName});
|
||||
mRows.add(new BrowseRowDef(TvApp.getApplication().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;
|
||||
@@ -135,23 +101,12 @@ public class BrowseViewFragment extends EnhancedBrowseFragment {
|
||||
nextUpQuery.setLimit(50);
|
||||
nextUpQuery.setParentId(mFolder.getId());
|
||||
nextUpQuery.setImageTypeLimit(1);
|
||||
nextUpQuery.setFields(new ItemFields[]{
|
||||
ItemFields.PrimaryImageAspectRatio,
|
||||
ItemFields.Overview,
|
||||
ItemFields.ChildCount,
|
||||
ItemFields.MediaSources,
|
||||
ItemFields.MediaStreams
|
||||
});
|
||||
mRows.add(new BrowseRowDef(TvApp.getApplication().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 (get(UserPreferences.class).get(UserPreferences.Companion.getPremieresEnabled())) {
|
||||
StdItemQuery newQuery = new StdItemQuery(new ItemFields[]{
|
||||
ItemFields.DateCreated,
|
||||
ItemFields.PrimaryImageAspectRatio,
|
||||
ItemFields.Overview,
|
||||
ItemFields.ChildCount
|
||||
});
|
||||
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.setParentId(mFolder.getId());
|
||||
@@ -164,24 +119,18 @@ public class BrowseViewFragment extends EnhancedBrowseFragment {
|
||||
newQuery.setSortOrder(SortOrder.Descending);
|
||||
newQuery.setEnableTotalRecordCount(false);
|
||||
newQuery.setLimit(300);
|
||||
mRows.add(new BrowseRowDef(TvApp.getApplication().getString(R.string.lbl_new_premieres), newQuery, 0, true, true, new ChangeTriggerType[]{ChangeTriggerType.TvPlayback}, QueryType.Premieres));
|
||||
mRows.add(new BrowseRowDef(mApplication.getString(R.string.lbl_new_premieres), newQuery, 0, true, true, new ChangeTriggerType[]{ChangeTriggerType.TvPlayback}, QueryType.Premieres));
|
||||
}
|
||||
|
||||
//Latest content added
|
||||
LatestItemsQuery latestSeries = new LatestItemsQuery();
|
||||
latestSeries.setFields(new ItemFields[]{
|
||||
ItemFields.PrimaryImageAspectRatio,
|
||||
ItemFields.Overview,
|
||||
ItemFields.ChildCount,
|
||||
ItemFields.MediaSources,
|
||||
ItemFields.MediaStreams
|
||||
});
|
||||
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(TvApp.getApplication().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();
|
||||
@@ -191,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(TvApp.getApplication().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;
|
||||
@@ -199,17 +148,13 @@ public class BrowseViewFragment extends EnhancedBrowseFragment {
|
||||
|
||||
//Latest
|
||||
LatestItemsQuery latestAlbums = new LatestItemsQuery();
|
||||
latestAlbums.setFields(new ItemFields[]{
|
||||
ItemFields.PrimaryImageAspectRatio,
|
||||
ItemFields.Overview,
|
||||
ItemFields.ChildCount
|
||||
});
|
||||
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(TvApp.getApplication().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();
|
||||
@@ -222,7 +167,7 @@ public class BrowseViewFragment extends EnhancedBrowseFragment {
|
||||
lastPlayed.setSortOrder(SortOrder.Descending);
|
||||
lastPlayed.setEnableTotalRecordCount(false);
|
||||
lastPlayed.setLimit(50);
|
||||
mRows.add(new BrowseRowDef(TvApp.getApplication().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();
|
||||
@@ -232,20 +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(TvApp.getApplication().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,
|
||||
ItemFields.ChildCount
|
||||
});
|
||||
StdItemQuery playlists = new StdItemQuery(new ItemFields[]{ItemFields.PrimaryImageAspectRatio, ItemFields.CumulativeRunTimeTicks});
|
||||
playlists.setIncludeItemTypes(new String[]{"Playlist"});
|
||||
playlists.setImageTypeLimit(1);
|
||||
playlists.setRecursive(true);
|
||||
playlists.setSortBy(new String[]{ItemSortBy.DateCreated});
|
||||
playlists.setSortOrder(SortOrder.Descending);
|
||||
mRows.add(new BrowseRowDef(TvApp.getApplication().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;
|
||||
@@ -256,60 +197,46 @@ public class BrowseViewFragment extends EnhancedBrowseFragment {
|
||||
//On now
|
||||
RecommendedProgramQuery onNow = new RecommendedProgramQuery();
|
||||
onNow.setIsAiring(true);
|
||||
onNow.setFields(new ItemFields[]{
|
||||
ItemFields.Overview,
|
||||
ItemFields.PrimaryImageAspectRatio,
|
||||
ItemFields.ChannelInfo,
|
||||
ItemFields.ChildCount
|
||||
});
|
||||
onNow.setFields(new ItemFields[]{ItemFields.Overview, ItemFields.PrimaryImageAspectRatio, ItemFields.ChannelInfo});
|
||||
onNow.setUserId(TvApp.getApplication().getCurrentUser().getId());
|
||||
onNow.setImageTypeLimit(1);
|
||||
onNow.setEnableTotalRecordCount(false);
|
||||
onNow.setLimit(150);
|
||||
mRows.add(new BrowseRowDef(TvApp.getApplication().getString(R.string.lbl_on_now), onNow));
|
||||
mRows.add(new BrowseRowDef(mApplication.getString(R.string.lbl_on_now), onNow));
|
||||
|
||||
//Upcoming
|
||||
RecommendedProgramQuery upcomingTv = new RecommendedProgramQuery();
|
||||
upcomingTv.setUserId(TvApp.getApplication().getCurrentUser().getId());
|
||||
upcomingTv.setFields(new ItemFields[]{
|
||||
ItemFields.Overview,
|
||||
ItemFields.PrimaryImageAspectRatio,
|
||||
ItemFields.ChannelInfo,
|
||||
ItemFields.ChildCount
|
||||
});
|
||||
upcomingTv.setFields(new ItemFields[]{ItemFields.Overview, ItemFields.PrimaryImageAspectRatio, ItemFields.ChannelInfo});
|
||||
upcomingTv.setIsAiring(false);
|
||||
upcomingTv.setHasAired(false);
|
||||
upcomingTv.setImageTypeLimit(1);
|
||||
upcomingTv.setEnableTotalRecordCount(false);
|
||||
upcomingTv.setLimit(150);
|
||||
mRows.add(new BrowseRowDef(TvApp.getApplication().getString(R.string.lbl_coming_up), upcomingTv));
|
||||
mRows.add(new BrowseRowDef(mApplication.getString(R.string.lbl_coming_up), upcomingTv));
|
||||
|
||||
//Fav Channels
|
||||
LiveTvChannelQuery favTv = new LiveTvChannelQuery();
|
||||
favTv.setUserId(TvApp.getApplication().getCurrentUser().getId());
|
||||
favTv.setEnableFavoriteSorting(true);
|
||||
favTv.setIsFavorite(true);
|
||||
mRows.add(new BrowseRowDef(TvApp.getApplication().getString(R.string.lbl_favorite_channels), favTv));
|
||||
mRows.add(new BrowseRowDef(mApplication.getString(R.string.lbl_favorite_channels), favTv));
|
||||
|
||||
//Other Channels
|
||||
LiveTvChannelQuery otherTv = new LiveTvChannelQuery();
|
||||
otherTv.setUserId(TvApp.getApplication().getCurrentUser().getId());
|
||||
otherTv.setIsFavorite(false);
|
||||
mRows.add(new BrowseRowDef(TvApp.getApplication().getString(R.string.lbl_other_channels), otherTv));
|
||||
mRows.add(new BrowseRowDef(mApplication.getString(R.string.lbl_other_channels), otherTv));
|
||||
|
||||
//Latest Recordings
|
||||
RecordingQuery recordings = new RecordingQuery();
|
||||
recordings.setFields(new ItemFields[]{
|
||||
ItemFields.Overview,
|
||||
ItemFields.PrimaryImageAspectRatio,
|
||||
ItemFields.ChildCount
|
||||
});
|
||||
recordings.setFields(new ItemFields[]{ItemFields.Overview, ItemFields.PrimaryImageAspectRatio});
|
||||
recordings.setUserId(TvApp.getApplication().getCurrentUser().getId());
|
||||
recordings.setEnableImages(true);
|
||||
recordings.setLimit(40);
|
||||
|
||||
//Do a straight query and then split the returned items into logical groups
|
||||
apiClient.getValue().GetLiveTvRecordingsAsync(recordings, new Response<ItemsResult>() {
|
||||
TvApp.getApplication().getApiClient().GetLiveTvRecordingsAsync(recordings, new Response<ItemsResult>() {
|
||||
@Override
|
||||
public void onResponse(ItemsResult response) {
|
||||
final ItemsResult recordingsResponse = response;
|
||||
@@ -317,7 +244,7 @@ public class BrowseViewFragment extends EnhancedBrowseFragment {
|
||||
|
||||
// Also get scheduled recordings for next 24 hours
|
||||
final TimerQuery scheduled = new TimerQuery();
|
||||
apiClient.getValue().GetLiveTvTimersAsync(scheduled, new Response<TimerInfoDtoResult>() {
|
||||
TvApp.getApplication().getApiClient().GetLiveTvTimersAsync(scheduled, new Response<TimerInfoDtoResult>() {
|
||||
@Override
|
||||
public void onResponse(TimerInfoDtoResult response) {
|
||||
List<BaseItemDto> nearTimers = new ArrayList<>();
|
||||
@@ -331,7 +258,7 @@ public class BrowseViewFragment extends EnhancedBrowseFragment {
|
||||
programInfo.setId(timer.getId());
|
||||
programInfo.setChannelName(timer.getChannelName());
|
||||
programInfo.setName(Utils.getSafeValue(timer.getName(), "Unknown"));
|
||||
Timber.w("No program info for timer %s. Creating one...", programInfo.getName());
|
||||
TvApp.getApplication().getLogger().Warn("No program info for timer %s. Creating one...", programInfo.getName());
|
||||
programInfo.setBaseItemType(BaseItemType.Program);
|
||||
programInfo.setTimerId(timer.getId());
|
||||
programInfo.setSeriesTimerId(timer.getSeriesTimerId());
|
||||
@@ -362,11 +289,7 @@ public class BrowseViewFragment extends EnhancedBrowseFragment {
|
||||
//First put all recordings in and retrieve
|
||||
//All Recordings
|
||||
RecordingQuery recordings = new RecordingQuery();
|
||||
recordings.setFields(new ItemFields[]{
|
||||
ItemFields.Overview,
|
||||
ItemFields.PrimaryImageAspectRatio,
|
||||
ItemFields.ChildCount
|
||||
});
|
||||
recordings.setFields(new ItemFields[]{ItemFields.Overview, ItemFields.PrimaryImageAspectRatio});
|
||||
recordings.setUserId(TvApp.getApplication().getCurrentUser().getId());
|
||||
recordings.setEnableImages(true);
|
||||
mRows.add(new BrowseRowDef(mActivity.getString(R.string.lbl_recent_recordings), recordings, 50));
|
||||
@@ -415,7 +338,7 @@ public class BrowseViewFragment extends EnhancedBrowseFragment {
|
||||
|
||||
@Override
|
||||
public void onError(Exception exception) {
|
||||
Utils.showToast(TvApp.getApplication(), exception.getLocalizedMessage());
|
||||
Utils.showToast(mApplication, exception.getLocalizedMessage());
|
||||
}
|
||||
});
|
||||
|
||||
@@ -436,7 +359,7 @@ public class BrowseViewFragment extends EnhancedBrowseFragment {
|
||||
query.setImageTypeLimit(1);
|
||||
query.setSortBy(new String[]{ItemSortBy.SortName});
|
||||
|
||||
apiClient.getValue().GetItemsAsync(query, new Response<ItemsResult>() {
|
||||
TvApp.getApplication().getApiClient().GetItemsAsync(query, new Response<ItemsResult>() {
|
||||
@Override
|
||||
public void onResponse(ItemsResult response) {
|
||||
if (response.getTotalRecordCount() > 0) {
|
||||
@@ -464,35 +387,14 @@ public class BrowseViewFragment extends EnhancedBrowseFragment {
|
||||
protected void addAdditionalRows(ArrayObjectAdapter rowAdapter) {
|
||||
if (isLiveTvLibrary) {
|
||||
//Views row
|
||||
HeaderItem gridHeader = new HeaderItem(mRowsAdapter.size(), TvApp.getApplication().getString(R.string.lbl_views));
|
||||
HeaderItem gridHeader = new HeaderItem(mRowsAdapter.size(), mApplication.getString(R.string.lbl_views));
|
||||
|
||||
GridButtonPresenter mGridPresenter = new GridButtonPresenter();
|
||||
ArrayObjectAdapter gridRowAdapter = new ArrayObjectAdapter(mGridPresenter);
|
||||
gridRowAdapter.add(new GridButton(
|
||||
TvApp.LIVE_TV_GUIDE_OPTION_ID,
|
||||
TvApp.getApplication().getResources().getString(R.string.lbl_live_tv_guide),
|
||||
R.drawable.tile_port_guide,
|
||||
null
|
||||
));
|
||||
gridRowAdapter.add(new GridButton(
|
||||
TvApp.LIVE_TV_RECORDINGS_OPTION_ID,
|
||||
TvApp.getApplication().getResources().getString(R.string.lbl_recorded_tv),
|
||||
R.drawable.tile_port_record,
|
||||
null
|
||||
));
|
||||
if (TvApp.getApplication().canManageRecordings()) {
|
||||
gridRowAdapter.add(new GridButton(
|
||||
TvApp.LIVE_TV_SCHEDULE_OPTION_ID,
|
||||
TvApp.getApplication().getResources().getString(R.string.lbl_schedule),
|
||||
R.drawable.tile_port_time,
|
||||
null
|
||||
));
|
||||
gridRowAdapter.add(new GridButton(
|
||||
TvApp.LIVE_TV_SERIES_OPTION_ID,
|
||||
TvApp.getApplication().getResources().getString(R.string.lbl_series),
|
||||
R.drawable.tile_port_series_timer,
|
||||
null
|
||||
));
|
||||
gridRowAdapter.add(new GridButton(TvApp.LIVE_TV_RECORDINGS_OPTION_ID, TvApp.getApplication().getResources().getString(R.string.lbl_recorded_tv), R.drawable.tile_port_record));
|
||||
if (mApplication.canManageRecordings()) {
|
||||
gridRowAdapter.add(new GridButton(TvApp.LIVE_TV_SCHEDULE_OPTION_ID, TvApp.getApplication().getResources().getString(R.string.lbl_schedule), R.drawable.tile_port_time));
|
||||
gridRowAdapter.add(new GridButton(TvApp.LIVE_TV_SERIES_OPTION_ID, TvApp.getApplication().getResources().getString(R.string.lbl_series), R.drawable.tile_port_series_timer));
|
||||
}
|
||||
|
||||
mRowsAdapter.add(new ListRow(gridHeader, gridRowAdapter));
|
||||
@@ -0,0 +1,15 @@
|
||||
package org.jellyfin.androidtv.browsing;
|
||||
|
||||
import android.os.Bundle;
|
||||
|
||||
import org.jellyfin.androidtv.R;
|
||||
import org.jellyfin.androidtv.base.BaseActivity;
|
||||
|
||||
public class ByGenreActivity extends BaseActivity {
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_by_genre);
|
||||
}
|
||||
}
|
||||
@@ -1,20 +1,20 @@
|
||||
package org.jellyfin.androidtv.ui.browsing;
|
||||
package org.jellyfin.androidtv.browsing;
|
||||
|
||||
import android.os.Bundle;
|
||||
|
||||
import org.jellyfin.androidtv.TvApp;
|
||||
import org.jellyfin.androidtv.data.querying.StdItemQuery;
|
||||
import org.jellyfin.androidtv.querying.StdItemQuery;
|
||||
import org.jellyfin.androidtv.util.DelayedMessage;
|
||||
|
||||
import org.jellyfin.androidtv.util.Utils;
|
||||
import org.jellyfin.apiclient.interaction.ApiClient;
|
||||
import org.jellyfin.apiclient.interaction.Response;
|
||||
import org.jellyfin.apiclient.model.dto.BaseItemDto;
|
||||
import org.jellyfin.apiclient.model.querying.ItemSortBy;
|
||||
import org.jellyfin.apiclient.model.querying.ItemsByNameQuery;
|
||||
import org.jellyfin.apiclient.model.querying.ItemsResult;
|
||||
|
||||
import static org.koin.java.KoinJavaComponent.get;
|
||||
|
||||
public class ByGenreFragment extends CustomViewFragment {
|
||||
|
||||
@Override
|
||||
public void onActivityCreated(Bundle savedInstanceState) {
|
||||
super.onActivityCreated(savedInstanceState);
|
||||
@@ -22,7 +22,10 @@ public class ByGenreFragment extends CustomViewFragment {
|
||||
|
||||
@Override
|
||||
protected void setupQueries(final IRowLoader rowLoader) {
|
||||
|
||||
if (Utils.getSafeValue(mFolder.getChildCount(), 0) > 0) {
|
||||
final DelayedMessage message = new DelayedMessage(getActivity());
|
||||
|
||||
//Get all genres for this folder
|
||||
ItemsByNameQuery genres = new ItemsByNameQuery();
|
||||
genres.setParentId(mFolder.getId());
|
||||
@@ -30,16 +33,15 @@ public class ByGenreFragment extends CustomViewFragment {
|
||||
if (includeType != null) genres.setIncludeItemTypes(new String[]{includeType});
|
||||
genres.setRecursive(true);
|
||||
genres.setUserId(TvApp.getApplication().getCurrentUser().getId());
|
||||
get(ApiClient.class).GetGenresAsync(genres, new Response<ItemsResult>() {
|
||||
TvApp.getApplication().getApiClient().GetGenresAsync(genres, new Response<ItemsResult>() {
|
||||
@Override
|
||||
public void onResponse(ItemsResult response) {
|
||||
for (BaseItemDto genre : response.getItems()) {
|
||||
StdItemQuery genreQuery = new StdItemQuery();
|
||||
genreQuery.setParentId(mFolder.getId());
|
||||
genreQuery.setSortBy(new String[]{ItemSortBy.SortName});
|
||||
if (includeType != null)
|
||||
genreQuery.setIncludeItemTypes(new String[]{includeType});
|
||||
genreQuery.setGenres(new String[]{genre.getName()});
|
||||
if (includeType != null) genreQuery.setIncludeItemTypes(new String[]{includeType});
|
||||
genreQuery.setGenres(new String[] {genre.getName()});
|
||||
genreQuery.setRecursive(true);
|
||||
mRows.add(new BrowseRowDef(genre.getName(), genreQuery, 40));
|
||||
}
|
||||
@@ -47,10 +49,15 @@ public class ByGenreFragment extends CustomViewFragment {
|
||||
if (mRows.size() < 2) setHeadersState(HEADERS_DISABLED);
|
||||
|
||||
rowLoader.loadRows(mRows);
|
||||
|
||||
message.Cancel();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
}
|
||||
else {
|
||||
setHeadersState(HEADERS_DISABLED);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package org.jellyfin.androidtv.browsing;
|
||||
|
||||
import android.os.Bundle;
|
||||
|
||||
import org.jellyfin.androidtv.R;
|
||||
import org.jellyfin.androidtv.base.BaseActivity;
|
||||
|
||||
public class ByLetterActivity extends BaseActivity {
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_by_letter);
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
package org.jellyfin.androidtv.ui.browsing;
|
||||
package org.jellyfin.androidtv.browsing;
|
||||
|
||||
import org.jellyfin.androidtv.R;
|
||||
import org.jellyfin.androidtv.TvApp;
|
||||
import org.jellyfin.androidtv.data.querying.StdItemQuery;
|
||||
import org.jellyfin.androidtv.querying.StdItemQuery;
|
||||
|
||||
import org.jellyfin.androidtv.util.Utils;
|
||||
import org.jellyfin.apiclient.model.querying.ItemSortBy;
|
||||
@@ -0,0 +1,15 @@
|
||||
package org.jellyfin.androidtv.browsing;
|
||||
|
||||
import android.os.Bundle;
|
||||
|
||||
import org.jellyfin.androidtv.R;
|
||||
import org.jellyfin.androidtv.base.BaseActivity;
|
||||
|
||||
public class CollectionActivity extends BaseActivity {
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_collection);
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,10 @@
|
||||
package org.jellyfin.androidtv.ui.browsing;
|
||||
package org.jellyfin.androidtv.browsing;
|
||||
|
||||
import android.os.Bundle;
|
||||
|
||||
import org.jellyfin.androidtv.R;
|
||||
import org.jellyfin.androidtv.TvApp;
|
||||
import org.jellyfin.androidtv.data.querying.StdItemQuery;
|
||||
import org.jellyfin.androidtv.querying.StdItemQuery;
|
||||
import org.jellyfin.androidtv.util.Utils;
|
||||
import org.jellyfin.apiclient.model.querying.ItemFields;
|
||||
|
||||
public class CollectionFragment extends EnhancedBrowseFragment {
|
||||
|
||||
@@ -19,28 +17,20 @@ public class CollectionFragment extends EnhancedBrowseFragment {
|
||||
@Override
|
||||
protected void setupQueries(IRowLoader rowLoader) {
|
||||
if (Utils.getSafeValue(mFolder.getChildCount(), 0) > 0) {
|
||||
StdItemQuery movies = new StdItemQuery(new ItemFields[]{
|
||||
ItemFields.PrimaryImageAspectRatio,
|
||||
ItemFields.Overview,
|
||||
ItemFields.ItemCounts,
|
||||
ItemFields.DisplayPreferencesId,
|
||||
ItemFields.ChildCount,
|
||||
ItemFields.MediaStreams,
|
||||
ItemFields.MediaSources
|
||||
});
|
||||
StdItemQuery movies = new StdItemQuery();
|
||||
movies.setParentId(mFolder.getId());
|
||||
movies.setIncludeItemTypes(new String[]{"Movie"});
|
||||
mRows.add(new BrowseRowDef(TvApp.getApplication().getString(R.string.lbl_movies), movies, 100));
|
||||
mRows.add(new BrowseRowDef(mApplication.getString(R.string.lbl_movies), movies, 100));
|
||||
|
||||
StdItemQuery series = new StdItemQuery();
|
||||
series.setParentId(mFolder.getId());
|
||||
series.setIncludeItemTypes(new String[]{"Series"});
|
||||
mRows.add(new BrowseRowDef(TvApp.getApplication().getString(R.string.lbl_tv_series), series, 100));
|
||||
mRows.add(new BrowseRowDef(mApplication.getString(R.string.lbl_tv_series), series, 100));
|
||||
|
||||
StdItemQuery others = new StdItemQuery();
|
||||
others.setParentId(mFolder.getId());
|
||||
others.setExcludeItemTypes(new String[]{"Movie", "Series"});
|
||||
mRows.add(new BrowseRowDef(TvApp.getApplication().getString(R.string.lbl_other), others, 100));
|
||||
mRows.add(new BrowseRowDef(mApplication.getString(R.string.lbl_other), others, 100));
|
||||
|
||||
|
||||
rowLoader.loadRows(mRows);
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.jellyfin.androidtv.ui.browsing;
|
||||
package org.jellyfin.androidtv.browsing;
|
||||
|
||||
import androidx.leanback.widget.OnItemViewClickedListener;
|
||||
import androidx.leanback.widget.Presenter;
|
||||
@@ -8,6 +8,9 @@ import androidx.leanback.widget.RowPresenter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Created by Eric on 4/15/2015.
|
||||
*/
|
||||
public class CompositeClickedListener implements OnItemViewClickedListener {
|
||||
private List<OnItemViewClickedListener> registeredListeners = new ArrayList<>();
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.jellyfin.androidtv.ui.browsing;
|
||||
package org.jellyfin.androidtv.browsing;
|
||||
|
||||
import androidx.leanback.widget.OnItemViewSelectedListener;
|
||||
import androidx.leanback.widget.Presenter;
|
||||
@@ -0,0 +1,299 @@
|
||||
package org.jellyfin.androidtv.browsing;
|
||||
|
||||
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.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.OnItemViewSelectedListener;
|
||||
import androidx.leanback.widget.Presenter;
|
||||
import androidx.leanback.widget.Row;
|
||||
import androidx.leanback.widget.RowPresenter;
|
||||
import android.util.DisplayMetrics;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import com.bumptech.glide.Glide;
|
||||
import com.bumptech.glide.request.animation.GlideAnimation;
|
||||
import com.bumptech.glide.request.target.SimpleTarget;
|
||||
|
||||
import org.jellyfin.androidtv.R;
|
||||
import org.jellyfin.androidtv.TvApp;
|
||||
import org.jellyfin.androidtv.base.BaseActivity;
|
||||
import org.jellyfin.androidtv.itemhandling.BaseRowItem;
|
||||
import org.jellyfin.androidtv.itemhandling.ItemLauncher;
|
||||
import org.jellyfin.androidtv.itemhandling.ItemRowAdapter;
|
||||
import org.jellyfin.androidtv.presentation.CardPresenter;
|
||||
import org.jellyfin.androidtv.presentation.PositionableListRowPresenter;
|
||||
import org.jellyfin.androidtv.querying.QueryType;
|
||||
import org.jellyfin.androidtv.querying.ViewQuery;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Timer;
|
||||
import java.util.TimerTask;
|
||||
|
||||
public class CustomBrowseFragment extends Fragment implements IRowLoader {
|
||||
private RowsSupportFragment mRowsFragment;
|
||||
private static final int BACKGROUND_UPDATE_DELAY = 100;
|
||||
|
||||
protected String MainTitle;
|
||||
protected boolean ShowBadge = true;
|
||||
protected TvApp mApplication;
|
||||
protected BaseActivity mActivity;
|
||||
protected BaseRowItem mCurrentItem;
|
||||
protected ListRow mCurrentRow;
|
||||
protected CompositeClickedListener mClickedListener = new CompositeClickedListener();
|
||||
protected CompositeSelectedListener mSelectedListener = new CompositeSelectedListener();
|
||||
protected ArrayObjectAdapter mRowsAdapter;
|
||||
private SimpleTarget<Bitmap> mBackgroundTarget;
|
||||
private DisplayMetrics mMetrics;
|
||||
private Timer mBackgroundTimer;
|
||||
private final Handler mHandler = new Handler();
|
||||
private String mBackgroundUrl;
|
||||
protected ArrayList<BrowseRowDef> mRows = new ArrayList<>();
|
||||
CardPresenter mCardPresenter;
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||
|
||||
View root = inflater.inflate(R.layout.fragment_custom_browse, container, false);
|
||||
|
||||
// Inject the RowsSupportFragment in the results container
|
||||
if (getChildFragmentManager().findFragmentById(R.id.rows_area) == null) {
|
||||
mRowsFragment = new RowsSupportFragment();
|
||||
getChildFragmentManager().beginTransaction()
|
||||
.replace(R.id.rows_area, mRowsFragment).commit();
|
||||
} else {
|
||||
mRowsFragment = (RowsSupportFragment) getChildFragmentManager()
|
||||
.findFragmentById(R.id.rows_area);
|
||||
}
|
||||
|
||||
mRowsAdapter = new ArrayObjectAdapter(new PositionableListRowPresenter());
|
||||
mRowsFragment.setAdapter(mRowsAdapter);
|
||||
|
||||
return root;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityCreated(Bundle savedInstanceState) {
|
||||
super.onActivityCreated(savedInstanceState);
|
||||
|
||||
mApplication = TvApp.getApplication();
|
||||
//mActivity = (BaseActivity) getActivity();
|
||||
|
||||
prepareBackgroundManager();
|
||||
|
||||
setupUIElements();
|
||||
|
||||
setupQueries(this);
|
||||
|
||||
setupEventListeners();
|
||||
}
|
||||
|
||||
protected void setupQueries(IRowLoader rowLoader) {
|
||||
rowLoader.loadRows(mRows);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
if (null != mBackgroundTimer) {
|
||||
mBackgroundTimer.cancel();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
|
||||
//Re-retrieve anything that needs it but delay slightly so we don't take away gui landing
|
||||
if (mRowsAdapter != null) {
|
||||
new Handler().postDelayed(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
for (int i = 0; i < mRowsAdapter.size(); i++) {
|
||||
if (mRowsAdapter.get(i) instanceof ListRow) {
|
||||
if (((ListRow) mRowsAdapter.get(i)).getAdapter() instanceof ItemRowAdapter) {
|
||||
((ItemRowAdapter) ((ListRow) mRowsAdapter.get(i)).getAdapter()).ReRetrieveIfNeeded();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},1500);
|
||||
}
|
||||
}
|
||||
|
||||
public void loadRows(List<BrowseRowDef> rows) {
|
||||
|
||||
mRowsAdapter = new ArrayObjectAdapter(new PositionableListRowPresenter());
|
||||
mCardPresenter = new CardPresenter();
|
||||
|
||||
for (BrowseRowDef def : rows) {
|
||||
HeaderItem header = new HeaderItem(def.getHeaderText());
|
||||
ItemRowAdapter rowAdapter;
|
||||
switch (def.getQueryType()) {
|
||||
case NextUp:
|
||||
rowAdapter = new ItemRowAdapter(def.getNextUpQuery(), true, mCardPresenter, mRowsAdapter);
|
||||
break;
|
||||
case Season:
|
||||
rowAdapter = new ItemRowAdapter(def.getSeasonQuery(), mCardPresenter, mRowsAdapter);
|
||||
break;
|
||||
case Upcoming:
|
||||
rowAdapter = new ItemRowAdapter(def.getUpcomingQuery(), mCardPresenter, mRowsAdapter);
|
||||
break;
|
||||
case Views:
|
||||
rowAdapter = new ItemRowAdapter(new ViewQuery(), mCardPresenter, mRowsAdapter);
|
||||
break;
|
||||
case SimilarSeries:
|
||||
rowAdapter = new ItemRowAdapter(def.getSimilarQuery(), QueryType.SimilarSeries, mCardPresenter, mRowsAdapter);
|
||||
break;
|
||||
case SimilarMovies:
|
||||
rowAdapter = new ItemRowAdapter(def.getSimilarQuery(), QueryType.SimilarMovies, mCardPresenter, mRowsAdapter);
|
||||
break;
|
||||
case Persons:
|
||||
rowAdapter = new ItemRowAdapter(def.getPersonsQuery(), def.getChunkSize(), mCardPresenter, mRowsAdapter);
|
||||
break;
|
||||
case LiveTvChannel:
|
||||
rowAdapter = new ItemRowAdapter(def.getTvChannelQuery(), 40, mCardPresenter, mRowsAdapter);
|
||||
break;
|
||||
case LiveTvProgram:
|
||||
rowAdapter = new ItemRowAdapter(def.getProgramQuery(), mCardPresenter, mRowsAdapter);
|
||||
break;
|
||||
case LiveTvRecording:
|
||||
rowAdapter = new ItemRowAdapter(def.getRecordingQuery(), def.getChunkSize(), mCardPresenter, mRowsAdapter);
|
||||
break;
|
||||
default:
|
||||
rowAdapter = new ItemRowAdapter(def.getQuery(), def.getChunkSize(), def.getPreferParentThumb(), def.isStaticHeight(), mCardPresenter, mRowsAdapter);
|
||||
break;
|
||||
}
|
||||
|
||||
rowAdapter.setReRetrieveTriggers(def.getChangeTriggers());
|
||||
|
||||
ListRow row = new ListRow(header, rowAdapter);
|
||||
mRowsAdapter.add(row);
|
||||
rowAdapter.setRow(row);
|
||||
rowAdapter.Retrieve();
|
||||
}
|
||||
|
||||
addAdditionalRows(mRowsAdapter);
|
||||
|
||||
mRowsFragment.setAdapter(mRowsAdapter);
|
||||
|
||||
}
|
||||
|
||||
protected void addAdditionalRows(ArrayObjectAdapter rowAdapter) {
|
||||
|
||||
}
|
||||
|
||||
private void prepareBackgroundManager() {
|
||||
|
||||
final BackgroundManager backgroundManager = BackgroundManager.getInstance(getActivity());
|
||||
backgroundManager.attach(getActivity().getWindow());
|
||||
|
||||
mMetrics = new DisplayMetrics();
|
||||
getActivity().getWindowManager().getDefaultDisplay().getMetrics(mMetrics);
|
||||
|
||||
mBackgroundTarget = new SimpleTarget<Bitmap>(mMetrics.widthPixels, mMetrics.heightPixels) {
|
||||
@Override
|
||||
public void onResourceReady(Bitmap resource, GlideAnimation<? super Bitmap> glideAnimation) {
|
||||
backgroundManager.setBitmap(resource);
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
protected void setupUIElements() {
|
||||
}
|
||||
|
||||
protected void setupEventListeners() {
|
||||
|
||||
mRowsFragment.setOnItemViewClickedListener(mClickedListener);
|
||||
mClickedListener.registerListener(new ItemViewClickedListener());
|
||||
|
||||
mRowsFragment.setOnItemViewSelectedListener(mSelectedListener);
|
||||
mSelectedListener.registerListener(new ItemViewSelectedListener());
|
||||
}
|
||||
|
||||
private final class ItemViewClickedListener implements OnItemViewClickedListener {
|
||||
@Override
|
||||
public void onItemClicked(final 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());
|
||||
}
|
||||
}
|
||||
|
||||
private final class ItemViewSelectedListener implements OnItemViewSelectedListener {
|
||||
@Override
|
||||
public void onItemSelected(Presenter.ViewHolder itemViewHolder, Object item,
|
||||
RowPresenter.ViewHolder rowViewHolder, Row row) {
|
||||
if (!(item instanceof BaseRowItem)) {
|
||||
//fill in default background
|
||||
mBackgroundUrl = null;
|
||||
startBackgroundTimer();
|
||||
return;
|
||||
}
|
||||
|
||||
BaseRowItem rowItem = (BaseRowItem) item;
|
||||
mCurrentItem = rowItem;
|
||||
mCurrentRow = (ListRow) row;
|
||||
|
||||
//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());
|
||||
|
||||
mBackgroundUrl = rowItem.getBackdropImageUrl();
|
||||
startBackgroundTimer();
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
protected void updateBackground(String url) {
|
||||
if (url == null) {
|
||||
clearBackground();
|
||||
} else {
|
||||
Glide.with(getActivity())
|
||||
.load(url)
|
||||
.asBitmap()
|
||||
.override(mMetrics.widthPixels, mMetrics.heightPixels)
|
||||
.centerCrop()
|
||||
.into(mBackgroundTarget);
|
||||
}
|
||||
}
|
||||
|
||||
protected void clearBackground() {
|
||||
BackgroundManager.getInstance(getActivity()).setDrawable(null);
|
||||
}
|
||||
|
||||
private void startBackgroundTimer() {
|
||||
if (null != mBackgroundTimer) {
|
||||
mBackgroundTimer.cancel();
|
||||
}
|
||||
mBackgroundTimer = new Timer();
|
||||
mBackgroundTimer.schedule(new UpdateBackgroundTask(), BACKGROUND_UPDATE_DELAY);
|
||||
}
|
||||
|
||||
private class UpdateBackgroundTask extends TimerTask {
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
mHandler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
updateBackground(mBackgroundUrl);
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package org.jellyfin.androidtv.browsing;
|
||||
|
||||
import android.os.Bundle;
|
||||
|
||||
import org.jellyfin.androidtv.TvApp;
|
||||
|
||||
/**
|
||||
* Created by Eric on 1/18/2015.
|
||||
*/
|
||||
public class CustomViewFragment extends BrowseFolderFragment {
|
||||
protected String includeType;
|
||||
|
||||
@Override
|
||||
public void onActivityCreated(Bundle savedInstanceState) {
|
||||
includeType = getActivity().getIntent().getStringExtra("IncludeType");
|
||||
TvApp.getApplication().getLogger().Debug("Item type: "+includeType);
|
||||
showViews = false;
|
||||
|
||||
super.onActivityCreated(savedInstanceState);
|
||||
}
|
||||
}
|
||||
@@ -1,19 +1,14 @@
|
||||
package org.jellyfin.androidtv.ui.browsing;
|
||||
package org.jellyfin.androidtv.browsing;
|
||||
|
||||
import androidx.fragment.app.Fragment;
|
||||
import android.content.Intent;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.Typeface;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentActivity;
|
||||
import androidx.leanback.app.BackgroundManager;
|
||||
import androidx.leanback.app.RowsSupportFragment;
|
||||
import androidx.leanback.widget.ArrayObjectAdapter;
|
||||
import androidx.leanback.widget.ClassPresenterSelector;
|
||||
@@ -24,56 +19,63 @@ import androidx.leanback.widget.OnItemViewSelectedListener;
|
||||
import androidx.leanback.widget.Presenter;
|
||||
import androidx.leanback.widget.Row;
|
||||
import androidx.leanback.widget.RowPresenter;
|
||||
import android.util.DisplayMetrics;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.bumptech.glide.Glide;
|
||||
import com.bumptech.glide.request.animation.GlideAnimation;
|
||||
import com.bumptech.glide.request.target.SimpleTarget;
|
||||
|
||||
import org.jellyfin.androidtv.R;
|
||||
import org.jellyfin.androidtv.TvApp;
|
||||
import org.jellyfin.androidtv.constant.CustomMessage;
|
||||
import org.jellyfin.androidtv.constant.Extras;
|
||||
import org.jellyfin.androidtv.constant.QueryType;
|
||||
import org.jellyfin.androidtv.data.model.DataRefreshService;
|
||||
import org.jellyfin.androidtv.data.querying.ViewQuery;
|
||||
import org.jellyfin.androidtv.data.service.BackgroundService;
|
||||
import org.jellyfin.androidtv.base.BaseActivity;
|
||||
import org.jellyfin.androidtv.base.CustomMessage;
|
||||
import org.jellyfin.androidtv.base.IKeyListener;
|
||||
import org.jellyfin.androidtv.base.IMessageListener;
|
||||
import org.jellyfin.androidtv.details.ItemListActivity;
|
||||
import org.jellyfin.androidtv.itemhandling.BaseRowItem;
|
||||
import org.jellyfin.androidtv.itemhandling.ItemLauncher;
|
||||
import org.jellyfin.androidtv.itemhandling.ItemRowAdapter;
|
||||
import org.jellyfin.androidtv.playback.MediaManager;
|
||||
import org.jellyfin.androidtv.presentation.CardPresenter;
|
||||
import org.jellyfin.androidtv.presentation.GridButtonPresenter;
|
||||
import org.jellyfin.androidtv.presentation.PositionableListRowPresenter;
|
||||
import org.jellyfin.androidtv.querying.QueryType;
|
||||
import org.jellyfin.androidtv.querying.ViewQuery;
|
||||
import org.jellyfin.androidtv.ui.GridButton;
|
||||
import org.jellyfin.androidtv.ui.itemdetail.ItemListActivity;
|
||||
import org.jellyfin.androidtv.ui.itemhandling.BaseRowItem;
|
||||
import org.jellyfin.androidtv.ui.itemhandling.ItemLauncher;
|
||||
import org.jellyfin.androidtv.ui.itemhandling.ItemRowAdapter;
|
||||
import org.jellyfin.androidtv.ui.livetv.LiveTvGuideActivity;
|
||||
import org.jellyfin.androidtv.ui.playback.MediaManager;
|
||||
import org.jellyfin.androidtv.ui.presentation.CardPresenter;
|
||||
import org.jellyfin.androidtv.ui.presentation.GridButtonPresenter;
|
||||
import org.jellyfin.androidtv.ui.presentation.PositionableListRowPresenter;
|
||||
import org.jellyfin.androidtv.ui.search.SearchActivity;
|
||||
import org.jellyfin.androidtv.ui.shared.BaseActivity;
|
||||
import org.jellyfin.androidtv.ui.shared.IKeyListener;
|
||||
import org.jellyfin.androidtv.ui.shared.IMessageListener;
|
||||
import org.jellyfin.androidtv.util.InfoLayoutHelper;
|
||||
import org.jellyfin.androidtv.util.KeyProcessor;
|
||||
import org.jellyfin.androidtv.util.TextUtilsKt;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Timer;
|
||||
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;
|
||||
import org.jellyfin.apiclient.serialization.GsonJsonSerializer;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import kotlin.Lazy;
|
||||
|
||||
import static org.koin.java.KoinJavaComponent.get;
|
||||
import static org.koin.java.KoinJavaComponent.inject;
|
||||
|
||||
public class EnhancedBrowseFragment extends Fragment implements IRowLoader {
|
||||
protected FragmentActivity mActivity;
|
||||
private static final int BACKGROUND_UPDATE_DELAY = 100;
|
||||
Typeface roboto;
|
||||
BaseActivity mActivity;
|
||||
TvApp mApplication;
|
||||
|
||||
protected TextView mTitle;
|
||||
private LinearLayout mInfoRow;
|
||||
private TextView mSummary;
|
||||
TextView mTitle;
|
||||
LinearLayout mInfoRow;
|
||||
TextView mSummary;
|
||||
|
||||
protected static final int BY_LETTER = 0;
|
||||
protected static final int GENRES = 1;
|
||||
protected static final int YEARS = 2;
|
||||
protected static final int PERSONS = 3;
|
||||
protected static final int SUGGESTED = 4;
|
||||
protected static final int SEARCH = 5;
|
||||
@@ -87,22 +89,25 @@ public class EnhancedBrowseFragment extends Fragment implements IRowLoader {
|
||||
protected String itemTypeString;
|
||||
protected boolean showViews = true;
|
||||
protected boolean justLoaded = true;
|
||||
protected boolean ShowFanart = false;
|
||||
|
||||
protected BaseRowItem favSongsRowItem;
|
||||
|
||||
protected RowsSupportFragment mRowsFragment;
|
||||
private SimpleTarget<Bitmap> mBackgroundTarget;
|
||||
private DisplayMetrics mMetrics;
|
||||
|
||||
RowsSupportFragment mRowsFragment;
|
||||
protected CompositeClickedListener mClickedListener = new CompositeClickedListener();
|
||||
protected CompositeSelectedListener mSelectedListener = new CompositeSelectedListener();
|
||||
protected ArrayObjectAdapter mRowsAdapter;
|
||||
private Timer mBackgroundTimer;
|
||||
private final Handler mHandler = new Handler();
|
||||
private String mBackgroundUrl;
|
||||
protected ArrayList<BrowseRowDef> mRows = new ArrayList<>();
|
||||
protected CardPresenter mCardPresenter;
|
||||
CardPresenter mCardPresenter;
|
||||
protected BaseRowItem mCurrentItem;
|
||||
protected ListRow mCurrentRow;
|
||||
|
||||
private Lazy<GsonJsonSerializer> serializer = inject(GsonJsonSerializer.class);
|
||||
private Lazy<BackgroundService> backgroundService = inject(BackgroundService.class);
|
||||
private Lazy<MediaManager> mediaManager = inject(MediaManager.class);
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
@@ -117,19 +122,24 @@ public class EnhancedBrowseFragment extends Fragment implements IRowLoader {
|
||||
@Nullable
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||
|
||||
View root = inflater.inflate(R.layout.enhanced_detail_browse, container, false);
|
||||
|
||||
mActivity = getActivity();
|
||||
mActivity = (BaseActivity) getActivity();
|
||||
roboto = TvApp.getApplication().getDefaultFont();
|
||||
|
||||
mTitle = (TextView) root.findViewById(R.id.title);
|
||||
mTitle.setTypeface(roboto);
|
||||
mTitle.setShadowLayer(5, 5, 5, Color.BLACK);
|
||||
mInfoRow = (LinearLayout) root.findViewById(R.id.infoRow);
|
||||
mSummary = (TextView) root.findViewById(R.id.summary);
|
||||
mSummary.setTypeface(roboto);
|
||||
mSummary.setShadowLayer(5, 5, 5, Color.BLACK);
|
||||
|
||||
// Inject the RowsSupportFragment in the results container
|
||||
if (getChildFragmentManager().findFragmentById(R.id.rowsFragment) == null) {
|
||||
mRowsFragment = new RowsSupportFragment();
|
||||
getChildFragmentManager()
|
||||
.beginTransaction()
|
||||
getChildFragmentManager().beginTransaction()
|
||||
.replace(R.id.rowsFragment, mRowsFragment).commit();
|
||||
} else {
|
||||
mRowsFragment = (RowsSupportFragment) getChildFragmentManager()
|
||||
@@ -140,16 +150,23 @@ public class EnhancedBrowseFragment extends Fragment implements IRowLoader {
|
||||
mRowsFragment.setAdapter(mRowsAdapter);
|
||||
|
||||
return root;
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityCreated(Bundle savedInstanceState) {
|
||||
super.onActivityCreated(savedInstanceState);
|
||||
|
||||
backgroundService.getValue().attach(requireActivity());
|
||||
mApplication = TvApp.getApplication();
|
||||
|
||||
prepareBackgroundManager();
|
||||
|
||||
setupViews();
|
||||
|
||||
setupUIElements();
|
||||
|
||||
setupQueries(this);
|
||||
|
||||
setupEventListeners();
|
||||
}
|
||||
|
||||
@@ -158,7 +175,7 @@ public class EnhancedBrowseFragment extends Fragment implements IRowLoader {
|
||||
}
|
||||
|
||||
protected void setupViews() {
|
||||
mFolder = serializer.getValue().DeserializeFromString(requireActivity().getIntent().getStringExtra(Extras.Folder), BaseItemDto.class);
|
||||
mFolder = TvApp.getApplication().getSerializer().DeserializeFromString(getActivity().getIntent().getStringExtra("Folder"),BaseItemDto.class);
|
||||
if (mFolder == null) return;
|
||||
|
||||
if (mFolder.getCollectionType() != null) {
|
||||
@@ -181,21 +198,31 @@ public class EnhancedBrowseFragment extends Fragment implements IRowLoader {
|
||||
} else {
|
||||
showViews = false;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
if (null != mBackgroundTimer) {
|
||||
mBackgroundTimer.cancel();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
|
||||
// React to deletion
|
||||
DataRefreshService dataRefreshService = get(DataRefreshService.class);
|
||||
if (getActivity() != null && !getActivity().isFinishing() && mCurrentRow != null && mCurrentItem != null && mCurrentItem.getItemId() != null && mCurrentItem.getItemId().equals(dataRefreshService.getLastDeletedItemId())) {
|
||||
((ItemRowAdapter) mCurrentRow.getAdapter()).remove(mCurrentItem);
|
||||
dataRefreshService.setLastDeletedItemId(null);
|
||||
ShowFanart = mApplication.getPrefs().getBoolean("pref_show_backdrop", true);
|
||||
|
||||
//React to deletion
|
||||
if (getActivity() != null && !getActivity().isFinishing() && mCurrentRow != null && mCurrentItem != null && mCurrentItem.getItemId() != null && mCurrentItem.getItemId().equals(TvApp.getApplication().getLastDeletedItemId())) {
|
||||
((ItemRowAdapter)mCurrentRow.getAdapter()).remove(mCurrentItem);
|
||||
TvApp.getApplication().setLastDeletedItemId(null);
|
||||
}
|
||||
|
||||
if (!justLoaded) {
|
||||
// Re-retrieve anything that needs it but delay slightly so we don't take away gui landing
|
||||
//Re-retrieve anything that needs it but delay slightly so we don't take away gui landing
|
||||
if (mRowsAdapter != null) {
|
||||
refreshCurrentItem();
|
||||
new Handler().postDelayed(new Runnable() {
|
||||
@@ -212,18 +239,21 @@ public class EnhancedBrowseFragment extends Fragment implements IRowLoader {
|
||||
}
|
||||
}, 1500);
|
||||
}
|
||||
|
||||
} else {
|
||||
justLoaded = false;
|
||||
}
|
||||
}
|
||||
|
||||
public void loadRows(List<BrowseRowDef> rows) {
|
||||
|
||||
mRowsAdapter = new ArrayObjectAdapter(new PositionableListRowPresenter());
|
||||
mCardPresenter = new CardPresenter(false, 280);
|
||||
ClassPresenterSelector ps = new ClassPresenterSelector();
|
||||
ps.addClassPresenter(BaseRowItem.class, mCardPresenter);
|
||||
ps.addClassPresenter(GridButton.class, new GridButtonPresenter(false, 310, 280));
|
||||
|
||||
|
||||
for (BrowseRowDef def : rows) {
|
||||
HeaderItem header = new HeaderItem(def.getHeaderText());
|
||||
ItemRowAdapter rowAdapter;
|
||||
@@ -286,64 +316,69 @@ public class EnhancedBrowseFragment extends Fragment implements IRowLoader {
|
||||
addAdditionalRows(mRowsAdapter);
|
||||
|
||||
mRowsFragment.setAdapter(mRowsAdapter);
|
||||
|
||||
}
|
||||
|
||||
protected void addAdditionalRows(ArrayObjectAdapter rowAdapter) {
|
||||
if (!showViews) return;
|
||||
if (showViews) {
|
||||
HeaderItem gridHeader = new HeaderItem(rowAdapter.size(), mApplication.getString(R.string.lbl_views));
|
||||
|
||||
HeaderItem gridHeader = new HeaderItem(rowAdapter.size(), TvApp.getApplication().getString(R.string.lbl_views));
|
||||
GridButtonPresenter mGridPresenter = new GridButtonPresenter();
|
||||
ArrayObjectAdapter gridRowAdapter = new ArrayObjectAdapter(mGridPresenter);
|
||||
GridButtonPresenter mGridPresenter = new GridButtonPresenter();
|
||||
ArrayObjectAdapter gridRowAdapter = new ArrayObjectAdapter(mGridPresenter);
|
||||
switch (itemTypeString) {
|
||||
case "Movie":
|
||||
gridRowAdapter.add(new GridButton(SUGGESTED, mApplication.getString(R.string.lbl_suggested), R.drawable.tile_suggestions));
|
||||
addStandardViewButtons(gridRowAdapter);
|
||||
break;
|
||||
case "MusicAlbum":
|
||||
gridRowAdapter.add(new GridButton(ALBUMS, TvApp.getApplication().getString(R.string.lbl_albums), R.drawable.tile_audio));
|
||||
gridRowAdapter.add(new GridButton(ARTISTS, TvApp.getApplication().getString(R.string.lbl_artists), R.drawable.tile_artists));
|
||||
gridRowAdapter.add(new GridButton(GENRES, mApplication.getString(R.string.lbl_genres), R.drawable.tile_genres));
|
||||
gridRowAdapter.add(new GridButton(SEARCH, mApplication.getString(R.string.lbl_search), R.drawable.tile_search));
|
||||
break;
|
||||
default:
|
||||
addStandardViewButtons(gridRowAdapter);
|
||||
break;
|
||||
}
|
||||
rowAdapter.add(new ListRow(gridHeader, gridRowAdapter));
|
||||
|
||||
switch (itemTypeString) {
|
||||
case "Movie":
|
||||
gridRowAdapter.add(new GridButton(SUGGESTED, TvApp.getApplication().getString(R.string.lbl_suggested), R.drawable.tile_suggestions, null));
|
||||
addStandardViewButtons(gridRowAdapter);
|
||||
break;
|
||||
|
||||
case "MusicAlbum":
|
||||
gridRowAdapter.add(new GridButton(ALBUMS, TvApp.getApplication().getString(R.string.lbl_albums), R.drawable.tile_audio, null));
|
||||
gridRowAdapter.add(new GridButton(ARTISTS, TvApp.getApplication().getString(R.string.lbl_artists), R.drawable.tile_artists, null));
|
||||
gridRowAdapter.add(new GridButton(GENRES, TvApp.getApplication().getString(R.string.lbl_genres), R.drawable.tile_genres, null));
|
||||
gridRowAdapter.add(new GridButton(SEARCH, TvApp.getApplication().getString(R.string.lbl_search), R.drawable.tile_search, null));
|
||||
break;
|
||||
|
||||
default:
|
||||
addStandardViewButtons(gridRowAdapter);
|
||||
break;
|
||||
}
|
||||
|
||||
rowAdapter.add(new ListRow(gridHeader, gridRowAdapter));
|
||||
}
|
||||
|
||||
protected void addStandardViewButtons(ArrayObjectAdapter gridRowAdapter) {
|
||||
gridRowAdapter.add(new GridButton(GRID, TvApp.getApplication().getString(R.string.lbl_all_items), R.drawable.tile_port_grid, null));
|
||||
gridRowAdapter.add(new GridButton(BY_LETTER, TvApp.getApplication().getString(R.string.lbl_by_letter), R.drawable.tile_letters, null));
|
||||
gridRowAdapter.add(new GridButton(GENRES, TvApp.getApplication().getString(R.string.lbl_genres), R.drawable.tile_genres, null));
|
||||
// Disabled because the screen doesn't behave properly
|
||||
// gridRowAdapter.add(new GridButton(PERSONS, TvApp.getApplication().getString(R.string.lbl_performers), R.drawable.tile_actors));
|
||||
gridRowAdapter.add(new GridButton(SEARCH, TvApp.getApplication().getString(R.string.lbl_search), R.drawable.tile_search, null));
|
||||
gridRowAdapter.add(new GridButton(GRID, TvApp.getApplication().getString(R.string.lbl_all_items), R.drawable.tile_port_grid));
|
||||
gridRowAdapter.add(new GridButton(BY_LETTER, mApplication.getString(R.string.lbl_by_letter), R.drawable.tile_letters));
|
||||
gridRowAdapter.add(new GridButton(GENRES, mApplication.getString(R.string.lbl_genres), R.drawable.tile_genres));
|
||||
//gridRowAdapter.add(new GridButton(PERSONS, mApplication.getString(R.string.lbl_performers), R.drawable.tile_actors));
|
||||
gridRowAdapter.add(new GridButton(SEARCH, mApplication.getString(R.string.lbl_search), R.drawable.tile_search));
|
||||
|
||||
}
|
||||
|
||||
protected void setupUIElements() {
|
||||
}
|
||||
|
||||
protected void setupEventListeners() {
|
||||
|
||||
mRowsFragment.setOnItemViewClickedListener(mClickedListener);
|
||||
mClickedListener.registerListener(new ItemViewClickedListener());
|
||||
if (showViews) mClickedListener.registerListener(new SpecialViewClickedListener());
|
||||
|
||||
mRowsFragment.setOnItemViewSelectedListener(mSelectedListener);
|
||||
mSelectedListener.registerListener(new ItemViewSelectedListener());
|
||||
if (mActivity != null && mActivity instanceof BaseActivity) {
|
||||
((BaseActivity) mActivity).registerKeyListener(new IKeyListener() {
|
||||
if (mActivity != null) {
|
||||
mActivity.registerKeyListener(new IKeyListener() {
|
||||
@Override
|
||||
public boolean onKeyUp(int key, KeyEvent event) {
|
||||
return KeyProcessor.HandleKey(key, mCurrentItem, ((BaseActivity) mActivity));
|
||||
return KeyProcessor.HandleKey(key, mCurrentItem, mActivity);
|
||||
}
|
||||
});
|
||||
|
||||
((BaseActivity) mActivity).registerMessageListener(new IMessageListener() {
|
||||
mActivity.registerMessageListener(new IMessageListener() {
|
||||
@Override
|
||||
public void onMessageReceived(CustomMessage message) {
|
||||
switch (message) {
|
||||
|
||||
case RefreshCurrentItem:
|
||||
refreshCurrentItem();
|
||||
break;
|
||||
@@ -354,12 +389,8 @@ public class EnhancedBrowseFragment extends Fragment implements IRowLoader {
|
||||
}
|
||||
|
||||
private void refreshCurrentItem() {
|
||||
if (mCurrentItem != null &&
|
||||
mCurrentItem.getBaseItemType() != BaseItemType.Photo &&
|
||||
mCurrentItem.getBaseItemType() != BaseItemType.MusicArtist &&
|
||||
mCurrentItem.getBaseItemType() != BaseItemType.MusicAlbum &&
|
||||
mCurrentItem.getBaseItemType() != BaseItemType.Playlist
|
||||
) {
|
||||
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() {
|
||||
@@ -367,12 +398,16 @@ public class EnhancedBrowseFragment extends Fragment implements IRowLoader {
|
||||
adapter.notifyArrayItemRangeChanged(adapter.indexOf(mCurrentItem), 1);
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private final class SpecialViewClickedListener implements OnItemViewClickedListener {
|
||||
@Override
|
||||
public void onItemClicked(Presenter.ViewHolder itemViewHolder, Object item, RowPresenter.ViewHolder rowViewHolder, Row row) {
|
||||
public void onItemClicked(Presenter.ViewHolder itemViewHolder, Object item,
|
||||
RowPresenter.ViewHolder rowViewHolder, Row row) {
|
||||
|
||||
if (item instanceof GridButton) {
|
||||
switch (((GridButton) item).getId()) {
|
||||
case GRID:
|
||||
@@ -380,75 +415,72 @@ public class EnhancedBrowseFragment extends Fragment implements IRowLoader {
|
||||
@Override
|
||||
public void onResponse(DisplayPreferences response) {
|
||||
Intent folderIntent = new Intent(getActivity(), GenericGridActivity.class);
|
||||
folderIntent.putExtra(Extras.Folder, serializer.getValue().SerializeToString(mFolder));
|
||||
requireActivity().startActivity(folderIntent);
|
||||
folderIntent.putExtra("Folder", TvApp.getApplication().getSerializer().SerializeToString(mFolder));
|
||||
getActivity().startActivity(folderIntent);
|
||||
}
|
||||
});
|
||||
break;
|
||||
|
||||
case ALBUMS:
|
||||
mFolder.setDisplayPreferencesId(mFolder.getId() + "AL");
|
||||
mFolder.setDisplayPreferencesId(mFolder.getId()+"AL");
|
||||
TvApp.getApplication().getDisplayPrefsAsync(mFolder.getDisplayPreferencesId(), new Response<DisplayPreferences>() {
|
||||
@Override
|
||||
public void onResponse(DisplayPreferences response) {
|
||||
Intent folderIntent = new Intent(getActivity(), GenericGridActivity.class);
|
||||
folderIntent.putExtra(Extras.Folder, serializer.getValue().SerializeToString(mFolder));
|
||||
folderIntent.putExtra(Extras.IncludeType, "MusicAlbum");
|
||||
requireActivity().startActivity(folderIntent);
|
||||
folderIntent.putExtra("Folder", TvApp.getApplication().getSerializer().SerializeToString(mFolder));
|
||||
folderIntent.putExtra("IncludeType", "MusicAlbum");
|
||||
getActivity().startActivity(folderIntent);
|
||||
}
|
||||
});
|
||||
break;
|
||||
|
||||
case ARTISTS:
|
||||
mFolder.setDisplayPreferencesId(mFolder.getId() + "AR");
|
||||
mFolder.setDisplayPreferencesId(mFolder.getId()+"AR");
|
||||
TvApp.getApplication().getDisplayPrefsAsync(mFolder.getDisplayPreferencesId(), new Response<DisplayPreferences>() {
|
||||
@Override
|
||||
public void onResponse(DisplayPreferences response) {
|
||||
Intent folderIntent = new Intent(getActivity(), GenericGridActivity.class);
|
||||
folderIntent.putExtra(Extras.Folder, serializer.getValue().SerializeToString(mFolder));
|
||||
folderIntent.putExtra(Extras.IncludeType, "AlbumArtist");
|
||||
requireActivity().startActivity(folderIntent);
|
||||
folderIntent.putExtra("Folder", TvApp.getApplication().getSerializer().SerializeToString(mFolder));
|
||||
folderIntent.putExtra("IncludeType", "AlbumArtist");
|
||||
getActivity().startActivity(folderIntent);
|
||||
}
|
||||
});
|
||||
break;
|
||||
|
||||
case BY_LETTER:
|
||||
Intent intent = new Intent(getActivity(), ByLetterActivity.class);
|
||||
intent.putExtra(Extras.Folder, serializer.getValue().SerializeToString(mFolder));
|
||||
intent.putExtra(Extras.IncludeType, itemTypeString);
|
||||
intent.putExtra("Folder", TvApp.getApplication().getSerializer().SerializeToString(mFolder));
|
||||
intent.putExtra("IncludeType", itemTypeString);
|
||||
|
||||
requireActivity().startActivity(intent);
|
||||
getActivity().startActivity(intent);
|
||||
break;
|
||||
|
||||
case GENRES:
|
||||
Intent genreIntent = new Intent(getActivity(), ByGenreActivity.class);
|
||||
genreIntent.putExtra(Extras.Folder, serializer.getValue().SerializeToString(mFolder));
|
||||
genreIntent.putExtra(Extras.IncludeType, itemTypeString);
|
||||
genreIntent.putExtra("Folder", TvApp.getApplication().getSerializer().SerializeToString(mFolder));
|
||||
genreIntent.putExtra("IncludeType", itemTypeString);
|
||||
|
||||
requireActivity().startActivity(genreIntent);
|
||||
getActivity().startActivity(genreIntent);
|
||||
break;
|
||||
|
||||
case SUGGESTED:
|
||||
Intent suggIntent = new Intent(getActivity(), SuggestedMoviesActivity.class);
|
||||
suggIntent.putExtra(Extras.Folder, serializer.getValue().SerializeToString(mFolder));
|
||||
suggIntent.putExtra(Extras.IncludeType, itemTypeString);
|
||||
suggIntent.putExtra("Folder", TvApp.getApplication().getSerializer().SerializeToString(mFolder));
|
||||
suggIntent.putExtra("IncludeType", itemTypeString);
|
||||
|
||||
requireActivity().startActivity(suggIntent);
|
||||
getActivity().startActivity(suggIntent);
|
||||
break;
|
||||
|
||||
case PERSONS:
|
||||
Intent personIntent = new Intent(getActivity(), BrowsePersonsActivity.class);
|
||||
personIntent.putExtra(Extras.Folder, serializer.getValue().SerializeToString(mFolder));
|
||||
personIntent.putExtra(Extras.IncludeType, itemTypeString);
|
||||
personIntent.putExtra("Folder", TvApp.getApplication().getSerializer().SerializeToString(mFolder));
|
||||
personIntent.putExtra("IncludeType", itemTypeString);
|
||||
|
||||
requireActivity().startActivity(personIntent);
|
||||
getActivity().startActivity(personIntent);
|
||||
break;
|
||||
|
||||
case SEARCH:
|
||||
Intent searchIntent = new Intent(getActivity(), SearchActivity.class);
|
||||
searchIntent.putExtra("MusicOnly", "MusicAlbum".equals(itemTypeString));
|
||||
|
||||
startActivity(searchIntent);
|
||||
TvApp.getApplication().showSearch(getActivity(), "MusicAlbum".equals(itemTypeString));
|
||||
break;
|
||||
|
||||
case FAVSONGS:
|
||||
@@ -456,7 +488,7 @@ public class EnhancedBrowseFragment extends Fragment implements IRowLoader {
|
||||
favIntent.putExtra("ItemId", ItemListActivity.FAV_SONGS);
|
||||
favIntent.putExtra("ParentId", mFolder.getId());
|
||||
|
||||
requireActivity().startActivity(favIntent);
|
||||
getActivity().startActivity(favIntent);
|
||||
break;
|
||||
|
||||
case SERIES:
|
||||
@@ -466,15 +498,15 @@ public class EnhancedBrowseFragment extends Fragment implements IRowLoader {
|
||||
seriesTimers.setId("SERIESTIMERS");
|
||||
seriesTimers.setCollectionType("SeriesTimers");
|
||||
seriesTimers.setName(mActivity.getString(R.string.lbl_series_recordings));
|
||||
seriesIntent.putExtra(Extras.Folder, serializer.getValue().SerializeToString(seriesTimers));
|
||||
seriesIntent.putExtra("Folder", TvApp.getApplication().getSerializer().SerializeToString(seriesTimers));
|
||||
|
||||
requireActivity().startActivity(seriesIntent);
|
||||
getActivity().startActivity(seriesIntent);
|
||||
break;
|
||||
|
||||
case SCHEDULE:
|
||||
case TvApp.LIVE_TV_SCHEDULE_OPTION_ID:
|
||||
Intent schedIntent = new Intent(mActivity, BrowseScheduleActivity.class);
|
||||
requireActivity().startActivity(schedIntent);
|
||||
getActivity().startActivity(schedIntent);
|
||||
break;
|
||||
|
||||
case TvApp.LIVE_TV_RECORDINGS_OPTION_ID:
|
||||
@@ -482,30 +514,26 @@ public class EnhancedBrowseFragment extends Fragment implements IRowLoader {
|
||||
BaseItemDto folder = new BaseItemDto();
|
||||
folder.setId("");
|
||||
folder.setName(TvApp.getApplication().getResources().getString(R.string.lbl_recorded_tv));
|
||||
recordings.putExtra(Extras.Folder, serializer.getValue().SerializeToString(folder));
|
||||
recordings.putExtra("Folder", TvApp.getApplication().getSerializer().SerializeToString(folder));
|
||||
mActivity.startActivity(recordings);
|
||||
break;
|
||||
|
||||
case TvApp.LIVE_TV_GUIDE_OPTION_ID:
|
||||
Intent guide = new Intent(mActivity, LiveTvGuideActivity.class);
|
||||
mActivity.startActivity(guide);
|
||||
break;
|
||||
|
||||
default:
|
||||
Toast.makeText(getActivity(), item.toString() + TvApp.getApplication().getString(R.string.msg_not_implemented), Toast.LENGTH_SHORT).show();
|
||||
Toast.makeText(getActivity(), item.toString() + mApplication.getString(R.string.msg_not_implemented), Toast.LENGTH_SHORT)
|
||||
.show();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private final class ItemViewClickedListener implements OnItemViewClickedListener {
|
||||
@Override
|
||||
public void onItemClicked(final Presenter.ViewHolder itemViewHolder, Object item, RowPresenter.ViewHolder rowViewHolder, Row row) {
|
||||
if (!(item instanceof BaseRowItem)) return;
|
||||
public void onItemClicked(final Presenter.ViewHolder itemViewHolder, Object item,
|
||||
RowPresenter.ViewHolder rowViewHolder, Row row) {
|
||||
|
||||
mediaManager.getValue().setCurrentMediaTitle(row.getHeaderItem().getName());
|
||||
ItemLauncher.launch((BaseRowItem) item, (ItemRowAdapter) ((ListRow) row).getAdapter(), ((BaseRowItem) item).getIndex(), getActivity());
|
||||
if (!(item instanceof BaseRowItem)) return;
|
||||
MediaManager.setCurrentMediaTitle(row.getHeaderItem().getName());
|
||||
ItemLauncher.launch((BaseRowItem) item, (ItemRowAdapter) ((ListRow)row).getAdapter(), ((BaseRowItem)item).getIndex(), getActivity());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -513,8 +541,9 @@ public class EnhancedBrowseFragment extends Fragment implements IRowLoader {
|
||||
@Override
|
||||
public void onItemSelected(Presenter.ViewHolder itemViewHolder, Object item,
|
||||
RowPresenter.ViewHolder rowViewHolder, Row row) {
|
||||
if (item instanceof GridButton && ((GridButton) item).getId() == FAVSONGS) {
|
||||
// Set to specialized item
|
||||
mHandler.removeCallbacks(updateContentTask);
|
||||
if (item instanceof GridButton && ((GridButton)item).getId() == FAVSONGS) {
|
||||
//set to specialized item
|
||||
mCurrentItem = favSongsRowItem;
|
||||
}
|
||||
|
||||
@@ -522,8 +551,9 @@ public class EnhancedBrowseFragment extends Fragment implements IRowLoader {
|
||||
mTitle.setText(mFolder != null ? mFolder.getName() : "");
|
||||
mInfoRow.removeAllViews();
|
||||
mSummary.setText("");
|
||||
// Fill in default background
|
||||
backgroundService.getValue().clearBackgrounds();
|
||||
//fill in default background
|
||||
mBackgroundUrl = null;
|
||||
startBackgroundTimer();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -531,20 +561,87 @@ public class EnhancedBrowseFragment extends Fragment implements IRowLoader {
|
||||
|
||||
mCurrentItem = rowItem;
|
||||
mCurrentRow = (ListRow) row;
|
||||
mTitle.setText(mCurrentItem.getName());
|
||||
mInfoRow.removeAllViews();
|
||||
mSummary.setText("");
|
||||
mHandler.postDelayed(updateContentTask, 500);
|
||||
|
||||
mTitle.setText(rowItem.getName(requireContext()));
|
||||
|
||||
String summary = rowItem.getSummary(requireContext());
|
||||
if (summary != null) mSummary.setText(TextUtilsKt.toHtmlSpanned(summary));
|
||||
else mSummary.setText(null);
|
||||
|
||||
InfoLayoutHelper.addInfoRow(mActivity, rowItem, mInfoRow, true, true);
|
||||
|
||||
ItemRowAdapter adapter = (ItemRowAdapter) ((ListRow) row).getAdapter();
|
||||
//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());
|
||||
|
||||
backgroundService.getValue().setBackground(rowItem.getBaseItem());
|
||||
if (ShowFanart) {
|
||||
mBackgroundUrl = rowItem.getBackdropImageUrl();
|
||||
startBackgroundTimer();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
protected Runnable updateContentTask = new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (mCurrentItem == null) return;
|
||||
mTitle.setText(mCurrentItem.getName());
|
||||
mSummary.setText(mCurrentItem.getSummary());
|
||||
InfoLayoutHelper.addInfoRow(mActivity, mCurrentItem, mInfoRow, true, true);
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
private void prepareBackgroundManager() {
|
||||
final BackgroundManager backgroundManager = BackgroundManager.getInstance(getActivity());
|
||||
backgroundManager.attach(getActivity().getWindow());
|
||||
|
||||
mMetrics = new DisplayMetrics();
|
||||
getActivity().getWindowManager().getDefaultDisplay().getMetrics(mMetrics);
|
||||
|
||||
mBackgroundTarget = new SimpleTarget<Bitmap>(mMetrics.widthPixels, mMetrics.heightPixels) {
|
||||
@Override
|
||||
public void onResourceReady(Bitmap resource, GlideAnimation<? super Bitmap> glideAnimation) {
|
||||
backgroundManager.setBitmap(resource);
|
||||
TvApp.getApplication().setCurrentBackground(resource);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
protected void updateBackground(String url) {
|
||||
if (url == null) {
|
||||
clearBackground();
|
||||
} else {
|
||||
Glide.with(getActivity())
|
||||
.load(url)
|
||||
.asBitmap()
|
||||
.override(mMetrics.widthPixels, mMetrics.heightPixels)
|
||||
.centerCrop()
|
||||
.into(mBackgroundTarget);
|
||||
}
|
||||
}
|
||||
|
||||
protected void clearBackground() {
|
||||
BackgroundManager.getInstance(getActivity()).setDrawable(null);
|
||||
}
|
||||
|
||||
private void startBackgroundTimer() {
|
||||
if (null != mBackgroundTimer) {
|
||||
mBackgroundTimer.cancel();
|
||||
}
|
||||
mBackgroundTimer = new Timer();
|
||||
mBackgroundTimer.schedule(new UpdateBackgroundTask(), BACKGROUND_UPDATE_DELAY);
|
||||
}
|
||||
|
||||
private class UpdateBackgroundTask extends TimerTask {
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
mHandler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
updateBackground(mBackgroundUrl);
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package org.jellyfin.androidtv.browsing;
|
||||
|
||||
import android.os.Bundle;
|
||||
|
||||
import org.jellyfin.androidtv.R;
|
||||
import org.jellyfin.androidtv.base.BaseActivity;
|
||||
|
||||
public class GenericFolderActivity extends BaseActivity {
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_generic_folder);
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,13 @@
|
||||
package org.jellyfin.androidtv.ui.browsing;
|
||||
package org.jellyfin.androidtv.browsing;
|
||||
|
||||
import android.os.Bundle;
|
||||
|
||||
import org.jellyfin.androidtv.R;
|
||||
import org.jellyfin.androidtv.TvApp;
|
||||
import org.jellyfin.androidtv.data.querying.StdItemQuery;
|
||||
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;
|
||||
@@ -13,8 +16,6 @@ import org.jellyfin.apiclient.model.querying.ItemFields;
|
||||
import org.jellyfin.apiclient.model.querying.ItemFilter;
|
||||
import org.jellyfin.apiclient.model.querying.ItemSortBy;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
public class GenericFolderFragment extends EnhancedBrowseFragment {
|
||||
|
||||
@Override
|
||||
@@ -32,11 +33,8 @@ public class GenericFolderFragment extends EnhancedBrowseFragment {
|
||||
RecordingQuery query = new RecordingQuery();
|
||||
query.setUserId(TvApp.getApplication().getCurrentUser().getId());
|
||||
query.setGroupId(mFolder.getId());
|
||||
query.setFields(new ItemFields[] {
|
||||
ItemFields.PrimaryImageAspectRatio,
|
||||
ItemFields.ChildCount
|
||||
});
|
||||
mRows.add(new BrowseRowDef(TvApp.getApplication().getResources().getString(R.string.lbl_all_items), query));
|
||||
query.setFields(new ItemFields[] {ItemFields.PrimaryImageAspectRatio});
|
||||
mRows.add(new BrowseRowDef(mApplication.getResources().getString(R.string.lbl_all_items), query));
|
||||
rowLoader.loadRows(mRows);
|
||||
} else {
|
||||
|
||||
@@ -55,7 +53,7 @@ public class GenericFolderFragment extends EnhancedBrowseFragment {
|
||||
resume.setFilters(new ItemFilter[]{ItemFilter.IsResumable});
|
||||
resume.setSortBy(new String[]{ItemSortBy.DatePlayed});
|
||||
resume.setSortOrder(SortOrder.Descending);
|
||||
mRows.add(new BrowseRowDef(TvApp.getApplication().getString(R.string.lbl_continue_watching), resume, 0));
|
||||
mRows.add(new BrowseRowDef(mApplication.getString(R.string.lbl_continue_watching), resume, 0));
|
||||
}
|
||||
|
||||
StdItemQuery latest = new StdItemQuery();
|
||||
@@ -64,15 +62,14 @@ public class GenericFolderFragment extends EnhancedBrowseFragment {
|
||||
latest.setFilters(new ItemFilter[]{ItemFilter.IsUnplayed});
|
||||
latest.setSortBy(new String[]{ItemSortBy.DateCreated});
|
||||
latest.setSortOrder(SortOrder.Descending);
|
||||
mRows.add(new BrowseRowDef(TvApp.getApplication().getString(R.string.lbl_latest_additions), latest, 0));
|
||||
mRows.add(new BrowseRowDef(mApplication.getString(R.string.lbl_latest_additions), latest, 0));
|
||||
|
||||
}
|
||||
|
||||
|
||||
StdItemQuery byName = new StdItemQuery();
|
||||
byName.setParentId(mFolder.getId());
|
||||
String header = (mFolder.getBaseItemType() == BaseItemType.Season) ? mFolder.getName() : TvApp.getApplication().getString(R.string.lbl_by_name);
|
||||
mRows.add(new BrowseRowDef(header, byName, 100));
|
||||
mRows.add(new BrowseRowDef(mApplication.getString(R.string.lbl_by_name), byName, 100));
|
||||
|
||||
rowLoader.loadRows(mRows);
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
package org.jellyfin.androidtv.browsing;
|
||||
|
||||
import android.os.Bundle;
|
||||
|
||||
import org.jellyfin.androidtv.R;
|
||||
import org.jellyfin.androidtv.base.BaseActivity;
|
||||
|
||||
public class GenericGridActivity extends BaseActivity {
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_browse_grid);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.jellyfin.androidtv.ui.browsing;
|
||||
package org.jellyfin.androidtv.browsing;
|
||||
|
||||
public interface IGridLoader {
|
||||
void loadGrid(BrowseRowDef rowDef);
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.jellyfin.androidtv.ui.browsing;
|
||||
package org.jellyfin.androidtv.browsing;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -1,21 +1,23 @@
|
||||
package org.jellyfin.androidtv.ui.browsing;
|
||||
/*
|
||||
* Copyright (C) 2014 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||
* in compliance with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software distributed under the License
|
||||
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
|
||||
* or implied. See the License for the specific language governing permissions and limitations under
|
||||
* the License.
|
||||
*/
|
||||
|
||||
import static org.koin.java.KoinJavaComponent.get;
|
||||
import static org.koin.java.KoinJavaComponent.inject;
|
||||
package org.jellyfin.androidtv.browsing;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.graphics.Bitmap;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.view.Gravity;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextClock;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.leanback.app.BackgroundManager;
|
||||
import androidx.leanback.app.BrowseSupportFragment;
|
||||
import androidx.leanback.widget.ArrayObjectAdapter;
|
||||
import androidx.leanback.widget.HeaderItem;
|
||||
@@ -25,60 +27,86 @@ import androidx.leanback.widget.OnItemViewSelectedListener;
|
||||
import androidx.leanback.widget.Presenter;
|
||||
import androidx.leanback.widget.Row;
|
||||
import androidx.leanback.widget.RowPresenter;
|
||||
import android.util.DisplayMetrics;
|
||||
import android.view.Gravity;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.animation.Animation;
|
||||
import android.view.animation.AnimationUtils;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.bumptech.glide.Glide;
|
||||
import com.bumptech.glide.request.animation.GlideAnimation;
|
||||
import com.bumptech.glide.request.target.SimpleTarget;
|
||||
|
||||
import org.jellyfin.androidtv.R;
|
||||
import org.jellyfin.androidtv.constant.CustomMessage;
|
||||
import org.jellyfin.androidtv.constant.QueryType;
|
||||
import org.jellyfin.androidtv.data.model.DataRefreshService;
|
||||
import org.jellyfin.androidtv.data.querying.ViewQuery;
|
||||
import org.jellyfin.androidtv.data.service.BackgroundService;
|
||||
import org.jellyfin.androidtv.preference.UserPreferences;
|
||||
import org.jellyfin.androidtv.preference.constant.ClockBehavior;
|
||||
import org.jellyfin.androidtv.TvApp;
|
||||
import org.jellyfin.androidtv.base.BaseActivity;
|
||||
import org.jellyfin.androidtv.base.CustomMessage;
|
||||
import org.jellyfin.androidtv.base.IKeyListener;
|
||||
import org.jellyfin.androidtv.base.IMessageListener;
|
||||
import org.jellyfin.androidtv.itemhandling.AudioQueueItem;
|
||||
import org.jellyfin.androidtv.itemhandling.BaseRowItem;
|
||||
import org.jellyfin.androidtv.itemhandling.ItemLauncher;
|
||||
import org.jellyfin.androidtv.itemhandling.ItemRowAdapter;
|
||||
import org.jellyfin.androidtv.presentation.CardPresenter;
|
||||
import org.jellyfin.androidtv.presentation.PositionableListRowPresenter;
|
||||
import org.jellyfin.androidtv.querying.QueryType;
|
||||
import org.jellyfin.androidtv.querying.ViewQuery;
|
||||
import org.jellyfin.androidtv.ui.ClockUserView;
|
||||
import org.jellyfin.androidtv.ui.itemhandling.BaseRowItem;
|
||||
import org.jellyfin.androidtv.ui.itemhandling.ItemLauncher;
|
||||
import org.jellyfin.androidtv.ui.itemhandling.ItemRowAdapter;
|
||||
import org.jellyfin.androidtv.ui.presentation.CardPresenter;
|
||||
import org.jellyfin.androidtv.ui.presentation.PositionableListRowPresenter;
|
||||
import org.jellyfin.androidtv.ui.search.SearchActivity;
|
||||
import org.jellyfin.androidtv.ui.shared.BaseActivity;
|
||||
import org.jellyfin.androidtv.ui.shared.IKeyListener;
|
||||
import org.jellyfin.androidtv.ui.shared.IMessageListener;
|
||||
import org.jellyfin.androidtv.ui.ItemPanel;
|
||||
import org.jellyfin.androidtv.util.KeyProcessor;
|
||||
import org.jellyfin.androidtv.util.Utils;
|
||||
import org.jellyfin.apiclient.interaction.EmptyResponse;
|
||||
import org.jellyfin.apiclient.model.dto.BaseItemType;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Timer;
|
||||
import java.util.TimerTask;
|
||||
|
||||
import kotlin.Lazy;
|
||||
import timber.log.Timber;
|
||||
import org.jellyfin.apiclient.interaction.EmptyResponse;
|
||||
import org.jellyfin.apiclient.model.dto.BaseItemType;
|
||||
|
||||
public class StdBrowseFragment extends BrowseSupportFragment implements IRowLoader {
|
||||
private static final String TAG = "StdBrowseFragment";
|
||||
|
||||
private static final int BACKGROUND_UPDATE_DELAY = 100;
|
||||
|
||||
protected String MainTitle;
|
||||
protected boolean ShowBadge = true;
|
||||
protected boolean ShowInfoPanel = true;
|
||||
protected boolean ShowFanart = false;
|
||||
protected TvApp mApplication;
|
||||
protected BaseActivity mActivity;
|
||||
protected BaseRowItem mCurrentItem;
|
||||
protected ListRow mCurrentRow;
|
||||
protected CompositeClickedListener mClickedListener = new CompositeClickedListener();
|
||||
protected CompositeSelectedListener mSelectedListener = new CompositeSelectedListener();
|
||||
protected ArrayObjectAdapter mRowsAdapter;
|
||||
private SimpleTarget<Bitmap> mBackgroundTarget;
|
||||
private DisplayMetrics mMetrics;
|
||||
private Timer mBackgroundTimer;
|
||||
private final Handler mHandler = new Handler();
|
||||
private String mBackgroundUrl;
|
||||
protected ArrayList<BrowseRowDef> mRows = new ArrayList<>();
|
||||
protected CardPresenter mCardPresenter;
|
||||
private TextClock mClock;
|
||||
|
||||
protected boolean justLoaded = true;
|
||||
|
||||
private Lazy<BackgroundService> backgroundService = inject(BackgroundService.class);
|
||||
private ItemPanel mItemPanel;
|
||||
private Animation fadeInPanel;
|
||||
private Animation fadeOutPanel;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
public void onActivityCreated(Bundle savedInstanceState) {
|
||||
super.onActivityCreated(savedInstanceState);
|
||||
|
||||
mApplication = TvApp.getApplication();
|
||||
if (getActivity() instanceof BaseActivity) mActivity = (BaseActivity)getActivity();
|
||||
|
||||
backgroundService.getValue().attach(requireActivity());
|
||||
prepareBackgroundManager();
|
||||
|
||||
setupUIElements();
|
||||
|
||||
@@ -91,22 +119,32 @@ public class StdBrowseFragment extends BrowseSupportFragment implements IRowLoad
|
||||
rowLoader.loadRows(mRows);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
if (null != mBackgroundTimer) {
|
||||
mBackgroundTimer.cancel();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPause() {
|
||||
super.onPause();
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
|
||||
ClockBehavior showClock = get(UserPreferences.class).get(UserPreferences.Companion.getClockBehavior());
|
||||
|
||||
if (showClock == ClockBehavior.ALWAYS || showClock == ClockBehavior.IN_MENUS)
|
||||
mClock.setVisibility(View.VISIBLE);
|
||||
else
|
||||
mClock.setVisibility(View.GONE);
|
||||
// set info panel option
|
||||
ShowInfoPanel = mApplication.getPrefs().getBoolean("pref_enable_info_panel", true);
|
||||
ShowFanart = mApplication.getPrefs().getBoolean("pref_show_backdrop", true);
|
||||
|
||||
//React to deletion
|
||||
DataRefreshService dataRefreshService = get(DataRefreshService.class);
|
||||
if (getActivity() != null && !getActivity().isFinishing() && mCurrentRow != null && mCurrentItem != null && mCurrentItem.getItemId() != null && mCurrentItem.getItemId().equals(dataRefreshService.getLastDeletedItemId())) {
|
||||
if (getActivity() != null && !getActivity().isFinishing() && mCurrentRow != null && mCurrentItem != null && mCurrentItem.getItemId() != null && mCurrentItem.getItemId().equals(TvApp.getApplication().getLastDeletedItemId())) {
|
||||
((ItemRowAdapter)mCurrentRow.getAdapter()).remove(mCurrentItem);
|
||||
dataRefreshService.setLastDeletedItemId(null);
|
||||
TvApp.getApplication().setLastDeletedItemId(null);
|
||||
}
|
||||
|
||||
if (!justLoaded) {
|
||||
@@ -125,19 +163,21 @@ public class StdBrowseFragment extends BrowseSupportFragment implements IRowLoad
|
||||
new Handler().postDelayed(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (mActivity == null || mActivity.isFinishing()) return;
|
||||
if (mActivity.isFinishing()) return;
|
||||
for (int i = 0; i < mRowsAdapter.size(); i++) {
|
||||
if (mRowsAdapter.get(i) instanceof ListRow) {
|
||||
if (((ListRow) mRowsAdapter.get(i)).getAdapter() instanceof ItemRowAdapter) {
|
||||
if (((ListRow) mRowsAdapter.get(i)).getAdapter() instanceof ItemRowAdapter && !mActivity.isFinishing()) {
|
||||
((ItemRowAdapter) ((ListRow) mRowsAdapter.get(i)).getAdapter()).ReRetrieveIfNeeded();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},1500);
|
||||
|
||||
}
|
||||
|
||||
public void loadRows(List<BrowseRowDef> rows) {
|
||||
|
||||
mRowsAdapter = new ArrayObjectAdapter(new PositionableListRowPresenter());
|
||||
mCardPresenter = new CardPresenter();
|
||||
|
||||
@@ -197,60 +237,138 @@ public class StdBrowseFragment extends BrowseSupportFragment implements IRowLoad
|
||||
addAdditionalRows(mRowsAdapter);
|
||||
|
||||
setAdapter(mRowsAdapter);
|
||||
|
||||
}
|
||||
|
||||
protected void addAdditionalRows(ArrayObjectAdapter rowAdapter) {
|
||||
|
||||
}
|
||||
|
||||
protected void setupUIElements() {
|
||||
if (ShowBadge)
|
||||
setBadgeDrawable(ContextCompat.getDrawable(requireContext(), R.drawable.app_logo));
|
||||
private void prepareBackgroundManager() {
|
||||
|
||||
final BackgroundManager backgroundManager = BackgroundManager.getInstance(getActivity());
|
||||
backgroundManager.attach(getActivity().getWindow());
|
||||
|
||||
mMetrics = new DisplayMetrics();
|
||||
getActivity().getWindowManager().getDefaultDisplay().getMetrics(mMetrics);
|
||||
|
||||
mBackgroundTarget = new SimpleTarget<Bitmap>(mMetrics.widthPixels, mMetrics.heightPixels) {
|
||||
@Override
|
||||
public void onResourceReady(Bitmap resource, GlideAnimation<? super Bitmap> glideAnimation) {
|
||||
backgroundManager.setBitmap(resource);
|
||||
mApplication.setCurrentBackground(resource);
|
||||
}
|
||||
};
|
||||
|
||||
setTitle(MainTitle); // Badge, when set, takes precedent over title
|
||||
setHeadersState(HEADERS_DISABLED);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStart() {
|
||||
super.onStart();
|
||||
protected void setupUIElements() {
|
||||
if (ShowBadge) setBadgeDrawable(getActivity().getResources().getDrawable(R.drawable.app_logo_transparent));
|
||||
setTitle(MainTitle); // Badge, when set, takes precedent over title
|
||||
setHeadersState(HEADERS_ENABLED);
|
||||
setHeadersTransitionOnBackEnabled(true);
|
||||
|
||||
// move the badge/title to the left to make way for our clock/user bug
|
||||
ImageView badge = (ImageView) getActivity().findViewById(R.id.title_badge);
|
||||
TextView title = (TextView) getActivity().findViewById(R.id.title_text);
|
||||
if (badge != null) {
|
||||
FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) badge.getLayoutParams();
|
||||
lp.rightMargin = Utils.convertDpToPixel(getActivity(), 120);
|
||||
lp.width = Utils.convertDpToPixel(getActivity(), 250);
|
||||
badge.setLayoutParams(lp);
|
||||
}
|
||||
TextView title = (TextView) getActivity().findViewById(R.id.title_text);
|
||||
if (title != null) {
|
||||
FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) title.getLayoutParams();
|
||||
lp.rightMargin = Utils.convertDpToPixel(getActivity(), 120);
|
||||
lp.width = Utils.convertDpToPixel(getActivity(), 250);
|
||||
title.setLayoutParams(lp);
|
||||
}
|
||||
|
||||
ViewGroup root = (ViewGroup) getActivity().findViewById(android.R.id.content);
|
||||
|
||||
// add item panel
|
||||
mItemPanel = new ItemPanel(getActivity());
|
||||
FrameLayout.LayoutParams panelLayout = new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, Utils.convertDpToPixel(TvApp.getApplication(), 145));
|
||||
panelLayout.gravity = Gravity.BOTTOM;
|
||||
panelLayout.bottomMargin = -10;
|
||||
mItemPanel.setLayoutParams(panelLayout);
|
||||
root.addView(mItemPanel);
|
||||
mItemPanel.setVisibility(View.INVISIBLE);
|
||||
|
||||
// and add the clock element
|
||||
FrameLayout.LayoutParams layoutParams = new FrameLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||
ClockUserView userClock = new ClockUserView(getActivity(), null);
|
||||
mClock = userClock.findViewById(R.id.clock);
|
||||
ClockUserView clock = new ClockUserView(getActivity());
|
||||
layoutParams.gravity = Gravity.TOP | Gravity.RIGHT;
|
||||
layoutParams.rightMargin = Utils.convertDpToPixel(getActivity(), 40);
|
||||
layoutParams.topMargin = Utils.convertDpToPixel(getActivity(), 20);
|
||||
userClock.setLayoutParams(layoutParams);
|
||||
root.addView(userClock);
|
||||
clock.setLayoutParams(layoutParams);
|
||||
root.addView(clock);
|
||||
|
||||
// load item panel animation
|
||||
fadeInPanel = AnimationUtils.loadAnimation(mActivity, R.anim.abc_fade_in);
|
||||
fadeInPanel.setAnimationListener(new Animation.AnimationListener() {
|
||||
@Override
|
||||
public void onAnimationStart(Animation animation) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAnimationEnd(Animation animation) {
|
||||
mItemPanel.setVisibility(View.VISIBLE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAnimationRepeat(Animation animation) {
|
||||
|
||||
}
|
||||
});
|
||||
|
||||
fadeOutPanel = AnimationUtils.loadAnimation(mActivity, R.anim.abc_fade_out);
|
||||
fadeOutPanel.setAnimationListener(new Animation.AnimationListener() {
|
||||
@Override
|
||||
public void onAnimationStart(Animation animation) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAnimationEnd(Animation animation) {
|
||||
mItemPanel.setVisibility(View.INVISIBLE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAnimationRepeat(Animation animation) {
|
||||
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private Runnable showItemPanel = new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (mCurrentItem != null && !mCurrentItem.isFolder() && (mCurrentItem.getRuntimeTicks() > 0 || (mCurrentItem.getSummary() != null && mCurrentItem.getSummary().length() > 0))) {
|
||||
mItemPanel.setItem(mCurrentItem);
|
||||
mItemPanel.startAnimation(fadeInPanel);
|
||||
mHandler.removeCallbacks(hideItemPanel);
|
||||
mHandler.postDelayed(hideItemPanel, 20000);
|
||||
} else {
|
||||
mItemPanel.setVisibility(View.INVISIBLE);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private Runnable hideItemPanel = new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
mItemPanel.startAnimation(fadeOutPanel);
|
||||
}
|
||||
};
|
||||
|
||||
protected void setupEventListeners() {
|
||||
setOnSearchClickedListener(new View.OnClickListener() {
|
||||
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
Intent intent = new Intent(getActivity(), SearchActivity.class);
|
||||
intent.putExtra("MusicOnly", false);
|
||||
|
||||
startActivity(intent);
|
||||
TvApp.getApplication().showSearch(getActivity(), false);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -284,7 +402,7 @@ public class StdBrowseFragment extends BrowseSupportFragment implements IRowLoad
|
||||
|
||||
private void refreshCurrentItem() {
|
||||
if (mCurrentItem != null && mCurrentItem.getBaseItemType() != BaseItemType.UserView && mCurrentItem.getBaseItemType() != BaseItemType.CollectionFolder) {
|
||||
Timber.d("Refresh item \"%s\"", mCurrentItem.getFullName(requireContext()));
|
||||
TvApp.getApplication().getLogger().Debug("Refresh item "+mCurrentItem.getFullName());
|
||||
mCurrentItem.refresh(new EmptyResponse() {
|
||||
@Override
|
||||
public void onResponse() {
|
||||
@@ -311,25 +429,82 @@ public class StdBrowseFragment extends BrowseSupportFragment implements IRowLoad
|
||||
public void onItemSelected(Presenter.ViewHolder itemViewHolder, Object item,
|
||||
RowPresenter.ViewHolder rowViewHolder, Row row) {
|
||||
|
||||
if (ShowInfoPanel) {
|
||||
// cancel any delayed showing and hide item panel
|
||||
mHandler.removeCallbacks(showItemPanel);
|
||||
mItemPanel.setVisibility(View.INVISIBLE);
|
||||
}
|
||||
|
||||
if (!(item instanceof BaseRowItem)) {
|
||||
mCurrentItem = null;
|
||||
mHandler.removeCallbacks(hideItemPanel);
|
||||
//fill in default background
|
||||
backgroundService.getValue().clearBackgrounds();
|
||||
mBackgroundUrl = null;
|
||||
startBackgroundTimer();
|
||||
return;
|
||||
} else {
|
||||
mCurrentItem = (BaseRowItem)item;
|
||||
if (!isShowingHeaders() && ShowInfoPanel && !(item instanceof AudioQueueItem)) {
|
||||
// delay show the item panel
|
||||
mHandler.postDelayed(showItemPanel, 1000);
|
||||
}
|
||||
}
|
||||
|
||||
mCurrentRow = (ListRow) row;
|
||||
BaseRowItem rowItem = (BaseRowItem) item;
|
||||
|
||||
if (((ListRow) row).getAdapter() instanceof ItemRowAdapter) {
|
||||
//TvApp.getApplication().getLogger().Debug("Selected Item "+rowItem.getIndex() + " type: "+ (rowItem.getItemType().equals(BaseRowItem.ItemType.BaseItem) ? rowItem.getBaseItem().getType() : "other"));
|
||||
//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());
|
||||
}
|
||||
|
||||
backgroundService.getValue().setBackground(rowItem.getBaseItem());
|
||||
if (ShowFanart) {
|
||||
mBackgroundUrl = rowItem.getBackdropImageUrl();
|
||||
startBackgroundTimer();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
protected void updateBackground(String url) {
|
||||
if (url == null) {
|
||||
clearBackground();
|
||||
} else {
|
||||
Glide.with(getActivity())
|
||||
.load(url)
|
||||
.asBitmap()
|
||||
.override(mMetrics.widthPixels, mMetrics.heightPixels)
|
||||
.centerCrop()
|
||||
.into(mBackgroundTarget);
|
||||
}
|
||||
}
|
||||
|
||||
protected void clearBackground() {
|
||||
BackgroundManager.getInstance(getActivity()).setDrawable(null);
|
||||
}
|
||||
|
||||
private void startBackgroundTimer() {
|
||||
if (null != mBackgroundTimer) {
|
||||
mBackgroundTimer.cancel();
|
||||
}
|
||||
mBackgroundTimer = new Timer();
|
||||
mBackgroundTimer.schedule(new UpdateBackgroundTask(), BACKGROUND_UPDATE_DELAY);
|
||||
}
|
||||
|
||||
private class UpdateBackgroundTask extends TimerTask {
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
mHandler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
updateBackground(mBackgroundUrl);
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -1,84 +1,106 @@
|
||||
package org.jellyfin.androidtv.ui.browsing;
|
||||
/*
|
||||
* Copyright (C) 2014 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||
* in compliance with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software distributed under the License
|
||||
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
|
||||
* or implied. See the License for the specific language governing permissions and limitations under
|
||||
* the License.
|
||||
*/
|
||||
|
||||
import android.content.Intent;
|
||||
package org.jellyfin.androidtv.browsing;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.drawable.ColorDrawable;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import androidx.leanback.app.BackgroundManager;
|
||||
import androidx.leanback.widget.OnItemViewClickedListener;
|
||||
import androidx.leanback.widget.OnItemViewSelectedListener;
|
||||
import androidx.leanback.widget.Presenter;
|
||||
import androidx.leanback.widget.Row;
|
||||
import androidx.leanback.widget.RowPresenter;
|
||||
import android.util.DisplayMetrics;
|
||||
import android.view.Gravity;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.ImageButton;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.PopupMenu;
|
||||
import android.widget.PopupWindow;
|
||||
|
||||
import androidx.leanback.widget.OnItemViewClickedListener;
|
||||
import androidx.leanback.widget.OnItemViewSelectedListener;
|
||||
import androidx.leanback.widget.Presenter;
|
||||
import androidx.leanback.widget.Row;
|
||||
import androidx.leanback.widget.RowPresenter;
|
||||
import androidx.leanback.widget.VerticalGridPresenter;
|
||||
import com.bumptech.glide.Glide;
|
||||
import com.bumptech.glide.request.animation.GlideAnimation;
|
||||
import com.bumptech.glide.request.target.SimpleTarget;
|
||||
|
||||
import org.jellyfin.androidtv.R;
|
||||
import org.jellyfin.androidtv.TvApp;
|
||||
import org.jellyfin.androidtv.constant.CustomMessage;
|
||||
import org.jellyfin.androidtv.constant.Extras;
|
||||
import org.jellyfin.androidtv.constant.GridDirection;
|
||||
import org.jellyfin.androidtv.constant.ImageType;
|
||||
import org.jellyfin.androidtv.constant.PosterSize;
|
||||
import org.jellyfin.androidtv.constant.QueryType;
|
||||
import org.jellyfin.androidtv.data.model.FilterOptions;
|
||||
import org.jellyfin.androidtv.data.querying.ViewQuery;
|
||||
import org.jellyfin.androidtv.data.service.BackgroundService;
|
||||
import org.jellyfin.androidtv.ui.AlphaPicker;
|
||||
import org.jellyfin.androidtv.ui.GridFragment;
|
||||
import org.jellyfin.androidtv.ui.itemhandling.BaseRowItem;
|
||||
import org.jellyfin.androidtv.ui.itemhandling.ItemLauncher;
|
||||
import org.jellyfin.androidtv.ui.itemhandling.ItemRowAdapter;
|
||||
import org.jellyfin.androidtv.ui.playback.MediaManager;
|
||||
import org.jellyfin.androidtv.ui.preference.PreferencesActivity;
|
||||
import org.jellyfin.androidtv.ui.presentation.CardPresenter;
|
||||
import org.jellyfin.androidtv.ui.presentation.HorizontalGridPresenter;
|
||||
import org.jellyfin.androidtv.ui.search.SearchActivity;
|
||||
import org.jellyfin.androidtv.ui.shared.BaseActivity;
|
||||
import org.jellyfin.androidtv.ui.shared.IKeyListener;
|
||||
import org.jellyfin.androidtv.ui.shared.IMessageListener;
|
||||
import org.jellyfin.androidtv.base.BaseActivity;
|
||||
import org.jellyfin.androidtv.base.CustomMessage;
|
||||
import org.jellyfin.androidtv.base.IKeyListener;
|
||||
import org.jellyfin.androidtv.base.IMessageListener;
|
||||
import org.jellyfin.androidtv.itemhandling.BaseRowItem;
|
||||
import org.jellyfin.androidtv.itemhandling.ItemLauncher;
|
||||
import org.jellyfin.androidtv.itemhandling.ItemRowAdapter;
|
||||
import org.jellyfin.androidtv.model.FilterOptions;
|
||||
import org.jellyfin.androidtv.model.ImageType;
|
||||
import org.jellyfin.androidtv.model.PosterSize;
|
||||
import org.jellyfin.androidtv.playback.MediaManager;
|
||||
import org.jellyfin.androidtv.presentation.CardPresenter;
|
||||
import org.jellyfin.androidtv.presentation.HorizontalGridPresenter;
|
||||
import org.jellyfin.androidtv.querying.QueryType;
|
||||
import org.jellyfin.androidtv.querying.ViewQuery;
|
||||
import org.jellyfin.androidtv.ui.CharSelectedListener;
|
||||
import org.jellyfin.androidtv.ui.DisplayPrefsPopup;
|
||||
import org.jellyfin.androidtv.ui.HorizontalGridFragment;
|
||||
import org.jellyfin.androidtv.ui.ImageButton;
|
||||
import org.jellyfin.androidtv.ui.JumpList;
|
||||
import org.jellyfin.androidtv.util.KeyProcessor;
|
||||
import org.jellyfin.androidtv.util.Utils;
|
||||
import org.jellyfin.apiclient.interaction.EmptyResponse;
|
||||
import org.jellyfin.apiclient.model.dto.BaseItemDto;
|
||||
import org.jellyfin.apiclient.model.dto.BaseItemType;
|
||||
import org.jellyfin.apiclient.model.entities.CollectionType;
|
||||
import org.jellyfin.apiclient.model.entities.DisplayPreferences;
|
||||
import org.jellyfin.apiclient.serialization.GsonJsonSerializer;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Timer;
|
||||
import java.util.TimerTask;
|
||||
|
||||
import kotlin.Lazy;
|
||||
import timber.log.Timber;
|
||||
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 static org.koin.java.KoinJavaComponent.get;
|
||||
import static org.koin.java.KoinJavaComponent.inject;
|
||||
public class StdGridFragment extends HorizontalGridFragment implements IGridLoader {
|
||||
private static final String TAG = "StdGridFragment";
|
||||
|
||||
private static final int BACKGROUND_UPDATE_DELAY = 100;
|
||||
|
||||
public class StdGridFragment extends GridFragment implements IGridLoader {
|
||||
protected String MainTitle;
|
||||
protected TvApp mApplication;
|
||||
protected BaseActivity mActivity;
|
||||
protected BaseRowItem mCurrentItem;
|
||||
protected CompositeClickedListener mClickedListener = new CompositeClickedListener();
|
||||
protected CompositeSelectedListener mSelectedListener = new CompositeSelectedListener();
|
||||
protected ItemRowAdapter mGridAdapter;
|
||||
private SimpleTarget<Bitmap> mBackgroundTarget;
|
||||
private DisplayMetrics mMetrics;
|
||||
private Timer mBackgroundTimer;
|
||||
private final Handler mHandler = new Handler();
|
||||
private String mBackgroundUrl;
|
||||
protected BrowseRowDef mRowDef;
|
||||
CardPresenter mCardPresenter;
|
||||
|
||||
protected boolean justLoaded = true;
|
||||
protected boolean ShowFanart = false;
|
||||
protected String mPosterSizeSetting = PosterSize.AUTO;
|
||||
protected String mImageType = ImageType.DEFAULT;
|
||||
protected String mGridDirection = GridDirection.HORIZONTAL.name();
|
||||
protected boolean determiningPosterSize = false;
|
||||
|
||||
protected String mParentId;
|
||||
@@ -88,94 +110,34 @@ public class StdGridFragment extends GridFragment implements IGridLoader {
|
||||
private int mCardHeight = SMALL_CARD;
|
||||
|
||||
protected boolean mAllowViewSelection = true;
|
||||
private Lazy<BackgroundService> backgroundService = inject(BackgroundService.class);
|
||||
private Lazy<MediaManager> mediaManager = inject(MediaManager.class);
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
mFolder = get(GsonJsonSerializer.class).DeserializeFromString(requireActivity().getIntent().getStringExtra(Extras.Folder), BaseItemDto.class);
|
||||
mFolder = TvApp.getApplication().getSerializer().DeserializeFromString(getActivity().getIntent().getStringExtra("Folder"), BaseItemDto.class);
|
||||
mParentId = mFolder.getId();
|
||||
MainTitle = mFolder.getName();
|
||||
mDisplayPrefs = TvApp.getApplication().getCachedDisplayPrefs(mFolder.getDisplayPreferencesId()); //These should have already been loaded
|
||||
mPosterSizeSetting = mDisplayPrefs.getCustomPrefs().get("PosterSize");
|
||||
mImageType = mDisplayPrefs.getCustomPrefs().get("ImageType");
|
||||
mGridDirection = mDisplayPrefs.getCustomPrefs().get("GridDirection");
|
||||
if (mImageType == null) mImageType = ImageType.DEFAULT;
|
||||
if (mPosterSizeSetting == null) mPosterSizeSetting = PosterSize.AUTO;
|
||||
if (mGridDirection == null) mGridDirection = GridDirection.HORIZONTAL.name();
|
||||
|
||||
if (mGridDirection.equals(GridDirection.VERTICAL.name()))
|
||||
setGridPresenter(new VerticalGridPresenter());
|
||||
else
|
||||
setGridPresenter(new HorizontalGridPresenter());
|
||||
|
||||
mCardHeight = getCardHeight(mPosterSizeSetting);
|
||||
setCardHeight(mCardHeight);
|
||||
|
||||
setGridSizes();
|
||||
setupUIElements();
|
||||
|
||||
mJumplistPopup = new JumplistPopup();
|
||||
}
|
||||
|
||||
private void setGridSizes() {
|
||||
Presenter gridPresenter = getGridPresenter();
|
||||
|
||||
if (gridPresenter instanceof HorizontalGridPresenter) {
|
||||
((HorizontalGridPresenter) gridPresenter).setNumberOfRows(getGridHeight() / getCardHeight());
|
||||
} else if (gridPresenter instanceof VerticalGridPresenter) {
|
||||
// Why is this hardcoded you ask? Well did you ever look at getGridHeight()? Yup that one is hardcoded too
|
||||
// This whole fragment is only optimized for 16:9 screens anyway
|
||||
// is this bad? Yup it definitely is, we'll fix it when this screen is rewritten
|
||||
|
||||
int size;
|
||||
switch (mImageType) {
|
||||
case ImageType.DEFAULT:
|
||||
default:
|
||||
if (mCardHeight == SMALL_VERTICAL_POSTER) {
|
||||
size = 10;
|
||||
} else if (mCardHeight == MED_VERTICAL_POSTER || mCardHeight == SMALL_VERTICAL_SQUARE) {
|
||||
size = 7;
|
||||
} else if (mCardHeight == LARGE_VERTICAL_POSTER) {
|
||||
size = 6;
|
||||
} else if (mCardHeight == MED_VERTICAL_SQUARE) {
|
||||
size = 5;
|
||||
} else {
|
||||
size = 4;
|
||||
}
|
||||
break;
|
||||
case ImageType.THUMB:
|
||||
if (mCardHeight == SMALL_VERTICAL_THUMB) {
|
||||
size = 4;
|
||||
} else if (mCardHeight == MED_VERTICAL_THUMB) {
|
||||
size = 3;
|
||||
} else {
|
||||
size = 2;
|
||||
}
|
||||
break;
|
||||
case ImageType.BANNER:
|
||||
if (mCardHeight == SMALL_VERTICAL_BANNER) {
|
||||
size = 3;
|
||||
} else if (mCardHeight == MED_VERTICAL_BANNER) {
|
||||
size = 2;
|
||||
} else {
|
||||
size = 1;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
((VerticalGridPresenter) gridPresenter).setNumberOfColumns(size);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityCreated(Bundle savedInstanceState) {
|
||||
super.onActivityCreated(savedInstanceState);
|
||||
|
||||
mApplication = TvApp.getApplication();
|
||||
if (getActivity() instanceof BaseActivity) mActivity = (BaseActivity)getActivity();
|
||||
|
||||
backgroundService.getValue().attach(requireActivity());
|
||||
prepareBackgroundManager();
|
||||
|
||||
setupQueries(this);
|
||||
|
||||
@@ -187,37 +149,25 @@ public class StdGridFragment extends GridFragment implements IGridLoader {
|
||||
protected void setupQueries(IGridLoader gridLoader) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
if (null != mBackgroundTimer) {
|
||||
mBackgroundTimer.cancel();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPause() {
|
||||
super.onPause();
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
|
||||
String imageType = mDisplayPrefs.getCustomPrefs().get("ImageType");
|
||||
if (imageType == null) imageType = ImageType.DEFAULT;
|
||||
String posterSizeSetting = mDisplayPrefs.getCustomPrefs().get("PosterSize");
|
||||
if (posterSizeSetting == null) posterSizeSetting = PosterSize.AUTO;
|
||||
String gridDirection = mDisplayPrefs.getCustomPrefs().get("GridDirection");
|
||||
if (gridDirection == null) gridDirection = GridDirection.HORIZONTAL.name();
|
||||
|
||||
if (mImageType != imageType || mPosterSizeSetting != posterSizeSetting || mGridDirection != gridDirection) {
|
||||
mImageType = imageType;
|
||||
mPosterSizeSetting = posterSizeSetting;
|
||||
mGridDirection = gridDirection;
|
||||
|
||||
if (mGridDirection.equals(GridDirection.VERTICAL.name()) && (getGridPresenter() == null || !(getGridPresenter() instanceof VerticalGridPresenter)))
|
||||
setGridPresenter(new VerticalGridPresenter());
|
||||
else if (mGridDirection.equals(GridDirection.HORIZONTAL.name()) && (getGridPresenter() == null || !(getGridPresenter() instanceof HorizontalGridPresenter)))
|
||||
setGridPresenter(new HorizontalGridPresenter());
|
||||
|
||||
int cardHeight = getCardHeight(mPosterSizeSetting);
|
||||
if (mCardHeight != cardHeight) {
|
||||
mCardHeight = cardHeight;
|
||||
setCardHeight(mCardHeight);
|
||||
}
|
||||
|
||||
setGridSizes();
|
||||
createGrid();
|
||||
loadGrid(mRowDef);
|
||||
}
|
||||
ShowFanart = mApplication.getPrefs().getBoolean("pref_show_backdrop", true);
|
||||
|
||||
if (!justLoaded) {
|
||||
//Re-retrieve anything that needs it but delay slightly so we don't take away gui landing
|
||||
@@ -225,7 +175,7 @@ public class StdGridFragment extends GridFragment implements IGridLoader {
|
||||
new Handler().postDelayed(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (mActivity == null || mActivity.isFinishing()) return;
|
||||
if (mActivity.isFinishing()) return;
|
||||
if (mGridAdapter != null && mGridAdapter.size() > 0) {
|
||||
if (!mGridAdapter.ReRetrieveIfNeeded()) refreshCurrentItem();
|
||||
}
|
||||
@@ -238,6 +188,10 @@ public class StdGridFragment extends GridFragment implements IGridLoader {
|
||||
}
|
||||
}
|
||||
|
||||
public void setCardHeight(int height) {
|
||||
mCardHeight = height;
|
||||
}
|
||||
|
||||
public int getCardHeight() {
|
||||
return mCardHeight;
|
||||
}
|
||||
@@ -294,6 +248,8 @@ public class StdGridFragment extends GridFragment implements IGridLoader {
|
||||
setupRetrieveListeners();
|
||||
mGridAdapter.setFilters(filters);
|
||||
setAdapter(mGridAdapter);
|
||||
|
||||
|
||||
}
|
||||
|
||||
public void loadGrid(final BrowseRowDef rowDef) {
|
||||
@@ -301,53 +257,88 @@ public class StdGridFragment extends GridFragment implements IGridLoader {
|
||||
buildAdapter(rowDef);
|
||||
|
||||
if (mPosterSizeSetting.equals(PosterSize.AUTO)) {
|
||||
// Use "medium" cards by default
|
||||
int autoHeight = getCardHeight(PosterSize.MED);
|
||||
if (autoHeight != mCardHeight) {
|
||||
mCardHeight = autoHeight;
|
||||
setCardHeight(mCardHeight);
|
||||
|
||||
setGridSizes();
|
||||
createGrid();
|
||||
Timber.d("Auto card height is %d", mCardHeight);
|
||||
buildAdapter(rowDef);
|
||||
}
|
||||
mGridAdapter.GetResultSizeAsync(new Response<Integer>() {
|
||||
@Override
|
||||
public void onResponse(Integer response) {
|
||||
int autoHeight = getAutoCardHeight(response);
|
||||
if (autoHeight != mCardHeight) {
|
||||
mCardHeight = autoHeight;
|
||||
setNumberOfRows();
|
||||
createGrid();
|
||||
TvApp.getApplication().getLogger().Debug("Auto card height is "+mCardHeight);
|
||||
buildAdapter(rowDef);
|
||||
}
|
||||
mGridAdapter.setSortBy(getSortOption(mDisplayPrefs.getSortBy()));
|
||||
mGridAdapter.Retrieve();
|
||||
determiningPosterSize = false;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
mGridAdapter.setSortBy(getSortOption(mDisplayPrefs.getSortBy()));
|
||||
mGridAdapter.Retrieve();
|
||||
determiningPosterSize = false;
|
||||
}
|
||||
|
||||
mGridAdapter.setSortBy(getSortOption(mDisplayPrefs.getSortBy()));
|
||||
mGridAdapter.Retrieve();
|
||||
determiningPosterSize = false;
|
||||
}
|
||||
|
||||
protected int getCardHeight(String heightSetting) {
|
||||
if (getGridPresenter() instanceof VerticalGridPresenter) {
|
||||
boolean isSquareCard = mFolder.getCollectionType().equals(CollectionType.Music);
|
||||
switch (heightSetting) {
|
||||
case PosterSize.MED:
|
||||
return mImageType.equals(ImageType.BANNER) ? MED_VERTICAL_BANNER : mImageType.equals(ImageType.THUMB) ? MED_VERTICAL_THUMB : (isSquareCard) ? MED_VERTICAL_SQUARE : MED_VERTICAL_POSTER;
|
||||
case PosterSize.LARGE:
|
||||
return mImageType.equals(ImageType.BANNER) ? LARGE_VERTICAL_BANNER : mImageType.equals(ImageType.THUMB) ? LARGE_VERTICAL_THUMB : (isSquareCard) ? LARGE_VERTICAL_SQUARE : LARGE_VERTICAL_POSTER;
|
||||
default:
|
||||
return mImageType.equals(ImageType.BANNER) ? SMALL_VERTICAL_BANNER : mImageType.equals(ImageType.THUMB) ? SMALL_VERTICAL_THUMB : (isSquareCard) ? SMALL_VERTICAL_SQUARE : SMALL_VERTICAL_POSTER;
|
||||
}
|
||||
} else {
|
||||
switch (heightSetting) {
|
||||
case PosterSize.MED:
|
||||
return mImageType.equals(ImageType.BANNER) ? MED_BANNER : MED_CARD;
|
||||
case PosterSize.LARGE:
|
||||
return mImageType.equals(ImageType.BANNER) ? LARGE_BANNER : LARGE_CARD;
|
||||
default:
|
||||
return mImageType.equals(ImageType.BANNER) ? SMALL_BANNER : SMALL_CARD;
|
||||
}
|
||||
switch (heightSetting) {
|
||||
case PosterSize.MED:
|
||||
return mImageType.equals(ImageType.BANNER) ? MED_BANNER : MED_CARD;
|
||||
case PosterSize.LARGE:
|
||||
return mImageType.equals(ImageType.BANNER) ? LARGE_BANNER : LARGE_CARD;
|
||||
default:
|
||||
return mImageType.equals(ImageType.BANNER) ? SMALL_BANNER : SMALL_CARD;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
protected ImageButton mSortButton;
|
||||
protected ImageButton mSearchButton;
|
||||
protected ImageButton mSettingsButton;
|
||||
protected int getAutoCardHeight(Integer size) {
|
||||
TvApp.getApplication().getLogger().Debug("Result size for auto card height is " + size);
|
||||
if (size > 35)
|
||||
return getCardHeight(PosterSize.SMALL);
|
||||
else if (size > 10)
|
||||
return getCardHeight(PosterSize.MED);
|
||||
else
|
||||
return getCardHeight(PosterSize.LARGE);
|
||||
|
||||
}
|
||||
private void prepareBackgroundManager() {
|
||||
|
||||
final BackgroundManager backgroundManager = BackgroundManager.getInstance(getActivity());
|
||||
backgroundManager.attach(getActivity().getWindow());
|
||||
|
||||
mMetrics = new DisplayMetrics();
|
||||
getActivity().getWindowManager().getDefaultDisplay().getMetrics(mMetrics);
|
||||
|
||||
mBackgroundTarget = new SimpleTarget<Bitmap>(mMetrics.widthPixels, mMetrics.heightPixels) {
|
||||
@Override
|
||||
public void onResourceReady(Bitmap resource, GlideAnimation<? super Bitmap> glideAnimation) {
|
||||
backgroundManager.setBitmap(resource);
|
||||
mApplication.setCurrentBackground(resource);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
protected void setupUIElements() {
|
||||
|
||||
HorizontalGridPresenter gridPresenter = new HorizontalGridPresenter();
|
||||
setGridPresenter(gridPresenter);
|
||||
setNumberOfRows();
|
||||
|
||||
mJumplistPopup = new JumplistPopup(getActivity());
|
||||
}
|
||||
|
||||
protected void setNumberOfRows() {
|
||||
// calculate number of rows based on card height
|
||||
getGridPresenter().setNumberOfRows(getGridHeight() / getCardHeight());
|
||||
|
||||
}
|
||||
|
||||
protected ImageButton mUnwatchedButton;
|
||||
protected ImageButton mFavoriteButton;
|
||||
protected ImageButton mLetterButton;
|
||||
protected DisplayPrefsPopup mDisplayPrefsPopup;
|
||||
|
||||
protected void updateDisplayPrefs() {
|
||||
if (mDisplayPrefs.getCustomPrefs() == null)
|
||||
@@ -362,17 +353,29 @@ public class StdGridFragment extends GridFragment implements IGridLoader {
|
||||
protected void addTools() {
|
||||
//Add tools
|
||||
LinearLayout toolBar = getToolBar();
|
||||
int size = Utils.convertDpToPixel(requireContext(), 26);
|
||||
int size = Utils.convertDpToPixel(getActivity(), 24);
|
||||
|
||||
mSortButton = new ImageButton(requireContext(), null, 0, R.style.Button_Icon);
|
||||
mSortButton.setImageResource(R.drawable.ic_sort);
|
||||
mSortButton.setMaxHeight(size);
|
||||
mSortButton.setAdjustViewBounds(true);
|
||||
mSortButton.setOnClickListener(new View.OnClickListener() {
|
||||
mDisplayPrefsPopup = new DisplayPrefsPopup(getActivity(), mGridDock, mAllowViewSelection, new Response<Boolean>() {
|
||||
@Override
|
||||
public void onResponse(Boolean response) {
|
||||
TvApp.getApplication().updateDisplayPrefs(mDisplayPrefs);
|
||||
if (response)
|
||||
{
|
||||
mImageType = mDisplayPrefs.getCustomPrefs().get("ImageType");
|
||||
mPosterSizeSetting = mDisplayPrefs.getCustomPrefs().get("PosterSize");
|
||||
mCardHeight = getCardHeight(mPosterSizeSetting);
|
||||
setNumberOfRows();
|
||||
createGrid();
|
||||
loadGrid(mRowDef);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
toolBar.addView(new ImageButton(getActivity(), R.drawable.ic_sort, size, new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
//Create sort menu
|
||||
PopupMenu sortMenu = new PopupMenu(getActivity(), getToolBar(), Gravity.END);
|
||||
PopupMenu sortMenu = Utils.createPopupMenu(getActivity(), getToolBar(), Gravity.RIGHT);
|
||||
for (Integer key : sortOptions.keySet()) {
|
||||
SortOption option = sortOptions.get(key);
|
||||
if (option == null) option = sortOptions.get(0);
|
||||
@@ -392,18 +395,10 @@ public class StdGridFragment extends GridFragment implements IGridLoader {
|
||||
});
|
||||
sortMenu.show();
|
||||
}
|
||||
});
|
||||
mSortButton.setContentDescription(getString(R.string.lbl_sort_by));
|
||||
|
||||
toolBar.addView(mSortButton);
|
||||
}));
|
||||
|
||||
if (mRowDef.getQueryType() == QueryType.Items) {
|
||||
mUnwatchedButton = new ImageButton(requireContext(), null, 0, R.style.Button_Icon);
|
||||
mUnwatchedButton.setImageResource(R.drawable.ic_unwatch);
|
||||
mUnwatchedButton.setActivated(mGridAdapter.getFilters().isUnwatchedOnly());
|
||||
mUnwatchedButton.setMaxHeight(size);
|
||||
mUnwatchedButton.setAdjustViewBounds(true);
|
||||
mUnwatchedButton.setOnClickListener(new View.OnClickListener() {
|
||||
mUnwatchedButton = new ImageButton(getActivity(), mGridAdapter.getFilters().isUnwatchedOnly() ? R.drawable.ic_unwatch_red : R.drawable.ic_unwatch, size, new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
FilterOptions filters = mGridAdapter.getFilters();
|
||||
@@ -417,19 +412,15 @@ public class StdGridFragment extends GridFragment implements IGridLoader {
|
||||
} else {
|
||||
mGridAdapter.Retrieve();
|
||||
}
|
||||
mUnwatchedButton.setActivated(filters.isUnwatchedOnly());
|
||||
mUnwatchedButton.setImageResource(filters.isUnwatchedOnly() ? R.drawable.ic_unwatch_red : R.drawable.ic_unwatch);
|
||||
|
||||
|
||||
}
|
||||
});
|
||||
mUnwatchedButton.setContentDescription(getString(R.string.lbl_unwatched));
|
||||
toolBar.addView(mUnwatchedButton);
|
||||
}
|
||||
|
||||
mFavoriteButton = new ImageButton(requireContext(), null, 0, R.style.Button_Icon);
|
||||
mFavoriteButton.setImageResource(R.drawable.ic_heart);
|
||||
mFavoriteButton.setActivated(mGridAdapter.getFilters().isFavoriteOnly());
|
||||
mFavoriteButton.setMaxHeight(size);
|
||||
mFavoriteButton.setAdjustViewBounds(true);
|
||||
mFavoriteButton.setOnClickListener(new View.OnClickListener() {
|
||||
mFavoriteButton =new ImageButton(getActivity(), mGridAdapter.getFilters().isFavoriteOnly() ? R.drawable.ic_heart_red : R.drawable.ic_heart, size, new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
FilterOptions filters = mGridAdapter.getFilters();
|
||||
@@ -443,102 +434,87 @@ public class StdGridFragment extends GridFragment implements IGridLoader {
|
||||
} else {
|
||||
mGridAdapter.Retrieve();
|
||||
}
|
||||
mFavoriteButton.setActivated(filters.isFavoriteOnly());
|
||||
mFavoriteButton.setImageResource(filters.isFavoriteOnly() ? R.drawable.ic_heart_red : R.drawable.ic_heart);
|
||||
|
||||
}
|
||||
});
|
||||
mFavoriteButton.setContentDescription(getString(R.string.lbl_favorite));
|
||||
toolBar.addView(mFavoriteButton);
|
||||
|
||||
mLetterButton = new ImageButton(requireContext(), null, 0, R.style.Button_Icon);
|
||||
mLetterButton.setImageResource(R.drawable.ic_jump_letter);
|
||||
mLetterButton.setMaxHeight(size);
|
||||
mLetterButton.setAdjustViewBounds(true);
|
||||
mLetterButton.setOnClickListener(new View.OnClickListener() {
|
||||
mLetterButton = new ImageButton(getActivity(), R.drawable.ic_jump_letter, size, new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
//Open letter jump popup
|
||||
mJumplistPopup.show();
|
||||
}
|
||||
});
|
||||
mLetterButton.setContentDescription(getString(R.string.lbl_by_letter));
|
||||
toolBar.addView(mLetterButton);
|
||||
|
||||
mSearchButton = new ImageButton(requireContext(), null, 0, R.style.Button_Icon);
|
||||
mSearchButton.setImageResource(R.drawable.ic_search);
|
||||
mSearchButton.setMaxHeight(size);
|
||||
mSearchButton.setAdjustViewBounds(true);
|
||||
mSearchButton.setOnClickListener(new View.OnClickListener() {
|
||||
toolBar.addView(new ImageButton(getActivity(), R.drawable.ic_search, size, new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
Intent intent = new Intent(getActivity(), SearchActivity.class);
|
||||
intent.putExtra("MusicOnly", "music".equals(mFolder.getCollectionType()) || mFolder.getBaseItemType() == BaseItemType.MusicAlbum || mFolder.getBaseItemType() == BaseItemType.MusicArtist);
|
||||
|
||||
startActivity(intent);
|
||||
TvApp.getApplication().showSearch(getActivity(), "music".equals(mFolder.getCollectionType()) || mFolder.getBaseItemType() == BaseItemType.MusicAlbum || mFolder.getBaseItemType() == BaseItemType.MusicArtist);
|
||||
}
|
||||
});
|
||||
mSearchButton.setContentDescription(getString(R.string.lbl_search));
|
||||
toolBar.addView(mSearchButton);
|
||||
}));
|
||||
|
||||
mSettingsButton = new ImageButton(requireContext(), null, 0, R.style.Button_Icon);
|
||||
mSettingsButton.setImageResource(R.drawable.ic_settings);
|
||||
mSettingsButton.setMaxHeight(size);
|
||||
mSettingsButton.setAdjustViewBounds(true);
|
||||
mSettingsButton.setOnClickListener(new View.OnClickListener() {
|
||||
toolBar.addView(new ImageButton(getActivity(), R.drawable.ic_settings, size, new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
Intent settingsIntent = new Intent(getActivity(), PreferencesActivity.class);
|
||||
settingsIntent.putExtra(PreferencesActivity.EXTRA_SCREEN, DisplayPreferencesScreen.class.getCanonicalName());
|
||||
Bundle screenArgs = new Bundle();
|
||||
screenArgs.putString(DisplayPreferencesScreen.ARG_PREFERENCES_ID, mFolder.getDisplayPreferencesId());
|
||||
screenArgs.putBoolean(DisplayPreferencesScreen.ARG_ALLOW_VIEW_SELECTION, mAllowViewSelection);
|
||||
settingsIntent.putExtra(PreferencesActivity.EXTRA_SCREEN_ARGS, screenArgs);
|
||||
getActivity().startActivity(settingsIntent);
|
||||
mDisplayPrefsPopup.show(mDisplayPrefs, mFolder.getCollectionType());
|
||||
}
|
||||
});
|
||||
mSettingsButton.setContentDescription(getString(R.string.lbl_settings));
|
||||
toolBar.addView(mSettingsButton);
|
||||
}));
|
||||
|
||||
|
||||
}
|
||||
|
||||
private JumplistPopup mJumplistPopup;
|
||||
class JumplistPopup {
|
||||
|
||||
private final int WIDTH = Utils.convertDpToPixel(requireContext(), 900);
|
||||
private final int HEIGHT = Utils.convertDpToPixel(requireContext(), 55);
|
||||
final int WIDTH = Utils.convertDpToPixel(TvApp.getApplication(), 900);
|
||||
final int HEIGHT = Utils.convertDpToPixel(TvApp.getApplication(), 55);
|
||||
|
||||
private final PopupWindow popupWindow;
|
||||
private final AlphaPicker alphaPicker;
|
||||
PopupWindow mPopup;
|
||||
Activity mActivity;
|
||||
JumpList mJumplist;
|
||||
|
||||
JumplistPopup() {
|
||||
LayoutInflater inflater = LayoutInflater.from(requireContext());
|
||||
View layout = inflater.inflate(R.layout.popup_empty, mGridDock, false);
|
||||
popupWindow = new PopupWindow(layout, WIDTH, HEIGHT, true);
|
||||
popupWindow.setOutsideTouchable(true);
|
||||
popupWindow.setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT)); // necessary for popup to dismiss
|
||||
popupWindow.setAnimationStyle(R.style.WindowAnimation_SlideTop);
|
||||
JumplistPopup(Activity activity) {
|
||||
mActivity = activity;
|
||||
LayoutInflater inflater = (LayoutInflater) activity.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
|
||||
View layout = inflater.inflate(R.layout.empty_popup, null);
|
||||
mPopup = new PopupWindow(layout, WIDTH, HEIGHT);
|
||||
mPopup.setFocusable(true);
|
||||
mPopup.setOutsideTouchable(true);
|
||||
mPopup.setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT)); // necessary for popup to dismiss
|
||||
mPopup.setAnimationStyle(R.style.PopupSlideInTop);
|
||||
|
||||
alphaPicker = new AlphaPicker(requireContext(), null);
|
||||
alphaPicker.setOnAlphaSelected(letter -> {
|
||||
mGridAdapter.setStartLetter(letter.toString());
|
||||
loadGrid(mRowDef);
|
||||
dismiss();
|
||||
return null;
|
||||
mJumplist = new JumpList(activity, new CharSelectedListener() {
|
||||
@Override
|
||||
public void onCharSelected(String ch) {
|
||||
mGridAdapter.setStartLetter(ch);
|
||||
loadGrid(mRowDef);
|
||||
dismiss();
|
||||
}
|
||||
});
|
||||
|
||||
FrameLayout root = layout.findViewById(R.id.empty_popup);
|
||||
root.addView(alphaPicker);
|
||||
mJumplist.setGravity(Gravity.CENTER_HORIZONTAL);
|
||||
FrameLayout root = (FrameLayout) layout.findViewById(R.id.root);
|
||||
root.addView(mJumplist);
|
||||
|
||||
}
|
||||
|
||||
public boolean isShowing() {
|
||||
return (mPopup != null && mPopup.isShowing());
|
||||
}
|
||||
|
||||
public void show() {
|
||||
popupWindow.showAtLocation(mGridDock, Gravity.TOP, mGridDock.getLeft(), mGridDock.getTop());
|
||||
if (mGridAdapter.getStartLetter() != null && !mGridAdapter.getStartLetter().isEmpty()) {
|
||||
alphaPicker.focus(mGridAdapter.getStartLetter().charAt(0));
|
||||
}
|
||||
|
||||
mPopup.showAtLocation(mGridDock, Gravity.TOP, mGridDock.getLeft(), mGridDock.getTop());
|
||||
mJumplist.setFocus(mGridAdapter.getStartLetter());
|
||||
|
||||
}
|
||||
|
||||
public void dismiss() {
|
||||
if (popupWindow != null && popupWindow.isShowing()) {
|
||||
popupWindow.dismiss();
|
||||
if (mPopup != null && mPopup.isShowing()) {
|
||||
mPopup.dismiss();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -556,9 +532,9 @@ public class StdGridFragment extends GridFragment implements IGridLoader {
|
||||
@Override
|
||||
public boolean onKeyUp(int key, KeyEvent event) {
|
||||
if (key == KeyEvent.KEYCODE_MEDIA_PLAY || key == KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE) {
|
||||
mediaManager.getValue().setCurrentMediaAdapter(mGridAdapter);
|
||||
mediaManager.getValue().setCurrentMediaPosition(mCurrentItem.getIndex());
|
||||
mediaManager.getValue().setCurrentMediaTitle(mFolder.getName());
|
||||
MediaManager.setCurrentMediaAdapter(mGridAdapter);
|
||||
MediaManager.setCurrentMediaPosition(mCurrentItem.getIndex());
|
||||
MediaManager.setCurrentMediaTitle(mFolder.getName());
|
||||
}
|
||||
return KeyProcessor.HandleKey(key, mCurrentItem, mActivity);
|
||||
}
|
||||
@@ -579,9 +555,17 @@ public class StdGridFragment extends GridFragment implements IGridLoader {
|
||||
}
|
||||
|
||||
protected void setupRetrieveListeners() {
|
||||
mGridAdapter.setRetrieveStartedListener(new EmptyResponse() {
|
||||
@Override
|
||||
public void onResponse() {
|
||||
showSpinner();
|
||||
|
||||
}
|
||||
});
|
||||
mGridAdapter.setRetrieveFinishedListener(new EmptyResponse() {
|
||||
@Override
|
||||
public void onResponse() {
|
||||
hideSpinner();
|
||||
setStatusText(mFolder.getName());
|
||||
updateCounter(mGridAdapter.getTotalItems() > 0 ? 1 : 0);
|
||||
mLetterButton.setVisibility("SortName".equals(mGridAdapter.getSortBy()) ? View.VISIBLE : View.GONE);
|
||||
@@ -601,19 +585,14 @@ public class StdGridFragment extends GridFragment implements IGridLoader {
|
||||
}
|
||||
|
||||
private void refreshCurrentItem() {
|
||||
if (mediaManager.getValue().getCurrentMediaPosition() >= 0) {
|
||||
mCurrentItem = mediaManager.getValue().getCurrentMediaItem();
|
||||
|
||||
Presenter presenter = getGridPresenter();
|
||||
if (presenter instanceof HorizontalGridPresenter)
|
||||
((HorizontalGridPresenter) presenter).setPosition(mediaManager.getValue().getCurrentMediaPosition());
|
||||
// Don't do anything for vertical grids as the presenter does not allow setting the position
|
||||
|
||||
mediaManager.getValue().setCurrentMediaPosition(-1); // re-set so it doesn't mess with parent views
|
||||
if (MediaManager.getCurrentMediaPosition() >= 0) {
|
||||
mCurrentItem = MediaManager.getCurrentMediaItem();
|
||||
getGridPresenter().setPosition(MediaManager.getCurrentMediaPosition());
|
||||
MediaManager.setCurrentMediaPosition(-1); // re-set so it doesn't mess with parent views
|
||||
}
|
||||
if (mCurrentItem != null && mCurrentItem.getBaseItemType() != BaseItemType.Photo && mCurrentItem.getBaseItemType() != BaseItemType.PhotoAlbum
|
||||
&& mCurrentItem.getBaseItemType() != BaseItemType.MusicArtist && mCurrentItem.getBaseItemType() != BaseItemType.MusicAlbum) {
|
||||
Timber.d("Refresh item \"%s\"", mCurrentItem.getFullName(requireContext()));
|
||||
TvApp.getApplication().getLogger().Debug("Refresh item "+mCurrentItem.getFullName());
|
||||
mCurrentItem.refresh(new EmptyResponse() {
|
||||
@Override
|
||||
public void onResponse() {
|
||||
@@ -631,6 +610,7 @@ public class StdGridFragment extends GridFragment implements IGridLoader {
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -647,7 +627,10 @@ public class StdGridFragment extends GridFragment implements IGridLoader {
|
||||
private final Runnable mDelayedSetItem = new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
backgroundService.getValue().setBackground(mCurrentItem.getBaseItem());
|
||||
if (ShowFanart) {
|
||||
mBackgroundUrl = mCurrentItem.getBackdropImageUrl();
|
||||
startBackgroundTimer();
|
||||
}
|
||||
setItem(mCurrentItem);
|
||||
}
|
||||
};
|
||||
@@ -662,10 +645,11 @@ public class StdGridFragment extends GridFragment implements IGridLoader {
|
||||
mCurrentItem = null;
|
||||
setTitle(MainTitle);
|
||||
//fill in default background
|
||||
backgroundService.getValue().clearBackgrounds();
|
||||
mBackgroundUrl = null;
|
||||
startBackgroundTimer();
|
||||
} else {
|
||||
mCurrentItem = (BaseRowItem)item;
|
||||
mTitleView.setText(mCurrentItem.getName(requireContext()));
|
||||
mTitleView.setText(mCurrentItem.getName());
|
||||
mInfoRow.removeAllViews();
|
||||
mHandler.postDelayed(mDelayedSetItem, 400);
|
||||
|
||||
@@ -675,4 +659,45 @@ public class StdGridFragment extends GridFragment implements IGridLoader {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
protected void updateBackground(String url) {
|
||||
if (url == null) {
|
||||
clearBackground();
|
||||
} else {
|
||||
Glide.with(getActivity())
|
||||
.load(url)
|
||||
.asBitmap()
|
||||
.override(mMetrics.widthPixels, mMetrics.heightPixels)
|
||||
.centerCrop()
|
||||
.into(mBackgroundTarget);
|
||||
}
|
||||
}
|
||||
|
||||
protected void clearBackground() {
|
||||
BackgroundManager.getInstance(getActivity()).setDrawable(null);
|
||||
}
|
||||
|
||||
private void startBackgroundTimer() {
|
||||
if (null != mBackgroundTimer) {
|
||||
mBackgroundTimer.cancel();
|
||||
}
|
||||
mBackgroundTimer = new Timer();
|
||||
mBackgroundTimer.schedule(new UpdateBackgroundTask(), BACKGROUND_UPDATE_DELAY);
|
||||
}
|
||||
|
||||
private class UpdateBackgroundTask extends TimerTask {
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
mHandler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
updateBackground(mBackgroundUrl);
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package org.jellyfin.androidtv.browsing;
|
||||
|
||||
import android.os.Bundle;
|
||||
|
||||
import org.jellyfin.androidtv.R;
|
||||
import org.jellyfin.androidtv.base.BaseActivity;
|
||||
|
||||
public class SuggestedMoviesActivity extends BaseActivity {
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_suggested_movie);
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,12 @@
|
||||
package org.jellyfin.androidtv.ui.browsing;
|
||||
package org.jellyfin.androidtv.browsing;
|
||||
|
||||
import android.os.Bundle;
|
||||
|
||||
import org.jellyfin.androidtv.R;
|
||||
import org.jellyfin.androidtv.TvApp;
|
||||
import org.jellyfin.androidtv.constant.QueryType;
|
||||
import org.jellyfin.androidtv.data.querying.StdItemQuery;
|
||||
import org.jellyfin.apiclient.interaction.ApiClient;
|
||||
import org.jellyfin.androidtv.querying.QueryType;
|
||||
import org.jellyfin.androidtv.querying.StdItemQuery;
|
||||
|
||||
import org.jellyfin.apiclient.interaction.Response;
|
||||
import org.jellyfin.apiclient.model.dto.BaseItemDto;
|
||||
import org.jellyfin.apiclient.model.entities.SortOrder;
|
||||
@@ -15,9 +15,11 @@ import org.jellyfin.apiclient.model.querying.ItemSortBy;
|
||||
import org.jellyfin.apiclient.model.querying.ItemsResult;
|
||||
import org.jellyfin.apiclient.model.querying.SimilarItemsQuery;
|
||||
|
||||
import static org.koin.java.KoinJavaComponent.get;
|
||||
|
||||
/**
|
||||
* Created by Eric on 12/4/2014.
|
||||
*/
|
||||
public class SuggestedMoviesFragment extends EnhancedBrowseFragment {
|
||||
|
||||
@Override
|
||||
public void onActivityCreated(Bundle savedInstanceState) {
|
||||
showViews = false;
|
||||
@@ -26,6 +28,7 @@ public class SuggestedMoviesFragment extends EnhancedBrowseFragment {
|
||||
|
||||
@Override
|
||||
protected void setupQueries(final IRowLoader rowLoader) {
|
||||
|
||||
StdItemQuery lastPlayed = new StdItemQuery();
|
||||
lastPlayed.setParentId(mFolder.getId());
|
||||
lastPlayed.setIncludeItemTypes(new String[]{"Movie"});
|
||||
@@ -35,25 +38,22 @@ public class SuggestedMoviesFragment extends EnhancedBrowseFragment {
|
||||
lastPlayed.setLimit(8);
|
||||
lastPlayed.setRecursive(true);
|
||||
|
||||
get(ApiClient.class).GetItemsAsync(lastPlayed, new Response<ItemsResult>() {
|
||||
TvApp.getApplication().getApiClient().GetItemsAsync(lastPlayed, new Response<ItemsResult>() {
|
||||
@Override
|
||||
public void onResponse(ItemsResult response) {
|
||||
for (BaseItemDto item : response.getItems()) {
|
||||
SimilarItemsQuery similar = new SimilarItemsQuery();
|
||||
similar.setId(item.getId());
|
||||
similar.setFields(new ItemFields[] {
|
||||
ItemFields.PrimaryImageAspectRatio,
|
||||
ItemFields.Overview,
|
||||
ItemFields.ChildCount,
|
||||
ItemFields.MediaStreams,
|
||||
ItemFields.MediaSources
|
||||
});
|
||||
similar.setFields(new ItemFields[] {ItemFields.PrimaryImageAspectRatio, ItemFields.Overview});
|
||||
similar.setLimit(7);
|
||||
mRows.add(new BrowseRowDef(TvApp.getApplication().getString(R.string.lbl_because_you_watched)+item.getName(), similar, QueryType.SimilarMovies));
|
||||
mRows.add(new BrowseRowDef(mApplication.getString(R.string.lbl_because_you_watched)+item.getName(), similar, QueryType.SimilarMovies));
|
||||
}
|
||||
|
||||
rowLoader.loadRows(mRows);
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package org.jellyfin.androidtv.browsing;
|
||||
|
||||
import android.os.Bundle;
|
||||
|
||||
import org.jellyfin.androidtv.R;
|
||||
import org.jellyfin.androidtv.base.BaseActivity;
|
||||
|
||||
public class UserViewActivity extends BaseActivity {
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_user_view);
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
package org.jellyfin.androidtv.constant;
|
||||
|
||||
public enum ChangeTriggerType {
|
||||
LibraryUpdated,
|
||||
MoviePlayback,
|
||||
TvPlayback,
|
||||
GuideNeedsLoad,
|
||||
MusicPlayback,
|
||||
Always,
|
||||
VideoQueueChange,
|
||||
FavoriteUpdate
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
package org.jellyfin.androidtv.constant;
|
||||
|
||||
public enum CustomMessage {
|
||||
RefreshCurrentItem,
|
||||
RemoveCurrentItem,
|
||||
ActionComplete,
|
||||
RefreshRows
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
package org.jellyfin.androidtv.constant
|
||||
|
||||
object Extras {
|
||||
const val Folder = "Folder"
|
||||
const val IncludeType = "IncludeType"
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
package org.jellyfin.androidtv.constant
|
||||
|
||||
import org.jellyfin.androidtv.R
|
||||
import org.jellyfin.androidtv.ui.preference.dsl.EnumDisplayOptions
|
||||
|
||||
enum class GridDirection {
|
||||
/**
|
||||
* Horizontal.
|
||||
*/
|
||||
@EnumDisplayOptions(R.string.grid_direction_horizontal)
|
||||
HORIZONTAL,
|
||||
|
||||
/**
|
||||
* Vertical.
|
||||
*/
|
||||
@EnumDisplayOptions(R.string.grid_direction_vertical)
|
||||
VERTICAL;
|
||||
|
||||
companion object {
|
||||
fun getGridDirection(direction: String?) = values().firstOrNull { it.name == direction }
|
||||
}
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
package org.jellyfin.androidtv.constant
|
||||
|
||||
/**
|
||||
* All possible homesections, synced with jellyfin-web
|
||||
*/
|
||||
enum class HomeSectionType(
|
||||
val id: String
|
||||
) {
|
||||
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");
|
||||
|
||||
companion object {
|
||||
fun getById(id: String) = values().firstOrNull { it.id == id }
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.jellyfin.androidtv.constant;
|
||||
package org.jellyfin.androidtv.constants;
|
||||
|
||||
public class CodecTypes {
|
||||
/* Audio Codec Types */
|
||||
@@ -33,4 +33,5 @@ public class CodecTypes {
|
||||
public static final String MPEG4 = "mpeg4";
|
||||
public static final String VP8 = "vp8";
|
||||
public static final String VP9 = "vp9";
|
||||
public static final String VPX = "vpx";
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.jellyfin.androidtv.constant;
|
||||
package org.jellyfin.androidtv.constants;
|
||||
|
||||
public class ContainerTypes {
|
||||
public static final String _3GP = "3gp";
|
||||
@@ -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,4 +1,4 @@
|
||||
package org.jellyfin.androidtv.constant;
|
||||
package org.jellyfin.androidtv.constants;
|
||||
|
||||
public class MediaTypes {
|
||||
public static final String HLS = "hls";
|
||||
@@ -1,12 +0,0 @@
|
||||
package org.jellyfin.androidtv.data.model
|
||||
|
||||
class DataRefreshService {
|
||||
var lastDeletedItemId: String? = null
|
||||
var lastPlayback: Long = 0
|
||||
var lastMoviePlayback: Long = 0
|
||||
var lastTvPlayback: Long = 0
|
||||
var lastMusicPlayback: Long = 0
|
||||
var lastLibraryChange: Long = 0
|
||||
var lastVideoQueueChange: Long = 0
|
||||
var lastFavoriteUpdate: Long = 0
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
package org.jellyfin.androidtv.data.querying;
|
||||
|
||||
public class ViewQuery {
|
||||
//Dummy indicator class
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user