1
0
mirror of https://github.com/TeamNewPipe/NewPipe synced 2024-12-23 08:30:44 +00:00

Merge branch 'dev' into update-readme-installation-section

This commit is contained in:
mhmdanas 2021-01-15 18:35:19 +03:00
commit 815dd0f706
259 changed files with 11722 additions and 9728 deletions

View File

@ -15,7 +15,7 @@ Oh no, a bug! It happens. Thanks for reporting an issue with NewPipe. To make it
<!-- The comments between these brackets won't show up in the submitted issue (as you can see in the Preview). --> <!-- The comments between these brackets won't show up in the submitted issue (as you can see in the Preview). -->
### Checklist ### Checklist
<!-- The first box has been checked for you to show you how it is done. --> <!-- This checklist is COMPULSORY. The first box has been checked for you to show you how it is done. -->
- [x] I am using the latest version - x.xx.x <!-- Check https://github.com/TeamNewPipe/NewPipe/releases --> - [x] I am using the latest version - x.xx.x <!-- Check https://github.com/TeamNewPipe/NewPipe/releases -->
- [ ] I checked, but didn't find any duplicates (open OR closed) of this issue in the repo. <!-- Seriously, check. O_O --> - [ ] I checked, but didn't find any duplicates (open OR closed) of this issue in the repo. <!-- Seriously, check. O_O -->

View File

@ -11,7 +11,7 @@ assignees: ''
<!-- The comments between these brackets won't show up in the submitted issue (as you can see in the Preview). --> <!-- The comments between these brackets won't show up in the submitted issue (as you can see in the Preview). -->
### Checklist ### Checklist
<!-- The first box has been checked for you to show you how it is done. --> <!-- This checklist is COMPULSORY. The first box has been checked for you to show you how it is done. -->
- [x] I checked, but didn't find any duplicates (open OR closed) of this issue in the repo. <!-- Seriously, check. O_O --> - [x] I checked, but didn't find any duplicates (open OR closed) of this issue in the repo. <!-- Seriously, check. O_O -->
- [ ] I have read the contribution guidelines given at https://github.com/TeamNewPipe/NewPipe/blob/HEAD/.github/CONTRIBUTING.md. - [ ] I have read the contribution guidelines given at https://github.com/TeamNewPipe/NewPipe/blob/HEAD/.github/CONTRIBUTING.md.

30
.github/workflows/ci.yml vendored Normal file
View File

@ -0,0 +1,30 @@
name: CI
on: [push, pull_request]
jobs:
build-and-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: set up JDK 1.8
uses: actions/setup-java@v1.4.3
with:
java-version: 1.8
- name: Cache Gradle dependencies
uses: actions/cache@v2
with:
path: ~/.gradle/caches
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle') }}
restore-keys: ${{ runner.os }}-gradle
- name: Build debug APK and run Tests
run: ./gradlew assembleDebug lintDebug testDebugUnitTest --stacktrace
- name: Upload APK
uses: actions/upload-artifact@v2
with:
name: app
path: app/build/outputs/apk/debug/*.apk

0
.gitmodules vendored
View File

View File

@ -1,18 +0,0 @@
language: android
jdk:
- oraclejdk8
android:
components:
# The BuildTools version used by NewPipe
- tools
- build-tools-29.0.3
# The SDK version used to compile NewPipe
- android-29
before_install:
- yes | sdkmanager "platforms;android-29"
script: ./gradlew -Dorg.gradle.jvmargs=-Xmx1536m assembleDebug lintDebug testDebugUnitTest
licenses:
- '.+'

View File

@ -1,4 +1,4 @@
<p align="center"><a href="https://newpipe.schabi.org"><img src="assets/new_pipe_icon_5.png" width="150"></a></p> <p align="center"><a href="https://newpipe.net"><img src="assets/new_pipe_icon_5.png" width="150"></a></p>
<h2 align="center"><b>NewPipe</b></h2> <h2 align="center"><b>NewPipe</b></h2>
<h4 align="center">A libre lightweight streaming frontend for Android.</h4> <h4 align="center">A libre lightweight streaming frontend for Android.</h4>
<p align="center"><a href="https://f-droid.org/packages/org.schabi.newpipe/"><img src="https://f-droid.org/wiki/images/0/06/F-Droid-button_get-it-on.png"></a></p> <p align="center"><a href="https://f-droid.org/packages/org.schabi.newpipe/"><img src="https://f-droid.org/wiki/images/0/06/F-Droid-button_get-it-on.png"></a></p>
@ -6,17 +6,17 @@
<p align="center"> <p align="center">
<a href="https://github.com/TeamNewPipe/NewPipe/releases" alt="GitHub release"><img src="https://img.shields.io/github/release/TeamNewPipe/NewPipe.svg" ></a> <a href="https://github.com/TeamNewPipe/NewPipe/releases" alt="GitHub release"><img src="https://img.shields.io/github/release/TeamNewPipe/NewPipe.svg" ></a>
<a href="https://www.gnu.org/licenses/gpl-3.0" alt="License: GPLv3"><img src="https://img.shields.io/badge/License-GPL%20v3-blue.svg"></a> <a href="https://www.gnu.org/licenses/gpl-3.0" alt="License: GPLv3"><img src="https://img.shields.io/badge/License-GPL%20v3-blue.svg"></a>
<a href="https://travis-ci.org/TeamNewPipe/NewPipe" alt="Build Status"><img src="https://travis-ci.org/TeamNewPipe/NewPipe.svg"></a> <a href="https://github.com/TeamNewPipe/NewPipe/actions" alt="Build Status"><img src="https://github.com/TeamNewPipe/NewPipe/workflows/CI/badge.svg?branch=dev&event=push"></a>
<a href="https://hosted.weblate.org/engage/newpipe/" alt="Translation Status"><img src="https://hosted.weblate.org/widgets/newpipe/-/svg-badge.svg"></a> <a href="https://hosted.weblate.org/engage/newpipe/" alt="Translation Status"><img src="https://hosted.weblate.org/widgets/newpipe/-/svg-badge.svg"></a>
<a href="http://webchat.freenode.net/?channels=%23newpipe" alt="IRC channel: #newpipe"><img src="https://img.shields.io/badge/IRC%20chat-%23newpipe-brightgreen.svg"></a> <a href="http://webchat.freenode.net/?channels=%23newpipe" alt="IRC channel: #newpipe"><img src="https://img.shields.io/badge/IRC%20chat-%23newpipe-brightgreen.svg"></a>
<a href="https://www.bountysource.com/teams/newpipe" alt="Bountysource bounties"><img src="https://img.shields.io/bountysource/team/newpipe/activity.svg?colorB=cd201f"></a> <a href="https://www.bountysource.com/teams/newpipe" alt="Bountysource bounties"><img src="https://img.shields.io/bountysource/team/newpipe/activity.svg?colorB=cd201f"></a>
</p> </p>
<hr> <hr>
<p align="center"><a href="#screenshots">Screenshots</a> &bull; <a href="#description">Description</a> &bull; <a href="#features">Features</a> &bull; <a href="#updates">Updates</a> &bull; <a href="#contribution">Contribution</a> &bull; <a href="#donate">Donate</a> &bull; <a href="#license">License</a></p> <p align="center"><a href="#screenshots">Screenshots</a> &bull; <a href="#description">Description</a> &bull; <a href="#features">Features</a> &bull; <a href="#updates">Updates</a> &bull; <a href="#contribution">Contribution</a> &bull; <a href="#donate">Donate</a> &bull; <a href="#license">License</a></p>
<p align="center"><a href="https://newpipe.schabi.org">Website</a> &bull; <a href="https://newpipe.schabi.org/blog/">Blog</a> &bull; <a href="https://newpipe.schabi.org/FAQ/">FAQ</a> &bull; <a href="https://newpipe.schabi.org/press/">Press</a></p> <p align="center"><a href="https://newpipe.net">Website</a> &bull; <a href="https://newpipe.net/blog/">Blog</a> &bull; <a href="https://newpipe.net/FAQ/">FAQ</a> &bull; <a href="https://newpipe.net/press/">Press</a></p>
<hr> <hr>
*Read this in other languages: [English](README.md), [한국어](README.ko.md).* *Read this in other languages: [English](README.md), [한국어](README.ko.md), [Soomaali](README.so.md), [Português Brasil](README.pt.br.md).*
<b>경고: 이 버전은 베타 버전이므로, 버그가 발생할 수도 있습니다. 만약 버그가 발생하였다면, 우리의 GITHUB 저장소에서 ISSUE를 열람하여 주십시오.</b> <b>경고: 이 버전은 베타 버전이므로, 버그가 발생할 수도 있습니다. 만약 버그가 발생하였다면, 우리의 GITHUB 저장소에서 ISSUE를 열람하여 주십시오.</b>
@ -86,7 +86,7 @@ NewPipe 코드의 변경이 있을 때(기능 추가 또는 버그 수정으로
1. 직접 디버그 APK를 생성할 수 있습니다. 이 방법은 당신의 기기에서 새로운 기능을 얻을 수 있는 가장 빠른 방법이지만, 꽤 많이 복잡합니다. 1. 직접 디버그 APK를 생성할 수 있습니다. 이 방법은 당신의 기기에서 새로운 기능을 얻을 수 있는 가장 빠른 방법이지만, 꽤 많이 복잡합니다.
따라서 우리는 다른 방법들 중 하나를 사용하는 것을 추천합니다. 따라서 우리는 다른 방법들 중 하나를 사용하는 것을 추천합니다.
2. 우리의 커스텀 저장소를 F-Droid에 추가하고 우리가 릴리즈를 게시하는 대로 저곳에서 릴리즈를 설치할 수 있습니다. 2. 우리의 커스텀 저장소를 F-Droid에 추가하고 우리가 릴리즈를 게시하는 대로 저곳에서 릴리즈를 설치할 수 있습니다.
이에 대한 설명서는 이곳에서 확인할 수 있습니다: https://newpipe.schabi.org/FAQ/tutorials/install-add-fdroid-repo/ 이에 대한 설명서는 이곳에서 확인할 수 있습니다: https://newpipe.net/FAQ/tutorials/install-add-fdroid-repo/
3. 우리가 릴리즈를 게시하는 대로 [Github Releases](https://github.com/TeamNewPipe/NewPipe/releases)에서 APK를 다운받고 이것을 설치할 수 있습니다. 3. 우리가 릴리즈를 게시하는 대로 [Github Releases](https://github.com/TeamNewPipe/NewPipe/releases)에서 APK를 다운받고 이것을 설치할 수 있습니다.
4. F-Droid를 통해 업데이트 할 수 있습니다. F-Droid는 변화를 인식하고, 스스로 APK를 생성하고, 이것에 서명하고, 사용자들에서 업데이트를 전달해야만 하기 때문에, 4. F-Droid를 통해 업데이트 할 수 있습니다. F-Droid는 변화를 인식하고, 스스로 APK를 생성하고, 이것에 서명하고, 사용자들에서 업데이트를 전달해야만 하기 때문에,
이것은 업데이트를 받는 가장 느린 방법입니다. 이것은 업데이트를 받는 가장 느린 방법입니다.
@ -98,7 +98,7 @@ NewPipe 코드의 변경이 있을 때(기능 추가 또는 버그 수정으로
1. 당신의 기록, 구독, 그리고 재생목록을 유지할 수 있도록 Settings > Content > Export Database 를 통해 데이터를 백업하십시오. 1. 당신의 기록, 구독, 그리고 재생목록을 유지할 수 있도록 Settings > Content > Export Database 를 통해 데이터를 백업하십시오.
2. NewPipe를 삭제하십시오. 2. NewPipe를 삭제하십시오.
3. 새로운 소스에서 APK를 다운로드하고 이것을 설치하십시오. 3. 새로운 소스에서 APK를 다운로드하고 이것을 설치하십시오.
4. Step 1의 Settings > Content > Export Database 을 통해 데이터를 불러오십시오. 4. Step 1의 Settings > Content > Import Database 을 통해 데이터를 불러오십시오.
## Contribution ## Contribution
당신이 아이디어, 번역, 디자인 변경, 코드 정리, 또는 정말 큰 코드 수정에 대한 의견이 있다면, 도움은 항상 환영합니다. 당신이 아이디어, 번역, 디자인 변경, 코드 정리, 또는 정말 큰 코드 수정에 대한 의견이 있다면, 도움은 항상 환영합니다.
@ -111,7 +111,7 @@ NewPipe 코드의 변경이 있을 때(기능 추가 또는 버그 수정으로
</a> </a>
## Donate ## Donate
만약 NewPipe가 마음에 들었다면, 우리는 기부에 대해 기꺼이 환영합니다. bitcoin을 보내거나, Bountysource 또는 Liberapay를 통해 기부할 수 있습니다. NewPipe에 기부하는 것에 대한 자세한 정보를 원한다면, 우리의 [웹사이트](https://newpipe.schabi.org/donate)를 방문하여 주십시오. 만약 NewPipe가 마음에 들었다면, 우리는 기부에 대해 기꺼이 환영합니다. bitcoin을 보내거나, Bountysource 또는 Liberapay를 통해 기부할 수 있습니다. NewPipe에 기부하는 것에 대한 자세한 정보를 원한다면, 우리의 [웹사이트](https://newpipe.net/donate)를 방문하여 주십시오.
<table> <table>
<tr> <tr>
@ -134,7 +134,7 @@ NewPipe 코드의 변경이 있을 때(기능 추가 또는 버그 수정으로
## Privacy Policy ## Privacy Policy
NewPipe 프로젝트는 미디어 웹 서비스를 사용하는 것에 대한 사적의, 익명의 경험을 제공하는 것을 목표로 하고 있습니다. NewPipe 프로젝트는 미디어 웹 서비스를 사용하는 것에 대한 사적의, 익명의 경험을 제공하는 것을 목표로 하고 있습니다.
그러므로, 앱은 당신의 동의 없이 어떤 데이터도 수집하지 않습니다. NewPipe의 개인정보보호정책은 당신이 충돌 리포트를 보내거나, 또는 우리의 블로그에 글을 남길 때 어떤 데이터가 보내지고 저장되는지에 대해 상세히 설명합니다. 이 문서는 [여기](https://newpipe.schabi.org/legal/privacy/)에서 확인할 수 있습니다. 그러므로, 앱은 당신의 동의 없이 어떤 데이터도 수집하지 않습니다. NewPipe의 개인정보보호정책은 당신이 충돌 리포트를 보내거나, 또는 우리의 블로그에 글을 남길 때 어떤 데이터가 보내지고 저장되는지에 대해 상세히 설명합니다. 이 문서는 [여기](https://newpipe.net/legal/privacy/)에서 확인할 수 있습니다.
## License ## License
[![GNU GPLv3 Image](https://www.gnu.org/graphics/gplv3-127x51.png)](http://www.gnu.org/licenses/gpl-3.0.en.html) [![GNU GPLv3 Image](https://www.gnu.org/graphics/gplv3-127x51.png)](http://www.gnu.org/licenses/gpl-3.0.en.html)

View File

@ -1,4 +1,4 @@
<p align="center"><a href="https://newpipe.schabi.org"><img src="assets/new_pipe_icon_5.png" width="150"></a></p> <p align="center"><a href="https://newpipe.net"><img src="assets/new_pipe_icon_5.png" width="150"></a></p>
<h2 align="center"><b>NewPipe</b></h2> <h2 align="center"><b>NewPipe</b></h2>
<h4 align="center">A libre lightweight streaming frontend for Android.</h4> <h4 align="center">A libre lightweight streaming frontend for Android.</h4>
@ -8,17 +8,17 @@
<p align="center"> <p align="center">
<a href="https://github.com/TeamNewPipe/NewPipe/releases" alt="GitHub release"><img src="https://img.shields.io/github/release/TeamNewPipe/NewPipe.svg" ></a> <a href="https://github.com/TeamNewPipe/NewPipe/releases" alt="GitHub release"><img src="https://img.shields.io/github/release/TeamNewPipe/NewPipe.svg" ></a>
<a href="https://www.gnu.org/licenses/gpl-3.0" alt="License: GPLv3"><img src="https://img.shields.io/badge/License-GPL%20v3-blue.svg"></a> <a href="https://www.gnu.org/licenses/gpl-3.0" alt="License: GPLv3"><img src="https://img.shields.io/badge/License-GPL%20v3-blue.svg"></a>
<a href="https://travis-ci.org/TeamNewPipe/NewPipe" alt="Build Status"><img src="https://travis-ci.org/TeamNewPipe/NewPipe.svg"></a> <a href="https://github.com/TeamNewPipe/NewPipe/actions" alt="Build Status"><img src="https://github.com/TeamNewPipe/NewPipe/workflows/CI/badge.svg?branch=dev&event=push"></a>
<a href="https://hosted.weblate.org/engage/newpipe/" alt="Translation Status"><img src="https://hosted.weblate.org/widgets/newpipe/-/svg-badge.svg"></a> <a href="https://hosted.weblate.org/engage/newpipe/" alt="Translation Status"><img src="https://hosted.weblate.org/widgets/newpipe/-/svg-badge.svg"></a>
<a href="http://webchat.freenode.net/?channels=%23newpipe" alt="IRC channel: #newpipe"><img src="https://img.shields.io/badge/IRC%20chat-%23newpipe-brightgreen.svg"></a> <a href="http://webchat.freenode.net/?channels=%23newpipe" alt="IRC channel: #newpipe"><img src="https://img.shields.io/badge/IRC%20chat-%23newpipe-brightgreen.svg"></a>
<a href="https://www.bountysource.com/teams/newpipe" alt="Bountysource bounties"><img src="https://img.shields.io/bountysource/team/newpipe/activity.svg?colorB=cd201f"></a> <a href="https://www.bountysource.com/teams/newpipe" alt="Bountysource bounties"><img src="https://img.shields.io/bountysource/team/newpipe/activity.svg?colorB=cd201f"></a>
</p> </p>
<hr> <hr>
<p align="center"><a href="#screenshots">Screenshots</a> &bull; <a href="#description">Description</a> &bull; <a href="#features">Features</a> &bull; <a href="#installation">Installation</a> &bull; <a href="#contribution">Contribution</a> &bull; <a href="#donate">Donate</a> &bull; <a href="#license">License</a></p> <p align="center"><a href="#screenshots">Screenshots</a> &bull; <a href="#description">Description</a> &bull; <a href="#features">Features</a> &bull; <a href="#installation">Installation</a> &bull; <a href="#contribution">Contribution</a> &bull; <a href="#donate">Donate</a> &bull; <a href="#license">License</a></p>
<p align="center"><a href="https://newpipe.schabi.org">Website</a> &bull; <a href="https://newpipe.schabi.org/blog/">Blog</a> &bull; <a href="https://newpipe.schabi.org/FAQ/">FAQ</a> &bull; <a href="https://newpipe.schabi.org/press/">Press</a></p> <p align="center"><a href="https://newpipe.net">Website</a> &bull; <a href="https://newpipe.net/blog/">Blog</a> &bull; <a href="https://newpipe.net/FAQ/">FAQ</a> &bull; <a href="https://newpipe.net/press/">Press</a></p>
<hr> <hr>
*Read this in other languages: [English](README.md), [한국어](README.ko.md).* *Read this in other languages: [English](README.md), [한국어](README.ko.md), [Soomaali](README.so.md), [Português Brasil](README.pt.br.md).*
<b>WARNING: THIS IS A BETA VERSION, THEREFORE YOU MAY ENCOUNTER BUGS. IF YOU DO, OPEN AN ISSUE VIA OUR GITHUB REPOSITORY.</b> <b>WARNING: THIS IS A BETA VERSION, THEREFORE YOU MAY ENCOUNTER BUGS. IF YOU DO, OPEN AN ISSUE VIA OUR GITHUB REPOSITORY.</b>
@ -111,7 +111,7 @@ If you'd like to get involved, check our [contribution notes](.github/CONTRIBUTI
</a> </a>
## Donate ## Donate
If you like NewPipe we'd be happy about a donation. You can either send bitcoin or donate via Bountysource or Liberapay. For further info on donating to NewPipe, please visit our [website](https://newpipe.schabi.org/donate). If you like NewPipe we'd be happy about a donation. You can either send bitcoin or donate via Bountysource or Liberapay. For further info on donating to NewPipe, please visit our [website](https://newpipe.net/donate).
<table> <table>
<tr> <tr>
@ -134,7 +134,7 @@ If you like NewPipe we'd be happy about a donation. You can either send bitcoin
## Privacy Policy ## Privacy Policy
The NewPipe project aims to provide a private, anonymous experience for using media web services. The NewPipe project aims to provide a private, anonymous experience for using media web services.
Therefore, the app does not collect any data without your consent. NewPipe's privacy policy explains in detail what data is sent and stored when you send a crash report, or comment in our blog. You can find the document [here](https://newpipe.schabi.org/legal/privacy/). Therefore, the app does not collect any data without your consent. NewPipe's privacy policy explains in detail what data is sent and stored when you send a crash report, or comment in our blog. You can find the document [here](https://newpipe.net/legal/privacy/).
## License ## License
[![GNU GPLv3 Image](https://www.gnu.org/graphics/gplv3-127x51.png)](http://www.gnu.org/licenses/gpl-3.0.en.html) [![GNU GPLv3 Image](https://www.gnu.org/graphics/gplv3-127x51.png)](http://www.gnu.org/licenses/gpl-3.0.en.html)

140
README.pt.br.md Normal file
View File

@ -0,0 +1,140 @@
<p align="center"><a href="https://newpipe.net"><img src="assets/new_pipe_icon_5.png" width="150"></a></p>
<h2 align="center"><b>NewPipe</b></h2>
<h4 align="center">Uma interface de streaming leve e gratuita para Android.</h4>
<p align="center"><a href="https://f-droid.org/packages/org.schabi.newpipe/"><img src="https://f-droid.org/wiki/images/0/06/F-Droid-button_get-it-on.png"></a></p>
<p align="center">
<a href="https://github.com/TeamNewPipe/NewPipe/releases" alt="GitHub release"><img src="https://img.shields.io/github/release/TeamNewPipe/NewPipe.svg" ></a>
<a href="https://www.gnu.org/licenses/gpl-3.0" alt="License: GPLv3"><img src="https://img.shields.io/badge/License-GPL%20v3-blue.svg"></a>
<a href="https://github.com/TeamNewPipe/NewPipe/actions" alt="Build Status"><img src="https://github.com/TeamNewPipe/NewPipe/workflows/CI/badge.svg?branch=dev&event=push"></a>
<a href="https://hosted.weblate.org/engage/newpipe/" alt="Translation Status"><img src="https://hosted.weblate.org/widgets/newpipe/-/svg-badge.svg"></a>
<a href="http://webchat.freenode.net/?channels=%23newpipe" alt="IRC channel: #newpipe"><img src="https://img.shields.io/badge/IRC%20chat-%23newpipe-brightgreen.svg"></a>
<a href="https://www.bountysource.com/teams/newpipe" alt="Bountysource bounties"><img src="https://img.shields.io/bountysource/team/newpipe/activity.svg?colorB=cd201f"></a>
</p>
<hr>
<p align="center"><a href="#screenshots">Screenshots</a> &bull; <a href="#description">Descrição</a> &bull; <a href="#features">Características</a> &bull; <a href="#updates">Atualizações</a> &bull; <a href="#contribution">Contribuição</a> &bull; <a href="#donate">Doar</a> &bull; <a href="#license">Licença</a></p>
<p align="center"><a href="https://newpipe.net">Site</a> &bull; <a href="https://newpipe.net/blog/">Blog</a> &bull; <a href="https://newpipe.net/FAQ/">FAQ</a> &bull; <a href="https://newpipe.net/press/">Press</a></p>
<hr>
*Read this in other languages: [English](README.md), [한국어](README.ko.md), [Soomaali](README.so.md), [Português Brasil](README.pt.br.md).*
<b>AVISO: ESTA É UMA VERSÃO BETA, PORTANTO, VOCÊ PODE ENCONTRAR BUGS. ENCONTROU ALGUM, ABRA UM ISSUE ATRAVÉS DO NOSSO REPOSITÓRIO GITHUB.</b>
<b>COLOCAR NEWPIPE OU QUALQUER FORK DELE NA GOOGLE PLAY STORE VIOLA SEUS TERMOS E CONDIÇÕES.</b>
## Screenshots
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_01.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_01.png)
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_02.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_02.png)
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_03.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_03.png)
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_04.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_04.png)
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_05.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_05.png)
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_06.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_06.png)
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_07.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_07.png)
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_08.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_08.png)
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_09.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_09.png)
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_10.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_10.png)
[<img src="fastlane/metadata/android/en-US/images/tenInchScreenshots/shot_11.png" width=405>](fastlane/metadata/android/en-US/images/tenInchScreenshots/shot_11.png)
[<img src="fastlane/metadata/android/en-US/images/tenInchScreenshots/shot_12.png" width=405>](fastlane/metadata/android/en-US/images/tenInchScreenshots/shot_12.png)
## Descrição
O NewPipe não usa nenhuma biblioteca de framework do Google, nem a API do YouTube. Os sites são apenas analisados para obter informações necessárias, para que este aplicativo possa ser usado em dispositivos sem os serviços do Google instalados. Além disso, você não precisa de uma conta no YouTube para usar o NewPipe, que é um software livre com copyleft.
### Características
* Procurar vídeos
* Exibir informações gerais sobre vídeos
* Assista aos vídeos do YouTube
* Ouça vídeos do YouTube
* Modo popup (player flutuante)
* Selecione o player para assistir streaming
* Baixar vídeos
* Baixar somente áudio
* Abrir vídeo no Kodi
* Mostrar vídeos próximos/relacionados
* Pesquise no YouTube em um idioma específico
* Assistir/Bloquear material restrito
* Exibir informações gerais sobre canais
* Pesquisar canais
* Assista a vídeos de um canal
* Suporte Orbot/Tor (ainda não diretamente)
* Suporte 1080p/2K/4K
* Ver histórico
* Inscreva-se nos canais
* Procurar histórico
* Porcurar/Assistir playlists
* Assistir playlists em fila
* Vídeos em fila
* Playlists Local
* Legenda
* Suporte a live
* Mostrar comentários
### Serviços Suportados
O NewPipe suporta vários serviços. Nosso [documentação](https://teamnewpipe.github.io/documentation/) fornecer mais informações sobre como um novo serviço pode ser adicionado ao aplicativo e ao extrator. Por favor, entre em contato conosco se você pretende adicionar um novo. Atualmente, os serviços suportados são:
* YouTube
* SoundCloud \[beta\]
* media.ccc.de \[beta\]
* PeerTube instances \[beta\]
## Atualizações
Quando uma alteração no código NewPipe (devido à adição de recursos ou fixação de bugs), eventualmente ocorrerá uma versão. Estes estão no formato x.xx.x . A fim de obter esta nova versão, você pode:
1. Construa um APK de depuração você mesmo. Esta é a maneira mais rápida de obter novos recursos em seu dispositivo, mas é muito mais complicado, por isso recomendamos usar um dos outros métodos.
2. Adicione nosso repo personalizado ao F-Droid e instale-o a partir daí assim que publicarmos um lançamento. As instruções estão aqui.: https://newpipe.net/FAQ/tutorials/install-add-fdroid-repo/
3. Baixe o APK do [Github Releases](https://github.com/TeamNewPipe/NewPipe/releases) e instalá-lo assim que publicarmos um lançamento.
4. Atualização via F-droid. Este é o método mais lento para obter atualizações, pois o F-Droid deve reconhecer alterações, construir o próprio APK, assiná-lo e, em seguida, enviar a atualização para os usuários.
Recomendamos o método 2 para a maioria dos usuários. Os APKs instalados usando o método 2 ou 3 são compatíveis entre si, mas não com aqueles instalados usando o método 4. Isso se deve à mesma chave de assinatura (nossa) sendo usada para 2 e 3, mas uma chave de assinatura diferente (F-Droid's) está sendo usada para 4. Construir um APK depuração usando o método 1 exclui totalmente uma chave. Assinar chaves ajudam a garantir que um usuário não seja enganado para instalar uma atualização maliciosa em um aplicativo.
Enquanto isso, se você quiser trocar de fontes por algum motivo (por exemplo, a funcionalidade principal do NewPipe foi quebrada e o F-Droid ainda não tem a atualização), recomendamos seguir este procedimento:
1. Back up your data via Settings > Content > Export Database so you keep your history, subscriptions, and playlistsFaça backup de seus dados através de Configurações > Conteúdo > Exportar Base de Dados para que você mantenha seu histórico, inscrições e playlists
2. Desinstale o NewPipe
3. Baixe o APK da nova fonte e instale-o
4. Importe os dados da etapa 1 via Configurações > Conteúdo > Inportar Banco de Dados
## Contribuição
Se você tem ideias, traduções, alterações de design, limpeza de códigos ou mudanças reais de código, a ajuda é sempre bem-vinda.
Quanto mais for feito, melhor fica!
Se você quiser se envolver, verifique nossa [notas de contribuição](.github/CONTRIBUTING.md).
<a href="https://hosted.weblate.org/engage/newpipe/">
<img src="https://hosted.weblate.org/widgets/newpipe/-/287x66-grey.png" alt="Translation status" />
</a>
## Doar
Se você gosta de NewPipe, ficaríamos felizes com uma doação. Você pode enviar bitcoin ou doar via Bountysource ou Liberapay. Para obter mais informações sobre como doar para a NewPipe, visite nosso [site](https://newpipe.net/donate).
<table>
<tr>
<td><img src="https://bitcoin.org/img/icons/logotop.svg" alt="Bitcoin"></td>
<td><img src="assets/bitcoin_qr_code.png" alt="Bitcoin QR code" width="100px"></td>
<td><samp>16A9J59ahMRqkLSZjhYj33n9j3fMztFxnh</samp></td>
</tr>
<tr>
<td><a href="https://liberapay.com/TeamNewPipe/"><img src="https://upload.wikimedia.org/wikipedia/commons/2/27/Liberapay_logo_v2_white-on-yellow.svg" alt="Liberapay" width="80px" ></a></td>
<td><a href="https://liberapay.com/TeamNewPipe/"><img src="assets/liberapay_qr_code.png" alt="Visit NewPipe at liberapay.com" width="100px"></a></td>
<td><a href="https://liberapay.com/TeamNewPipe/donate"><img src="assets/liberapay_donate_button.svg" alt="Donate via Liberapay" height="35px"></a></td>
</tr>
<tr>
<td><a href="https://www.bountysource.com/teams/newpipe"><img src="https://upload.wikimedia.org/wikipedia/commons/thumb/2/22/Bountysource.png/320px-Bountysource.png" alt="Bountysource" width="190px"></a></td>
<td><a href="https://www.bountysource.com/teams/newpipe"><img src="assets/bountysource_qr_code.png" alt="Visit NewPipe at bountysource.com" width="100px"></a></td>
<td><a href="https://www.bountysource.com/teams/newpipe/issues"><img src="https://img.shields.io/bountysource/team/newpipe/activity.svg?colorB=cd201f" height="30px" alt="Check out how many bounties you can earn."></a></td>
</tr>
</table>
## Política de Privacidade
O projeto NewPipe tem como objetivo proporcionar uma experiência privada e anônima para o uso de serviços web de mídia.
Portanto, o aplicativo não coleta nenhum dado sem o seu consentimento. A política de privacidade da NewPipe explica em detalhes quais dados são enviados e armazenados quando você envia um relatório de erro ou comenta em nosso blog. Você pode encontrar o documento [aqui](https://newpipe.net/legal/privacy/).
## Licença
[![GNU GPLv3 Image](https://www.gnu.org/graphics/gplv3-127x51.png)](http://www.gnu.org/licenses/gpl-3.0.en.html)
NewPipe é Software Livre: Você pode usar, estudar compartilhamento e melhorá-lo à sua vontade.
Especificamente, você pode redistribuir e/ou modificá-lo sob os termos do
[GNU General Public License](https://www.gnu.org/licenses/gpl.html) publicado pela Free Software Foundation, seja a versão 3 da Licença, ou
(a sua opção) qualquer versão posterior.

136
README.so.md Normal file
View File

@ -0,0 +1,136 @@
<p align="center"><a href="https://newpipe.net"><img src="assets/new_pipe_icon_5.png" width="150"></a></p>
<h2 align="center"><b>NewPipe</b></h2>
<h4 align="center">App bilaash ah oo fudud looguna talagalay in aaladaha nidaamka Android-ka ku shaqeeya wax loogu daawado.</h4>
<p align="center"><a href="https://f-droid.org/packages/org.schabi.newpipe/"><img src="https://f-droid.org/wiki/images/0/06/F-Droid-button_get-it-on.png"></a></p>
<p align="center">
<a href="https://github.com/TeamNewPipe/NewPipe/releases" alt="Siidaynta GitHub "><img src="https://img.shields.io/github/release/TeamNewPipe/NewPipe.svg" ></a>
<a href="https://www.gnu.org/licenses/gpl-3.0" alt="Laysinka: GPLv3"><img src="https://img.shields.io/badge/License-GPL%20v3-blue.svg"></a>
<a href="https://github.com/TeamNewPipe/NewPipe/actions" alt="Darajada Dhismaha"><img src="https://github.com/TeamNewPipe/NewPipe/workflows/CI/badge.svg?branch=dev&event=push"></a>
<a href="https://hosted.weblate.org/engage/newpipe/" alt="Heerka Turjimaada"><img src="https://hosted.weblate.org/widgets/newpipe/-/svg-badge.svg"></a>
<a href="http://webchat.freenode.net/?channels=%23newpipe" alt="Kanaalka IRC: #newpipe"><img src="https://img.shields.io/badge/IRC%20chat-%23newpipe-brightgreen.svg"></a>
<a href="https://www.bountysource.com/teams/newpipe" alt="Kuwa Bountysource "><img src="https://img.shields.io/bountysource/team/newpipe/activity.svg?colorB=cd201f"></a>
</p>
<hr>
<p align="center"><a href="#screenshots">Sawir-shaashadeed</a> &bull; <a href="#description">Faahfaahin</a> &bull; <a href="#features">Waxqabadka</a> &bull; <a href="#updates">Cusboonaysiin</a> &bull; <a href="#contribution">Kusoo Kordhin</a> &bull; <a href="#donate">Ugu Deeq</a> &bull; <a href="#license">Laysinka</a></p>
<p align="center"><a href="https://newpipe.net">Website-ka</a> &bull; <a href="https://newpipe.net/blog/">Maqaalada</a> &bull; <a href="https://newpipe.net/FAQ/">Su'aalaha Aalaa La-iswaydiiyo</a> &bull; <a href="https://newpipe.net/press/">Warbaahinta</a></p>
<hr>
*Ku akhri luuqad kale: [English](README.md), [한국어](README.ko.md), [Soomaali](README.so.md), [Português Brasil](README.pt.br.md).*
<b>DIGNIIN: MIDKAN [NOOCA APP-KA EE HADDA] WALI TIJAABO AYUU KU JIRAA (BETA), SIDAA DARTEED CILLADO AYAAD LA KULMI KARTAA. HADAAD LA KULANTO , KA FUR ARIN SHARAXAYA QAYBTANADA GITHUB-KA.</b>
<b>NEWPIPE AMA KUWA KU SALAYSAN PLAYSTORE-KA IN LA GALIYO WAXAY KA HOR IMANAYSAA SHARCIGA IYO SHURUUDAHA AY LEEYIHIIN.</b>
## Sawir-shaashadeed
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_01.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_01.png)
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_02.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_02.png)
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_03.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_03.png)
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_04.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_04.png)
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_05.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_05.png)
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_06.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_06.png)
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_07.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_07.png)
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_08.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_08.png)
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_09.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_09.png)
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_10.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_10.png)
[<img src="fastlane/metadata/android/en-US/images/tenInchScreenshots/shot_11.png" width=405>](fastlane/metadata/android/en-US/images/tenInchScreenshots/shot_11.png)
[<img src="fastlane/metadata/android/en-US/images/tenInchScreenshots/shot_12.png" width=405>](fastlane/metadata/android/en-US/images/tenInchScreenshots/shot_12.png)
## Faahfaahin
NewPipe ma isticmalo nidaamka wada shaqaynta Google, ama API-ga YouTube. Kaliya website-yada ayaa la furaa si xogta loo baahanyahay loogala soo dhex baxo, App-kan waxaa lagu isticmaali karaa aaladaha aysa ku jirin Adeegyada Google. Sidoo kale, uma baahnid akoon YouTube ah si aad u isticmaasho NewPipe, kaasoo ah barnaamij bilaash ah.
### Waxqabadka
* Raadi muuqaalo
* Soo bandhiga faahfaahin guud oo muuqaalada ku saabsan
* Ku daawo muuqaalada YouTube
* Dhagayso muuqaalada YouTube
* Qaab daaqad ah (muuqaal daare yar oo application-nada dul fuula)
* Dooro muuqaal daareha aad rabto inaad wax ku daawato
* Daji muuqaalada
* Daji dhagaysiga kaliya (cod)
* Ku fur muuqaal Kodi
* Tus muuqaalada ka xiga/kuwa lamidka ah
* Inaad luuqada aad rabto wax kaga dhex raadiso YouTube
* Daawo/xanib muuqaalada da'da ku xidhan
* Soo bandhig xog guud oo ku saabsan kanaalada
* Raadi kanaalo
* Daawo muuqaalada kanaal
* Taageerida Orbot/Tor (wali toos ma aha)
* Taageerida muuqaalada 1080p/2K/4K
* Kaydka wixii hore [aad u daawatay]
* Inaad rukumato kanaalada
* Kaydinta waxaad raadisay
* Raadi/daawo xulalka
* U daawo sidii xulal la horay
* Hormo gali muuqaalada
* Xulal gudaha [aalada] ah
* Qoraal-hooseed
* Taageerida waxyaabaha tooska ah
* Soo bandhiga faalooyinka
### Adeegyada la Taageero
NewPipe wuxuu taageeraa adeegyo badan. [warqadan](https://teamnewpipe.github.io/documentation/) ayaa si faahfaahsan u sharaxaysa sida adeeg cusub loogu soo dari lahaa iyo kala fur-furaha. Fadlan nala soo xidhiidh hadaad rabto inaad mid cusub kusoo darto. Adeegyada aan hadda taageero waxaa kamid ah:
* YouTube
* SoundCloud \[tijaabo\]
* media.ccc.de \[tijaabo\]
* PeerTube instances \[tijaabo\]
## Cusboonaysiin
Marka koodhka NewPipe isbadal ku dhaco (wax cusub oo lagusoo kordhiyay ama cilad bixin), ugu dambayn waxaa lasii daayaa mid cusub (Siidayn). Siidaynta qaabkeedu waa x.xx.x . Si aad midka cusub u hesho, waxaad samayn kartaa:
1. Inaad mid cusub (APK) adigu dhisato. Tani waa mida ugu dagdag badan eed waxyaabaha cusub ku heli karto, laakiin way adagtahay, sidaa darteed waxaan soojeedinaynaa inaad isticmaasho qababka kale.
2. Ku dar qayb gaar ah xaganaga F-Droid oo xagaas kaga shub isla markay siidayn soobaxdo. Hagitaanka xagan ka eeg: https://newpipe.net/FAQ/tutorials/install-add-fdroid-repo/
3. Kasoo dajiso APK-ga xaga [Siidaynta Github](https://github.com/TeamNewPipe/NewPipe/releases) oo ku shubo isla markay siidayn soobaxdo.
4. Ka cusboonaysii xaga F-Droid. Tani waa mida ugu daahitaanka badan, sababtoo ah F-Droid waxay fiirin isbadalka waxayna iyadu dhisi mid (app), sixiixi, kadibna ay cusboonaysiinta usiidayn isticmaalayaasha.
Waxaan usoojeedinaynaa isticmaaalka qaabka 2 dadka badankood. APK-yada loogu shubo qaabka 2 ama 3 way isqaadan karaan, laakiin isma qaadan karaan kuwa loogu shubay qaabka 4. Sababtuna waxaa weeye furaha sixiixa oo iskumid ah (kaanaga weeye) oo loo isticmaalay 2 iyo 3, laakiin furo sixiixeed ka duwan (midka F-Droid) oo loo isticmaalay 4. Dhisida APK ayadoo la isticmaalayo qaabka 1 waxay gabi ahaanba ka reebtaa wax fure ah. Furayaasha sixiixa waxay xaqiijiyaan in isticmaalaha aan lagu khaldin inuu ku shubto cusboonaysiin khalad ah (wax lasoo dhexraaciyay) app-ka.
Waxaa kale, hadaad rabto inaad tixraacayada kala badasho sabab jirta awgeed (tusaale shaqaynta aasaasiga ah ee NewPipe ayaa khalkhashay F-Droid-na wali cusboonysiin ma hayo), waxaan soojeedinaynaa isticmaalka qaabkan:
1. Xogtaada koobi ka samee adoo raacaya Fadhiga > Luuqada & Fadhiga Kale > Gudbi Xog Diyaaran si aysa kaaga bixin kaydka wixii hore, rukunka, iyo xulalka
2. Saar NewPipe
3. Kasoo daji APK-ga tixraaca cusub oo ku shub
4. Kasoo gali xogta talaabada 1 xaga Fadhiga > Luuqada & Fadhiga Kale > Soo Gali Xog Kaydsan
## Kusoo Kordhin
Hadaad hayso fikrado; rogid, qaab badal, nadiifin koodh, ama koodhka ood si wayn wax oga badashaa—caawinta marwalba waa lasoo dhawaynayaa. Waxbadan hadii la qabto waxbadan ayaa fiicnaan!
Hadaad jeceshahay inaad qayb ka noqoto, fiiri [ogaysiisyada kusoo kordhinta](.github/CONTRIBUTING.md).
<a href="https://hosted.weblate.org/engage/newpipe/">
<img src="https://hosted.weblate.org/widgets/newpipe/-/287x66-grey.png" alt="Translation status" />
</a>
## Ugu Deeq
Hadaad jeceshahay NewPipe waan ku faraxsanaan lahayn deeq. Waxaad soo diri kartaa bitcoin ama sidoo kale waxaad deeqda kusoo diri kartaa xaga Bountysource ama Liberapay. Faahfaahin dheeraad ah oo kusaabsan ugu deeqida NewPipe, fadlan booqo [website-kanaga](https://newpipe.net/donate).
<table>
<tr>
<td><img src="https://bitcoin.org/img/icons/logotop.svg" alt="Bitcoin"></td>
<td><img src="assets/bitcoin_qr_code.png" alt="Bitcoin QR code" width="100px"></td>
<td><samp>16A9J59ahMRqkLSZjhYj33n9j3fMztFxnh</samp></td>
</tr>
<tr>
<td><a href="https://liberapay.com/TeamNewPipe/"><img src="https://upload.wikimedia.org/wikipedia/commons/2/27/Liberapay_logo_v2_white-on-yellow.svg" alt="Liberapay" width="80px" ></a></td>
<td><a href="https://liberapay.com/TeamNewPipe/"><img src="assets/liberapay_qr_code.png" alt="Visit NewPipe at liberapay.com" width="100px"></a></td>
<td><a href="https://liberapay.com/TeamNewPipe/donate"><img src="assets/liberapay_donate_button.svg" alt="Donate via Liberapay" height="35px"></a></td>
</tr>
<tr>
<td><a href="https://www.bountysource.com/teams/newpipe"><img src="https://upload.wikimedia.org/wikipedia/commons/thumb/2/22/Bountysource.png/320px-Bountysource.png" alt="Bountysource" width="190px"></a></td>
<td><a href="https://www.bountysource.com/teams/newpipe"><img src="assets/bountysource_qr_code.png" alt="Visit NewPipe at bountysource.com" width="100px"></a></td>
<td><a href="https://www.bountysource.com/teams/newpipe/issues"><img src="https://img.shields.io/bountysource/team/newpipe/activity.svg?colorB=cd201f" height="30px" alt="Check out how many bounties you can earn."></a></td>
</tr>
</table>
## Siyaasada Sirdhawrka
Mashruuca NewPipe waxay ujeedadiisu tahay inuu bixiyo wax kuu gaar ah, oo adoon shaqsi ahaan laguu aqoonsan aad isticmaasho website-yada wax laga daawado/dhagaysto.
Sidaa darteed, app-ku wax xog ah ma uruuriyo fasaxaaga la'aantii. Siyaasada Sirdhawrka NewPipe ayaa si faahfaahsan u sharaxda waxii xog ah ee la diro markaad cillad wariso, ama aad bogganaga faallo ka dhiibato. Warqada waxaad ka heli kartaa [halkan](https://newpipe.net/legal/privacy/).
## Laysinka
[![GNU GPLv3 Image](https://www.gnu.org/graphics/gplv3-127x51.png)](http://www.gnu.org/licenses/gpl-3.0.en.html)
NewPipe waa barnaamij bilaash ah oon lahayn xuquuqda daabacaada: Waad isticmaali kartaa, waad wadaagi kartaa waadna hormarin kartaa hadaad rabto. Gaar ahaan waad sii daabici kartaa ama wax baad ka badali kartaa ayadoo la raacayo shuruudaha sharciga guud ee [GNU](https://www.gnu.org/licenses/gpl.html) sida ay soosaareen Ururka Barnaamijyada Bilaashka ah, soosaarista 3aad ee laysinka, ama (hadaad doonto) nooc walba oo kasii dambeeyay laysinkii 3aad.

View File

@ -13,8 +13,8 @@ android {
resValue "string", "app_name", "NewPipe" resValue "string", "app_name", "NewPipe"
minSdkVersion 19 minSdkVersion 19
targetSdkVersion 29 targetSdkVersion 29
versionCode 959 versionCode 962
versionName "0.20.5" versionName "0.20.8"
multiDexEnabled true multiDexEnabled true
@ -85,11 +85,15 @@ android {
sourceSets { sourceSets {
androidTest.assets.srcDirs += files("$projectDir/schemas".toString()) androidTest.assets.srcDirs += files("$projectDir/schemas".toString())
} }
buildFeatures {
viewBinding true
}
} }
ext { ext {
icepickVersion = '3.2.0' icepickVersion = '3.2.0'
checkstyleVersion = '8.37' checkstyleVersion = '8.38'
stethoVersion = '1.5.1' stethoVersion = '1.5.1'
leakCanaryVersion = '2.5' leakCanaryVersion = '2.5'
exoPlayerVersion = '2.11.8' exoPlayerVersion = '2.11.8'
@ -98,6 +102,7 @@ ext {
groupieVersion = '2.8.1' groupieVersion = '2.8.1'
markwonVersion = '4.6.0' markwonVersion = '4.6.0'
googleAutoServiceVersion = '1.0-rc7' googleAutoServiceVersion = '1.0-rc7'
mockitoVersion = '3.6.0'
} }
configurations { configurations {
@ -162,7 +167,7 @@ dependencies {
kapt "frankiesardo:icepick-processor:${icepickVersion}" kapt "frankiesardo:icepick-processor:${icepickVersion}"
checkstyle "com.puppycrawl.tools:checkstyle:${checkstyleVersion}" checkstyle "com.puppycrawl.tools:checkstyle:${checkstyleVersion}"
ktlint "com.pinterest:ktlint:0.39.0" ktlint "com.pinterest:ktlint:0.40.0"
debugImplementation "com.facebook.stetho:stetho:${stethoVersion}" debugImplementation "com.facebook.stetho:stetho:${stethoVersion}"
debugImplementation "com.facebook.stetho:stetho-okhttp3:${stethoVersion}" debugImplementation "com.facebook.stetho:stetho-okhttp3:${stethoVersion}"
@ -175,7 +180,7 @@ dependencies {
// NewPipe dependencies // NewPipe dependencies
// You can use a local version by uncommenting a few lines in settings.gradle // You can use a local version by uncommenting a few lines in settings.gradle
implementation 'com.github.TeamNewPipe:NewPipeExtractor:175df679e05b24b6094570d719cc11f8dfc17c68' implementation 'com.github.TeamNewPipe:NewPipeExtractor:2d93b237236b8dce98943fd5dced9b8e645a2e0a'
implementation "com.github.TeamNewPipe:nanojson:1d9e1aea9049fc9f85e68b43ba39fe7be1c1f751" implementation "com.github.TeamNewPipe:nanojson:1d9e1aea9049fc9f85e68b43ba39fe7be1c1f751"
implementation "org.jsoup:jsoup:1.13.1" implementation "org.jsoup:jsoup:1.13.1"
@ -199,6 +204,8 @@ dependencies {
implementation 'androidx.core:core-ktx:1.3.2' implementation 'androidx.core:core-ktx:1.3.2'
implementation 'androidx.documentfile:documentfile:1.0.1' implementation 'androidx.documentfile:documentfile:1.0.1'
implementation 'androidx.localbroadcastmanager:localbroadcastmanager:1.0.0' implementation 'androidx.localbroadcastmanager:localbroadcastmanager:1.0.0'
implementation 'androidx.media:media:1.2.1'
implementation 'androidx.webkit:webkit:1.4.0'
implementation "androidx.lifecycle:lifecycle-livedata:${androidxLifecycleVersion}" implementation "androidx.lifecycle:lifecycle-livedata:${androidxLifecycleVersion}"
implementation "androidx.lifecycle:lifecycle-viewmodel:${androidxLifecycleVersion}" implementation "androidx.lifecycle:lifecycle-viewmodel:${androidxLifecycleVersion}"
@ -229,7 +236,8 @@ dependencies {
implementation "org.ocpsoft.prettytime:prettytime:4.0.6.Final" implementation "org.ocpsoft.prettytime:prettytime:4.0.6.Final"
testImplementation 'junit:junit:4.13.1' testImplementation 'junit:junit:4.13.1'
testImplementation 'org.mockito:mockito-core:3.6.0' testImplementation "org.mockito:mockito-core:${mockitoVersion}"
testImplementation "org.mockito:mockito-inline:${mockitoVersion}"
androidTestImplementation "androidx.test.ext:junit:1.1.2" androidTestImplementation "androidx.test.ext:junit:1.1.2"
androidTestImplementation "androidx.room:room-testing:${androidxRoomVersion}" androidTestImplementation "androidx.room:room-testing:${androidxRoomVersion}"

View File

@ -53,7 +53,7 @@
</service> </service>
<activity <activity
android:name=".player.BackgroundPlayerActivity" android:name=".player.PlayQueueActivity"
android:label="@string/title_activity_play_queue" android:label="@string/title_activity_play_queue"
android:launchMode="singleTask" /> android:launchMode="singleTask" />
@ -227,20 +227,18 @@
<data android:host="invidio.us" /> <data android:host="invidio.us" />
<data android:host="dev.invidio.us" /> <data android:host="dev.invidio.us" />
<data android:host="www.invidio.us" /> <data android:host="www.invidio.us" />
<data android:host="vid.encryptionin.space" />
<data android:host="invidious.snopyta.org" /> <data android:host="invidious.snopyta.org" />
<data android:host="fi.invidious.snopyta.org" />
<data android:host="yewtu.be" /> <data android:host="yewtu.be" />
<data android:host="invidious.ggc-project.de" /> <data android:host="tube.connect.cafe" />
<data android:host="yt.maisputain.ovh" /> <data android:host="invidious.zapashcanon.fr" />
<data android:host="invidious.13ad.de" /> <data android:host="invidious.kavin.rocks" />
<data android:host="invidious.toot.koeln" /> <data android:host="invidious.tube" />
<data android:host="invidious.site" />
<data android:host="invidious.xyz" />
<data android:host="vid.mint.lgbt" />
<data android:host="invidiou.site" />
<data android:host="invidious.fdn.fr" /> <data android:host="invidious.fdn.fr" />
<data android:host="watch.nettohikari.com" />
<data android:host="invidious.snwmds.net" />
<data android:host="invidious.snwmds.org" />
<data android:host="invidious.snwmds.com" />
<data android:host="invidious.sunsetravens.com" />
<data android:host="invidious.gachirangers.com" />
<data android:pathPrefix="/" /> <data android:pathPrefix="/" />
</intent-filter> </intent-filter>
@ -268,7 +266,7 @@
<data android:mimeType="text/plain" /> <data android:mimeType="text/plain" />
</intent-filter> </intent-filter>
<!-- MediaCCC filter --> <!-- media.ccc.de filter -->
<intent-filter> <intent-filter>
<action android:name="android.intent.action.VIEW" /> <action android:name="android.intent.action.VIEW" />
<action android:name="android.media.action.MEDIA_PLAY_FROM_SEARCH" /> <action android:name="android.media.action.MEDIA_PLAY_FROM_SEARCH" />
@ -320,6 +318,8 @@
android:name=".RouterActivity$FetcherService" android:name=".RouterActivity$FetcherService"
android:exported="false" /> android:exported="false" />
<!-- opting out of sending metrics to Google in Android System WebView -->
<meta-data android:name="android.webkit.WebView.MetricsOptOut" android:value="true" />
<!-- see https://github.com/TeamNewPipe/NewPipe/issues/3947 --> <!-- see https://github.com/TeamNewPipe/NewPipe/issues/3947 -->
<!-- Version < 3.0. DeX Mode and Screen Mirroring support --> <!-- Version < 3.0. DeX Mode and Screen Mirroring support -->
<meta-data android:name="com.samsung.android.keepalive.density" android:value="true"/> <meta-data android:name="com.samsung.android.keepalive.density" android:value="true"/>

View File

@ -0,0 +1,245 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<!-- saved from url=(0050)https://www.eclipse.org/org/documents/epl-v10.html -->
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta content="text/html; charset=utf-8" http-equiv="Content-Type">
<title>Eclipse Public License - Version 1.0</title>
</head>
<body cz-shortcut-listen="true" lang="EN-US">
<h2>Eclipse Public License - v 1.0</h2>
<p>THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE
PUBLIC LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR
DISTRIBUTION OF THE PROGRAM CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS
AGREEMENT.</p>
<p><b>1. DEFINITIONS</b></p>
<p>"Contribution" means:</p>
<p class="list">a) in the case of the initial Contributor, the initial
code and documentation distributed under this Agreement, and</p>
<p class="list">b) in the case of each subsequent Contributor:</p>
<p class="list">i) changes to the Program, and</p>
<p class="list">ii) additions to the Program;</p>
<p class="list">where such changes and/or additions to the Program
originate from and are distributed by that particular Contributor. A
Contribution 'originates' from a Contributor if it was added to the
Program by such Contributor itself or anyone acting on such
Contributor's behalf. Contributions do not include additions to the
Program which: (i) are separate modules of software distributed in
conjunction with the Program under their own license agreement, and (ii)
are not derivative works of the Program.</p>
<p>"Contributor" means any person or entity that distributes
the Program.</p>
<p>"Licensed Patents" mean patent claims licensable by a
Contributor which are necessarily infringed by the use or sale of its
Contribution alone or when combined with the Program.</p>
<p>"Program" means the Contributions distributed in accordance
with this Agreement.</p>
<p>"Recipient" means anyone who receives the Program under
this Agreement, including all Contributors.</p>
<p><b>2. GRANT OF RIGHTS</b></p>
<p class="list">a) Subject to the terms of this Agreement, each
Contributor hereby grants Recipient a non-exclusive, worldwide,
royalty-free copyright license to reproduce, prepare derivative works
of, publicly display, publicly perform, distribute and sublicense the
Contribution of such Contributor, if any, and such derivative works, in
source code and object code form.</p>
<p class="list">b) Subject to the terms of this Agreement, each
Contributor hereby grants Recipient a non-exclusive, worldwide,
royalty-free patent license under Licensed Patents to make, use, sell,
offer to sell, import and otherwise transfer the Contribution of such
Contributor, if any, in source code and object code form. This patent
license shall apply to the combination of the Contribution and the
Program if, at the time the Contribution is added by the Contributor,
such addition of the Contribution causes such combination to be covered
by the Licensed Patents. The patent license shall not apply to any other
combinations which include the Contribution. No hardware per se is
licensed hereunder.</p>
<p class="list">c) Recipient understands that although each Contributor
grants the licenses to its Contributions set forth herein, no assurances
are provided by any Contributor that the Program does not infringe the
patent or other intellectual property rights of any other entity. Each
Contributor disclaims any liability to Recipient for claims brought by
any other entity based on infringement of intellectual property rights
or otherwise. As a condition to exercising the rights and licenses
granted hereunder, each Recipient hereby assumes sole responsibility to
secure any other intellectual property rights needed, if any. For
example, if a third party patent license is required to allow Recipient
to distribute the Program, it is Recipient's responsibility to acquire
that license before distributing the Program.</p>
<p class="list">d) Each Contributor represents that to its knowledge it
has sufficient copyright rights in its Contribution, if any, to grant
the copyright license set forth in this Agreement.</p>
<p><b>3. REQUIREMENTS</b></p>
<p>A Contributor may choose to distribute the Program in object code
form under its own license agreement, provided that:</p>
<p class="list">a) it complies with the terms and conditions of this
Agreement; and</p>
<p class="list">b) its license agreement:</p>
<p class="list">i) effectively disclaims on behalf of all Contributors
all warranties and conditions, express and implied, including warranties
or conditions of title and non-infringement, and implied warranties or
conditions of merchantability and fitness for a particular purpose;</p>
<p class="list">ii) effectively excludes on behalf of all Contributors
all liability for damages, including direct, indirect, special,
incidental and consequential damages, such as lost profits;</p>
<p class="list">iii) states that any provisions which differ from this
Agreement are offered by that Contributor alone and not by any other
party; and</p>
<p class="list">iv) states that source code for the Program is available
from such Contributor, and informs licensees how to obtain it in a
reasonable manner on or through a medium customarily used for software
exchange.</p>
<p>When the Program is made available in source code form:</p>
<p class="list">a) it must be made available under this Agreement; and</p>
<p class="list">b) a copy of this Agreement must be included with each
copy of the Program.</p>
<p>Contributors may not remove or alter any copyright notices contained
within the Program.</p>
<p>Each Contributor must identify itself as the originator of its
Contribution, if any, in a manner that reasonably allows subsequent
Recipients to identify the originator of the Contribution.</p>
<p><b>4. COMMERCIAL DISTRIBUTION</b></p>
<p>Commercial distributors of software may accept certain
responsibilities with respect to end users, business partners and the
like. While this license is intended to facilitate the commercial use of
the Program, the Contributor who includes the Program in a commercial
product offering should do so in a manner which does not create
potential liability for other Contributors. Therefore, if a Contributor
includes the Program in a commercial product offering, such Contributor
("Commercial Contributor") hereby agrees to defend and
indemnify every other Contributor ("Indemnified Contributor")
against any losses, damages and costs (collectively "Losses")
arising from claims, lawsuits and other legal actions brought by a third
party against the Indemnified Contributor to the extent caused by the
acts or omissions of such Commercial Contributor in connection with its
distribution of the Program in a commercial product offering. The
obligations in this section do not apply to any claims or Losses
relating to any actual or alleged intellectual property infringement. In
order to qualify, an Indemnified Contributor must: a) promptly notify
the Commercial Contributor in writing of such claim, and b) allow the
Commercial Contributor to control, and cooperate with the Commercial
Contributor in, the defense and any related settlement negotiations. The
Indemnified Contributor may participate in any such claim at its own
expense.</p>
<p>For example, a Contributor might include the Program in a commercial
product offering, Product X. That Contributor is then a Commercial
Contributor. If that Commercial Contributor then makes performance
claims, or offers warranties related to Product X, those performance
claims and warranties are such Commercial Contributor's responsibility
alone. Under this section, the Commercial Contributor would have to
defend claims against the other Contributors related to those
performance claims and warranties, and if a court requires any other
Contributor to pay any damages as a result, the Commercial Contributor
must pay those damages.</p>
<p><b>5. NO WARRANTY</b></p>
<p>EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, THE PROGRAM IS
PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS
OF ANY KIND, EITHER EXPRESS OR IMPLIED INCLUDING, WITHOUT LIMITATION,
ANY WARRANTIES OR CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY
OR FITNESS FOR A PARTICULAR PURPOSE. Each Recipient is solely
responsible for determining the appropriateness of using and
distributing the Program and assumes all risks associated with its
exercise of rights under this Agreement , including but not limited to
the risks and costs of program errors, compliance with applicable laws,
damage to or loss of data, programs or equipment, and unavailability or
interruption of operations.</p>
<p><b>6. DISCLAIMER OF LIABILITY</b></p>
<p>EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, NEITHER RECIPIENT
NOR ANY CONTRIBUTORS SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT,
INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING
WITHOUT LIMITATION LOST PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OR
DISTRIBUTION OF THE PROGRAM OR THE EXERCISE OF ANY RIGHTS GRANTED
HEREUNDER, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.</p>
<p><b>7. GENERAL</b></p>
<p>If any provision of this Agreement is invalid or unenforceable under
applicable law, it shall not affect the validity or enforceability of
the remainder of the terms of this Agreement, and without further action
by the parties hereto, such provision shall be reformed to the minimum
extent necessary to make such provision valid and enforceable.</p>
<p>If Recipient institutes patent litigation against any entity
(including a cross-claim or counterclaim in a lawsuit) alleging that the
Program itself (excluding combinations of the Program with other
software or hardware) infringes such Recipient's patent(s), then such
Recipient's rights granted under Section 2(b) shall terminate as of the
date such litigation is filed.</p>
<p>All Recipient's rights under this Agreement shall terminate if it
fails to comply with any of the material terms or conditions of this
Agreement and does not cure such failure in a reasonable period of time
after becoming aware of such noncompliance. If all Recipient's rights
under this Agreement terminate, Recipient agrees to cease use and
distribution of the Program as soon as reasonably practicable. However,
Recipient's obligations under this Agreement and any licenses granted by
Recipient relating to the Program shall continue and survive.</p>
<p>Everyone is permitted to copy and distribute copies of this
Agreement, but in order to avoid inconsistency the Agreement is
copyrighted and may only be modified in the following manner. The
Agreement Steward reserves the right to publish new versions (including
revisions) of this Agreement from time to time. No one other than the
Agreement Steward has the right to modify this Agreement. The Eclipse
Foundation is the initial Agreement Steward. The Eclipse Foundation may
assign the responsibility to serve as the Agreement Steward to a
suitable separate entity. Each new version of the Agreement will be
given a distinguishing version number. The Program (including
Contributions) may always be distributed subject to the version of the
Agreement under which it was received. In addition, after a new version
of the Agreement is published, Contributor may elect to distribute the
Program (including its Contributions) under the new version. Except as
expressly stated in Sections 2(a) and 2(b) above, Recipient receives no
rights or licenses to the intellectual property of any Contributor under
this Agreement, whether expressly, by implication, estoppel or
otherwise. All rights in the Program not expressly granted under this
Agreement are reserved.</p>
<p>This Agreement is governed by the laws of the State of New York and
the intellectual property laws of the United States of America. No party
to this Agreement will bring a legal action under this Agreement more
than one year after the cause of action arose. Each party waives its
rights to a jury trial in any resulting litigation.</p>
</body>
</html>

View File

@ -27,7 +27,7 @@ public final class FlingBehavior extends AppBarLayout.Behavior {
private boolean allowScroll = true; private boolean allowScroll = true;
private final Rect globalRect = new Rect(); private final Rect globalRect = new Rect();
private final List<Integer> skipInterceptionOfElements = Arrays.asList( private final List<Integer> skipInterceptionOfElements = Arrays.asList(
R.id.playQueuePanel, R.id.playbackSeekBar, R.id.itemsListPanel, R.id.playbackSeekBar,
R.id.playPauseButton, R.id.playPreviousButton, R.id.playNextButton); R.id.playPauseButton, R.id.playPreviousButton, R.id.playNextButton);
@Override @Override

View File

@ -6,38 +6,13 @@ import android.content.Context;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.os.Build; import android.os.Build;
import android.util.Log; import android.util.Log;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.multidex.MultiDexApplication; import androidx.multidex.MultiDexApplication;
import androidx.preference.PreferenceManager; import androidx.preference.PreferenceManager;
import com.nostra13.universalimageloader.cache.memory.impl.LRULimitedMemoryCache; import com.nostra13.universalimageloader.cache.memory.impl.LRULimitedMemoryCache;
import com.nostra13.universalimageloader.core.ImageLoader; import com.nostra13.universalimageloader.core.ImageLoader;
import com.nostra13.universalimageloader.core.ImageLoaderConfiguration; import com.nostra13.universalimageloader.core.ImageLoaderConfiguration;
import org.acra.ACRA;
import org.acra.config.ACRAConfigurationException;
import org.acra.config.CoreConfiguration;
import org.acra.config.CoreConfigurationBuilder;
import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.downloader.Downloader;
import org.schabi.newpipe.report.ErrorActivity;
import org.schabi.newpipe.report.ErrorInfo;
import org.schabi.newpipe.report.UserAction;
import org.schabi.newpipe.settings.SettingsActivity;
import org.schabi.newpipe.util.ExceptionUtils;
import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.util.ServiceHelper;
import org.schabi.newpipe.util.StateSaver;
import java.io.IOException;
import java.io.InterruptedIOException;
import java.net.SocketException;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import io.reactivex.rxjava3.disposables.Disposable; import io.reactivex.rxjava3.disposables.Disposable;
import io.reactivex.rxjava3.exceptions.CompositeException; import io.reactivex.rxjava3.exceptions.CompositeException;
import io.reactivex.rxjava3.exceptions.MissingBackpressureException; import io.reactivex.rxjava3.exceptions.MissingBackpressureException;
@ -45,6 +20,26 @@ import io.reactivex.rxjava3.exceptions.OnErrorNotImplementedException;
import io.reactivex.rxjava3.exceptions.UndeliverableException; import io.reactivex.rxjava3.exceptions.UndeliverableException;
import io.reactivex.rxjava3.functions.Consumer; import io.reactivex.rxjava3.functions.Consumer;
import io.reactivex.rxjava3.plugins.RxJavaPlugins; import io.reactivex.rxjava3.plugins.RxJavaPlugins;
import java.io.IOException;
import java.io.InterruptedIOException;
import java.net.SocketException;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import org.acra.ACRA;
import org.acra.config.ACRAConfigurationException;
import org.acra.config.CoreConfiguration;
import org.acra.config.CoreConfigurationBuilder;
import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.downloader.Downloader;
import org.schabi.newpipe.ktx.ExceptionUtils;
import org.schabi.newpipe.report.ErrorActivity;
import org.schabi.newpipe.report.ErrorInfo;
import org.schabi.newpipe.report.UserAction;
import org.schabi.newpipe.settings.SettingsActivity;
import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.util.ServiceHelper;
import org.schabi.newpipe.util.StateSaver;
/* /*
* Copyright (C) Hans-Christoph Steiner 2016 <hans@eds.org> * Copyright (C) Hans-Christoph Steiner 2016 <hans@eds.org>
@ -67,8 +62,10 @@ import io.reactivex.rxjava3.plugins.RxJavaPlugins;
public class App extends MultiDexApplication { public class App extends MultiDexApplication {
protected static final String TAG = App.class.toString(); protected static final String TAG = App.class.toString();
private static App app; private static App app;
public static final String PACKAGE_NAME = BuildConfig.APPLICATION_ID;
@Nullable private Disposable disposable = null; @Nullable
private Disposable disposable = null;
@NonNull @NonNull
public static App getApp() { public static App getApp() {
@ -93,7 +90,7 @@ public class App extends MultiDexApplication {
NewPipe.init(getDownloader(), NewPipe.init(getDownloader(),
Localization.getPreferredLocalization(this), Localization.getPreferredLocalization(this),
Localization.getPreferredContentCountry(this)); Localization.getPreferredContentCountry(this));
Localization.init(getApplicationContext()); Localization.initPrettyTime(Localization.resolvePrettyTime(getApplicationContext()));
StateSaver.init(this); StateSaver.init(this);
initNotificationChannels(); initNotificationChannels();
@ -242,8 +239,9 @@ public class App extends MultiDexApplication {
String name = getString(R.string.notification_channel_name); String name = getString(R.string.notification_channel_name);
String description = getString(R.string.notification_channel_description); String description = getString(R.string.notification_channel_description);
// Keep this below DEFAULT to avoid making noise on every notification update // Keep this below DEFAULT to avoid making noise on every notification update for the main
final int importance = NotificationManager.IMPORTANCE_LOW; // and update channels
int importance = NotificationManager.IMPORTANCE_LOW;
final NotificationChannel mainChannel = new NotificationChannel(id, name, importance); final NotificationChannel mainChannel = new NotificationChannel(id, name, importance);
mainChannel.setDescription(description); mainChannel.setDescription(description);
@ -255,9 +253,17 @@ public class App extends MultiDexApplication {
final NotificationChannel appUpdateChannel = new NotificationChannel(id, name, importance); final NotificationChannel appUpdateChannel = new NotificationChannel(id, name, importance);
appUpdateChannel.setDescription(description); appUpdateChannel.setDescription(description);
id = getString(R.string.hash_channel_id);
name = getString(R.string.hash_channel_name);
description = getString(R.string.hash_channel_description);
importance = NotificationManager.IMPORTANCE_HIGH;
final NotificationChannel hashChannel = new NotificationChannel(id, name, importance);
hashChannel.setDescription(description);
final NotificationManager notificationManager = getSystemService(NotificationManager.class); final NotificationManager notificationManager = getSystemService(NotificationManager.class);
notificationManager.createNotificationChannels(Arrays.asList(mainChannel, notificationManager.createNotificationChannels(Arrays.asList(mainChannel,
appUpdateChannel)); appUpdateChannel, hashChannel));
} }
protected boolean isDisposedRxExceptionsReported() { protected boolean isDisposedRxExceptionsReported() {

View File

@ -48,13 +48,13 @@ public final class CheckForNewAppVersion {
private static final String GITHUB_APK_SHA1 private static final String GITHUB_APK_SHA1
= "B0:2E:90:7C:1C:D6:FC:57:C3:35:F0:88:D0:8F:50:5F:94:E4:D2:15"; = "B0:2E:90:7C:1C:D6:FC:57:C3:35:F0:88:D0:8F:50:5F:94:E4:D2:15";
private static final String NEWPIPE_API_URL = "https://newpipe.schabi.org/api/data.json"; private static final String NEWPIPE_API_URL = "https://newpipe.net/api/data.json";
/** /**
* Method to get the apk's SHA1 key. See https://stackoverflow.com/questions/9293019/#22506133. * Method to get the APK's SHA1 key. See https://stackoverflow.com/questions/9293019/#22506133.
* *
* @param application The application * @param application The application
* @return String with the apk's SHA1 fingeprint in hexadecimal * @return String with the APK's SHA1 fingerprint in hexadecimal
*/ */
@NonNull @NonNull
private static String getCertificateSHA1Fingerprint(@NonNull final Application application) { private static String getCertificateSHA1Fingerprint(@NonNull final Application application) {

View File

@ -20,6 +20,8 @@
package org.schabi.newpipe; package org.schabi.newpipe;
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
import android.content.BroadcastReceiver; import android.content.BroadcastReceiver;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
@ -39,26 +41,27 @@ import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.widget.AdapterView; import android.widget.AdapterView;
import android.widget.ArrayAdapter; import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.FrameLayout; import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.Spinner; import android.widget.Spinner;
import android.widget.TextView;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.appcompat.app.ActionBar; import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.ActionBarDrawerToggle; import androidx.appcompat.app.ActionBarDrawerToggle;
import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.Toolbar; import androidx.core.app.ActivityCompat;
import androidx.core.view.GravityCompat; import androidx.core.view.GravityCompat;
import androidx.drawerlayout.widget.DrawerLayout; import androidx.drawerlayout.widget.DrawerLayout;
import androidx.fragment.app.Fragment; import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager; import androidx.fragment.app.FragmentManager;
import androidx.preference.PreferenceManager; import androidx.preference.PreferenceManager;
import com.google.android.material.bottomsheet.BottomSheetBehavior; import com.google.android.material.bottomsheet.BottomSheetBehavior;
import com.google.android.material.navigation.NavigationView; import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import org.schabi.newpipe.databinding.ActivityMainBinding;
import org.schabi.newpipe.databinding.DrawerHeaderBinding;
import org.schabi.newpipe.databinding.DrawerLayoutBinding;
import org.schabi.newpipe.databinding.InstanceSpinnerLayoutBinding;
import org.schabi.newpipe.databinding.ToolbarLayoutBinding;
import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.StreamingService; import org.schabi.newpipe.extractor.StreamingService;
import org.schabi.newpipe.extractor.exceptions.ExtractionException; import org.schabi.newpipe.extractor.exceptions.ExtractionException;
@ -67,7 +70,7 @@ import org.schabi.newpipe.fragments.BackPressable;
import org.schabi.newpipe.fragments.MainFragment; import org.schabi.newpipe.fragments.MainFragment;
import org.schabi.newpipe.fragments.detail.VideoDetailFragment; import org.schabi.newpipe.fragments.detail.VideoDetailFragment;
import org.schabi.newpipe.fragments.list.search.SearchFragment; import org.schabi.newpipe.fragments.list.search.SearchFragment;
import org.schabi.newpipe.player.VideoPlayer; import org.schabi.newpipe.player.Player;
import org.schabi.newpipe.player.event.OnKeyDownListener; import org.schabi.newpipe.player.event.OnKeyDownListener;
import org.schabi.newpipe.player.helper.PlayerHolder; import org.schabi.newpipe.player.helper.PlayerHolder;
import org.schabi.newpipe.player.playqueue.PlayQueue; import org.schabi.newpipe.player.playqueue.PlayQueue;
@ -86,25 +89,18 @@ import org.schabi.newpipe.util.TLSSocketFactoryCompat;
import org.schabi.newpipe.util.ThemeHelper; import org.schabi.newpipe.util.ThemeHelper;
import org.schabi.newpipe.views.FocusOverlayView; import org.schabi.newpipe.views.FocusOverlayView;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
public class MainActivity extends AppCompatActivity { public class MainActivity extends AppCompatActivity {
private static final String TAG = "MainActivity"; private static final String TAG = "MainActivity";
public static final boolean DEBUG = !BuildConfig.BUILD_TYPE.equals("release"); public static final boolean DEBUG = !BuildConfig.BUILD_TYPE.equals("release");
private ActivityMainBinding mainBinding;
private DrawerHeaderBinding drawerHeaderBinding;
private DrawerLayoutBinding drawerLayoutBinding;
private ToolbarLayoutBinding toolbarLayoutBinding;
private ActionBarDrawerToggle toggle; private ActionBarDrawerToggle toggle;
private DrawerLayout drawer;
private NavigationView drawerItems;
private ImageView headerServiceIcon;
private TextView headerServiceView;
private Button toggleServiceButton;
private boolean servicesShown = false; private boolean servicesShown = false;
private ImageView serviceArrow;
private BroadcastReceiver broadcastReceiver; private BroadcastReceiver broadcastReceiver;
@ -129,7 +125,7 @@ public class MainActivity extends AppCompatActivity {
+ "savedInstanceState = [" + savedInstanceState + "]"); + "savedInstanceState = [" + savedInstanceState + "]");
} }
// enable TLS1.1/1.2 for kitkat devices, to fix download and play for mediaCCC sources // enable TLS1.1/1.2 for kitkat devices, to fix download and play for media.ccc.de sources
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.KITKAT) { if (Build.VERSION.SDK_INT == Build.VERSION_CODES.KITKAT) {
TLSSocketFactoryCompat.setAsDefault(); TLSSocketFactoryCompat.setAsDefault();
} }
@ -137,13 +133,19 @@ public class MainActivity extends AppCompatActivity {
assureCorrectAppLanguage(this); assureCorrectAppLanguage(this);
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mainBinding = ActivityMainBinding.inflate(getLayoutInflater());
drawerLayoutBinding = mainBinding.drawerLayout;
drawerHeaderBinding = DrawerHeaderBinding.bind(drawerLayoutBinding.navigation
.getHeaderView(0));
toolbarLayoutBinding = mainBinding.toolbarLayout;
setContentView(mainBinding.getRoot());
if (getSupportFragmentManager().getBackStackEntryCount() == 0) { if (getSupportFragmentManager().getBackStackEntryCount() == 0) {
initFragments(); initFragments();
} }
setSupportActionBar(findViewById(R.id.toolbar)); setSupportActionBar(toolbarLayoutBinding.toolbar);
try { try {
setupDrawer(); setupDrawer();
} catch (final Exception e) { } catch (final Exception e) {
@ -157,10 +159,6 @@ public class MainActivity extends AppCompatActivity {
} }
private void setupDrawer() throws Exception { private void setupDrawer() throws Exception {
final Toolbar toolbar = findViewById(R.id.toolbar);
drawer = findViewById(R.id.drawer_layout);
drawerItems = findViewById(R.id.navigation);
//Tabs //Tabs
final int currentServiceId = ServiceHelper.getSelectedServiceId(this); final int currentServiceId = ServiceHelper.getSelectedServiceId(this);
final StreamingService service = NewPipe.getService(currentServiceId); final StreamingService service = NewPipe.getService(currentServiceId);
@ -168,43 +166,43 @@ public class MainActivity extends AppCompatActivity {
int kioskId = 0; int kioskId = 0;
for (final String ks : service.getKioskList().getAvailableKiosks()) { for (final String ks : service.getKioskList().getAvailableKiosks()) {
drawerItems.getMenu() drawerLayoutBinding.navigation.getMenu()
.add(R.id.menu_tabs_group, kioskId, 0, KioskTranslator .add(R.id.menu_tabs_group, kioskId, 0, KioskTranslator
.getTranslatedKioskName(ks, this)) .getTranslatedKioskName(ks, this))
.setIcon(KioskTranslator.getKioskIcon(ks, this)); .setIcon(KioskTranslator.getKioskIcon(ks, this));
kioskId++; kioskId++;
} }
drawerItems.getMenu() drawerLayoutBinding.navigation.getMenu()
.add(R.id.menu_tabs_group, ITEM_ID_SUBSCRIPTIONS, ORDER, .add(R.id.menu_tabs_group, ITEM_ID_SUBSCRIPTIONS, ORDER,
R.string.tab_subscriptions) R.string.tab_subscriptions)
.setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.ic_channel)); .setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.ic_channel));
drawerItems.getMenu() drawerLayoutBinding.navigation.getMenu()
.add(R.id.menu_tabs_group, ITEM_ID_FEED, ORDER, R.string.fragment_feed_title) .add(R.id.menu_tabs_group, ITEM_ID_FEED, ORDER, R.string.fragment_feed_title)
.setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.ic_rss)); .setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.ic_rss));
drawerItems.getMenu() drawerLayoutBinding.navigation.getMenu()
.add(R.id.menu_tabs_group, ITEM_ID_BOOKMARKS, ORDER, R.string.tab_bookmarks) .add(R.id.menu_tabs_group, ITEM_ID_BOOKMARKS, ORDER, R.string.tab_bookmarks)
.setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.ic_bookmark)); .setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.ic_bookmark));
drawerItems.getMenu() drawerLayoutBinding.navigation.getMenu()
.add(R.id.menu_tabs_group, ITEM_ID_DOWNLOADS, ORDER, R.string.downloads) .add(R.id.menu_tabs_group, ITEM_ID_DOWNLOADS, ORDER, R.string.downloads)
.setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.ic_file_download)); .setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.ic_file_download));
drawerItems.getMenu() drawerLayoutBinding.navigation.getMenu()
.add(R.id.menu_tabs_group, ITEM_ID_HISTORY, ORDER, R.string.action_history) .add(R.id.menu_tabs_group, ITEM_ID_HISTORY, ORDER, R.string.action_history)
.setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.ic_history)); .setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.ic_history));
//Settings and About //Settings and About
drawerItems.getMenu() drawerLayoutBinding.navigation.getMenu()
.add(R.id.menu_options_about_group, ITEM_ID_SETTINGS, ORDER, R.string.settings) .add(R.id.menu_options_about_group, ITEM_ID_SETTINGS, ORDER, R.string.settings)
.setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.ic_settings)); .setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.ic_settings));
drawerItems.getMenu() drawerLayoutBinding.navigation.getMenu()
.add(R.id.menu_options_about_group, ITEM_ID_ABOUT, ORDER, R.string.tab_about) .add(R.id.menu_options_about_group, ITEM_ID_ABOUT, ORDER, R.string.tab_about)
.setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.ic_info_outline)); .setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.ic_info_outline));
toggle = new ActionBarDrawerToggle(this, drawer, toolbar, R.string.drawer_open, toggle = new ActionBarDrawerToggle(this, mainBinding.getRoot(),
R.string.drawer_close); toolbarLayoutBinding.toolbar, R.string.drawer_open, R.string.drawer_close);
toggle.syncState(); toggle.syncState();
drawer.addDrawerListener(toggle); mainBinding.getRoot().addDrawerListener(toggle);
drawer.addDrawerListener(new DrawerLayout.SimpleDrawerListener() { mainBinding.getRoot().addDrawerListener(new DrawerLayout.SimpleDrawerListener() {
private int lastService; private int lastService;
@Override @Override
@ -218,12 +216,12 @@ public class MainActivity extends AppCompatActivity {
toggleServices(); toggleServices();
} }
if (lastService != ServiceHelper.getSelectedServiceId(MainActivity.this)) { if (lastService != ServiceHelper.getSelectedServiceId(MainActivity.this)) {
new Handler(Looper.getMainLooper()).post(MainActivity.this::recreate); ActivityCompat.recreate(MainActivity.this);
} }
} }
}); });
drawerItems.setNavigationItemSelectedListener(this::drawerItemSelected); drawerLayoutBinding.navigation.setNavigationItemSelectedListener(this::drawerItemSelected);
setupDrawerHeader(); setupDrawerHeader();
} }
@ -246,15 +244,17 @@ public class MainActivity extends AppCompatActivity {
return false; return false;
} }
drawer.closeDrawers(); mainBinding.getRoot().closeDrawers();
return true; return true;
} }
private void changeService(final MenuItem item) { private void changeService(final MenuItem item) {
drawerItems.getMenu().getItem(ServiceHelper.getSelectedServiceId(this)) drawerLayoutBinding.navigation.getMenu()
.getItem(ServiceHelper.getSelectedServiceId(this))
.setChecked(false); .setChecked(false);
ServiceHelper.setSelectedServiceId(this, item.getItemId()); ServiceHelper.setSelectedServiceId(this, item.getItemId());
drawerItems.getMenu().getItem(ServiceHelper.getSelectedServiceId(this)) drawerLayoutBinding.navigation.getMenu()
.getItem(ServiceHelper.getSelectedServiceId(this))
.setChecked(true); .setChecked(true);
} }
@ -306,26 +306,19 @@ public class MainActivity extends AppCompatActivity {
} }
private void setupDrawerHeader() { private void setupDrawerHeader() {
final NavigationView navigationView = findViewById(R.id.navigation); drawerHeaderBinding.drawerHeaderActionButton.setOnClickListener(view -> toggleServices());
final View hView = navigationView.getHeaderView(0);
serviceArrow = hView.findViewById(R.id.drawer_arrow);
headerServiceIcon = hView.findViewById(R.id.drawer_header_service_icon);
headerServiceView = hView.findViewById(R.id.drawer_header_service_view);
toggleServiceButton = hView.findViewById(R.id.drawer_header_action_button);
toggleServiceButton.setOnClickListener(view -> toggleServices());
// If the current app name is bigger than the default "NewPipe" (7 chars), // If the current app name is bigger than the default "NewPipe" (7 chars),
// let the text view grow a little more as well. // let the text view grow a little more as well.
if (getString(R.string.app_name).length() > "NewPipe".length()) { if (getString(R.string.app_name).length() > "NewPipe".length()) {
final TextView headerTitle = hView.findViewById(R.id.drawer_header_newpipe_title); final ViewGroup.LayoutParams layoutParams =
final ViewGroup.LayoutParams layoutParams = headerTitle.getLayoutParams(); drawerHeaderBinding.drawerHeaderNewpipeTitle.getLayoutParams();
layoutParams.width = ViewGroup.LayoutParams.WRAP_CONTENT; layoutParams.width = ViewGroup.LayoutParams.WRAP_CONTENT;
headerTitle.setLayoutParams(layoutParams); drawerHeaderBinding.drawerHeaderNewpipeTitle.setLayoutParams(layoutParams);
headerTitle.setMaxLines(2); drawerHeaderBinding.drawerHeaderNewpipeTitle.setMaxLines(2);
headerTitle.setMinWidth(getResources() drawerHeaderBinding.drawerHeaderNewpipeTitle.setMinWidth(getResources()
.getDimensionPixelSize(R.dimen.drawer_header_newpipe_title_default_width)); .getDimensionPixelSize(R.dimen.drawer_header_newpipe_title_default_width));
headerTitle.setMaxWidth(getResources() drawerHeaderBinding.drawerHeaderNewpipeTitle.setMaxWidth(getResources()
.getDimensionPixelSize(R.dimen.drawer_header_newpipe_title_max_width)); .getDimensionPixelSize(R.dimen.drawer_header_newpipe_title_max_width));
} }
} }
@ -333,9 +326,9 @@ public class MainActivity extends AppCompatActivity {
private void toggleServices() { private void toggleServices() {
servicesShown = !servicesShown; servicesShown = !servicesShown;
drawerItems.getMenu().removeGroup(R.id.menu_services_group); drawerLayoutBinding.navigation.getMenu().removeGroup(R.id.menu_services_group);
drawerItems.getMenu().removeGroup(R.id.menu_tabs_group); drawerLayoutBinding.navigation.getMenu().removeGroup(R.id.menu_tabs_group);
drawerItems.getMenu().removeGroup(R.id.menu_options_about_group); drawerLayoutBinding.navigation.getMenu().removeGroup(R.id.menu_options_about_group);
if (servicesShown) { if (servicesShown) {
showServices(); showServices();
@ -349,13 +342,13 @@ public class MainActivity extends AppCompatActivity {
} }
private void showServices() { private void showServices() {
serviceArrow.setImageResource(R.drawable.ic_arrow_drop_up_white_24dp); drawerHeaderBinding.drawerArrow.setImageResource(R.drawable.ic_arrow_drop_up_white_24dp);
for (final StreamingService s : NewPipe.getServices()) { for (final StreamingService s : NewPipe.getServices()) {
final String title = s.getServiceInfo().getName() final String title = s.getServiceInfo().getName()
+ (ServiceHelper.isBeta(s) ? " (beta)" : ""); + (ServiceHelper.isBeta(s) ? " (beta)" : "");
final MenuItem menuItem = drawerItems.getMenu() final MenuItem menuItem = drawerLayoutBinding.navigation.getMenu()
.add(R.id.menu_services_group, s.getServiceId(), ORDER, title) .add(R.id.menu_services_group, s.getServiceId(), ORDER, title)
.setIcon(ServiceHelper.getIcon(s.getServiceId())); .setIcon(ServiceHelper.getIcon(s.getServiceId()));
@ -364,21 +357,22 @@ public class MainActivity extends AppCompatActivity {
enhancePeertubeMenu(s, menuItem); enhancePeertubeMenu(s, menuItem);
} }
} }
drawerItems.getMenu().getItem(ServiceHelper.getSelectedServiceId(this)) drawerLayoutBinding.navigation.getMenu()
.getItem(ServiceHelper.getSelectedServiceId(this))
.setChecked(true); .setChecked(true);
} }
private void enhancePeertubeMenu(final StreamingService s, final MenuItem menuItem) { private void enhancePeertubeMenu(final StreamingService s, final MenuItem menuItem) {
final PeertubeInstance currentInstace = PeertubeHelper.getCurrentInstance(); final PeertubeInstance currentInstance = PeertubeHelper.getCurrentInstance();
menuItem.setTitle(currentInstace.getName() + (ServiceHelper.isBeta(s) ? " (beta)" : "")); menuItem.setTitle(currentInstance.getName() + (ServiceHelper.isBeta(s) ? " (beta)" : ""));
final Spinner spinner = (Spinner) LayoutInflater.from(this) final Spinner spinner = InstanceSpinnerLayoutBinding.inflate(LayoutInflater.from(this))
.inflate(R.layout.instance_spinner_layout, null); .getRoot();
final List<PeertubeInstance> instances = PeertubeHelper.getInstanceList(this); final List<PeertubeInstance> instances = PeertubeHelper.getInstanceList(this);
final List<String> items = new ArrayList<>(); final List<String> items = new ArrayList<>();
int defaultSelect = 0; int defaultSelect = 0;
for (final PeertubeInstance instance : instances) { for (final PeertubeInstance instance : instances) {
items.add(instance.getName()); items.add(instance.getName());
if (instance.getUrl().equals(currentInstace.getUrl())) { if (instance.getUrl().equals(currentInstance.getUrl())) {
defaultSelect = items.size() - 1; defaultSelect = items.size() - 1;
} }
} }
@ -397,7 +391,7 @@ public class MainActivity extends AppCompatActivity {
} }
PeertubeHelper.selectInstance(newInstance, getApplicationContext()); PeertubeHelper.selectInstance(newInstance, getApplicationContext());
changeService(menuItem); changeService(menuItem);
drawer.closeDrawers(); mainBinding.getRoot().closeDrawers();
new Handler(Looper.getMainLooper()).postDelayed(() -> { new Handler(Looper.getMainLooper()).postDelayed(() -> {
getSupportFragmentManager().popBackStack(null, getSupportFragmentManager().popBackStack(null,
FragmentManager.POP_BACK_STACK_INCLUSIVE); FragmentManager.POP_BACK_STACK_INCLUSIVE);
@ -414,7 +408,7 @@ public class MainActivity extends AppCompatActivity {
} }
private void showTabs() throws ExtractionException { private void showTabs() throws ExtractionException {
serviceArrow.setImageResource(R.drawable.ic_arrow_drop_down_white_24dp); drawerHeaderBinding.drawerArrow.setImageResource(R.drawable.ic_arrow_drop_down_white_24dp);
//Tabs //Tabs
final int currentServiceId = ServiceHelper.getSelectedServiceId(this); final int currentServiceId = ServiceHelper.getSelectedServiceId(this);
@ -423,34 +417,34 @@ public class MainActivity extends AppCompatActivity {
int kioskId = 0; int kioskId = 0;
for (final String ks : service.getKioskList().getAvailableKiosks()) { for (final String ks : service.getKioskList().getAvailableKiosks()) {
drawerItems.getMenu() drawerLayoutBinding.navigation.getMenu()
.add(R.id.menu_tabs_group, kioskId, ORDER, .add(R.id.menu_tabs_group, kioskId, ORDER,
KioskTranslator.getTranslatedKioskName(ks, this)) KioskTranslator.getTranslatedKioskName(ks, this))
.setIcon(KioskTranslator.getKioskIcon(ks, this)); .setIcon(KioskTranslator.getKioskIcon(ks, this));
kioskId++; kioskId++;
} }
drawerItems.getMenu() drawerLayoutBinding.navigation.getMenu()
.add(R.id.menu_tabs_group, ITEM_ID_SUBSCRIPTIONS, ORDER, R.string.tab_subscriptions) .add(R.id.menu_tabs_group, ITEM_ID_SUBSCRIPTIONS, ORDER, R.string.tab_subscriptions)
.setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.ic_channel)); .setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.ic_channel));
drawerItems.getMenu() drawerLayoutBinding.navigation.getMenu()
.add(R.id.menu_tabs_group, ITEM_ID_FEED, ORDER, R.string.fragment_feed_title) .add(R.id.menu_tabs_group, ITEM_ID_FEED, ORDER, R.string.fragment_feed_title)
.setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.ic_rss)); .setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.ic_rss));
drawerItems.getMenu() drawerLayoutBinding.navigation.getMenu()
.add(R.id.menu_tabs_group, ITEM_ID_BOOKMARKS, ORDER, R.string.tab_bookmarks) .add(R.id.menu_tabs_group, ITEM_ID_BOOKMARKS, ORDER, R.string.tab_bookmarks)
.setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.ic_bookmark)); .setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.ic_bookmark));
drawerItems.getMenu() drawerLayoutBinding.navigation.getMenu()
.add(R.id.menu_tabs_group, ITEM_ID_DOWNLOADS, ORDER, R.string.downloads) .add(R.id.menu_tabs_group, ITEM_ID_DOWNLOADS, ORDER, R.string.downloads)
.setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.ic_file_download)); .setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.ic_file_download));
drawerItems.getMenu() drawerLayoutBinding.navigation.getMenu()
.add(R.id.menu_tabs_group, ITEM_ID_HISTORY, ORDER, R.string.action_history) .add(R.id.menu_tabs_group, ITEM_ID_HISTORY, ORDER, R.string.action_history)
.setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.ic_history)); .setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.ic_history));
//Settings and About //Settings and About
drawerItems.getMenu() drawerLayoutBinding.navigation.getMenu()
.add(R.id.menu_options_about_group, ITEM_ID_SETTINGS, ORDER, R.string.settings) .add(R.id.menu_options_about_group, ITEM_ID_SETTINGS, ORDER, R.string.settings)
.setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.ic_settings)); .setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.ic_settings));
drawerItems.getMenu() drawerLayoutBinding.navigation.getMenu()
.add(R.id.menu_options_about_group, ITEM_ID_ABOUT, ORDER, R.string.tab_about) .add(R.id.menu_options_about_group, ITEM_ID_ABOUT, ORDER, R.string.tab_about)
.setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.ic_info_outline)); .setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.ic_info_outline));
} }
@ -470,21 +464,23 @@ public class MainActivity extends AppCompatActivity {
protected void onResume() { protected void onResume() {
assureCorrectAppLanguage(this); assureCorrectAppLanguage(this);
// Change the date format to match the selected language on resume // Change the date format to match the selected language on resume
Localization.init(getApplicationContext()); Localization.initPrettyTime(Localization.resolvePrettyTime(getApplicationContext()));
super.onResume(); super.onResume();
// Close drawer on return, and don't show animation, // Close drawer on return, and don't show animation,
// so it looks like the drawer isn't open when the user returns to MainActivity // so it looks like the drawer isn't open when the user returns to MainActivity
drawer.closeDrawer(GravityCompat.START, false); mainBinding.getRoot().closeDrawer(GravityCompat.START, false);
try { try {
final int selectedServiceId = ServiceHelper.getSelectedServiceId(this); final int selectedServiceId = ServiceHelper.getSelectedServiceId(this);
final String selectedServiceName = NewPipe.getService(selectedServiceId) final String selectedServiceName = NewPipe.getService(selectedServiceId)
.getServiceInfo().getName(); .getServiceInfo().getName();
headerServiceView.setText(selectedServiceName); drawerHeaderBinding.drawerHeaderServiceView.setText(selectedServiceName);
headerServiceIcon.setImageResource(ServiceHelper.getIcon(selectedServiceId)); drawerHeaderBinding.drawerHeaderServiceIcon.setImageResource(ServiceHelper
.getIcon(selectedServiceId));
headerServiceView.post(() -> headerServiceView.setSelected(true)); drawerHeaderBinding.drawerHeaderServiceView.post(() -> drawerHeaderBinding
toggleServiceButton.setContentDescription( .drawerHeaderServiceView.setSelected(true));
drawerHeaderBinding.drawerHeaderActionButton.setContentDescription(
getString(R.string.drawer_header_description) + selectedServiceName); getString(R.string.drawer_header_description) + selectedServiceName);
} catch (final Exception e) { } catch (final Exception e) {
ErrorActivity.reportUiError(this, e); ErrorActivity.reportUiError(this, e);
@ -497,10 +493,7 @@ public class MainActivity extends AppCompatActivity {
Log.d(TAG, "Theme has changed, recreating activity..."); Log.d(TAG, "Theme has changed, recreating activity...");
} }
sharedPreferences.edit().putBoolean(Constants.KEY_THEME_CHANGE, false).apply(); sharedPreferences.edit().putBoolean(Constants.KEY_THEME_CHANGE, false).apply();
// https://stackoverflow.com/questions/10844112/ ActivityCompat.recreate(this);
// Briefly, let the activity resume
// properly posting the recreate call to end of the message queue
new Handler(Looper.getMainLooper()).post(MainActivity.this::recreate);
} }
if (sharedPreferences.getBoolean(Constants.KEY_MAIN_PAGE_CHANGE, false)) { if (sharedPreferences.getBoolean(Constants.KEY_MAIN_PAGE_CHANGE, false)) {
@ -513,7 +506,8 @@ public class MainActivity extends AppCompatActivity {
final boolean isHistoryEnabled = sharedPreferences.getBoolean( final boolean isHistoryEnabled = sharedPreferences.getBoolean(
getString(R.string.enable_watch_history_key), true); getString(R.string.enable_watch_history_key), true);
drawerItems.getMenu().findItem(ITEM_ID_HISTORY).setVisible(isHistoryEnabled); drawerLayoutBinding.navigation.getMenu().findItem(ITEM_ID_HISTORY)
.setVisible(isHistoryEnabled);
} }
@Override @Override
@ -557,9 +551,8 @@ public class MainActivity extends AppCompatActivity {
} }
if (DeviceUtils.isTv(this)) { if (DeviceUtils.isTv(this)) {
final View drawerPanel = findViewById(R.id.navigation); if (mainBinding.getRoot().isDrawerOpen(drawerLayoutBinding.navigation)) {
if (drawer.isDrawerOpen(drawerPanel)) { mainBinding.getRoot().closeDrawers();
drawer.closeDrawers();
return; return;
} }
} }
@ -585,9 +578,7 @@ public class MainActivity extends AppCompatActivity {
// delegate the back press to it // delegate the back press to it
if (fragmentPlayer instanceof BackPressable) { if (fragmentPlayer instanceof BackPressable) {
if (!((BackPressable) fragmentPlayer).onBackPressed()) { if (!((BackPressable) fragmentPlayer).onBackPressed()) {
final FrameLayout bottomSheetLayout = BottomSheetBehavior.from(mainBinding.fragmentPlayerHolder)
findViewById(R.id.fragment_player_holder);
BottomSheetBehavior.from(bottomSheetLayout)
.setState(BottomSheetBehavior.STATE_COLLAPSED); .setState(BottomSheetBehavior.STATE_COLLAPSED);
} }
return; return;
@ -670,8 +661,7 @@ public class MainActivity extends AppCompatActivity {
final Fragment fragment final Fragment fragment
= getSupportFragmentManager().findFragmentById(R.id.fragment_holder); = getSupportFragmentManager().findFragmentById(R.id.fragment_holder);
if (!(fragment instanceof SearchFragment)) { if (!(fragment instanceof SearchFragment)) {
findViewById(R.id.toolbar).findViewById(R.id.toolbar_search_container) toolbarLayoutBinding.toolbarSearchContainer.getRoot().setVisibility(View.GONE);
.setVisibility(View.GONE);
} }
final ActionBar actionBar = getSupportActionBar(); final ActionBar actionBar = getSupportActionBar();
@ -732,21 +722,20 @@ public class MainActivity extends AppCompatActivity {
return; return;
} }
final Toolbar toolbar = findViewById(R.id.toolbar);
final Fragment fragment = getSupportFragmentManager() final Fragment fragment = getSupportFragmentManager()
.findFragmentById(R.id.fragment_holder); .findFragmentById(R.id.fragment_holder);
if (fragment instanceof MainFragment) { if (fragment instanceof MainFragment) {
getSupportActionBar().setDisplayHomeAsUpEnabled(false); getSupportActionBar().setDisplayHomeAsUpEnabled(false);
if (toggle != null) { if (toggle != null) {
toggle.syncState(); toggle.syncState();
toolbar.setNavigationOnClickListener(v -> drawer.openDrawer(GravityCompat.START)); toolbarLayoutBinding.toolbar.setNavigationOnClickListener(v -> mainBinding.getRoot()
drawer.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNDEFINED); .openDrawer(GravityCompat.START));
mainBinding.getRoot().setDrawerLockMode(DrawerLayout.LOCK_MODE_UNDEFINED);
} }
} else { } else {
drawer.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED); mainBinding.getRoot().setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED);
getSupportActionBar().setDisplayHomeAsUpEnabled(true); getSupportActionBar().setDisplayHomeAsUpEnabled(true);
toolbar.setNavigationOnClickListener(v -> onHomeButtonPressed()); toolbarLayoutBinding.toolbar.setNavigationOnClickListener(v -> onHomeButtonPressed());
} }
} }
@ -770,7 +759,7 @@ public class MainActivity extends AppCompatActivity {
switch (linkType) { switch (linkType) {
case STREAM: case STREAM:
final String intentCacheKey = intent.getStringExtra( final String intentCacheKey = intent.getStringExtra(
VideoPlayer.PLAY_QUEUE_KEY); Player.PLAY_QUEUE_KEY);
final PlayQueue playQueue = intentCacheKey != null final PlayQueue playQueue = intentCacheKey != null
? SerializedCache.getInstance() ? SerializedCache.getInstance()
.take(intentCacheKey, PlayQueue.class) .take(intentCacheKey, PlayQueue.class)
@ -854,9 +843,8 @@ public class MainActivity extends AppCompatActivity {
} }
private boolean bottomSheetHiddenOrCollapsed() { private boolean bottomSheetHiddenOrCollapsed() {
final FrameLayout bottomSheetLayout = findViewById(R.id.fragment_player_holder);
final BottomSheetBehavior<FrameLayout> bottomSheetBehavior = final BottomSheetBehavior<FrameLayout> bottomSheetBehavior =
BottomSheetBehavior.from(bottomSheetLayout); BottomSheetBehavior.from(mainBinding.fragmentPlayerHolder);
final int sheetState = bottomSheetBehavior.getState(); final int sheetState = bottomSheetBehavior.getState();
return sheetState == BottomSheetBehavior.STATE_HIDDEN return sheetState == BottomSheetBehavior.STATE_HIDDEN

View File

@ -8,20 +8,18 @@ import android.util.Log;
import android.view.Menu; import android.view.Menu;
import android.view.MenuItem; import android.view.MenuItem;
import android.webkit.CookieManager; import android.webkit.CookieManager;
import android.webkit.WebResourceRequest;
import android.webkit.WebSettings; import android.webkit.WebSettings;
import android.webkit.WebView; import android.webkit.WebView;
import android.webkit.WebViewClient;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.appcompat.app.ActionBar; import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.Toolbar;
import androidx.core.app.NavUtils; import androidx.core.app.NavUtils;
import androidx.preference.PreferenceManager; import androidx.preference.PreferenceManager;
import androidx.webkit.WebViewClientCompat;
import org.schabi.newpipe.databinding.ActivityRecaptchaBinding;
import org.schabi.newpipe.util.ThemeHelper; import org.schabi.newpipe.util.ThemeHelper;
import java.io.UnsupportedEncodingException; import java.io.UnsupportedEncodingException;
@ -53,46 +51,37 @@ public class ReCaptchaActivity extends AppCompatActivity {
public static final String YT_URL = "https://www.youtube.com"; public static final String YT_URL = "https://www.youtube.com";
public static final String RECAPTCHA_COOKIES_KEY = "recaptcha_cookies"; public static final String RECAPTCHA_COOKIES_KEY = "recaptcha_cookies";
private WebView webView; public static String sanitizeRecaptchaUrl(@Nullable final String url) {
if (url == null || url.trim().isEmpty()) {
return YT_URL; // YouTube is the most likely service to have thrown a recaptcha
} else {
// remove "pbj=1" parameter from YouYube urls, as it makes the page JSON and not HTML
return url.replace("&pbj=1", "").replace("pbj=1&", "").replace("?pbj=1", "");
}
}
private ActivityRecaptchaBinding recaptchaBinding;
private String foundCookies = ""; private String foundCookies = "";
@Override @Override
protected void onCreate(final Bundle savedInstanceState) { protected void onCreate(final Bundle savedInstanceState) {
ThemeHelper.setTheme(this); ThemeHelper.setTheme(this);
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
setContentView(R.layout.activity_recaptcha);
final Toolbar toolbar = findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
String url = getIntent().getStringExtra(RECAPTCHA_URL_EXTRA); recaptchaBinding = ActivityRecaptchaBinding.inflate(getLayoutInflater());
if (url == null || url.isEmpty()) { setContentView(recaptchaBinding.getRoot());
url = YT_URL; setSupportActionBar(recaptchaBinding.toolbar);
}
final String url = sanitizeRecaptchaUrl(getIntent().getStringExtra(RECAPTCHA_URL_EXTRA));
// set return to Cancel by default // set return to Cancel by default
setResult(RESULT_CANCELED); setResult(RESULT_CANCELED);
webView = findViewById(R.id.reCaptchaWebView);
// enable Javascript // enable Javascript
final WebSettings webSettings = webView.getSettings(); final WebSettings webSettings = recaptchaBinding.reCaptchaWebView.getSettings();
webSettings.setJavaScriptEnabled(true); webSettings.setJavaScriptEnabled(true);
webSettings.setUserAgentString(DownloaderImpl.USER_AGENT);
webView.setWebViewClient(new WebViewClient() { recaptchaBinding.reCaptchaWebView.setWebViewClient(new WebViewClientCompat() {
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
@Override
public boolean shouldOverrideUrlLoading(final WebView view,
final WebResourceRequest request) {
final String url = request.getUrl().toString();
if (MainActivity.DEBUG) {
Log.d(TAG, "shouldOverrideUrlLoading: request.url=" + url);
}
handleCookiesFromUrl(url);
return false;
}
@Override @Override
public boolean shouldOverrideUrlLoading(final WebView view, final String url) { public boolean shouldOverrideUrlLoading(final WebView view, final String url) {
if (MainActivity.DEBUG) { if (MainActivity.DEBUG) {
@ -111,17 +100,16 @@ public class ReCaptchaActivity extends AppCompatActivity {
}); });
// cleaning cache, history and cookies from webView // cleaning cache, history and cookies from webView
webView.clearCache(true); recaptchaBinding.reCaptchaWebView.clearCache(true);
webView.clearHistory(); recaptchaBinding.reCaptchaWebView.clearHistory();
final android.webkit.CookieManager cookieManager = CookieManager.getInstance(); final CookieManager cookieManager = CookieManager.getInstance();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
cookieManager.removeAllCookies(aBoolean -> { cookieManager.removeAllCookies(value -> { });
});
} else { } else {
cookieManager.removeAllCookie(); cookieManager.removeAllCookie();
} }
webView.loadUrl(url); recaptchaBinding.reCaptchaWebView.loadUrl(url);
} }
@Override @Override
@ -145,18 +133,16 @@ public class ReCaptchaActivity extends AppCompatActivity {
@Override @Override
public boolean onOptionsItemSelected(final MenuItem item) { public boolean onOptionsItemSelected(final MenuItem item) {
final int id = item.getItemId(); if (item.getItemId() == R.id.menu_item_done) {
switch (id) {
case R.id.menu_item_done:
saveCookiesAndFinish(); saveCookiesAndFinish();
return true; return true;
default:
return false;
} }
return false;
} }
private void saveCookiesAndFinish() { private void saveCookiesAndFinish() {
handleCookiesFromUrl(webView.getUrl()); // try to get cookies of unclosed page // try to get cookies of unclosed page
handleCookiesFromUrl(recaptchaBinding.reCaptchaWebView.getUrl());
if (MainActivity.DEBUG) { if (MainActivity.DEBUG) {
Log.d(TAG, "saveCookiesAndFinish: foundCookies=" + foundCookies); Log.d(TAG, "saveCookiesAndFinish: foundCookies=" + foundCookies);
} }

View File

@ -14,7 +14,6 @@ import android.view.LayoutInflater;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.widget.Button; import android.widget.Button;
import android.widget.LinearLayout;
import android.widget.RadioButton; import android.widget.RadioButton;
import android.widget.RadioGroup; import android.widget.RadioGroup;
import android.widget.Toast; import android.widget.Toast;
@ -26,10 +25,13 @@ import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.content.res.AppCompatResources; import androidx.appcompat.content.res.AppCompatResources;
import androidx.core.app.NotificationCompat; import androidx.core.app.NotificationCompat;
import androidx.core.app.ServiceCompat;
import androidx.core.widget.TextViewCompat; import androidx.core.widget.TextViewCompat;
import androidx.fragment.app.FragmentManager; import androidx.fragment.app.FragmentManager;
import androidx.preference.PreferenceManager; import androidx.preference.PreferenceManager;
import org.schabi.newpipe.databinding.ListRadioIconItemBinding;
import org.schabi.newpipe.databinding.SingleChoiceDialogViewBinding;
import org.schabi.newpipe.download.DownloadDialog; import org.schabi.newpipe.download.DownloadDialog;
import org.schabi.newpipe.extractor.Info; import org.schabi.newpipe.extractor.Info;
import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipe.extractor.NewPipe;
@ -267,9 +269,8 @@ public class RouterActivity extends AppCompatActivity {
final Context themeWrapperContext = getThemeWrapperContext(); final Context themeWrapperContext = getThemeWrapperContext();
final LayoutInflater inflater = LayoutInflater.from(themeWrapperContext); final LayoutInflater inflater = LayoutInflater.from(themeWrapperContext);
final LinearLayout rootLayout = (LinearLayout) inflater.inflate( final RadioGroup radioGroup = SingleChoiceDialogViewBinding.inflate(getLayoutInflater())
R.layout.single_choice_dialog_view, null, false); .list;
final RadioGroup radioGroup = rootLayout.findViewById(android.R.id.list);
final DialogInterface.OnClickListener dialogButtonsClickListener = (dialog, which) -> { final DialogInterface.OnClickListener dialogButtonsClickListener = (dialog, which) -> {
final int indexOfChild = radioGroup.indexOfChild( final int indexOfChild = radioGroup.indexOfChild(
@ -322,8 +323,7 @@ public class RouterActivity extends AppCompatActivity {
int id = 12345; int id = 12345;
for (final AdapterChoiceItem item : choices) { for (final AdapterChoiceItem item : choices) {
final RadioButton radioButton final RadioButton radioButton = ListRadioIconItemBinding.inflate(inflater).getRoot();
= (RadioButton) inflater.inflate(R.layout.list_radio_icon_item, null);
radioButton.setText(item.description); radioButton.setText(item.description);
TextViewCompat.setCompoundDrawablesRelativeWithIntrinsicBounds(radioButton, TextViewCompat.setCompoundDrawablesRelativeWithIntrinsicBounds(radioButton,
AppCompatResources.getDrawable(getApplicationContext(), item.icon), AppCompatResources.getDrawable(getApplicationContext(), item.icon),
@ -696,7 +696,7 @@ public class RouterActivity extends AppCompatActivity {
@Override @Override
public void onDestroy() { public void onDestroy() {
super.onDestroy(); super.onDestroy();
stopForeground(true); ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE);
if (fetcher != null) { if (fetcher != null) {
fetcher.dispose(); fetcher.dispose();
} }

View File

@ -6,22 +6,19 @@ import android.view.LayoutInflater;
import android.view.MenuItem; import android.view.MenuItem;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.widget.TextView;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.Toolbar;
import androidx.fragment.app.Fragment; import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentActivity; import androidx.fragment.app.FragmentActivity;
import androidx.recyclerview.widget.RecyclerView;
import androidx.viewpager2.adapter.FragmentStateAdapter; import androidx.viewpager2.adapter.FragmentStateAdapter;
import androidx.viewpager2.widget.ViewPager2;
import com.google.android.material.tabs.TabLayout;
import com.google.android.material.tabs.TabLayoutMediator; import com.google.android.material.tabs.TabLayoutMediator;
import org.schabi.newpipe.BuildConfig; import org.schabi.newpipe.BuildConfig;
import org.schabi.newpipe.R; import org.schabi.newpipe.R;
import org.schabi.newpipe.databinding.ActivityAboutBinding;
import org.schabi.newpipe.databinding.FragmentAboutBinding;
import org.schabi.newpipe.util.ThemeHelper; import org.schabi.newpipe.util.ThemeHelper;
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage; import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
@ -32,76 +29,72 @@ public class AboutActivity extends AppCompatActivity {
* List of all software components. * List of all software components.
*/ */
private static final SoftwareComponent[] SOFTWARE_COMPONENTS = { private static final SoftwareComponent[] SOFTWARE_COMPONENTS = {
new SoftwareComponent("Giga Get", "2014 - 2015", "Peter Cai", new SoftwareComponent("ACRA", "2013", "Kevin Gaudin",
"https://github.com/ACRA/acra", StandardLicenses.APACHE2),
new SoftwareComponent("AndroidX", "2005 - 2011", "The Android Open Source Project",
"https://developer.android.com/jetpack", StandardLicenses.APACHE2),
new SoftwareComponent("CircleImageView", "2014 - 2020", "Henning Dodenhof",
"https://github.com/hdodenhof/CircleImageView",
StandardLicenses.APACHE2),
new SoftwareComponent("ExoPlayer", "2014 - 2020", "Google, Inc.",
"https://github.com/google/ExoPlayer", StandardLicenses.APACHE2),
new SoftwareComponent("GigaGet", "2014 - 2015", "Peter Cai",
"https://github.com/PaperAirplane-Dev-Team/GigaGet", StandardLicenses.GPL3), "https://github.com/PaperAirplane-Dev-Team/GigaGet", StandardLicenses.GPL3),
new SoftwareComponent("Groupie", "2016", "Lisa Wray",
"https://github.com/lisawray/groupie", StandardLicenses.MIT),
new SoftwareComponent("Icepick", "2015", "Frankie Sardo",
"https://github.com/frankiesardo/icepick", StandardLicenses.EPL1),
new SoftwareComponent("Jsoup", "2009 - 2020", "Jonathan Hedley",
"https://github.com/jhy/jsoup", StandardLicenses.MIT),
new SoftwareComponent("Markwon", "2019", "Dimitry Ivanov",
"https://github.com/noties/Markwon", StandardLicenses.APACHE2),
new SoftwareComponent("Material Components for Android", "2016 - 2020", "Google, Inc.",
"https://github.com/material-components/material-components-android",
StandardLicenses.APACHE2),
new SoftwareComponent("NewPipe Extractor", "2017 - 2020", "Christian Schabesberger", new SoftwareComponent("NewPipe Extractor", "2017 - 2020", "Christian Schabesberger",
"https://github.com/TeamNewPipe/NewPipeExtractor", StandardLicenses.GPL3), "https://github.com/TeamNewPipe/NewPipeExtractor", StandardLicenses.GPL3),
new SoftwareComponent("Jsoup", "2017", "Jonathan Hedley", new SoftwareComponent("NoNonsense-FilePicker", "2016", "Jonas Kalderstam",
"https://github.com/jhy/jsoup", StandardLicenses.MIT), "https://github.com/spacecowboy/NoNonsense-FilePicker",
new SoftwareComponent("Rhino", "2015", "Mozilla", StandardLicenses.MPL2),
"https://www.mozilla.org/rhino/", StandardLicenses.MPL2), new SoftwareComponent("OkHttp", "2019", "Square, Inc.",
new SoftwareComponent("ACRA", "2013", "Kevin Gaudin", "https://square.github.io/okhttp/", StandardLicenses.APACHE2),
"http://www.acra.ch", StandardLicenses.APACHE2), new SoftwareComponent("PrettyTime", "2012 - 2020", "Lincoln Baxter, III",
"https://github.com/ocpsoft/prettytime", StandardLicenses.APACHE2),
new SoftwareComponent("RxAndroid", "2015", "The RxAndroid authors",
"https://github.com/ReactiveX/RxAndroid", StandardLicenses.APACHE2),
new SoftwareComponent("RxBinding", "2015", "Jake Wharton",
"https://github.com/JakeWharton/RxBinding", StandardLicenses.APACHE2),
new SoftwareComponent("RxJava", "2016 - 2020", "RxJava Contributors",
"https://github.com/ReactiveX/RxJava", StandardLicenses.APACHE2),
new SoftwareComponent("Universal Image Loader", "2011 - 2015", "Sergey Tarasevich", new SoftwareComponent("Universal Image Loader", "2011 - 2015", "Sergey Tarasevich",
"https://github.com/nostra13/Android-Universal-Image-Loader", "https://github.com/nostra13/Android-Universal-Image-Loader",
StandardLicenses.APACHE2), StandardLicenses.APACHE2),
new SoftwareComponent("CircleImageView", "2014 - 2020", "Henning Dodenhof",
"https://github.com/hdodenhof/CircleImageView", StandardLicenses.APACHE2),
new SoftwareComponent("NoNonsense-FilePicker", "2016", "Jonas Kalderstam",
"https://github.com/spacecowboy/NoNonsense-FilePicker", StandardLicenses.MPL2),
new SoftwareComponent("ExoPlayer", "2014 - 2020", "Google Inc",
"https://github.com/google/ExoPlayer", StandardLicenses.APACHE2),
new SoftwareComponent("RxAndroid", "2015 - 2018", "The RxAndroid authors",
"https://github.com/ReactiveX/RxAndroid", StandardLicenses.APACHE2),
new SoftwareComponent("RxJava", "2016 - 2020", "RxJava Contributors",
"https://github.com/ReactiveX/RxJava", StandardLicenses.APACHE2),
new SoftwareComponent("RxBinding", "2015 - 2018", "Jake Wharton",
"https://github.com/JakeWharton/RxBinding", StandardLicenses.APACHE2),
new SoftwareComponent("PrettyTime", "2012 - 2020", "Lincoln Baxter, III",
"https://github.com/ocpsoft/prettytime", StandardLicenses.APACHE2),
new SoftwareComponent("Markwon", "2017 - 2020", "Noties",
"https://github.com/noties/Markwon", StandardLicenses.APACHE2),
new SoftwareComponent("Groupie", "2016", "Lisa Wray",
"https://github.com/lisawray/groupie", StandardLicenses.MIT)
}; };
private static final int POS_ABOUT = 0; private static final int POS_ABOUT = 0;
private static final int POS_LICENSE = 1; private static final int POS_LICENSE = 1;
private static final int TOTAL_COUNT = 2; private static final int TOTAL_COUNT = 2;
/**
* The {@link RecyclerView.Adapter} that will provide
* fragments for each of the sections. We use a
* {@link FragmentStateAdapter} derivative, which will keep every
* loaded fragment in memory.
*/
private SectionsPagerAdapter mSectionsPagerAdapter;
/**
* The {@link ViewPager2} that will host the section contents.
*/
private ViewPager2 mViewPager;
@Override @Override
protected void onCreate(final Bundle savedInstanceState) { protected void onCreate(final Bundle savedInstanceState) {
assureCorrectAppLanguage(this); assureCorrectAppLanguage(this);
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
ThemeHelper.setTheme(this); ThemeHelper.setTheme(this);
this.setTitle(getString(R.string.title_activity_about)); setTitle(getString(R.string.title_activity_about));
setContentView(R.layout.activity_about); final ActivityAboutBinding aboutBinding = ActivityAboutBinding.inflate(getLayoutInflater());
setContentView(aboutBinding.getRoot());
final Toolbar toolbar = findViewById(R.id.toolbar); setSupportActionBar(aboutBinding.toolbar);
setSupportActionBar(toolbar);
getSupportActionBar().setDisplayHomeAsUpEnabled(true); getSupportActionBar().setDisplayHomeAsUpEnabled(true);
// Create the adapter that will return a fragment for each of the three // Create the adapter that will return a fragment for each of the three
// primary sections of the activity. // primary sections of the activity.
mSectionsPagerAdapter = new SectionsPagerAdapter(this); final SectionsPagerAdapter mSectionsPagerAdapter = new SectionsPagerAdapter(this);
// Set up the ViewPager with the sections adapter. // Set up the ViewPager with the sections adapter.
mViewPager = findViewById(R.id.container); aboutBinding.container.setAdapter(mSectionsPagerAdapter);
mViewPager.setAdapter(mSectionsPagerAdapter);
final TabLayout tabLayout = findViewById(R.id.tabs); new TabLayoutMediator(aboutBinding.tabs, aboutBinding.container, (tab, position) -> {
new TabLayoutMediator(tabLayout, mViewPager, (tab, position) -> {
switch (position) { switch (position) {
default: default:
case POS_ABOUT: case POS_ABOUT:
@ -131,7 +124,8 @@ public class AboutActivity extends AppCompatActivity {
* A placeholder fragment containing a simple view. * A placeholder fragment containing a simple view.
*/ */
public static class AboutFragment extends Fragment { public static class AboutFragment extends Fragment {
public AboutFragment() { } public AboutFragment() {
}
/** /**
* Created a new instance of this fragment for the given section number. * Created a new instance of this fragment for the given section number.
@ -143,33 +137,28 @@ public class AboutActivity extends AppCompatActivity {
} }
@Override @Override
public View onCreateView(final LayoutInflater inflater, final ViewGroup container, public View onCreateView(@NonNull final LayoutInflater inflater, final ViewGroup container,
final Bundle savedInstanceState) { final Bundle savedInstanceState) {
final View rootView = inflater.inflate(R.layout.fragment_about, container, false); final FragmentAboutBinding aboutBinding =
final Context context = this.getContext(); FragmentAboutBinding.inflate(inflater, container, false);
final Context context = getContext();
final TextView version = rootView.findViewById(R.id.app_version); aboutBinding.appVersion.setText(BuildConfig.VERSION_NAME);
version.setText(BuildConfig.VERSION_NAME);
final View githubLink = rootView.findViewById(R.id.github_link); aboutBinding.githubLink.setOnClickListener(nv ->
githubLink.setOnClickListener(nv ->
openUrlInBrowser(context, context.getString(R.string.github_url))); openUrlInBrowser(context, context.getString(R.string.github_url)));
final View donationLink = rootView.findViewById(R.id.donation_link); aboutBinding.donationLink.setOnClickListener(v ->
donationLink.setOnClickListener(v ->
openUrlInBrowser(context, context.getString(R.string.donation_url))); openUrlInBrowser(context, context.getString(R.string.donation_url)));
final View websiteLink = rootView.findViewById(R.id.website_link); aboutBinding.websiteLink.setOnClickListener(nv ->
websiteLink.setOnClickListener(nv ->
openUrlInBrowser(context, context.getString(R.string.website_url))); openUrlInBrowser(context, context.getString(R.string.website_url)));
final View privacyPolicyLink = rootView.findViewById(R.id.privacy_policy_link); aboutBinding.privacyPolicyLink.setOnClickListener(v ->
privacyPolicyLink.setOnClickListener(v ->
openUrlInBrowser(context, context.getString(R.string.privacy_policy_url))); openUrlInBrowser(context, context.getString(R.string.privacy_policy_url)));
return rootView; return aboutBinding.getRoot();
} }
} }
/** /**

View File

@ -7,18 +7,20 @@ import android.view.MenuInflater;
import android.view.MenuItem; import android.view.MenuItem;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.widget.TextView;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment; import androidx.fragment.app.Fragment;
import org.schabi.newpipe.R; import org.schabi.newpipe.R;
import org.schabi.newpipe.databinding.FragmentLicensesBinding;
import org.schabi.newpipe.databinding.ItemSoftwareComponentBinding;
import org.schabi.newpipe.util.ShareUtils; import org.schabi.newpipe.util.ShareUtils;
import java.io.Serializable; import java.io.Serializable;
import java.util.Arrays; import java.util.Arrays;
import java.util.Comparator; import java.util.Comparator;
import java.util.Objects;
import io.reactivex.rxjava3.disposables.CompositeDisposable; import io.reactivex.rxjava3.disposables.CompositeDisposable;
@ -35,12 +37,9 @@ public class LicenseFragment extends Fragment {
private final CompositeDisposable compositeDisposable = new CompositeDisposable(); private final CompositeDisposable compositeDisposable = new CompositeDisposable();
public static LicenseFragment newInstance(final SoftwareComponent[] softwareComponents) { public static LicenseFragment newInstance(final SoftwareComponent[] softwareComponents) {
if (softwareComponents == null) {
throw new NullPointerException("softwareComponents is null");
}
final LicenseFragment fragment = new LicenseFragment();
final Bundle bundle = new Bundle(); final Bundle bundle = new Bundle();
bundle.putParcelableArray(ARG_COMPONENTS, softwareComponents); bundle.putParcelableArray(ARG_COMPONENTS, Objects.requireNonNull(softwareComponents));
final LicenseFragment fragment = new LicenseFragment();
fragment.setArguments(bundle); fragment.setArguments(bundle);
return fragment; return fragment;
} }
@ -69,43 +68,42 @@ public class LicenseFragment extends Fragment {
@Nullable @Nullable
@Override @Override
public View onCreateView(final LayoutInflater inflater, @Nullable final ViewGroup container, public View onCreateView(@NonNull final LayoutInflater inflater,
@Nullable final ViewGroup container,
@Nullable final Bundle savedInstanceState) { @Nullable final Bundle savedInstanceState) {
final View rootView = inflater.inflate(R.layout.fragment_licenses, container, false); final FragmentLicensesBinding binding = FragmentLicensesBinding
final ViewGroup softwareComponentsView = rootView.findViewById(R.id.software_components); .inflate(inflater, container, false);
final View licenseLink = rootView.findViewById(R.id.app_read_license); binding.appReadLicense.setOnClickListener(v -> {
licenseLink.setOnClickListener(v -> {
activeLicense = StandardLicenses.GPL3; activeLicense = StandardLicenses.GPL3;
compositeDisposable.add(LicenseFragmentHelper.showLicense(getActivity(), compositeDisposable.add(LicenseFragmentHelper.showLicense(getActivity(),
StandardLicenses.GPL3)); StandardLicenses.GPL3));
}); });
for (final SoftwareComponent component : softwareComponents) { for (final SoftwareComponent component : softwareComponents) {
final View componentView = inflater final ItemSoftwareComponentBinding componentBinding = ItemSoftwareComponentBinding
.inflate(R.layout.item_software_component, container, false); .inflate(inflater, container, false);
final TextView softwareName = componentView.findViewById(R.id.name); componentBinding.name.setText(component.getName());
final TextView copyright = componentView.findViewById(R.id.copyright); componentBinding.copyright.setText(getString(R.string.copyright,
softwareName.setText(component.getName());
copyright.setText(getString(R.string.copyright,
component.getYears(), component.getYears(),
component.getCopyrightOwner(), component.getCopyrightOwner(),
component.getLicense().getAbbreviation())); component.getLicense().getAbbreviation()));
componentView.setTag(component); final View root = componentBinding.getRoot();
componentView.setOnClickListener(v -> { root.setTag(component);
root.setOnClickListener(v -> {
activeLicense = component.getLicense(); activeLicense = component.getLicense();
compositeDisposable.add(LicenseFragmentHelper.showLicense(getActivity(), compositeDisposable.add(LicenseFragmentHelper.showLicense(getActivity(),
component.getLicense())); component.getLicense()));
}); });
softwareComponentsView.addView(componentView); binding.softwareComponents.addView(root);
registerForContextMenu(componentView); registerForContextMenu(root);
} }
if (activeLicense != null) { if (activeLicense != null) {
compositeDisposable.add(LicenseFragmentHelper.showLicense(getActivity(), compositeDisposable.add(LicenseFragmentHelper.showLicense(getActivity(),
activeLicense)); activeLicense));
} }
return rootView; return binding.getRoot();
} }
@Override @Override

View File

@ -12,6 +12,8 @@ public final class StandardLicenses {
= new License("Mozilla Public License, Version 2.0", "MPL 2.0", "mpl2.html"); = new License("Mozilla Public License, Version 2.0", "MPL 2.0", "mpl2.html");
public static final License MIT public static final License MIT
= new License("MIT License", "MIT", "mit.html"); = new License("MIT License", "MIT", "mit.html");
public static final License EPL1
= new License("Eclipse Public License, Version 1.0", "EPL 1.0", "epl1.html");
private StandardLicenses() { } private StandardLicenses() { }
} }

View File

@ -9,7 +9,7 @@ import org.schabi.newpipe.database.stream.model.StreamStateEntity
import org.schabi.newpipe.extractor.stream.StreamInfoItem import org.schabi.newpipe.extractor.stream.StreamInfoItem
import kotlin.jvm.Throws import kotlin.jvm.Throws
class PlaylistStreamEntry( data class PlaylistStreamEntry(
@Embedded @Embedded
val streamEntity: StreamEntity, val streamEntity: StreamEntity,

View File

@ -9,10 +9,10 @@ import android.view.ViewTreeObserver;
import androidx.appcompat.app.ActionBar; import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.Toolbar;
import androidx.fragment.app.FragmentTransaction; import androidx.fragment.app.FragmentTransaction;
import org.schabi.newpipe.R; import org.schabi.newpipe.R;
import org.schabi.newpipe.databinding.ActivityDownloaderBinding;
import org.schabi.newpipe.util.DeviceUtils; import org.schabi.newpipe.util.DeviceUtils;
import org.schabi.newpipe.util.ThemeHelper; import org.schabi.newpipe.util.ThemeHelper;
import org.schabi.newpipe.views.FocusOverlayView; import org.schabi.newpipe.views.FocusOverlayView;
@ -35,11 +35,14 @@ public class DownloadActivity extends AppCompatActivity {
assureCorrectAppLanguage(this); assureCorrectAppLanguage(this);
ThemeHelper.setTheme(this); ThemeHelper.setTheme(this);
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_downloader);
final Toolbar toolbar = findViewById(R.id.toolbar); super.onCreate(savedInstanceState);
setSupportActionBar(toolbar);
final ActivityDownloaderBinding downloaderBinding =
ActivityDownloaderBinding.inflate(getLayoutInflater());
setContentView(downloaderBinding.getRoot());
setSupportActionBar(downloaderBinding.toolbarLayout.toolbar);
final ActionBar actionBar = getSupportActionBar(); final ActionBar actionBar = getSupportActionBar();
if (actionBar != null) { if (actionBar != null) {

View File

@ -16,12 +16,8 @@ import android.view.LayoutInflater;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.widget.AdapterView; import android.widget.AdapterView;
import android.widget.EditText;
import android.widget.RadioButton;
import android.widget.RadioGroup; import android.widget.RadioGroup;
import android.widget.SeekBar; import android.widget.SeekBar;
import android.widget.Spinner;
import android.widget.TextView;
import android.widget.Toast; import android.widget.Toast;
import androidx.annotation.IdRes; import androidx.annotation.IdRes;
@ -40,6 +36,7 @@ import com.nononsenseapps.filepicker.Utils;
import org.schabi.newpipe.MainActivity; import org.schabi.newpipe.MainActivity;
import org.schabi.newpipe.R; import org.schabi.newpipe.R;
import org.schabi.newpipe.RouterActivity; import org.schabi.newpipe.RouterActivity;
import org.schabi.newpipe.databinding.DownloadDialogBinding;
import org.schabi.newpipe.extractor.MediaFormat; import org.schabi.newpipe.extractor.MediaFormat;
import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.localization.Localization; import org.schabi.newpipe.extractor.localization.Localization;
@ -116,11 +113,7 @@ public class DownloadDialog extends DialogFragment
private final CompositeDisposable disposables = new CompositeDisposable(); private final CompositeDisposable disposables = new CompositeDisposable();
private EditText nameEditText; private DownloadDialogBinding dialogBinding;
private Spinner streamsSpinner;
private RadioGroup radioStreamsGroup;
private TextView threadsCountTextView;
private SeekBar threadsSeekBar;
private SharedPreferences prefs; private SharedPreferences prefs;
@ -277,38 +270,35 @@ public class DownloadDialog extends DialogFragment
@Override @Override
public void onViewCreated(@NonNull final View view, @Nullable final Bundle savedInstanceState) { public void onViewCreated(@NonNull final View view, @Nullable final Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState); super.onViewCreated(view, savedInstanceState);
nameEditText = view.findViewById(R.id.file_name); dialogBinding = DownloadDialogBinding.bind(view);
nameEditText.setText(FilenameUtils.createFilename(getContext(), currentInfo.getName()));
dialogBinding.fileName.setText(FilenameUtils.createFilename(getContext(),
currentInfo.getName()));
selectedAudioIndex = ListHelper selectedAudioIndex = ListHelper
.getDefaultAudioFormat(getContext(), currentInfo.getAudioStreams()); .getDefaultAudioFormat(getContext(), currentInfo.getAudioStreams());
selectedSubtitleIndex = getSubtitleIndexBy(subtitleStreamsAdapter.getAll()); selectedSubtitleIndex = getSubtitleIndexBy(subtitleStreamsAdapter.getAll());
streamsSpinner = view.findViewById(R.id.quality_spinner); dialogBinding.qualitySpinner.setOnItemSelectedListener(this);
streamsSpinner.setOnItemSelectedListener(this);
threadsCountTextView = view.findViewById(R.id.threads_count); dialogBinding.videoAudioGroup.setOnCheckedChangeListener(this);
threadsSeekBar = view.findViewById(R.id.threads);
radioStreamsGroup = view.findViewById(R.id.video_audio_group); initToolbar(dialogBinding.toolbarLayout.toolbar);
radioStreamsGroup.setOnCheckedChangeListener(this);
initToolbar(view.findViewById(R.id.toolbar));
setupDownloadOptions(); setupDownloadOptions();
prefs = PreferenceManager.getDefaultSharedPreferences(requireContext()); prefs = PreferenceManager.getDefaultSharedPreferences(requireContext());
final int threads = prefs.getInt(getString(R.string.default_download_threads), 3); final int threads = prefs.getInt(getString(R.string.default_download_threads), 3);
threadsCountTextView.setText(String.valueOf(threads)); dialogBinding.threadsCount.setText(String.valueOf(threads));
threadsSeekBar.setProgress(threads - 1); dialogBinding.threads.setProgress(threads - 1);
threadsSeekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { dialogBinding.threads.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
@Override @Override
public void onProgressChanged(final SeekBar seekbar, final int progress, public void onProgressChanged(final SeekBar seekbar, final int progress,
final boolean fromUser) { final boolean fromUser) {
final int newProgress = progress + 1; final int newProgress = progress + 1;
prefs.edit().putInt(getString(R.string.default_download_threads), newProgress) prefs.edit().putInt(getString(R.string.default_download_threads), newProgress)
.apply(); .apply();
threadsCountTextView.setText(String.valueOf(newProgress)); dialogBinding.threadsCount.setText(String.valueOf(newProgress));
} }
@Override @Override
@ -326,19 +316,19 @@ public class DownloadDialog extends DialogFragment
disposables.add(StreamSizeWrapper.fetchSizeForWrapper(wrappedVideoStreams) disposables.add(StreamSizeWrapper.fetchSizeForWrapper(wrappedVideoStreams)
.subscribe(result -> { .subscribe(result -> {
if (radioStreamsGroup.getCheckedRadioButtonId() == R.id.video_button) { if (dialogBinding.videoAudioGroup.getCheckedRadioButtonId() == R.id.video_button) {
setupVideoSpinner(); setupVideoSpinner();
} }
})); }));
disposables.add(StreamSizeWrapper.fetchSizeForWrapper(wrappedAudioStreams) disposables.add(StreamSizeWrapper.fetchSizeForWrapper(wrappedAudioStreams)
.subscribe(result -> { .subscribe(result -> {
if (radioStreamsGroup.getCheckedRadioButtonId() == R.id.audio_button) { if (dialogBinding.videoAudioGroup.getCheckedRadioButtonId() == R.id.audio_button) {
setupAudioSpinner(); setupAudioSpinner();
} }
})); }));
disposables.add(StreamSizeWrapper.fetchSizeForWrapper(wrappedSubtitleStreams) disposables.add(StreamSizeWrapper.fetchSizeForWrapper(wrappedSubtitleStreams)
.subscribe(result -> { .subscribe(result -> {
if (radioStreamsGroup.getCheckedRadioButtonId() == R.id.subtitle_button) { if (dialogBinding.videoAudioGroup.getCheckedRadioButtonId() == R.id.subtitle_button) {
setupSubtitleSpinner(); setupSubtitleSpinner();
} }
})); }));
@ -350,6 +340,12 @@ public class DownloadDialog extends DialogFragment
disposables.clear(); disposables.clear();
} }
@Override
public void onDestroyView() {
dialogBinding = null;
super.onDestroyView();
}
/*////////////////////////////////////////////////////////////////////////// /*//////////////////////////////////////////////////////////////////////////
// Radio group Video&Audio options - Listener // Radio group Video&Audio options - Listener
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/
@ -429,8 +425,8 @@ public class DownloadDialog extends DialogFragment
return; return;
} }
streamsSpinner.setAdapter(audioStreamsAdapter); dialogBinding.qualitySpinner.setAdapter(audioStreamsAdapter);
streamsSpinner.setSelection(selectedAudioIndex); dialogBinding.qualitySpinner.setSelection(selectedAudioIndex);
setRadioButtonsState(true); setRadioButtonsState(true);
} }
@ -439,8 +435,8 @@ public class DownloadDialog extends DialogFragment
return; return;
} }
streamsSpinner.setAdapter(videoStreamsAdapter); dialogBinding.qualitySpinner.setAdapter(videoStreamsAdapter);
streamsSpinner.setSelection(selectedVideoIndex); dialogBinding.qualitySpinner.setSelection(selectedVideoIndex);
setRadioButtonsState(true); setRadioButtonsState(true);
} }
@ -449,8 +445,8 @@ public class DownloadDialog extends DialogFragment
return; return;
} }
streamsSpinner.setAdapter(subtitleStreamsAdapter); dialogBinding.qualitySpinner.setAdapter(subtitleStreamsAdapter);
streamsSpinner.setSelection(selectedSubtitleIndex); dialogBinding.qualitySpinner.setSelection(selectedSubtitleIndex);
setRadioButtonsState(true); setRadioButtonsState(true);
} }
@ -475,7 +471,7 @@ public class DownloadDialog extends DialogFragment
break; break;
} }
threadsSeekBar.setEnabled(flag); dialogBinding.threads.setEnabled(flag);
} }
@Override @Override
@ -486,7 +482,7 @@ public class DownloadDialog extends DialogFragment
+ "parent = [" + parent + "], view = [" + view + "], " + "parent = [" + parent + "], view = [" + view + "], "
+ "position = [" + position + "], id = [" + id + "]"); + "position = [" + position + "], id = [" + id + "]");
} }
switch (radioStreamsGroup.getCheckedRadioButtonId()) { switch (dialogBinding.videoAudioGroup.getCheckedRadioButtonId()) {
case R.id.audio_button: case R.id.audio_button:
selectedAudioIndex = position; selectedAudioIndex = position;
break; break;
@ -506,16 +502,14 @@ public class DownloadDialog extends DialogFragment
protected void setupDownloadOptions() { protected void setupDownloadOptions() {
setRadioButtonsState(false); setRadioButtonsState(false);
final RadioButton audioButton = radioStreamsGroup.findViewById(R.id.audio_button);
final RadioButton videoButton = radioStreamsGroup.findViewById(R.id.video_button);
final RadioButton subtitleButton = radioStreamsGroup.findViewById(R.id.subtitle_button);
final boolean isVideoStreamsAvailable = videoStreamsAdapter.getCount() > 0; final boolean isVideoStreamsAvailable = videoStreamsAdapter.getCount() > 0;
final boolean isAudioStreamsAvailable = audioStreamsAdapter.getCount() > 0; final boolean isAudioStreamsAvailable = audioStreamsAdapter.getCount() > 0;
final boolean isSubtitleStreamsAvailable = subtitleStreamsAdapter.getCount() > 0; final boolean isSubtitleStreamsAvailable = subtitleStreamsAdapter.getCount() > 0;
audioButton.setVisibility(isAudioStreamsAvailable ? View.VISIBLE : View.GONE); dialogBinding.audioButton.setVisibility(isAudioStreamsAvailable ? View.VISIBLE : View.GONE);
videoButton.setVisibility(isVideoStreamsAvailable ? View.VISIBLE : View.GONE); dialogBinding.videoButton.setVisibility(isVideoStreamsAvailable ? View.VISIBLE : View.GONE);
subtitleButton.setVisibility(isSubtitleStreamsAvailable ? View.VISIBLE : View.GONE); dialogBinding.subtitleButton.setVisibility(isSubtitleStreamsAvailable
? View.VISIBLE : View.GONE);
prefs = PreferenceManager.getDefaultSharedPreferences(getContext()); prefs = PreferenceManager.getDefaultSharedPreferences(getContext());
final String defaultMedia = prefs.getString(getString(R.string.last_used_download_type), final String defaultMedia = prefs.getString(getString(R.string.last_used_download_type),
@ -523,24 +517,24 @@ public class DownloadDialog extends DialogFragment
if (isVideoStreamsAvailable if (isVideoStreamsAvailable
&& (defaultMedia.equals(getString(R.string.last_download_type_video_key)))) { && (defaultMedia.equals(getString(R.string.last_download_type_video_key)))) {
videoButton.setChecked(true); dialogBinding.videoButton.setChecked(true);
setupVideoSpinner(); setupVideoSpinner();
} else if (isAudioStreamsAvailable } else if (isAudioStreamsAvailable
&& (defaultMedia.equals(getString(R.string.last_download_type_audio_key)))) { && (defaultMedia.equals(getString(R.string.last_download_type_audio_key)))) {
audioButton.setChecked(true); dialogBinding.audioButton.setChecked(true);
setupAudioSpinner(); setupAudioSpinner();
} else if (isSubtitleStreamsAvailable } else if (isSubtitleStreamsAvailable
&& (defaultMedia.equals(getString(R.string.last_download_type_subtitle_key)))) { && (defaultMedia.equals(getString(R.string.last_download_type_subtitle_key)))) {
subtitleButton.setChecked(true); dialogBinding.subtitleButton.setChecked(true);
setupSubtitleSpinner(); setupSubtitleSpinner();
} else if (isVideoStreamsAvailable) { } else if (isVideoStreamsAvailable) {
videoButton.setChecked(true); dialogBinding.videoButton.setChecked(true);
setupVideoSpinner(); setupVideoSpinner();
} else if (isAudioStreamsAvailable) { } else if (isAudioStreamsAvailable) {
audioButton.setChecked(true); dialogBinding.audioButton.setChecked(true);
setupAudioSpinner(); setupAudioSpinner();
} else if (isSubtitleStreamsAvailable) { } else if (isSubtitleStreamsAvailable) {
subtitleButton.setChecked(true); dialogBinding.subtitleButton.setChecked(true);
setupSubtitleSpinner(); setupSubtitleSpinner();
} else { } else {
Toast.makeText(getContext(), R.string.no_streams_available_download, Toast.makeText(getContext(), R.string.no_streams_available_download,
@ -550,9 +544,9 @@ public class DownloadDialog extends DialogFragment
} }
private void setRadioButtonsState(final boolean enabled) { private void setRadioButtonsState(final boolean enabled) {
radioStreamsGroup.findViewById(R.id.audio_button).setEnabled(enabled); dialogBinding.audioButton.setEnabled(enabled);
radioStreamsGroup.findViewById(R.id.video_button).setEnabled(enabled); dialogBinding.videoButton.setEnabled(enabled);
radioStreamsGroup.findViewById(R.id.subtitle_button).setEnabled(enabled); dialogBinding.subtitleButton.setEnabled(enabled);
} }
private int getSubtitleIndexBy(final List<SubtitlesStream> streams) { private int getSubtitleIndexBy(final List<SubtitlesStream> streams) {
@ -582,7 +576,7 @@ public class DownloadDialog extends DialogFragment
} }
private String getNameEditText() { private String getNameEditText() {
final String str = nameEditText.getText().toString().trim(); final String str = dialogBinding.fileName.getText().toString().trim();
return FilenameUtils.createFilename(context, str.isEmpty() ? currentInfo.getName() : str); return FilenameUtils.createFilename(context, str.isEmpty() ? currentInfo.getName() : str);
} }
@ -619,7 +613,7 @@ public class DownloadDialog extends DialogFragment
String filename = getNameEditText().concat("."); String filename = getNameEditText().concat(".");
switch (radioStreamsGroup.getCheckedRadioButtonId()) { switch (dialogBinding.videoAudioGroup.getCheckedRadioButtonId()) {
case R.id.audio_button: case R.id.audio_button:
selectedMediaType = getString(R.string.last_download_type_audio_key); selectedMediaType = getString(R.string.last_download_type_audio_key);
mainStorage = mainStorageAudio; mainStorage = mainStorageAudio;
@ -669,7 +663,7 @@ public class DownloadDialog extends DialogFragment
filename, mime); filename, mime);
} else { } else {
File initialSavePath; File initialSavePath;
if (radioStreamsGroup.getCheckedRadioButtonId() == R.id.audio_button) { if (dialogBinding.videoAudioGroup.getCheckedRadioButtonId() == R.id.audio_button) {
initialSavePath = NewPipeSettings.getDir(Environment.DIRECTORY_MUSIC); initialSavePath = NewPipeSettings.getDir(Environment.DIRECTORY_MUSIC);
} else { } else {
initialSavePath = NewPipeSettings.getDir(Environment.DIRECTORY_MOVIES); initialSavePath = NewPipeSettings.getDir(Environment.DIRECTORY_MOVIES);
@ -862,7 +856,7 @@ public class DownloadDialog extends DialogFragment
final Stream selectedStream; final Stream selectedStream;
Stream secondaryStream = null; Stream secondaryStream = null;
final char kind; final char kind;
int threads = threadsSeekBar.getProgress() + 1; int threads = dialogBinding.threads.getProgress() + 1;
final String[] urls; final String[] urls;
final MissionRecoveryInfo[] recoveryInfo; final MissionRecoveryInfo[] recoveryInfo;
String psName = null; String psName = null;
@ -870,7 +864,7 @@ public class DownloadDialog extends DialogFragment
long nearLength = 0; long nearLength = 0;
// more download logic: select muxer, subtitle converter, etc. // more download logic: select muxer, subtitle converter, etc.
switch (radioStreamsGroup.getCheckedRadioButtonId()) { switch (dialogBinding.videoAudioGroup.getCheckedRadioButtonId()) {
case R.id.audio_button: case R.id.audio_button:
kind = 'a'; kind = 'a';
selectedStream = audioStreamsAdapter.getItem(selectedAudioIndex); selectedStream = audioStreamsAdapter.getItem(selectedAudioIndex);

View File

@ -22,10 +22,10 @@ import org.schabi.newpipe.ReCaptchaActivity;
import org.schabi.newpipe.extractor.exceptions.ContentNotAvailableException; import org.schabi.newpipe.extractor.exceptions.ContentNotAvailableException;
import org.schabi.newpipe.extractor.exceptions.ContentNotSupportedException; import org.schabi.newpipe.extractor.exceptions.ContentNotSupportedException;
import org.schabi.newpipe.extractor.exceptions.ReCaptchaException; import org.schabi.newpipe.extractor.exceptions.ReCaptchaException;
import org.schabi.newpipe.ktx.ExceptionUtils;
import org.schabi.newpipe.report.ErrorActivity; import org.schabi.newpipe.report.ErrorActivity;
import org.schabi.newpipe.report.ErrorInfo; import org.schabi.newpipe.report.ErrorInfo;
import org.schabi.newpipe.report.UserAction; import org.schabi.newpipe.report.UserAction;
import org.schabi.newpipe.util.ExceptionUtils;
import org.schabi.newpipe.util.InfoCache; import org.schabi.newpipe.util.InfoCache;
import java.util.Collections; import java.util.Collections;

View File

@ -3,7 +3,6 @@ package org.schabi.newpipe.fragments;
import android.content.Context; import android.content.Context;
import android.content.res.ColorStateList; import android.content.res.ColorStateList;
import android.os.Bundle; import android.os.Bundle;
import androidx.preference.PreferenceManager;
import android.util.Log; import android.util.Log;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.Menu; import android.view.Menu;
@ -19,12 +18,13 @@ import androidx.appcompat.app.AppCompatActivity;
import androidx.fragment.app.Fragment; import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager; import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentStatePagerAdapterMenuWorkaround; import androidx.fragment.app.FragmentStatePagerAdapterMenuWorkaround;
import androidx.viewpager.widget.ViewPager; import androidx.preference.PreferenceManager;
import com.google.android.material.tabs.TabLayout; import com.google.android.material.tabs.TabLayout;
import org.schabi.newpipe.BaseFragment; import org.schabi.newpipe.BaseFragment;
import org.schabi.newpipe.R; import org.schabi.newpipe.R;
import org.schabi.newpipe.databinding.FragmentMainBinding;
import org.schabi.newpipe.extractor.exceptions.ExtractionException; import org.schabi.newpipe.extractor.exceptions.ExtractionException;
import org.schabi.newpipe.report.ErrorActivity; import org.schabi.newpipe.report.ErrorActivity;
import org.schabi.newpipe.report.ErrorInfo; import org.schabi.newpipe.report.ErrorInfo;
@ -34,15 +34,13 @@ import org.schabi.newpipe.settings.tabs.TabsManager;
import org.schabi.newpipe.util.NavigationHelper; import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.ServiceHelper; import org.schabi.newpipe.util.ServiceHelper;
import org.schabi.newpipe.util.ThemeHelper; import org.schabi.newpipe.util.ThemeHelper;
import org.schabi.newpipe.views.ScrollableTabLayout;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
public class MainFragment extends BaseFragment implements TabLayout.OnTabSelectedListener { public class MainFragment extends BaseFragment implements TabLayout.OnTabSelectedListener {
private ViewPager viewPager; private FragmentMainBinding binding;
private SelectedTabsPagerAdapter pagerAdapter; private SelectedTabsPagerAdapter pagerAdapter;
private ScrollableTabLayout tabLayout;
private final List<Tab> tabsList = new ArrayList<>(); private final List<Tab> tabsList = new ArrayList<>();
private TabsManager tabsManager; private TabsManager tabsManager;
@ -90,13 +88,12 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte
protected void initViews(final View rootView, final Bundle savedInstanceState) { protected void initViews(final View rootView, final Bundle savedInstanceState) {
super.initViews(rootView, savedInstanceState); super.initViews(rootView, savedInstanceState);
tabLayout = rootView.findViewById(R.id.main_tab_layout); binding = FragmentMainBinding.bind(rootView);
viewPager = rootView.findViewById(R.id.pager);
tabLayout.setTabIconTint(ColorStateList.valueOf( binding.mainTabLayout.setTabIconTint(ColorStateList.valueOf(
ThemeHelper.resolveColorFromAttr(requireContext(), R.attr.colorAccent))); ThemeHelper.resolveColorFromAttr(requireContext(), R.attr.colorAccent)));
tabLayout.setupWithViewPager(viewPager); binding.mainTabLayout.setupWithViewPager(binding.pager);
tabLayout.addOnTabSelectedListener(this); binding.mainTabLayout.addOnTabSelectedListener(this);
setupTabs(); setupTabs();
} }
@ -120,8 +117,9 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte
public void onDestroy() { public void onDestroy() {
super.onDestroy(); super.onDestroy();
tabsManager.unsetSavedTabsListener(); tabsManager.unsetSavedTabsListener();
if (viewPager != null) { if (binding != null) {
viewPager.setAdapter(null); binding.pager.setAdapter(null);
binding = null;
} }
} }
@ -172,19 +170,19 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte
getChildFragmentManager(), tabsList); getChildFragmentManager(), tabsList);
} }
viewPager.setAdapter(null); binding.pager.setAdapter(null);
viewPager.setOffscreenPageLimit(tabsList.size()); binding.pager.setOffscreenPageLimit(tabsList.size());
viewPager.setAdapter(pagerAdapter); binding.pager.setAdapter(pagerAdapter);
updateTabsIconAndDescription(); updateTabsIconAndDescription();
updateTitleForTab(viewPager.getCurrentItem()); updateTitleForTab(binding.pager.getCurrentItem());
hasTabsChanged = false; hasTabsChanged = false;
} }
private void updateTabsIconAndDescription() { private void updateTabsIconAndDescription() {
for (int i = 0; i < tabsList.size(); i++) { for (int i = 0; i < tabsList.size(); i++) {
final TabLayout.Tab tabToSet = tabLayout.getTabAt(i); final TabLayout.Tab tabToSet = binding.mainTabLayout.getTabAt(i);
if (tabToSet != null) { if (tabToSet != null) {
final Tab tab = tabsList.get(i); final Tab tab = tabsList.get(i);
tabToSet.setIcon(tab.getTabIconRes(requireContext())); tabToSet.setIcon(tab.getTabIconRes(requireContext()));

View File

@ -16,7 +16,6 @@ import android.os.Bundle;
import android.os.Handler; import android.os.Handler;
import android.os.Looper; import android.os.Looper;
import android.provider.Settings; import android.provider.Settings;
import android.text.TextUtils;
import android.text.util.Linkify; import android.text.util.Linkify;
import android.util.DisplayMetrics; import android.util.DisplayMetrics;
import android.util.Log; import android.util.Log;
@ -50,7 +49,6 @@ import androidx.viewpager.widget.ViewPager;
import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.ExoPlaybackException;
import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.exoplayer2.PlaybackParameters;
import com.google.android.exoplayer2.Player;
import com.google.android.material.appbar.AppBarLayout; import com.google.android.material.appbar.AppBarLayout;
import com.google.android.material.bottomsheet.BottomSheetBehavior; import com.google.android.material.bottomsheet.BottomSheetBehavior;
import com.google.android.material.tabs.TabLayout; import com.google.android.material.tabs.TabLayout;
@ -81,9 +79,8 @@ import org.schabi.newpipe.fragments.list.videos.RelatedVideosFragment;
import org.schabi.newpipe.local.dialog.PlaylistAppendDialog; import org.schabi.newpipe.local.dialog.PlaylistAppendDialog;
import org.schabi.newpipe.local.dialog.PlaylistCreationDialog; import org.schabi.newpipe.local.dialog.PlaylistCreationDialog;
import org.schabi.newpipe.local.history.HistoryRecordManager; import org.schabi.newpipe.local.history.HistoryRecordManager;
import org.schabi.newpipe.player.BasePlayer; import org.schabi.newpipe.player.Player;
import org.schabi.newpipe.player.MainPlayer; import org.schabi.newpipe.player.MainPlayer;
import org.schabi.newpipe.player.VideoPlayerImpl;
import org.schabi.newpipe.player.event.OnKeyDownListener; import org.schabi.newpipe.player.event.OnKeyDownListener;
import org.schabi.newpipe.player.event.PlayerServiceExtendedEventListener; import org.schabi.newpipe.player.event.PlayerServiceExtendedEventListener;
import org.schabi.newpipe.player.helper.PlayerHelper; import org.schabi.newpipe.player.helper.PlayerHelper;
@ -122,12 +119,14 @@ import io.reactivex.rxjava3.disposables.CompositeDisposable;
import io.reactivex.rxjava3.disposables.Disposable; import io.reactivex.rxjava3.disposables.Disposable;
import io.reactivex.rxjava3.schedulers.Schedulers; import io.reactivex.rxjava3.schedulers.Schedulers;
import static android.text.TextUtils.isEmpty;
import static org.schabi.newpipe.extractor.StreamingService.ServiceInfo.MediaCapability.COMMENTS; import static org.schabi.newpipe.extractor.StreamingService.ServiceInfo.MediaCapability.COMMENTS;
import static org.schabi.newpipe.extractor.stream.StreamExtractor.NO_AGE_LIMIT; import static org.schabi.newpipe.extractor.stream.StreamExtractor.NO_AGE_LIMIT;
import static org.schabi.newpipe.player.helper.PlayerHelper.globalScreenOrientationLocked; import static org.schabi.newpipe.player.helper.PlayerHelper.globalScreenOrientationLocked;
import static org.schabi.newpipe.player.helper.PlayerHelper.isClearingQueueConfirmationRequired; import static org.schabi.newpipe.player.helper.PlayerHelper.isClearingQueueConfirmationRequired;
import static org.schabi.newpipe.player.playqueue.PlayQueueItem.RECOVERY_UNSET; import static org.schabi.newpipe.player.playqueue.PlayQueueItem.RECOVERY_UNSET;
import static org.schabi.newpipe.util.AnimationUtils.animateView; import static org.schabi.newpipe.util.AnimationUtils.animateView;
import static org.schabi.newpipe.util.ExtractorHelper.showMetaInfoInTextView;
public final class VideoDetailFragment public final class VideoDetailFragment
extends BaseStateFragment<StreamInfo> extends BaseStateFragment<StreamInfo>
@ -145,15 +144,15 @@ public final class VideoDetailFragment
private static final float MAX_PLAYER_HEIGHT = 0.7f; private static final float MAX_PLAYER_HEIGHT = 0.7f;
public static final String ACTION_SHOW_MAIN_PLAYER = public static final String ACTION_SHOW_MAIN_PLAYER =
"org.schabi.newpipe.VideoDetailFragment.ACTION_SHOW_MAIN_PLAYER"; App.PACKAGE_NAME + ".VideoDetailFragment.ACTION_SHOW_MAIN_PLAYER";
public static final String ACTION_HIDE_MAIN_PLAYER = public static final String ACTION_HIDE_MAIN_PLAYER =
"org.schabi.newpipe.VideoDetailFragment.ACTION_HIDE_MAIN_PLAYER"; App.PACKAGE_NAME + ".VideoDetailFragment.ACTION_HIDE_MAIN_PLAYER";
public static final String ACTION_PLAYER_STARTED = public static final String ACTION_PLAYER_STARTED =
"org.schabi.newpipe.VideoDetailFragment.ACTION_PLAYER_STARTED"; App.PACKAGE_NAME + ".VideoDetailFragment.ACTION_PLAYER_STARTED";
public static final String ACTION_VIDEO_FRAGMENT_RESUMED = public static final String ACTION_VIDEO_FRAGMENT_RESUMED =
"org.schabi.newpipe.VideoDetailFragment.ACTION_VIDEO_FRAGMENT_RESUMED"; App.PACKAGE_NAME + ".VideoDetailFragment.ACTION_VIDEO_FRAGMENT_RESUMED";
public static final String ACTION_VIDEO_FRAGMENT_STOPPED = public static final String ACTION_VIDEO_FRAGMENT_STOPPED =
"org.schabi.newpipe.VideoDetailFragment.ACTION_VIDEO_FRAGMENT_STOPPED"; App.PACKAGE_NAME + ".VideoDetailFragment.ACTION_VIDEO_FRAGMENT_STOPPED";
private static final String COMMENTS_TAB_TAG = "COMMENTS"; private static final String COMMENTS_TAB_TAG = "COMMENTS";
private static final String RELATED_TAB_TAG = "NEXT VIDEO"; private static final String RELATED_TAB_TAG = "NEXT VIDEO";
@ -218,6 +217,9 @@ public final class VideoDetailFragment
private TextView detailDurationView; private TextView detailDurationView;
private TextView detailPositionView; private TextView detailPositionView;
private View detailMetaInfoSeparator;
private TextView detailMetaInfoTextView;
private LinearLayout videoDescriptionRootLayout; private LinearLayout videoDescriptionRootLayout;
private TextView videoUploadDateView; private TextView videoUploadDateView;
private TextView videoDescriptionView; private TextView videoDescriptionView;
@ -251,14 +253,14 @@ public final class VideoDetailFragment
private ContentObserver settingsContentObserver; private ContentObserver settingsContentObserver;
private MainPlayer playerService; private MainPlayer playerService;
private VideoPlayerImpl player; private Player player;
/*////////////////////////////////////////////////////////////////////////// /*//////////////////////////////////////////////////////////////////////////
// Service management // Service management
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/
@Override @Override
public void onServiceConnected(final VideoPlayerImpl connectedPlayer, public void onServiceConnected(final Player connectedPlayer,
final MainPlayer connectedPlayerService, final MainPlayer connectedPlayerService,
final boolean playAfterConnect) { final boolean playAfterConnect) {
player = connectedPlayer; player = connectedPlayer;
@ -275,7 +277,9 @@ public final class VideoDetailFragment
// If the video is playing but orientation changed // If the video is playing but orientation changed
// let's make the video in fullscreen again // let's make the video in fullscreen again
checkLandscape(); checkLandscape();
} else if (player.isFullscreen() && !player.isVerticalVideo()) { } else if (player.isFullscreen() && !player.isVerticalVideo()
// Tablet UI has orientation-independent fullscreen
&& !DeviceUtils.isTablet(activity)) {
// Device is in portrait orientation after rotation but UI is in fullscreen. // Device is in portrait orientation after rotation but UI is in fullscreen.
// Return back to non-fullscreen state // Return back to non-fullscreen state
player.toggleFullscreen(); player.toggleFullscreen();
@ -508,8 +512,8 @@ public final class VideoDetailFragment
} }
break; break;
case R.id.detail_uploader_root_layout: case R.id.detail_uploader_root_layout:
if (TextUtils.isEmpty(currentInfo.getSubChannelUrl())) { if (isEmpty(currentInfo.getSubChannelUrl())) {
if (!TextUtils.isEmpty(currentInfo.getUploaderUrl())) { if (!isEmpty(currentInfo.getUploaderUrl())) {
openChannel(currentInfo.getUploaderUrl(), currentInfo.getUploaderName()); openChannel(currentInfo.getUploaderUrl(), currentInfo.getUploaderName());
} }
@ -535,7 +539,7 @@ public final class VideoDetailFragment
break; break;
case R.id.overlay_play_pause_button: case R.id.overlay_play_pause_button:
if (playerIsNotStopped()) { if (playerIsNotStopped()) {
player.onPlayPause(); player.playPause();
player.hideControls(0, 0); player.hideControls(0, 0);
showSystemUi(); showSystemUi();
} else { } else {
@ -583,7 +587,7 @@ public final class VideoDetailFragment
} }
break; break;
case R.id.detail_uploader_root_layout: case R.id.detail_uploader_root_layout:
if (TextUtils.isEmpty(currentInfo.getSubChannelUrl())) { if (isEmpty(currentInfo.getSubChannelUrl())) {
Log.w(TAG, Log.w(TAG,
"Can't open parent channel because we got no parent channel URL"); "Can't open parent channel because we got no parent channel URL");
} else { } else {
@ -644,6 +648,9 @@ public final class VideoDetailFragment
detailDurationView = rootView.findViewById(R.id.detail_duration_view); detailDurationView = rootView.findViewById(R.id.detail_duration_view);
detailPositionView = rootView.findViewById(R.id.detail_position_view); detailPositionView = rootView.findViewById(R.id.detail_position_view);
detailMetaInfoSeparator = rootView.findViewById(R.id.detail_meta_info_separator);
detailMetaInfoTextView = rootView.findViewById(R.id.detail_meta_info_text_view);
videoDescriptionRootLayout = rootView.findViewById(R.id.detail_description_root_layout); videoDescriptionRootLayout = rootView.findViewById(R.id.detail_description_root_layout);
videoUploadDateView = rootView.findViewById(R.id.detail_upload_date_view); videoUploadDateView = rootView.findViewById(R.id.detail_upload_date_view);
videoDescriptionView = rootView.findViewById(R.id.detail_description_view); videoDescriptionView = rootView.findViewById(R.id.detail_description_view);
@ -748,7 +755,7 @@ public final class VideoDetailFragment
private void initThumbnailViews(@NonNull final StreamInfo info) { private void initThumbnailViews(@NonNull final StreamInfo info) {
thumbnailImageView.setImageResource(R.drawable.dummy_thumbnail_dark); thumbnailImageView.setImageResource(R.drawable.dummy_thumbnail_dark);
if (!TextUtils.isEmpty(info.getThumbnailUrl())) { if (!isEmpty(info.getThumbnailUrl())) {
final String infoServiceName = NewPipe.getNameOfService(info.getServiceId()); final String infoServiceName = NewPipe.getNameOfService(info.getServiceId());
final ImageLoadingListener onFailListener = new SimpleImageLoadingListener() { final ImageLoadingListener onFailListener = new SimpleImageLoadingListener() {
@Override @Override
@ -763,12 +770,12 @@ public final class VideoDetailFragment
ImageDisplayConstants.DISPLAY_THUMBNAIL_OPTIONS, onFailListener); ImageDisplayConstants.DISPLAY_THUMBNAIL_OPTIONS, onFailListener);
} }
if (!TextUtils.isEmpty(info.getSubChannelAvatarUrl())) { if (!isEmpty(info.getSubChannelAvatarUrl())) {
IMAGE_LOADER.displayImage(info.getSubChannelAvatarUrl(), subChannelThumb, IMAGE_LOADER.displayImage(info.getSubChannelAvatarUrl(), subChannelThumb,
ImageDisplayConstants.DISPLAY_AVATAR_OPTIONS); ImageDisplayConstants.DISPLAY_AVATAR_OPTIONS);
} }
if (!TextUtils.isEmpty(info.getUploaderAvatarUrl())) { if (!isEmpty(info.getUploaderAvatarUrl())) {
IMAGE_LOADER.displayImage(info.getUploaderAvatarUrl(), uploaderThumb, IMAGE_LOADER.displayImage(info.getUploaderAvatarUrl(), uploaderThumb,
ImageDisplayConstants.DISPLAY_AVATAR_OPTIONS); ImageDisplayConstants.DISPLAY_AVATAR_OPTIONS);
} }
@ -798,7 +805,7 @@ public final class VideoDetailFragment
// If we are in fullscreen mode just exit from it via first back press // If we are in fullscreen mode just exit from it via first back press
if (player != null && player.isFullscreen()) { if (player != null && player.isFullscreen()) {
if (!DeviceUtils.isTablet(activity)) { if (!DeviceUtils.isTablet(activity)) {
player.onPause(); player.pause();
} }
restoreDefaultOrientation(); restoreDefaultOrientation();
setAutoPlay(false); setAutoPlay(false);
@ -843,7 +850,7 @@ public final class VideoDetailFragment
final PlayQueueItem playQueueItem = item.getPlayQueue().getItem(); final PlayQueueItem playQueueItem = item.getPlayQueue().getItem();
// Update title, url, uploader from the last item in the stack (it's current now) // Update title, url, uploader from the last item in the stack (it's current now)
final boolean isPlayerStopped = player == null || player.isPlayerStopped(); final boolean isPlayerStopped = player == null || player.isStopped();
if (playQueueItem != null && isPlayerStopped) { if (playQueueItem != null && isPlayerStopped) {
updateOverlayData(playQueueItem.getTitle(), updateOverlayData(playQueueItem.getTitle(),
playQueueItem.getUploader(), playQueueItem.getThumbnailUrl()); playQueueItem.getUploader(), playQueueItem.getThumbnailUrl());
@ -1217,7 +1224,7 @@ public final class VideoDetailFragment
} }
private void prepareDescription(final Description description) { private void prepareDescription(final Description description) {
if (description == null || TextUtils.isEmpty(description.getContent()) if (description == null || isEmpty(description.getContent())
|| description == Description.emptyDescription) { || description == Description.emptyDescription) {
return; return;
} }
@ -1462,9 +1469,9 @@ public final class VideoDetailFragment
animateView(thumbnailPlayButton, true, 200); animateView(thumbnailPlayButton, true, 200);
videoTitleTextView.setText(title); videoTitleTextView.setText(title);
if (!TextUtils.isEmpty(info.getSubChannelName())) { if (!isEmpty(info.getSubChannelName())) {
displayBothUploaderAndSubChannel(info); displayBothUploaderAndSubChannel(info);
} else if (!TextUtils.isEmpty(info.getUploaderName())) { } else if (!isEmpty(info.getUploaderName())) {
displayUploaderAsSubChannel(info); displayUploaderAsSubChannel(info);
} else { } else {
uploaderTextView.setVisibility(View.GONE); uploaderTextView.setVisibility(View.GONE);
@ -1559,8 +1566,10 @@ public final class VideoDetailFragment
prepareDescription(info.getDescription()); prepareDescription(info.getDescription());
updateProgressInfo(info); updateProgressInfo(info);
initThumbnailViews(info); initThumbnailViews(info);
showMetaInfoInTextView(info.getMetaInfo(), detailMetaInfoTextView, detailMetaInfoSeparator);
if (player == null || player.isPlayerStopped()) {
if (player == null || player.isStopped()) {
updateOverlayData(info.getName(), info.getUploaderName(), info.getThumbnailUrl()); updateOverlayData(info.getName(), info.getUploaderName(), info.getThumbnailUrl());
} }
@ -1610,7 +1619,7 @@ public final class VideoDetailFragment
subChannelThumb.setVisibility(View.VISIBLE); subChannelThumb.setVisibility(View.VISIBLE);
if (!TextUtils.isEmpty(info.getUploaderName())) { if (!isEmpty(info.getUploaderName())) {
uploaderTextView.setText( uploaderTextView.setText(
String.format(getString(R.string.video_detail_by), info.getUploaderName())); String.format(getString(R.string.video_detail_by), info.getUploaderName()));
uploaderTextView.setVisibility(View.VISIBLE); uploaderTextView.setVisibility(View.VISIBLE);
@ -1788,7 +1797,7 @@ public final class VideoDetailFragment
setOverlayPlayPauseImage(player != null && player.isPlaying()); setOverlayPlayPauseImage(player != null && player.isPlaying());
switch (state) { switch (state) {
case BasePlayer.STATE_PLAYING: case Player.STATE_PLAYING:
if (positionView.getAlpha() != 1.0f if (positionView.getAlpha() != 1.0f
&& player.getPlayQueue() != null && player.getPlayQueue() != null
&& player.getPlayQueue().getItem() != null && player.getPlayQueue().getItem() != null
@ -1805,7 +1814,7 @@ public final class VideoDetailFragment
final int duration, final int duration,
final int bufferPercent) { final int bufferPercent) {
// Progress updates every second even if media is paused. It's useless until playing // Progress updates every second even if media is paused. It's useless until playing
if (!player.getPlayer().isPlaying() || playQueue == null) { if (!player.isPlaying() || playQueue == null) {
return; return;
} }
@ -2009,9 +2018,7 @@ public final class VideoDetailFragment
} }
private boolean playerIsNotStopped() { private boolean playerIsNotStopped() {
return player != null return player != null && !player.isStopped();
&& player.getPlayer() != null
&& player.getPlayer().getPlaybackState() != Player.STATE_IDLE;
} }
private void restoreDefaultBrightness() { private void restoreDefaultBrightness() {
@ -2039,6 +2046,10 @@ public final class VideoDetailFragment
// Apply system brightness when the player is not in fullscreen // Apply system brightness when the player is not in fullscreen
restoreDefaultBrightness(); restoreDefaultBrightness();
} else { } else {
// Do not restore if user has disabled brightness gesture
if (!PlayerHelper.isBrightnessGestureEnabled(activity)) {
return;
}
// Restore already saved brightness level // Restore already saved brightness level
final float brightnessLevel = PlayerHelper.getScreenBrightness(activity); final float brightnessLevel = PlayerHelper.getScreenBrightness(activity);
if (brightnessLevel == lp.screenBrightness) { if (brightnessLevel == lp.screenBrightness) {
@ -2058,7 +2069,7 @@ public final class VideoDetailFragment
player.checkLandscape(); player.checkLandscape();
// Let's give a user time to look at video information page if video is not playing // Let's give a user time to look at video information page if video is not playing
if (globalScreenOrientationLocked(activity) && !player.isPlaying()) { if (globalScreenOrientationLocked(activity) && !player.isPlaying()) {
player.onPlay(); player.play();
} }
} }
@ -2272,7 +2283,7 @@ public final class VideoDetailFragment
// Re-enable clicks // Re-enable clicks
setOverlayElementsClickable(true); setOverlayElementsClickable(true);
if (player != null) { if (player != null) {
player.onQueueClosed(); player.closeItemsList();
} }
setOverlayLook(appBarLayout, behavior, 0); setOverlayLook(appBarLayout, behavior, 0);
break; break;
@ -2305,10 +2316,10 @@ public final class VideoDetailFragment
private void updateOverlayData(@Nullable final String overlayTitle, private void updateOverlayData(@Nullable final String overlayTitle,
@Nullable final String uploader, @Nullable final String uploader,
@Nullable final String thumbnailUrl) { @Nullable final String thumbnailUrl) {
overlayTitleTextView.setText(TextUtils.isEmpty(overlayTitle) ? "" : overlayTitle); overlayTitleTextView.setText(isEmpty(title) ? "" : title);
overlayChannelTextView.setText(TextUtils.isEmpty(uploader) ? "" : uploader); overlayChannelTextView.setText(isEmpty(uploader) ? "" : uploader);
overlayThumbnailImageView.setImageResource(R.drawable.dummy_thumbnail_dark); overlayThumbnailImageView.setImageResource(R.drawable.dummy_thumbnail_dark);
if (!TextUtils.isEmpty(thumbnailUrl)) { if (!isEmpty(thumbnailUrl)) {
IMAGE_LOADER.displayImage(thumbnailUrl, overlayThumbnailImageView, IMAGE_LOADER.displayImage(thumbnailUrl, overlayThumbnailImageView,
ImageDisplayConstants.DISPLAY_THUMBNAIL_OPTIONS, null); ImageDisplayConstants.DISPLAY_THUMBNAIL_OPTIONS, null);
} }

View File

@ -12,13 +12,16 @@ import android.view.MenuInflater;
import android.view.View; import android.view.View;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.ActionBar; import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.app.AppCompatActivity;
import androidx.preference.PreferenceManager; import androidx.preference.PreferenceManager;
import androidx.recyclerview.widget.GridLayoutManager; import androidx.recyclerview.widget.GridLayoutManager;
import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.RecyclerView;
import androidx.viewbinding.ViewBinding;
import org.schabi.newpipe.R; import org.schabi.newpipe.R;
import org.schabi.newpipe.databinding.PignateFooterBinding;
import org.schabi.newpipe.extractor.InfoItem; import org.schabi.newpipe.extractor.InfoItem;
import org.schabi.newpipe.extractor.channel.ChannelInfoItem; import org.schabi.newpipe.extractor.channel.ChannelInfoItem;
import org.schabi.newpipe.extractor.comments.CommentsInfoItem; import org.schabi.newpipe.extractor.comments.CommentsInfoItem;
@ -31,6 +34,7 @@ import org.schabi.newpipe.info_list.InfoItemDialog;
import org.schabi.newpipe.info_list.InfoListAdapter; import org.schabi.newpipe.info_list.InfoListAdapter;
import org.schabi.newpipe.player.helper.PlayerHolder; import org.schabi.newpipe.player.helper.PlayerHolder;
import org.schabi.newpipe.report.ErrorActivity; import org.schabi.newpipe.report.ErrorActivity;
import org.schabi.newpipe.util.KoreUtil;
import org.schabi.newpipe.util.NavigationHelper; import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.OnClickGesture; import org.schabi.newpipe.util.OnClickGesture;
import org.schabi.newpipe.util.StateSaver; import org.schabi.newpipe.util.StateSaver;
@ -214,12 +218,13 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I>
// Init // Init
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/
protected View getListHeader() { @Nullable
protected ViewBinding getListHeader() {
return null; return null;
} }
protected View getListFooter() { protected ViewBinding getListFooter() {
return activity.getLayoutInflater().inflate(R.layout.pignate_footer, itemsList, false); return PignateFooterBinding.inflate(activity.getLayoutInflater(), itemsList, false);
} }
protected RecyclerView.LayoutManager getListLayoutManager() { protected RecyclerView.LayoutManager getListLayoutManager() {
@ -246,8 +251,12 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I>
itemsList.setLayoutManager(useGrid ? getGridLayoutManager() : getListLayoutManager()); itemsList.setLayoutManager(useGrid ? getGridLayoutManager() : getListLayoutManager());
infoListAdapter.setUseGridVariant(useGrid); infoListAdapter.setUseGridVariant(useGrid);
infoListAdapter.setFooter(getListFooter()); infoListAdapter.setFooter(getListFooter().getRoot());
infoListAdapter.setHeader(getListHeader());
final ViewBinding listHeader = getListHeader();
if (listHeader != null) {
infoListAdapter.setHeader(listHeader.getRoot());
}
itemsList.setAdapter(infoListAdapter); itemsList.setAdapter(infoListAdapter);
} }
@ -332,7 +341,6 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I>
} }
} }
protected void showStreamDialog(final StreamInfoItem item) { protected void showStreamDialog(final StreamInfoItem item) {
final Context context = getContext(); final Context context = getContext();
final Activity activity = getActivity(); final Activity activity = getActivity();
@ -359,6 +367,9 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I>
StreamDialogEntry.share StreamDialogEntry.share
)); ));
} }
if (KoreUtil.shouldShowPlayWithKodi(context, item.getServiceId())) {
entries.add(StreamDialogEntry.play_with_kodi);
}
StreamDialogEntry.setEnabledEntries(entries); StreamDialogEntry.setEnabledEntries(entries);
new InfoItemDialog(activity, item, StreamDialogEntry.getCommands(context), new InfoItemDialog(activity, item, StreamDialogEntry.getCommands(context),

View File

@ -14,20 +14,21 @@ import android.view.MenuItem;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.widget.Button; import android.widget.Button;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.appcompat.app.ActionBar; import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.app.AppCompatActivity;
import androidx.core.content.ContextCompat; import androidx.core.content.ContextCompat;
import androidx.viewbinding.ViewBinding;
import com.jakewharton.rxbinding4.view.RxView; import com.jakewharton.rxbinding4.view.RxView;
import org.schabi.newpipe.R; import org.schabi.newpipe.R;
import org.schabi.newpipe.database.subscription.SubscriptionEntity; import org.schabi.newpipe.database.subscription.SubscriptionEntity;
import org.schabi.newpipe.databinding.ChannelHeaderBinding;
import org.schabi.newpipe.databinding.FragmentChannelBinding;
import org.schabi.newpipe.databinding.PlaylistControlBinding;
import org.schabi.newpipe.extractor.InfoItem; import org.schabi.newpipe.extractor.InfoItem;
import org.schabi.newpipe.extractor.ListExtractor; import org.schabi.newpipe.extractor.ListExtractor;
import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipe.extractor.NewPipe;
@ -78,22 +79,12 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo>
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/
private SubscriptionManager subscriptionManager; private SubscriptionManager subscriptionManager;
private View headerRootLayout;
private ImageView headerChannelBanner; private FragmentChannelBinding channelBinding;
private ImageView headerAvatarView; private ChannelHeaderBinding headerBinding;
private TextView headerTitleView; private PlaylistControlBinding playlistControlBinding;
private ImageView headerSubChannelAvatarView;
private TextView headerSubChannelTitleView;
private TextView headerSubscribersTextView;
private Button headerSubscribeButton;
private View playlistCtrl;
private LinearLayout headerPlayAllButton;
private LinearLayout headerPopupButton;
private LinearLayout headerBackgroundButton;
private MenuItem menuRssButton; private MenuItem menuRssButton;
private TextView contentNotSupportedTextView;
private TextView kaomojiTextView;
private TextView noVideosTextView;
public static ChannelFragment getInstance(final int serviceId, final String url, public static ChannelFragment getInstance(final int serviceId, final String url,
final String name) { final String name) {
@ -132,53 +123,45 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo>
@Override @Override
public void onViewCreated(final View rootView, final Bundle savedInstanceState) { public void onViewCreated(final View rootView, final Bundle savedInstanceState) {
super.onViewCreated(rootView, savedInstanceState); super.onViewCreated(rootView, savedInstanceState);
contentNotSupportedTextView = rootView.findViewById(R.id.error_content_not_supported); channelBinding = FragmentChannelBinding.bind(rootView);
kaomojiTextView = rootView.findViewById(R.id.channel_kaomoji);
noVideosTextView = rootView.findViewById(R.id.channel_no_videos);
} }
@Override @Override
public void onDestroy() { public void onDestroy() {
super.onDestroy(); super.onDestroy();
if (disposables != null) {
disposables.clear(); disposables.clear();
}
if (subscribeButtonMonitor != null) { if (subscribeButtonMonitor != null) {
subscribeButtonMonitor.dispose(); subscribeButtonMonitor.dispose();
} }
} }
@Override
public void onDestroyView() {
channelBinding = null;
headerBinding = null;
playlistControlBinding = null;
super.onDestroyView();
}
/*////////////////////////////////////////////////////////////////////////// /*//////////////////////////////////////////////////////////////////////////
// Init // Init
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/
protected View getListHeader() { @Override
headerRootLayout = activity.getLayoutInflater() protected ViewBinding getListHeader() {
.inflate(R.layout.channel_header, itemsList, false); headerBinding = ChannelHeaderBinding
headerChannelBanner = headerRootLayout.findViewById(R.id.channel_banner_image); .inflate(activity.getLayoutInflater(), itemsList, false);
headerAvatarView = headerRootLayout.findViewById(R.id.channel_avatar_view); playlistControlBinding = headerBinding.playlistControl;
headerTitleView = headerRootLayout.findViewById(R.id.channel_title_view);
headerSubscribersTextView = headerRootLayout.findViewById(R.id.channel_subscriber_view);
headerSubscribeButton = headerRootLayout.findViewById(R.id.channel_subscribe_button);
playlistCtrl = headerRootLayout.findViewById(R.id.playlist_control);
headerSubChannelAvatarView =
headerRootLayout.findViewById(R.id.sub_channel_avatar_view);
headerSubChannelTitleView =
headerRootLayout.findViewById(R.id.sub_channel_title_view);
headerPlayAllButton = headerRootLayout.findViewById(R.id.playlist_ctrl_play_all_button); return headerBinding;
headerPopupButton = headerRootLayout.findViewById(R.id.playlist_ctrl_play_popup_button);
headerBackgroundButton = headerRootLayout.findViewById(R.id.playlist_ctrl_play_bg_button);
return headerRootLayout;
} }
@Override @Override
protected void initListeners() { protected void initListeners() {
super.initListeners(); super.initListeners();
headerSubChannelTitleView.setOnClickListener(this); headerBinding.subChannelTitleView.setOnClickListener(this);
headerSubChannelAvatarView.setOnClickListener(this); headerBinding.subChannelAvatarView.setOnClickListener(this);
} }
/*////////////////////////////////////////////////////////////////////////// /*//////////////////////////////////////////////////////////////////////////
@ -241,7 +224,7 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo>
private void monitorSubscription(final ChannelInfo info) { private void monitorSubscription(final ChannelInfo info) {
final Consumer<Throwable> onError = (Throwable throwable) -> { final Consumer<Throwable> onError = (Throwable throwable) -> {
animateView(headerSubscribeButton, false, 100); animateView(headerBinding.channelSubscribeButton, false, 100);
showSnackBarError(throwable, UserAction.SUBSCRIPTION, showSnackBarError(throwable, UserAction.SUBSCRIPTION,
NewPipe.getNameOfService(currentInfo.getServiceId()), NewPipe.getNameOfService(currentInfo.getServiceId()),
"Get subscription status", 0); "Get subscription status", 0);
@ -351,15 +334,15 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo>
info.getAvatarUrl(), info.getAvatarUrl(),
info.getDescription(), info.getDescription(),
info.getSubscriberCount()); info.getSubscriberCount());
subscribeButtonMonitor = monitorSubscribeButton(headerSubscribeButton, subscribeButtonMonitor = monitorSubscribeButton(
mapOnSubscribe(channel, info)); headerBinding.channelSubscribeButton, mapOnSubscribe(channel, info));
} else { } else {
if (DEBUG) { if (DEBUG) {
Log.d(TAG, "Found subscription to this channel!"); Log.d(TAG, "Found subscription to this channel!");
} }
final SubscriptionEntity subscription = subscriptionEntities.get(0); final SubscriptionEntity subscription = subscriptionEntities.get(0);
subscribeButtonMonitor = monitorSubscribeButton(headerSubscribeButton, subscribeButtonMonitor = monitorSubscribeButton(
mapOnUnsubscribe(subscription)); headerBinding.channelSubscribeButton, mapOnUnsubscribe(subscription));
} }
}; };
} }
@ -370,7 +353,8 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo>
+ "isSubscribed = [" + isSubscribed + "]"); + "isSubscribed = [" + isSubscribed + "]");
} }
final boolean isButtonVisible = headerSubscribeButton.getVisibility() == View.VISIBLE; final boolean isButtonVisible = headerBinding.channelSubscribeButton.getVisibility()
== View.VISIBLE;
final int backgroundDuration = isButtonVisible ? 300 : 0; final int backgroundDuration = isButtonVisible ? 300 : 0;
final int textDuration = isButtonVisible ? 200 : 0; final int textDuration = isButtonVisible ? 200 : 0;
@ -382,18 +366,21 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo>
final int subscribedText = ContextCompat.getColor(activity, R.color.subscribed_text_color); final int subscribedText = ContextCompat.getColor(activity, R.color.subscribed_text_color);
if (!isSubscribed) { if (!isSubscribed) {
headerSubscribeButton.setText(R.string.subscribe_button_title); headerBinding.channelSubscribeButton.setText(R.string.subscribe_button_title);
animateBackgroundColor(headerSubscribeButton, backgroundDuration, subscribedBackground, animateBackgroundColor(headerBinding.channelSubscribeButton, backgroundDuration,
subscribeBackground); subscribedBackground, subscribeBackground);
animateTextColor(headerSubscribeButton, textDuration, subscribedText, subscribeText); animateTextColor(headerBinding.channelSubscribeButton, textDuration, subscribedText,
subscribeText);
} else { } else {
headerSubscribeButton.setText(R.string.subscribed_button_title); headerBinding.channelSubscribeButton.setText(R.string.subscribed_button_title);
animateBackgroundColor(headerSubscribeButton, backgroundDuration, subscribeBackground, animateBackgroundColor(headerBinding.channelSubscribeButton, backgroundDuration,
subscribedBackground); subscribeBackground, subscribedBackground);
animateTextColor(headerSubscribeButton, textDuration, subscribeText, subscribedText); animateTextColor(headerBinding.channelSubscribeButton, textDuration, subscribeText,
subscribedText);
} }
animateView(headerSubscribeButton, AnimationUtils.Type.LIGHT_SCALE_AND_ALPHA, true, 100); animateView(headerBinding.channelSubscribeButton, AnimationUtils.Type.LIGHT_SCALE_AND_ALPHA,
true, 100);
} }
/*////////////////////////////////////////////////////////////////////////// /*//////////////////////////////////////////////////////////////////////////
@ -446,48 +433,49 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo>
public void showLoading() { public void showLoading() {
super.showLoading(); super.showLoading();
IMAGE_LOADER.cancelDisplayTask(headerChannelBanner); IMAGE_LOADER.cancelDisplayTask(headerBinding.channelBannerImage);
IMAGE_LOADER.cancelDisplayTask(headerAvatarView); IMAGE_LOADER.cancelDisplayTask(headerBinding.channelAvatarView);
IMAGE_LOADER.cancelDisplayTask(headerSubChannelAvatarView); IMAGE_LOADER.cancelDisplayTask(headerBinding.subChannelAvatarView);
animateView(headerSubscribeButton, false, 100); animateView(headerBinding.channelSubscribeButton, false, 100);
} }
@Override @Override
public void handleResult(@NonNull final ChannelInfo result) { public void handleResult(@NonNull final ChannelInfo result) {
super.handleResult(result); super.handleResult(result);
headerRootLayout.setVisibility(View.VISIBLE); headerBinding.getRoot().setVisibility(View.VISIBLE);
IMAGE_LOADER.displayImage(result.getBannerUrl(), headerChannelBanner, IMAGE_LOADER.displayImage(result.getBannerUrl(), headerBinding.channelBannerImage,
ImageDisplayConstants.DISPLAY_BANNER_OPTIONS); ImageDisplayConstants.DISPLAY_BANNER_OPTIONS);
IMAGE_LOADER.displayImage(result.getAvatarUrl(), headerAvatarView, IMAGE_LOADER.displayImage(result.getAvatarUrl(), headerBinding.channelAvatarView,
ImageDisplayConstants.DISPLAY_AVATAR_OPTIONS); ImageDisplayConstants.DISPLAY_AVATAR_OPTIONS);
IMAGE_LOADER.displayImage(result.getParentChannelAvatarUrl(), headerSubChannelAvatarView, IMAGE_LOADER.displayImage(result.getParentChannelAvatarUrl(),
headerBinding.subChannelAvatarView,
ImageDisplayConstants.DISPLAY_AVATAR_OPTIONS); ImageDisplayConstants.DISPLAY_AVATAR_OPTIONS);
headerSubscribersTextView.setVisibility(View.VISIBLE); headerBinding.channelSubscriberView.setVisibility(View.VISIBLE);
if (result.getSubscriberCount() >= 0) { if (result.getSubscriberCount() >= 0) {
headerSubscribersTextView.setText(Localization headerBinding.channelSubscriberView.setText(Localization
.shortSubscriberCount(activity, result.getSubscriberCount())); .shortSubscriberCount(activity, result.getSubscriberCount()));
} else { } else {
headerSubscribersTextView.setText(R.string.subscribers_count_not_available); headerBinding.channelSubscriberView.setText(R.string.subscribers_count_not_available);
} }
if (!TextUtils.isEmpty(currentInfo.getParentChannelName())) { if (!TextUtils.isEmpty(currentInfo.getParentChannelName())) {
headerSubChannelTitleView.setText(String.format( headerBinding.subChannelTitleView.setText(String.format(
getString(R.string.channel_created_by), getString(R.string.channel_created_by),
currentInfo.getParentChannelName()) currentInfo.getParentChannelName())
); );
headerSubChannelTitleView.setVisibility(View.VISIBLE); headerBinding.subChannelTitleView.setVisibility(View.VISIBLE);
headerSubChannelAvatarView.setVisibility(View.VISIBLE); headerBinding.subChannelAvatarView.setVisibility(View.VISIBLE);
} else { } else {
headerSubChannelTitleView.setVisibility(View.GONE); headerBinding.subChannelTitleView.setVisibility(View.GONE);
} }
if (menuRssButton != null) { if (menuRssButton != null) {
menuRssButton.setVisible(!TextUtils.isEmpty(result.getFeedUrl())); menuRssButton.setVisible(!TextUtils.isEmpty(result.getFeedUrl()));
} }
playlistCtrl.setVisibility(View.VISIBLE); playlistControlBinding.getRoot().setVisibility(View.VISIBLE);
final List<Throwable> errors = new ArrayList<>(result.getErrors()); final List<Throwable> errors = new ArrayList<>(result.getErrors());
if (!errors.isEmpty()) { if (!errors.isEmpty()) {
@ -516,29 +504,32 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo>
updateSubscription(result); updateSubscription(result);
monitorSubscription(result); monitorSubscription(result);
headerPlayAllButton.setOnClickListener(view -> NavigationHelper playlistControlBinding.playlistCtrlPlayAllButton
.setOnClickListener(view -> NavigationHelper
.playOnMainPlayer(activity, getPlayQueue())); .playOnMainPlayer(activity, getPlayQueue()));
headerPopupButton.setOnClickListener(view -> NavigationHelper playlistControlBinding.playlistCtrlPlayPopupButton
.setOnClickListener(view -> NavigationHelper
.playOnPopupPlayer(activity, getPlayQueue(), false)); .playOnPopupPlayer(activity, getPlayQueue(), false));
headerBackgroundButton.setOnClickListener(view -> NavigationHelper playlistControlBinding.playlistCtrlPlayBgButton
.setOnClickListener(view -> NavigationHelper
.playOnBackgroundPlayer(activity, getPlayQueue(), false)); .playOnBackgroundPlayer(activity, getPlayQueue(), false));
headerPopupButton.setOnLongClickListener(view -> { playlistControlBinding.playlistCtrlPlayPopupButton.setOnLongClickListener(view -> {
NavigationHelper.enqueueOnPopupPlayer(activity, getPlayQueue(), true); NavigationHelper.enqueueOnPopupPlayer(activity, getPlayQueue(), true);
return true; return true;
}); });
headerBackgroundButton.setOnLongClickListener(view -> { playlistControlBinding.playlistCtrlPlayBgButton.setOnLongClickListener(view -> {
NavigationHelper.enqueueOnBackgroundPlayer(activity, getPlayQueue(), true); NavigationHelper.enqueueOnBackgroundPlayer(activity, getPlayQueue(), true);
return true; return true;
}); });
} }
private void showContentNotSupported() { private void showContentNotSupported() {
contentNotSupportedTextView.setVisibility(View.VISIBLE); channelBinding.errorContentNotSupported.setVisibility(View.VISIBLE);
kaomojiTextView.setText("(︶︹︺)"); channelBinding.channelKaomoji.setText("(︶︹︺)");
kaomojiTextView.setTextSize(TypedValue.COMPLEX_UNIT_SP, 45f); channelBinding.channelKaomoji.setTextSize(TypedValue.COMPLEX_UNIT_SP, 45f);
noVideosTextView.setVisibility(View.GONE); channelBinding.channelNoVideos.setVisibility(View.GONE);
} }
private PlayQueue getPlayQueue() { private PlayQueue getPlayQueue() {
@ -596,7 +587,7 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo>
public void setTitle(final String title) { public void setTitle(final String title) {
super.setTitle(title); super.setTitle(title);
if (!useAsFrontPage) { if (!useAsFrontPage) {
headerTitleView.setText(title); headerBinding.channelTitleView.setText(title);
} }
} }
} }

View File

@ -11,23 +11,27 @@ import android.view.MenuInflater;
import android.view.MenuItem; import android.view.MenuItem;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.content.res.AppCompatResources;
import androidx.viewbinding.ViewBinding;
import org.reactivestreams.Subscriber; import org.reactivestreams.Subscriber;
import org.reactivestreams.Subscription; import org.reactivestreams.Subscription;
import org.schabi.newpipe.NewPipeDatabase; import org.schabi.newpipe.NewPipeDatabase;
import org.schabi.newpipe.R; import org.schabi.newpipe.R;
import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity; import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity;
import org.schabi.newpipe.databinding.PlaylistControlBinding;
import org.schabi.newpipe.databinding.PlaylistHeaderBinding;
import org.schabi.newpipe.extractor.InfoItem; import org.schabi.newpipe.extractor.InfoItem;
import org.schabi.newpipe.extractor.ListExtractor; import org.schabi.newpipe.extractor.ListExtractor;
import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.ServiceList;
import org.schabi.newpipe.extractor.exceptions.ExtractionException; import org.schabi.newpipe.extractor.exceptions.ExtractionException;
import org.schabi.newpipe.extractor.playlist.PlaylistInfo; import org.schabi.newpipe.extractor.playlist.PlaylistInfo;
import org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper;
import org.schabi.newpipe.extractor.stream.StreamInfoItem; import org.schabi.newpipe.extractor.stream.StreamInfoItem;
import org.schabi.newpipe.extractor.stream.StreamType; import org.schabi.newpipe.extractor.stream.StreamType;
import org.schabi.newpipe.fragments.list.BaseListInfoFragment; import org.schabi.newpipe.fragments.list.BaseListInfoFragment;
@ -40,11 +44,11 @@ import org.schabi.newpipe.report.ErrorActivity;
import org.schabi.newpipe.report.UserAction; import org.schabi.newpipe.report.UserAction;
import org.schabi.newpipe.util.ExtractorHelper; import org.schabi.newpipe.util.ExtractorHelper;
import org.schabi.newpipe.util.ImageDisplayConstants; import org.schabi.newpipe.util.ImageDisplayConstants;
import org.schabi.newpipe.util.KoreUtil;
import org.schabi.newpipe.util.Localization; import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.util.NavigationHelper; import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.ShareUtils; import org.schabi.newpipe.util.ShareUtils;
import org.schabi.newpipe.util.StreamDialogEntry; import org.schabi.newpipe.util.StreamDialogEntry;
import org.schabi.newpipe.util.ThemeHelper;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
@ -58,6 +62,7 @@ import io.reactivex.rxjava3.disposables.CompositeDisposable;
import io.reactivex.rxjava3.disposables.Disposable; import io.reactivex.rxjava3.disposables.Disposable;
import static org.schabi.newpipe.util.AnimationUtils.animateView; import static org.schabi.newpipe.util.AnimationUtils.animateView;
import static org.schabi.newpipe.util.ThemeHelper.resolveResourceIdFromAttr;
public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> { public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
private CompositeDisposable disposables; private CompositeDisposable disposables;
@ -70,17 +75,8 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
// Views // Views
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/
private View headerRootLayout; private PlaylistHeaderBinding headerBinding;
private TextView headerTitleView; private PlaylistControlBinding playlistControlBinding;
private View headerUploaderLayout;
private TextView headerUploaderName;
private ImageView headerUploaderAvatar;
private TextView headerStreamCount;
private View playlistCtrl;
private View headerPlayAllButton;
private View headerPopupButton;
private View headerBackgroundButton;
private MenuItem playlistBookmarkButton; private MenuItem playlistBookmarkButton;
@ -115,22 +111,13 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
// Init // Init
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/
protected View getListHeader() { @Override
headerRootLayout = activity.getLayoutInflater() protected ViewBinding getListHeader() {
.inflate(R.layout.playlist_header, itemsList, false); headerBinding = PlaylistHeaderBinding
headerTitleView = headerRootLayout.findViewById(R.id.playlist_title_view); .inflate(activity.getLayoutInflater(), itemsList, false);
headerUploaderLayout = headerRootLayout.findViewById(R.id.uploader_layout); playlistControlBinding = headerBinding.playlistControl;
headerUploaderName = headerRootLayout.findViewById(R.id.uploader_name);
headerUploaderAvatar = headerRootLayout.findViewById(R.id.uploader_avatar_view);
headerStreamCount = headerRootLayout.findViewById(R.id.playlist_stream_count);
playlistCtrl = headerRootLayout.findViewById(R.id.playlist_control);
headerPlayAllButton = headerRootLayout.findViewById(R.id.playlist_ctrl_play_all_button); return headerBinding;
headerPopupButton = headerRootLayout.findViewById(R.id.playlist_ctrl_play_popup_button);
headerBackgroundButton = headerRootLayout.findViewById(R.id.playlist_ctrl_play_bg_button);
return headerRootLayout;
} }
@Override @Override
@ -171,6 +158,9 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
StreamDialogEntry.share StreamDialogEntry.share
)); ));
} }
if (KoreUtil.shouldShowPlayWithKodi(context, item.getServiceId())) {
entries.add(StreamDialogEntry.play_with_kodi);
}
StreamDialogEntry.setEnabledEntries(entries); StreamDialogEntry.setEnabledEntries(entries);
StreamDialogEntry.start_here_on_background.setCustomAction((fragment, infoItem) -> StreamDialogEntry.start_here_on_background.setCustomAction((fragment, infoItem) ->
@ -196,6 +186,9 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
@Override @Override
public void onDestroyView() { public void onDestroyView() {
headerBinding = null;
playlistControlBinding = null;
super.onDestroyView(); super.onDestroyView();
if (isBookmarkButtonReady != null) { if (isBookmarkButtonReady != null) {
isBookmarkButtonReady.set(false); isBookmarkButtonReady.set(false);
@ -268,25 +261,25 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
@Override @Override
public void showLoading() { public void showLoading() {
super.showLoading(); super.showLoading();
animateView(headerRootLayout, false, 200); animateView(headerBinding.getRoot(), false, 200);
animateView(itemsList, false, 100); animateView(itemsList, false, 100);
IMAGE_LOADER.cancelDisplayTask(headerUploaderAvatar); IMAGE_LOADER.cancelDisplayTask(headerBinding.uploaderAvatarView);
animateView(headerUploaderLayout, false, 200); animateView(headerBinding.uploaderLayout, false, 200);
} }
@Override @Override
public void handleResult(@NonNull final PlaylistInfo result) { public void handleResult(@NonNull final PlaylistInfo result) {
super.handleResult(result); super.handleResult(result);
animateView(headerRootLayout, true, 100); animateView(headerBinding.getRoot(), true, 100);
animateView(headerUploaderLayout, true, 300); animateView(headerBinding.uploaderLayout, true, 300);
headerUploaderLayout.setOnClickListener(null); headerBinding.uploaderLayout.setOnClickListener(null);
// If we have an uploader put them into the UI // If we have an uploader put them into the UI
if (!TextUtils.isEmpty(result.getUploaderName())) { if (!TextUtils.isEmpty(result.getUploaderName())) {
headerUploaderName.setText(result.getUploaderName()); headerBinding.uploaderName.setText(result.getUploaderName());
if (!TextUtils.isEmpty(result.getUploaderUrl())) { if (!TextUtils.isEmpty(result.getUploaderUrl())) {
headerUploaderLayout.setOnClickListener(v -> { headerBinding.uploaderLayout.setOnClickListener(v -> {
try { try {
NavigationHelper.openChannelFragment(getFM(), result.getServiceId(), NavigationHelper.openChannelFragment(getFM(), result.getServiceId(),
result.getUploaderUrl(), result.getUploaderName()); result.getUploaderUrl(), result.getUploaderName());
@ -296,14 +289,29 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
}); });
} }
} else { // Otherwise say we have no uploader } else { // Otherwise say we have no uploader
headerUploaderName.setText(R.string.playlist_no_uploader); headerBinding.uploaderName.setText(R.string.playlist_no_uploader);
} }
playlistCtrl.setVisibility(View.VISIBLE); playlistControlBinding.getRoot().setVisibility(View.VISIBLE);
IMAGE_LOADER.displayImage(result.getUploaderAvatarUrl(), headerUploaderAvatar, final String avatarUrl = result.getUploaderAvatarUrl();
if (result.getServiceId() == ServiceList.YouTube.getServiceId()
&& (YoutubeParsingHelper.isYoutubeMixId(result.getId())
|| YoutubeParsingHelper.isYoutubeMusicMixId(result.getId()))) {
// this is an auto-generated playlist (e.g. Youtube mix), so a radio is shown
headerBinding.uploaderAvatarView.setDisableCircularTransformation(true);
headerBinding.uploaderAvatarView.setBorderColor(
getResources().getColor(R.color.transparent_background_color));
headerBinding.uploaderAvatarView.setImageDrawable(
AppCompatResources.getDrawable(requireContext(),
resolveResourceIdFromAttr(requireContext(), R.attr.ic_radio))
);
} else {
IMAGE_LOADER.displayImage(avatarUrl, headerBinding.uploaderAvatarView,
ImageDisplayConstants.DISPLAY_AVATAR_OPTIONS); ImageDisplayConstants.DISPLAY_AVATAR_OPTIONS);
headerStreamCount.setText(Localization }
headerBinding.playlistStreamCount.setText(Localization
.localizeStreamCount(getContext(), result.getStreamCount())); .localizeStreamCount(getContext(), result.getStreamCount()));
if (!result.getErrors().isEmpty()) { if (!result.getErrors().isEmpty()) {
@ -317,19 +325,19 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.subscribe(getPlaylistBookmarkSubscriber()); .subscribe(getPlaylistBookmarkSubscriber());
headerPlayAllButton.setOnClickListener(view -> playlistControlBinding.playlistCtrlPlayAllButton.setOnClickListener(view ->
NavigationHelper.playOnMainPlayer(activity, getPlayQueue())); NavigationHelper.playOnMainPlayer(activity, getPlayQueue()));
headerPopupButton.setOnClickListener(view -> playlistControlBinding.playlistCtrlPlayPopupButton.setOnClickListener(view ->
NavigationHelper.playOnPopupPlayer(activity, getPlayQueue(), false)); NavigationHelper.playOnPopupPlayer(activity, getPlayQueue(), false));
headerBackgroundButton.setOnClickListener(view -> playlistControlBinding.playlistCtrlPlayBgButton.setOnClickListener(view ->
NavigationHelper.playOnBackgroundPlayer(activity, getPlayQueue(), false)); NavigationHelper.playOnBackgroundPlayer(activity, getPlayQueue(), false));
headerPopupButton.setOnLongClickListener(view -> { playlistControlBinding.playlistCtrlPlayPopupButton.setOnLongClickListener(view -> {
NavigationHelper.enqueueOnPopupPlayer(activity, getPlayQueue(), true); NavigationHelper.enqueueOnPopupPlayer(activity, getPlayQueue(), true);
return true; return true;
}); });
headerBackgroundButton.setOnLongClickListener(view -> { playlistControlBinding.playlistCtrlPlayBgButton.setOnLongClickListener(view -> {
NavigationHelper.enqueueOnBackgroundPlayer(activity, getPlayQueue(), true); NavigationHelper.enqueueOnBackgroundPlayer(activity, getPlayQueue(), true);
return true; return true;
}); });
@ -438,7 +446,7 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
@Override @Override
public void setTitle(final String title) { public void setTitle(final String title) {
super.setTitle(title); super.setTitle(title);
headerTitleView.setText(title); headerBinding.playlistTitleView.setText(title);
} }
private void onBookmarkClicked() { private void onBookmarkClicked() {
@ -476,7 +484,7 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
final int titleRes = playlistEntity == null final int titleRes = playlistEntity == null
? R.string.bookmark_playlist : R.string.unbookmark_playlist; ? R.string.bookmark_playlist : R.string.unbookmark_playlist;
playlistBookmarkButton.setIcon(ThemeHelper.resolveResourceIdFromAttr(activity, iconAttr)); playlistBookmarkButton.setIcon(resolveResourceIdFromAttr(activity, iconAttr));
playlistBookmarkButton.setTitle(titleRes); playlistBookmarkButton.setTitle(titleRes);
} }
} }

View File

@ -37,16 +37,21 @@ import androidx.recyclerview.widget.RecyclerView;
import org.schabi.newpipe.R; import org.schabi.newpipe.R;
import org.schabi.newpipe.ReCaptchaActivity; import org.schabi.newpipe.ReCaptchaActivity;
import org.schabi.newpipe.database.history.model.SearchHistoryEntry; import org.schabi.newpipe.database.history.model.SearchHistoryEntry;
import org.schabi.newpipe.databinding.FragmentSearchBinding;
import org.schabi.newpipe.extractor.InfoItem; import org.schabi.newpipe.extractor.InfoItem;
import org.schabi.newpipe.extractor.ListExtractor; import org.schabi.newpipe.extractor.ListExtractor;
import org.schabi.newpipe.extractor.MetaInfo;
import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.Page; import org.schabi.newpipe.extractor.Page;
import org.schabi.newpipe.extractor.StreamingService; import org.schabi.newpipe.extractor.StreamingService;
import org.schabi.newpipe.extractor.exceptions.ParsingException; import org.schabi.newpipe.extractor.exceptions.ParsingException;
import org.schabi.newpipe.extractor.search.SearchExtractor; import org.schabi.newpipe.extractor.search.SearchExtractor;
import org.schabi.newpipe.extractor.search.SearchInfo; import org.schabi.newpipe.extractor.search.SearchInfo;
import org.schabi.newpipe.extractor.services.peertube.linkHandler.PeertubeSearchQueryHandlerFactory;
import org.schabi.newpipe.extractor.services.youtube.linkHandler.YoutubeSearchQueryHandlerFactory;
import org.schabi.newpipe.fragments.BackPressable; import org.schabi.newpipe.fragments.BackPressable;
import org.schabi.newpipe.fragments.list.BaseListFragment; import org.schabi.newpipe.fragments.list.BaseListFragment;
import org.schabi.newpipe.ktx.ExceptionUtils;
import org.schabi.newpipe.local.history.HistoryRecordManager; import org.schabi.newpipe.local.history.HistoryRecordManager;
import org.schabi.newpipe.report.ErrorActivity; import org.schabi.newpipe.report.ErrorActivity;
import org.schabi.newpipe.report.ErrorInfo; import org.schabi.newpipe.report.ErrorInfo;
@ -54,7 +59,6 @@ import org.schabi.newpipe.report.UserAction;
import org.schabi.newpipe.util.AnimationUtils; import org.schabi.newpipe.util.AnimationUtils;
import org.schabi.newpipe.util.Constants; import org.schabi.newpipe.util.Constants;
import org.schabi.newpipe.util.DeviceUtils; import org.schabi.newpipe.util.DeviceUtils;
import org.schabi.newpipe.util.ExceptionUtils;
import org.schabi.newpipe.util.ExtractorHelper; import org.schabi.newpipe.util.ExtractorHelper;
import org.schabi.newpipe.util.NavigationHelper; import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.ServiceHelper; import org.schabi.newpipe.util.ServiceHelper;
@ -79,6 +83,7 @@ import io.reactivex.rxjava3.subjects.PublishSubject;
import static androidx.recyclerview.widget.ItemTouchHelper.Callback.makeMovementFlags; import static androidx.recyclerview.widget.ItemTouchHelper.Callback.makeMovementFlags;
import static java.util.Arrays.asList; import static java.util.Arrays.asList;
import static org.schabi.newpipe.util.AnimationUtils.animateView; import static org.schabi.newpipe.util.AnimationUtils.animateView;
import static org.schabi.newpipe.util.ExtractorHelper.showMetaInfoInTextView;
public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.InfoItemsPage<?>> public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.InfoItemsPage<?>>
implements BackPressable { implements BackPressable {
@ -129,6 +134,9 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
@State @State
boolean isCorrectedSearch; boolean isCorrectedSearch;
@State
MetaInfo[] metaInfo;
@State @State
boolean wasSearchFocused = false; boolean wasSearchFocused = false;
@ -148,15 +156,18 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
// Views // Views
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/
private FragmentSearchBinding searchBinding;
private View searchToolbarContainer; private View searchToolbarContainer;
private EditText searchEditText; private EditText searchEditText;
private View searchClear; private View searchClear;
private TextView correctSuggestion; private TextView correctSuggestion;
private TextView metaInfoTextView;
private View metaInfoSeparator;
private View suggestionsPanel; private View suggestionsPanel;
private boolean suggestionsPanelVisible = false; private boolean suggestionsPanelVisible = false;
private RecyclerView suggestionsRecyclerView;
/*////////////////////////////////////////////////////////////////////////*/ /*////////////////////////////////////////////////////////////////////////*/
@ -269,6 +280,9 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
handleSearchSuggestion(); handleSearchSuggestion();
showMetaInfoInTextView(metaInfo == null ? null : Arrays.asList(metaInfo),
metaInfoTextView, metaInfoSeparator);
if (suggestionDisposable == null || suggestionDisposable.isDisposed()) { if (suggestionDisposable == null || suggestionDisposable.isDisposed()) {
initSuggestionObserver(); initSuggestionObserver();
} }
@ -289,6 +303,8 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
Log.d(TAG, "onDestroyView() called"); Log.d(TAG, "onDestroyView() called");
} }
unsetSearchListeners(); unsetSearchListeners();
searchBinding = null;
super.onDestroyView(); super.onDestroyView();
} }
@ -325,9 +341,9 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
@Override @Override
protected void initViews(final View rootView, final Bundle savedInstanceState) { protected void initViews(final View rootView, final Bundle savedInstanceState) {
super.initViews(rootView, savedInstanceState); super.initViews(rootView, savedInstanceState);
suggestionsPanel = rootView.findViewById(R.id.suggestions_panel); searchBinding = FragmentSearchBinding.bind(rootView);
suggestionsRecyclerView = rootView.findViewById(R.id.suggestions_list);
suggestionsRecyclerView.setAdapter(suggestionListAdapter); searchBinding.suggestionsList.setAdapter(suggestionListAdapter);
new ItemTouchHelper(new ItemTouchHelper.Callback() { new ItemTouchHelper(new ItemTouchHelper.Callback() {
@Override @Override
public int getMovementFlags(@NonNull final RecyclerView recyclerView, public int getMovementFlags(@NonNull final RecyclerView recyclerView,
@ -346,13 +362,15 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
public void onSwiped(@NonNull final RecyclerView.ViewHolder viewHolder, final int i) { public void onSwiped(@NonNull final RecyclerView.ViewHolder viewHolder, final int i) {
onSuggestionItemSwiped(viewHolder); onSuggestionItemSwiped(viewHolder);
} }
}).attachToRecyclerView(suggestionsRecyclerView); }).attachToRecyclerView(searchBinding.suggestionsList);
searchToolbarContainer = activity.findViewById(R.id.toolbar_search_container); searchToolbarContainer = activity.findViewById(R.id.toolbar_search_container);
searchEditText = searchToolbarContainer.findViewById(R.id.toolbar_search_edit_text); searchEditText = searchToolbarContainer.findViewById(R.id.toolbar_search_edit_text);
searchClear = searchToolbarContainer.findViewById(R.id.toolbar_search_clear); searchClear = searchToolbarContainer.findViewById(R.id.toolbar_search_clear);
correctSuggestion = rootView.findViewById(R.id.correct_suggestion); correctSuggestion = rootView.findViewById(R.id.correct_suggestion);
metaInfoTextView = rootView.findViewById(R.id.search_meta_info_text_view);
metaInfoSeparator = rootView.findViewById(R.id.search_meta_info_separator);
} }
/*////////////////////////////////////////////////////////////////////////// /*//////////////////////////////////////////////////////////////////////////
@ -419,12 +437,18 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
boolean isFirstItem = true; boolean isFirstItem = true;
final Context c = getContext(); final Context c = getContext();
for (final String filter : service.getSearchQHFactory().getAvailableContentFilter()) { for (final String filter : service.getSearchQHFactory().getAvailableContentFilter()) {
if (filter.equals("music_songs")) { if (filter.equals(YoutubeSearchQueryHandlerFactory.MUSIC_SONGS)) {
final MenuItem musicItem = menu.add(2, final MenuItem musicItem = menu.add(2,
itemId++, itemId++,
0, 0,
"YouTube Music"); "YouTube Music");
musicItem.setEnabled(false); musicItem.setEnabled(false);
} else if (filter.equals(PeertubeSearchQueryHandlerFactory.SEPIA_VIDEOS)) {
final MenuItem sepiaItem = menu.add(2,
itemId++,
0,
"Sepia Search");
sepiaItem.setEnabled(false);
} }
menuItemToFilterName.put(itemId, filter); menuItemToFilterName.put(itemId, filter);
final MenuItem item = menu.add(1, final MenuItem item = menu.add(1,
@ -503,7 +527,7 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
return; return;
} }
correctSuggestion.setVisibility(View.GONE); searchBinding.correctSuggestion.setVisibility(View.GONE);
searchEditText.setText(""); searchEditText.setText("");
suggestionListAdapter.setItems(new ArrayList<>()); suggestionListAdapter.setItems(new ArrayList<>());
@ -620,7 +644,8 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
Log.d(TAG, "showSuggestionsPanel() called"); Log.d(TAG, "showSuggestionsPanel() called");
} }
suggestionsPanelVisible = true; suggestionsPanelVisible = true;
animateView(suggestionsPanel, AnimationUtils.Type.LIGHT_SLIDE_AND_ALPHA, true, 200); animateView(searchBinding.suggestionsPanel, AnimationUtils.Type.LIGHT_SLIDE_AND_ALPHA,
true, 200);
} }
private void hideSuggestionsPanel() { private void hideSuggestionsPanel() {
@ -628,7 +653,8 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
Log.d(TAG, "hideSuggestionsPanel() called"); Log.d(TAG, "hideSuggestionsPanel() called");
} }
suggestionsPanelVisible = false; suggestionsPanelVisible = false;
animateView(suggestionsPanel, AnimationUtils.Type.LIGHT_SLIDE_AND_ALPHA, false, 200); animateView(searchBinding.suggestionsPanel, AnimationUtils.Type.LIGHT_SLIDE_AND_ALPHA,
false, 200);
} }
private void showKeyboardSearch() { private void showKeyboardSearch() {
@ -916,8 +942,8 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
if (DEBUG) { if (DEBUG) {
Log.d(TAG, "handleSuggestions() called with: suggestions = [" + suggestions + "]"); Log.d(TAG, "handleSuggestions() called with: suggestions = [" + suggestions + "]");
} }
suggestionsRecyclerView.smoothScrollToPosition(0); searchBinding.suggestionsList.smoothScrollToPosition(0);
suggestionsRecyclerView.post(() -> suggestionListAdapter.setItems(suggestions)); searchBinding.suggestionsList.post(() -> suggestionListAdapter.setItems(suggestions));
if (suggestionsPanelVisible && errorPanelRoot.getVisibility() == View.VISIBLE) { if (suggestionsPanelVisible && errorPanelRoot.getVisibility() == View.VISIBLE) {
hideLoading(); hideLoading();
@ -973,8 +999,14 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
searchSuggestion = result.getSearchSuggestion(); searchSuggestion = result.getSearchSuggestion();
isCorrectedSearch = result.isCorrectedSearch(); isCorrectedSearch = result.isCorrectedSearch();
// List<MetaInfo> cannot be bundled without creating some containers
metaInfo = new MetaInfo[result.getMetaInfo().size()];
metaInfo = result.getMetaInfo().toArray(metaInfo);
handleSearchSuggestion(); handleSearchSuggestion();
showMetaInfoInTextView(result.getMetaInfo(), metaInfoTextView, metaInfoSeparator);
lastSearchedString = searchString; lastSearchedString = searchString;
nextPage = result.getNextPage(); nextPage = result.getNextPage();
@ -993,7 +1025,7 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
private void handleSearchSuggestion() { private void handleSearchSuggestion() {
if (TextUtils.isEmpty(searchSuggestion)) { if (TextUtils.isEmpty(searchSuggestion)) {
correctSuggestion.setVisibility(View.GONE); searchBinding.correctSuggestion.setVisibility(View.GONE);
} else { } else {
final String helperText = getString(isCorrectedSearch final String helperText = getString(isCorrectedSearch
? R.string.search_showing_result_for ? R.string.search_showing_result_for
@ -1002,22 +1034,23 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
final String highlightedSearchSuggestion = final String highlightedSearchSuggestion =
"<b><i>" + Html.escapeHtml(searchSuggestion) + "</i></b>"; "<b><i>" + Html.escapeHtml(searchSuggestion) + "</i></b>";
final String text = String.format(helperText, highlightedSearchSuggestion); final String text = String.format(helperText, highlightedSearchSuggestion);
correctSuggestion.setText(HtmlCompat.fromHtml(text, HtmlCompat.FROM_HTML_MODE_LEGACY)); searchBinding.correctSuggestion.setText(HtmlCompat.fromHtml(text,
HtmlCompat.FROM_HTML_MODE_LEGACY));
correctSuggestion.setOnClickListener(v -> { searchBinding.correctSuggestion.setOnClickListener(v -> {
correctSuggestion.setVisibility(View.GONE); searchBinding.correctSuggestion.setVisibility(View.GONE);
search(searchSuggestion, contentFilter, sortFilter); search(searchSuggestion, contentFilter, sortFilter);
searchEditText.setText(searchSuggestion); searchEditText.setText(searchSuggestion);
}); });
correctSuggestion.setOnLongClickListener(v -> { searchBinding.correctSuggestion.setOnLongClickListener(v -> {
searchEditText.setText(searchSuggestion); searchEditText.setText(searchSuggestion);
searchEditText.setSelection(searchSuggestion.length()); searchEditText.setSelection(searchSuggestion.length());
showKeyboardSearch(); showKeyboardSearch();
return true; return true;
}); });
correctSuggestion.setVisibility(View.VISIBLE); searchBinding.correctSuggestion.setVisibility(View.VISIBLE);
} }
} }

View File

@ -8,13 +8,14 @@ import android.view.Menu;
import android.view.MenuInflater; import android.view.MenuInflater;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.widget.Switch;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.preference.PreferenceManager; import androidx.preference.PreferenceManager;
import androidx.viewbinding.ViewBinding;
import org.schabi.newpipe.R; import org.schabi.newpipe.R;
import org.schabi.newpipe.databinding.RelatedStreamsHeaderBinding;
import org.schabi.newpipe.extractor.ListExtractor; import org.schabi.newpipe.extractor.ListExtractor;
import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.stream.StreamInfo; import org.schabi.newpipe.extractor.stream.StreamInfo;
@ -38,8 +39,7 @@ public class RelatedVideosFragment extends BaseListInfoFragment<RelatedStreamInf
// Views // Views
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/
private View headerRootLayout; private RelatedStreamsHeaderBinding headerBinding;
private Switch autoplaySwitch;
public static RelatedVideosFragment getInstance(final StreamInfo info) { public static RelatedVideosFragment getInstance(final StreamInfo info) {
final RelatedVideosFragment instance = new RelatedVideosFragment(); final RelatedVideosFragment instance = new RelatedVideosFragment();
@ -66,25 +66,29 @@ public class RelatedVideosFragment extends BaseListInfoFragment<RelatedStreamInf
@Override @Override
public void onDestroy() { public void onDestroy() {
super.onDestroy(); super.onDestroy();
if (disposables != null) {
disposables.clear(); disposables.clear();
} }
@Override
public void onDestroyView() {
headerBinding = null;
super.onDestroyView();
} }
protected View getListHeader() { @Override
protected ViewBinding getListHeader() {
if (relatedStreamInfo != null && relatedStreamInfo.getRelatedItems() != null) { if (relatedStreamInfo != null && relatedStreamInfo.getRelatedItems() != null) {
headerRootLayout = activity.getLayoutInflater() headerBinding = RelatedStreamsHeaderBinding
.inflate(R.layout.related_streams_header, itemsList, false); .inflate(activity.getLayoutInflater(), itemsList, false);
autoplaySwitch = headerRootLayout.findViewById(R.id.autoplay_switch);
final SharedPreferences pref = PreferenceManager final SharedPreferences pref = PreferenceManager
.getDefaultSharedPreferences(requireContext()); .getDefaultSharedPreferences(requireContext());
final boolean autoplay = pref.getBoolean(getString(R.string.auto_queue_key), false); final boolean autoplay = pref.getBoolean(getString(R.string.auto_queue_key), false);
autoplaySwitch.setChecked(autoplay); headerBinding.autoplaySwitch.setChecked(autoplay);
autoplaySwitch.setOnCheckedChangeListener((compoundButton, b) -> headerBinding.autoplaySwitch.setOnCheckedChangeListener((compoundButton, b) ->
PreferenceManager.getDefaultSharedPreferences(requireContext()).edit() PreferenceManager.getDefaultSharedPreferences(requireContext()).edit()
.putBoolean(getString(R.string.auto_queue_key), b).apply()); .putBoolean(getString(R.string.auto_queue_key), b).apply());
return headerRootLayout; return headerBinding;
} else { } else {
return null; return null;
} }
@ -107,8 +111,8 @@ public class RelatedVideosFragment extends BaseListInfoFragment<RelatedStreamInf
@Override @Override
public void showLoading() { public void showLoading() {
super.showLoading(); super.showLoading();
if (headerRootLayout != null) { if (headerBinding != null) {
headerRootLayout.setVisibility(View.INVISIBLE); headerBinding.getRoot().setVisibility(View.INVISIBLE);
} }
} }
@ -116,8 +120,8 @@ public class RelatedVideosFragment extends BaseListInfoFragment<RelatedStreamInf
public void handleResult(@NonNull final RelatedStreamInfo result) { public void handleResult(@NonNull final RelatedStreamInfo result) {
super.handleResult(result); super.handleResult(result);
if (headerRootLayout != null) { if (headerBinding != null) {
headerRootLayout.setVisibility(View.VISIBLE); headerBinding.getRoot().setVisibility(View.VISIBLE);
} }
AnimationUtils.slideUp(getView(), 120, 96, 0.06f); AnimationUtils.slideUp(getView(), 120, 96, 0.06f);
@ -126,10 +130,8 @@ public class RelatedVideosFragment extends BaseListInfoFragment<RelatedStreamInf
NewPipe.getNameOfService(result.getServiceId()), result.getUrl(), 0); NewPipe.getNameOfService(result.getServiceId()), result.getUrl(), 0);
} }
if (disposables != null) {
disposables.clear(); disposables.clear();
} }
}
@Override @Override
public void handleNextItems(final ListExtractor.InfoItemsPage result) { public void handleNextItems(final ListExtractor.InfoItemsPage result) {
@ -202,8 +204,8 @@ public class RelatedVideosFragment extends BaseListInfoFragment<RelatedStreamInf
final SharedPreferences pref = final SharedPreferences pref =
PreferenceManager.getDefaultSharedPreferences(requireContext()); PreferenceManager.getDefaultSharedPreferences(requireContext());
final boolean autoplay = pref.getBoolean(getString(R.string.auto_queue_key), false); final boolean autoplay = pref.getBoolean(getString(R.string.auto_queue_key), false);
if (autoplaySwitch != null) { if (headerBinding != null) {
autoplaySwitch.setChecked(autoplay); headerBinding.autoplaySwitch.setChecked(autoplay);
} }
} }

View File

@ -0,0 +1,66 @@
package org.schabi.newpipe.info_list
import android.util.Log
import com.xwray.groupie.GroupAdapter
import com.xwray.groupie.GroupieViewHolder
import org.schabi.newpipe.extractor.stream.StreamInfo
import kotlin.math.max
/**
* Custom RecyclerView.Adapter/GroupieAdapter for [StreamSegmentItem] for handling selection state.
*/
class StreamSegmentAdapter(
private val listener: StreamSegmentListener
) : GroupAdapter<GroupieViewHolder>() {
var currentIndex: Int = 0
private set
/**
* Returns `true` if the provided [StreamInfo] contains segments, `false` otherwise.
*/
fun setItems(info: StreamInfo): Boolean {
if (info.streamSegments.isNotEmpty()) {
clear()
addAll(info.streamSegments.map { StreamSegmentItem(it, listener) })
return true
}
return false
}
fun selectSegment(segment: StreamSegmentItem) {
unSelectCurrentSegment()
currentIndex = max(0, getAdapterPosition(segment))
segment.isSelected = true
segment.notifyChanged(StreamSegmentItem.PAYLOAD_SELECT)
}
fun selectSegmentAt(position: Int) {
try {
selectSegment(getGroupAtAdapterPosition(position) as StreamSegmentItem)
} catch (e: IndexOutOfBoundsException) {
// Just to make sure that getGroupAtAdapterPosition doesn't close the app
// Shouldn't happen since setItems is always called before select-methods but just in case
currentIndex = 0
Log.e("StreamSegmentAdapter", "selectSegmentAt: ${e.message}")
}
}
private fun unSelectCurrentSegment() {
try {
val segmentItem = getGroupAtAdapterPosition(currentIndex) as StreamSegmentItem
currentIndex = 0
segmentItem.isSelected = false
segmentItem.notifyChanged(StreamSegmentItem.PAYLOAD_SELECT)
} catch (e: IndexOutOfBoundsException) {
// Just to make sure that getGroupAtAdapterPosition doesn't close the app
// Shouldn't happen since setItems is always called before select-methods but just in case
currentIndex = 0
Log.e("StreamSegmentAdapter", "unSelectCurrentSegment: ${e.message}")
}
}
interface StreamSegmentListener {
fun onItemClick(item: StreamSegmentItem, seconds: Int)
}
}

View File

@ -0,0 +1,47 @@
package org.schabi.newpipe.info_list
import android.widget.ImageView
import android.widget.TextView
import com.nostra13.universalimageloader.core.ImageLoader
import com.xwray.groupie.GroupieViewHolder
import com.xwray.groupie.Item
import org.schabi.newpipe.R
import org.schabi.newpipe.extractor.stream.StreamSegment
import org.schabi.newpipe.util.ImageDisplayConstants
import org.schabi.newpipe.util.Localization
class StreamSegmentItem(
private val item: StreamSegment,
private val onClick: StreamSegmentAdapter.StreamSegmentListener
) : Item<GroupieViewHolder>() {
companion object {
const val PAYLOAD_SELECT = 1
}
var isSelected = false
override fun bind(viewHolder: GroupieViewHolder, position: Int) {
item.previewUrl?.let {
ImageLoader.getInstance().displayImage(
it, viewHolder.root.findViewById<ImageView>(R.id.previewImage),
ImageDisplayConstants.DISPLAY_THUMBNAIL_OPTIONS
)
}
viewHolder.root.findViewById<TextView>(R.id.textViewTitle).text = item.title
viewHolder.root.findViewById<TextView>(R.id.textViewStartSeconds).text =
Localization.getDurationString(item.startTimeSeconds.toLong())
viewHolder.root.setOnClickListener { onClick.onItemClick(this, item.startTimeSeconds) }
viewHolder.root.isSelected = isSelected
}
override fun bind(viewHolder: GroupieViewHolder, position: Int, payloads: MutableList<Any>) {
if (payloads.contains(PAYLOAD_SELECT)) {
viewHolder.root.isSelected = isSelected
return
}
super.bind(viewHolder, position, payloads)
}
override fun getLayout() = R.layout.item_stream_segment
}

View File

@ -70,7 +70,8 @@ public class StreamMiniInfoItemHolder extends InfoItemHolder {
} else { } else {
itemProgressView.setVisibility(View.GONE); itemProgressView.setVisibility(View.GONE);
} }
} else if (item.getStreamType() == StreamType.LIVE_STREAM) { } else if (item.getStreamType() == StreamType.LIVE_STREAM
|| item.getStreamType() == StreamType.AUDIO_LIVE_STREAM) {
itemDurationView.setText(R.string.duration_live); itemDurationView.setText(R.string.duration_live);
itemDurationView.setBackgroundColor(ContextCompat.getColor(itemBuilder.getContext(), itemDurationView.setBackgroundColor(ContextCompat.getColor(itemBuilder.getContext(),
R.color.live_duration_background_color)); R.color.live_duration_background_color));

View File

@ -1,10 +1,29 @@
package org.schabi.newpipe.ktx package org.schabi.newpipe.ktx
import java.time.OffsetDateTime import java.time.OffsetDateTime
import java.time.ZoneId import java.time.ZoneOffset
import java.time.temporal.ChronoField
import java.util.Calendar import java.util.Calendar
import java.util.Date
import java.util.GregorianCalendar import java.util.GregorianCalendar
import java.util.TimeZone
fun OffsetDateTime.toCalendar(zoneId: ZoneId = ZoneId.systemDefault()): Calendar { // This method is a modified version of GregorianCalendar.from(ZonedDateTime).
return GregorianCalendar.from(if (zoneId != offset) atZoneSameInstant(zoneId) else toZonedDateTime()) // Math.addExact() and Math.multiplyExact() are desugared even though lint displays a warning.
@SuppressWarnings("NewApi")
fun OffsetDateTime.toCalendar(): Calendar {
val cal = GregorianCalendar(TimeZone.getTimeZone("UTC"))
val offsetDateTimeUTC = withOffsetSameInstant(ZoneOffset.UTC)
cal.gregorianChange = Date(Long.MIN_VALUE)
cal.firstDayOfWeek = Calendar.MONDAY
cal.minimalDaysInFirstWeek = 4
try {
cal.timeInMillis = Math.addExact(
Math.multiplyExact(offsetDateTimeUTC.toEpochSecond(), 1000),
offsetDateTimeUTC[ChronoField.MILLI_OF_SECOND].toLong()
)
} catch (ex: ArithmeticException) {
throw IllegalArgumentException(ex)
}
return cal
} }

View File

@ -0,0 +1,75 @@
@file:JvmName("ExceptionUtils")
package org.schabi.newpipe.ktx
import java.io.IOException
import java.io.InterruptedIOException
/**
* @return if throwable is related to Interrupted exceptions, or one of its causes is.
*/
val Throwable.isInterruptedCaused: Boolean
get() = hasExactCause(InterruptedIOException::class.java, InterruptedException::class.java)
/**
* @return if throwable is related to network issues, or one of its causes is.
*/
val Throwable.isNetworkRelated: Boolean
get() = hasAssignableCause<IOException>()
/**
* Calls [hasCause] with the `checkSubtypes` parameter set to false.
*/
fun Throwable.hasExactCause(vararg causesToCheck: Class<*>) = hasCause(false, *causesToCheck)
/**
* Calls [hasCause] with a reified [Throwable] type.
*/
inline fun <reified T : Throwable> Throwable.hasExactCause() = hasExactCause(T::class.java)
/**
* Calls [hasCause] with the `checkSubtypes` parameter set to true.
*/
fun Throwable?.hasAssignableCause(vararg causesToCheck: Class<*>) = hasCause(true, *causesToCheck)
/**
* Calls [hasCause] with a reified [Throwable] type.
*/
inline fun <reified T : Throwable> Throwable?.hasAssignableCause() = hasAssignableCause(T::class.java)
/**
* Check if the throwable has some cause from the causes to check, or is itself in it.
*
* If `checkIfAssignable` is true, not only the exact type will be considered equals, but also its subtypes.
*
* @param checkSubtypes if subtypes are also checked.
* @param causesToCheck an array of causes to check.
*
* @see Class.isAssignableFrom
*/
tailrec fun Throwable?.hasCause(checkSubtypes: Boolean, vararg causesToCheck: Class<*>): Boolean {
if (this == null) {
return false
}
// Check if throwable is a subtype of any of the causes to check
causesToCheck.forEach { causeClass ->
if (checkSubtypes) {
if (causeClass.isAssignableFrom(this.javaClass)) {
return true
}
} else {
if (causeClass == this.javaClass) {
return true
}
}
}
val currentCause: Throwable? = cause
// Check if cause is not pointing to the same instance, to avoid infinite loops.
if (this !== currentCause) {
return currentCause.hasCause(checkSubtypes, *causesToCheck)
}
return false
}

View File

@ -4,6 +4,8 @@ import android.content.SharedPreferences;
import android.content.res.Configuration; import android.content.res.Configuration;
import android.content.res.Resources; import android.content.res.Resources;
import android.os.Bundle; import android.os.Bundle;
import androidx.annotation.Nullable;
import androidx.preference.PreferenceManager; import androidx.preference.PreferenceManager;
import android.util.Log; import android.util.Log;
import android.view.Menu; import android.view.Menu;
@ -15,8 +17,10 @@ import androidx.fragment.app.Fragment;
import androidx.recyclerview.widget.GridLayoutManager; import androidx.recyclerview.widget.GridLayoutManager;
import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.RecyclerView;
import androidx.viewbinding.ViewBinding;
import org.schabi.newpipe.R; import org.schabi.newpipe.R;
import org.schabi.newpipe.databinding.PignateFooterBinding;
import org.schabi.newpipe.fragments.BaseStateFragment; import org.schabi.newpipe.fragments.BaseStateFragment;
import org.schabi.newpipe.fragments.list.ListViewContract; import org.schabi.newpipe.fragments.list.ListViewContract;
@ -42,8 +46,8 @@ public abstract class BaseLocalListFragment<I, N> extends BaseStateFragment<I>
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/
private static final int LIST_MODE_UPDATE_FLAG = 0x32; private static final int LIST_MODE_UPDATE_FLAG = 0x32;
private View headerRootView; private ViewBinding headerRootBinding;
private View footerRootView; private ViewBinding footerRootBinding;
protected LocalItemListAdapter itemListAdapter; protected LocalItemListAdapter itemListAdapter;
protected RecyclerView itemsList; protected RecyclerView itemsList;
private int updateFlags = 0; private int updateFlags = 0;
@ -86,12 +90,13 @@ public abstract class BaseLocalListFragment<I, N> extends BaseStateFragment<I>
// Lifecycle - View // Lifecycle - View
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/
protected View getListHeader() { @Nullable
protected ViewBinding getListHeader() {
return null; return null;
} }
protected View getListFooter() { protected ViewBinding getListFooter() {
return activity.getLayoutInflater().inflate(R.layout.pignate_footer, itemsList, false); return PignateFooterBinding.inflate(activity.getLayoutInflater(), itemsList, false);
} }
protected RecyclerView.LayoutManager getGridLayoutManager() { protected RecyclerView.LayoutManager getGridLayoutManager() {
@ -120,10 +125,12 @@ public abstract class BaseLocalListFragment<I, N> extends BaseStateFragment<I>
itemsList.setLayoutManager(useGrid ? getGridLayoutManager() : getListLayoutManager()); itemsList.setLayoutManager(useGrid ? getGridLayoutManager() : getListLayoutManager());
itemListAdapter.setUseGridVariant(useGrid); itemListAdapter.setUseGridVariant(useGrid);
headerRootView = getListHeader(); headerRootBinding = getListHeader();
itemListAdapter.setHeader(headerRootView); if (headerRootBinding != null) {
footerRootView = getListFooter(); itemListAdapter.setHeader(headerRootBinding.getRoot());
itemListAdapter.setFooter(footerRootView); }
footerRootBinding = getListFooter();
itemListAdapter.setFooter(footerRootBinding.getRoot());
itemsList.setAdapter(itemListAdapter); itemsList.setAdapter(itemListAdapter);
} }
@ -180,8 +187,8 @@ public abstract class BaseLocalListFragment<I, N> extends BaseStateFragment<I>
if (itemsList != null) { if (itemsList != null) {
animateView(itemsList, false, 200); animateView(itemsList, false, 200);
} }
if (headerRootView != null) { if (headerRootBinding != null) {
animateView(headerRootView, false, 200); animateView(headerRootBinding.getRoot(), false, 200);
} }
} }
@ -191,8 +198,8 @@ public abstract class BaseLocalListFragment<I, N> extends BaseStateFragment<I>
if (itemsList != null) { if (itemsList != null) {
animateView(itemsList, true, 200); animateView(itemsList, true, 200);
} }
if (headerRootView != null) { if (headerRootBinding != null) {
animateView(headerRootView, true, 200); animateView(headerRootBinding.getRoot(), true, 200);
} }
} }
@ -204,8 +211,8 @@ public abstract class BaseLocalListFragment<I, N> extends BaseStateFragment<I>
if (itemsList != null) { if (itemsList != null) {
animateView(itemsList, false, 200); animateView(itemsList, false, 200);
} }
if (headerRootView != null) { if (headerRootBinding != null) {
animateView(headerRootView, false, 200); animateView(headerRootBinding.getRoot(), false, 200);
} }
} }

View File

@ -37,18 +37,10 @@ import androidx.lifecycle.ViewModelProvider
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import icepick.State import icepick.State
import kotlinx.android.synthetic.main.error_retry.error_button_retry
import kotlinx.android.synthetic.main.error_retry.error_message_view
import kotlinx.android.synthetic.main.fragment_feed.empty_state_view
import kotlinx.android.synthetic.main.fragment_feed.error_panel
import kotlinx.android.synthetic.main.fragment_feed.items_list
import kotlinx.android.synthetic.main.fragment_feed.loading_progress_bar
import kotlinx.android.synthetic.main.fragment_feed.loading_progress_text
import kotlinx.android.synthetic.main.fragment_feed.refresh_root_view
import kotlinx.android.synthetic.main.fragment_feed.refresh_subtitle_text
import kotlinx.android.synthetic.main.fragment_feed.refresh_text
import org.schabi.newpipe.R import org.schabi.newpipe.R
import org.schabi.newpipe.database.feed.model.FeedGroupEntity import org.schabi.newpipe.database.feed.model.FeedGroupEntity
import org.schabi.newpipe.databinding.ErrorRetryBinding
import org.schabi.newpipe.databinding.FragmentFeedBinding
import org.schabi.newpipe.fragments.list.BaseListFragment import org.schabi.newpipe.fragments.list.BaseListFragment
import org.schabi.newpipe.local.feed.service.FeedLoadService import org.schabi.newpipe.local.feed.service.FeedLoadService
import org.schabi.newpipe.report.UserAction import org.schabi.newpipe.report.UserAction
@ -57,6 +49,12 @@ import org.schabi.newpipe.util.Localization
import java.util.Calendar import java.util.Calendar
class FeedFragment : BaseListFragment<FeedState, Unit>() { class FeedFragment : BaseListFragment<FeedState, Unit>() {
private var _feedBinding: FragmentFeedBinding? = null
private val feedBinding get() = _feedBinding!!
private var _errorBinding: ErrorRetryBinding? = null
private val errorBinding get() = _errorBinding!!
private lateinit var viewModel: FeedViewModel private lateinit var viewModel: FeedViewModel
private lateinit var swipeRefreshLayout: SwipeRefreshLayout private lateinit var swipeRefreshLayout: SwipeRefreshLayout
@State @State
@ -86,15 +84,17 @@ class FeedFragment : BaseListFragment<FeedState, Unit>() {
override fun onViewCreated(rootView: View, savedInstanceState: Bundle?) { override fun onViewCreated(rootView: View, savedInstanceState: Bundle?) {
super.onViewCreated(rootView, savedInstanceState) super.onViewCreated(rootView, savedInstanceState)
swipeRefreshLayout = requireView().findViewById(R.id.swiperefresh) _feedBinding = FragmentFeedBinding.bind(rootView)
swipeRefreshLayout.setOnRefreshListener { reloadContent() } _errorBinding = feedBinding.errorPanel
feedBinding.swiperefresh.setOnRefreshListener { reloadContent() }
viewModel = ViewModelProvider(this, FeedViewModel.Factory(requireContext(), groupId)).get(FeedViewModel::class.java) viewModel = ViewModelProvider(this, FeedViewModel.Factory(requireContext(), groupId)).get(FeedViewModel::class.java)
viewModel.stateLiveData.observe(viewLifecycleOwner, Observer { it?.let(::handleResult) }) viewModel.stateLiveData.observe(viewLifecycleOwner, Observer { it?.let(::handleResult) })
} }
override fun onPause() { override fun onPause() {
super.onPause() super.onPause()
listState = items_list?.layoutManager?.onSaveInstanceState() listState = _feedBinding?.itemsList?.layoutManager?.onSaveInstanceState()
} }
override fun onResume() { override fun onResume() {
@ -112,7 +112,8 @@ class FeedFragment : BaseListFragment<FeedState, Unit>() {
override fun initListeners() { override fun initListeners() {
super.initListeners() super.initListeners()
refresh_root_view.setOnClickListener { // Using the non-null property may result in a NullPointerException
_feedBinding?.refreshRootView?.setOnClickListener {
triggerUpdate() triggerUpdate()
} }
} }
@ -169,55 +170,60 @@ class FeedFragment : BaseListFragment<FeedState, Unit>() {
activity?.supportActionBar?.subtitle = null activity?.supportActionBar?.subtitle = null
} }
override fun onDestroyView() {
_feedBinding = null
super.onDestroyView()
}
// ///////////////////////////////////////////////////////////////////////// // /////////////////////////////////////////////////////////////////////////
// Handling // Handling
// ///////////////////////////////////////////////////////////////////////// // /////////////////////////////////////////////////////////////////////////
override fun showLoading() { override fun showLoading() {
animateView(refresh_root_view, false, 0) animateView(feedBinding.refreshRootView, false, 0)
animateView(items_list, false, 0) animateView(feedBinding.itemsList, false, 0)
animateView(loading_progress_bar, true, 200) animateView(feedBinding.loadingProgressBar, true, 200)
animateView(loading_progress_text, true, 200) animateView(feedBinding.loadingProgressText, true, 200)
empty_state_view?.let { animateView(it, false, 0) } animateView(feedBinding.emptyStateView.root, false, 0)
animateView(error_panel, false, 0) animateView(errorBinding.root, false, 0)
} }
override fun hideLoading() { override fun hideLoading() {
animateView(refresh_root_view, true, 200) animateView(feedBinding.refreshRootView, true, 200)
animateView(items_list, true, 300) animateView(feedBinding.itemsList, true, 300)
animateView(loading_progress_bar, false, 0) animateView(feedBinding.loadingProgressBar, false, 0)
animateView(loading_progress_text, false, 0) animateView(feedBinding.loadingProgressText, false, 0)
empty_state_view?.let { animateView(it, false, 0) } animateView(feedBinding.emptyStateView.root, false, 0)
animateView(error_panel, false, 0) animateView(errorBinding.root, false, 0)
swipeRefreshLayout.isRefreshing = false feedBinding.swiperefresh.isRefreshing = false
} }
override fun showEmptyState() { override fun showEmptyState() {
animateView(refresh_root_view, true, 200) animateView(feedBinding.refreshRootView, true, 200)
animateView(items_list, false, 0) animateView(feedBinding.itemsList, false, 0)
animateView(loading_progress_bar, false, 0) animateView(feedBinding.loadingProgressBar, false, 0)
animateView(loading_progress_text, false, 0) animateView(feedBinding.loadingProgressText, false, 0)
empty_state_view?.let { animateView(it, true, 800) } animateView(feedBinding.emptyStateView.root, true, 800)
animateView(error_panel, false, 0) animateView(errorBinding.root, false, 0)
} }
override fun showError(message: String, showRetryButton: Boolean) { override fun showError(message: String, showRetryButton: Boolean) {
infoListAdapter.clearStreamItemList() infoListAdapter.clearStreamItemList()
animateView(refresh_root_view, false, 120) animateView(feedBinding.refreshRootView, false, 120)
animateView(items_list, false, 120) animateView(feedBinding.itemsList, false, 120)
animateView(loading_progress_bar, false, 120) animateView(feedBinding.loadingProgressBar, false, 120)
animateView(loading_progress_text, false, 120) animateView(feedBinding.loadingProgressText, false, 120)
error_message_view.text = message errorBinding.errorMessageView.text = message
animateView(error_button_retry, showRetryButton, if (showRetryButton) 600 else 0) animateView(errorBinding.errorButtonRetry, showRetryButton, if (showRetryButton) 600 else 0)
animateView(error_panel, true, 300) animateView(errorBinding.root, true, 300)
} }
override fun handleResult(result: FeedState) { override fun handleResult(result: FeedState) {
@ -237,33 +243,36 @@ class FeedFragment : BaseListFragment<FeedState, Unit>() {
progressState.maxProgress == -1 progressState.maxProgress == -1
if (!isIndeterminate) { if (!isIndeterminate) {
loading_progress_text.text = "${progressState.currentProgress}/${progressState.maxProgress}" feedBinding.loadingProgressText.text = "${progressState.currentProgress}/${progressState.maxProgress}"
} else if (progressState.progressMessage > 0) { } else if (progressState.progressMessage > 0) {
loading_progress_text?.setText(progressState.progressMessage) _feedBinding?.loadingProgressText?.setText(progressState.progressMessage)
} else { } else {
loading_progress_text?.text = "∞/∞" _feedBinding?.loadingProgressText?.text = "∞/∞"
} }
loading_progress_bar.isIndeterminate = isIndeterminate || feedBinding.loadingProgressBar.isIndeterminate = isIndeterminate ||
(progressState.maxProgress > 0 && progressState.currentProgress == 0) (progressState.maxProgress > 0 && progressState.currentProgress == 0)
loading_progress_bar.progress = progressState.currentProgress feedBinding.loadingProgressBar.progress = progressState.currentProgress
loading_progress_bar.max = progressState.maxProgress feedBinding.loadingProgressBar.max = progressState.maxProgress
} }
private fun handleLoadedState(loadedState: FeedState.LoadedState) { private fun handleLoadedState(loadedState: FeedState.LoadedState) {
infoListAdapter.setInfoItemList(loadedState.items) infoListAdapter.setInfoItemList(loadedState.items)
listState?.run { listState?.run {
items_list.layoutManager?.onRestoreInstanceState(listState) feedBinding.itemsList.layoutManager?.onRestoreInstanceState(listState)
listState = null listState = null
} }
oldestSubscriptionUpdate = loadedState.oldestUpdate oldestSubscriptionUpdate = loadedState.oldestUpdate
val loadedCount = loadedState.notLoadedCount > 0 val loadedCount = loadedState.notLoadedCount > 0
refresh_subtitle_text.isVisible = loadedCount feedBinding.refreshSubtitleText.isVisible = loadedCount
if (loadedCount) { if (loadedCount) {
refresh_subtitle_text.text = getString(R.string.feed_subscription_not_loaded_count, loadedState.notLoadedCount) feedBinding.refreshSubtitleText.text = getString(
R.string.feed_subscription_not_loaded_count,
loadedState.notLoadedCount
)
} }
if (loadedState.itemsErrors.isNotEmpty()) { if (loadedState.itemsErrors.isNotEmpty()) {
@ -300,7 +309,7 @@ class FeedFragment : BaseListFragment<FeedState, Unit>() {
else -> "" else -> ""
} }
refresh_text?.text = getString(R.string.feed_oldest_subscription_update, oldestSubscriptionUpdateText) feedBinding.refreshText.text = getString(R.string.feed_oldest_subscription_update, oldestSubscriptionUpdateText)
} }
// ///////////////////////////////////////////////////////////////////////// // /////////////////////////////////////////////////////////////////////////

View File

@ -30,6 +30,7 @@ import android.os.IBinder
import android.util.Log import android.util.Log
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat import androidx.core.app.NotificationManagerCompat
import androidx.core.app.ServiceCompat
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.core.Flowable import io.reactivex.rxjava3.core.Flowable
@ -42,19 +43,20 @@ import io.reactivex.rxjava3.processors.PublishProcessor
import io.reactivex.rxjava3.schedulers.Schedulers import io.reactivex.rxjava3.schedulers.Schedulers
import org.reactivestreams.Subscriber import org.reactivestreams.Subscriber
import org.reactivestreams.Subscription import org.reactivestreams.Subscription
import org.schabi.newpipe.App
import org.schabi.newpipe.MainActivity.DEBUG import org.schabi.newpipe.MainActivity.DEBUG
import org.schabi.newpipe.R import org.schabi.newpipe.R
import org.schabi.newpipe.database.feed.model.FeedGroupEntity import org.schabi.newpipe.database.feed.model.FeedGroupEntity
import org.schabi.newpipe.extractor.ListInfo import org.schabi.newpipe.extractor.ListInfo
import org.schabi.newpipe.extractor.exceptions.ReCaptchaException import org.schabi.newpipe.extractor.exceptions.ReCaptchaException
import org.schabi.newpipe.extractor.stream.StreamInfoItem import org.schabi.newpipe.extractor.stream.StreamInfoItem
import org.schabi.newpipe.ktx.isNetworkRelated
import org.schabi.newpipe.local.feed.FeedDatabaseManager import org.schabi.newpipe.local.feed.FeedDatabaseManager
import org.schabi.newpipe.local.feed.service.FeedEventManager.Event.ErrorResultEvent import org.schabi.newpipe.local.feed.service.FeedEventManager.Event.ErrorResultEvent
import org.schabi.newpipe.local.feed.service.FeedEventManager.Event.ProgressEvent import org.schabi.newpipe.local.feed.service.FeedEventManager.Event.ProgressEvent
import org.schabi.newpipe.local.feed.service.FeedEventManager.Event.SuccessResultEvent import org.schabi.newpipe.local.feed.service.FeedEventManager.Event.SuccessResultEvent
import org.schabi.newpipe.local.feed.service.FeedEventManager.postEvent import org.schabi.newpipe.local.feed.service.FeedEventManager.postEvent
import org.schabi.newpipe.local.subscription.SubscriptionManager import org.schabi.newpipe.local.subscription.SubscriptionManager
import org.schabi.newpipe.util.ExceptionUtils
import org.schabi.newpipe.util.ExtractorHelper import org.schabi.newpipe.util.ExtractorHelper
import java.io.IOException import java.io.IOException
import java.time.OffsetDateTime import java.time.OffsetDateTime
@ -67,7 +69,7 @@ class FeedLoadService : Service() {
companion object { companion object {
private val TAG = FeedLoadService::class.java.simpleName private val TAG = FeedLoadService::class.java.simpleName
private const val NOTIFICATION_ID = 7293450 private const val NOTIFICATION_ID = 7293450
private const val ACTION_CANCEL = "org.schabi.newpipe.local.feed.service.FeedLoadService.CANCEL" private const val ACTION_CANCEL = App.PACKAGE_NAME + ".local.feed.service.FeedLoadService.CANCEL"
/** /**
* How often the notification will be updated. * How often the notification will be updated.
@ -147,7 +149,7 @@ class FeedLoadService : Service() {
private fun stopService() { private fun stopService() {
disposeAll() disposeAll()
stopForeground(true) ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE)
notificationManager.cancel(NOTIFICATION_ID) notificationManager.cancel(NOTIFICATION_ID)
stopSelf() stopSelf()
} }
@ -342,7 +344,7 @@ class FeedLoadService : Service() {
error is IOException -> throw error error is IOException -> throw error
cause is IOException -> throw cause cause is IOException -> throw cause
ExceptionUtils.isNetworkRelated(error) -> throw IOException(error) error.isNetworkRelated -> throw IOException(error)
} }
} }
} }

View File

@ -10,13 +10,12 @@ import android.view.MenuInflater;
import android.view.MenuItem; import android.view.MenuItem;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import android.widget.Toast; import android.widget.Toast;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog; import androidx.appcompat.app.AlertDialog;
import androidx.viewbinding.ViewBinding;
import com.google.android.material.snackbar.Snackbar; import com.google.android.material.snackbar.Snackbar;
@ -26,6 +25,8 @@ import org.schabi.newpipe.R;
import org.schabi.newpipe.database.LocalItem; import org.schabi.newpipe.database.LocalItem;
import org.schabi.newpipe.database.stream.StreamStatisticsEntry; import org.schabi.newpipe.database.stream.StreamStatisticsEntry;
import org.schabi.newpipe.database.stream.model.StreamEntity; import org.schabi.newpipe.database.stream.model.StreamEntity;
import org.schabi.newpipe.databinding.PlaylistControlBinding;
import org.schabi.newpipe.databinding.StatisticPlaylistControlBinding;
import org.schabi.newpipe.extractor.stream.StreamInfoItem; import org.schabi.newpipe.extractor.stream.StreamInfoItem;
import org.schabi.newpipe.extractor.stream.StreamType; import org.schabi.newpipe.extractor.stream.StreamType;
import org.schabi.newpipe.info_list.InfoItemDialog; import org.schabi.newpipe.info_list.InfoItemDialog;
@ -37,6 +38,7 @@ import org.schabi.newpipe.report.ErrorActivity;
import org.schabi.newpipe.report.ErrorInfo; import org.schabi.newpipe.report.ErrorInfo;
import org.schabi.newpipe.report.UserAction; import org.schabi.newpipe.report.UserAction;
import org.schabi.newpipe.settings.SettingsActivity; import org.schabi.newpipe.settings.SettingsActivity;
import org.schabi.newpipe.util.KoreUtil;
import org.schabi.newpipe.util.NavigationHelper; import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.OnClickGesture; import org.schabi.newpipe.util.OnClickGesture;
import org.schabi.newpipe.util.StreamDialogEntry; import org.schabi.newpipe.util.StreamDialogEntry;
@ -59,13 +61,10 @@ public class StatisticsPlaylistFragment
@State @State
Parcelable itemsListState; Parcelable itemsListState;
private StatisticSortMode sortMode = StatisticSortMode.LAST_PLAYED; private StatisticSortMode sortMode = StatisticSortMode.LAST_PLAYED;
private View headerPlayAllButton;
private View headerPopupButton; private StatisticPlaylistControlBinding headerBinding;
private View headerBackgroundButton; private PlaylistControlBinding playlistControlBinding;
private View playlistCtrl;
private View sortButton;
private ImageView sortButtonIcon;
private TextView sortButtonText;
/* Used for independent events */ /* Used for independent events */
private Subscription databaseSubscription; private Subscription databaseSubscription;
private HistoryRecordManager recordManager; private HistoryRecordManager recordManager;
@ -130,17 +129,12 @@ public class StatisticsPlaylistFragment
} }
@Override @Override
protected View getListHeader() { protected ViewBinding getListHeader() {
final View headerRootLayout = activity.getLayoutInflater() headerBinding = StatisticPlaylistControlBinding.inflate(activity.getLayoutInflater(),
.inflate(R.layout.statistic_playlist_control, itemsList, false); itemsList, false);
playlistCtrl = headerRootLayout.findViewById(R.id.playlist_control); playlistControlBinding = headerBinding.playlistControl;
headerPlayAllButton = headerRootLayout.findViewById(R.id.playlist_ctrl_play_all_button);
headerPopupButton = headerRootLayout.findViewById(R.id.playlist_ctrl_play_popup_button); return headerBinding;
headerBackgroundButton = headerRootLayout.findViewById(R.id.playlist_ctrl_play_bg_button);
sortButton = headerRootLayout.findViewById(R.id.sortButton);
sortButtonIcon = headerRootLayout.findViewById(R.id.sortButtonIcon);
sortButtonText = headerRootLayout.findViewById(R.id.sortButtonText);
return headerRootLayout;
} }
@Override @Override
@ -244,14 +238,13 @@ public class StatisticsPlaylistFragment
if (itemListAdapter != null) { if (itemListAdapter != null) {
itemListAdapter.unsetSelectedListener(); itemListAdapter.unsetSelectedListener();
} }
if (headerBackgroundButton != null) { if (playlistControlBinding != null) {
headerBackgroundButton.setOnClickListener(null); playlistControlBinding.playlistCtrlPlayBgButton.setOnClickListener(null);
} playlistControlBinding.playlistCtrlPlayAllButton.setOnClickListener(null);
if (headerPlayAllButton != null) { playlistControlBinding.playlistCtrlPlayPopupButton.setOnClickListener(null);
headerPlayAllButton.setOnClickListener(null);
} headerBinding = null;
if (headerPopupButton != null) { playlistControlBinding = null;
headerPopupButton.setOnClickListener(null);
} }
if (databaseSubscription != null) { if (databaseSubscription != null) {
@ -310,7 +303,7 @@ public class StatisticsPlaylistFragment
return; return;
} }
playlistCtrl.setVisibility(View.VISIBLE); playlistControlBinding.getRoot().setVisibility(View.VISIBLE);
itemListAdapter.clearStreamItemList(); itemListAdapter.clearStreamItemList();
@ -325,13 +318,13 @@ public class StatisticsPlaylistFragment
itemsListState = null; itemsListState = null;
} }
headerPlayAllButton.setOnClickListener(view -> playlistControlBinding.playlistCtrlPlayAllButton.setOnClickListener(view ->
NavigationHelper.playOnMainPlayer(activity, getPlayQueue())); NavigationHelper.playOnMainPlayer(activity, getPlayQueue()));
headerPopupButton.setOnClickListener(view -> playlistControlBinding.playlistCtrlPlayPopupButton.setOnClickListener(view ->
NavigationHelper.playOnPopupPlayer(activity, getPlayQueue(), false)); NavigationHelper.playOnPopupPlayer(activity, getPlayQueue(), false));
headerBackgroundButton.setOnClickListener(view -> playlistControlBinding.playlistCtrlPlayBgButton.setOnClickListener(view ->
NavigationHelper.playOnBackgroundPlayer(activity, getPlayQueue(), false)); NavigationHelper.playOnBackgroundPlayer(activity, getPlayQueue(), false));
sortButton.setOnClickListener(view -> toggleSortMode()); headerBinding.sortButton.setOnClickListener(view -> toggleSortMode());
hideLoading(); hideLoading();
} }
@ -367,15 +360,15 @@ public class StatisticsPlaylistFragment
if (sortMode == StatisticSortMode.LAST_PLAYED) { if (sortMode == StatisticSortMode.LAST_PLAYED) {
sortMode = StatisticSortMode.MOST_PLAYED; sortMode = StatisticSortMode.MOST_PLAYED;
setTitle(getString(R.string.title_most_played)); setTitle(getString(R.string.title_most_played));
sortButtonIcon.setImageResource( headerBinding.sortButtonIcon.setImageResource(
ThemeHelper.resolveResourceIdFromAttr(requireContext(), R.attr.ic_history)); ThemeHelper.resolveResourceIdFromAttr(requireContext(), R.attr.ic_history));
sortButtonText.setText(R.string.title_last_played); headerBinding.sortButtonText.setText(R.string.title_last_played);
} else { } else {
sortMode = StatisticSortMode.LAST_PLAYED; sortMode = StatisticSortMode.LAST_PLAYED;
setTitle(getString(R.string.title_last_played)); setTitle(getString(R.string.title_last_played));
sortButtonIcon.setImageResource( headerBinding.sortButtonIcon.setImageResource(
ThemeHelper.resolveResourceIdFromAttr(requireContext(), R.attr.ic_filter_list)); ThemeHelper.resolveResourceIdFromAttr(requireContext(), R.attr.ic_filter_list));
sortButtonText.setText(R.string.title_most_played); headerBinding.sortButtonText.setText(R.string.title_most_played);
} }
startLoading(true); startLoading(true);
} }
@ -413,6 +406,9 @@ public class StatisticsPlaylistFragment
StreamDialogEntry.share StreamDialogEntry.share
)); ));
} }
if (KoreUtil.shouldShowPlayWithKodi(context, infoItem.getServiceId())) {
entries.add(StreamDialogEntry.play_with_kodi);
}
StreamDialogEntry.setEnabledEntries(entries); StreamDialogEntry.setEnabledEntries(entries);
StreamDialogEntry.start_here_on_background.setCustomAction((fragment, infoItemDuplicate) -> StreamDialogEntry.start_here_on_background.setCustomAction((fragment, infoItemDuplicate) ->

View File

@ -14,7 +14,6 @@ import android.view.MenuItem;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.widget.EditText; import android.widget.EditText;
import android.widget.TextView;
import android.widget.Toast; import android.widget.Toast;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
@ -22,6 +21,7 @@ import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog; import androidx.appcompat.app.AlertDialog;
import androidx.recyclerview.widget.ItemTouchHelper; import androidx.recyclerview.widget.ItemTouchHelper;
import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.RecyclerView;
import androidx.viewbinding.ViewBinding;
import org.reactivestreams.Subscriber; import org.reactivestreams.Subscriber;
import org.reactivestreams.Subscription; import org.reactivestreams.Subscription;
@ -32,6 +32,8 @@ import org.schabi.newpipe.database.history.model.StreamHistoryEntry;
import org.schabi.newpipe.database.playlist.PlaylistStreamEntry; import org.schabi.newpipe.database.playlist.PlaylistStreamEntry;
import org.schabi.newpipe.database.stream.model.StreamEntity; import org.schabi.newpipe.database.stream.model.StreamEntity;
import org.schabi.newpipe.database.stream.model.StreamStateEntity; import org.schabi.newpipe.database.stream.model.StreamStateEntity;
import org.schabi.newpipe.databinding.LocalPlaylistHeaderBinding;
import org.schabi.newpipe.databinding.PlaylistControlBinding;
import org.schabi.newpipe.extractor.stream.StreamInfoItem; import org.schabi.newpipe.extractor.stream.StreamInfoItem;
import org.schabi.newpipe.extractor.stream.StreamType; import org.schabi.newpipe.extractor.stream.StreamType;
import org.schabi.newpipe.info_list.InfoItemDialog; import org.schabi.newpipe.info_list.InfoItemDialog;
@ -41,6 +43,7 @@ import org.schabi.newpipe.player.helper.PlayerHolder;
import org.schabi.newpipe.player.playqueue.PlayQueue; import org.schabi.newpipe.player.playqueue.PlayQueue;
import org.schabi.newpipe.player.playqueue.SinglePlayQueue; import org.schabi.newpipe.player.playqueue.SinglePlayQueue;
import org.schabi.newpipe.report.UserAction; import org.schabi.newpipe.report.UserAction;
import org.schabi.newpipe.util.KoreUtil;
import org.schabi.newpipe.util.Localization; import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.util.NavigationHelper; import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.OnClickGesture; import org.schabi.newpipe.util.OnClickGesture;
@ -76,13 +79,8 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
@State @State
Parcelable itemsListState; Parcelable itemsListState;
private View headerRootLayout; private LocalPlaylistHeaderBinding headerBinding;
private TextView headerTitleView; private PlaylistControlBinding playlistControlBinding;
private TextView headerStreamCount;
private View playlistControl;
private View headerPlayAllButton;
private View headerPopupButton;
private View headerBackgroundButton;
private ItemTouchHelper itemTouchHelper; private ItemTouchHelper itemTouchHelper;
@ -136,8 +134,8 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
public void setTitle(final String title) { public void setTitle(final String title) {
super.setTitle(title); super.setTitle(title);
if (headerTitleView != null) { if (headerBinding != null) {
headerTitleView.setText(title); headerBinding.playlistTitleView.setText(title);
} }
} }
@ -148,28 +146,21 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
} }
@Override @Override
protected View getListHeader() { protected ViewBinding getListHeader() {
headerRootLayout = activity.getLayoutInflater() headerBinding = LocalPlaylistHeaderBinding.inflate(activity.getLayoutInflater(), itemsList,
.inflate(R.layout.local_playlist_header, itemsList, false); false);
playlistControlBinding = headerBinding.playlistControl;
headerTitleView = headerRootLayout.findViewById(R.id.playlist_title_view); headerBinding.playlistTitleView.setSelected(true);
headerTitleView.setSelected(true);
headerStreamCount = headerRootLayout.findViewById(R.id.playlist_stream_count); return headerBinding;
playlistControl = headerRootLayout.findViewById(R.id.playlist_control);
headerPlayAllButton = headerRootLayout.findViewById(R.id.playlist_ctrl_play_all_button);
headerPopupButton = headerRootLayout.findViewById(R.id.playlist_ctrl_play_popup_button);
headerBackgroundButton = headerRootLayout.findViewById(R.id.playlist_ctrl_play_bg_button);
return headerRootLayout;
} }
@Override @Override
protected void initListeners() { protected void initListeners() {
super.initListeners(); super.initListeners();
headerTitleView.setOnClickListener(view -> createRenameDialog()); headerBinding.playlistTitleView.setOnClickListener(view -> createRenameDialog());
itemTouchHelper = new ItemTouchHelper(getItemTouchCallback()); itemTouchHelper = new ItemTouchHelper(getItemTouchCallback());
itemTouchHelper.attachToRecyclerView(itemsList); itemTouchHelper.attachToRecyclerView(itemsList);
@ -209,22 +200,18 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
@Override @Override
public void showLoading() { public void showLoading() {
super.showLoading(); super.showLoading();
if (headerRootLayout != null) { if (headerBinding != null) {
animateView(headerRootLayout, false, 200); animateView(headerBinding.getRoot(), false, 200);
} animateView(playlistControlBinding.getRoot(), false, 200);
if (playlistControl != null) {
animateView(playlistControl, false, 200);
} }
} }
@Override @Override
public void hideLoading() { public void hideLoading() {
super.hideLoading(); super.hideLoading();
if (headerRootLayout != null) { if (headerBinding != null) {
animateView(headerRootLayout, true, 200); animateView(headerBinding.getRoot(), true, 200);
} animateView(playlistControlBinding.getRoot(), true, 200);
if (playlistControl != null) {
animateView(playlistControl, true, 200);
} }
} }
@ -276,14 +263,13 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
if (itemListAdapter != null) { if (itemListAdapter != null) {
itemListAdapter.unsetSelectedListener(); itemListAdapter.unsetSelectedListener();
} }
if (headerBackgroundButton != null) { if (playlistControlBinding != null) {
headerBackgroundButton.setOnClickListener(null); playlistControlBinding.playlistCtrlPlayBgButton.setOnClickListener(null);
} playlistControlBinding.playlistCtrlPlayAllButton.setOnClickListener(null);
if (headerPlayAllButton != null) { playlistControlBinding.playlistCtrlPlayPopupButton.setOnClickListener(null);
headerPlayAllButton.setOnClickListener(null);
} headerBinding = null;
if (headerPopupButton != null) { playlistControlBinding = null;
headerPopupButton.setOnClickListener(null);
} }
if (databaseSubscription != null) { if (databaseSubscription != null) {
@ -493,19 +479,19 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
} }
setVideoCount(itemListAdapter.getItemsList().size()); setVideoCount(itemListAdapter.getItemsList().size());
headerPlayAllButton.setOnClickListener(view -> playlistControlBinding.playlistCtrlPlayAllButton.setOnClickListener(view ->
NavigationHelper.playOnMainPlayer(activity, getPlayQueue())); NavigationHelper.playOnMainPlayer(activity, getPlayQueue()));
headerPopupButton.setOnClickListener(view -> playlistControlBinding.playlistCtrlPlayPopupButton.setOnClickListener(view ->
NavigationHelper.playOnPopupPlayer(activity, getPlayQueue(), false)); NavigationHelper.playOnPopupPlayer(activity, getPlayQueue(), false));
headerBackgroundButton.setOnClickListener(view -> playlistControlBinding.playlistCtrlPlayBgButton.setOnClickListener(view ->
NavigationHelper.playOnBackgroundPlayer(activity, getPlayQueue(), false)); NavigationHelper.playOnBackgroundPlayer(activity, getPlayQueue(), false));
headerPopupButton.setOnLongClickListener(view -> { playlistControlBinding.playlistCtrlPlayPopupButton.setOnLongClickListener(view -> {
NavigationHelper.enqueueOnPopupPlayer(activity, getPlayQueue(), true); NavigationHelper.enqueueOnPopupPlayer(activity, getPlayQueue(), true);
return true; return true;
}); });
headerBackgroundButton.setOnLongClickListener(view -> { playlistControlBinding.playlistCtrlPlayBgButton.setOnLongClickListener(view -> {
NavigationHelper.enqueueOnBackgroundPlayer(activity, getPlayQueue(), true); NavigationHelper.enqueueOnBackgroundPlayer(activity, getPlayQueue(), true);
return true; return true;
}); });
@ -781,6 +767,9 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
StreamDialogEntry.share StreamDialogEntry.share
)); ));
} }
if (KoreUtil.shouldShowPlayWithKodi(context, infoItem.getServiceId())) {
entries.add(StreamDialogEntry.play_with_kodi);
}
StreamDialogEntry.setEnabledEntries(entries); StreamDialogEntry.setEnabledEntries(entries);
StreamDialogEntry.start_here_on_background.setCustomAction((fragment, infoItemDuplicate) -> StreamDialogEntry.start_here_on_background.setCustomAction((fragment, infoItemDuplicate) ->
@ -802,8 +791,9 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
} }
private void setVideoCount(final long count) { private void setVideoCount(final long count) {
if (activity != null && headerStreamCount != null) { if (activity != null && headerBinding != null) {
headerStreamCount.setText(Localization.localizeStreamCount(activity, count)); headerBinding.playlistStreamCount.setText(Localization
.localizeStreamCount(activity, count));
} }
} }

View File

@ -17,6 +17,7 @@ import android.view.MenuInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.Toast import android.widget.Toast
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import androidx.localbroadcastmanager.content.LocalBroadcastManager import androidx.localbroadcastmanager.content.LocalBroadcastManager
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
@ -29,11 +30,10 @@ import com.xwray.groupie.Section
import com.xwray.groupie.kotlinandroidextensions.GroupieViewHolder import com.xwray.groupie.kotlinandroidextensions.GroupieViewHolder
import icepick.State import icepick.State
import io.reactivex.rxjava3.disposables.CompositeDisposable import io.reactivex.rxjava3.disposables.CompositeDisposable
import kotlinx.android.synthetic.main.dialog_title.view.itemAdditionalDetails
import kotlinx.android.synthetic.main.dialog_title.view.itemTitleView
import kotlinx.android.synthetic.main.fragment_subscription.items_list
import org.schabi.newpipe.R import org.schabi.newpipe.R
import org.schabi.newpipe.database.feed.model.FeedGroupEntity import org.schabi.newpipe.database.feed.model.FeedGroupEntity
import org.schabi.newpipe.databinding.DialogTitleBinding
import org.schabi.newpipe.databinding.FragmentSubscriptionBinding
import org.schabi.newpipe.extractor.channel.ChannelInfoItem import org.schabi.newpipe.extractor.channel.ChannelInfoItem
import org.schabi.newpipe.fragments.BaseStateFragment import org.schabi.newpipe.fragments.BaseStateFragment
import org.schabi.newpipe.local.subscription.SubscriptionViewModel.SubscriptionState import org.schabi.newpipe.local.subscription.SubscriptionViewModel.SubscriptionState
@ -70,6 +70,9 @@ import kotlin.math.floor
import kotlin.math.max import kotlin.math.max
class SubscriptionFragment : BaseStateFragment<SubscriptionState>() { class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
private var _binding: FragmentSubscriptionBinding? = null
private val binding get() = _binding!!
private lateinit var viewModel: SubscriptionViewModel private lateinit var viewModel: SubscriptionViewModel
private lateinit var subscriptionManager: SubscriptionManager private lateinit var subscriptionManager: SubscriptionManager
private val disposables: CompositeDisposable = CompositeDisposable() private val disposables: CompositeDisposable = CompositeDisposable()
@ -129,7 +132,7 @@ class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
override fun onPause() { override fun onPause() {
super.onPause() super.onPause()
itemsListState = items_list.layoutManager?.onSaveInstanceState() itemsListState = binding.itemsList.layoutManager?.onSaveInstanceState()
feedGroupsListState = feedGroupsCarousel?.onSaveInstanceState() feedGroupsListState = feedGroupsCarousel?.onSaveInstanceState()
importExportItemExpandedState = importExportItem.isExpanded importExportItemExpandedState = importExportItem.isExpanded
@ -169,7 +172,7 @@ class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
filters.addAction(IMPORT_COMPLETE_ACTION) filters.addAction(IMPORT_COMPLETE_ACTION)
subscriptionBroadcastReceiver = object : BroadcastReceiver() { subscriptionBroadcastReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) { override fun onReceive(context: Context, intent: Intent) {
items_list?.post { _binding?.itemsList?.post {
importExportItem.isExpanded = false importExportItem.isExpanded = false
importExportItem.notifyChanged(FeedImportExportItem.REFRESH_EXPANDED_STATUS) importExportItem.notifyChanged(FeedImportExportItem.REFRESH_EXPANDED_STATUS)
} }
@ -275,17 +278,18 @@ class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
override fun initViews(rootView: View, savedInstanceState: Bundle?) { override fun initViews(rootView: View, savedInstanceState: Bundle?) {
super.initViews(rootView, savedInstanceState) super.initViews(rootView, savedInstanceState)
_binding = FragmentSubscriptionBinding.bind(rootView)
val shouldUseGridLayout = shouldUseGridLayout() val shouldUseGridLayout = shouldUseGridLayout()
groupAdapter.spanCount = if (shouldUseGridLayout) getGridSpanCount() else 1 groupAdapter.spanCount = if (shouldUseGridLayout) getGridSpanCount() else 1
items_list.layoutManager = GridLayoutManager(requireContext(), groupAdapter.spanCount).apply { binding.itemsList.layoutManager = GridLayoutManager(requireContext(), groupAdapter.spanCount).apply {
spanSizeLookup = groupAdapter.spanSizeLookup spanSizeLookup = groupAdapter.spanSizeLookup
} }
items_list.adapter = groupAdapter binding.itemsList.adapter = groupAdapter
viewModel = ViewModelProvider(this).get(SubscriptionViewModel::class.java) viewModel = ViewModelProvider(this).get(SubscriptionViewModel::class.java)
viewModel.stateLiveData.observe(viewLifecycleOwner, androidx.lifecycle.Observer { it?.let(this::handleResult) }) viewModel.stateLiveData.observe(viewLifecycleOwner, Observer { it?.let(this::handleResult) })
viewModel.feedGroupsLiveData.observe(viewLifecycleOwner, androidx.lifecycle.Observer { it?.let(this::handleFeedGroups) }) viewModel.feedGroupsLiveData.observe(viewLifecycleOwner, Observer { it?.let(this::handleFeedGroups) })
} }
private fun showLongTapDialog(selectedItem: ChannelInfoItem) { private fun showLongTapDialog(selectedItem: ChannelInfoItem) {
@ -301,13 +305,13 @@ class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
} }
} }
val bannerView = View.inflate(requireContext(), R.layout.dialog_title, null) val dialogTitleBinding = DialogTitleBinding.inflate(LayoutInflater.from(requireContext()))
bannerView.isSelected = true dialogTitleBinding.root.isSelected = true
bannerView.itemTitleView.text = selectedItem.name dialogTitleBinding.itemTitleView.text = selectedItem.name
bannerView.itemAdditionalDetails.visibility = View.GONE dialogTitleBinding.itemAdditionalDetails.visibility = View.GONE
AlertDialog.Builder(requireContext()) AlertDialog.Builder(requireContext())
.setCustomTitle(bannerView) .setCustomTitle(dialogTitleBinding.root)
.setItems(commands, actions) .setItems(commands, actions)
.create() .create()
.show() .show()
@ -368,14 +372,14 @@ class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
subscriptionsSection.setHideWhenEmpty(false) subscriptionsSection.setHideWhenEmpty(false)
if (result.subscriptions.isEmpty() && importExportItemExpandedState == null) { if (result.subscriptions.isEmpty() && importExportItemExpandedState == null) {
items_list.post { binding.itemsList.post {
importExportItem.isExpanded = true importExportItem.isExpanded = true
importExportItem.notifyChanged(FeedImportExportItem.REFRESH_EXPANDED_STATUS) importExportItem.notifyChanged(FeedImportExportItem.REFRESH_EXPANDED_STATUS)
} }
} }
if (itemsListState != null) { if (itemsListState != null) {
items_list.layoutManager?.onRestoreInstanceState(itemsListState) binding.itemsList.layoutManager?.onRestoreInstanceState(itemsListState)
itemsListState = null itemsListState = null
} }
} }
@ -394,7 +398,7 @@ class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
} }
feedGroupsSortMenuItem.showMenuItem = groups.size > 1 feedGroupsSortMenuItem.showMenuItem = groups.size > 1
items_list.post { feedGroupsSortMenuItem.notifyChanged(PAYLOAD_UPDATE_VISIBILITY_MENU_ITEM) } binding.itemsList.post { feedGroupsSortMenuItem.notifyChanged(PAYLOAD_UPDATE_VISIBILITY_MENU_ITEM) }
} }
// ///////////////////////////////////////////////////////////////////////// // /////////////////////////////////////////////////////////////////////////
@ -403,12 +407,12 @@ class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
override fun showLoading() { override fun showLoading() {
super.showLoading() super.showLoading()
animateView(items_list, false, 100) animateView(binding.itemsList, false, 100)
} }
override fun hideLoading() { override fun hideLoading() {
super.hideLoading() super.hideLoading()
animateView(items_list, true, 200) animateView(binding.itemsList, true, 200)
} }
// ///////////////////////////////////////////////////////////////////////// // /////////////////////////////////////////////////////////////////////////

View File

@ -24,10 +24,10 @@ import com.xwray.groupie.Section
import com.xwray.groupie.kotlinandroidextensions.GroupieViewHolder import com.xwray.groupie.kotlinandroidextensions.GroupieViewHolder
import icepick.Icepick import icepick.Icepick
import icepick.State import icepick.State
import kotlinx.android.synthetic.main.dialog_feed_group_create.*
import kotlinx.android.synthetic.main.toolbar_search_layout.*
import org.schabi.newpipe.R import org.schabi.newpipe.R
import org.schabi.newpipe.database.feed.model.FeedGroupEntity import org.schabi.newpipe.database.feed.model.FeedGroupEntity
import org.schabi.newpipe.databinding.DialogFeedGroupCreateBinding
import org.schabi.newpipe.databinding.ToolbarSearchLayoutBinding
import org.schabi.newpipe.fragments.BackPressable import org.schabi.newpipe.fragments.BackPressable
import org.schabi.newpipe.local.subscription.FeedGroupIcon import org.schabi.newpipe.local.subscription.FeedGroupIcon
import org.schabi.newpipe.local.subscription.dialog.FeedGroupDialog.ScreenState.DeleteScreen import org.schabi.newpipe.local.subscription.dialog.FeedGroupDialog.ScreenState.DeleteScreen
@ -45,6 +45,12 @@ import java.io.Serializable
import kotlin.collections.contains import kotlin.collections.contains
class FeedGroupDialog : DialogFragment(), BackPressable { class FeedGroupDialog : DialogFragment(), BackPressable {
private var _feedGroupCreateBinding: DialogFeedGroupCreateBinding? = null
private val feedGroupCreateBinding get() = _feedGroupCreateBinding!!
private var _searchLayoutBinding: ToolbarSearchLayoutBinding? = null
private val searchLayoutBinding get() = _searchLayoutBinding!!
private lateinit var viewModel: FeedGroupDialogViewModel private lateinit var viewModel: FeedGroupDialogViewModel
private var groupId: Long = NO_GROUP_SELECTED private var groupId: Long = NO_GROUP_SELECTED
private var groupIcon: FeedGroupIcon? = null private var groupIcon: FeedGroupIcon? = null
@ -107,14 +113,16 @@ class FeedGroupDialog : DialogFragment(), BackPressable {
override fun onSaveInstanceState(outState: Bundle) { override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState) super.onSaveInstanceState(outState)
iconsListState = icon_selector.layoutManager?.onSaveInstanceState() iconsListState = feedGroupCreateBinding.iconSelector.layoutManager?.onSaveInstanceState()
subscriptionsListState = subscriptions_selector_list.layoutManager?.onSaveInstanceState() subscriptionsListState = feedGroupCreateBinding.subscriptionsSelectorList.layoutManager?.onSaveInstanceState()
Icepick.saveInstanceState(this, outState) Icepick.saveInstanceState(this, outState)
} }
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
_feedGroupCreateBinding = DialogFeedGroupCreateBinding.bind(view)
_searchLayoutBinding = feedGroupCreateBinding.subscriptionsHeaderSearchContainer
viewModel = ViewModelProvider( viewModel = ViewModelProvider(
this, this,
@ -146,7 +154,7 @@ class FeedGroupDialog : DialogFragment(), BackPressable {
add(subscriptionEmptyFooter) add(subscriptionEmptyFooter)
spanCount = 4 spanCount = 4
} }
subscriptions_selector_list.apply { feedGroupCreateBinding.subscriptionsSelectorList.apply {
// Disable animations, too distracting. // Disable animations, too distracting.
itemAnimator = null itemAnimator = null
adapter = subscriptionGroupAdapter adapter = subscriptionGroupAdapter
@ -172,8 +180,11 @@ class FeedGroupDialog : DialogFragment(), BackPressable {
override fun onDestroyView() { override fun onDestroyView() {
super.onDestroyView() super.onDestroyView()
subscriptions_selector_list?.adapter = null feedGroupCreateBinding.subscriptionsSelectorList.adapter = null
icon_selector?.adapter = null feedGroupCreateBinding.iconSelector.adapter = null
_feedGroupCreateBinding = null
_searchLayoutBinding = null
} }
/*/////////////////////////////////////////////////////////////////////////// /*///////////////////////////////////////////////////////////////////////////
@ -193,30 +204,30 @@ class FeedGroupDialog : DialogFragment(), BackPressable {
} }
private fun setupListeners() { private fun setupListeners() {
delete_button.setOnClickListener { showScreen(DeleteScreen) } feedGroupCreateBinding.deleteButton.setOnClickListener { showScreen(DeleteScreen) }
cancel_button.setOnClickListener { feedGroupCreateBinding.cancelButton.setOnClickListener {
when (currentScreen) { when (currentScreen) {
InitialScreen -> dismiss() InitialScreen -> dismiss()
else -> showScreen(InitialScreen) else -> showScreen(InitialScreen)
} }
} }
group_name_input_container.error = null feedGroupCreateBinding.groupNameInputContainer.error = null
group_name_input.doOnTextChanged { text, _, _, _ -> feedGroupCreateBinding.groupNameInput.doOnTextChanged { text, _, _, _ ->
if (group_name_input_container.isErrorEnabled && !text.isNullOrBlank()) { if (feedGroupCreateBinding.groupNameInputContainer.isErrorEnabled && !text.isNullOrBlank()) {
group_name_input_container.error = null feedGroupCreateBinding.groupNameInputContainer.error = null
} }
} }
confirm_button.setOnClickListener { handlePositiveButton() } feedGroupCreateBinding.confirmButton.setOnClickListener { handlePositiveButton() }
select_channel_button.setOnClickListener { feedGroupCreateBinding.selectChannelButton.setOnClickListener {
subscriptions_selector_list.scrollToPosition(0) feedGroupCreateBinding.subscriptionsSelectorList.scrollToPosition(0)
showScreen(SubscriptionsPickerScreen) showScreen(SubscriptionsPickerScreen)
} }
val headerMenu = subscriptions_header_toolbar.menu val headerMenu = feedGroupCreateBinding.subscriptionsHeaderToolbar.menu
requireActivity().menuInflater.inflate(R.menu.menu_feed_group_dialog, headerMenu) requireActivity().menuInflater.inflate(R.menu.menu_feed_group_dialog, headerMenu)
headerMenu.findItem(R.id.action_search).setOnMenuItemClickListener { headerMenu.findItem(R.id.action_search).setOnMenuItemClickListener {
@ -234,8 +245,8 @@ class FeedGroupDialog : DialogFragment(), BackPressable {
} }
} }
toolbar_search_clear.setOnClickListener { searchLayoutBinding.toolbarSearchClear.setOnClickListener {
if (toolbar_search_edit_text.text.isEmpty()) { if (searchLayoutBinding.toolbarSearchEditText.text.isNullOrEmpty()) {
hideSearch() hideSearch()
return@setOnClickListener return@setOnClickListener
} }
@ -243,14 +254,14 @@ class FeedGroupDialog : DialogFragment(), BackPressable {
showKeyboardSearch() showKeyboardSearch()
} }
toolbar_search_edit_text.setOnClickListener { searchLayoutBinding.toolbarSearchEditText.setOnClickListener {
if (DeviceUtils.isTv(context)) { if (DeviceUtils.isTv(context)) {
showKeyboardSearch() showKeyboardSearch()
} }
} }
toolbar_search_edit_text.doOnTextChanged { _, _, _, _ -> searchLayoutBinding.toolbarSearchEditText.doOnTextChanged { _, _, _, _ ->
val newQuery: String = toolbar_search_edit_text.text.toString() val newQuery: String = searchLayoutBinding.toolbarSearchEditText.text.toString()
subscriptionsCurrentSearchQuery = newQuery subscriptionsCurrentSearchQuery = newQuery
viewModel.filterSubscriptionsBy(newQuery) viewModel.filterSubscriptionsBy(newQuery)
} }
@ -266,16 +277,16 @@ class FeedGroupDialog : DialogFragment(), BackPressable {
} }
private fun handlePositiveButtonInitialScreen() { private fun handlePositiveButtonInitialScreen() {
val name = group_name_input.text.toString().trim() val name = feedGroupCreateBinding.groupNameInput.text.toString().trim()
val icon = selectedIcon ?: groupIcon ?: FeedGroupIcon.ALL val icon = selectedIcon ?: groupIcon ?: FeedGroupIcon.ALL
if (name.isBlank()) { if (name.isBlank()) {
group_name_input_container.error = getString(R.string.feed_group_dialog_empty_name) feedGroupCreateBinding.groupNameInputContainer.error = getString(R.string.feed_group_dialog_empty_name)
group_name_input.text = null feedGroupCreateBinding.groupNameInput.text = null
group_name_input.requestFocus() feedGroupCreateBinding.groupNameInput.requestFocus()
return return
} else { } else {
group_name_input_container.error = null feedGroupCreateBinding.groupNameInputContainer.error = null
} }
if (selectedSubscriptions.isEmpty()) { if (selectedSubscriptions.isEmpty()) {
@ -296,10 +307,10 @@ class FeedGroupDialog : DialogFragment(), BackPressable {
groupSortOrder = feedGroupEntity?.sortOrder ?: -1 groupSortOrder = feedGroupEntity?.sortOrder ?: -1
val feedGroupIcon = if (selectedIcon == null) icon else selectedIcon!! val feedGroupIcon = if (selectedIcon == null) icon else selectedIcon!!
icon_preview.setImageResource(feedGroupIcon.getDrawableRes(requireContext())) feedGroupCreateBinding.iconPreview.setImageResource(feedGroupIcon.getDrawableRes(requireContext()))
if (group_name_input.text.isNullOrBlank()) { if (feedGroupCreateBinding.groupNameInput.text.isNullOrBlank()) {
group_name_input.setText(name) feedGroupCreateBinding.groupNameInput.setText(name)
} }
} }
@ -346,10 +357,10 @@ class FeedGroupDialog : DialogFragment(), BackPressable {
subscriptionMainSection.update(subscriptions, false) subscriptionMainSection.update(subscriptions, false)
if (subscriptionsListState != null) { if (subscriptionsListState != null) {
subscriptions_selector_list.layoutManager?.onRestoreInstanceState(subscriptionsListState) feedGroupCreateBinding.subscriptionsSelectorList.layoutManager?.onRestoreInstanceState(subscriptionsListState)
subscriptionsListState = null subscriptionsListState = null
} else { } else {
subscriptions_selector_list.scrollToPosition(0) feedGroupCreateBinding.subscriptionsSelectorList.scrollToPosition(0)
} }
} }
@ -359,15 +370,15 @@ class FeedGroupDialog : DialogFragment(), BackPressable {
R.plurals.feed_group_dialog_selection_count, R.plurals.feed_group_dialog_selection_count,
selectedCount, selectedCount selectedCount, selectedCount
) )
selected_subscription_count_view.text = selectedCountText feedGroupCreateBinding.selectedSubscriptionCountView.text = selectedCountText
subscriptions_header_info.text = selectedCountText feedGroupCreateBinding.subscriptionsHeaderInfo.text = selectedCountText
} }
private fun setupIconPicker() { private fun setupIconPicker() {
val groupAdapter = GroupAdapter<GroupieViewHolder>() val groupAdapter = GroupAdapter<GroupieViewHolder>()
groupAdapter.addAll(FeedGroupIcon.values().map { PickerIconItem(requireContext(), it) }) groupAdapter.addAll(FeedGroupIcon.values().map { PickerIconItem(requireContext(), it) })
icon_selector.apply { feedGroupCreateBinding.iconSelector.apply {
layoutManager = GridLayoutManager(requireContext(), 7, RecyclerView.VERTICAL, false) layoutManager = GridLayoutManager(requireContext(), 7, RecyclerView.VERTICAL, false)
adapter = groupAdapter adapter = groupAdapter
@ -381,20 +392,20 @@ class FeedGroupDialog : DialogFragment(), BackPressable {
when (item) { when (item) {
is PickerIconItem -> { is PickerIconItem -> {
selectedIcon = item.icon selectedIcon = item.icon
icon_preview.setImageResource(item.iconRes) feedGroupCreateBinding.iconPreview.setImageResource(item.iconRes)
showScreen(InitialScreen) showScreen(InitialScreen)
} }
} }
} }
icon_preview.setOnClickListener { feedGroupCreateBinding.iconPreview.setOnClickListener {
icon_selector.scrollToPosition(0) feedGroupCreateBinding.iconSelector.scrollToPosition(0)
showScreen(IconPickerScreen) showScreen(IconPickerScreen)
} }
if (groupId == NO_GROUP_SELECTED) { if (groupId == NO_GROUP_SELECTED) {
val icon = selectedIcon ?: FeedGroupIcon.ALL val icon = selectedIcon ?: FeedGroupIcon.ALL
icon_preview.setImageResource(icon.getDrawableRes(requireContext())) feedGroupCreateBinding.iconPreview.setImageResource(icon.getDrawableRes(requireContext()))
} }
} }
@ -405,22 +416,22 @@ class FeedGroupDialog : DialogFragment(), BackPressable {
private fun showScreen(screen: ScreenState) { private fun showScreen(screen: ScreenState) {
currentScreen = screen currentScreen = screen
options_root.onlyVisibleIn(InitialScreen) feedGroupCreateBinding.optionsRoot.onlyVisibleIn(InitialScreen)
icon_selector.onlyVisibleIn(IconPickerScreen) feedGroupCreateBinding.iconSelector.onlyVisibleIn(IconPickerScreen)
subscriptions_selector.onlyVisibleIn(SubscriptionsPickerScreen) feedGroupCreateBinding.subscriptionsSelector.onlyVisibleIn(SubscriptionsPickerScreen)
delete_screen_message.onlyVisibleIn(DeleteScreen) feedGroupCreateBinding.deleteScreenMessage.onlyVisibleIn(DeleteScreen)
separator.onlyVisibleIn(SubscriptionsPickerScreen, IconPickerScreen) feedGroupCreateBinding.separator.onlyVisibleIn(SubscriptionsPickerScreen, IconPickerScreen)
cancel_button.onlyVisibleIn(InitialScreen, DeleteScreen) feedGroupCreateBinding.cancelButton.onlyVisibleIn(InitialScreen, DeleteScreen)
confirm_button.setText( feedGroupCreateBinding.confirmButton.setText(
when { when {
currentScreen == InitialScreen && groupId == NO_GROUP_SELECTED -> R.string.create currentScreen == InitialScreen && groupId == NO_GROUP_SELECTED -> R.string.create
else -> android.R.string.ok else -> android.R.string.ok
} }
) )
delete_button.isGone = currentScreen != InitialScreen || groupId == NO_GROUP_SELECTED feedGroupCreateBinding.deleteButton.isGone = currentScreen != InitialScreen || groupId == NO_GROUP_SELECTED
hideKeyboard() hideKeyboard()
hideSearch() hideSearch()
@ -434,26 +445,26 @@ class FeedGroupDialog : DialogFragment(), BackPressable {
// Utils // Utils
////////////////////////////////////////////////////////////////////////// */ ////////////////////////////////////////////////////////////////////////// */
private fun isSearchVisible() = subscriptions_header_search_container?.visibility == View.VISIBLE private fun isSearchVisible() = _searchLayoutBinding?.root?.visibility == View.VISIBLE
private fun resetSearch() { private fun resetSearch() {
toolbar_search_edit_text.setText("") searchLayoutBinding.toolbarSearchEditText.setText("")
subscriptionsCurrentSearchQuery = "" subscriptionsCurrentSearchQuery = ""
viewModel.clearSubscriptionsFilter() viewModel.clearSubscriptionsFilter()
} }
private fun hideSearch() { private fun hideSearch() {
resetSearch() resetSearch()
subscriptions_header_search_container.visibility = View.GONE searchLayoutBinding.root.visibility = View.GONE
subscriptions_header_info_container.visibility = View.VISIBLE feedGroupCreateBinding.subscriptionsHeaderInfoContainer.visibility = View.VISIBLE
subscriptions_header_toolbar.menu.findItem(R.id.action_search).isVisible = true feedGroupCreateBinding.subscriptionsHeaderToolbar.menu.findItem(R.id.action_search).isVisible = true
hideKeyboardSearch() hideKeyboardSearch()
} }
private fun showSearch() { private fun showSearch() {
subscriptions_header_search_container.visibility = View.VISIBLE searchLayoutBinding.root.visibility = View.VISIBLE
subscriptions_header_info_container.visibility = View.GONE feedGroupCreateBinding.subscriptionsHeaderInfoContainer.visibility = View.GONE
subscriptions_header_toolbar.menu.findItem(R.id.action_search).isVisible = false feedGroupCreateBinding.subscriptionsHeaderToolbar.menu.findItem(R.id.action_search).isVisible = false
showKeyboardSearch() showKeyboardSearch()
} }
@ -462,37 +473,43 @@ class FeedGroupDialog : DialogFragment(), BackPressable {
} }
private fun showKeyboardSearch() { private fun showKeyboardSearch() {
if (toolbar_search_edit_text.requestFocus()) { if (searchLayoutBinding.toolbarSearchEditText.requestFocus()) {
inputMethodManager.showSoftInput(toolbar_search_edit_text, InputMethodManager.SHOW_IMPLICIT) inputMethodManager.showSoftInput(
searchLayoutBinding.toolbarSearchEditText,
InputMethodManager.SHOW_IMPLICIT
)
} }
} }
private fun hideKeyboardSearch() { private fun hideKeyboardSearch() {
inputMethodManager.hideSoftInputFromWindow( inputMethodManager.hideSoftInputFromWindow(
toolbar_search_edit_text.windowToken, searchLayoutBinding.toolbarSearchEditText.windowToken,
InputMethodManager.RESULT_UNCHANGED_SHOWN InputMethodManager.RESULT_UNCHANGED_SHOWN
) )
toolbar_search_edit_text.clearFocus() searchLayoutBinding.toolbarSearchEditText.clearFocus()
} }
private fun showKeyboard() { private fun showKeyboard() {
if (group_name_input.requestFocus()) { if (feedGroupCreateBinding.groupNameInput.requestFocus()) {
inputMethodManager.showSoftInput(group_name_input, InputMethodManager.SHOW_IMPLICIT) inputMethodManager.showSoftInput(
feedGroupCreateBinding.groupNameInput,
InputMethodManager.SHOW_IMPLICIT
)
} }
} }
private fun hideKeyboard() { private fun hideKeyboard() {
inputMethodManager.hideSoftInputFromWindow( inputMethodManager.hideSoftInputFromWindow(
group_name_input.windowToken, feedGroupCreateBinding.groupNameInput.windowToken,
InputMethodManager.RESULT_UNCHANGED_SHOWN InputMethodManager.RESULT_UNCHANGED_SHOWN
) )
group_name_input.clearFocus() feedGroupCreateBinding.groupNameInput.clearFocus()
} }
private fun disableInput() { private fun disableInput() {
delete_button?.isEnabled = false _feedGroupCreateBinding?.deleteButton?.isEnabled = false
confirm_button?.isEnabled = false _feedGroupCreateBinding?.confirmButton?.isEnabled = false
cancel_button?.isEnabled = false _feedGroupCreateBinding?.cancelButton?.isEnabled = false
isCancelable = false isCancelable = false
hideKeyboard() hideKeyboard()

View File

@ -16,10 +16,9 @@ import com.xwray.groupie.TouchCallback
import com.xwray.groupie.kotlinandroidextensions.GroupieViewHolder import com.xwray.groupie.kotlinandroidextensions.GroupieViewHolder
import icepick.Icepick import icepick.Icepick
import icepick.State import icepick.State
import kotlinx.android.synthetic.main.dialog_feed_group_reorder.confirm_button
import kotlinx.android.synthetic.main.dialog_feed_group_reorder.feed_groups_list
import org.schabi.newpipe.R import org.schabi.newpipe.R
import org.schabi.newpipe.database.feed.model.FeedGroupEntity import org.schabi.newpipe.database.feed.model.FeedGroupEntity
import org.schabi.newpipe.databinding.DialogFeedGroupReorderBinding
import org.schabi.newpipe.local.subscription.dialog.FeedGroupReorderDialogViewModel.DialogEvent.ProcessingEvent import org.schabi.newpipe.local.subscription.dialog.FeedGroupReorderDialogViewModel.DialogEvent.ProcessingEvent
import org.schabi.newpipe.local.subscription.dialog.FeedGroupReorderDialogViewModel.DialogEvent.SuccessEvent import org.schabi.newpipe.local.subscription.dialog.FeedGroupReorderDialogViewModel.DialogEvent.SuccessEvent
import org.schabi.newpipe.local.subscription.item.FeedGroupReorderItem import org.schabi.newpipe.local.subscription.item.FeedGroupReorderItem
@ -27,6 +26,9 @@ import org.schabi.newpipe.util.ThemeHelper
import java.util.Collections import java.util.Collections
class FeedGroupReorderDialog : DialogFragment() { class FeedGroupReorderDialog : DialogFragment() {
private var _binding: DialogFeedGroupReorderBinding? = null
private val binding get() = _binding!!
private lateinit var viewModel: FeedGroupReorderDialogViewModel private lateinit var viewModel: FeedGroupReorderDialogViewModel
@State @State
@ -48,6 +50,7 @@ class FeedGroupReorderDialog : DialogFragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
_binding = DialogFeedGroupReorderBinding.bind(view)
viewModel = ViewModelProvider(this).get(FeedGroupReorderDialogViewModel::class.java) viewModel = ViewModelProvider(this).get(FeedGroupReorderDialogViewModel::class.java)
viewModel.groupsLiveData.observe(viewLifecycleOwner, Observer(::handleGroups)) viewModel.groupsLiveData.observe(viewLifecycleOwner, Observer(::handleGroups))
@ -61,15 +64,20 @@ class FeedGroupReorderDialog : DialogFragment() {
} }
) )
feed_groups_list.layoutManager = LinearLayoutManager(requireContext()) binding.feedGroupsList.layoutManager = LinearLayoutManager(requireContext())
feed_groups_list.adapter = groupAdapter binding.feedGroupsList.adapter = groupAdapter
itemTouchHelper.attachToRecyclerView(feed_groups_list) itemTouchHelper.attachToRecyclerView(binding.feedGroupsList)
confirm_button.setOnClickListener { binding.confirmButton.setOnClickListener {
viewModel.updateOrder(groupOrderedIdList) viewModel.updateOrder(groupOrderedIdList)
} }
} }
override fun onDestroyView() {
_binding = null
super.onDestroyView()
}
override fun onSaveInstanceState(outState: Bundle) { override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState) super.onSaveInstanceState(outState)
Icepick.saveInstanceState(this, outState) Icepick.saveInstanceState(this, outState)
@ -89,7 +97,7 @@ class FeedGroupReorderDialog : DialogFragment() {
} }
private fun disableInput() { private fun disableInput() {
confirm_button?.isEnabled = false _binding?.confirmButton?.isEnabled = false
isCancelable = false isCancelable = false
} }

View File

@ -31,15 +31,16 @@ import androidx.annotation.Nullable;
import androidx.annotation.StringRes; import androidx.annotation.StringRes;
import androidx.core.app.NotificationCompat; import androidx.core.app.NotificationCompat;
import androidx.core.app.NotificationManagerCompat; import androidx.core.app.NotificationManagerCompat;
import androidx.core.app.ServiceCompat;
import org.reactivestreams.Publisher; import org.reactivestreams.Publisher;
import org.schabi.newpipe.R; import org.schabi.newpipe.R;
import org.schabi.newpipe.extractor.subscription.SubscriptionExtractor; import org.schabi.newpipe.extractor.subscription.SubscriptionExtractor;
import org.schabi.newpipe.ktx.ExceptionUtils;
import org.schabi.newpipe.local.subscription.SubscriptionManager; import org.schabi.newpipe.local.subscription.SubscriptionManager;
import org.schabi.newpipe.report.ErrorActivity; import org.schabi.newpipe.report.ErrorActivity;
import org.schabi.newpipe.report.ErrorInfo; import org.schabi.newpipe.report.ErrorInfo;
import org.schabi.newpipe.report.UserAction; import org.schabi.newpipe.report.UserAction;
import org.schabi.newpipe.util.ExceptionUtils;
import java.io.FileNotFoundException; import java.io.FileNotFoundException;
import java.util.Collections; import java.util.Collections;
@ -162,7 +163,7 @@ public abstract class BaseImportExportService extends Service {
protected void postErrorResult(final String title, final String text) { protected void postErrorResult(final String title, final String text) {
disposeAll(); disposeAll();
stopForeground(true); ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE);
stopSelf(); stopSelf();
if (title == null) { if (title == null) {

View File

@ -9,7 +9,7 @@ public interface ImportExportEventListener {
void onSizeReceived(int size); void onSizeReceived(int size);
/** /**
* Called everytime an item has been parsed/resolved. * Called every time an item has been parsed/resolved.
* *
* @param itemName the name of the subscription item * @param itemName the name of the subscription item
*/ */

View File

@ -27,6 +27,7 @@ import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import org.reactivestreams.Subscriber; import org.reactivestreams.Subscriber;
import org.reactivestreams.Subscription; import org.reactivestreams.Subscription;
import org.schabi.newpipe.App;
import org.schabi.newpipe.R; import org.schabi.newpipe.R;
import org.schabi.newpipe.database.subscription.SubscriptionEntity; import org.schabi.newpipe.database.subscription.SubscriptionEntity;
import org.schabi.newpipe.extractor.subscription.SubscriptionItem; import org.schabi.newpipe.extractor.subscription.SubscriptionItem;
@ -50,7 +51,7 @@ public class SubscriptionsExportService extends BaseImportExportService {
* A {@link LocalBroadcastManager local broadcast} will be made with this action * A {@link LocalBroadcastManager local broadcast} will be made with this action
* when the export is successfully completed. * when the export is successfully completed.
*/ */
public static final String EXPORT_COMPLETE_ACTION = "org.schabi.newpipe.local.subscription" public static final String EXPORT_COMPLETE_ACTION = App.PACKAGE_NAME + ".local.subscription"
+ ".services.SubscriptionsExportService.EXPORT_COMPLETE"; + ".services.SubscriptionsExportService.EXPORT_COMPLETE";
private Subscription subscription; private Subscription subscription;

View File

@ -29,13 +29,14 @@ import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import org.reactivestreams.Subscriber; import org.reactivestreams.Subscriber;
import org.reactivestreams.Subscription; import org.reactivestreams.Subscription;
import org.schabi.newpipe.App;
import org.schabi.newpipe.R; import org.schabi.newpipe.R;
import org.schabi.newpipe.database.subscription.SubscriptionEntity; import org.schabi.newpipe.database.subscription.SubscriptionEntity;
import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.channel.ChannelInfo; import org.schabi.newpipe.extractor.channel.ChannelInfo;
import org.schabi.newpipe.extractor.subscription.SubscriptionItem; import org.schabi.newpipe.extractor.subscription.SubscriptionItem;
import org.schabi.newpipe.ktx.ExceptionUtils;
import org.schabi.newpipe.util.Constants; import org.schabi.newpipe.util.Constants;
import org.schabi.newpipe.util.ExceptionUtils;
import org.schabi.newpipe.util.ExtractorHelper; import org.schabi.newpipe.util.ExtractorHelper;
import java.io.File; import java.io.File;
@ -66,7 +67,7 @@ public class SubscriptionsImportService extends BaseImportExportService {
* A {@link LocalBroadcastManager local broadcast} will be made with this action * A {@link LocalBroadcastManager local broadcast} will be made with this action
* when the import is successfully completed. * when the import is successfully completed.
*/ */
public static final String IMPORT_COMPLETE_ACTION = "org.schabi.newpipe.local.subscription" public static final String IMPORT_COMPLETE_ACTION = App.PACKAGE_NAME + ".local.subscription"
+ ".services.SubscriptionsImportService.IMPORT_COMPLETE"; + ".services.SubscriptionsImportService.IMPORT_COMPLETE";
/** /**

View File

@ -1,57 +0,0 @@
package org.schabi.newpipe.player;
import android.content.Intent;
import android.view.Menu;
import org.schabi.newpipe.R;
public final class BackgroundPlayerActivity extends ServicePlayerActivity {
private static final String TAG = "BackgroundPlayerActivity";
@Override
public String getTag() {
return TAG;
}
@Override
public String getSupportActionTitle() {
return getResources().getString(R.string.title_activity_play_queue);
}
@Override
public Intent getBindIntent() {
return new Intent(this, MainPlayer.class);
}
@Override
public void startPlayerListener() {
if (player instanceof VideoPlayerImpl) {
((VideoPlayerImpl) player).setActivityListener(this);
}
}
@Override
public void stopPlayerListener() {
if (player instanceof VideoPlayerImpl) {
((VideoPlayerImpl) player).removeActivityListener(this);
}
}
@Override
public int getPlayerOptionMenuResource() {
return R.menu.menu_play_queue_bg;
}
@Override
public void setupMenu(final Menu menu) {
if (player == null) {
return;
}
menu.findItem(R.id.action_switch_popup)
.setVisible(!((VideoPlayerImpl) player).popupPlayerSelected());
menu.findItem(R.id.action_switch_background)
.setVisible(!((VideoPlayerImpl) player).audioPlayerSelected());
}
}

File diff suppressed because it is too large Load Diff

View File

@ -26,6 +26,7 @@ import android.os.Binder;
import android.os.IBinder; import android.os.IBinder;
import android.util.DisplayMetrics; import android.util.DisplayMetrics;
import android.util.Log; import android.util.Log;
import android.view.LayoutInflater;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.view.WindowManager; import android.view.WindowManager;
@ -33,7 +34,8 @@ import android.view.WindowManager;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.core.content.ContextCompat; import androidx.core.content.ContextCompat;
import org.schabi.newpipe.R; import org.schabi.newpipe.databinding.PlayerBinding;
import org.schabi.newpipe.App;
import org.schabi.newpipe.util.ThemeHelper; import org.schabi.newpipe.util.ThemeHelper;
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage; import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
@ -46,9 +48,9 @@ import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
*/ */
public final class MainPlayer extends Service { public final class MainPlayer extends Service {
private static final String TAG = "MainPlayer"; private static final String TAG = "MainPlayer";
private static final boolean DEBUG = BasePlayer.DEBUG; private static final boolean DEBUG = Player.DEBUG;
private VideoPlayerImpl playerImpl; private Player player;
private WindowManager windowManager; private WindowManager windowManager;
private final IBinder mBinder = new MainPlayer.LocalBinder(); private final IBinder mBinder = new MainPlayer.LocalBinder();
@ -64,25 +66,23 @@ public final class MainPlayer extends Service {
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/
static final String ACTION_CLOSE static final String ACTION_CLOSE
= "org.schabi.newpipe.player.MainPlayer.CLOSE"; = App.PACKAGE_NAME + ".player.MainPlayer.CLOSE";
static final String ACTION_PLAY_PAUSE static final String ACTION_PLAY_PAUSE
= "org.schabi.newpipe.player.MainPlayer.PLAY_PAUSE"; = App.PACKAGE_NAME + ".player.MainPlayer.PLAY_PAUSE";
static final String ACTION_OPEN_CONTROLS
= "org.schabi.newpipe.player.MainPlayer.OPEN_CONTROLS";
static final String ACTION_REPEAT static final String ACTION_REPEAT
= "org.schabi.newpipe.player.MainPlayer.REPEAT"; = App.PACKAGE_NAME + ".player.MainPlayer.REPEAT";
static final String ACTION_PLAY_NEXT static final String ACTION_PLAY_NEXT
= "org.schabi.newpipe.player.MainPlayer.ACTION_PLAY_NEXT"; = App.PACKAGE_NAME + ".player.MainPlayer.ACTION_PLAY_NEXT";
static final String ACTION_PLAY_PREVIOUS static final String ACTION_PLAY_PREVIOUS
= "org.schabi.newpipe.player.MainPlayer.ACTION_PLAY_PREVIOUS"; = App.PACKAGE_NAME + ".player.MainPlayer.ACTION_PLAY_PREVIOUS";
static final String ACTION_FAST_REWIND static final String ACTION_FAST_REWIND
= "org.schabi.newpipe.player.MainPlayer.ACTION_FAST_REWIND"; = App.PACKAGE_NAME + ".player.MainPlayer.ACTION_FAST_REWIND";
static final String ACTION_FAST_FORWARD static final String ACTION_FAST_FORWARD
= "org.schabi.newpipe.player.MainPlayer.ACTION_FAST_FORWARD"; = App.PACKAGE_NAME + ".player.MainPlayer.ACTION_FAST_FORWARD";
static final String ACTION_SHUFFLE static final String ACTION_SHUFFLE
= "org.schabi.newpipe.player.MainPlayer.ACTION_SHUFFLE"; = App.PACKAGE_NAME + ".player.MainPlayer.ACTION_SHUFFLE";
public static final String ACTION_RECREATE_NOTIFICATION public static final String ACTION_RECREATE_NOTIFICATION
= "org.schabi.newpipe.player.MainPlayer.ACTION_RECREATE_NOTIFICATION"; = App.PACKAGE_NAME + ".player.MainPlayer.ACTION_RECREATE_NOTIFICATION";
/*////////////////////////////////////////////////////////////////////////// /*//////////////////////////////////////////////////////////////////////////
// Service's LifeCycle // Service's LifeCycle
@ -101,13 +101,12 @@ public final class MainPlayer extends Service {
} }
private void createView() { private void createView() {
final View layout = View.inflate(this, R.layout.player, null); final PlayerBinding binding = PlayerBinding.inflate(LayoutInflater.from(this));
playerImpl = new VideoPlayerImpl(this); player = new Player(this);
playerImpl.setup(layout); player.setupFromView(binding);
playerImpl.shouldUpdateOnProgress = true;
NotificationUtil.getInstance().createNotificationAndStartForeground(playerImpl, this); NotificationUtil.getInstance().createNotificationAndStartForeground(player, this);
} }
@Override @Override
@ -117,19 +116,19 @@ public final class MainPlayer extends Service {
+ "], flags = [" + flags + "], startId = [" + startId + "]"); + "], flags = [" + flags + "], startId = [" + startId + "]");
} }
if (Intent.ACTION_MEDIA_BUTTON.equals(intent.getAction()) if (Intent.ACTION_MEDIA_BUTTON.equals(intent.getAction())
&& playerImpl.playQueue == null) { && player.getPlayQueue() == null) {
// Player is not working, no need to process media button's action // Player is not working, no need to process media button's action
return START_NOT_STICKY; return START_NOT_STICKY;
} }
if (Intent.ACTION_MEDIA_BUTTON.equals(intent.getAction()) if (Intent.ACTION_MEDIA_BUTTON.equals(intent.getAction())
|| intent.getStringExtra(VideoPlayer.PLAY_QUEUE_KEY) != null) { || intent.getStringExtra(Player.PLAY_QUEUE_KEY) != null) {
NotificationUtil.getInstance().createNotificationAndStartForeground(playerImpl, this); NotificationUtil.getInstance().createNotificationAndStartForeground(player, this);
} }
playerImpl.handleIntent(intent); player.handleIntent(intent);
if (playerImpl.mediaSessionManager != null) { if (player.getMediaSessionManager() != null) {
playerImpl.mediaSessionManager.handleMediaButtonIntent(intent); player.getMediaSessionManager().handleMediaButtonIntent(intent);
} }
return START_NOT_STICKY; return START_NOT_STICKY;
} }
@ -139,20 +138,20 @@ public final class MainPlayer extends Service {
Log.d(TAG, "stop() called"); Log.d(TAG, "stop() called");
} }
if (playerImpl.getPlayer() != null) { if (!player.exoPlayerIsNull()) {
playerImpl.wasPlaying = playerImpl.getPlayer().getPlayWhenReady(); player.saveWasPlaying();
// Releases wifi & cpu, disables keepScreenOn, etc. // Releases wifi & cpu, disables keepScreenOn, etc.
if (!autoplayEnabled) { if (!autoplayEnabled) {
playerImpl.onPause(); player.pause();
} }
// We can't just pause the player here because it will make transition // We can't just pause the player here because it will make transition
// from one stream to a new stream not smooth // from one stream to a new stream not smooth
playerImpl.getPlayer().stop(false); player.smoothStopPlayer();
playerImpl.setRecovery(); player.setRecovery();
// Android TV will handle back button in case controls will be visible // Android TV will handle back button in case controls will be visible
// (one more additional unneeded click while the player is hidden) // (one more additional unneeded click while the player is hidden)
playerImpl.hideControls(0, 0); player.hideControls(0, 0);
playerImpl.onQueueClosed(); player.closeItemsList();
// Notification shows information about old stream but if a user selects // Notification shows information about old stream but if a user selects
// a stream from backStack it's not actual anymore // a stream from backStack it's not actual anymore
// So we should hide the notification at all. // So we should hide the notification at all.
@ -166,7 +165,7 @@ public final class MainPlayer extends Service {
@Override @Override
public void onTaskRemoved(final Intent rootIntent) { public void onTaskRemoved(final Intent rootIntent) {
super.onTaskRemoved(rootIntent); super.onTaskRemoved(rootIntent);
if (!playerImpl.videoPlayerSelected()) { if (!player.videoPlayerSelected()) {
return; return;
} }
onDestroy(); onDestroy();
@ -179,7 +178,23 @@ public final class MainPlayer extends Service {
if (DEBUG) { if (DEBUG) {
Log.d(TAG, "destroy() called"); Log.d(TAG, "destroy() called");
} }
onClose();
if (player != null) {
// Exit from fullscreen when user closes the player via notification
if (player.isFullscreen()) {
player.toggleFullscreen();
}
removeViewFromParent();
player.saveStreamProgressState();
player.setRecovery();
player.stopActivityBinding();
player.removePopupFromView();
player.destroy();
}
NotificationUtil.getInstance().cancelNotificationAndStopForeground(this);
stopSelf();
} }
@Override @Override
@ -192,32 +207,6 @@ public final class MainPlayer extends Service {
return mBinder; return mBinder;
} }
/*//////////////////////////////////////////////////////////////////////////
// Actions
//////////////////////////////////////////////////////////////////////////*/
private void onClose() {
if (DEBUG) {
Log.d(TAG, "onClose() called");
}
if (playerImpl != null) {
// Exit from fullscreen when user closes the player via notification
if (playerImpl.isFullscreen()) {
playerImpl.toggleFullscreen();
}
removeViewFromParent();
playerImpl.setRecovery();
playerImpl.savePlaybackState();
playerImpl.stopActivityBinding();
playerImpl.removePopupFromView();
playerImpl.destroy();
}
NotificationUtil.getInstance().cancelNotificationAndStopForeground(this);
stopSelf();
}
/*////////////////////////////////////////////////////////////////////////// /*//////////////////////////////////////////////////////////////////////////
// Utils // Utils
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/
@ -225,25 +214,25 @@ public final class MainPlayer extends Service {
boolean isLandscape() { boolean isLandscape() {
// DisplayMetrics from activity context knows about MultiWindow feature // DisplayMetrics from activity context knows about MultiWindow feature
// while DisplayMetrics from app context doesn't // while DisplayMetrics from app context doesn't
final DisplayMetrics metrics = (playerImpl != null final DisplayMetrics metrics = (player != null
&& playerImpl.getParentActivity() != null && player.getParentActivity() != null
? playerImpl.getParentActivity().getResources() ? player.getParentActivity().getResources()
: getResources()).getDisplayMetrics(); : getResources()).getDisplayMetrics();
return metrics.heightPixels < metrics.widthPixels; return metrics.heightPixels < metrics.widthPixels;
} }
@Nullable @Nullable
public View getView() { public View getView() {
if (playerImpl == null) { if (player == null) {
return null; return null;
} }
return playerImpl.getRootView(); return player.getRootView();
} }
public void removeViewFromParent() { public void removeViewFromParent() {
if (getView() != null && getView().getParent() != null) { if (getView() != null && getView().getParent() != null) {
if (playerImpl.getParentActivity() != null) { if (player.getParentActivity() != null) {
// This means view was added to fragment // This means view was added to fragment
final ViewGroup parent = (ViewGroup) getView().getParent(); final ViewGroup parent = (ViewGroup) getView().getParent();
parent.removeView(getView()); parent.removeView(getView());
@ -261,8 +250,8 @@ public final class MainPlayer extends Service {
return MainPlayer.this; return MainPlayer.this;
} }
public VideoPlayerImpl getPlayer() { public Player getPlayer() {
return MainPlayer.this.playerImpl; return MainPlayer.this.player;
} }
} }
} }

View File

@ -15,6 +15,7 @@ import androidx.annotation.Nullable;
import androidx.annotation.StringRes; import androidx.annotation.StringRes;
import androidx.core.app.NotificationCompat; import androidx.core.app.NotificationCompat;
import androidx.core.app.NotificationManagerCompat; import androidx.core.app.NotificationManagerCompat;
import androidx.core.app.ServiceCompat;
import androidx.core.content.ContextCompat; import androidx.core.content.ContextCompat;
import org.schabi.newpipe.MainActivity; import org.schabi.newpipe.MainActivity;
@ -42,7 +43,7 @@ import static org.schabi.newpipe.player.MainPlayer.ACTION_SHUFFLE;
*/ */
public final class NotificationUtil { public final class NotificationUtil {
private static final String TAG = NotificationUtil.class.getSimpleName(); private static final String TAG = NotificationUtil.class.getSimpleName();
private static final boolean DEBUG = BasePlayer.DEBUG; private static final boolean DEBUG = Player.DEBUG;
private static final int NOTIFICATION_ID = 123789; private static final int NOTIFICATION_ID = 123789;
@Nullable private static NotificationUtil instance = null; @Nullable private static NotificationUtil instance = null;
@ -75,7 +76,7 @@ public final class NotificationUtil {
* @param forceRecreate whether to force the recreation of the notification even if it already * @param forceRecreate whether to force the recreation of the notification even if it already
* exists * exists
*/ */
synchronized void createNotificationIfNeededAndUpdate(final VideoPlayerImpl player, synchronized void createNotificationIfNeededAndUpdate(final Player player,
final boolean forceRecreate) { final boolean forceRecreate) {
if (forceRecreate || notificationBuilder == null) { if (forceRecreate || notificationBuilder == null) {
notificationBuilder = createNotification(player); notificationBuilder = createNotification(player);
@ -84,14 +85,14 @@ public final class NotificationUtil {
notificationManager.notify(NOTIFICATION_ID, notificationBuilder.build()); notificationManager.notify(NOTIFICATION_ID, notificationBuilder.build());
} }
private synchronized NotificationCompat.Builder createNotification( private synchronized NotificationCompat.Builder createNotification(final Player player) {
final VideoPlayerImpl player) {
if (DEBUG) { if (DEBUG) {
Log.d(TAG, "createNotification()"); Log.d(TAG, "createNotification()");
} }
notificationManager = NotificationManagerCompat.from(player.context); notificationManager = NotificationManagerCompat.from(player.getContext());
final NotificationCompat.Builder builder = new NotificationCompat.Builder(player.context, final NotificationCompat.Builder builder =
player.context.getString(R.string.notification_channel_id)); new NotificationCompat.Builder(player.getContext(),
player.getContext().getString(R.string.notification_channel_id));
initializeNotificationSlots(player); initializeNotificationSlots(player);
@ -106,25 +107,25 @@ public final class NotificationUtil {
// build the compact slot indices array (need code to convert from Integer... because Java) // build the compact slot indices array (need code to convert from Integer... because Java)
final List<Integer> compactSlotList = NotificationConstants.getCompactSlotsFromPreferences( final List<Integer> compactSlotList = NotificationConstants.getCompactSlotsFromPreferences(
player.context, player.sharedPreferences, nonNothingSlotCount); player.getContext(), player.getPrefs(), nonNothingSlotCount);
final int[] compactSlots = new int[compactSlotList.size()]; final int[] compactSlots = new int[compactSlotList.size()];
for (int i = 0; i < compactSlotList.size(); i++) { for (int i = 0; i < compactSlotList.size(); i++) {
compactSlots[i] = compactSlotList.get(i); compactSlots[i] = compactSlotList.get(i);
} }
builder.setStyle(new androidx.media.app.NotificationCompat.MediaStyle() builder.setStyle(new androidx.media.app.NotificationCompat.MediaStyle()
.setMediaSession(player.mediaSessionManager.getSessionToken()) .setMediaSession(player.getMediaSessionManager().getSessionToken())
.setShowActionsInCompactView(compactSlots)) .setShowActionsInCompactView(compactSlots))
.setPriority(NotificationCompat.PRIORITY_HIGH) .setPriority(NotificationCompat.PRIORITY_HIGH)
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC) .setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
.setCategory(NotificationCompat.CATEGORY_TRANSPORT) .setCategory(NotificationCompat.CATEGORY_TRANSPORT)
.setShowWhen(false) .setShowWhen(false)
.setSmallIcon(R.drawable.ic_newpipe_triangle_white) .setSmallIcon(R.drawable.ic_newpipe_triangle_white)
.setColor(ContextCompat.getColor(player.context, R.color.dark_background_color)) .setColor(ContextCompat.getColor(player.getContext(),
.setColorized(player.sharedPreferences.getBoolean( R.color.dark_background_color))
player.context.getString(R.string.notification_colorize_key), .setColorized(player.getPrefs().getBoolean(
true)) player.getContext().getString(R.string.notification_colorize_key), true))
.setDeleteIntent(PendingIntent.getBroadcast(player.context, NOTIFICATION_ID, .setDeleteIntent(PendingIntent.getBroadcast(player.getContext(), NOTIFICATION_ID,
new Intent(ACTION_CLOSE), FLAG_UPDATE_CURRENT)); new Intent(ACTION_CLOSE), FLAG_UPDATE_CURRENT));
return builder; return builder;
@ -134,20 +135,20 @@ public final class NotificationUtil {
* Updates the notification builder and the button icons depending on the playback state. * Updates the notification builder and the button icons depending on the playback state.
* @param player the player currently open, to take data from * @param player the player currently open, to take data from
*/ */
private synchronized void updateNotification(final VideoPlayerImpl player) { private synchronized void updateNotification(final Player player) {
if (DEBUG) { if (DEBUG) {
Log.d(TAG, "updateNotification()"); Log.d(TAG, "updateNotification()");
} }
// also update content intent, in case the user switched players // also update content intent, in case the user switched players
notificationBuilder.setContentIntent(PendingIntent.getActivity(player.context, notificationBuilder.setContentIntent(PendingIntent.getActivity(player.getContext(),
NOTIFICATION_ID, getIntentForNotification(player), FLAG_UPDATE_CURRENT)); NOTIFICATION_ID, getIntentForNotification(player), FLAG_UPDATE_CURRENT));
notificationBuilder.setContentTitle(player.getVideoTitle()); notificationBuilder.setContentTitle(player.getVideoTitle());
notificationBuilder.setContentText(player.getUploaderName()); notificationBuilder.setContentText(player.getUploaderName());
notificationBuilder.setTicker(player.getVideoTitle()); notificationBuilder.setTicker(player.getVideoTitle());
updateActions(notificationBuilder, player); updateActions(notificationBuilder, player);
final boolean showThumbnail = player.sharedPreferences.getBoolean( final boolean showThumbnail = player.getPrefs().getBoolean(
player.context.getString(R.string.show_thumbnail_key), true); player.getContext().getString(R.string.show_thumbnail_key), true);
if (showThumbnail) { if (showThumbnail) {
setLargeIcon(notificationBuilder, player); setLargeIcon(notificationBuilder, player);
} }
@ -173,7 +174,7 @@ public final class NotificationUtil {
} }
void createNotificationAndStartForeground(final VideoPlayerImpl player, final Service service) { void createNotificationAndStartForeground(final Player player, final Service service) {
if (notificationBuilder == null) { if (notificationBuilder == null) {
notificationBuilder = createNotification(player); notificationBuilder = createNotification(player);
} }
@ -188,7 +189,7 @@ public final class NotificationUtil {
} }
void cancelNotificationAndStopForeground(final Service service) { void cancelNotificationAndStopForeground(final Service service) {
service.stopForeground(true); ServiceCompat.stopForeground(service, ServiceCompat.STOP_FOREGROUND_REMOVE);
if (notificationManager != null) { if (notificationManager != null) {
notificationManager.cancel(NOTIFICATION_ID); notificationManager.cancel(NOTIFICATION_ID);
@ -202,17 +203,16 @@ public final class NotificationUtil {
// ACTIONS // ACTIONS
///////////////////////////////////////////////////// /////////////////////////////////////////////////////
private void initializeNotificationSlots(final VideoPlayerImpl player) { private void initializeNotificationSlots(final Player player) {
for (int i = 0; i < 5; ++i) { for (int i = 0; i < 5; ++i) {
notificationSlots[i] = player.sharedPreferences.getInt( notificationSlots[i] = player.getPrefs().getInt(
player.context.getString(NotificationConstants.SLOT_PREF_KEYS[i]), player.getContext().getString(NotificationConstants.SLOT_PREF_KEYS[i]),
NotificationConstants.SLOT_DEFAULTS[i]); NotificationConstants.SLOT_DEFAULTS[i]);
} }
} }
@SuppressLint("RestrictedApi") @SuppressLint("RestrictedApi")
private void updateActions(final NotificationCompat.Builder builder, private void updateActions(final NotificationCompat.Builder builder, final Player player) {
final VideoPlayerImpl player) {
builder.mActions.clear(); builder.mActions.clear();
for (int i = 0; i < 5; ++i) { for (int i = 0; i < 5; ++i) {
addAction(builder, player, notificationSlots[i]); addAction(builder, player, notificationSlots[i]);
@ -220,7 +220,7 @@ public final class NotificationUtil {
} }
private void addAction(final NotificationCompat.Builder builder, private void addAction(final NotificationCompat.Builder builder,
final VideoPlayerImpl player, final Player player,
@NotificationConstants.Action final int slot) { @NotificationConstants.Action final int slot) {
final NotificationCompat.Action action = getAction(player, slot); final NotificationCompat.Action action = getAction(player, slot);
if (action != null) { if (action != null) {
@ -230,7 +230,7 @@ public final class NotificationUtil {
@Nullable @Nullable
private NotificationCompat.Action getAction( private NotificationCompat.Action getAction(
final VideoPlayerImpl player, final Player player,
@NotificationConstants.Action final int selectedAction) { @NotificationConstants.Action final int selectedAction) {
final int baseActionIcon = NotificationConstants.ACTION_ICONS[selectedAction]; final int baseActionIcon = NotificationConstants.ACTION_ICONS[selectedAction];
switch (selectedAction) { switch (selectedAction) {
@ -251,7 +251,7 @@ public final class NotificationUtil {
R.string.exo_controls_fastforward_description, ACTION_FAST_FORWARD); R.string.exo_controls_fastforward_description, ACTION_FAST_FORWARD);
case NotificationConstants.SMART_REWIND_PREVIOUS: case NotificationConstants.SMART_REWIND_PREVIOUS:
if (player.playQueue != null && player.playQueue.size() > 1) { if (player.getPlayQueue() != null && player.getPlayQueue().size() > 1) {
return getAction(player, R.drawable.exo_notification_previous, return getAction(player, R.drawable.exo_notification_previous,
R.string.exo_controls_previous_description, ACTION_PLAY_PREVIOUS); R.string.exo_controls_previous_description, ACTION_PLAY_PREVIOUS);
} else { } else {
@ -260,7 +260,7 @@ public final class NotificationUtil {
} }
case NotificationConstants.SMART_FORWARD_NEXT: case NotificationConstants.SMART_FORWARD_NEXT:
if (player.playQueue != null && player.playQueue.size() > 1) { if (player.getPlayQueue() != null && player.getPlayQueue().size() > 1) {
return getAction(player, R.drawable.exo_notification_next, return getAction(player, R.drawable.exo_notification_next,
R.string.exo_controls_next_description, ACTION_PLAY_NEXT); R.string.exo_controls_next_description, ACTION_PLAY_NEXT);
} else { } else {
@ -269,23 +269,23 @@ public final class NotificationUtil {
} }
case NotificationConstants.PLAY_PAUSE_BUFFERING: case NotificationConstants.PLAY_PAUSE_BUFFERING:
if (player.getCurrentState() == BasePlayer.STATE_PREFLIGHT if (player.getCurrentState() == Player.STATE_PREFLIGHT
|| player.getCurrentState() == BasePlayer.STATE_BLOCKED || player.getCurrentState() == Player.STATE_BLOCKED
|| player.getCurrentState() == BasePlayer.STATE_BUFFERING) { || player.getCurrentState() == Player.STATE_BUFFERING) {
// null intent -> show hourglass icon that does nothing when clicked // null intent -> show hourglass icon that does nothing when clicked
return new NotificationCompat.Action(R.drawable.ic_hourglass_top_white_24dp_png, return new NotificationCompat.Action(R.drawable.ic_hourglass_top_white_24dp_png,
player.context.getString(R.string.notification_action_buffering), player.getContext().getString(R.string.notification_action_buffering),
null); null);
} }
case NotificationConstants.PLAY_PAUSE: case NotificationConstants.PLAY_PAUSE:
if (player.getCurrentState() == BasePlayer.STATE_COMPLETED) { if (player.getCurrentState() == Player.STATE_COMPLETED) {
return getAction(player, R.drawable.ic_replay_white_24dp_png, return getAction(player, R.drawable.ic_replay_white_24dp_png,
R.string.exo_controls_pause_description, ACTION_PLAY_PAUSE); R.string.exo_controls_pause_description, ACTION_PLAY_PAUSE);
} else if (player.isPlaying() } else if (player.isPlaying()
|| player.getCurrentState() == BasePlayer.STATE_PREFLIGHT || player.getCurrentState() == Player.STATE_PREFLIGHT
|| player.getCurrentState() == BasePlayer.STATE_BLOCKED || player.getCurrentState() == Player.STATE_BLOCKED
|| player.getCurrentState() == BasePlayer.STATE_BUFFERING) { || player.getCurrentState() == Player.STATE_BUFFERING) {
return getAction(player, R.drawable.exo_notification_pause, return getAction(player, R.drawable.exo_notification_pause,
R.string.exo_controls_pause_description, ACTION_PLAY_PAUSE); R.string.exo_controls_pause_description, ACTION_PLAY_PAUSE);
} else { } else {
@ -306,7 +306,7 @@ public final class NotificationUtil {
} }
case NotificationConstants.SHUFFLE: case NotificationConstants.SHUFFLE:
if (player.playQueue != null && player.playQueue.isShuffled()) { if (player.getPlayQueue() != null && player.getPlayQueue().isShuffled()) {
return getAction(player, R.drawable.exo_controls_shuffle_on, return getAction(player, R.drawable.exo_controls_shuffle_on,
R.string.exo_controls_shuffle_on_description, ACTION_SHUFFLE); R.string.exo_controls_shuffle_on_description, ACTION_SHUFFLE);
} else { } else {
@ -325,23 +325,23 @@ public final class NotificationUtil {
} }
} }
private NotificationCompat.Action getAction(final VideoPlayerImpl player, private NotificationCompat.Action getAction(final Player player,
@DrawableRes final int drawable, @DrawableRes final int drawable,
@StringRes final int title, @StringRes final int title,
final String intentAction) { final String intentAction) {
return new NotificationCompat.Action(drawable, player.context.getString(title), return new NotificationCompat.Action(drawable, player.getContext().getString(title),
PendingIntent.getBroadcast(player.context, NOTIFICATION_ID, PendingIntent.getBroadcast(player.getContext(), NOTIFICATION_ID,
new Intent(intentAction), FLAG_UPDATE_CURRENT)); new Intent(intentAction), FLAG_UPDATE_CURRENT));
} }
private Intent getIntentForNotification(final VideoPlayerImpl player) { private Intent getIntentForNotification(final Player player) {
if (player.audioPlayerSelected() || player.popupPlayerSelected()) { if (player.audioPlayerSelected() || player.popupPlayerSelected()) {
// Means we play in popup or audio only. Let's show the play queue // Means we play in popup or audio only. Let's show the play queue
return NavigationHelper.getPlayQueueActivityIntent(player.context); return NavigationHelper.getPlayQueueActivityIntent(player.getContext());
} else { } else {
// We are playing in fragment. Don't open another activity just show fragment. That's it // We are playing in fragment. Don't open another activity just show fragment. That's it
final Intent intent = NavigationHelper.getPlayerIntent( final Intent intent = NavigationHelper.getPlayerIntent(
player.context, MainActivity.class, null, true); player.getContext(), MainActivity.class, null, true);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.setAction(Intent.ACTION_MAIN); intent.setAction(Intent.ACTION_MAIN);
intent.addCategory(Intent.CATEGORY_LAUNCHER); intent.addCategory(Intent.CATEGORY_LAUNCHER);
@ -354,10 +354,9 @@ public final class NotificationUtil {
// BITMAP // BITMAP
///////////////////////////////////////////////////// /////////////////////////////////////////////////////
private void setLargeIcon(final NotificationCompat.Builder builder, private void setLargeIcon(final NotificationCompat.Builder builder, final Player player) {
final VideoPlayerImpl player) { final boolean scaleImageToSquareAspectRatio = player.getPrefs().getBoolean(
final boolean scaleImageToSquareAspectRatio = player.sharedPreferences.getBoolean( player.getContext().getString(R.string.scale_to_square_image_in_notifications_key),
player.context.getString(R.string.scale_to_square_image_in_notifications_key),
false); false);
if (scaleImageToSquareAspectRatio) { if (scaleImageToSquareAspectRatio) {
builder.setLargeIcon(getBitmapWithSquareAspectRatio(player.getThumbnail())); builder.setLargeIcon(getBitmapWithSquareAspectRatio(player.getThumbnail()));

View File

@ -1,6 +1,7 @@
package org.schabi.newpipe.player; package org.schabi.newpipe.player;
import android.content.ComponentName; import android.content.ComponentName;
import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.content.ServiceConnection; import android.content.ServiceConnection;
import android.os.Bundle; import android.os.Bundle;
@ -11,23 +12,18 @@ import android.view.Menu;
import android.view.MenuItem; import android.view.MenuItem;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.widget.ImageButton;
import android.widget.LinearLayout;
import android.widget.PopupMenu; import android.widget.PopupMenu;
import android.widget.ProgressBar;
import android.widget.SeekBar; import android.widget.SeekBar;
import android.widget.TextView;
import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.Toolbar;
import androidx.recyclerview.widget.ItemTouchHelper; import androidx.recyclerview.widget.ItemTouchHelper;
import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.RecyclerView;
import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.exoplayer2.PlaybackParameters;
import com.google.android.exoplayer2.Player;
import org.schabi.newpipe.R; import org.schabi.newpipe.R;
import org.schabi.newpipe.databinding.ActivityPlayerQueueControlBinding;
import org.schabi.newpipe.extractor.stream.StreamInfo; import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.fragments.OnScrollBelowItemsListener; import org.schabi.newpipe.fragments.OnScrollBelowItemsListener;
import org.schabi.newpipe.local.dialog.PlaylistAppendDialog; import org.schabi.newpipe.local.dialog.PlaylistAppendDialog;
@ -51,68 +47,32 @@ import java.util.List;
import static org.schabi.newpipe.player.helper.PlayerHelper.formatSpeed; import static org.schabi.newpipe.player.helper.PlayerHelper.formatSpeed;
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage; import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
public abstract class ServicePlayerActivity extends AppCompatActivity public final class PlayQueueActivity extends AppCompatActivity
implements PlayerEventListener, SeekBar.OnSeekBarChangeListener, implements PlayerEventListener, SeekBar.OnSeekBarChangeListener,
View.OnClickListener, PlaybackParameterDialog.Callback { View.OnClickListener, PlaybackParameterDialog.Callback {
private static final String TAG = PlayQueueActivity.class.getSimpleName();
private static final int RECYCLER_ITEM_POPUP_MENU_GROUP_ID = 47; private static final int RECYCLER_ITEM_POPUP_MENU_GROUP_ID = 47;
private static final int SMOOTH_SCROLL_MAXIMUM_DISTANCE = 80; private static final int SMOOTH_SCROLL_MAXIMUM_DISTANCE = 80;
protected BasePlayer player; protected Player player;
private boolean serviceBound; private boolean serviceBound;
private ServiceConnection serviceConnection; private ServiceConnection serviceConnection;
private boolean seeking; private boolean seeking;
private boolean redraw;
//////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////
// Views // Views
//////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////
private View rootView; private ActivityPlayerQueueControlBinding queueControlBinding;
private RecyclerView itemsList;
private ItemTouchHelper itemTouchHelper; private ItemTouchHelper itemTouchHelper;
private LinearLayout metadata;
private TextView metadataTitle;
private TextView metadataArtist;
private SeekBar progressSeekBar;
private TextView progressCurrentTime;
private TextView progressEndTime;
private TextView progressLiveSync;
private TextView seekDisplay;
private ImageButton repeatButton;
private ImageButton backwardButton;
private ImageButton fastRewindButton;
private ImageButton playPauseButton;
private ImageButton fastForwardButton;
private ImageButton forwardButton;
private ImageButton shuffleButton;
private ProgressBar progressBar;
private Menu menu; private Menu menu;
////////////////////////////////////////////////////////////////////////////
// Abstracts
////////////////////////////////////////////////////////////////////////////
public abstract String getTag();
public abstract String getSupportActionTitle();
public abstract Intent getBindIntent();
public abstract void startPlayerListener();
public abstract void stopPlayerListener();
public abstract int getPlayerOptionMenuResource();
public abstract void setupMenu(Menu m);
//////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////
// Activity Lifecycle // Activity Lifecycle
//////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////
@ -122,42 +82,39 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
assureCorrectAppLanguage(this); assureCorrectAppLanguage(this);
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
ThemeHelper.setTheme(this); ThemeHelper.setTheme(this);
setContentView(R.layout.activity_player_queue_control);
rootView = findViewById(R.id.main_content);
final Toolbar toolbar = rootView.findViewById(R.id.toolbar); queueControlBinding = ActivityPlayerQueueControlBinding.inflate(getLayoutInflater());
setSupportActionBar(toolbar); setContentView(queueControlBinding.getRoot());
setSupportActionBar(queueControlBinding.toolbar);
if (getSupportActionBar() != null) { if (getSupportActionBar() != null) {
getSupportActionBar().setDisplayHomeAsUpEnabled(true); getSupportActionBar().setDisplayHomeAsUpEnabled(true);
getSupportActionBar().setTitle(getSupportActionTitle()); getSupportActionBar().setTitle(R.string.title_activity_play_queue);
} }
serviceConnection = getServiceConnection(); serviceConnection = getServiceConnection();
bind(); bind();
} }
@Override
protected void onResume() {
super.onResume();
if (redraw) {
recreate();
redraw = false;
}
}
@Override @Override
public boolean onCreateOptionsMenu(final Menu m) { public boolean onCreateOptionsMenu(final Menu m) {
this.menu = m; this.menu = m;
getMenuInflater().inflate(R.menu.menu_play_queue, m); getMenuInflater().inflate(R.menu.menu_play_queue, m);
getMenuInflater().inflate(getPlayerOptionMenuResource(), m); getMenuInflater().inflate(R.menu.menu_play_queue_bg, m);
onMaybeMuteChanged(); onMaybeMuteChanged();
onPlaybackParameterChanged(player.getPlaybackParameters());
return true; return true;
} }
// Allow to setup visibility of menuItems // Allow to setup visibility of menuItems
@Override @Override
public boolean onPrepareOptionsMenu(final Menu m) { public boolean onPrepareOptionsMenu(final Menu m) {
setupMenu(m); if (player != null) {
menu.findItem(R.id.action_switch_popup)
.setVisible(!player.popupPlayerSelected());
menu.findItem(R.id.action_switch_background)
.setVisible(!player.audioPlayerSelected());
}
return super.onPrepareOptionsMenu(m); return super.onPrepareOptionsMenu(m);
} }
@ -189,14 +146,14 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
case R.id.action_switch_popup: case R.id.action_switch_popup:
if (PermissionHelper.isPopupEnabled(this)) { if (PermissionHelper.isPopupEnabled(this)) {
this.player.setRecovery(); this.player.setRecovery();
NavigationHelper.playOnPopupPlayer(this, player.playQueue, true); NavigationHelper.playOnPopupPlayer(this, player.getPlayQueue(), true);
} else { } else {
PermissionHelper.showPopupEnablementToast(this); PermissionHelper.showPopupEnablementToast(this);
} }
return true; return true;
case R.id.action_switch_background: case R.id.action_switch_background:
this.player.setRecovery(); this.player.setRecovery();
NavigationHelper.playOnBackgroundPlayer(this, player.playQueue, true); NavigationHelper.playOnBackgroundPlayer(this, player.getPlayQueue(), true);
return true; return true;
} }
return super.onOptionsItemSelected(item); return super.onOptionsItemSelected(item);
@ -213,7 +170,8 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
//////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////
private void bind() { private void bind() {
final boolean success = bindService(getBindIntent(), serviceConnection, BIND_AUTO_CREATE); final Intent bindIntent = new Intent(this, MainPlayer.class);
final boolean success = bindService(bindIntent, serviceConnection, BIND_AUTO_CREATE);
if (!success) { if (!success) {
unbindService(serviceConnection); unbindService(serviceConnection);
} }
@ -224,19 +182,18 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
if (serviceBound) { if (serviceBound) {
unbindService(serviceConnection); unbindService(serviceConnection);
serviceBound = false; serviceBound = false;
stopPlayerListener(); if (player != null) {
player.removeActivityListener(this);
}
if (player != null && player.getPlayQueueAdapter() != null) { if (player != null && player.getPlayQueueAdapter() != null) {
player.getPlayQueueAdapter().unsetSelectedListener(); player.getPlayQueueAdapter().unsetSelectedListener();
} }
if (itemsList != null) { queueControlBinding.playQueue.setAdapter(null);
itemsList.setAdapter(null);
}
if (itemTouchHelper != null) { if (itemTouchHelper != null) {
itemTouchHelper.attachToRecyclerView(null); itemTouchHelper.attachToRecyclerView(null);
} }
itemsList = null;
itemTouchHelper = null; itemTouchHelper = null;
player = null; player = null;
} }
@ -246,12 +203,12 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
return new ServiceConnection() { return new ServiceConnection() {
@Override @Override
public void onServiceDisconnected(final ComponentName name) { public void onServiceDisconnected(final ComponentName name) {
Log.d(getTag(), "Player service is disconnected"); Log.d(TAG, "Player service is disconnected");
} }
@Override @Override
public void onServiceConnected(final ComponentName name, final IBinder service) { public void onServiceConnected(final ComponentName name, final IBinder service) {
Log.d(getTag(), "Player service is connected"); Log.d(TAG, "Player service is connected");
if (service instanceof PlayerServiceBinder) { if (service instanceof PlayerServiceBinder) {
player = ((PlayerServiceBinder) service).getPlayerInstance(); player = ((PlayerServiceBinder) service).getPlayerInstance();
@ -260,12 +217,14 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
} }
if (player == null || player.getPlayQueue() == null if (player == null || player.getPlayQueue() == null
|| player.getPlayQueueAdapter() == null || player.getPlayer() == null) { || player.getPlayQueueAdapter() == null || player.exoPlayerIsNull()) {
unbind(); unbind();
finish(); finish();
} else { } else {
buildComponents(); buildComponents();
startPlayerListener(); if (player != null) {
player.setActivityListener(PlayQueueActivity.this);
}
} }
} }
}; };
@ -283,58 +242,38 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
} }
private void buildQueue() { private void buildQueue() {
itemsList = findViewById(R.id.play_queue); queueControlBinding.playQueue.setLayoutManager(new LinearLayoutManager(this));
itemsList.setLayoutManager(new LinearLayoutManager(this)); queueControlBinding.playQueue.setAdapter(player.getPlayQueueAdapter());
itemsList.setAdapter(player.getPlayQueueAdapter()); queueControlBinding.playQueue.setClickable(true);
itemsList.setClickable(true); queueControlBinding.playQueue.setLongClickable(true);
itemsList.setLongClickable(true); queueControlBinding.playQueue.clearOnScrollListeners();
itemsList.clearOnScrollListeners(); queueControlBinding.playQueue.addOnScrollListener(getQueueScrollListener());
itemsList.addOnScrollListener(getQueueScrollListener());
itemTouchHelper = new ItemTouchHelper(getItemTouchCallback()); itemTouchHelper = new ItemTouchHelper(getItemTouchCallback());
itemTouchHelper.attachToRecyclerView(itemsList); itemTouchHelper.attachToRecyclerView(queueControlBinding.playQueue);
player.getPlayQueueAdapter().setSelectedListener(getOnSelectedListener()); player.getPlayQueueAdapter().setSelectedListener(getOnSelectedListener());
} }
private void buildMetadata() { private void buildMetadata() {
metadata = rootView.findViewById(R.id.metadata); queueControlBinding.metadata.setOnClickListener(this);
metadataTitle = rootView.findViewById(R.id.song_name); queueControlBinding.songName.setSelected(true);
metadataArtist = rootView.findViewById(R.id.artist_name); queueControlBinding.artistName.setSelected(true);
metadata.setOnClickListener(this);
metadataTitle.setSelected(true);
metadataArtist.setSelected(true);
} }
private void buildSeekBar() { private void buildSeekBar() {
progressCurrentTime = rootView.findViewById(R.id.current_time); queueControlBinding.seekBar.setOnSeekBarChangeListener(this);
progressSeekBar = rootView.findViewById(R.id.seek_bar); queueControlBinding.liveSync.setOnClickListener(this);
progressEndTime = rootView.findViewById(R.id.end_time);
progressLiveSync = rootView.findViewById(R.id.live_sync);
seekDisplay = rootView.findViewById(R.id.seek_display);
progressSeekBar.setOnSeekBarChangeListener(this);
progressLiveSync.setOnClickListener(this);
} }
private void buildControls() { private void buildControls() {
repeatButton = rootView.findViewById(R.id.control_repeat); queueControlBinding.controlRepeat.setOnClickListener(this);
backwardButton = rootView.findViewById(R.id.control_backward); queueControlBinding.controlBackward.setOnClickListener(this);
fastRewindButton = rootView.findViewById(R.id.control_fast_rewind); queueControlBinding.controlFastRewind.setOnClickListener(this);
playPauseButton = rootView.findViewById(R.id.control_play_pause); queueControlBinding.controlPlayPause.setOnClickListener(this);
fastForwardButton = rootView.findViewById(R.id.control_fast_forward); queueControlBinding.controlFastForward.setOnClickListener(this);
forwardButton = rootView.findViewById(R.id.control_forward); queueControlBinding.controlForward.setOnClickListener(this);
shuffleButton = rootView.findViewById(R.id.control_shuffle); queueControlBinding.controlShuffle.setOnClickListener(this);
progressBar = rootView.findViewById(R.id.control_progress_bar);
repeatButton.setOnClickListener(this);
backwardButton.setOnClickListener(this);
fastRewindButton.setOnClickListener(this);
playPauseButton.setOnClickListener(this);
fastForwardButton.setOnClickListener(this);
forwardButton.setOnClickListener(this);
shuffleButton.setOnClickListener(this);
} }
private void buildItemPopupMenu(final PlayQueueItem item, final View view) { private void buildItemPopupMenu(final PlayQueueItem item, final View view) {
@ -390,8 +329,8 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
if (player != null && player.getPlayQueue() != null if (player != null && player.getPlayQueue() != null
&& !player.getPlayQueue().isComplete()) { && !player.getPlayQueue().isComplete()) {
player.getPlayQueue().fetch(); player.getPlayQueue().fetch();
} else if (itemsList != null) { } else {
itemsList.clearOnScrollListeners(); queueControlBinding.playQueue.clearOnScrollListeners();
} }
} }
}; };
@ -420,7 +359,7 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
@Override @Override
public void selected(final PlayQueueItem item, final View view) { public void selected(final PlayQueueItem item, final View view) {
if (player != null) { if (player != null) {
player.onSelected(item); player.selectQueueItem(item);
} }
} }
@ -452,8 +391,9 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
final int currentPlayingIndex = player.getPlayQueue().getIndex(); final int currentPlayingIndex = player.getPlayQueue().getIndex();
final int currentVisibleIndex; final int currentVisibleIndex;
if (itemsList.getLayoutManager() instanceof LinearLayoutManager) { if (queueControlBinding.playQueue.getLayoutManager() instanceof LinearLayoutManager) {
final LinearLayoutManager layout = ((LinearLayoutManager) itemsList.getLayoutManager()); final LinearLayoutManager layout =
(LinearLayoutManager) queueControlBinding.playQueue.getLayoutManager();
currentVisibleIndex = layout.findFirstVisibleItemPosition(); currentVisibleIndex = layout.findFirstVisibleItemPosition();
} else { } else {
currentVisibleIndex = 0; currentVisibleIndex = 0;
@ -461,9 +401,9 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
final int distance = Math.abs(currentPlayingIndex - currentVisibleIndex); final int distance = Math.abs(currentPlayingIndex - currentVisibleIndex);
if (distance < SMOOTH_SCROLL_MAXIMUM_DISTANCE) { if (distance < SMOOTH_SCROLL_MAXIMUM_DISTANCE) {
itemsList.smoothScrollToPosition(currentPlayingIndex); queueControlBinding.playQueue.smoothScrollToPosition(currentPlayingIndex);
} else { } else {
itemsList.scrollToPosition(currentPlayingIndex); queueControlBinding.playQueue.scrollToPosition(currentPlayingIndex);
} }
} }
@ -477,23 +417,23 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
return; return;
} }
if (view.getId() == repeatButton.getId()) { if (view.getId() == queueControlBinding.controlRepeat.getId()) {
player.onRepeatClicked(); player.onRepeatClicked();
} else if (view.getId() == backwardButton.getId()) { } else if (view.getId() == queueControlBinding.controlBackward.getId()) {
player.onPlayPrevious(); player.playPrevious();
} else if (view.getId() == fastRewindButton.getId()) { } else if (view.getId() == queueControlBinding.controlFastRewind.getId()) {
player.onFastRewind(); player.fastRewind();
} else if (view.getId() == playPauseButton.getId()) { } else if (view.getId() == queueControlBinding.controlPlayPause.getId()) {
player.onPlayPause(); player.playPause();
} else if (view.getId() == fastForwardButton.getId()) { } else if (view.getId() == queueControlBinding.controlFastForward.getId()) {
player.onFastForward(); player.fastForward();
} else if (view.getId() == forwardButton.getId()) { } else if (view.getId() == queueControlBinding.controlForward.getId()) {
player.onPlayNext(); player.playNext();
} else if (view.getId() == shuffleButton.getId()) { } else if (view.getId() == queueControlBinding.controlShuffle.getId()) {
player.onShuffleClicked(); player.onShuffleClicked();
} else if (view.getId() == metadata.getId()) { } else if (view.getId() == queueControlBinding.metadata.getId()) {
scrollToSelected(); scrollToSelected();
} else if (view.getId() == progressLiveSync.getId()) { } else if (view.getId() == queueControlBinding.liveSync.getId()) {
player.seekToDefault(); player.seekToDefault();
} }
} }
@ -507,7 +447,7 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
return; return;
} }
PlaybackParameterDialog.newInstance(player.getPlaybackSpeed(), player.getPlaybackPitch(), PlaybackParameterDialog.newInstance(player.getPlaybackSpeed(), player.getPlaybackPitch(),
player.getPlaybackSkipSilence(), this).show(getSupportFragmentManager(), getTag()); player.getPlaybackSkipSilence(), this).show(getSupportFragmentManager(), TAG);
} }
@Override @Override
@ -527,15 +467,15 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
final boolean fromUser) { final boolean fromUser) {
if (fromUser) { if (fromUser) {
final String seekTime = Localization.getDurationString(progress / 1000); final String seekTime = Localization.getDurationString(progress / 1000);
progressCurrentTime.setText(seekTime); queueControlBinding.currentTime.setText(seekTime);
seekDisplay.setText(seekTime); queueControlBinding.seekDisplay.setText(seekTime);
} }
} }
@Override @Override
public void onStartTrackingTouch(final SeekBar seekBar) { public void onStartTrackingTouch(final SeekBar seekBar) {
seeking = true; seeking = true;
seekDisplay.setVisibility(View.VISIBLE); queueControlBinding.seekDisplay.setVisibility(View.VISIBLE);
} }
@Override @Override
@ -543,7 +483,7 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
if (player != null) { if (player != null) {
player.seekTo(seekBar.getProgress()); player.seekTo(seekBar.getProgress());
} }
seekDisplay.setVisibility(View.GONE); queueControlBinding.seekDisplay.setVisibility(View.GONE);
seeking = false; seeking = false;
} }
@ -561,10 +501,8 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
final PlaylistAppendDialog d = PlaylistAppendDialog.fromPlayQueueItems(playlist); final PlaylistAppendDialog d = PlaylistAppendDialog.fromPlayQueueItems(playlist);
PlaylistAppendDialog.onPlaylistFound(getApplicationContext(), PlaylistAppendDialog.onPlaylistFound(getApplicationContext(),
() -> d.show(getSupportFragmentManager(), getTag()), () -> d.show(getSupportFragmentManager(), TAG),
() -> PlaylistCreationDialog.newInstance(d) () -> PlaylistCreationDialog.newInstance(d).show(getSupportFragmentManager(), TAG));
.show(getSupportFragmentManager(), getTag()
));
} }
//////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////
@ -601,45 +539,46 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
public void onProgressUpdate(final int currentProgress, final int duration, public void onProgressUpdate(final int currentProgress, final int duration,
final int bufferPercent) { final int bufferPercent) {
// Set buffer progress // Set buffer progress
progressSeekBar.setSecondaryProgress((int) (progressSeekBar.getMax() queueControlBinding.seekBar.setSecondaryProgress((int) (queueControlBinding.seekBar.getMax()
* ((float) bufferPercent / 100))); * ((float) bufferPercent / 100)));
// Set Duration // Set Duration
progressSeekBar.setMax(duration); queueControlBinding.seekBar.setMax(duration);
progressEndTime.setText(Localization.getDurationString(duration / 1000)); queueControlBinding.endTime.setText(Localization.getDurationString(duration / 1000));
// Set current time if not seeking // Set current time if not seeking
if (!seeking) { if (!seeking) {
progressSeekBar.setProgress(currentProgress); queueControlBinding.seekBar.setProgress(currentProgress);
progressCurrentTime.setText(Localization.getDurationString(currentProgress / 1000)); queueControlBinding.currentTime.setText(Localization
.getDurationString(currentProgress / 1000));
} }
if (player != null) { if (player != null) {
progressLiveSync.setClickable(!player.isLiveEdge()); queueControlBinding.liveSync.setClickable(!player.isLiveEdge());
} }
// this will make shure progressCurrentTime has the same width as progressEndTime // this will make sure progressCurrentTime has the same width as progressEndTime
final ViewGroup.LayoutParams endTimeParams = progressEndTime.getLayoutParams(); final ViewGroup.LayoutParams currentTimeParams =
final ViewGroup.LayoutParams currentTimeParams = progressCurrentTime.getLayoutParams(); queueControlBinding.currentTime.getLayoutParams();
currentTimeParams.width = progressEndTime.getWidth(); currentTimeParams.width = queueControlBinding.endTime.getWidth();
progressCurrentTime.setLayoutParams(currentTimeParams); queueControlBinding.currentTime.setLayoutParams(currentTimeParams);
} }
@Override @Override
public void onMetadataUpdate(final StreamInfo info, final PlayQueue queue) { public void onMetadataUpdate(final StreamInfo info, final PlayQueue queue) {
if (info != null) { if (info != null) {
metadataTitle.setText(info.getName()); queueControlBinding.songName.setText(info.getName());
metadataArtist.setText(info.getUploaderName()); queueControlBinding.artistName.setText(info.getUploaderName());
progressEndTime.setVisibility(View.GONE); queueControlBinding.endTime.setVisibility(View.GONE);
progressLiveSync.setVisibility(View.GONE); queueControlBinding.liveSync.setVisibility(View.GONE);
switch (info.getStreamType()) { switch (info.getStreamType()) {
case LIVE_STREAM: case LIVE_STREAM:
case AUDIO_LIVE_STREAM: case AUDIO_LIVE_STREAM:
progressLiveSync.setVisibility(View.VISIBLE); queueControlBinding.liveSync.setVisibility(View.VISIBLE);
break; break;
default: default:
progressEndTime.setVisibility(View.VISIBLE); queueControlBinding.endTime.setVisibility(View.VISIBLE);
break; break;
} }
@ -659,50 +598,56 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
private void onStateChanged(final int state) { private void onStateChanged(final int state) {
switch (state) { switch (state) {
case BasePlayer.STATE_PAUSED: case Player.STATE_PAUSED:
playPauseButton.setImageResource(R.drawable.ic_play_arrow_white_24dp); queueControlBinding.controlPlayPause
.setImageResource(R.drawable.ic_play_arrow_white_24dp);
break; break;
case BasePlayer.STATE_PLAYING: case Player.STATE_PLAYING:
playPauseButton.setImageResource(R.drawable.ic_pause_white_24dp); queueControlBinding.controlPlayPause
.setImageResource(R.drawable.ic_pause_white_24dp);
break; break;
case BasePlayer.STATE_COMPLETED: case Player.STATE_COMPLETED:
playPauseButton.setImageResource(R.drawable.ic_replay_white_24dp); queueControlBinding.controlPlayPause
.setImageResource(R.drawable.ic_replay_white_24dp);
break; break;
default: default:
break; break;
} }
switch (state) { switch (state) {
case BasePlayer.STATE_PAUSED: case Player.STATE_PAUSED:
case BasePlayer.STATE_PLAYING: case Player.STATE_PLAYING:
case BasePlayer.STATE_COMPLETED: case Player.STATE_COMPLETED:
playPauseButton.setClickable(true); queueControlBinding.controlPlayPause.setClickable(true);
playPauseButton.setVisibility(View.VISIBLE); queueControlBinding.controlPlayPause.setVisibility(View.VISIBLE);
progressBar.setVisibility(View.GONE); queueControlBinding.controlProgressBar.setVisibility(View.GONE);
break; break;
default: default:
playPauseButton.setClickable(false); queueControlBinding.controlPlayPause.setClickable(false);
playPauseButton.setVisibility(View.INVISIBLE); queueControlBinding.controlPlayPause.setVisibility(View.INVISIBLE);
progressBar.setVisibility(View.VISIBLE); queueControlBinding.controlProgressBar.setVisibility(View.VISIBLE);
break; break;
} }
} }
private void onPlayModeChanged(final int repeatMode, final boolean shuffled) { private void onPlayModeChanged(final int repeatMode, final boolean shuffled) {
switch (repeatMode) { switch (repeatMode) {
case Player.REPEAT_MODE_OFF: case com.google.android.exoplayer2.Player.REPEAT_MODE_OFF:
repeatButton.setImageResource(R.drawable.exo_controls_repeat_off); queueControlBinding.controlRepeat
.setImageResource(R.drawable.exo_controls_repeat_off);
break; break;
case Player.REPEAT_MODE_ONE: case com.google.android.exoplayer2.Player.REPEAT_MODE_ONE:
repeatButton.setImageResource(R.drawable.exo_controls_repeat_one); queueControlBinding.controlRepeat
.setImageResource(R.drawable.exo_controls_repeat_one);
break; break;
case Player.REPEAT_MODE_ALL: case com.google.android.exoplayer2.Player.REPEAT_MODE_ALL:
repeatButton.setImageResource(R.drawable.exo_controls_repeat_all); queueControlBinding.controlRepeat
.setImageResource(R.drawable.exo_controls_repeat_all);
break; break;
} }
final int shuffleAlpha = shuffled ? 255 : 77; final int shuffleAlpha = shuffled ? 255 : 77;
shuffleButton.setImageAlpha(shuffleAlpha); queueControlBinding.controlShuffle.setImageAlpha(shuffleAlpha);
} }
private void onPlaybackParameterChanged(final PlaybackParameters parameters) { private void onPlaybackParameterChanged(final PlaybackParameters parameters) {
@ -715,12 +660,13 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
} }
private void onMaybePlaybackAdapterChanged() { private void onMaybePlaybackAdapterChanged() {
if (itemsList == null || player == null) { if (player == null) {
return; return;
} }
final PlayQueueAdapter maybeNewAdapter = player.getPlayQueueAdapter(); final PlayQueueAdapter maybeNewAdapter = player.getPlayQueueAdapter();
if (maybeNewAdapter != null && itemsList.getAdapter() != maybeNewAdapter) { if (maybeNewAdapter != null
itemsList.setAdapter(maybeNewAdapter); && queueControlBinding.playQueue.getAdapter() != maybeNewAdapter) {
queueControlBinding.playQueue.setAdapter(maybeNewAdapter);
} }
} }
@ -734,10 +680,9 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
//2) Icon change accordingly to current App Theme //2) Icon change accordingly to current App Theme
// using rootView.getContext() because getApplicationContext() didn't work // using rootView.getContext() because getApplicationContext() didn't work
item.setIcon(ThemeHelper.resolveResourceIdFromAttr(rootView.getContext(), final Context context = queueControlBinding.getRoot().getContext();
player.isMuted() item.setIcon(ThemeHelper.resolveResourceIdFromAttr(context,
? R.attr.ic_volume_off player.isMuted() ? R.attr.ic_volume_off : R.attr.ic_volume_up));
: R.attr.ic_volume_up));
} }
} }
} }

File diff suppressed because it is too large Load Diff

View File

@ -5,13 +5,13 @@ import android.os.Binder;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
class PlayerServiceBinder extends Binder { class PlayerServiceBinder extends Binder {
private final BasePlayer basePlayer; private final Player player;
PlayerServiceBinder(@NonNull final BasePlayer basePlayer) { PlayerServiceBinder(@NonNull final Player player) {
this.basePlayer = basePlayer; this.player = player;
} }
BasePlayer getPlayerInstance() { Player getPlayerInstance() {
return basePlayer; return player;
} }
} }

File diff suppressed because it is too large Load Diff

View File

@ -7,10 +7,10 @@ import android.view.GestureDetector
import android.view.MotionEvent import android.view.MotionEvent
import android.view.View import android.view.View
import android.view.ViewConfiguration import android.view.ViewConfiguration
import org.schabi.newpipe.player.BasePlayer
import org.schabi.newpipe.player.MainPlayer import org.schabi.newpipe.player.MainPlayer
import org.schabi.newpipe.player.VideoPlayerImpl import org.schabi.newpipe.player.Player
import org.schabi.newpipe.player.helper.PlayerHelper import org.schabi.newpipe.player.helper.PlayerHelper
import org.schabi.newpipe.player.helper.PlayerHelper.savePopupPositionAndSizeToPrefs
import org.schabi.newpipe.util.AnimationUtils import org.schabi.newpipe.util.AnimationUtils
import kotlin.math.abs import kotlin.math.abs
import kotlin.math.hypot import kotlin.math.hypot
@ -18,14 +18,14 @@ import kotlin.math.max
import kotlin.math.min import kotlin.math.min
/** /**
* Base gesture handling for [VideoPlayerImpl] * Base gesture handling for [Player]
* *
* This class contains the logic for the player gestures like View preparations * This class contains the logic for the player gestures like View preparations
* and provides some abstract methods to make it easier separating the logic from the UI. * and provides some abstract methods to make it easier separating the logic from the UI.
*/ */
abstract class BasePlayerGestureListener( abstract class BasePlayerGestureListener(
@JvmField @JvmField
protected val playerImpl: VideoPlayerImpl, protected val player: Player,
@JvmField @JvmField
protected val service: MainPlayer protected val service: MainPlayer
) : GestureDetector.SimpleOnGestureListener(), View.OnTouchListener { ) : GestureDetector.SimpleOnGestureListener(), View.OnTouchListener {
@ -78,7 +78,7 @@ abstract class BasePlayerGestureListener(
// /////////////////////////////////////////////////////////////////// // ///////////////////////////////////////////////////////////////////
override fun onTouch(v: View, event: MotionEvent): Boolean { override fun onTouch(v: View, event: MotionEvent): Boolean {
return if (playerImpl.popupPlayerSelected()) { return if (player.popupPlayerSelected()) {
onTouchInPopup(v, event) onTouchInPopup(v, event)
} else { } else {
onTouchInMain(v, event) onTouchInMain(v, event)
@ -86,14 +86,14 @@ abstract class BasePlayerGestureListener(
} }
private fun onTouchInMain(v: View, event: MotionEvent): Boolean { private fun onTouchInMain(v: View, event: MotionEvent): Boolean {
playerImpl.gestureDetector.onTouchEvent(event) player.gestureDetector.onTouchEvent(event)
if (event.action == MotionEvent.ACTION_UP && isMovingInMain) { if (event.action == MotionEvent.ACTION_UP && isMovingInMain) {
isMovingInMain = false isMovingInMain = false
onScrollEnd(MainPlayer.PlayerType.VIDEO, event) onScrollEnd(MainPlayer.PlayerType.VIDEO, event)
} }
return when (event.action) { return when (event.action) {
MotionEvent.ACTION_DOWN, MotionEvent.ACTION_MOVE -> { MotionEvent.ACTION_DOWN, MotionEvent.ACTION_MOVE -> {
v.parent.requestDisallowInterceptTouchEvent(playerImpl.isFullscreen) v.parent.requestDisallowInterceptTouchEvent(player.isFullscreen)
true true
} }
MotionEvent.ACTION_UP -> { MotionEvent.ACTION_UP -> {
@ -105,7 +105,7 @@ abstract class BasePlayerGestureListener(
} }
private fun onTouchInPopup(v: View, event: MotionEvent): Boolean { private fun onTouchInPopup(v: View, event: MotionEvent): Boolean {
playerImpl.gestureDetector.onTouchEvent(event) player.gestureDetector.onTouchEvent(event)
if (event.pointerCount == 2 && !isMovingInPopup && !isResizing) { if (event.pointerCount == 2 && !isMovingInPopup && !isResizing) {
if (DEBUG) { if (DEBUG) {
Log.d(TAG, "onTouch() 2 finger pointer detected, enabling resizing.") Log.d(TAG, "onTouch() 2 finger pointer detected, enabling resizing.")
@ -157,10 +157,10 @@ abstract class BasePlayerGestureListener(
initSecPointerY = (-1).toFloat() initSecPointerY = (-1).toFloat()
onPopupResizingEnd() onPopupResizingEnd()
playerImpl.changeState(playerImpl.currentState) player.changeState(player.currentState)
} }
if (!playerImpl.isPopupClosing) { if (!player.isPopupClosing) {
playerImpl.savePositionAndSize() savePopupPositionAndSizeToPrefs(player)
} }
} }
@ -190,19 +190,15 @@ abstract class BasePlayerGestureListener(
event.getY(0) - event.getY(1).toDouble() event.getY(0) - event.getY(1).toDouble()
) )
val popupWidth = playerImpl.popupWidth.toDouble() val popupWidth = player.popupLayoutParams!!.width.toDouble()
// change co-ordinates of popup so the center stays at the same position // change co-ordinates of popup so the center stays at the same position
val newWidth = popupWidth * currentPointerDistance / initPointerDistance val newWidth = popupWidth * currentPointerDistance / initPointerDistance
initPointerDistance = currentPointerDistance initPointerDistance = currentPointerDistance
playerImpl.popupLayoutParams.x += ((popupWidth - newWidth) / 2.0).toInt() player.popupLayoutParams!!.x += ((popupWidth - newWidth) / 2.0).toInt()
playerImpl.checkPopupPositionBounds() player.checkPopupPositionBounds()
playerImpl.updateScreenSize() player.updateScreenSize()
player.changePopupSize(min(player.screenWidth.toDouble(), newWidth).toInt())
playerImpl.updatePopupSize(
min(playerImpl.screenWidth.toDouble(), newWidth).toInt(),
-1
)
return true return true
} }
} }
@ -222,7 +218,7 @@ abstract class BasePlayerGestureListener(
return true return true
} }
return if (playerImpl.popupPlayerSelected()) return if (player.popupPlayerSelected())
onDownInPopup(e) onDownInPopup(e)
else else
true true
@ -231,12 +227,10 @@ abstract class BasePlayerGestureListener(
private fun onDownInPopup(e: MotionEvent): Boolean { private fun onDownInPopup(e: MotionEvent): Boolean {
// Fix popup position when the user touch it, it may have the wrong one // Fix popup position when the user touch it, it may have the wrong one
// because the soft input is visible (the draggable area is currently resized). // because the soft input is visible (the draggable area is currently resized).
playerImpl.updateScreenSize() player.updateScreenSize()
playerImpl.checkPopupPositionBounds() player.checkPopupPositionBounds()
initialPopupX = playerImpl.popupLayoutParams.x initialPopupX = player.popupLayoutParams!!.x
initialPopupY = playerImpl.popupLayoutParams.y initialPopupY = player.popupLayoutParams!!.y
playerImpl.popupWidth = playerImpl.popupLayoutParams.width.toFloat()
playerImpl.popupHeight = playerImpl.popupLayoutParams.height.toFloat()
return super.onDown(e) return super.onDown(e)
} }
@ -255,15 +249,15 @@ abstract class BasePlayerGestureListener(
if (isDoubleTapping) if (isDoubleTapping)
return true return true
if (playerImpl.popupPlayerSelected()) { if (player.popupPlayerSelected()) {
if (playerImpl.player == null) if (player.exoPlayerIsNull())
return false return false
onSingleTap(MainPlayer.PlayerType.POPUP) onSingleTap(MainPlayer.PlayerType.POPUP)
return true return true
} else { } else {
super.onSingleTapConfirmed(e) super.onSingleTapConfirmed(e)
if (playerImpl.currentState == BasePlayer.STATE_BLOCKED) if (player.currentState == Player.STATE_BLOCKED)
return true return true
onSingleTap(MainPlayer.PlayerType.VIDEO) onSingleTap(MainPlayer.PlayerType.VIDEO)
@ -272,10 +266,10 @@ abstract class BasePlayerGestureListener(
} }
override fun onLongPress(e: MotionEvent?) { override fun onLongPress(e: MotionEvent?) {
if (playerImpl.popupPlayerSelected()) { if (player.popupPlayerSelected()) {
playerImpl.updateScreenSize() player.updateScreenSize()
playerImpl.checkPopupPositionBounds() player.checkPopupPositionBounds()
playerImpl.updatePopupSize(playerImpl.screenWidth.toInt(), -1) player.changePopupSize(player.screenWidth.toInt())
} }
} }
@ -285,7 +279,7 @@ abstract class BasePlayerGestureListener(
distanceX: Float, distanceX: Float,
distanceY: Float distanceY: Float
): Boolean { ): Boolean {
return if (playerImpl.popupPlayerSelected()) { return if (player.popupPlayerSelected()) {
onScrollInPopup(initialEvent, movingEvent, distanceX, distanceY) onScrollInPopup(initialEvent, movingEvent, distanceX, distanceY)
} else { } else {
onScrollInMain(initialEvent, movingEvent, distanceX, distanceY) onScrollInMain(initialEvent, movingEvent, distanceX, distanceY)
@ -298,19 +292,18 @@ abstract class BasePlayerGestureListener(
velocityX: Float, velocityX: Float,
velocityY: Float velocityY: Float
): Boolean { ): Boolean {
return if (playerImpl.popupPlayerSelected()) { return if (player.popupPlayerSelected()) {
val absVelocityX = abs(velocityX) val absVelocityX = abs(velocityX)
val absVelocityY = abs(velocityY) val absVelocityY = abs(velocityY)
if (absVelocityX.coerceAtLeast(absVelocityY) > tossFlingVelocity) { if (absVelocityX.coerceAtLeast(absVelocityY) > tossFlingVelocity) {
if (absVelocityX > tossFlingVelocity) { if (absVelocityX > tossFlingVelocity) {
playerImpl.popupLayoutParams.x = velocityX.toInt() player.popupLayoutParams!!.x = velocityX.toInt()
} }
if (absVelocityY > tossFlingVelocity) { if (absVelocityY > tossFlingVelocity) {
playerImpl.popupLayoutParams.y = velocityY.toInt() player.popupLayoutParams!!.y = velocityY.toInt()
} }
playerImpl.checkPopupPositionBounds() player.checkPopupPositionBounds()
playerImpl.windowManager player.windowManager!!.updateViewLayout(player.rootView, player.popupLayoutParams)
.updateViewLayout(playerImpl.rootView, playerImpl.popupLayoutParams)
return true return true
} }
return false return false
@ -326,13 +319,13 @@ abstract class BasePlayerGestureListener(
distanceY: Float distanceY: Float
): Boolean { ): Boolean {
if (!playerImpl.isFullscreen) { if (!player.isFullscreen) {
return false return false
} }
val isTouchingStatusBar: Boolean = initialEvent.y < getStatusBarHeight(service) val isTouchingStatusBar: Boolean = initialEvent.y < getStatusBarHeight(service)
val isTouchingNavigationBar: Boolean = val isTouchingNavigationBar: Boolean =
initialEvent.y > (playerImpl.rootView.height - getNavigationBarHeight(service)) initialEvent.y > (player.rootView.height - getNavigationBarHeight(service))
if (isTouchingStatusBar || isTouchingNavigationBar) { if (isTouchingStatusBar || isTouchingNavigationBar) {
return false return false
} }
@ -340,7 +333,7 @@ abstract class BasePlayerGestureListener(
val insideThreshold = abs(movingEvent.y - initialEvent.y) <= MOVEMENT_THRESHOLD val insideThreshold = abs(movingEvent.y - initialEvent.y) <= MOVEMENT_THRESHOLD
if ( if (
!isMovingInMain && (insideThreshold || abs(distanceX) > abs(distanceY)) || !isMovingInMain && (insideThreshold || abs(distanceX) > abs(distanceY)) ||
playerImpl.currentState == BasePlayer.STATE_COMPLETED player.currentState == Player.STATE_COMPLETED
) { ) {
return false return false
} }
@ -371,7 +364,7 @@ abstract class BasePlayerGestureListener(
} }
if (!isMovingInPopup) { if (!isMovingInPopup) {
AnimationUtils.animateView(playerImpl.closeOverlayButton, true, 200) AnimationUtils.animateView(player.closeOverlayButton, true, 200)
} }
isMovingInPopup = true isMovingInPopup = true
@ -381,20 +374,20 @@ abstract class BasePlayerGestureListener(
val diffY: Float = (movingEvent.rawY - initialEvent.rawY) val diffY: Float = (movingEvent.rawY - initialEvent.rawY)
var posY: Float = (initialPopupY + diffY) var posY: Float = (initialPopupY + diffY)
if (posX > playerImpl.screenWidth - playerImpl.popupWidth) { if (posX > player.screenWidth - player.popupLayoutParams!!.width) {
posX = (playerImpl.screenWidth - playerImpl.popupWidth) posX = (player.screenWidth - player.popupLayoutParams!!.width)
} else if (posX < 0) { } else if (posX < 0) {
posX = 0f posX = 0f
} }
if (posY > playerImpl.screenHeight - playerImpl.popupHeight) { if (posY > player.screenHeight - player.popupLayoutParams!!.height) {
posY = (playerImpl.screenHeight - playerImpl.popupHeight) posY = (player.screenHeight - player.popupLayoutParams!!.height)
} else if (posY < 0) { } else if (posY < 0) {
posY = 0f posY = 0f
} }
playerImpl.popupLayoutParams.x = posX.toInt() player.popupLayoutParams!!.x = posX.toInt()
playerImpl.popupLayoutParams.y = posY.toInt() player.popupLayoutParams!!.y = posY.toInt()
onScroll( onScroll(
MainPlayer.PlayerType.POPUP, MainPlayer.PlayerType.POPUP,
@ -405,8 +398,7 @@ abstract class BasePlayerGestureListener(
distanceY distanceY
) )
playerImpl.windowManager player.windowManager!!.updateViewLayout(player.rootView, player.popupLayoutParams)
.updateViewLayout(playerImpl.rootView, playerImpl.popupLayoutParams)
return true return true
} }
@ -474,16 +466,16 @@ abstract class BasePlayerGestureListener(
// /////////////////////////////////////////////////////////////////// // ///////////////////////////////////////////////////////////////////
private fun getDisplayPortion(e: MotionEvent): DisplayPortion { private fun getDisplayPortion(e: MotionEvent): DisplayPortion {
return if (playerImpl.playerType == MainPlayer.PlayerType.POPUP) { return if (player.playerType == MainPlayer.PlayerType.POPUP) {
when { when {
e.x < playerImpl.popupWidth / 3.0 -> DisplayPortion.LEFT e.x < player.popupLayoutParams!!.width / 3.0 -> DisplayPortion.LEFT
e.x > playerImpl.popupWidth * 2.0 / 3.0 -> DisplayPortion.RIGHT e.x > player.popupLayoutParams!!.width * 2.0 / 3.0 -> DisplayPortion.RIGHT
else -> DisplayPortion.MIDDLE else -> DisplayPortion.MIDDLE
} }
} else /* MainPlayer.PlayerType.VIDEO */ { } else /* MainPlayer.PlayerType.VIDEO */ {
when { when {
e.x < playerImpl.rootView.width / 3.0 -> DisplayPortion.LEFT e.x < player.rootView.width / 3.0 -> DisplayPortion.LEFT
e.x > playerImpl.rootView.width * 2.0 / 3.0 -> DisplayPortion.RIGHT e.x > player.rootView.width * 2.0 / 3.0 -> DisplayPortion.RIGHT
else -> DisplayPortion.MIDDLE else -> DisplayPortion.MIDDLE
} }
} }
@ -491,14 +483,14 @@ abstract class BasePlayerGestureListener(
// Currently needed for scrolling since there is no action more the middle portion // Currently needed for scrolling since there is no action more the middle portion
private fun getDisplayHalfPortion(e: MotionEvent): DisplayPortion { private fun getDisplayHalfPortion(e: MotionEvent): DisplayPortion {
return if (playerImpl.playerType == MainPlayer.PlayerType.POPUP) { return if (player.playerType == MainPlayer.PlayerType.POPUP) {
when { when {
e.x < playerImpl.popupWidth / 2.0 -> DisplayPortion.LEFT_HALF e.x < player.popupLayoutParams!!.width / 2.0 -> DisplayPortion.LEFT_HALF
else -> DisplayPortion.RIGHT_HALF else -> DisplayPortion.RIGHT_HALF
} }
} else /* MainPlayer.PlayerType.VIDEO */ { } else /* MainPlayer.PlayerType.VIDEO */ {
when { when {
e.x < playerImpl.rootView.width / 2.0 -> DisplayPortion.LEFT_HALF e.x < player.rootView.width / 2.0 -> DisplayPortion.LEFT_HALF
else -> DisplayPortion.RIGHT_HALF else -> DisplayPortion.RIGHT_HALF
} }
} }
@ -522,7 +514,7 @@ abstract class BasePlayerGestureListener(
companion object { companion object {
private const val TAG = "BasePlayerGestListener" private const val TAG = "BasePlayerGestListener"
private val DEBUG = BasePlayer.DEBUG private val DEBUG = Player.DEBUG
private const val DOUBLE_TAP_DELAY = 550L private const val DOUBLE_TAP_DELAY = 550L
private const val MOVEMENT_THRESHOLD = 40 private const val MOVEMENT_THRESHOLD = 40

View File

@ -24,7 +24,7 @@ public class CustomBottomSheetBehavior extends BottomSheetBehavior<FrameLayout>
private boolean skippingInterception = false; private boolean skippingInterception = false;
private final List<Integer> skipInterceptionOfElements = Arrays.asList( private final List<Integer> skipInterceptionOfElements = Arrays.asList(
R.id.detail_content_root_layout, R.id.relatedStreamsLayout, R.id.detail_content_root_layout, R.id.relatedStreamsLayout,
R.id.playQueuePanel, R.id.viewpager, R.id.bottomControls, R.id.itemsListPanel, R.id.viewpager, R.id.bottomControls,
R.id.playPauseButton, R.id.playPreviousButton, R.id.playNextButton); R.id.playPauseButton, R.id.playPreviousButton, R.id.playNextButton);
@Override @Override

View File

@ -11,15 +11,15 @@ import android.widget.ProgressBar;
import androidx.appcompat.content.res.AppCompatResources; import androidx.appcompat.content.res.AppCompatResources;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import org.schabi.newpipe.MainActivity;
import org.schabi.newpipe.R; import org.schabi.newpipe.R;
import org.schabi.newpipe.player.BasePlayer;
import org.schabi.newpipe.player.MainPlayer; import org.schabi.newpipe.player.MainPlayer;
import org.schabi.newpipe.player.VideoPlayerImpl; import org.schabi.newpipe.player.Player;
import org.schabi.newpipe.player.helper.PlayerHelper; import org.schabi.newpipe.player.helper.PlayerHelper;
import static org.schabi.newpipe.player.BasePlayer.STATE_PLAYING; import static org.schabi.newpipe.player.Player.STATE_PLAYING;
import static org.schabi.newpipe.player.VideoPlayer.DEFAULT_CONTROLS_DURATION; import static org.schabi.newpipe.player.Player.DEFAULT_CONTROLS_DURATION;
import static org.schabi.newpipe.player.VideoPlayer.DEFAULT_CONTROLS_HIDE_TIME; import static org.schabi.newpipe.player.Player.DEFAULT_CONTROLS_HIDE_TIME;
import static org.schabi.newpipe.util.AnimationUtils.Type.SCALE_AND_ALPHA; import static org.schabi.newpipe.util.AnimationUtils.Type.SCALE_AND_ALPHA;
import static org.schabi.newpipe.util.AnimationUtils.animateView; import static org.schabi.newpipe.util.AnimationUtils.animateView;
@ -33,14 +33,14 @@ import static org.schabi.newpipe.util.AnimationUtils.animateView;
public class PlayerGestureListener public class PlayerGestureListener
extends BasePlayerGestureListener extends BasePlayerGestureListener
implements View.OnTouchListener { implements View.OnTouchListener {
private static final String TAG = ".PlayerGestureListener"; private static final String TAG = PlayerGestureListener.class.getSimpleName();
private static final boolean DEBUG = BasePlayer.DEBUG; private static final boolean DEBUG = MainActivity.DEBUG;
private final int maxVolume; private final int maxVolume;
public PlayerGestureListener(final VideoPlayerImpl playerImpl, final MainPlayer service) { public PlayerGestureListener(final Player player, final MainPlayer service) {
super(playerImpl, service); super(player, service);
maxVolume = playerImpl.getAudioReactor().getMaxVolume(); maxVolume = player.getAudioReactor().getMaxVolume();
} }
@Override @Override
@ -48,46 +48,44 @@ public class PlayerGestureListener
@NotNull final DisplayPortion portion) { @NotNull final DisplayPortion portion) {
if (DEBUG) { if (DEBUG) {
Log.d(TAG, "onDoubleTap called with playerType = [" Log.d(TAG, "onDoubleTap called with playerType = ["
+ playerImpl.getPlayerType() + "], portion = [" + player.getPlayerType() + "], portion = [" + portion + "]");
+ portion + "]");
} }
if (playerImpl.isSomePopupMenuVisible()) { if (player.isSomePopupMenuVisible()) {
playerImpl.hideControls(0, 0); player.hideControls(0, 0);
} }
if (portion == DisplayPortion.LEFT) { if (portion == DisplayPortion.LEFT) {
playerImpl.onFastRewind(); player.fastRewind();
} else if (portion == DisplayPortion.MIDDLE) { } else if (portion == DisplayPortion.MIDDLE) {
playerImpl.onPlayPause(); player.playPause();
} else if (portion == DisplayPortion.RIGHT) { } else if (portion == DisplayPortion.RIGHT) {
playerImpl.onFastForward(); player.fastForward();
} }
} }
@Override @Override
public void onSingleTap(@NotNull final MainPlayer.PlayerType playerType) { public void onSingleTap(@NotNull final MainPlayer.PlayerType playerType) {
if (DEBUG) { if (DEBUG) {
Log.d(TAG, "onSingleTap called with playerType = [" Log.d(TAG, "onSingleTap called with playerType = [" + player.getPlayerType() + "]");
+ playerImpl.getPlayerType() + "]");
} }
if (playerType == MainPlayer.PlayerType.POPUP) { if (playerType == MainPlayer.PlayerType.POPUP) {
if (playerImpl.isControlsVisible()) { if (player.isControlsVisible()) {
playerImpl.hideControls(100, 100); player.hideControls(100, 100);
} else { } else {
playerImpl.getPlayPauseButton().requestFocus(); player.getPlayPauseButton().requestFocus();
playerImpl.showControlsThenHide(); player.showControlsThenHide();
} }
} else /* playerType == MainPlayer.PlayerType.VIDEO */ { } else /* playerType == MainPlayer.PlayerType.VIDEO */ {
if (playerImpl.isControlsVisible()) { if (player.isControlsVisible()) {
playerImpl.hideControls(150, 0); player.hideControls(150, 0);
} else { } else {
if (playerImpl.getCurrentState() == BasePlayer.STATE_COMPLETED) { if (player.getCurrentState() == Player.STATE_COMPLETED) {
playerImpl.showControls(0); player.showControls(0);
} else { } else {
playerImpl.showControlsThenHide(); player.showControlsThenHide();
} }
} }
} }
@ -101,8 +99,7 @@ public class PlayerGestureListener
final float distanceX, final float distanceY) { final float distanceX, final float distanceY) {
if (DEBUG) { if (DEBUG) {
Log.d(TAG, "onScroll called with playerType = [" Log.d(TAG, "onScroll called with playerType = ["
+ playerImpl.getPlayerType() + "], portion = [" + player.getPlayerType() + "], portion = [" + portion + "]");
+ portion + "]");
} }
if (playerType == MainPlayer.PlayerType.VIDEO) { if (playerType == MainPlayer.PlayerType.VIDEO) {
final boolean isBrightnessGestureEnabled = final boolean isBrightnessGestureEnabled =
@ -123,8 +120,8 @@ public class PlayerGestureListener
} }
} else /* MainPlayer.PlayerType.POPUP */ { } else /* MainPlayer.PlayerType.POPUP */ {
final View closingOverlayView = playerImpl.getClosingOverlayView(); final View closingOverlayView = player.getClosingOverlayView();
if (playerImpl.isInsideClosingRadius(movingEvent)) { if (player.isInsideClosingRadius(movingEvent)) {
if (closingOverlayView.getVisibility() == View.GONE) { if (closingOverlayView.getVisibility() == View.GONE) {
animateView(closingOverlayView, true, 250); animateView(closingOverlayView, true, 250);
} }
@ -137,17 +134,17 @@ public class PlayerGestureListener
} }
private void onScrollMainVolume(final float distanceX, final float distanceY) { private void onScrollMainVolume(final float distanceX, final float distanceY) {
playerImpl.getVolumeProgressBar().incrementProgressBy((int) distanceY); player.getVolumeProgressBar().incrementProgressBy((int) distanceY);
final float currentProgressPercent = (float) playerImpl final float currentProgressPercent = (float) player
.getVolumeProgressBar().getProgress() / playerImpl.getMaxGestureLength(); .getVolumeProgressBar().getProgress() / player.getMaxGestureLength();
final int currentVolume = (int) (maxVolume * currentProgressPercent); final int currentVolume = (int) (maxVolume * currentProgressPercent);
playerImpl.getAudioReactor().setVolume(currentVolume); player.getAudioReactor().setVolume(currentVolume);
if (DEBUG) { if (DEBUG) {
Log.d(TAG, "onScroll().volumeControl, currentVolume = " + currentVolume); Log.d(TAG, "onScroll().volumeControl, currentVolume = " + currentVolume);
} }
playerImpl.getVolumeImageView().setImageDrawable( player.getVolumeImageView().setImageDrawable(
AppCompatResources.getDrawable(service, currentProgressPercent <= 0 AppCompatResources.getDrawable(service, currentProgressPercent <= 0
? R.drawable.ic_volume_off_white_24dp ? R.drawable.ic_volume_off_white_24dp
: currentProgressPercent < 0.25 ? R.drawable.ic_volume_mute_white_24dp : currentProgressPercent < 0.25 ? R.drawable.ic_volume_mute_white_24dp
@ -155,23 +152,23 @@ public class PlayerGestureListener
: R.drawable.ic_volume_up_white_24dp) : R.drawable.ic_volume_up_white_24dp)
); );
if (playerImpl.getVolumeRelativeLayout().getVisibility() != View.VISIBLE) { if (player.getVolumeRelativeLayout().getVisibility() != View.VISIBLE) {
animateView(playerImpl.getVolumeRelativeLayout(), SCALE_AND_ALPHA, true, 200); animateView(player.getVolumeRelativeLayout(), SCALE_AND_ALPHA, true, 200);
} }
if (playerImpl.getBrightnessRelativeLayout().getVisibility() == View.VISIBLE) { if (player.getBrightnessRelativeLayout().getVisibility() == View.VISIBLE) {
playerImpl.getBrightnessRelativeLayout().setVisibility(View.GONE); player.getBrightnessRelativeLayout().setVisibility(View.GONE);
} }
} }
private void onScrollMainBrightness(final float distanceX, final float distanceY) { private void onScrollMainBrightness(final float distanceX, final float distanceY) {
final Activity parent = playerImpl.getParentActivity(); final Activity parent = player.getParentActivity();
if (parent == null) { if (parent == null) {
return; return;
} }
final Window window = parent.getWindow(); final Window window = parent.getWindow();
final WindowManager.LayoutParams layoutParams = window.getAttributes(); final WindowManager.LayoutParams layoutParams = window.getAttributes();
final ProgressBar bar = playerImpl.getBrightnessProgressBar(); final ProgressBar bar = player.getBrightnessProgressBar();
final float oldBrightness = layoutParams.screenBrightness; final float oldBrightness = layoutParams.screenBrightness;
bar.setProgress((int) (bar.getMax() * Math.max(0, Math.min(1, oldBrightness)))); bar.setProgress((int) (bar.getMax() * Math.max(0, Math.min(1, oldBrightness))));
bar.incrementProgressBy((int) distanceY); bar.incrementProgressBy((int) distanceY);
@ -188,7 +185,7 @@ public class PlayerGestureListener
+ "currentBrightness = " + currentProgressPercent); + "currentBrightness = " + currentProgressPercent);
} }
playerImpl.getBrightnessImageView().setImageDrawable( player.getBrightnessImageView().setImageDrawable(
AppCompatResources.getDrawable(service, AppCompatResources.getDrawable(service,
currentProgressPercent < 0.25 currentProgressPercent < 0.25
? R.drawable.ic_brightness_low_white_24dp ? R.drawable.ic_brightness_low_white_24dp
@ -197,11 +194,11 @@ public class PlayerGestureListener
: R.drawable.ic_brightness_high_white_24dp) : R.drawable.ic_brightness_high_white_24dp)
); );
if (playerImpl.getBrightnessRelativeLayout().getVisibility() != View.VISIBLE) { if (player.getBrightnessRelativeLayout().getVisibility() != View.VISIBLE) {
animateView(playerImpl.getBrightnessRelativeLayout(), SCALE_AND_ALPHA, true, 200); animateView(player.getBrightnessRelativeLayout(), SCALE_AND_ALPHA, true, 200);
} }
if (playerImpl.getVolumeRelativeLayout().getVisibility() == View.VISIBLE) { if (player.getVolumeRelativeLayout().getVisibility() == View.VISIBLE) {
playerImpl.getVolumeRelativeLayout().setVisibility(View.GONE); player.getVolumeRelativeLayout().setVisibility(View.GONE);
} }
} }
@ -210,40 +207,40 @@ public class PlayerGestureListener
@NotNull final MotionEvent event) { @NotNull final MotionEvent event) {
if (DEBUG) { if (DEBUG) {
Log.d(TAG, "onScrollEnd called with playerType = [" Log.d(TAG, "onScrollEnd called with playerType = ["
+ playerImpl.getPlayerType() + "]"); + player.getPlayerType() + "]");
} }
if (playerType == MainPlayer.PlayerType.VIDEO) { if (playerType == MainPlayer.PlayerType.VIDEO) {
if (DEBUG) { if (DEBUG) {
Log.d(TAG, "onScrollEnd() called"); Log.d(TAG, "onScrollEnd() called");
} }
if (playerImpl.getVolumeRelativeLayout().getVisibility() == View.VISIBLE) { if (player.getVolumeRelativeLayout().getVisibility() == View.VISIBLE) {
animateView(playerImpl.getVolumeRelativeLayout(), SCALE_AND_ALPHA, animateView(player.getVolumeRelativeLayout(), SCALE_AND_ALPHA,
false, 200, 200); false, 200, 200);
} }
if (playerImpl.getBrightnessRelativeLayout().getVisibility() == View.VISIBLE) { if (player.getBrightnessRelativeLayout().getVisibility() == View.VISIBLE) {
animateView(playerImpl.getBrightnessRelativeLayout(), SCALE_AND_ALPHA, animateView(player.getBrightnessRelativeLayout(), SCALE_AND_ALPHA,
false, 200, 200); false, 200, 200);
} }
if (playerImpl.isControlsVisible() && playerImpl.getCurrentState() == STATE_PLAYING) { if (player.isControlsVisible() && player.getCurrentState() == STATE_PLAYING) {
playerImpl.hideControls(DEFAULT_CONTROLS_DURATION, DEFAULT_CONTROLS_HIDE_TIME); player.hideControls(DEFAULT_CONTROLS_DURATION, DEFAULT_CONTROLS_HIDE_TIME);
} }
} else { } else {
if (playerImpl == null) { if (player == null) {
return; return;
} }
if (playerImpl.isControlsVisible() && playerImpl.getCurrentState() == STATE_PLAYING) { if (player.isControlsVisible() && player.getCurrentState() == STATE_PLAYING) {
playerImpl.hideControls(DEFAULT_CONTROLS_DURATION, DEFAULT_CONTROLS_HIDE_TIME); player.hideControls(DEFAULT_CONTROLS_DURATION, DEFAULT_CONTROLS_HIDE_TIME);
} }
if (playerImpl.isInsideClosingRadius(event)) { if (player.isInsideClosingRadius(event)) {
playerImpl.closePopup(); player.closePopup();
} else { } else {
animateView(playerImpl.getClosingOverlayView(), false, 0); animateView(player.getClosingOverlayView(), false, 0);
if (!playerImpl.isPopupClosing) { if (!player.isPopupClosing()) {
animateView(playerImpl.getCloseOverlayButton(), false, 200); animateView(player.getCloseOverlayButton(), false, 200);
} }
} }
} }
@ -254,12 +251,12 @@ public class PlayerGestureListener
if (DEBUG) { if (DEBUG) {
Log.d(TAG, "onPopupResizingStart called"); Log.d(TAG, "onPopupResizingStart called");
} }
playerImpl.showAndAnimateControl(-1, true); player.showAndAnimateControl(-1, true);
playerImpl.getLoadingPanel().setVisibility(View.GONE); player.getLoadingPanel().setVisibility(View.GONE);
playerImpl.hideControls(0, 0); player.hideControls(0, 0);
animateView(playerImpl.getCurrentDisplaySeek(), false, 0, 0); animateView(player.getCurrentDisplaySeek(), false, 0, 0);
animateView(playerImpl.getResizingIndicator(), true, 200, 0); animateView(player.getResizingIndicator(), true, 200, 0);
} }
@Override @Override
@ -267,7 +264,7 @@ public class PlayerGestureListener
if (DEBUG) { if (DEBUG) {
Log.d(TAG, "onPopupResizingEnd called"); Log.d(TAG, "onPopupResizingEnd called");
} }
animateView(playerImpl.getResizingIndicator(), false, 100, 0); animateView(player.getResizingIndicator(), false, 100, 0);
} }
} }

View File

@ -1,10 +1,10 @@
package org.schabi.newpipe.player.event; package org.schabi.newpipe.player.event;
import org.schabi.newpipe.player.MainPlayer; import org.schabi.newpipe.player.MainPlayer;
import org.schabi.newpipe.player.VideoPlayerImpl; import org.schabi.newpipe.player.Player;
public interface PlayerServiceExtendedEventListener extends PlayerServiceEventListener { public interface PlayerServiceExtendedEventListener extends PlayerServiceEventListener {
void onServiceConnected(VideoPlayerImpl player, void onServiceConnected(Player player,
MainPlayer playerService, MainPlayer playerService,
boolean playAfterConnect); boolean playAfterConnect);
void onServiceDisconnected(); void onServiceDisconnected();

View File

@ -5,14 +5,14 @@ import android.animation.AnimatorListenerAdapter;
import android.animation.ValueAnimator; import android.animation.ValueAnimator;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.media.AudioFocusRequest;
import android.media.AudioManager; import android.media.AudioManager;
import android.media.audiofx.AudioEffect; import android.media.audiofx.AudioEffect;
import android.os.Build;
import android.util.Log; import android.util.Log;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.core.content.ContextCompat; import androidx.core.content.ContextCompat;
import androidx.media.AudioFocusRequestCompat;
import androidx.media.AudioManagerCompat;
import com.google.android.exoplayer2.SimpleExoPlayer; import com.google.android.exoplayer2.SimpleExoPlayer;
import com.google.android.exoplayer2.analytics.AnalyticsListener; import com.google.android.exoplayer2.analytics.AnalyticsListener;
@ -21,20 +21,17 @@ public class AudioReactor implements AudioManager.OnAudioFocusChangeListener, An
private static final String TAG = "AudioFocusReactor"; private static final String TAG = "AudioFocusReactor";
private static final boolean SHOULD_BUILD_FOCUS_REQUEST =
Build.VERSION.SDK_INT >= Build.VERSION_CODES.O;
private static final int DUCK_DURATION = 1500; private static final int DUCK_DURATION = 1500;
private static final float DUCK_AUDIO_TO = .2f; private static final float DUCK_AUDIO_TO = .2f;
private static final int FOCUS_GAIN_TYPE = AudioManager.AUDIOFOCUS_GAIN; private static final int FOCUS_GAIN_TYPE = AudioManagerCompat.AUDIOFOCUS_GAIN;
private static final int STREAM_TYPE = AudioManager.STREAM_MUSIC; private static final int STREAM_TYPE = AudioManager.STREAM_MUSIC;
private final SimpleExoPlayer player; private final SimpleExoPlayer player;
private final Context context; private final Context context;
private final AudioManager audioManager; private final AudioManager audioManager;
private final AudioFocusRequest request; private final AudioFocusRequestCompat request;
public AudioReactor(@NonNull final Context context, public AudioReactor(@NonNull final Context context,
@NonNull final SimpleExoPlayer player) { @NonNull final SimpleExoPlayer player) {
@ -43,15 +40,11 @@ public class AudioReactor implements AudioManager.OnAudioFocusChangeListener, An
this.audioManager = ContextCompat.getSystemService(context, AudioManager.class); this.audioManager = ContextCompat.getSystemService(context, AudioManager.class);
player.addAnalyticsListener(this); player.addAnalyticsListener(this);
if (SHOULD_BUILD_FOCUS_REQUEST) { request = new AudioFocusRequestCompat.Builder(FOCUS_GAIN_TYPE)
request = new AudioFocusRequest.Builder(FOCUS_GAIN_TYPE) //.setAcceptsDelayedFocusGain(true)
.setAcceptsDelayedFocusGain(true)
.setWillPauseWhenDucked(true) .setWillPauseWhenDucked(true)
.setOnAudioFocusChangeListener(this) .setOnAudioFocusChangeListener(this)
.build(); .build();
} else {
request = null;
}
} }
public void dispose() { public void dispose() {
@ -64,19 +57,11 @@ public class AudioReactor implements AudioManager.OnAudioFocusChangeListener, An
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/
public void requestAudioFocus() { public void requestAudioFocus() {
if (SHOULD_BUILD_FOCUS_REQUEST) { AudioManagerCompat.requestAudioFocus(audioManager, request);
audioManager.requestAudioFocus(request);
} else {
audioManager.requestAudioFocus(this, STREAM_TYPE, FOCUS_GAIN_TYPE);
}
} }
public void abandonAudioFocus() { public void abandonAudioFocus() {
if (SHOULD_BUILD_FOCUS_REQUEST) { AudioManagerCompat.abandonAudioFocusRequest(audioManager, request);
audioManager.abandonAudioFocusRequest(request);
} else {
audioManager.abandonAudioFocus(this);
}
} }
public int getVolume() { public int getVolume() {
@ -88,7 +73,7 @@ public class AudioReactor implements AudioManager.OnAudioFocusChangeListener, An
} }
public int getMaxVolume() { public int getMaxVolume() {
return audioManager.getStreamMaxVolume(STREAM_TYPE); return AudioManagerCompat.getStreamMaxVolume(audioManager, STREAM_TYPE);
} }
/*////////////////////////////////////////////////////////////////////////// /*//////////////////////////////////////////////////////////////////////////

View File

@ -26,12 +26,12 @@ public class LoadController implements LoadControl {
} }
private LoadController(final int initialPlaybackBufferMs, private LoadController(final int initialPlaybackBufferMs,
final int minimumPlaybackbufferMs, final int minimumPlaybackBufferMs,
final int optimalPlaybackBufferMs) { final int optimalPlaybackBufferMs) {
this.initialPlaybackBufferUs = initialPlaybackBufferMs * 1000; this.initialPlaybackBufferUs = initialPlaybackBufferMs * 1000;
final DefaultLoadControl.Builder builder = new DefaultLoadControl.Builder(); final DefaultLoadControl.Builder builder = new DefaultLoadControl.Builder();
builder.setBufferDurationsMs(minimumPlaybackbufferMs, optimalPlaybackBufferMs, builder.setBufferDurationsMs(minimumPlaybackBufferMs, optimalPlaybackBufferMs,
initialPlaybackBufferMs, initialPlaybackBufferMs); initialPlaybackBufferMs, initialPlaybackBufferMs);
internalLoadControl = builder.createDefaultLoadControl(); internalLoadControl = builder.createDefaultLoadControl();
} }

View File

@ -18,7 +18,7 @@ import androidx.fragment.app.DialogFragment;
import org.schabi.newpipe.R; import org.schabi.newpipe.R;
import org.schabi.newpipe.util.SliderStrategy; import org.schabi.newpipe.util.SliderStrategy;
import static org.schabi.newpipe.player.BasePlayer.DEBUG; import static org.schabi.newpipe.player.Player.DEBUG;
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage; import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
public class PlaybackParameterDialog extends DialogFragment { public class PlaybackParameterDialog extends DialogFragment {

View File

@ -1,8 +1,15 @@
package org.schabi.newpipe.player.helper; package org.schabi.newpipe.player.helper;
import android.annotation.SuppressLint;
import android.content.Context; import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.graphics.PixelFormat;
import android.os.Build;
import android.provider.Settings; import android.provider.Settings;
import android.view.Gravity;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.view.accessibility.CaptioningManager; import android.view.accessibility.CaptioningManager;
import androidx.annotation.IntDef; import androidx.annotation.IntDef;
@ -11,11 +18,14 @@ import androidx.annotation.Nullable;
import androidx.core.content.ContextCompat; import androidx.core.content.ContextCompat;
import androidx.preference.PreferenceManager; import androidx.preference.PreferenceManager;
import com.google.android.exoplayer2.PlaybackParameters;
import com.google.android.exoplayer2.Player.RepeatMode;
import com.google.android.exoplayer2.SeekParameters; import com.google.android.exoplayer2.SeekParameters;
import com.google.android.exoplayer2.text.CaptionStyleCompat; import com.google.android.exoplayer2.text.CaptionStyleCompat;
import com.google.android.exoplayer2.trackselection.AdaptiveTrackSelection; import com.google.android.exoplayer2.trackselection.AdaptiveTrackSelection;
import com.google.android.exoplayer2.trackselection.TrackSelection; import com.google.android.exoplayer2.trackselection.TrackSelection;
import com.google.android.exoplayer2.ui.AspectRatioFrameLayout; import com.google.android.exoplayer2.ui.AspectRatioFrameLayout;
import com.google.android.exoplayer2.ui.AspectRatioFrameLayout.ResizeMode;
import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.MimeTypes;
import org.schabi.newpipe.R; import org.schabi.newpipe.R;
@ -27,6 +37,8 @@ import org.schabi.newpipe.extractor.stream.StreamInfoItem;
import org.schabi.newpipe.extractor.stream.SubtitlesStream; import org.schabi.newpipe.extractor.stream.SubtitlesStream;
import org.schabi.newpipe.extractor.stream.VideoStream; import org.schabi.newpipe.extractor.stream.VideoStream;
import org.schabi.newpipe.extractor.utils.Utils; import org.schabi.newpipe.extractor.utils.Utils;
import org.schabi.newpipe.player.MainPlayer;
import org.schabi.newpipe.player.Player;
import org.schabi.newpipe.player.playqueue.PlayQueue; import org.schabi.newpipe.player.playqueue.PlayQueue;
import org.schabi.newpipe.player.playqueue.PlayQueueItem; import org.schabi.newpipe.player.playqueue.PlayQueueItem;
import org.schabi.newpipe.player.playqueue.SinglePlayQueue; import org.schabi.newpipe.player.playqueue.SinglePlayQueue;
@ -41,13 +53,16 @@ import java.util.Formatter;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.Objects;
import java.util.Set; import java.util.Set;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import static com.google.android.exoplayer2.ui.AspectRatioFrameLayout.RESIZE_MODE_FILL; import static com.google.android.exoplayer2.Player.REPEAT_MODE_ALL;
import static com.google.android.exoplayer2.ui.AspectRatioFrameLayout.RESIZE_MODE_FIT; import static com.google.android.exoplayer2.Player.REPEAT_MODE_OFF;
import static com.google.android.exoplayer2.ui.AspectRatioFrameLayout.RESIZE_MODE_ZOOM; import static com.google.android.exoplayer2.Player.REPEAT_MODE_ONE;
import static java.lang.annotation.RetentionPolicy.SOURCE; import static java.lang.annotation.RetentionPolicy.SOURCE;
import static org.schabi.newpipe.player.Player.IDLE_WINDOW_FLAGS;
import static org.schabi.newpipe.player.Player.PLAYER_TYPE;
import static org.schabi.newpipe.player.helper.PlayerHelper.AutoplayType.AUTOPLAY_TYPE_ALWAYS; import static org.schabi.newpipe.player.helper.PlayerHelper.AutoplayType.AUTOPLAY_TYPE_ALWAYS;
import static org.schabi.newpipe.player.helper.PlayerHelper.AutoplayType.AUTOPLAY_TYPE_NEVER; import static org.schabi.newpipe.player.helper.PlayerHelper.AutoplayType.AUTOPLAY_TYPE_NEVER;
import static org.schabi.newpipe.player.helper.PlayerHelper.AutoplayType.AUTOPLAY_TYPE_WIFI; import static org.schabi.newpipe.player.helper.PlayerHelper.AutoplayType.AUTOPLAY_TYPE_WIFI;
@ -71,6 +86,15 @@ public final class PlayerHelper {
int AUTOPLAY_TYPE_NEVER = 2; int AUTOPLAY_TYPE_NEVER = 2;
} }
@Retention(SOURCE)
@IntDef({MINIMIZE_ON_EXIT_MODE_NONE, MINIMIZE_ON_EXIT_MODE_BACKGROUND,
MINIMIZE_ON_EXIT_MODE_POPUP})
public @interface MinimizeMode {
int MINIMIZE_ON_EXIT_MODE_NONE = 0;
int MINIMIZE_ON_EXIT_MODE_BACKGROUND = 1;
int MINIMIZE_ON_EXIT_MODE_POPUP = 2;
}
private PlayerHelper() { } private PlayerHelper() { }
//////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////
@ -121,14 +145,16 @@ public final class PlayerHelper {
@NonNull @NonNull
public static String resizeTypeOf(@NonNull final Context context, public static String resizeTypeOf(@NonNull final Context context,
@AspectRatioFrameLayout.ResizeMode final int resizeMode) { @ResizeMode final int resizeMode) {
switch (resizeMode) { switch (resizeMode) {
case RESIZE_MODE_FIT: case AspectRatioFrameLayout.RESIZE_MODE_FIT:
return context.getResources().getString(R.string.resize_fit); return context.getResources().getString(R.string.resize_fit);
case RESIZE_MODE_FILL: case AspectRatioFrameLayout.RESIZE_MODE_FILL:
return context.getResources().getString(R.string.resize_fill); return context.getResources().getString(R.string.resize_fill);
case RESIZE_MODE_ZOOM: case AspectRatioFrameLayout.RESIZE_MODE_ZOOM:
return context.getResources().getString(R.string.resize_zoom); return context.getResources().getString(R.string.resize_zoom);
case AspectRatioFrameLayout.RESIZE_MODE_FIXED_HEIGHT:
case AspectRatioFrameLayout.RESIZE_MODE_FIXED_WIDTH:
default: default:
throw new IllegalArgumentException("Unrecognized resize mode: " + resizeMode); throw new IllegalArgumentException("Unrecognized resize mode: " + resizeMode);
} }
@ -199,23 +225,23 @@ public final class PlayerHelper {
//////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////
public static boolean isResumeAfterAudioFocusGain(@NonNull final Context context) { public static boolean isResumeAfterAudioFocusGain(@NonNull final Context context) {
return isResumeAfterAudioFocusGain(context, false); return getPreferences(context)
.getBoolean(context.getString(R.string.resume_on_audio_focus_gain_key), false);
} }
public static boolean isVolumeGestureEnabled(@NonNull final Context context) { public static boolean isVolumeGestureEnabled(@NonNull final Context context) {
return isVolumeGestureEnabled(context, true); return getPreferences(context)
.getBoolean(context.getString(R.string.volume_gesture_control_key), true);
} }
public static boolean isBrightnessGestureEnabled(@NonNull final Context context) { public static boolean isBrightnessGestureEnabled(@NonNull final Context context) {
return isBrightnessGestureEnabled(context, true); return getPreferences(context)
} .getBoolean(context.getString(R.string.brightness_gesture_control_key), true);
public static boolean isRememberingPopupDimensions(@NonNull final Context context) {
return isRememberingPopupDimensions(context, true);
} }
public static boolean isAutoQueueEnabled(@NonNull final Context context) { public static boolean isAutoQueueEnabled(@NonNull final Context context) {
return isAutoQueueEnabled(context, false); return getPreferences(context)
.getBoolean(context.getString(R.string.auto_queue_key), false);
} }
public static boolean isClearingQueueConfirmationRequired(@NonNull final Context context) { public static boolean isClearingQueueConfirmationRequired(@NonNull final Context context) {
@ -229,7 +255,8 @@ public final class PlayerHelper {
final String popupAction = context.getString(R.string.minimize_on_exit_popup_key); final String popupAction = context.getString(R.string.minimize_on_exit_popup_key);
final String backgroundAction = context.getString(R.string.minimize_on_exit_background_key); final String backgroundAction = context.getString(R.string.minimize_on_exit_background_key);
final String action = getMinimizeOnExitAction(context, defaultAction); final String action = getPreferences(context)
.getString(context.getString(R.string.minimize_on_exit_key), defaultAction);
if (action.equals(popupAction)) { if (action.equals(popupAction)) {
return MINIMIZE_ON_EXIT_MODE_POPUP; return MINIMIZE_ON_EXIT_MODE_POPUP;
} else if (action.equals(backgroundAction)) { } else if (action.equals(backgroundAction)) {
@ -239,9 +266,23 @@ public final class PlayerHelper {
} }
} }
public static boolean isMinimizeOnExitToPopup(@NonNull final Context context) {
return getMinimizeOnExitAction(context) == MINIMIZE_ON_EXIT_MODE_POPUP;
}
public static boolean isMinimizeOnExitToBackground(@NonNull final Context context) {
return getMinimizeOnExitAction(context) == MINIMIZE_ON_EXIT_MODE_BACKGROUND;
}
public static boolean isMinimizeOnExitDisabled(@NonNull final Context context) {
return getMinimizeOnExitAction(context) == MINIMIZE_ON_EXIT_MODE_NONE;
}
@AutoplayType @AutoplayType
public static int getAutoplayType(@NonNull final Context context) { public static int getAutoplayType(@NonNull final Context context) {
final String type = getAutoplayType(context, context.getString(R.string.autoplay_wifi_key)); final String type = getPreferences(context).getString(
context.getString(R.string.autoplay_key),
context.getString(R.string.autoplay_wifi_key));
if (type.equals(context.getString(R.string.autoplay_always_key))) { if (type.equals(context.getString(R.string.autoplay_always_key))) {
return AUTOPLAY_TYPE_ALWAYS; return AUTOPLAY_TYPE_ALWAYS;
} else if (type.equals(context.getString(R.string.autoplay_never_key))) { } else if (type.equals(context.getString(R.string.autoplay_never_key))) {
@ -350,14 +391,32 @@ public final class PlayerHelper {
return captioningManager.getFontScale(); return captioningManager.getFontScale();
} }
/**
* @param context the Android context
* @return the screen brightness to use. A value less than 0 (the default) means to use the
* preferred screen brightness
*/
public static float getScreenBrightness(@NonNull final Context context) { public static float getScreenBrightness(@NonNull final Context context) {
//a value of less than 0, the default, means to use the preferred screen brightness final SharedPreferences sp = getPreferences(context);
return getScreenBrightness(context, -1); final long timestamp =
sp.getLong(context.getString(R.string.screen_brightness_timestamp_key), 0);
// Hypothesis: 4h covers a viewing block, e.g. evening.
// External lightning conditions will change in the next
// viewing block so we fall back to the default brightness
if ((System.currentTimeMillis() - timestamp) > TimeUnit.HOURS.toMillis(4)) {
return -1;
} else {
return sp.getFloat(context.getString(R.string.screen_brightness_key), -1);
}
} }
public static void setScreenBrightness(@NonNull final Context context, public static void setScreenBrightness(@NonNull final Context context,
final float setScreenBrightness) { final float screenBrightness) {
setScreenBrightness(context, setScreenBrightness, System.currentTimeMillis()); getPreferences(context).edit()
.putFloat(context.getString(R.string.screen_brightness_key), screenBrightness)
.putLong(context.getString(R.string.screen_brightness_timestamp_key),
System.currentTimeMillis())
.apply();
} }
public static boolean globalScreenOrientationLocked(final Context context) { public static boolean globalScreenOrientationLocked(final Context context) {
@ -376,75 +435,11 @@ public final class PlayerHelper {
return PreferenceManager.getDefaultSharedPreferences(context); return PreferenceManager.getDefaultSharedPreferences(context);
} }
private static boolean isResumeAfterAudioFocusGain(@NonNull final Context context,
final boolean b) {
return getPreferences(context)
.getBoolean(context.getString(R.string.resume_on_audio_focus_gain_key), b);
}
private static boolean isVolumeGestureEnabled(@NonNull final Context context,
final boolean b) {
return getPreferences(context)
.getBoolean(context.getString(R.string.volume_gesture_control_key), b);
}
private static boolean isBrightnessGestureEnabled(@NonNull final Context context,
final boolean b) {
return getPreferences(context)
.getBoolean(context.getString(R.string.brightness_gesture_control_key), b);
}
private static boolean isRememberingPopupDimensions(@NonNull final Context context,
final boolean b) {
return getPreferences(context)
.getBoolean(context.getString(R.string.popup_remember_size_pos_key), b);
}
private static boolean isUsingInexactSeek(@NonNull final Context context) { private static boolean isUsingInexactSeek(@NonNull final Context context) {
return getPreferences(context) return getPreferences(context)
.getBoolean(context.getString(R.string.use_inexact_seek_key), false); .getBoolean(context.getString(R.string.use_inexact_seek_key), false);
} }
private static boolean isAutoQueueEnabled(@NonNull final Context context, final boolean b) {
return getPreferences(context).getBoolean(context.getString(R.string.auto_queue_key), b);
}
private static void setScreenBrightness(@NonNull final Context context,
final float screenBrightness, final long timestamp) {
final SharedPreferences.Editor editor = getPreferences(context).edit();
editor.putFloat(context.getString(R.string.screen_brightness_key), screenBrightness);
editor.putLong(context.getString(R.string.screen_brightness_timestamp_key), timestamp);
editor.apply();
}
private static float getScreenBrightness(@NonNull final Context context,
final float screenBrightness) {
final SharedPreferences sp = getPreferences(context);
final long timestamp = sp
.getLong(context.getString(R.string.screen_brightness_timestamp_key), 0);
// Hypothesis: 4h covers a viewing block, e.g. evening.
// External lightning conditions will change in the next
// viewing block so we fall back to the default brightness
if ((System.currentTimeMillis() - timestamp) > TimeUnit.HOURS.toMillis(4)) {
return screenBrightness;
} else {
return sp
.getFloat(context.getString(R.string.screen_brightness_key), screenBrightness);
}
}
private static String getMinimizeOnExitAction(@NonNull final Context context,
final String key) {
return getPreferences(context)
.getString(context.getString(R.string.minimize_on_exit_key), key);
}
private static String getAutoplayType(@NonNull final Context context,
final String key) {
return getPreferences(context).getString(context.getString(R.string.autoplay_key),
key);
}
private static SinglePlayQueue getAutoQueuedSinglePlayQueue( private static SinglePlayQueue getAutoQueuedSinglePlayQueue(
final StreamInfoItem streamInfoItem) { final StreamInfoItem streamInfoItem) {
final SinglePlayQueue singlePlayQueue = new SinglePlayQueue(streamInfoItem); final SinglePlayQueue singlePlayQueue = new SinglePlayQueue(streamInfoItem);
@ -452,12 +447,168 @@ public final class PlayerHelper {
return singlePlayQueue; return singlePlayQueue;
} }
@Retention(SOURCE)
@IntDef({MINIMIZE_ON_EXIT_MODE_NONE, MINIMIZE_ON_EXIT_MODE_BACKGROUND, ////////////////////////////////////////////////////////////////////////////
MINIMIZE_ON_EXIT_MODE_POPUP}) // Utils used by player
public @interface MinimizeMode { ////////////////////////////////////////////////////////////////////////////
int MINIMIZE_ON_EXIT_MODE_NONE = 0;
int MINIMIZE_ON_EXIT_MODE_BACKGROUND = 1; public static MainPlayer.PlayerType retrievePlayerTypeFromIntent(final Intent intent) {
int MINIMIZE_ON_EXIT_MODE_POPUP = 2; // If you want to open popup from the app just include Constants.POPUP_ONLY into an extra
return MainPlayer.PlayerType.values()[
intent.getIntExtra(PLAYER_TYPE, MainPlayer.PlayerType.VIDEO.ordinal())];
}
public static boolean isPlaybackResumeEnabled(final Player player) {
return player.getPrefs().getBoolean(
player.getContext().getString(R.string.enable_watch_history_key), true)
&& player.getPrefs().getBoolean(
player.getContext().getString(R.string.enable_playback_resume_key), true);
}
@RepeatMode
public static int nextRepeatMode(@RepeatMode final int repeatMode) {
switch (repeatMode) {
case REPEAT_MODE_OFF:
return REPEAT_MODE_ONE;
case REPEAT_MODE_ONE:
return REPEAT_MODE_ALL;
case REPEAT_MODE_ALL: default:
return REPEAT_MODE_OFF;
}
}
@ResizeMode
public static int retrieveResizeModeFromPrefs(final Player player) {
return player.getPrefs().getInt(player.getContext().getString(R.string.last_resize_mode),
AspectRatioFrameLayout.RESIZE_MODE_FIT);
}
@SuppressLint("SwitchIntDef") // only fit, fill and zoom are supported by NewPipe
@ResizeMode
public static int nextResizeModeAndSaveToPrefs(final Player player,
@ResizeMode final int resizeMode) {
final int newResizeMode;
switch (resizeMode) {
case AspectRatioFrameLayout.RESIZE_MODE_FIT:
newResizeMode = AspectRatioFrameLayout.RESIZE_MODE_FILL;
break;
case AspectRatioFrameLayout.RESIZE_MODE_FILL:
newResizeMode = AspectRatioFrameLayout.RESIZE_MODE_ZOOM;
break;
case AspectRatioFrameLayout.RESIZE_MODE_ZOOM:
default:
newResizeMode = AspectRatioFrameLayout.RESIZE_MODE_FIT;
break;
}
player.getPrefs().edit().putInt(
player.getContext().getString(R.string.last_resize_mode), resizeMode).apply();
return newResizeMode;
}
public static PlaybackParameters retrievePlaybackParametersFromPrefs(final Player player) {
final float speed = player.getPrefs().getFloat(player.getContext().getString(
R.string.playback_speed_key), player.getPlaybackSpeed());
final float pitch = player.getPrefs().getFloat(player.getContext().getString(
R.string.playback_pitch_key), player.getPlaybackPitch());
final boolean skipSilence = player.getPrefs().getBoolean(player.getContext().getString(
R.string.playback_skip_silence_key), player.getPlaybackSkipSilence());
return new PlaybackParameters(speed, pitch, skipSilence);
}
public static void savePlaybackParametersToPrefs(final Player player,
final float speed,
final float pitch,
final boolean skipSilence) {
player.getPrefs().edit()
.putFloat(player.getContext().getString(R.string.playback_speed_key), speed)
.putFloat(player.getContext().getString(R.string.playback_pitch_key), pitch)
.putBoolean(player.getContext().getString(R.string.playback_skip_silence_key),
skipSilence)
.apply();
}
/**
* @param player {@code screenWidth} and {@code screenHeight} must have been initialized
* @return the popup starting layout params
*/
@SuppressLint("RtlHardcoded")
public static WindowManager.LayoutParams retrievePopupLayoutParamsFromPrefs(
final Player player) {
final boolean popupRememberSizeAndPos = player.getPrefs().getBoolean(
player.getContext().getString(R.string.popup_remember_size_pos_key), true);
final float defaultSize =
player.getContext().getResources().getDimension(R.dimen.popup_default_width);
final float popupWidth = popupRememberSizeAndPos
? player.getPrefs().getFloat(player.getContext().getString(
R.string.popup_saved_width_key), defaultSize)
: defaultSize;
final float popupHeight = getMinimumVideoHeight(popupWidth);
final WindowManager.LayoutParams popupLayoutParams = new WindowManager.LayoutParams(
(int) popupWidth, (int) popupHeight,
popupLayoutParamType(),
IDLE_WINDOW_FLAGS,
PixelFormat.TRANSLUCENT);
popupLayoutParams.gravity = Gravity.LEFT | Gravity.TOP;
popupLayoutParams.softInputMode = WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE;
final int centerX = (int) (player.getScreenWidth() / 2f - popupWidth / 2f);
final int centerY = (int) (player.getScreenHeight() / 2f - popupHeight / 2f);
popupLayoutParams.x = popupRememberSizeAndPos
? player.getPrefs().getInt(player.getContext().getString(
R.string.popup_saved_x_key), centerX) : centerX;
popupLayoutParams.y = popupRememberSizeAndPos
? player.getPrefs().getInt(player.getContext().getString(
R.string.popup_saved_y_key), centerY) : centerY;
return popupLayoutParams;
}
public static void savePopupPositionAndSizeToPrefs(final Player player) {
if (player.getPopupLayoutParams() != null) {
player.getPrefs().edit()
.putFloat(player.getContext().getString(R.string.popup_saved_width_key),
player.getPopupLayoutParams().width)
.putInt(player.getContext().getString(R.string.popup_saved_x_key),
player.getPopupLayoutParams().x)
.putInt(player.getContext().getString(R.string.popup_saved_y_key),
player.getPopupLayoutParams().y)
.apply();
}
}
public static float getMinimumVideoHeight(final float width) {
return width / (16.0f / 9.0f); // Respect the 16:9 ratio that most videos have
}
@SuppressLint("RtlHardcoded")
public static WindowManager.LayoutParams buildCloseOverlayLayoutParams() {
final int flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
| WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE
| WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM;
final WindowManager.LayoutParams closeOverlayLayoutParams = new WindowManager.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT,
popupLayoutParamType(),
flags,
PixelFormat.TRANSLUCENT);
closeOverlayLayoutParams.gravity = Gravity.LEFT | Gravity.TOP;
closeOverlayLayoutParams.softInputMode =
WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE;
return closeOverlayLayoutParams;
}
public static int popupLayoutParamType() {
return Build.VERSION.SDK_INT < Build.VERSION_CODES.O
? WindowManager.LayoutParams.TYPE_PHONE
: WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
}
public static int retrieveSeekDurationFromPreferences(final Player player) {
return Integer.parseInt(Objects.requireNonNull(player.getPrefs().getString(
player.getContext().getString(R.string.seek_duration_key),
player.getContext().getString(R.string.seek_duration_default_value))));
} }
} }

View File

@ -16,7 +16,7 @@ import org.schabi.newpipe.App;
import org.schabi.newpipe.MainActivity; import org.schabi.newpipe.MainActivity;
import org.schabi.newpipe.extractor.stream.StreamInfo; import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.player.MainPlayer; import org.schabi.newpipe.player.MainPlayer;
import org.schabi.newpipe.player.VideoPlayerImpl; import org.schabi.newpipe.player.Player;
import org.schabi.newpipe.player.event.PlayerServiceEventListener; import org.schabi.newpipe.player.event.PlayerServiceEventListener;
import org.schabi.newpipe.player.event.PlayerServiceExtendedEventListener; import org.schabi.newpipe.player.event.PlayerServiceExtendedEventListener;
import org.schabi.newpipe.player.playqueue.PlayQueue; import org.schabi.newpipe.player.playqueue.PlayQueue;
@ -33,7 +33,7 @@ public final class PlayerHolder {
private static ServiceConnection serviceConnection; private static ServiceConnection serviceConnection;
public static boolean bound; public static boolean bound;
private static MainPlayer playerService; private static MainPlayer playerService;
private static VideoPlayerImpl player; private static Player player;
/** /**
* Returns the current {@link MainPlayer.PlayerType} of the {@link MainPlayer} service, * Returns the current {@link MainPlayer.PlayerType} of the {@link MainPlayer} service,

View File

@ -3,11 +3,11 @@ package org.schabi.newpipe.player.mediasession;
import android.support.v4.media.MediaDescriptionCompat; import android.support.v4.media.MediaDescriptionCompat;
public interface MediaSessionCallback { public interface MediaSessionCallback {
void onSkipToPrevious(); void playPrevious();
void onSkipToNext(); void playNext();
void onSkipToIndex(int index); void playItemAtIndex(int index);
int getCurrentPlayingIndex(); int getCurrentPlayingIndex();
@ -15,7 +15,7 @@ public interface MediaSessionCallback {
MediaDescriptionCompat getQueueMetadata(int index); MediaDescriptionCompat getQueueMetadata(int index);
void onPlay(); void play();
void onPause(); void pause();
} }

View File

@ -65,18 +65,18 @@ public class PlayQueueNavigator implements MediaSessionConnector.QueueNavigator
@Override @Override
public void onSkipToPrevious(final Player player, final ControlDispatcher controlDispatcher) { public void onSkipToPrevious(final Player player, final ControlDispatcher controlDispatcher) {
callback.onSkipToPrevious(); callback.playPrevious();
} }
@Override @Override
public void onSkipToQueueItem(final Player player, final ControlDispatcher controlDispatcher, public void onSkipToQueueItem(final Player player, final ControlDispatcher controlDispatcher,
final long id) { final long id) {
callback.onSkipToIndex((int) id); callback.playItemAtIndex((int) id);
} }
@Override @Override
public void onSkipToNext(final Player player, final ControlDispatcher controlDispatcher) { public void onSkipToNext(final Player player, final ControlDispatcher controlDispatcher) {
callback.onSkipToNext(); callback.playNext();
} }
private void publishFloatingQueueWindow() { private void publishFloatingQueueWindow() {

View File

@ -14,9 +14,9 @@ public class PlayQueuePlaybackController extends DefaultControlDispatcher {
@Override @Override
public boolean dispatchSetPlayWhenReady(final Player player, final boolean playWhenReady) { public boolean dispatchSetPlayWhenReady(final Player player, final boolean playWhenReady) {
if (playWhenReady) { if (playWhenReady) {
callback.onPlay(); callback.play();
} else { } else {
callback.onPause(); callback.pause();
} }
return true; return true;
} }

View File

@ -5,33 +5,33 @@ import android.os.Bundle;
import android.support.v4.media.MediaDescriptionCompat; import android.support.v4.media.MediaDescriptionCompat;
import android.support.v4.media.MediaMetadataCompat; import android.support.v4.media.MediaMetadataCompat;
import org.schabi.newpipe.player.BasePlayer; import org.schabi.newpipe.player.Player;
import org.schabi.newpipe.player.mediasession.MediaSessionCallback; import org.schabi.newpipe.player.mediasession.MediaSessionCallback;
import org.schabi.newpipe.player.playqueue.PlayQueueItem; import org.schabi.newpipe.player.playqueue.PlayQueueItem;
public class BasePlayerMediaSession implements MediaSessionCallback { public class PlayerMediaSession implements MediaSessionCallback {
private final BasePlayer player; private final Player player;
public BasePlayerMediaSession(final BasePlayer player) { public PlayerMediaSession(final Player player) {
this.player = player; this.player = player;
} }
@Override @Override
public void onSkipToPrevious() { public void playPrevious() {
player.onPlayPrevious(); player.playPrevious();
} }
@Override @Override
public void onSkipToNext() { public void playNext() {
player.onPlayNext(); player.playNext();
} }
@Override @Override
public void onSkipToIndex(final int index) { public void playItemAtIndex(final int index) {
if (player.getPlayQueue() == null) { if (player.getPlayQueue() == null) {
return; return;
} }
player.onSelected(player.getPlayQueue().getItem(index)); player.selectQueueItem(player.getPlayQueue().getItem(index));
} }
@Override @Override
@ -52,11 +52,14 @@ public class BasePlayerMediaSession implements MediaSessionCallback {
@Override @Override
public MediaDescriptionCompat getQueueMetadata(final int index) { public MediaDescriptionCompat getQueueMetadata(final int index) {
if (player.getPlayQueue() == null || player.getPlayQueue().getItem(index) == null) { if (player.getPlayQueue() == null) {
return null;
}
final PlayQueueItem item = player.getPlayQueue().getItem(index);
if (item == null) {
return null; return null;
} }
final PlayQueueItem item = player.getPlayQueue().getItem(index);
final MediaDescriptionCompat.Builder descriptionBuilder final MediaDescriptionCompat.Builder descriptionBuilder
= new MediaDescriptionCompat.Builder() = new MediaDescriptionCompat.Builder()
.setMediaId(String.valueOf(index)) .setMediaId(String.valueOf(index))
@ -83,12 +86,12 @@ public class BasePlayerMediaSession implements MediaSessionCallback {
} }
@Override @Override
public void onPlay() { public void play() {
player.onPlay(); player.play();
} }
@Override @Override
public void onPause() { public void pause() {
player.onPause(); player.pause();
} }
} }

View File

@ -128,9 +128,9 @@ abstract class AbstractInfoPlayQueue<T extends ListInfo, U extends InfoItem> ext
fetchReactor = null; fetchReactor = null;
} }
private static List<PlayQueueItem> extractListItems(final List<StreamInfoItem> infos) { private static List<PlayQueueItem> extractListItems(final List<StreamInfoItem> infoItems) {
final List<PlayQueueItem> result = new ArrayList<>(); final List<PlayQueueItem> result = new ArrayList<>();
for (final InfoItem stream : infos) { for (final InfoItem stream : infoItems) {
if (stream instanceof StreamInfoItem) { if (stream instanceof StreamInfoItem) {
result.add(new PlayQueueItem((StreamInfoItem) stream)); result.add(new PlayQueueItem((StreamInfoItem) stream));
} }

View File

@ -14,15 +14,11 @@ import android.view.Menu;
import android.view.MenuInflater; import android.view.MenuInflater;
import android.view.MenuItem; import android.view.MenuItem;
import android.view.View; import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;
import android.widget.Toast; import android.widget.Toast;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.appcompat.app.ActionBar; import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.Toolbar;
import androidx.core.app.NavUtils; import androidx.core.app.NavUtils;
import com.google.android.material.snackbar.Snackbar; import com.google.android.material.snackbar.Snackbar;
@ -34,6 +30,7 @@ import org.schabi.newpipe.ActivityCommunicator;
import org.schabi.newpipe.BuildConfig; import org.schabi.newpipe.BuildConfig;
import org.schabi.newpipe.MainActivity; import org.schabi.newpipe.MainActivity;
import org.schabi.newpipe.R; import org.schabi.newpipe.R;
import org.schabi.newpipe.databinding.ActivityErrorBinding;
import org.schabi.newpipe.util.Localization; import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.util.ShareUtils; import org.schabi.newpipe.util.ShareUtils;
import org.schabi.newpipe.util.ThemeHelper; import org.schabi.newpipe.util.ThemeHelper;
@ -87,7 +84,8 @@ public class ErrorActivity extends AppCompatActivity {
private ErrorInfo errorInfo; private ErrorInfo errorInfo;
private Class returnActivity; private Class returnActivity;
private String currentTimeStamp; private String currentTimeStamp;
private EditText userCommentBox;
private ActivityErrorBinding activityErrorBinding;
public static void reportUiError(final AppCompatActivity activity, final Throwable el) { public static void reportUiError(final AppCompatActivity activity, final Throwable el) {
reportError(activity, el, activity.getClass(), null, ErrorInfo.make(UserAction.UI_ERROR, reportError(activity, el, activity.getClass(), null, ErrorInfo.make(UserAction.UI_ERROR,
@ -181,12 +179,13 @@ public class ErrorActivity extends AppCompatActivity {
assureCorrectAppLanguage(this); assureCorrectAppLanguage(this);
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
ThemeHelper.setTheme(this); ThemeHelper.setTheme(this);
setContentView(R.layout.activity_error);
activityErrorBinding = ActivityErrorBinding.inflate(getLayoutInflater());
setContentView(activityErrorBinding.getRoot());
final Intent intent = getIntent(); final Intent intent = getIntent();
final Toolbar toolbar = findViewById(R.id.toolbar); setSupportActionBar(activityErrorBinding.toolbarLayout.toolbar);
setSupportActionBar(toolbar);
final ActionBar actionBar = getSupportActionBar(); final ActionBar actionBar = getSupportActionBar();
if (actionBar != null) { if (actionBar != null) {
@ -195,15 +194,6 @@ public class ErrorActivity extends AppCompatActivity {
actionBar.setDisplayShowTitleEnabled(true); actionBar.setDisplayShowTitleEnabled(true);
} }
final Button reportEmailButton = findViewById(R.id.errorReportEmailButton);
final Button copyButton = findViewById(R.id.errorReportCopyButton);
final Button reportGithubButton = findViewById(R.id.errorReportGitHubButton);
userCommentBox = findViewById(R.id.errorCommentBox);
final TextView errorView = findViewById(R.id.errorView);
final TextView infoView = findViewById(R.id.errorInfosView);
final TextView errorMessageView = findViewById(R.id.errorMessageView);
final ActivityCommunicator ac = ActivityCommunicator.getCommunicator(); final ActivityCommunicator ac = ActivityCommunicator.getCommunicator();
returnActivity = ac.getReturnActivity(); returnActivity = ac.getReturnActivity();
errorInfo = intent.getParcelableExtra(ERROR_INFO); errorInfo = intent.getParcelableExtra(ERROR_INFO);
@ -213,28 +203,27 @@ public class ErrorActivity extends AppCompatActivity {
addGuruMeditation(); addGuruMeditation();
currentTimeStamp = getCurrentTimeStamp(); currentTimeStamp = getCurrentTimeStamp();
reportEmailButton.setOnClickListener(v -> activityErrorBinding.errorReportEmailButton.setOnClickListener(v ->
openPrivacyPolicyDialog(this, "EMAIL")); openPrivacyPolicyDialog(this, "EMAIL"));
copyButton.setOnClickListener(v -> { activityErrorBinding.errorReportCopyButton.setOnClickListener(v -> {
ShareUtils.copyToClipboard(this, buildMarkdown()); ShareUtils.copyToClipboard(this, buildMarkdown());
Toast.makeText(this, R.string.msg_copied, Toast.LENGTH_SHORT).show(); Toast.makeText(this, R.string.msg_copied, Toast.LENGTH_SHORT).show();
}); });
reportGithubButton.setOnClickListener(v -> activityErrorBinding.errorReportGitHubButton.setOnClickListener(v ->
openPrivacyPolicyDialog(this, "GITHUB")); openPrivacyPolicyDialog(this, "GITHUB"));
// normal bugreport // normal bugreport
buildInfo(errorInfo); buildInfo(errorInfo);
if (errorInfo.getMessage() != 0) { if (errorInfo.getMessage() != 0) {
errorMessageView.setText(errorInfo.getMessage()); activityErrorBinding.errorMessageView.setText(errorInfo.getMessage());
} else { } else {
errorMessageView.setVisibility(View.GONE); activityErrorBinding.errorMessageView.setVisibility(View.GONE);
findViewById(R.id.messageWhatHappenedView).setVisibility(View.GONE); activityErrorBinding.messageWhatHappenedView.setVisibility(View.GONE);
} }
errorView.setText(formErrorText(errorList)); activityErrorBinding.errorView.setText(formErrorText(errorList));
// print stack trace once again for debugging: // print stack trace once again for debugging:
for (final String e : errorList) { for (final String e : errorList) {
@ -339,11 +328,10 @@ public class ErrorActivity extends AppCompatActivity {
} }
private void buildInfo(final ErrorInfo info) { private void buildInfo(final ErrorInfo info) {
final TextView infoLabelView = findViewById(R.id.errorInfoLabelsView);
final TextView infoView = findViewById(R.id.errorInfosView);
String text = ""; String text = "";
infoLabelView.setText(getString(R.string.info_labels).replace("\\n", "\n")); activityErrorBinding.errorInfoLabelsView.setText(getString(R.string.info_labels)
.replace("\\n", "\n"));
text += getUserActionString(info.getUserAction()) + "\n" text += getUserActionString(info.getUserAction()) + "\n"
+ info.getRequest() + "\n" + info.getRequest() + "\n"
@ -356,7 +344,7 @@ public class ErrorActivity extends AppCompatActivity {
+ BuildConfig.VERSION_NAME + "\n" + BuildConfig.VERSION_NAME + "\n"
+ getOsString(); + getOsString();
infoView.setText(text); activityErrorBinding.errorInfosView.setText(text);
} }
private String buildJson() { private String buildJson() {
@ -374,7 +362,8 @@ public class ErrorActivity extends AppCompatActivity {
.value("os", getOsString()) .value("os", getOsString())
.value("time", currentTimeStamp) .value("time", currentTimeStamp)
.array("exceptions", Arrays.asList(errorList)) .array("exceptions", Arrays.asList(errorList))
.value("user_comment", userCommentBox.getText().toString()) .value("user_comment", activityErrorBinding.errorCommentBox.getText()
.toString())
.end() .end()
.done(); .done();
} catch (final Throwable e) { } catch (final Throwable e) {
@ -389,7 +378,7 @@ public class ErrorActivity extends AppCompatActivity {
try { try {
final StringBuilder htmlErrorReport = new StringBuilder(); final StringBuilder htmlErrorReport = new StringBuilder();
final String userComment = userCommentBox.getText().toString(); final String userComment = activityErrorBinding.errorCommentBox.getText().toString();
if (!userComment.isEmpty()) { if (!userComment.isEmpty()) {
htmlErrorReport.append(userComment).append("\n"); htmlErrorReport.append(userComment).append("\n");
} }
@ -473,10 +462,9 @@ public class ErrorActivity extends AppCompatActivity {
private void addGuruMeditation() { private void addGuruMeditation() {
//just an easter egg //just an easter egg
final TextView sorryView = findViewById(R.id.errorSorryView); String text = activityErrorBinding.errorSorryView.getText().toString();
String text = sorryView.getText().toString();
text += "\n" + getString(R.string.guru_meditation); text += "\n" + getString(R.string.guru_meditation);
sorryView.setText(text); activityErrorBinding.errorSorryView.setText(text);
} }
@Override @Override

View File

@ -8,6 +8,7 @@ import android.provider.Settings;
import android.widget.Toast; import android.widget.Toast;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.core.app.ActivityCompat;
import androidx.preference.Preference; import androidx.preference.Preference;
import org.schabi.newpipe.R; import org.schabi.newpipe.R;
@ -31,7 +32,7 @@ public class AppearanceSettingsFragment extends BasePreferenceFragment {
if (!newValue.equals(startThemeKey) && getActivity() != null) { if (!newValue.equals(startThemeKey) && getActivity() != null) {
// If it's not the current theme // If it's not the current theme
getActivity().recreate(); ActivityCompat.recreate(requireActivity());
} }
return false; return false;

View File

@ -11,6 +11,7 @@ import android.widget.Toast;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.core.content.ContextCompat;
import androidx.preference.Preference; import androidx.preference.Preference;
import androidx.preference.PreferenceManager; import androidx.preference.PreferenceManager;
@ -30,19 +31,10 @@ import org.schabi.newpipe.report.UserAction;
import org.schabi.newpipe.util.FilePickerActivityHelper; import org.schabi.newpipe.util.FilePickerActivityHelper;
import org.schabi.newpipe.util.ZipHelper; import org.schabi.newpipe.util.ZipHelper;
import java.io.BufferedOutputStream;
import java.io.File; import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.text.SimpleDateFormat; import java.text.SimpleDateFormat;
import java.util.Date; import java.util.Date;
import java.util.Locale; import java.util.Locale;
import java.util.Map;
import java.util.zip.ZipFile;
import java.util.zip.ZipOutputStream;
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage; import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
@ -50,12 +42,7 @@ public class ContentSettingsFragment extends BasePreferenceFragment {
private static final int REQUEST_IMPORT_PATH = 8945; private static final int REQUEST_IMPORT_PATH = 8945;
private static final int REQUEST_EXPORT_PATH = 30945; private static final int REQUEST_EXPORT_PATH = 30945;
private File databasesDir; private ContentSettingsManager manager;
private File newpipeDb;
private File newpipeDbJournal;
private File newpipeDbShm;
private File newpipeDbWal;
private File newpipeSettings;
private String thumbnailLoadToggleKey; private String thumbnailLoadToggleKey;
private String youtubeRestrictedModeEnabledKey; private String youtubeRestrictedModeEnabledKey;
@ -120,16 +107,9 @@ public class ContentSettingsFragment extends BasePreferenceFragment {
@Override @Override
public void onCreatePreferences(final Bundle savedInstanceState, final String rootKey) { public void onCreatePreferences(final Bundle savedInstanceState, final String rootKey) {
final File homeDir = ContextCompat.getDataDir(requireContext());
final String homeDir = getActivity().getApplicationInfo().dataDir; manager = new ContentSettingsManager(new NewPipeFileLocator(homeDir));
databasesDir = new File(homeDir + "/databases"); manager.deleteSettingsFile();
newpipeDb = new File(homeDir + "/databases/newpipe.db");
newpipeDbJournal = new File(homeDir + "/databases/newpipe.db-journal");
newpipeDbShm = new File(homeDir + "/databases/newpipe.db-shm");
newpipeDbWal = new File(homeDir + "/databases/newpipe.db-wal");
newpipeSettings = new File(homeDir + "/databases/newpipe.settings");
newpipeSettings.delete();
addPreferencesFromResource(R.xml.content_settings); addPreferencesFromResource(R.xml.content_settings);
@ -212,62 +192,36 @@ public class ContentSettingsFragment extends BasePreferenceFragment {
//checkpoint before export //checkpoint before export
NewPipeDatabase.checkpoint(); NewPipeDatabase.checkpoint();
try (ZipOutputStream outZip = new ZipOutputStream(new BufferedOutputStream( final SharedPreferences preferences = PreferenceManager
new FileOutputStream(path)))) { .getDefaultSharedPreferences(requireContext());
ZipHelper.addFileToZip(outZip, newpipeDb.getPath(), "newpipe.db"); manager.exportDatabase(preferences, path);
saveSharedPreferencesToFile(newpipeSettings); Toast.makeText(getContext(), R.string.export_complete_toast, Toast.LENGTH_SHORT).show();
ZipHelper.addFileToZip(outZip, newpipeSettings.getPath(),
"newpipe.settings");
}
Toast.makeText(getContext(), R.string.export_complete_toast, Toast.LENGTH_SHORT)
.show();
} catch (final Exception e) { } catch (final Exception e) {
onError(e); onError(e);
} }
} }
private void saveSharedPreferencesToFile(final File dst) {
try (ObjectOutputStream output = new ObjectOutputStream(new FileOutputStream(dst))) {
final SharedPreferences pref
= PreferenceManager.getDefaultSharedPreferences(requireContext());
output.writeObject(pref.getAll());
output.flush();
} catch (final IOException e) {
e.printStackTrace();
}
}
private void importDatabase(final String filePath) { private void importDatabase(final String filePath) {
// check if file is supported // check if file is supported
try (ZipFile zipFile = new ZipFile(filePath)) { if (!ZipHelper.isValidZipFile(filePath)) {
} catch (final IOException ioe) {
Toast.makeText(getContext(), R.string.no_valid_zip_file, Toast.LENGTH_SHORT) Toast.makeText(getContext(), R.string.no_valid_zip_file, Toast.LENGTH_SHORT)
.show(); .show();
return; return;
} }
try { try {
if (!databasesDir.exists() && !databasesDir.mkdir()) { if (!manager.ensureDbDirectoryExists()) {
throw new Exception("Could not create databases dir"); throw new Exception("Could not create databases dir");
} }
final boolean isDbFileExtracted = ZipHelper.extractFileFromZip(filePath, if (!manager.extractDb(filePath)) {
newpipeDb.getPath(), "newpipe.db");
if (isDbFileExtracted) {
newpipeDbJournal.delete();
newpipeDbWal.delete();
newpipeDbShm.delete();
} else {
Toast.makeText(getContext(), R.string.could_not_import_all_files, Toast.LENGTH_LONG) Toast.makeText(getContext(), R.string.could_not_import_all_files, Toast.LENGTH_LONG)
.show(); .show();
} }
//If settings file exist, ask if it should be imported. //If settings file exist, ask if it should be imported.
if (ZipHelper.extractFileFromZip(filePath, newpipeSettings.getPath(), if (manager.extractSettings(filePath)) {
"newpipe.settings")) {
final AlertDialog.Builder alert = new AlertDialog.Builder(getContext()); final AlertDialog.Builder alert = new AlertDialog.Builder(getContext());
alert.setTitle(R.string.import_settings); alert.setTitle(R.string.import_settings);
@ -278,7 +232,8 @@ public class ContentSettingsFragment extends BasePreferenceFragment {
}); });
alert.setPositiveButton(getString(R.string.finish), (dialog, which) -> { alert.setPositiveButton(getString(R.string.finish), (dialog, which) -> {
dialog.dismiss(); dialog.dismiss();
loadSharedPreferences(newpipeSettings); manager.loadSharedPreferences(PreferenceManager
.getDefaultSharedPreferences(requireContext()));
// restart app to properly load db // restart app to properly load db
System.exit(0); System.exit(0);
}); });
@ -292,34 +247,6 @@ public class ContentSettingsFragment extends BasePreferenceFragment {
} }
} }
private void loadSharedPreferences(final File src) {
try (ObjectInputStream input = new ObjectInputStream(new FileInputStream(src))) {
final SharedPreferences.Editor prefEdit = PreferenceManager
.getDefaultSharedPreferences(requireContext()).edit();
prefEdit.clear();
final Map<String, ?> entries = (Map<String, ?>) input.readObject();
for (final Map.Entry<String, ?> entry : entries.entrySet()) {
final Object v = entry.getValue();
final String key = entry.getKey();
if (v instanceof Boolean) {
prefEdit.putBoolean(key, (Boolean) v);
} else if (v instanceof Float) {
prefEdit.putFloat(key, (Float) v);
} else if (v instanceof Integer) {
prefEdit.putInt(key, (Integer) v);
} else if (v instanceof Long) {
prefEdit.putLong(key, (Long) v);
} else if (v instanceof String) {
prefEdit.putString(key, (String) v);
}
}
prefEdit.commit();
} catch (final IOException | ClassNotFoundException e) {
e.printStackTrace();
}
}
/*////////////////////////////////////////////////////////////////////////// /*//////////////////////////////////////////////////////////////////////////
// Error // Error
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/

View File

@ -0,0 +1,102 @@
package org.schabi.newpipe.settings
import android.content.SharedPreferences
import org.schabi.newpipe.util.ZipHelper
import java.io.BufferedOutputStream
import java.io.FileInputStream
import java.io.FileOutputStream
import java.io.IOException
import java.io.ObjectInputStream
import java.io.ObjectOutputStream
import java.util.zip.ZipOutputStream
class ContentSettingsManager(private val fileLocator: NewPipeFileLocator) {
/**
* Exports given [SharedPreferences] to the file in given outputPath.
* It also creates the file.
*/
@Throws(Exception::class)
fun exportDatabase(preferences: SharedPreferences, outputPath: String) {
ZipOutputStream(BufferedOutputStream(FileOutputStream(outputPath)))
.use { outZip ->
ZipHelper.addFileToZip(outZip, fileLocator.db.path, "newpipe.db")
try {
ObjectOutputStream(FileOutputStream(fileLocator.settings)).use { output ->
output.writeObject(preferences.all)
output.flush()
}
} catch (e: IOException) {
e.printStackTrace()
}
ZipHelper.addFileToZip(outZip, fileLocator.settings.path, "newpipe.settings")
}
}
fun deleteSettingsFile() {
fileLocator.settings.delete()
}
/**
* Tries to create database directory if it does not exist.
*
* @return Whether the directory exists afterwards.
*/
fun ensureDbDirectoryExists(): Boolean {
return fileLocator.dbDir.exists() || fileLocator.dbDir.mkdir()
}
fun extractDb(filePath: String): Boolean {
val success = ZipHelper.extractFileFromZip(filePath, fileLocator.db.path, "newpipe.db")
if (success) {
fileLocator.dbJournal.delete()
fileLocator.dbWal.delete()
fileLocator.dbShm.delete()
}
return success
}
fun extractSettings(filePath: String): Boolean {
return ZipHelper
.extractFileFromZip(filePath, fileLocator.settings.path, "newpipe.settings")
}
fun loadSharedPreferences(preferences: SharedPreferences) {
try {
val preferenceEditor = preferences.edit()
ObjectInputStream(FileInputStream(fileLocator.settings)).use { input ->
preferenceEditor.clear()
@Suppress("UNCHECKED_CAST")
val entries = input.readObject() as Map<String, *>
for ((key, value) in entries) {
when (value) {
is Boolean -> {
preferenceEditor.putBoolean(key, value)
}
is Float -> {
preferenceEditor.putFloat(key, value)
}
is Int -> {
preferenceEditor.putInt(key, value)
}
is Long -> {
preferenceEditor.putLong(key, value)
}
is String -> {
preferenceEditor.putString(key, value)
}
}
}
preferenceEditor.commit()
}
} catch (e: IOException) {
e.printStackTrace()
} catch (e: ClassNotFoundException) {
e.printStackTrace()
}
}
}

View File

@ -246,10 +246,7 @@ public class DownloadSettingsFragment extends BasePreferenceFragment {
// revoke permissions on the old save path (required for SAF only) // revoke permissions on the old save path (required for SAF only)
final Context context = getContext(); final Context context = requireContext();
if (context == null) {
throw new NullPointerException("getContext()");
}
forgetSAFTree(context, defaultPreferences.getString(key, "")); forgetSAFTree(context, defaultPreferences.getString(key, ""));

View File

@ -0,0 +1,21 @@
package org.schabi.newpipe.settings
import java.io.File
/**
* Locates specific files of NewPipe based on the home directory of the app.
*/
class NewPipeFileLocator(private val homeDir: File) {
val dbDir by lazy { File(homeDir, "/databases") }
val db by lazy { File(homeDir, "/databases/newpipe.db") }
val dbJournal by lazy { File(homeDir, "/databases/newpipe.db-journal") }
val dbShm by lazy { File(homeDir, "/databases/newpipe.db-shm") }
val dbWal by lazy { File(homeDir, "/databases/newpipe.db-wal") }
val settings by lazy { File(homeDir, "/databases/newpipe.settings") }
}

View File

@ -7,12 +7,12 @@ import android.view.MenuItem;
import androidx.appcompat.app.ActionBar; import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.Toolbar;
import androidx.fragment.app.Fragment; import androidx.fragment.app.Fragment;
import androidx.preference.Preference; import androidx.preference.Preference;
import androidx.preference.PreferenceFragmentCompat; import androidx.preference.PreferenceFragmentCompat;
import org.schabi.newpipe.R; import org.schabi.newpipe.R;
import org.schabi.newpipe.databinding.SettingsLayoutBinding;
import org.schabi.newpipe.util.DeviceUtils; import org.schabi.newpipe.util.DeviceUtils;
import org.schabi.newpipe.util.ThemeHelper; import org.schabi.newpipe.util.ThemeHelper;
import org.schabi.newpipe.views.FocusOverlayView; import org.schabi.newpipe.views.FocusOverlayView;
@ -51,10 +51,12 @@ public class SettingsActivity extends AppCompatActivity
setTheme(ThemeHelper.getSettingsThemeStyle(this)); setTheme(ThemeHelper.getSettingsThemeStyle(this));
assureCorrectAppLanguage(this); assureCorrectAppLanguage(this);
super.onCreate(savedInstanceBundle); super.onCreate(savedInstanceBundle);
setContentView(R.layout.settings_layout);
final Toolbar toolbar = findViewById(R.id.toolbar); final SettingsLayoutBinding settingsLayoutBinding =
setSupportActionBar(toolbar); SettingsLayoutBinding.inflate(getLayoutInflater());
setContentView(settingsLayoutBinding.getRoot());
setSupportActionBar(settingsLayoutBinding.toolbarLayout.toolbar);
if (savedInstanceBundle == null) { if (savedInstanceBundle == null) {
getSupportFragmentManager().beginTransaction() getSupportFragmentManager().beginTransaction()

View File

@ -483,7 +483,7 @@ public class Mp4FromDashWriter {
// stsc_table_entry = [first_chunk, samples_per_chunk, sample_description_index] // stsc_table_entry = [first_chunk, samples_per_chunk, sample_description_index]
tables.stscBEntries = new int[tables.stsc * 3]; tables.stscBEntries = new int[tables.stsc * 3];
tables.stco = remainChunkOffset + 1; // total entrys in chunk offset box tables.stco = remainChunkOffset + 1; // total entries in chunk offset box
tables.stscBEntries[index++] = 1; tables.stscBEntries[index++] = 1;
tables.stscBEntries[index++] = firstCount; tables.stscBEntries[index++] = firstCount;

View File

@ -1,17 +0,0 @@
package org.schabi.newpipe.util;
public final class Constants {
public static final String KEY_SERVICE_ID = "key_service_id";
public static final String KEY_URL = "key_url";
public static final String KEY_TITLE = "key_title";
public static final String KEY_LINK_TYPE = "key_link_type";
public static final String KEY_OPEN_SEARCH = "key_open_search";
public static final String KEY_SEARCH_STRING = "key_search_string";
public static final String KEY_THEME_CHANGE = "key_theme_change";
public static final String KEY_MAIN_PAGE_CHANGE = "key_main_page_change";
public static final int NO_SERVICE_ID = -1;
private Constants() { }
}

View File

@ -0,0 +1,20 @@
@file:JvmName("Constants")
package org.schabi.newpipe.util
/**
* Default duration when using throttle functions across the app, in milliseconds.
*/
const val DEFAULT_THROTTLE_TIMEOUT = 120L
const val KEY_SERVICE_ID = "key_service_id"
const val KEY_URL = "key_url"
const val KEY_TITLE = "key_title"
const val KEY_LINK_TYPE = "key_link_type"
const val KEY_OPEN_SEARCH = "key_open_search"
const val KEY_SEARCH_STRING = "key_search_string"
const val KEY_THEME_CHANGE = "key_theme_change"
const val KEY_MAIN_PAGE_CHANGE = "key_main_page_change"
const val NO_SERVICE_ID = -1

View File

@ -1,6 +0,0 @@
package org.schabi.newpipe.util
/**
* Default duration when using throttle functions across the app, in milliseconds.
*/
const val DEFAULT_THROTTLE_TIMEOUT = 120L

View File

@ -1,86 +0,0 @@
package org.schabi.newpipe.util
import java.io.IOException
import java.io.InterruptedIOException
class ExceptionUtils {
companion object {
/**
* @return if throwable is related to Interrupted exceptions, or one of its causes is.
*/
@JvmStatic
fun isInterruptedCaused(throwable: Throwable): Boolean {
return hasExactCause(
throwable,
InterruptedIOException::class.java,
InterruptedException::class.java
)
}
/**
* @return if throwable is related to network issues, or one of its causes is.
*/
@JvmStatic
fun isNetworkRelated(throwable: Throwable): Boolean {
return hasAssignableCause(
throwable,
IOException::class.java
)
}
/**
* Calls [hasCause] with the `checkSubtypes` parameter set to false.
*/
@JvmStatic
fun hasExactCause(throwable: Throwable, vararg causesToCheck: Class<*>): Boolean {
return hasCause(throwable, false, *causesToCheck)
}
/**
* Calls [hasCause] with the `checkSubtypes` parameter set to true.
*/
@JvmStatic
fun hasAssignableCause(throwable: Throwable?, vararg causesToCheck: Class<*>): Boolean {
return hasCause(throwable, true, *causesToCheck)
}
/**
* Check if throwable has some cause from the causes to check, or is itself in it.
*
* If `checkIfAssignable` is true, not only the exact type will be considered equals, but also its subtypes.
*
* @param throwable throwable that will be checked.
* @param checkSubtypes if subtypes are also checked.
* @param causesToCheck an array of causes to check.
*
* @see Class.isAssignableFrom
*/
@JvmStatic
tailrec fun hasCause(throwable: Throwable?, checkSubtypes: Boolean, vararg causesToCheck: Class<*>): Boolean {
if (throwable == null) {
return false
}
// Check if throwable is a subtype of any of the causes to check
causesToCheck.forEach { causeClass ->
if (checkSubtypes) {
if (causeClass.isAssignableFrom(throwable.javaClass)) {
return true
}
} else {
if (causeClass == throwable.javaClass) {
return true
}
}
}
val currentCause: Throwable? = throwable.cause
// Check if cause is not pointing to the same instance, to avoid infinite loops.
if (throwable !== currentCause) {
return hasCause(currentCause, checkSubtypes, *causesToCheck)
}
return false
}
}
}

View File

@ -22,9 +22,16 @@ package org.schabi.newpipe.util;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.os.Handler; import android.os.Handler;
import android.text.method.LinkMovementMethod;
import android.util.Log; import android.util.Log;
import android.view.View;
import android.widget.TextView;
import android.widget.Toast; import android.widget.Toast;
import androidx.annotation.Nullable;
import androidx.core.text.HtmlCompat;
import androidx.preference.PreferenceManager;
import org.schabi.newpipe.MainActivity; import org.schabi.newpipe.MainActivity;
import org.schabi.newpipe.R; import org.schabi.newpipe.R;
import org.schabi.newpipe.ReCaptchaActivity; import org.schabi.newpipe.ReCaptchaActivity;
@ -32,6 +39,7 @@ import org.schabi.newpipe.extractor.Info;
import org.schabi.newpipe.extractor.InfoItem; import org.schabi.newpipe.extractor.InfoItem;
import org.schabi.newpipe.extractor.ListExtractor.InfoItemsPage; import org.schabi.newpipe.extractor.ListExtractor.InfoItemsPage;
import org.schabi.newpipe.extractor.ListInfo; import org.schabi.newpipe.extractor.ListInfo;
import org.schabi.newpipe.extractor.MetaInfo;
import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.Page; import org.schabi.newpipe.extractor.Page;
import org.schabi.newpipe.extractor.StreamingService; import org.schabi.newpipe.extractor.StreamingService;
@ -50,6 +58,7 @@ import org.schabi.newpipe.extractor.services.youtube.extractors.YoutubeStreamExt
import org.schabi.newpipe.extractor.stream.StreamInfo; import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.extractor.stream.StreamInfoItem; import org.schabi.newpipe.extractor.stream.StreamInfoItem;
import org.schabi.newpipe.extractor.suggestion.SuggestionExtractor; import org.schabi.newpipe.extractor.suggestion.SuggestionExtractor;
import org.schabi.newpipe.ktx.ExceptionUtils;
import org.schabi.newpipe.report.ErrorActivity; import org.schabi.newpipe.report.ErrorActivity;
import org.schabi.newpipe.report.ErrorInfo; import org.schabi.newpipe.report.ErrorInfo;
import org.schabi.newpipe.report.UserAction; import org.schabi.newpipe.report.UserAction;
@ -60,6 +69,8 @@ import java.util.List;
import io.reactivex.rxjava3.core.Maybe; import io.reactivex.rxjava3.core.Maybe;
import io.reactivex.rxjava3.core.Single; import io.reactivex.rxjava3.core.Single;
import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty;
public final class ExtractorHelper { public final class ExtractorHelper {
private static final String TAG = ExtractorHelper.class.getSimpleName(); private static final String TAG = ExtractorHelper.class.getSimpleName();
private static final InfoCache CACHE = InfoCache.getInstance(); private static final InfoCache CACHE = InfoCache.getInstance();
@ -306,4 +317,73 @@ public final class ExtractorHelper {
} }
}); });
} }
/**
* Formats the text contained in the meta info list as HTML and puts it into the text view,
* while also making the separator visible. If the list is null or empty, or the user chose not
* to see meta information, both the text view and the separator are hidden
* @param metaInfos a list of meta information, can be null or empty
* @param metaInfoTextView the text view in which to show the formatted HTML
* @param metaInfoSeparator another view to be shown or hidden accordingly to the text view
*/
public static void showMetaInfoInTextView(@Nullable final List<MetaInfo> metaInfos,
final TextView metaInfoTextView,
final View metaInfoSeparator) {
final Context context = metaInfoTextView.getContext();
final boolean showMetaInfo = PreferenceManager.getDefaultSharedPreferences(context)
.getBoolean(context.getString(R.string.show_meta_info_key), true);
if (!showMetaInfo || metaInfos == null || metaInfos.isEmpty()) {
metaInfoTextView.setVisibility(View.GONE);
metaInfoSeparator.setVisibility(View.GONE);
} else {
final StringBuilder stringBuilder = new StringBuilder();
for (final MetaInfo metaInfo : metaInfos) {
if (!isNullOrEmpty(metaInfo.getTitle())) {
stringBuilder.append("<b>").append(metaInfo.getTitle()).append("</b>")
.append(Localization.DOT_SEPARATOR);
}
String content = metaInfo.getContent().getContent().trim();
if (content.endsWith(".")) {
content = content.substring(0, content.length() - 1); // remove . at end
}
stringBuilder.append(content);
for (int i = 0; i < metaInfo.getUrls().size(); i++) {
if (i == 0) {
stringBuilder.append(Localization.DOT_SEPARATOR);
} else {
stringBuilder.append("<br/><br/>");
}
stringBuilder
.append("<a href=\"").append(metaInfo.getUrls().get(i)).append("\">")
.append(capitalizeIfAllUppercase(metaInfo.getUrlTexts().get(i).trim()))
.append("</a>");
}
}
metaInfoTextView.setText(HtmlCompat.fromHtml(stringBuilder.toString(),
HtmlCompat.FROM_HTML_SEPARATOR_LINE_BREAK_HEADING));
metaInfoTextView.setMovementMethod(LinkMovementMethod.getInstance());
metaInfoTextView.setVisibility(View.VISIBLE);
metaInfoSeparator.setVisibility(View.VISIBLE);
}
}
private static String capitalizeIfAllUppercase(final String text) {
for (int i = 0; i < text.length(); i++) {
if (Character.isLowerCase(text.charAt(i))) {
return text; // there is at least a lowercase letter -> not all uppercase
}
}
if (text.isEmpty()) {
return text;
} else {
return text.substring(0, 1).toUpperCase() + text.substring(1).toLowerCase();
}
}
} }

View File

@ -5,7 +5,7 @@ import android.content.Context;
import org.schabi.newpipe.R; import org.schabi.newpipe.R;
/** /**
* Created by Chrsitian Schabesberger on 28.09.17. * Created by Christian Schabesberger on 28.09.17.
* KioskTranslator.java is part of NewPipe. * KioskTranslator.java is part of NewPipe.
* <p> * <p>
* NewPipe is free software: you can redistribute it and/or modify * NewPipe is free software: you can redistribute it and/or modify
@ -44,6 +44,10 @@ public final class KioskTranslator {
return c.getString(R.string.most_liked); return c.getString(R.string.most_liked);
case "conferences": case "conferences":
return c.getString(R.string.conferences); return c.getString(R.string.conferences);
case "recent":
return c.getString(R.string.recent);
case "live":
return c.getString(R.string.duration_live);
default: default:
return kioskId; return kioskId;
} }
@ -59,9 +63,12 @@ public final class KioskTranslator {
case "Local": case "Local":
return ThemeHelper.resolveResourceIdFromAttr(c, R.attr.ic_kiosk_local); return ThemeHelper.resolveResourceIdFromAttr(c, R.attr.ic_kiosk_local);
case "Recently added": case "Recently added":
case "recent":
return ThemeHelper.resolveResourceIdFromAttr(c, R.attr.ic_kiosk_recent); return ThemeHelper.resolveResourceIdFromAttr(c, R.attr.ic_kiosk_recent);
case "Most liked": case "Most liked":
return ThemeHelper.resolveResourceIdFromAttr(c, R.attr.ic_thumb_up); return ThemeHelper.resolveResourceIdFromAttr(c, R.attr.ic_thumb_up);
case "live":
return ThemeHelper.resolveResourceIdFromAttr(c, R.attr.ic_live_tv);
default: default:
return 0; return 0;
} }

View File

@ -1,9 +1,9 @@
package org.schabi.newpipe.util; package org.schabi.newpipe.util;
import android.content.Context; import android.content.Context;
import androidx.appcompat.app.AlertDialog; import androidx.appcompat.app.AlertDialog;
import androidx.preference.PreferenceManager;
import org.schabi.newpipe.R; import org.schabi.newpipe.R;
import org.schabi.newpipe.extractor.ServiceList; import org.schabi.newpipe.extractor.ServiceList;
@ -16,6 +16,12 @@ public final class KoreUtil {
|| serviceId == ServiceList.SoundCloud.getServiceId()); || serviceId == ServiceList.SoundCloud.getServiceId());
} }
public static boolean shouldShowPlayWithKodi(final Context context, final int serviceId) {
return isServiceSupportedByKore(serviceId)
&& PreferenceManager.getDefaultSharedPreferences(context)
.getBoolean(context.getString(R.string.show_play_with_kodi_key), false);
}
public static void showInstallKoreDialog(final Context context) { public static void showInstallKoreDialog(final Context context) {
final AlertDialog.Builder builder = new AlertDialog.Builder(context); final AlertDialog.Builder builder = new AlertDialog.Builder(context);
builder.setMessage(R.string.kore_not_found) builder.setMessage(R.string.kore_not_found)

View File

@ -141,7 +141,7 @@ public final class ListHelper {
final SharedPreferences preferences final SharedPreferences preferences
= PreferenceManager.getDefaultSharedPreferences(context); = PreferenceManager.getDefaultSharedPreferences(context);
// Load the prefered resolution otherwise the best available // Load the preferred resolution otherwise the best available
String resolution = preferences != null String resolution = preferences != null
? preferences.getString(context.getString(key), context.getString(value)) ? preferences.getString(context.getString(key), context.getString(value))
: context.getString(R.string.best_resolution_key); : context.getString(R.string.best_resolution_key);
@ -161,7 +161,7 @@ public final class ListHelper {
* *
* @param defaultResolution the default resolution to look for * @param defaultResolution the default resolution to look for
* @param bestResolutionKey key of the best resolution * @param bestResolutionKey key of the best resolution
* @param defaultFormat the default fomat to look for * @param defaultFormat the default format to look for
* @param videoStreams list of the video streams to check * @param videoStreams list of the video streams to check
* @return index of the default resolution&format * @return index of the default resolution&format
*/ */
@ -351,7 +351,7 @@ public final class ListHelper {
* @param targetResolution the resolution to look for * @param targetResolution the resolution to look for
* @param targetFormat the format to look for * @param targetFormat the format to look for
* @param videoStreams the available video streams * @param videoStreams the available video streams
* @return the index of the prefered video stream * @return the index of the preferred video stream
*/ */
static int getVideoStreamIndex(final String targetResolution, final MediaFormat targetFormat, static int getVideoStreamIndex(final String targetResolution, final MediaFormat targetFormat,
final List<VideoStream> videoStreams) { final List<VideoStream> videoStreams) {
@ -413,7 +413,7 @@ public final class ListHelper {
* @param context Android app context * @param context Android app context
* @param defaultResolution the default resolution * @param defaultResolution the default resolution
* @param videoStreams the list of video streams to check * @param videoStreams the list of video streams to check
* @return the index of the prefered video stream * @return the index of the preferred video stream
*/ */
private static int getDefaultResolutionWithDefaultFormat(final Context context, private static int getDefaultResolutionWithDefaultFormat(final Context context,
final String defaultResolution, final String defaultResolution,

View File

@ -9,18 +9,10 @@ import android.icu.text.CompactDecimalFormat;
import android.os.Build; import android.os.Build;
import android.text.TextUtils; import android.text.TextUtils;
import android.util.DisplayMetrics; import android.util.DisplayMetrics;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.PluralsRes; import androidx.annotation.PluralsRes;
import androidx.annotation.StringRes; import androidx.annotation.StringRes;
import androidx.preference.PreferenceManager; import androidx.preference.PreferenceManager;
import org.ocpsoft.prettytime.PrettyTime;
import org.ocpsoft.prettytime.units.Decade;
import org.schabi.newpipe.R;
import org.schabi.newpipe.extractor.ListExtractor;
import org.schabi.newpipe.extractor.localization.ContentCountry;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.math.RoundingMode; import java.math.RoundingMode;
import java.text.NumberFormat; import java.text.NumberFormat;
@ -30,9 +22,14 @@ import java.time.format.DateTimeFormatter;
import java.time.format.FormatStyle; import java.time.format.FormatStyle;
import java.util.Arrays; import java.util.Arrays;
import java.util.Calendar; import java.util.Calendar;
import java.util.GregorianCalendar;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
import org.ocpsoft.prettytime.PrettyTime;
import org.ocpsoft.prettytime.units.Decade;
import org.schabi.newpipe.R;
import org.schabi.newpipe.extractor.ListExtractor;
import org.schabi.newpipe.extractor.localization.ContentCountry;
import org.schabi.newpipe.ktx.OffsetDateTimeKt;
/* /*
@ -57,15 +54,11 @@ import java.util.Locale;
public final class Localization { public final class Localization {
private static final String DOT_SEPARATOR = ""; public static final String DOT_SEPARATOR = "";
private static PrettyTime prettyTime; private static PrettyTime prettyTime;
private Localization() { } private Localization() { }
public static void init(final Context context) {
initPrettyTime(context);
}
@NonNull @NonNull
public static String concatenateStrings(final String... strings) { public static String concatenateStrings(final String... strings) {
return concatenateStrings(Arrays.asList(strings)); return concatenateStrings(Arrays.asList(strings));
@ -307,14 +300,18 @@ public final class Localization {
// Pretty Time // Pretty Time
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/
private static void initPrettyTime(final Context context) { public static void initPrettyTime(final PrettyTime time) {
prettyTime = new PrettyTime(getAppLocale(context)); prettyTime = time;
// Do not use decades as YouTube doesn't either. // Do not use decades as YouTube doesn't either.
prettyTime.removeUnit(Decade.class); prettyTime.removeUnit(Decade.class);
} }
public static PrettyTime resolvePrettyTime(final Context context) {
return new PrettyTime(getAppLocale(context));
}
public static String relativeTime(final OffsetDateTime offsetDateTime) { public static String relativeTime(final OffsetDateTime offsetDateTime) {
return relativeTime(GregorianCalendar.from(offsetDateTime.toZonedDateTime())); return relativeTime(OffsetDateTimeKt.toCalendar(offsetDateTime));
} }
public static String relativeTime(final Calendar calendarTime) { public static String relativeTime(final Calendar calendarTime) {
@ -354,4 +351,19 @@ public final class Localization {
private static double round(final double value, final int places) { private static double round(final double value, final int places) {
return new BigDecimal(value).setScale(places, RoundingMode.HALF_UP).doubleValue(); return new BigDecimal(value).setScale(places, RoundingMode.HALF_UP).doubleValue();
} }
/**
* Workaround to match normalized captions like english to English or deutsch to Deutsch.
* @param list the list to search into
* @param toFind the string to look for
* @return whether the string was found or not
*/
public static boolean containsCaseInsensitive(final List<String> list, final String toFind) {
for (final String i : list) {
if (i.equalsIgnoreCase(toFind)) {
return true;
}
}
return false;
}
} }

View File

@ -46,10 +46,9 @@ import org.schabi.newpipe.local.history.StatisticsPlaylistFragment;
import org.schabi.newpipe.local.playlist.LocalPlaylistFragment; import org.schabi.newpipe.local.playlist.LocalPlaylistFragment;
import org.schabi.newpipe.local.subscription.SubscriptionFragment; import org.schabi.newpipe.local.subscription.SubscriptionFragment;
import org.schabi.newpipe.local.subscription.SubscriptionsImportFragment; import org.schabi.newpipe.local.subscription.SubscriptionsImportFragment;
import org.schabi.newpipe.player.BackgroundPlayerActivity; import org.schabi.newpipe.player.PlayQueueActivity;
import org.schabi.newpipe.player.BasePlayer; import org.schabi.newpipe.player.Player;
import org.schabi.newpipe.player.MainPlayer; import org.schabi.newpipe.player.MainPlayer;
import org.schabi.newpipe.player.VideoPlayer;
import org.schabi.newpipe.player.helper.PlayerHelper; import org.schabi.newpipe.player.helper.PlayerHelper;
import org.schabi.newpipe.player.helper.PlayerHolder; import org.schabi.newpipe.player.helper.PlayerHolder;
import org.schabi.newpipe.player.playqueue.PlayQueue; import org.schabi.newpipe.player.playqueue.PlayQueue;
@ -78,11 +77,11 @@ public final class NavigationHelper {
if (playQueue != null) { if (playQueue != null) {
final String cacheKey = SerializedCache.getInstance().put(playQueue, PlayQueue.class); final String cacheKey = SerializedCache.getInstance().put(playQueue, PlayQueue.class);
if (cacheKey != null) { if (cacheKey != null) {
intent.putExtra(VideoPlayer.PLAY_QUEUE_KEY, cacheKey); intent.putExtra(Player.PLAY_QUEUE_KEY, cacheKey);
} }
} }
intent.putExtra(VideoPlayer.RESUME_PLAYBACK, resumePlayback); intent.putExtra(Player.RESUME_PLAYBACK, resumePlayback);
intent.putExtra(VideoPlayer.PLAYER_TYPE, VideoPlayer.PLAYER_TYPE_VIDEO); intent.putExtra(Player.PLAYER_TYPE, MainPlayer.PlayerType.VIDEO.ordinal());
return intent; return intent;
} }
@ -94,7 +93,7 @@ public final class NavigationHelper {
final boolean resumePlayback, final boolean resumePlayback,
final boolean playWhenReady) { final boolean playWhenReady) {
return getPlayerIntent(context, targetClazz, playQueue, resumePlayback) return getPlayerIntent(context, targetClazz, playQueue, resumePlayback)
.putExtra(BasePlayer.PLAY_WHEN_READY, playWhenReady); .putExtra(Player.PLAY_WHEN_READY, playWhenReady);
} }
@NonNull @NonNull
@ -104,8 +103,8 @@ public final class NavigationHelper {
final boolean selectOnAppend, final boolean selectOnAppend,
final boolean resumePlayback) { final boolean resumePlayback) {
return getPlayerIntent(context, targetClazz, playQueue, resumePlayback) return getPlayerIntent(context, targetClazz, playQueue, resumePlayback)
.putExtra(BasePlayer.APPEND_ONLY, true) .putExtra(Player.APPEND_ONLY, true)
.putExtra(BasePlayer.SELECT_ON_APPEND, selectOnAppend); .putExtra(Player.SELECT_ON_APPEND, selectOnAppend);
} }
public static void playOnMainPlayer(final AppCompatActivity activity, public static void playOnMainPlayer(final AppCompatActivity activity,
@ -135,7 +134,7 @@ public final class NavigationHelper {
Toast.makeText(context, R.string.popup_playing_toast, Toast.LENGTH_SHORT).show(); Toast.makeText(context, R.string.popup_playing_toast, Toast.LENGTH_SHORT).show();
final Intent intent = getPlayerIntent(context, MainPlayer.class, queue, resumePlayback); final Intent intent = getPlayerIntent(context, MainPlayer.class, queue, resumePlayback);
intent.putExtra(VideoPlayer.PLAYER_TYPE, VideoPlayer.PLAYER_TYPE_POPUP); intent.putExtra(Player.PLAYER_TYPE, MainPlayer.PlayerType.POPUP.ordinal());
ContextCompat.startForegroundService(context, intent); ContextCompat.startForegroundService(context, intent);
} }
@ -145,7 +144,7 @@ public final class NavigationHelper {
Toast.makeText(context, R.string.background_player_playing_toast, Toast.LENGTH_SHORT) Toast.makeText(context, R.string.background_player_playing_toast, Toast.LENGTH_SHORT)
.show(); .show();
final Intent intent = getPlayerIntent(context, MainPlayer.class, queue, resumePlayback); final Intent intent = getPlayerIntent(context, MainPlayer.class, queue, resumePlayback);
intent.putExtra(VideoPlayer.PLAYER_TYPE, VideoPlayer.PLAYER_TYPE_AUDIO); intent.putExtra(Player.PLAYER_TYPE, MainPlayer.PlayerType.AUDIO.ordinal());
ContextCompat.startForegroundService(context, intent); ContextCompat.startForegroundService(context, intent);
} }
@ -162,7 +161,7 @@ public final class NavigationHelper {
final Intent intent = getPlayerEnqueueIntent( final Intent intent = getPlayerEnqueueIntent(
context, MainPlayer.class, queue, selectOnAppend, resumePlayback); context, MainPlayer.class, queue, selectOnAppend, resumePlayback);
intent.putExtra(VideoPlayer.PLAYER_TYPE, VideoPlayer.PLAYER_TYPE_VIDEO); intent.putExtra(Player.PLAYER_TYPE, MainPlayer.PlayerType.VIDEO.ordinal());
ContextCompat.startForegroundService(context, intent); ContextCompat.startForegroundService(context, intent);
} }
@ -182,7 +181,7 @@ public final class NavigationHelper {
Toast.makeText(context, R.string.enqueued, Toast.LENGTH_SHORT).show(); Toast.makeText(context, R.string.enqueued, Toast.LENGTH_SHORT).show();
final Intent intent = getPlayerEnqueueIntent( final Intent intent = getPlayerEnqueueIntent(
context, MainPlayer.class, queue, selectOnAppend, resumePlayback); context, MainPlayer.class, queue, selectOnAppend, resumePlayback);
intent.putExtra(VideoPlayer.PLAYER_TYPE, VideoPlayer.PLAYER_TYPE_POPUP); intent.putExtra(Player.PLAYER_TYPE, MainPlayer.PlayerType.POPUP.ordinal());
ContextCompat.startForegroundService(context, intent); ContextCompat.startForegroundService(context, intent);
} }
@ -198,7 +197,7 @@ public final class NavigationHelper {
Toast.makeText(context, R.string.enqueued, Toast.LENGTH_SHORT).show(); Toast.makeText(context, R.string.enqueued, Toast.LENGTH_SHORT).show();
final Intent intent = getPlayerEnqueueIntent( final Intent intent = getPlayerEnqueueIntent(
context, MainPlayer.class, queue, selectOnAppend, resumePlayback); context, MainPlayer.class, queue, selectOnAppend, resumePlayback);
intent.putExtra(VideoPlayer.PLAYER_TYPE, VideoPlayer.PLAYER_TYPE_AUDIO); intent.putExtra(Player.PLAYER_TYPE, MainPlayer.PlayerType.AUDIO.ordinal());
ContextCompat.startForegroundService(context, intent); ContextCompat.startForegroundService(context, intent);
} }
@ -493,7 +492,7 @@ public final class NavigationHelper {
if (playQueue != null) { if (playQueue != null) {
final String cacheKey = SerializedCache.getInstance().put(playQueue, PlayQueue.class); final String cacheKey = SerializedCache.getInstance().put(playQueue, PlayQueue.class);
if (cacheKey != null) { if (cacheKey != null) {
intent.putExtra(VideoPlayer.PLAY_QUEUE_KEY, cacheKey); intent.putExtra(Player.PLAY_QUEUE_KEY, cacheKey);
} }
} }
context.startActivity(intent); context.startActivity(intent);
@ -531,7 +530,7 @@ public final class NavigationHelper {
} }
public static Intent getPlayQueueActivityIntent(final Context context) { public static Intent getPlayQueueActivityIntent(final Context context) {
final Intent intent = new Intent(context, BackgroundPlayerActivity.class); final Intent intent = new Intent(context, PlayQueueActivity.class);
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
} }

View File

@ -48,6 +48,7 @@ public final class ServiceHelper {
case "all": case "all":
return c.getString(R.string.all); return c.getString(R.string.all);
case "videos": case "videos":
case "sepia_videos":
case "music_videos": case "music_videos":
return c.getString(R.string.videos_string); return c.getString(R.string.videos_string);
case "channels": case "channels":

View File

@ -1,6 +1,7 @@
package org.schabi.newpipe.util; package org.schabi.newpipe.util;
import android.content.Context; import android.content.Context;
import android.net.Uri;
import androidx.fragment.app.Fragment; import androidx.fragment.app.Fragment;
@ -70,6 +71,15 @@ public enum StreamDialogEntry {
} }
}), }),
play_with_kodi(R.string.play_with_kodi_title, (fragment, item) -> {
final Uri videoUrl = Uri.parse(item.getUrl());
try {
NavigationHelper.playWithKore(fragment.getContext(), videoUrl);
} catch (final Exception e) {
KoreUtil.showInstallKoreDialog(fragment.getActivity());
}
}),
share(R.string.share, (fragment, item) -> share(R.string.share, (fragment, item) ->
ShareUtils.shareUrl(fragment.getContext(), item.getName(), item.getUrl())); ShareUtils.shareUrl(fragment.getContext(), item.getName(), item.getUrl()));

View File

@ -4,7 +4,9 @@ import java.io.BufferedInputStream;
import java.io.File; import java.io.File;
import java.io.FileInputStream; import java.io.FileInputStream;
import java.io.FileOutputStream; import java.io.FileOutputStream;
import java.io.IOException;
import java.util.zip.ZipEntry; import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import java.util.zip.ZipInputStream; import java.util.zip.ZipInputStream;
import java.util.zip.ZipOutputStream; import java.util.zip.ZipOutputStream;
@ -99,4 +101,12 @@ public final class ZipHelper {
return found; return found;
} }
} }
public static boolean isValidZipFile(final String filePath) {
try (ZipFile ignored = new ZipFile(filePath)) {
return true;
} catch (final IOException ioe) {
return false;
}
}
} }

View File

@ -22,6 +22,7 @@ import java.net.SocketTimeoutException;
import java.net.URL; import java.net.URL;
import java.net.UnknownHostException; import java.net.UnknownHostException;
import java.nio.channels.ClosedByInterruptException; import java.nio.channels.ClosedByInterruptException;
import java.util.Objects;
import javax.net.ssl.SSLException; import javax.net.ssl.SSLException;
@ -154,8 +155,8 @@ public class DownloadMission extends Mission {
public transient Thread init = null; public transient Thread init = null;
public DownloadMission(String[] urls, StoredFileHelper storage, char kind, Postprocessing psInstance) { public DownloadMission(String[] urls, StoredFileHelper storage, char kind, Postprocessing psInstance) {
if (urls == null) throw new NullPointerException("urls is null"); if (Objects.requireNonNull(urls).length < 1)
if (urls.length < 1) throw new IllegalArgumentException("urls is empty"); throw new IllegalArgumentException("urls array is empty");
this.urls = urls; this.urls = urls;
this.kind = kind; this.kind = kind;
this.offsets = new long[urls.length]; this.offsets = new long[urls.length];

View File

@ -8,6 +8,7 @@ import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.net.HttpURLConnection; import java.net.HttpURLConnection;
import java.nio.channels.ClosedByInterruptException; import java.nio.channels.ClosedByInterruptException;
import java.util.Objects;
import us.shandian.giga.get.DownloadMission.Block; import us.shandian.giga.get.DownloadMission.Block;
import us.shandian.giga.get.DownloadMission.HttpError; import us.shandian.giga.get.DownloadMission.HttpError;
@ -29,8 +30,7 @@ public class DownloadRunnable extends Thread {
private HttpURLConnection mConn; private HttpURLConnection mConn;
DownloadRunnable(DownloadMission mission, int id) { DownloadRunnable(DownloadMission mission, int id) {
if (mission == null) throw new NullPointerException("mission is null"); mMission = Objects.requireNonNull(mission);
mMission = mission;
mId = id; mId = id;
} }

View File

@ -21,7 +21,7 @@ import static us.shandian.giga.get.DownloadMission.ERROR_HTTP_FORBIDDEN;
* Single-threaded fallback mode * Single-threaded fallback mode
*/ */
public class DownloadRunnableFallback extends Thread { public class DownloadRunnableFallback extends Thread {
private static final String TAG = "DownloadRunnableFallbac"; private static final String TAG = "DownloadRunnableFallback";
private final DownloadMission mMission; private final DownloadMission mMission;

View File

@ -12,6 +12,7 @@ import androidx.annotation.NonNull;
import java.io.File; import java.io.File;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Objects;
import us.shandian.giga.get.DownloadMission; import us.shandian.giga.get.DownloadMission;
import us.shandian.giga.get.FinishedMission; import us.shandian.giga.get.FinishedMission;
@ -140,9 +141,7 @@ public class FinishedMissionStore extends SQLiteOpenHelper {
} }
private FinishedMission getMissionFromCursor(Cursor cursor) { private FinishedMission getMissionFromCursor(Cursor cursor) {
if (cursor == null) throw new NullPointerException("cursor is null"); String kind = Objects.requireNonNull(cursor).getString(cursor.getColumnIndex(KEY_KIND));
String kind = cursor.getString(cursor.getColumnIndex(KEY_KIND));
if (kind == null || kind.isEmpty()) kind = "?"; if (kind == null || kind.isEmpty()) kind = "?";
String path = cursor.getString(cursor.getColumnIndexOrThrow(KEY_PATH)); String path = cursor.getString(cursor.getColumnIndexOrThrow(KEY_PATH));
@ -186,15 +185,13 @@ public class FinishedMissionStore extends SQLiteOpenHelper {
} }
public void addFinishedMission(DownloadMission downloadMission) { public void addFinishedMission(DownloadMission downloadMission) {
if (downloadMission == null) throw new NullPointerException("downloadMission is null"); ContentValues values = getValuesOfMission(Objects.requireNonNull(downloadMission));
SQLiteDatabase database = getWritableDatabase(); SQLiteDatabase database = getWritableDatabase();
ContentValues values = getValuesOfMission(downloadMission);
database.insert(FINISHED_TABLE_NAME, null, values); database.insert(FINISHED_TABLE_NAME, null, values);
} }
public void deleteMission(Mission mission) { public void deleteMission(Mission mission) {
if (mission == null) throw new NullPointerException("mission is null"); String ts = String.valueOf(Objects.requireNonNull(mission).timestamp);
String ts = String.valueOf(mission.timestamp);
SQLiteDatabase database = getWritableDatabase(); SQLiteDatabase database = getWritableDatabase();
@ -212,9 +209,8 @@ public class FinishedMissionStore extends SQLiteOpenHelper {
} }
public void updateMission(Mission mission) { public void updateMission(Mission mission) {
if (mission == null) throw new NullPointerException("mission is null"); ContentValues values = getValuesOfMission(Objects.requireNonNull(mission));
SQLiteDatabase database = getWritableDatabase(); SQLiteDatabase database = getWritableDatabase();
ContentValues values = getValuesOfMission(mission);
String ts = String.valueOf(mission.timestamp); String ts = String.valueOf(mission.timestamp);
int rowsAffected; int rowsAffected;

View File

@ -7,6 +7,7 @@ import org.schabi.newpipe.streams.io.SharpStream;
import java.io.File; import java.io.File;
import java.io.FileNotFoundException; import java.io.FileNotFoundException;
import java.io.IOException; import java.io.IOException;
import java.util.Objects;
public class CircularFileWriter extends SharpStream { public class CircularFileWriter extends SharpStream {
@ -27,9 +28,7 @@ public class CircularFileWriter extends SharpStream {
private BufferedFile aux; private BufferedFile aux;
public CircularFileWriter(SharpStream target, File temp, OffsetChecker checker) throws IOException { public CircularFileWriter(SharpStream target, File temp, OffsetChecker checker) throws IOException {
if (checker == null) { Objects.requireNonNull(checker);
throw new NullPointerException("checker is null");
}
if (!temp.exists()) { if (!temp.exists()) {
if (!temp.createNewFile()) { if (!temp.createNewFile()) {

View File

@ -25,6 +25,7 @@ import android.os.IBinder;
import android.os.Message; import android.os.Message;
import android.os.Parcelable; import android.os.Parcelable;
import androidx.core.app.ServiceCompat;
import androidx.core.content.ContextCompat; import androidx.core.content.ContextCompat;
import androidx.preference.PreferenceManager; import androidx.preference.PreferenceManager;
import android.util.Log; import android.util.Log;
@ -235,7 +236,7 @@ public class DownloadManagerService extends Service {
Log.d(TAG, "Destroying"); Log.d(TAG, "Destroying");
} }
stopForeground(true); ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE);
if (mNotificationManager != null && downloadDoneNotification != null) { if (mNotificationManager != null && downloadDoneNotification != null) {
downloadDoneNotification.setDeleteIntent(null);// prevent NewPipe running when is killed, cleared from recent, etc downloadDoneNotification.setDeleteIntent(null);// prevent NewPipe running when is killed, cleared from recent, etc
@ -363,7 +364,7 @@ public class DownloadManagerService extends Service {
if (state) { if (state) {
startForeground(FOREGROUND_NOTIFICATION_ID, mNotification); startForeground(FOREGROUND_NOTIFICATION_ID, mNotification);
} else { } else {
stopForeground(true); ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE);
} }
manageLock(state); manageLock(state);

View File

@ -1,7 +1,7 @@
package us.shandian.giga.ui.adapter; package us.shandian.giga.ui.adapter;
import android.annotation.SuppressLint; import android.annotation.SuppressLint;
import android.app.ProgressDialog; import android.app.NotificationManager;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.graphics.Color; import android.graphics.Color;
@ -26,6 +26,8 @@ import android.widget.Toast;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.StringRes; import androidx.annotation.StringRes;
import androidx.appcompat.app.AlertDialog; import androidx.appcompat.app.AlertDialog;
import androidx.core.app.NotificationCompat;
import androidx.core.content.ContextCompat;
import androidx.core.content.FileProvider; import androidx.core.content.FileProvider;
import androidx.core.view.ViewCompat; import androidx.core.view.ViewCompat;
import androidx.recyclerview.widget.DiffUtil; import androidx.recyclerview.widget.DiffUtil;
@ -91,6 +93,7 @@ public class MissionAdapter extends Adapter<ViewHolder> implements Handler.Callb
private static final String DEFAULT_MIME_TYPE = "*/*"; private static final String DEFAULT_MIME_TYPE = "*/*";
private static final String UNDEFINED_ETA = "--:--"; private static final String UNDEFINED_ETA = "--:--";
private static final int HASH_NOTIFICATION_ID = 123790;
static { static {
ALGORITHMS.put(R.id.md5, "MD5"); ALGORITHMS.put(R.id.md5, "MD5");
@ -678,28 +681,28 @@ public class MissionAdapter extends Adapter<ViewHolder> implements Handler.Callb
return true; return true;
case R.id.md5: case R.id.md5:
case R.id.sha1: case R.id.sha1:
ProgressDialog progressDialog = null; final NotificationManager notificationManager
if (mContext != null) { = ContextCompat.getSystemService(mContext, NotificationManager.class);
// Create dialog final NotificationCompat.Builder progressNotificationBuilder
progressDialog = new ProgressDialog(mContext); = new NotificationCompat.Builder(mContext,
progressDialog.setCancelable(false); mContext.getString(R.string.hash_channel_id))
progressDialog.setMessage(mContext.getString(R.string.msg_wait)); .setPriority(NotificationCompat.PRIORITY_HIGH)
progressDialog.show(); .setSmallIcon(R.drawable.ic_newpipe_triangle_white)
} .setContentTitle(mContext.getString(R.string.msg_calculating_hash))
final ProgressDialog finalProgressDialog = progressDialog; .setContentText(mContext.getString(R.string.msg_wait))
.setProgress(0, 0, true)
.setOngoing(true);
notificationManager.notify(HASH_NOTIFICATION_ID, progressNotificationBuilder
.build());
final StoredFileHelper storage = h.item.mission.storage; final StoredFileHelper storage = h.item.mission.storage;
compositeDisposable.add( compositeDisposable.add(
Observable.fromCallable(() -> Utility.checksum(storage, ALGORITHMS.get(id))) Observable.fromCallable(() -> Utility.checksum(storage, ALGORITHMS.get(id)))
.subscribeOn(Schedulers.computation()) .subscribeOn(Schedulers.computation())
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.subscribe(result -> { .subscribe(result -> {
if (finalProgressDialog != null) { Utility.copyToClipboard(mContext, result);
Utility.copyToClipboard(finalProgressDialog.getContext(), notificationManager.cancel(HASH_NOTIFICATION_ID);
result);
if (mContext != null) {
finalProgressDialog.dismiss();
}
}
}) })
); );
return true; return true;

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