diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index f9201f948..e74a5a761 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -15,7 +15,7 @@ Oh no, a bug! It happens. Thanks for reporting an issue with NewPipe. To make it ### Checklist - + - [x] I am using the latest version - x.xx.x - [ ] I checked, but didn't find any duplicates (open OR closed) of this issue in the repo. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index c4d378d14..361c8057f 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -11,7 +11,7 @@ assignees: '' ### Checklist - + - [x] I checked, but didn't find any duplicates (open OR closed) of this issue in the repo. - [ ] I have read the contribution guidelines given at https://github.com/TeamNewPipe/NewPipe/blob/HEAD/.github/CONTRIBUTING.md. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 000000000..6419c65dd --- /dev/null +++ b/.github/workflows/ci.yml @@ -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 diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index e69de29bb..000000000 diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 1714c70d5..000000000 --- a/.travis.yml +++ /dev/null @@ -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: - - '.+' diff --git a/README.ko.md b/README.ko.md index bb6bd653b..f5dc31ced 100644 --- a/README.ko.md +++ b/README.ko.md @@ -1,4 +1,4 @@ -

+

NewPipe

A libre lightweight streaming frontend for Android.

@@ -6,17 +6,17 @@

- +


ScreenshotsDescriptionFeaturesUpdatesContributionDonateLicense

-

WebsiteBlogFAQPress

+

WebsiteBlogFAQPress


-*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).* 경고: 이 버전은 베타 버전이므로, 버그가 발생할 수도 있습니다. 만약 버그가 발생하였다면, 우리의 GITHUB 저장소에서 ISSUE를 열람하여 주십시오. @@ -86,7 +86,7 @@ NewPipe 코드의 변경이 있을 때(기능 추가 또는 버그 수정으로 1. 직접 디버그 APK를 생성할 수 있습니다. 이 방법은 당신의 기기에서 새로운 기능을 얻을 수 있는 가장 빠른 방법이지만, 꽤 많이 복잡합니다. 따라서 우리는 다른 방법들 중 하나를 사용하는 것을 추천합니다. 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를 다운받고 이것을 설치할 수 있습니다. 4. F-Droid를 통해 업데이트 할 수 있습니다. F-Droid는 변화를 인식하고, 스스로 APK를 생성하고, 이것에 서명하고, 사용자들에서 업데이트를 전달해야만 하기 때문에, 이것은 업데이트를 받는 가장 느린 방법입니다. @@ -98,7 +98,7 @@ NewPipe 코드의 변경이 있을 때(기능 추가 또는 버그 수정으로 1. 당신의 기록, 구독, 그리고 재생목록을 유지할 수 있도록 Settings > Content > Export Database 를 통해 데이터를 백업하십시오. 2. NewPipe를 삭제하십시오. 3. 새로운 소스에서 APK를 다운로드하고 이것을 설치하십시오. -4. Step 1의 Settings > Content > Export Database 을 통해 데이터를 불러오십시오. +4. Step 1의 Settings > Content > Import Database 을 통해 데이터를 불러오십시오. ## Contribution 당신이 아이디어, 번역, 디자인 변경, 코드 정리, 또는 정말 큰 코드 수정에 대한 의견이 있다면, 도움은 항상 환영합니다. @@ -111,7 +111,7 @@ NewPipe 코드의 변경이 있을 때(기능 추가 또는 버그 수정으로 ## Donate -만약 NewPipe가 마음에 들었다면, 우리는 기부에 대해 기꺼이 환영합니다. bitcoin을 보내거나, Bountysource 또는 Liberapay를 통해 기부할 수 있습니다. NewPipe에 기부하는 것에 대한 자세한 정보를 원한다면, 우리의 [웹사이트](https://newpipe.schabi.org/donate)를 방문하여 주십시오. +만약 NewPipe가 마음에 들었다면, 우리는 기부에 대해 기꺼이 환영합니다. bitcoin을 보내거나, Bountysource 또는 Liberapay를 통해 기부할 수 있습니다. NewPipe에 기부하는 것에 대한 자세한 정보를 원한다면, 우리의 [웹사이트](https://newpipe.net/donate)를 방문하여 주십시오. @@ -134,7 +134,7 @@ NewPipe 코드의 변경이 있을 때(기능 추가 또는 버그 수정으로 ## Privacy Policy NewPipe 프로젝트는 미디어 웹 서비스를 사용하는 것에 대한 사적의, 익명의 경험을 제공하는 것을 목표로 하고 있습니다. -그러므로, 앱은 당신의 동의 없이 어떤 데이터도 수집하지 않습니다. NewPipe의 개인정보보호정책은 당신이 충돌 리포트를 보내거나, 또는 우리의 블로그에 글을 남길 때 어떤 데이터가 보내지고 저장되는지에 대해 상세히 설명합니다. 이 문서는 [여기](https://newpipe.schabi.org/legal/privacy/)에서 확인할 수 있습니다. +그러므로, 앱은 당신의 동의 없이 어떤 데이터도 수집하지 않습니다. NewPipe의 개인정보보호정책은 당신이 충돌 리포트를 보내거나, 또는 우리의 블로그에 글을 남길 때 어떤 데이터가 보내지고 저장되는지에 대해 상세히 설명합니다. 이 문서는 [여기](https://newpipe.net/legal/privacy/)에서 확인할 수 있습니다. ## License [![GNU GPLv3 Image](https://www.gnu.org/graphics/gplv3-127x51.png)](http://www.gnu.org/licenses/gpl-3.0.en.html) diff --git a/README.md b/README.md index 2f3612bbb..d61c65653 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -

+

NewPipe

A libre lightweight streaming frontend for Android.

@@ -8,17 +8,17 @@

- +


ScreenshotsDescriptionFeaturesInstallationContributionDonateLicense

-

WebsiteBlogFAQPress

+

WebsiteBlogFAQPress


-*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).* WARNING: THIS IS A BETA VERSION, THEREFORE YOU MAY ENCOUNTER BUGS. IF YOU DO, OPEN AN ISSUE VIA OUR GITHUB REPOSITORY. @@ -111,7 +111,7 @@ If you'd like to get involved, check our [contribution notes](.github/CONTRIBUTI ## 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).
@@ -134,7 +134,7 @@ If you like NewPipe we'd be happy about a donation. You can either send bitcoin ## Privacy Policy 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 [![GNU GPLv3 Image](https://www.gnu.org/graphics/gplv3-127x51.png)](http://www.gnu.org/licenses/gpl-3.0.en.html) diff --git a/README.pt.br.md b/README.pt.br.md new file mode 100644 index 000000000..4b2662cef --- /dev/null +++ b/README.pt.br.md @@ -0,0 +1,140 @@ +

+

NewPipe

+

Uma interface de streaming leve e gratuita para Android.

+

+ +

+ + + + + + +

+
+

ScreenshotsDescriçãoCaracterísticasAtualizaçõesContribuiçãoDoarLicença

+

SiteBlogFAQPress

+
+ +*Read this in other languages: [English](README.md), [한국어](README.ko.md), [Soomaali](README.so.md), [Português Brasil](README.pt.br.md).* + +AVISO: ESTA É UMA VERSÃO BETA, PORTANTO, VOCÊ PODE ENCONTRAR BUGS. ENCONTROU ALGUM, ABRA UM ISSUE ATRAVÉS DO NOSSO REPOSITÓRIO GITHUB. + +COLOCAR NEWPIPE OU QUALQUER FORK DELE NA GOOGLE PLAY STORE VIOLA SEUS TERMOS E CONDIÇÕES. + +## Screenshots + +[](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_01.png) +[](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_02.png) +[](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_03.png) +[](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_04.png) +[](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_05.png) +[](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_06.png) +[](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_07.png) +[](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_08.png) +[](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_09.png) +[](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_10.png) +[](fastlane/metadata/android/en-US/images/tenInchScreenshots/shot_11.png) +[](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). + + +Translation status + + +## 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). + +
+ + + + + + + + + + + + + + + +
BitcoinBitcoin QR code16A9J59ahMRqkLSZjhYj33n9j3fMztFxnh
LiberapayVisit NewPipe at liberapay.comDonate via Liberapay
BountysourceVisit NewPipe at bountysource.comCheck out how many bounties you can earn.
+ +## 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. diff --git a/README.so.md b/README.so.md new file mode 100644 index 000000000..e62acf988 --- /dev/null +++ b/README.so.md @@ -0,0 +1,136 @@ +

+

NewPipe

+

App bilaash ah oo fudud looguna talagalay in aaladaha nidaamka Android-ka ku shaqeeya wax loogu daawado.

+

+ +

+ + + + + + +

+
+

Sawir-shaashadeedFaahfaahinWaxqabadkaCusboonaysiinKusoo KordhinUgu DeeqLaysinka

+

Website-kaMaqaaladaSu'aalaha Aalaa La-iswaydiiyoWarbaahinta

+
+ +*Ku akhri luuqad kale: [English](README.md), [한국어](README.ko.md), [Soomaali](README.so.md), [Português Brasil](README.pt.br.md).* + +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. + +NEWPIPE AMA KUWA KU SALAYSAN PLAYSTORE-KA IN LA GALIYO WAXAY KA HOR IMANAYSAA SHARCIGA IYO SHURUUDAHA AY LEEYIHIIN. + +## Sawir-shaashadeed + +[](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_01.png) +[](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_02.png) +[](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_03.png) +[](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_04.png) +[](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_05.png) +[](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_06.png) +[](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_07.png) +[](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_08.png) +[](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_09.png) +[](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_10.png) +[](fastlane/metadata/android/en-US/images/tenInchScreenshots/shot_11.png) +[](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). + + +Translation status + + +## 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). + + + + + + + + + + + + + + + + + +
BitcoinBitcoin QR code16A9J59ahMRqkLSZjhYj33n9j3fMztFxnh
LiberapayVisit NewPipe at liberapay.comDonate via Liberapay
BountysourceVisit NewPipe at bountysource.comCheck out how many bounties you can earn.
+ +## 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. diff --git a/app/build.gradle b/app/build.gradle index 2ffb788d6..ab3449784 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -13,8 +13,8 @@ android { resValue "string", "app_name", "NewPipe" minSdkVersion 19 targetSdkVersion 29 - versionCode 959 - versionName "0.20.5" + versionCode 962 + versionName "0.20.8" multiDexEnabled true @@ -85,11 +85,15 @@ android { sourceSets { androidTest.assets.srcDirs += files("$projectDir/schemas".toString()) } + + buildFeatures { + viewBinding true + } } ext { icepickVersion = '3.2.0' - checkstyleVersion = '8.37' + checkstyleVersion = '8.38' stethoVersion = '1.5.1' leakCanaryVersion = '2.5' exoPlayerVersion = '2.11.8' @@ -98,6 +102,7 @@ ext { groupieVersion = '2.8.1' markwonVersion = '4.6.0' googleAutoServiceVersion = '1.0-rc7' + mockitoVersion = '3.6.0' } configurations { @@ -162,7 +167,7 @@ dependencies { kapt "frankiesardo:icepick-processor:${icepickVersion}" 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-okhttp3:${stethoVersion}" @@ -175,7 +180,7 @@ dependencies { // NewPipe dependencies // 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 "org.jsoup:jsoup:1.13.1" @@ -199,6 +204,8 @@ dependencies { implementation 'androidx.core:core-ktx:1.3.2' implementation 'androidx.documentfile:documentfile:1.0.1' 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-viewmodel:${androidxLifecycleVersion}" @@ -229,7 +236,8 @@ dependencies { implementation "org.ocpsoft.prettytime:prettytime:4.0.6.Final" 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.room:room-testing:${androidxRoomVersion}" diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 1b9de5620..ac33a7f0f 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -53,7 +53,7 @@ @@ -227,20 +227,18 @@ + - - - - - + + + + + + + + - - - - - - @@ -268,7 +266,7 @@ - + @@ -320,6 +318,8 @@ android:name=".RouterActivity$FetcherService" android:exported="false" /> + + diff --git a/app/src/main/assets/epl1.html b/app/src/main/assets/epl1.html new file mode 100644 index 000000000..7123552dd --- /dev/null +++ b/app/src/main/assets/epl1.html @@ -0,0 +1,245 @@ + + + + + + + Eclipse Public License - Version 1.0 + + + + + +

Eclipse Public License - v 1.0

+ +

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.

+ +

1. DEFINITIONS

+ +

"Contribution" means:

+ +

a) in the case of the initial Contributor, the initial + code and documentation distributed under this Agreement, and

+

b) in the case of each subsequent Contributor:

+

i) changes to the Program, and

+

ii) additions to the Program;

+

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.

+ +

"Contributor" means any person or entity that distributes + the Program.

+ +

"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.

+ +

"Program" means the Contributions distributed in accordance + with this Agreement.

+ +

"Recipient" means anyone who receives the Program under + this Agreement, including all Contributors.

+ +

2. GRANT OF RIGHTS

+ +

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.

+ +

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.

+ +

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.

+ +

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.

+ +

3. REQUIREMENTS

+ +

A Contributor may choose to distribute the Program in object code + form under its own license agreement, provided that:

+ +

a) it complies with the terms and conditions of this + Agreement; and

+ +

b) its license agreement:

+ +

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;

+ +

ii) effectively excludes on behalf of all Contributors + all liability for damages, including direct, indirect, special, + incidental and consequential damages, such as lost profits;

+ +

iii) states that any provisions which differ from this + Agreement are offered by that Contributor alone and not by any other + party; and

+ +

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.

+ +

When the Program is made available in source code form:

+ +

a) it must be made available under this Agreement; and

+ +

b) a copy of this Agreement must be included with each + copy of the Program.

+ +

Contributors may not remove or alter any copyright notices contained + within the Program.

+ +

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.

+ +

4. COMMERCIAL DISTRIBUTION

+ +

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.

+ +

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.

+ +

5. NO WARRANTY

+ +

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.

+ +

6. DISCLAIMER OF LIABILITY

+ +

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.

+ +

7. GENERAL

+ +

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.

+ +

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.

+ +

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.

+ +

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.

+ +

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.

+ + + + \ No newline at end of file diff --git a/app/src/main/java/com/google/android/material/appbar/FlingBehavior.java b/app/src/main/java/com/google/android/material/appbar/FlingBehavior.java index 3518aa139..6106d0437 100644 --- a/app/src/main/java/com/google/android/material/appbar/FlingBehavior.java +++ b/app/src/main/java/com/google/android/material/appbar/FlingBehavior.java @@ -27,7 +27,7 @@ public final class FlingBehavior extends AppBarLayout.Behavior { private boolean allowScroll = true; private final Rect globalRect = new Rect(); private final List skipInterceptionOfElements = Arrays.asList( - R.id.playQueuePanel, R.id.playbackSeekBar, + R.id.itemsListPanel, R.id.playbackSeekBar, R.id.playPauseButton, R.id.playPreviousButton, R.id.playNextButton); @Override diff --git a/app/src/main/java/org/schabi/newpipe/App.java b/app/src/main/java/org/schabi/newpipe/App.java index de401d4f5..342ce3498 100644 --- a/app/src/main/java/org/schabi/newpipe/App.java +++ b/app/src/main/java/org/schabi/newpipe/App.java @@ -6,38 +6,13 @@ import android.content.Context; import android.content.SharedPreferences; import android.os.Build; import android.util.Log; - import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.multidex.MultiDexApplication; import androidx.preference.PreferenceManager; - import com.nostra13.universalimageloader.cache.memory.impl.LRULimitedMemoryCache; import com.nostra13.universalimageloader.core.ImageLoader; 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.exceptions.CompositeException; 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.functions.Consumer; 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 @@ -67,8 +62,10 @@ import io.reactivex.rxjava3.plugins.RxJavaPlugins; public class App extends MultiDexApplication { protected static final String TAG = App.class.toString(); private static App app; + public static final String PACKAGE_NAME = BuildConfig.APPLICATION_ID; - @Nullable private Disposable disposable = null; + @Nullable + private Disposable disposable = null; @NonNull public static App getApp() { @@ -91,9 +88,9 @@ public class App extends MultiDexApplication { SettingsActivity.initSettings(this); NewPipe.init(getDownloader(), - Localization.getPreferredLocalization(this), - Localization.getPreferredContentCountry(this)); - Localization.init(getApplicationContext()); + Localization.getPreferredLocalization(this), + Localization.getPreferredContentCountry(this)); + Localization.initPrettyTime(Localization.resolvePrettyTime(getApplicationContext())); StateSaver.init(this); initNotificationChannels(); @@ -242,8 +239,9 @@ public class App extends MultiDexApplication { String name = getString(R.string.notification_channel_name); String description = getString(R.string.notification_channel_description); - // Keep this below DEFAULT to avoid making noise on every notification update - final int importance = NotificationManager.IMPORTANCE_LOW; + // Keep this below DEFAULT to avoid making noise on every notification update for the main + // and update channels + int importance = NotificationManager.IMPORTANCE_LOW; final NotificationChannel mainChannel = new NotificationChannel(id, name, importance); mainChannel.setDescription(description); @@ -255,9 +253,17 @@ public class App extends MultiDexApplication { final NotificationChannel appUpdateChannel = new NotificationChannel(id, name, importance); 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); notificationManager.createNotificationChannels(Arrays.asList(mainChannel, - appUpdateChannel)); + appUpdateChannel, hashChannel)); } protected boolean isDisposedRxExceptionsReported() { diff --git a/app/src/main/java/org/schabi/newpipe/CheckForNewAppVersion.java b/app/src/main/java/org/schabi/newpipe/CheckForNewAppVersion.java index 4dfd1bdce..0fecc3f96 100644 --- a/app/src/main/java/org/schabi/newpipe/CheckForNewAppVersion.java +++ b/app/src/main/java/org/schabi/newpipe/CheckForNewAppVersion.java @@ -48,13 +48,13 @@ public final class CheckForNewAppVersion { 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"; - 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 - * @return String with the apk's SHA1 fingeprint in hexadecimal + * @return String with the APK's SHA1 fingerprint in hexadecimal */ @NonNull private static String getCertificateSHA1Fingerprint(@NonNull final Application application) { diff --git a/app/src/main/java/org/schabi/newpipe/MainActivity.java b/app/src/main/java/org/schabi/newpipe/MainActivity.java index f51ecf2d3..5cecbc6d2 100644 --- a/app/src/main/java/org/schabi/newpipe/MainActivity.java +++ b/app/src/main/java/org/schabi/newpipe/MainActivity.java @@ -20,6 +20,8 @@ package org.schabi.newpipe; +import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage; + import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; @@ -39,26 +41,27 @@ import android.view.View; import android.view.ViewGroup; import android.widget.AdapterView; import android.widget.ArrayAdapter; -import android.widget.Button; import android.widget.FrameLayout; -import android.widget.ImageView; import android.widget.Spinner; -import android.widget.TextView; - import androidx.annotation.NonNull; import androidx.appcompat.app.ActionBar; import androidx.appcompat.app.ActionBarDrawerToggle; import androidx.appcompat.app.AppCompatActivity; -import androidx.appcompat.widget.Toolbar; +import androidx.core.app.ActivityCompat; import androidx.core.view.GravityCompat; import androidx.drawerlayout.widget.DrawerLayout; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentManager; import androidx.preference.PreferenceManager; - 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.StreamingService; 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.detail.VideoDetailFragment; 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.helper.PlayerHolder; 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.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 { private static final String TAG = "MainActivity"; 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 DrawerLayout drawer; - private NavigationView drawerItems; - private ImageView headerServiceIcon; - private TextView headerServiceView; - private Button toggleServiceButton; private boolean servicesShown = false; - private ImageView serviceArrow; private BroadcastReceiver broadcastReceiver; @@ -129,7 +125,7 @@ public class MainActivity extends AppCompatActivity { + "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) { TLSSocketFactoryCompat.setAsDefault(); } @@ -137,13 +133,19 @@ public class MainActivity extends AppCompatActivity { assureCorrectAppLanguage(this); 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) { initFragments(); } - setSupportActionBar(findViewById(R.id.toolbar)); + setSupportActionBar(toolbarLayoutBinding.toolbar); try { setupDrawer(); } catch (final Exception e) { @@ -157,10 +159,6 @@ public class MainActivity extends AppCompatActivity { } private void setupDrawer() throws Exception { - final Toolbar toolbar = findViewById(R.id.toolbar); - drawer = findViewById(R.id.drawer_layout); - drawerItems = findViewById(R.id.navigation); - //Tabs final int currentServiceId = ServiceHelper.getSelectedServiceId(this); final StreamingService service = NewPipe.getService(currentServiceId); @@ -168,43 +166,43 @@ public class MainActivity extends AppCompatActivity { int kioskId = 0; for (final String ks : service.getKioskList().getAvailableKiosks()) { - drawerItems.getMenu() + drawerLayoutBinding.navigation.getMenu() .add(R.id.menu_tabs_group, kioskId, 0, KioskTranslator .getTranslatedKioskName(ks, this)) .setIcon(KioskTranslator.getKioskIcon(ks, this)); kioskId++; } - drawerItems.getMenu() + drawerLayoutBinding.navigation.getMenu() .add(R.id.menu_tabs_group, ITEM_ID_SUBSCRIPTIONS, ORDER, R.string.tab_subscriptions) .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) .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) .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) .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) .setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.ic_history)); //Settings and About - drawerItems.getMenu() + drawerLayoutBinding.navigation.getMenu() .add(R.id.menu_options_about_group, ITEM_ID_SETTINGS, ORDER, R.string.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) .setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.ic_info_outline)); - toggle = new ActionBarDrawerToggle(this, drawer, toolbar, R.string.drawer_open, - R.string.drawer_close); + toggle = new ActionBarDrawerToggle(this, mainBinding.getRoot(), + toolbarLayoutBinding.toolbar, R.string.drawer_open, R.string.drawer_close); toggle.syncState(); - drawer.addDrawerListener(toggle); - drawer.addDrawerListener(new DrawerLayout.SimpleDrawerListener() { + mainBinding.getRoot().addDrawerListener(toggle); + mainBinding.getRoot().addDrawerListener(new DrawerLayout.SimpleDrawerListener() { private int lastService; @Override @@ -218,12 +216,12 @@ public class MainActivity extends AppCompatActivity { toggleServices(); } 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(); } @@ -246,15 +244,17 @@ public class MainActivity extends AppCompatActivity { return false; } - drawer.closeDrawers(); + mainBinding.getRoot().closeDrawers(); return true; } private void changeService(final MenuItem item) { - drawerItems.getMenu().getItem(ServiceHelper.getSelectedServiceId(this)) + drawerLayoutBinding.navigation.getMenu() + .getItem(ServiceHelper.getSelectedServiceId(this)) .setChecked(false); ServiceHelper.setSelectedServiceId(this, item.getItemId()); - drawerItems.getMenu().getItem(ServiceHelper.getSelectedServiceId(this)) + drawerLayoutBinding.navigation.getMenu() + .getItem(ServiceHelper.getSelectedServiceId(this)) .setChecked(true); } @@ -306,26 +306,19 @@ public class MainActivity extends AppCompatActivity { } private void setupDrawerHeader() { - final NavigationView navigationView = findViewById(R.id.navigation); - 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()); + drawerHeaderBinding.drawerHeaderActionButton.setOnClickListener(view -> toggleServices()); // If the current app name is bigger than the default "NewPipe" (7 chars), // let the text view grow a little more as well. if (getString(R.string.app_name).length() > "NewPipe".length()) { - final TextView headerTitle = hView.findViewById(R.id.drawer_header_newpipe_title); - final ViewGroup.LayoutParams layoutParams = headerTitle.getLayoutParams(); + final ViewGroup.LayoutParams layoutParams = + drawerHeaderBinding.drawerHeaderNewpipeTitle.getLayoutParams(); layoutParams.width = ViewGroup.LayoutParams.WRAP_CONTENT; - headerTitle.setLayoutParams(layoutParams); - headerTitle.setMaxLines(2); - headerTitle.setMinWidth(getResources() + drawerHeaderBinding.drawerHeaderNewpipeTitle.setLayoutParams(layoutParams); + drawerHeaderBinding.drawerHeaderNewpipeTitle.setMaxLines(2); + drawerHeaderBinding.drawerHeaderNewpipeTitle.setMinWidth(getResources() .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)); } } @@ -333,9 +326,9 @@ public class MainActivity extends AppCompatActivity { private void toggleServices() { servicesShown = !servicesShown; - drawerItems.getMenu().removeGroup(R.id.menu_services_group); - drawerItems.getMenu().removeGroup(R.id.menu_tabs_group); - drawerItems.getMenu().removeGroup(R.id.menu_options_about_group); + drawerLayoutBinding.navigation.getMenu().removeGroup(R.id.menu_services_group); + drawerLayoutBinding.navigation.getMenu().removeGroup(R.id.menu_tabs_group); + drawerLayoutBinding.navigation.getMenu().removeGroup(R.id.menu_options_about_group); if (servicesShown) { showServices(); @@ -349,13 +342,13 @@ public class MainActivity extends AppCompatActivity { } 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()) { final String title = s.getServiceInfo().getName() + (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) .setIcon(ServiceHelper.getIcon(s.getServiceId())); @@ -364,21 +357,22 @@ public class MainActivity extends AppCompatActivity { enhancePeertubeMenu(s, menuItem); } } - drawerItems.getMenu().getItem(ServiceHelper.getSelectedServiceId(this)) + drawerLayoutBinding.navigation.getMenu() + .getItem(ServiceHelper.getSelectedServiceId(this)) .setChecked(true); } private void enhancePeertubeMenu(final StreamingService s, final MenuItem menuItem) { - final PeertubeInstance currentInstace = PeertubeHelper.getCurrentInstance(); - menuItem.setTitle(currentInstace.getName() + (ServiceHelper.isBeta(s) ? " (beta)" : "")); - final Spinner spinner = (Spinner) LayoutInflater.from(this) - .inflate(R.layout.instance_spinner_layout, null); + final PeertubeInstance currentInstance = PeertubeHelper.getCurrentInstance(); + menuItem.setTitle(currentInstance.getName() + (ServiceHelper.isBeta(s) ? " (beta)" : "")); + final Spinner spinner = InstanceSpinnerLayoutBinding.inflate(LayoutInflater.from(this)) + .getRoot(); final List instances = PeertubeHelper.getInstanceList(this); final List items = new ArrayList<>(); int defaultSelect = 0; for (final PeertubeInstance instance : instances) { items.add(instance.getName()); - if (instance.getUrl().equals(currentInstace.getUrl())) { + if (instance.getUrl().equals(currentInstance.getUrl())) { defaultSelect = items.size() - 1; } } @@ -397,7 +391,7 @@ public class MainActivity extends AppCompatActivity { } PeertubeHelper.selectInstance(newInstance, getApplicationContext()); changeService(menuItem); - drawer.closeDrawers(); + mainBinding.getRoot().closeDrawers(); new Handler(Looper.getMainLooper()).postDelayed(() -> { getSupportFragmentManager().popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE); @@ -414,7 +408,7 @@ public class MainActivity extends AppCompatActivity { } 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 final int currentServiceId = ServiceHelper.getSelectedServiceId(this); @@ -423,34 +417,34 @@ public class MainActivity extends AppCompatActivity { int kioskId = 0; for (final String ks : service.getKioskList().getAvailableKiosks()) { - drawerItems.getMenu() + drawerLayoutBinding.navigation.getMenu() .add(R.id.menu_tabs_group, kioskId, ORDER, KioskTranslator.getTranslatedKioskName(ks, this)) .setIcon(KioskTranslator.getKioskIcon(ks, this)); kioskId++; } - drawerItems.getMenu() + drawerLayoutBinding.navigation.getMenu() .add(R.id.menu_tabs_group, ITEM_ID_SUBSCRIPTIONS, ORDER, R.string.tab_subscriptions) .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) .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) .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) .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) .setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.ic_history)); //Settings and About - drawerItems.getMenu() + drawerLayoutBinding.navigation.getMenu() .add(R.id.menu_options_about_group, ITEM_ID_SETTINGS, ORDER, R.string.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) .setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.ic_info_outline)); } @@ -470,21 +464,23 @@ public class MainActivity extends AppCompatActivity { protected void onResume() { assureCorrectAppLanguage(this); // Change the date format to match the selected language on resume - Localization.init(getApplicationContext()); + Localization.initPrettyTime(Localization.resolvePrettyTime(getApplicationContext())); super.onResume(); // Close drawer on return, and don't show animation, // 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 { final int selectedServiceId = ServiceHelper.getSelectedServiceId(this); final String selectedServiceName = NewPipe.getService(selectedServiceId) .getServiceInfo().getName(); - headerServiceView.setText(selectedServiceName); - headerServiceIcon.setImageResource(ServiceHelper.getIcon(selectedServiceId)); + drawerHeaderBinding.drawerHeaderServiceView.setText(selectedServiceName); + drawerHeaderBinding.drawerHeaderServiceIcon.setImageResource(ServiceHelper + .getIcon(selectedServiceId)); - headerServiceView.post(() -> headerServiceView.setSelected(true)); - toggleServiceButton.setContentDescription( + drawerHeaderBinding.drawerHeaderServiceView.post(() -> drawerHeaderBinding + .drawerHeaderServiceView.setSelected(true)); + drawerHeaderBinding.drawerHeaderActionButton.setContentDescription( getString(R.string.drawer_header_description) + selectedServiceName); } catch (final Exception e) { ErrorActivity.reportUiError(this, e); @@ -497,10 +493,7 @@ public class MainActivity extends AppCompatActivity { Log.d(TAG, "Theme has changed, recreating activity..."); } sharedPreferences.edit().putBoolean(Constants.KEY_THEME_CHANGE, false).apply(); - // https://stackoverflow.com/questions/10844112/ - // Briefly, let the activity resume - // properly posting the recreate call to end of the message queue - new Handler(Looper.getMainLooper()).post(MainActivity.this::recreate); + ActivityCompat.recreate(this); } if (sharedPreferences.getBoolean(Constants.KEY_MAIN_PAGE_CHANGE, false)) { @@ -513,7 +506,8 @@ public class MainActivity extends AppCompatActivity { final boolean isHistoryEnabled = sharedPreferences.getBoolean( 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 @@ -557,9 +551,8 @@ public class MainActivity extends AppCompatActivity { } if (DeviceUtils.isTv(this)) { - final View drawerPanel = findViewById(R.id.navigation); - if (drawer.isDrawerOpen(drawerPanel)) { - drawer.closeDrawers(); + if (mainBinding.getRoot().isDrawerOpen(drawerLayoutBinding.navigation)) { + mainBinding.getRoot().closeDrawers(); return; } } @@ -585,9 +578,7 @@ public class MainActivity extends AppCompatActivity { // delegate the back press to it if (fragmentPlayer instanceof BackPressable) { if (!((BackPressable) fragmentPlayer).onBackPressed()) { - final FrameLayout bottomSheetLayout = - findViewById(R.id.fragment_player_holder); - BottomSheetBehavior.from(bottomSheetLayout) + BottomSheetBehavior.from(mainBinding.fragmentPlayerHolder) .setState(BottomSheetBehavior.STATE_COLLAPSED); } return; @@ -670,8 +661,7 @@ public class MainActivity extends AppCompatActivity { final Fragment fragment = getSupportFragmentManager().findFragmentById(R.id.fragment_holder); if (!(fragment instanceof SearchFragment)) { - findViewById(R.id.toolbar).findViewById(R.id.toolbar_search_container) - .setVisibility(View.GONE); + toolbarLayoutBinding.toolbarSearchContainer.getRoot().setVisibility(View.GONE); } final ActionBar actionBar = getSupportActionBar(); @@ -732,21 +722,20 @@ public class MainActivity extends AppCompatActivity { return; } - final Toolbar toolbar = findViewById(R.id.toolbar); - final Fragment fragment = getSupportFragmentManager() .findFragmentById(R.id.fragment_holder); if (fragment instanceof MainFragment) { getSupportActionBar().setDisplayHomeAsUpEnabled(false); if (toggle != null) { toggle.syncState(); - toolbar.setNavigationOnClickListener(v -> drawer.openDrawer(GravityCompat.START)); - drawer.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNDEFINED); + toolbarLayoutBinding.toolbar.setNavigationOnClickListener(v -> mainBinding.getRoot() + .openDrawer(GravityCompat.START)); + mainBinding.getRoot().setDrawerLockMode(DrawerLayout.LOCK_MODE_UNDEFINED); } } else { - drawer.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED); + mainBinding.getRoot().setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED); getSupportActionBar().setDisplayHomeAsUpEnabled(true); - toolbar.setNavigationOnClickListener(v -> onHomeButtonPressed()); + toolbarLayoutBinding.toolbar.setNavigationOnClickListener(v -> onHomeButtonPressed()); } } @@ -770,7 +759,7 @@ public class MainActivity extends AppCompatActivity { switch (linkType) { case STREAM: final String intentCacheKey = intent.getStringExtra( - VideoPlayer.PLAY_QUEUE_KEY); + Player.PLAY_QUEUE_KEY); final PlayQueue playQueue = intentCacheKey != null ? SerializedCache.getInstance() .take(intentCacheKey, PlayQueue.class) @@ -854,9 +843,8 @@ public class MainActivity extends AppCompatActivity { } private boolean bottomSheetHiddenOrCollapsed() { - final FrameLayout bottomSheetLayout = findViewById(R.id.fragment_player_holder); final BottomSheetBehavior bottomSheetBehavior = - BottomSheetBehavior.from(bottomSheetLayout); + BottomSheetBehavior.from(mainBinding.fragmentPlayerHolder); final int sheetState = bottomSheetBehavior.getState(); return sheetState == BottomSheetBehavior.STATE_HIDDEN diff --git a/app/src/main/java/org/schabi/newpipe/ReCaptchaActivity.java b/app/src/main/java/org/schabi/newpipe/ReCaptchaActivity.java index c962ed99d..463fc24ac 100644 --- a/app/src/main/java/org/schabi/newpipe/ReCaptchaActivity.java +++ b/app/src/main/java/org/schabi/newpipe/ReCaptchaActivity.java @@ -8,20 +8,18 @@ import android.util.Log; import android.view.Menu; import android.view.MenuItem; import android.webkit.CookieManager; -import android.webkit.WebResourceRequest; import android.webkit.WebSettings; import android.webkit.WebView; -import android.webkit.WebViewClient; import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import androidx.annotation.RequiresApi; import androidx.appcompat.app.ActionBar; import androidx.appcompat.app.AppCompatActivity; -import androidx.appcompat.widget.Toolbar; import androidx.core.app.NavUtils; import androidx.preference.PreferenceManager; +import androidx.webkit.WebViewClientCompat; +import org.schabi.newpipe.databinding.ActivityRecaptchaBinding; import org.schabi.newpipe.util.ThemeHelper; 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 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 = ""; @Override protected void onCreate(final Bundle savedInstanceState) { ThemeHelper.setTheme(this); super.onCreate(savedInstanceState); - setContentView(R.layout.activity_recaptcha); - final Toolbar toolbar = findViewById(R.id.toolbar); - setSupportActionBar(toolbar); - String url = getIntent().getStringExtra(RECAPTCHA_URL_EXTRA); - if (url == null || url.isEmpty()) { - url = YT_URL; - } + recaptchaBinding = ActivityRecaptchaBinding.inflate(getLayoutInflater()); + setContentView(recaptchaBinding.getRoot()); + setSupportActionBar(recaptchaBinding.toolbar); + final String url = sanitizeRecaptchaUrl(getIntent().getStringExtra(RECAPTCHA_URL_EXTRA)); // set return to Cancel by default setResult(RESULT_CANCELED); - - webView = findViewById(R.id.reCaptchaWebView); - // enable Javascript - final WebSettings webSettings = webView.getSettings(); + final WebSettings webSettings = recaptchaBinding.reCaptchaWebView.getSettings(); webSettings.setJavaScriptEnabled(true); + webSettings.setUserAgentString(DownloaderImpl.USER_AGENT); - webView.setWebViewClient(new WebViewClient() { - @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; - } - + recaptchaBinding.reCaptchaWebView.setWebViewClient(new WebViewClientCompat() { @Override public boolean shouldOverrideUrlLoading(final WebView view, final String url) { if (MainActivity.DEBUG) { @@ -111,17 +100,16 @@ public class ReCaptchaActivity extends AppCompatActivity { }); // cleaning cache, history and cookies from webView - webView.clearCache(true); - webView.clearHistory(); - final android.webkit.CookieManager cookieManager = CookieManager.getInstance(); + recaptchaBinding.reCaptchaWebView.clearCache(true); + recaptchaBinding.reCaptchaWebView.clearHistory(); + final CookieManager cookieManager = CookieManager.getInstance(); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - cookieManager.removeAllCookies(aBoolean -> { - }); + cookieManager.removeAllCookies(value -> { }); } else { cookieManager.removeAllCookie(); } - webView.loadUrl(url); + recaptchaBinding.reCaptchaWebView.loadUrl(url); } @Override @@ -145,18 +133,16 @@ public class ReCaptchaActivity extends AppCompatActivity { @Override public boolean onOptionsItemSelected(final MenuItem item) { - final int id = item.getItemId(); - switch (id) { - case R.id.menu_item_done: - saveCookiesAndFinish(); - return true; - default: - return false; + if (item.getItemId() == R.id.menu_item_done) { + saveCookiesAndFinish(); + return true; } + return false; } 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) { Log.d(TAG, "saveCookiesAndFinish: foundCookies=" + foundCookies); } diff --git a/app/src/main/java/org/schabi/newpipe/RouterActivity.java b/app/src/main/java/org/schabi/newpipe/RouterActivity.java index 9ad993de1..98a0921e4 100644 --- a/app/src/main/java/org/schabi/newpipe/RouterActivity.java +++ b/app/src/main/java/org/schabi/newpipe/RouterActivity.java @@ -14,7 +14,6 @@ import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.Button; -import android.widget.LinearLayout; import android.widget.RadioButton; import android.widget.RadioGroup; import android.widget.Toast; @@ -26,10 +25,13 @@ import androidx.appcompat.app.AlertDialog; import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.content.res.AppCompatResources; import androidx.core.app.NotificationCompat; +import androidx.core.app.ServiceCompat; import androidx.core.widget.TextViewCompat; import androidx.fragment.app.FragmentManager; 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.extractor.Info; import org.schabi.newpipe.extractor.NewPipe; @@ -267,9 +269,8 @@ public class RouterActivity extends AppCompatActivity { final Context themeWrapperContext = getThemeWrapperContext(); final LayoutInflater inflater = LayoutInflater.from(themeWrapperContext); - final LinearLayout rootLayout = (LinearLayout) inflater.inflate( - R.layout.single_choice_dialog_view, null, false); - final RadioGroup radioGroup = rootLayout.findViewById(android.R.id.list); + final RadioGroup radioGroup = SingleChoiceDialogViewBinding.inflate(getLayoutInflater()) + .list; final DialogInterface.OnClickListener dialogButtonsClickListener = (dialog, which) -> { final int indexOfChild = radioGroup.indexOfChild( @@ -322,8 +323,7 @@ public class RouterActivity extends AppCompatActivity { int id = 12345; for (final AdapterChoiceItem item : choices) { - final RadioButton radioButton - = (RadioButton) inflater.inflate(R.layout.list_radio_icon_item, null); + final RadioButton radioButton = ListRadioIconItemBinding.inflate(inflater).getRoot(); radioButton.setText(item.description); TextViewCompat.setCompoundDrawablesRelativeWithIntrinsicBounds(radioButton, AppCompatResources.getDrawable(getApplicationContext(), item.icon), @@ -696,7 +696,7 @@ public class RouterActivity extends AppCompatActivity { @Override public void onDestroy() { super.onDestroy(); - stopForeground(true); + ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE); if (fetcher != null) { fetcher.dispose(); } diff --git a/app/src/main/java/org/schabi/newpipe/about/AboutActivity.java b/app/src/main/java/org/schabi/newpipe/about/AboutActivity.java index e3e56816c..2569f4a94 100644 --- a/app/src/main/java/org/schabi/newpipe/about/AboutActivity.java +++ b/app/src/main/java/org/schabi/newpipe/about/AboutActivity.java @@ -6,22 +6,19 @@ import android.view.LayoutInflater; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; -import android.widget.TextView; import androidx.annotation.NonNull; import androidx.appcompat.app.AppCompatActivity; -import androidx.appcompat.widget.Toolbar; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentActivity; -import androidx.recyclerview.widget.RecyclerView; 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 org.schabi.newpipe.BuildConfig; import org.schabi.newpipe.R; +import org.schabi.newpipe.databinding.ActivityAboutBinding; +import org.schabi.newpipe.databinding.FragmentAboutBinding; import org.schabi.newpipe.util.ThemeHelper; import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage; @@ -32,76 +29,72 @@ public class AboutActivity extends AppCompatActivity { * List of all 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), + 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", "https://github.com/TeamNewPipe/NewPipeExtractor", StandardLicenses.GPL3), - new SoftwareComponent("Jsoup", "2017", "Jonathan Hedley", - "https://github.com/jhy/jsoup", StandardLicenses.MIT), - new SoftwareComponent("Rhino", "2015", "Mozilla", - "https://www.mozilla.org/rhino/", StandardLicenses.MPL2), - new SoftwareComponent("ACRA", "2013", "Kevin Gaudin", - "http://www.acra.ch", StandardLicenses.APACHE2), + new SoftwareComponent("NoNonsense-FilePicker", "2016", "Jonas Kalderstam", + "https://github.com/spacecowboy/NoNonsense-FilePicker", + StandardLicenses.MPL2), + new SoftwareComponent("OkHttp", "2019", "Square, Inc.", + "https://square.github.io/okhttp/", 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", "https://github.com/nostra13/Android-Universal-Image-Loader", 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_LICENSE = 1; 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 protected void onCreate(final Bundle savedInstanceState) { assureCorrectAppLanguage(this); super.onCreate(savedInstanceState); 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(toolbar); + setSupportActionBar(aboutBinding.toolbar); getSupportActionBar().setDisplayHomeAsUpEnabled(true); // Create the adapter that will return a fragment for each of the three // primary sections of the activity. - mSectionsPagerAdapter = new SectionsPagerAdapter(this); + final SectionsPagerAdapter mSectionsPagerAdapter = new SectionsPagerAdapter(this); // Set up the ViewPager with the sections adapter. - mViewPager = findViewById(R.id.container); - mViewPager.setAdapter(mSectionsPagerAdapter); + aboutBinding.container.setAdapter(mSectionsPagerAdapter); - final TabLayout tabLayout = findViewById(R.id.tabs); - new TabLayoutMediator(tabLayout, mViewPager, (tab, position) -> { + new TabLayoutMediator(aboutBinding.tabs, aboutBinding.container, (tab, position) -> { switch (position) { default: case POS_ABOUT: @@ -131,7 +124,8 @@ public class AboutActivity extends AppCompatActivity { * A placeholder fragment containing a simple view. */ public static class AboutFragment extends Fragment { - public AboutFragment() { } + public AboutFragment() { + } /** * Created a new instance of this fragment for the given section number. @@ -143,33 +137,28 @@ public class AboutActivity extends AppCompatActivity { } @Override - public View onCreateView(final LayoutInflater inflater, final ViewGroup container, + public View onCreateView(@NonNull final LayoutInflater inflater, final ViewGroup container, final Bundle savedInstanceState) { - final View rootView = inflater.inflate(R.layout.fragment_about, container, false); - final Context context = this.getContext(); + final FragmentAboutBinding aboutBinding = + FragmentAboutBinding.inflate(inflater, container, false); + final Context context = getContext(); - final TextView version = rootView.findViewById(R.id.app_version); - version.setText(BuildConfig.VERSION_NAME); + aboutBinding.appVersion.setText(BuildConfig.VERSION_NAME); - final View githubLink = rootView.findViewById(R.id.github_link); - githubLink.setOnClickListener(nv -> + aboutBinding.githubLink.setOnClickListener(nv -> openUrlInBrowser(context, context.getString(R.string.github_url))); - final View donationLink = rootView.findViewById(R.id.donation_link); - donationLink.setOnClickListener(v -> + aboutBinding.donationLink.setOnClickListener(v -> openUrlInBrowser(context, context.getString(R.string.donation_url))); - final View websiteLink = rootView.findViewById(R.id.website_link); - websiteLink.setOnClickListener(nv -> + aboutBinding.websiteLink.setOnClickListener(nv -> openUrlInBrowser(context, context.getString(R.string.website_url))); - final View privacyPolicyLink = rootView.findViewById(R.id.privacy_policy_link); - privacyPolicyLink.setOnClickListener(v -> + aboutBinding.privacyPolicyLink.setOnClickListener(v -> openUrlInBrowser(context, context.getString(R.string.privacy_policy_url))); - return rootView; + return aboutBinding.getRoot(); } - } /** diff --git a/app/src/main/java/org/schabi/newpipe/about/LicenseFragment.java b/app/src/main/java/org/schabi/newpipe/about/LicenseFragment.java index 8367a75dc..f5bf4df19 100644 --- a/app/src/main/java/org/schabi/newpipe/about/LicenseFragment.java +++ b/app/src/main/java/org/schabi/newpipe/about/LicenseFragment.java @@ -7,18 +7,20 @@ import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; -import android.widget.TextView; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; import org.schabi.newpipe.R; +import org.schabi.newpipe.databinding.FragmentLicensesBinding; +import org.schabi.newpipe.databinding.ItemSoftwareComponentBinding; import org.schabi.newpipe.util.ShareUtils; import java.io.Serializable; import java.util.Arrays; import java.util.Comparator; +import java.util.Objects; import io.reactivex.rxjava3.disposables.CompositeDisposable; @@ -35,12 +37,9 @@ public class LicenseFragment extends Fragment { private final CompositeDisposable compositeDisposable = new CompositeDisposable(); 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(); - bundle.putParcelableArray(ARG_COMPONENTS, softwareComponents); + bundle.putParcelableArray(ARG_COMPONENTS, Objects.requireNonNull(softwareComponents)); + final LicenseFragment fragment = new LicenseFragment(); fragment.setArguments(bundle); return fragment; } @@ -69,43 +68,42 @@ public class LicenseFragment extends Fragment { @Nullable @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) { - final View rootView = inflater.inflate(R.layout.fragment_licenses, container, false); - final ViewGroup softwareComponentsView = rootView.findViewById(R.id.software_components); + final FragmentLicensesBinding binding = FragmentLicensesBinding + .inflate(inflater, container, false); - final View licenseLink = rootView.findViewById(R.id.app_read_license); - licenseLink.setOnClickListener(v -> { + binding.appReadLicense.setOnClickListener(v -> { activeLicense = StandardLicenses.GPL3; compositeDisposable.add(LicenseFragmentHelper.showLicense(getActivity(), StandardLicenses.GPL3)); }); for (final SoftwareComponent component : softwareComponents) { - final View componentView = inflater - .inflate(R.layout.item_software_component, container, false); - final TextView softwareName = componentView.findViewById(R.id.name); - final TextView copyright = componentView.findViewById(R.id.copyright); - softwareName.setText(component.getName()); - copyright.setText(getString(R.string.copyright, + final ItemSoftwareComponentBinding componentBinding = ItemSoftwareComponentBinding + .inflate(inflater, container, false); + componentBinding.name.setText(component.getName()); + componentBinding.copyright.setText(getString(R.string.copyright, component.getYears(), component.getCopyrightOwner(), component.getLicense().getAbbreviation())); - componentView.setTag(component); - componentView.setOnClickListener(v -> { + final View root = componentBinding.getRoot(); + root.setTag(component); + root.setOnClickListener(v -> { activeLicense = component.getLicense(); compositeDisposable.add(LicenseFragmentHelper.showLicense(getActivity(), component.getLicense())); }); - softwareComponentsView.addView(componentView); - registerForContextMenu(componentView); + binding.softwareComponents.addView(root); + registerForContextMenu(root); } if (activeLicense != null) { compositeDisposable.add(LicenseFragmentHelper.showLicense(getActivity(), activeLicense)); } - return rootView; + return binding.getRoot(); } @Override diff --git a/app/src/main/java/org/schabi/newpipe/about/StandardLicenses.java b/app/src/main/java/org/schabi/newpipe/about/StandardLicenses.java index 50ee5ebc3..60b1e168c 100644 --- a/app/src/main/java/org/schabi/newpipe/about/StandardLicenses.java +++ b/app/src/main/java/org/schabi/newpipe/about/StandardLicenses.java @@ -12,6 +12,8 @@ public final class StandardLicenses { = new License("Mozilla Public License, Version 2.0", "MPL 2.0", "mpl2.html"); public static final License MIT = 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() { } } diff --git a/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistStreamEntry.kt b/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistStreamEntry.kt index d9c892099..aff6205f2 100644 --- a/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistStreamEntry.kt +++ b/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistStreamEntry.kt @@ -9,7 +9,7 @@ import org.schabi.newpipe.database.stream.model.StreamStateEntity import org.schabi.newpipe.extractor.stream.StreamInfoItem import kotlin.jvm.Throws -class PlaylistStreamEntry( +data class PlaylistStreamEntry( @Embedded val streamEntity: StreamEntity, diff --git a/app/src/main/java/org/schabi/newpipe/download/DownloadActivity.java b/app/src/main/java/org/schabi/newpipe/download/DownloadActivity.java index 979f8be75..37eefed96 100644 --- a/app/src/main/java/org/schabi/newpipe/download/DownloadActivity.java +++ b/app/src/main/java/org/schabi/newpipe/download/DownloadActivity.java @@ -9,10 +9,10 @@ import android.view.ViewTreeObserver; import androidx.appcompat.app.ActionBar; import androidx.appcompat.app.AppCompatActivity; -import androidx.appcompat.widget.Toolbar; import androidx.fragment.app.FragmentTransaction; import org.schabi.newpipe.R; +import org.schabi.newpipe.databinding.ActivityDownloaderBinding; import org.schabi.newpipe.util.DeviceUtils; import org.schabi.newpipe.util.ThemeHelper; import org.schabi.newpipe.views.FocusOverlayView; @@ -35,11 +35,14 @@ public class DownloadActivity extends AppCompatActivity { assureCorrectAppLanguage(this); ThemeHelper.setTheme(this); - super.onCreate(savedInstanceState); - setContentView(R.layout.activity_downloader); - final Toolbar toolbar = findViewById(R.id.toolbar); - setSupportActionBar(toolbar); + super.onCreate(savedInstanceState); + + final ActivityDownloaderBinding downloaderBinding = + ActivityDownloaderBinding.inflate(getLayoutInflater()); + setContentView(downloaderBinding.getRoot()); + + setSupportActionBar(downloaderBinding.toolbarLayout.toolbar); final ActionBar actionBar = getSupportActionBar(); if (actionBar != null) { diff --git a/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java b/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java index e80a0ab21..68bcf3cc1 100644 --- a/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java +++ b/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java @@ -16,12 +16,8 @@ import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.AdapterView; -import android.widget.EditText; -import android.widget.RadioButton; import android.widget.RadioGroup; import android.widget.SeekBar; -import android.widget.Spinner; -import android.widget.TextView; import android.widget.Toast; import androidx.annotation.IdRes; @@ -40,6 +36,7 @@ import com.nononsenseapps.filepicker.Utils; import org.schabi.newpipe.MainActivity; import org.schabi.newpipe.R; import org.schabi.newpipe.RouterActivity; +import org.schabi.newpipe.databinding.DownloadDialogBinding; import org.schabi.newpipe.extractor.MediaFormat; import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipe.extractor.localization.Localization; @@ -116,11 +113,7 @@ public class DownloadDialog extends DialogFragment private final CompositeDisposable disposables = new CompositeDisposable(); - private EditText nameEditText; - private Spinner streamsSpinner; - private RadioGroup radioStreamsGroup; - private TextView threadsCountTextView; - private SeekBar threadsSeekBar; + private DownloadDialogBinding dialogBinding; private SharedPreferences prefs; @@ -277,38 +270,35 @@ public class DownloadDialog extends DialogFragment @Override public void onViewCreated(@NonNull final View view, @Nullable final Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); - nameEditText = view.findViewById(R.id.file_name); - nameEditText.setText(FilenameUtils.createFilename(getContext(), currentInfo.getName())); + dialogBinding = DownloadDialogBinding.bind(view); + + dialogBinding.fileName.setText(FilenameUtils.createFilename(getContext(), + currentInfo.getName())); selectedAudioIndex = ListHelper .getDefaultAudioFormat(getContext(), currentInfo.getAudioStreams()); selectedSubtitleIndex = getSubtitleIndexBy(subtitleStreamsAdapter.getAll()); - streamsSpinner = view.findViewById(R.id.quality_spinner); - streamsSpinner.setOnItemSelectedListener(this); + dialogBinding.qualitySpinner.setOnItemSelectedListener(this); - threadsCountTextView = view.findViewById(R.id.threads_count); - threadsSeekBar = view.findViewById(R.id.threads); + dialogBinding.videoAudioGroup.setOnCheckedChangeListener(this); - radioStreamsGroup = view.findViewById(R.id.video_audio_group); - radioStreamsGroup.setOnCheckedChangeListener(this); - - initToolbar(view.findViewById(R.id.toolbar)); + initToolbar(dialogBinding.toolbarLayout.toolbar); setupDownloadOptions(); prefs = PreferenceManager.getDefaultSharedPreferences(requireContext()); final int threads = prefs.getInt(getString(R.string.default_download_threads), 3); - threadsCountTextView.setText(String.valueOf(threads)); - threadsSeekBar.setProgress(threads - 1); - threadsSeekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { + dialogBinding.threadsCount.setText(String.valueOf(threads)); + dialogBinding.threads.setProgress(threads - 1); + dialogBinding.threads.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { @Override public void onProgressChanged(final SeekBar seekbar, final int progress, final boolean fromUser) { final int newProgress = progress + 1; prefs.edit().putInt(getString(R.string.default_download_threads), newProgress) .apply(); - threadsCountTextView.setText(String.valueOf(newProgress)); + dialogBinding.threadsCount.setText(String.valueOf(newProgress)); } @Override @@ -326,19 +316,19 @@ public class DownloadDialog extends DialogFragment disposables.add(StreamSizeWrapper.fetchSizeForWrapper(wrappedVideoStreams) .subscribe(result -> { - if (radioStreamsGroup.getCheckedRadioButtonId() == R.id.video_button) { + if (dialogBinding.videoAudioGroup.getCheckedRadioButtonId() == R.id.video_button) { setupVideoSpinner(); } })); disposables.add(StreamSizeWrapper.fetchSizeForWrapper(wrappedAudioStreams) .subscribe(result -> { - if (radioStreamsGroup.getCheckedRadioButtonId() == R.id.audio_button) { + if (dialogBinding.videoAudioGroup.getCheckedRadioButtonId() == R.id.audio_button) { setupAudioSpinner(); } })); disposables.add(StreamSizeWrapper.fetchSizeForWrapper(wrappedSubtitleStreams) .subscribe(result -> { - if (radioStreamsGroup.getCheckedRadioButtonId() == R.id.subtitle_button) { + if (dialogBinding.videoAudioGroup.getCheckedRadioButtonId() == R.id.subtitle_button) { setupSubtitleSpinner(); } })); @@ -350,6 +340,12 @@ public class DownloadDialog extends DialogFragment disposables.clear(); } + @Override + public void onDestroyView() { + dialogBinding = null; + super.onDestroyView(); + } + /*////////////////////////////////////////////////////////////////////////// // Radio group Video&Audio options - Listener //////////////////////////////////////////////////////////////////////////*/ @@ -429,8 +425,8 @@ public class DownloadDialog extends DialogFragment return; } - streamsSpinner.setAdapter(audioStreamsAdapter); - streamsSpinner.setSelection(selectedAudioIndex); + dialogBinding.qualitySpinner.setAdapter(audioStreamsAdapter); + dialogBinding.qualitySpinner.setSelection(selectedAudioIndex); setRadioButtonsState(true); } @@ -439,8 +435,8 @@ public class DownloadDialog extends DialogFragment return; } - streamsSpinner.setAdapter(videoStreamsAdapter); - streamsSpinner.setSelection(selectedVideoIndex); + dialogBinding.qualitySpinner.setAdapter(videoStreamsAdapter); + dialogBinding.qualitySpinner.setSelection(selectedVideoIndex); setRadioButtonsState(true); } @@ -449,8 +445,8 @@ public class DownloadDialog extends DialogFragment return; } - streamsSpinner.setAdapter(subtitleStreamsAdapter); - streamsSpinner.setSelection(selectedSubtitleIndex); + dialogBinding.qualitySpinner.setAdapter(subtitleStreamsAdapter); + dialogBinding.qualitySpinner.setSelection(selectedSubtitleIndex); setRadioButtonsState(true); } @@ -475,7 +471,7 @@ public class DownloadDialog extends DialogFragment break; } - threadsSeekBar.setEnabled(flag); + dialogBinding.threads.setEnabled(flag); } @Override @@ -486,7 +482,7 @@ public class DownloadDialog extends DialogFragment + "parent = [" + parent + "], view = [" + view + "], " + "position = [" + position + "], id = [" + id + "]"); } - switch (radioStreamsGroup.getCheckedRadioButtonId()) { + switch (dialogBinding.videoAudioGroup.getCheckedRadioButtonId()) { case R.id.audio_button: selectedAudioIndex = position; break; @@ -506,16 +502,14 @@ public class DownloadDialog extends DialogFragment protected void setupDownloadOptions() { 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 isAudioStreamsAvailable = audioStreamsAdapter.getCount() > 0; final boolean isSubtitleStreamsAvailable = subtitleStreamsAdapter.getCount() > 0; - audioButton.setVisibility(isAudioStreamsAvailable ? View.VISIBLE : View.GONE); - videoButton.setVisibility(isVideoStreamsAvailable ? View.VISIBLE : View.GONE); - subtitleButton.setVisibility(isSubtitleStreamsAvailable ? View.VISIBLE : View.GONE); + dialogBinding.audioButton.setVisibility(isAudioStreamsAvailable ? View.VISIBLE : View.GONE); + dialogBinding.videoButton.setVisibility(isVideoStreamsAvailable ? View.VISIBLE : View.GONE); + dialogBinding.subtitleButton.setVisibility(isSubtitleStreamsAvailable + ? View.VISIBLE : View.GONE); prefs = PreferenceManager.getDefaultSharedPreferences(getContext()); final String defaultMedia = prefs.getString(getString(R.string.last_used_download_type), @@ -523,24 +517,24 @@ public class DownloadDialog extends DialogFragment if (isVideoStreamsAvailable && (defaultMedia.equals(getString(R.string.last_download_type_video_key)))) { - videoButton.setChecked(true); + dialogBinding.videoButton.setChecked(true); setupVideoSpinner(); } else if (isAudioStreamsAvailable && (defaultMedia.equals(getString(R.string.last_download_type_audio_key)))) { - audioButton.setChecked(true); + dialogBinding.audioButton.setChecked(true); setupAudioSpinner(); } else if (isSubtitleStreamsAvailable && (defaultMedia.equals(getString(R.string.last_download_type_subtitle_key)))) { - subtitleButton.setChecked(true); + dialogBinding.subtitleButton.setChecked(true); setupSubtitleSpinner(); } else if (isVideoStreamsAvailable) { - videoButton.setChecked(true); + dialogBinding.videoButton.setChecked(true); setupVideoSpinner(); } else if (isAudioStreamsAvailable) { - audioButton.setChecked(true); + dialogBinding.audioButton.setChecked(true); setupAudioSpinner(); } else if (isSubtitleStreamsAvailable) { - subtitleButton.setChecked(true); + dialogBinding.subtitleButton.setChecked(true); setupSubtitleSpinner(); } else { Toast.makeText(getContext(), R.string.no_streams_available_download, @@ -550,9 +544,9 @@ public class DownloadDialog extends DialogFragment } private void setRadioButtonsState(final boolean enabled) { - radioStreamsGroup.findViewById(R.id.audio_button).setEnabled(enabled); - radioStreamsGroup.findViewById(R.id.video_button).setEnabled(enabled); - radioStreamsGroup.findViewById(R.id.subtitle_button).setEnabled(enabled); + dialogBinding.audioButton.setEnabled(enabled); + dialogBinding.videoButton.setEnabled(enabled); + dialogBinding.subtitleButton.setEnabled(enabled); } private int getSubtitleIndexBy(final List streams) { @@ -582,7 +576,7 @@ public class DownloadDialog extends DialogFragment } 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); } @@ -619,7 +613,7 @@ public class DownloadDialog extends DialogFragment String filename = getNameEditText().concat("."); - switch (radioStreamsGroup.getCheckedRadioButtonId()) { + switch (dialogBinding.videoAudioGroup.getCheckedRadioButtonId()) { case R.id.audio_button: selectedMediaType = getString(R.string.last_download_type_audio_key); mainStorage = mainStorageAudio; @@ -669,7 +663,7 @@ public class DownloadDialog extends DialogFragment filename, mime); } else { File initialSavePath; - if (radioStreamsGroup.getCheckedRadioButtonId() == R.id.audio_button) { + if (dialogBinding.videoAudioGroup.getCheckedRadioButtonId() == R.id.audio_button) { initialSavePath = NewPipeSettings.getDir(Environment.DIRECTORY_MUSIC); } else { initialSavePath = NewPipeSettings.getDir(Environment.DIRECTORY_MOVIES); @@ -862,7 +856,7 @@ public class DownloadDialog extends DialogFragment final Stream selectedStream; Stream secondaryStream = null; final char kind; - int threads = threadsSeekBar.getProgress() + 1; + int threads = dialogBinding.threads.getProgress() + 1; final String[] urls; final MissionRecoveryInfo[] recoveryInfo; String psName = null; @@ -870,7 +864,7 @@ public class DownloadDialog extends DialogFragment long nearLength = 0; // more download logic: select muxer, subtitle converter, etc. - switch (radioStreamsGroup.getCheckedRadioButtonId()) { + switch (dialogBinding.videoAudioGroup.getCheckedRadioButtonId()) { case R.id.audio_button: kind = 'a'; selectedStream = audioStreamsAdapter.getItem(selectedAudioIndex); diff --git a/app/src/main/java/org/schabi/newpipe/fragments/BaseStateFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/BaseStateFragment.java index 0d25765a4..9f1f57998 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/BaseStateFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/BaseStateFragment.java @@ -22,10 +22,10 @@ import org.schabi.newpipe.ReCaptchaActivity; import org.schabi.newpipe.extractor.exceptions.ContentNotAvailableException; import org.schabi.newpipe.extractor.exceptions.ContentNotSupportedException; import org.schabi.newpipe.extractor.exceptions.ReCaptchaException; +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.util.ExceptionUtils; import org.schabi.newpipe.util.InfoCache; import java.util.Collections; diff --git a/app/src/main/java/org/schabi/newpipe/fragments/MainFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/MainFragment.java index 866b324ec..4765e6265 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/MainFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/MainFragment.java @@ -3,7 +3,6 @@ package org.schabi.newpipe.fragments; import android.content.Context; import android.content.res.ColorStateList; import android.os.Bundle; -import androidx.preference.PreferenceManager; import android.util.Log; import android.view.LayoutInflater; import android.view.Menu; @@ -19,12 +18,13 @@ import androidx.appcompat.app.AppCompatActivity; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentManager; import androidx.fragment.app.FragmentStatePagerAdapterMenuWorkaround; -import androidx.viewpager.widget.ViewPager; +import androidx.preference.PreferenceManager; import com.google.android.material.tabs.TabLayout; import org.schabi.newpipe.BaseFragment; import org.schabi.newpipe.R; +import org.schabi.newpipe.databinding.FragmentMainBinding; import org.schabi.newpipe.extractor.exceptions.ExtractionException; import org.schabi.newpipe.report.ErrorActivity; 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.ServiceHelper; import org.schabi.newpipe.util.ThemeHelper; -import org.schabi.newpipe.views.ScrollableTabLayout; import java.util.ArrayList; import java.util.List; public class MainFragment extends BaseFragment implements TabLayout.OnTabSelectedListener { - private ViewPager viewPager; + private FragmentMainBinding binding; private SelectedTabsPagerAdapter pagerAdapter; - private ScrollableTabLayout tabLayout; private final List tabsList = new ArrayList<>(); private TabsManager tabsManager; @@ -90,13 +88,12 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte protected void initViews(final View rootView, final Bundle savedInstanceState) { super.initViews(rootView, savedInstanceState); - tabLayout = rootView.findViewById(R.id.main_tab_layout); - viewPager = rootView.findViewById(R.id.pager); + binding = FragmentMainBinding.bind(rootView); - tabLayout.setTabIconTint(ColorStateList.valueOf( + binding.mainTabLayout.setTabIconTint(ColorStateList.valueOf( ThemeHelper.resolveColorFromAttr(requireContext(), R.attr.colorAccent))); - tabLayout.setupWithViewPager(viewPager); - tabLayout.addOnTabSelectedListener(this); + binding.mainTabLayout.setupWithViewPager(binding.pager); + binding.mainTabLayout.addOnTabSelectedListener(this); setupTabs(); } @@ -120,8 +117,9 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte public void onDestroy() { super.onDestroy(); tabsManager.unsetSavedTabsListener(); - if (viewPager != null) { - viewPager.setAdapter(null); + if (binding != null) { + binding.pager.setAdapter(null); + binding = null; } } @@ -172,19 +170,19 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte getChildFragmentManager(), tabsList); } - viewPager.setAdapter(null); - viewPager.setOffscreenPageLimit(tabsList.size()); - viewPager.setAdapter(pagerAdapter); + binding.pager.setAdapter(null); + binding.pager.setOffscreenPageLimit(tabsList.size()); + binding.pager.setAdapter(pagerAdapter); updateTabsIconAndDescription(); - updateTitleForTab(viewPager.getCurrentItem()); + updateTitleForTab(binding.pager.getCurrentItem()); hasTabsChanged = false; } private void updateTabsIconAndDescription() { 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) { final Tab tab = tabsList.get(i); tabToSet.setIcon(tab.getTabIconRes(requireContext())); diff --git a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java index 427cff06e..b25d23694 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java @@ -16,7 +16,6 @@ import android.os.Bundle; import android.os.Handler; import android.os.Looper; import android.provider.Settings; -import android.text.TextUtils; import android.text.util.Linkify; import android.util.DisplayMetrics; import android.util.Log; @@ -50,7 +49,6 @@ import androidx.viewpager.widget.ViewPager; import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.PlaybackParameters; -import com.google.android.exoplayer2.Player; import com.google.android.material.appbar.AppBarLayout; import com.google.android.material.bottomsheet.BottomSheetBehavior; 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.PlaylistCreationDialog; 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.VideoPlayerImpl; import org.schabi.newpipe.player.event.OnKeyDownListener; import org.schabi.newpipe.player.event.PlayerServiceExtendedEventListener; 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.schedulers.Schedulers; +import static android.text.TextUtils.isEmpty; 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.player.helper.PlayerHelper.globalScreenOrientationLocked; import static org.schabi.newpipe.player.helper.PlayerHelper.isClearingQueueConfirmationRequired; import static org.schabi.newpipe.player.playqueue.PlayQueueItem.RECOVERY_UNSET; import static org.schabi.newpipe.util.AnimationUtils.animateView; +import static org.schabi.newpipe.util.ExtractorHelper.showMetaInfoInTextView; public final class VideoDetailFragment extends BaseStateFragment @@ -145,15 +144,15 @@ public final class VideoDetailFragment private static final float MAX_PLAYER_HEIGHT = 0.7f; 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 = - "org.schabi.newpipe.VideoDetailFragment.ACTION_HIDE_MAIN_PLAYER"; + App.PACKAGE_NAME + ".VideoDetailFragment.ACTION_HIDE_MAIN_PLAYER"; 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 = - "org.schabi.newpipe.VideoDetailFragment.ACTION_VIDEO_FRAGMENT_RESUMED"; + App.PACKAGE_NAME + ".VideoDetailFragment.ACTION_VIDEO_FRAGMENT_RESUMED"; 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 RELATED_TAB_TAG = "NEXT VIDEO"; @@ -218,6 +217,9 @@ public final class VideoDetailFragment private TextView detailDurationView; private TextView detailPositionView; + private View detailMetaInfoSeparator; + private TextView detailMetaInfoTextView; + private LinearLayout videoDescriptionRootLayout; private TextView videoUploadDateView; private TextView videoDescriptionView; @@ -251,14 +253,14 @@ public final class VideoDetailFragment private ContentObserver settingsContentObserver; private MainPlayer playerService; - private VideoPlayerImpl player; + private Player player; /*////////////////////////////////////////////////////////////////////////// // Service management //////////////////////////////////////////////////////////////////////////*/ @Override - public void onServiceConnected(final VideoPlayerImpl connectedPlayer, + public void onServiceConnected(final Player connectedPlayer, final MainPlayer connectedPlayerService, final boolean playAfterConnect) { player = connectedPlayer; @@ -275,7 +277,9 @@ public final class VideoDetailFragment // If the video is playing but orientation changed // let's make the video in fullscreen again 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. // Return back to non-fullscreen state player.toggleFullscreen(); @@ -494,10 +498,10 @@ public final class VideoDetailFragment final PlaylistAppendDialog d = PlaylistAppendDialog.fromStreamInfo(currentInfo); disposables.add( - PlaylistAppendDialog.onPlaylistFound(getContext(), - () -> d.show(getFM(), TAG), - () -> PlaylistCreationDialog.newInstance(d).show(getFM(), TAG) - ) + PlaylistAppendDialog.onPlaylistFound(getContext(), + () -> d.show(getFM(), TAG), + () -> PlaylistCreationDialog.newInstance(d).show(getFM(), TAG) + ) ); } break; @@ -508,8 +512,8 @@ public final class VideoDetailFragment } break; case R.id.detail_uploader_root_layout: - if (TextUtils.isEmpty(currentInfo.getSubChannelUrl())) { - if (!TextUtils.isEmpty(currentInfo.getUploaderUrl())) { + if (isEmpty(currentInfo.getSubChannelUrl())) { + if (!isEmpty(currentInfo.getUploaderUrl())) { openChannel(currentInfo.getUploaderUrl(), currentInfo.getUploaderName()); } @@ -535,7 +539,7 @@ public final class VideoDetailFragment break; case R.id.overlay_play_pause_button: if (playerIsNotStopped()) { - player.onPlayPause(); + player.playPause(); player.hideControls(0, 0); showSystemUi(); } else { @@ -583,7 +587,7 @@ public final class VideoDetailFragment } break; case R.id.detail_uploader_root_layout: - if (TextUtils.isEmpty(currentInfo.getSubChannelUrl())) { + if (isEmpty(currentInfo.getSubChannelUrl())) { Log.w(TAG, "Can't open parent channel because we got no parent channel URL"); } else { @@ -644,6 +648,9 @@ public final class VideoDetailFragment detailDurationView = rootView.findViewById(R.id.detail_duration_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); videoUploadDateView = rootView.findViewById(R.id.detail_upload_date_view); videoDescriptionView = rootView.findViewById(R.id.detail_description_view); @@ -748,7 +755,7 @@ public final class VideoDetailFragment private void initThumbnailViews(@NonNull final StreamInfo info) { thumbnailImageView.setImageResource(R.drawable.dummy_thumbnail_dark); - if (!TextUtils.isEmpty(info.getThumbnailUrl())) { + if (!isEmpty(info.getThumbnailUrl())) { final String infoServiceName = NewPipe.getNameOfService(info.getServiceId()); final ImageLoadingListener onFailListener = new SimpleImageLoadingListener() { @Override @@ -763,12 +770,12 @@ public final class VideoDetailFragment ImageDisplayConstants.DISPLAY_THUMBNAIL_OPTIONS, onFailListener); } - if (!TextUtils.isEmpty(info.getSubChannelAvatarUrl())) { + if (!isEmpty(info.getSubChannelAvatarUrl())) { IMAGE_LOADER.displayImage(info.getSubChannelAvatarUrl(), subChannelThumb, ImageDisplayConstants.DISPLAY_AVATAR_OPTIONS); } - if (!TextUtils.isEmpty(info.getUploaderAvatarUrl())) { + if (!isEmpty(info.getUploaderAvatarUrl())) { IMAGE_LOADER.displayImage(info.getUploaderAvatarUrl(), uploaderThumb, 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 (player != null && player.isFullscreen()) { if (!DeviceUtils.isTablet(activity)) { - player.onPause(); + player.pause(); } restoreDefaultOrientation(); setAutoPlay(false); @@ -843,7 +850,7 @@ public final class VideoDetailFragment final PlayQueueItem playQueueItem = item.getPlayQueue().getItem(); // 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) { updateOverlayData(playQueueItem.getTitle(), playQueueItem.getUploader(), playQueueItem.getThumbnailUrl()); @@ -1217,7 +1224,7 @@ public final class VideoDetailFragment } private void prepareDescription(final Description description) { - if (description == null || TextUtils.isEmpty(description.getContent()) + if (description == null || isEmpty(description.getContent()) || description == Description.emptyDescription) { return; } @@ -1462,9 +1469,9 @@ public final class VideoDetailFragment animateView(thumbnailPlayButton, true, 200); videoTitleTextView.setText(title); - if (!TextUtils.isEmpty(info.getSubChannelName())) { + if (!isEmpty(info.getSubChannelName())) { displayBothUploaderAndSubChannel(info); - } else if (!TextUtils.isEmpty(info.getUploaderName())) { + } else if (!isEmpty(info.getUploaderName())) { displayUploaderAsSubChannel(info); } else { uploaderTextView.setVisibility(View.GONE); @@ -1559,8 +1566,10 @@ public final class VideoDetailFragment prepareDescription(info.getDescription()); updateProgressInfo(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()); } @@ -1610,7 +1619,7 @@ public final class VideoDetailFragment subChannelThumb.setVisibility(View.VISIBLE); - if (!TextUtils.isEmpty(info.getUploaderName())) { + if (!isEmpty(info.getUploaderName())) { uploaderTextView.setText( String.format(getString(R.string.video_detail_by), info.getUploaderName())); uploaderTextView.setVisibility(View.VISIBLE); @@ -1788,7 +1797,7 @@ public final class VideoDetailFragment setOverlayPlayPauseImage(player != null && player.isPlaying()); switch (state) { - case BasePlayer.STATE_PLAYING: + case Player.STATE_PLAYING: if (positionView.getAlpha() != 1.0f && player.getPlayQueue() != null && player.getPlayQueue().getItem() != null @@ -1805,7 +1814,7 @@ public final class VideoDetailFragment final int duration, final int bufferPercent) { // 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; } @@ -2009,9 +2018,7 @@ public final class VideoDetailFragment } private boolean playerIsNotStopped() { - return player != null - && player.getPlayer() != null - && player.getPlayer().getPlaybackState() != Player.STATE_IDLE; + return player != null && !player.isStopped(); } private void restoreDefaultBrightness() { @@ -2039,6 +2046,10 @@ public final class VideoDetailFragment // Apply system brightness when the player is not in fullscreen restoreDefaultBrightness(); } else { + // Do not restore if user has disabled brightness gesture + if (!PlayerHelper.isBrightnessGestureEnabled(activity)) { + return; + } // Restore already saved brightness level final float brightnessLevel = PlayerHelper.getScreenBrightness(activity); if (brightnessLevel == lp.screenBrightness) { @@ -2058,7 +2069,7 @@ public final class VideoDetailFragment player.checkLandscape(); // Let's give a user time to look at video information page if video is not playing if (globalScreenOrientationLocked(activity) && !player.isPlaying()) { - player.onPlay(); + player.play(); } } @@ -2272,7 +2283,7 @@ public final class VideoDetailFragment // Re-enable clicks setOverlayElementsClickable(true); if (player != null) { - player.onQueueClosed(); + player.closeItemsList(); } setOverlayLook(appBarLayout, behavior, 0); break; @@ -2305,10 +2316,10 @@ public final class VideoDetailFragment private void updateOverlayData(@Nullable final String overlayTitle, @Nullable final String uploader, @Nullable final String thumbnailUrl) { - overlayTitleTextView.setText(TextUtils.isEmpty(overlayTitle) ? "" : overlayTitle); - overlayChannelTextView.setText(TextUtils.isEmpty(uploader) ? "" : uploader); + overlayTitleTextView.setText(isEmpty(title) ? "" : title); + overlayChannelTextView.setText(isEmpty(uploader) ? "" : uploader); overlayThumbnailImageView.setImageResource(R.drawable.dummy_thumbnail_dark); - if (!TextUtils.isEmpty(thumbnailUrl)) { + if (!isEmpty(thumbnailUrl)) { IMAGE_LOADER.displayImage(thumbnailUrl, overlayThumbnailImageView, ImageDisplayConstants.DISPLAY_THUMBNAIL_OPTIONS, null); } diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListFragment.java index 5252024c2..02c7a4818 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListFragment.java @@ -12,13 +12,16 @@ import android.view.MenuInflater; import android.view.View; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.appcompat.app.ActionBar; import androidx.appcompat.app.AppCompatActivity; import androidx.preference.PreferenceManager; import androidx.recyclerview.widget.GridLayoutManager; import androidx.recyclerview.widget.RecyclerView; +import androidx.viewbinding.ViewBinding; import org.schabi.newpipe.R; +import org.schabi.newpipe.databinding.PignateFooterBinding; import org.schabi.newpipe.extractor.InfoItem; import org.schabi.newpipe.extractor.channel.ChannelInfoItem; 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.player.helper.PlayerHolder; import org.schabi.newpipe.report.ErrorActivity; +import org.schabi.newpipe.util.KoreUtil; import org.schabi.newpipe.util.NavigationHelper; import org.schabi.newpipe.util.OnClickGesture; import org.schabi.newpipe.util.StateSaver; @@ -214,12 +218,13 @@ public abstract class BaseListFragment extends BaseStateFragment // Init //////////////////////////////////////////////////////////////////////////*/ - protected View getListHeader() { + @Nullable + protected ViewBinding getListHeader() { return null; } - protected View getListFooter() { - return activity.getLayoutInflater().inflate(R.layout.pignate_footer, itemsList, false); + protected ViewBinding getListFooter() { + return PignateFooterBinding.inflate(activity.getLayoutInflater(), itemsList, false); } protected RecyclerView.LayoutManager getListLayoutManager() { @@ -246,8 +251,12 @@ public abstract class BaseListFragment extends BaseStateFragment itemsList.setLayoutManager(useGrid ? getGridLayoutManager() : getListLayoutManager()); infoListAdapter.setUseGridVariant(useGrid); - infoListAdapter.setFooter(getListFooter()); - infoListAdapter.setHeader(getListHeader()); + infoListAdapter.setFooter(getListFooter().getRoot()); + + final ViewBinding listHeader = getListHeader(); + if (listHeader != null) { + infoListAdapter.setHeader(listHeader.getRoot()); + } itemsList.setAdapter(infoListAdapter); } @@ -332,7 +341,6 @@ public abstract class BaseListFragment extends BaseStateFragment } } - protected void showStreamDialog(final StreamInfoItem item) { final Context context = getContext(); final Activity activity = getActivity(); @@ -359,6 +367,9 @@ public abstract class BaseListFragment extends BaseStateFragment StreamDialogEntry.share )); } + if (KoreUtil.shouldShowPlayWithKodi(context, item.getServiceId())) { + entries.add(StreamDialogEntry.play_with_kodi); + } StreamDialogEntry.setEnabledEntries(entries); new InfoItemDialog(activity, item, StreamDialogEntry.getCommands(context), diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java index 0342bb99c..003893517 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java @@ -14,20 +14,21 @@ import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.widget.Button; -import android.widget.ImageView; -import android.widget.LinearLayout; -import android.widget.TextView; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.ActionBar; import androidx.appcompat.app.AppCompatActivity; import androidx.core.content.ContextCompat; +import androidx.viewbinding.ViewBinding; import com.jakewharton.rxbinding4.view.RxView; import org.schabi.newpipe.R; 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.ListExtractor; import org.schabi.newpipe.extractor.NewPipe; @@ -78,22 +79,12 @@ public class ChannelFragment extends BaseListInfoFragment //////////////////////////////////////////////////////////////////////////*/ private SubscriptionManager subscriptionManager; - private View headerRootLayout; - private ImageView headerChannelBanner; - private ImageView headerAvatarView; - private TextView headerTitleView; - 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 FragmentChannelBinding channelBinding; + private ChannelHeaderBinding headerBinding; + private PlaylistControlBinding playlistControlBinding; + private MenuItem menuRssButton; - private TextView contentNotSupportedTextView; - private TextView kaomojiTextView; - private TextView noVideosTextView; public static ChannelFragment getInstance(final int serviceId, final String url, final String name) { @@ -132,53 +123,45 @@ public class ChannelFragment extends BaseListInfoFragment @Override public void onViewCreated(final View rootView, final Bundle savedInstanceState) { super.onViewCreated(rootView, savedInstanceState); - contentNotSupportedTextView = rootView.findViewById(R.id.error_content_not_supported); - kaomojiTextView = rootView.findViewById(R.id.channel_kaomoji); - noVideosTextView = rootView.findViewById(R.id.channel_no_videos); + channelBinding = FragmentChannelBinding.bind(rootView); } @Override public void onDestroy() { super.onDestroy(); - if (disposables != null) { - disposables.clear(); - } + disposables.clear(); if (subscribeButtonMonitor != null) { subscribeButtonMonitor.dispose(); } } + @Override + public void onDestroyView() { + channelBinding = null; + headerBinding = null; + playlistControlBinding = null; + super.onDestroyView(); + } + /*////////////////////////////////////////////////////////////////////////// // Init //////////////////////////////////////////////////////////////////////////*/ - protected View getListHeader() { - headerRootLayout = activity.getLayoutInflater() - .inflate(R.layout.channel_header, itemsList, false); - headerChannelBanner = headerRootLayout.findViewById(R.id.channel_banner_image); - headerAvatarView = headerRootLayout.findViewById(R.id.channel_avatar_view); - 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); + @Override + protected ViewBinding getListHeader() { + headerBinding = ChannelHeaderBinding + .inflate(activity.getLayoutInflater(), itemsList, false); + playlistControlBinding = headerBinding.playlistControl; - 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; + return headerBinding; } @Override protected void initListeners() { super.initListeners(); - headerSubChannelTitleView.setOnClickListener(this); - headerSubChannelAvatarView.setOnClickListener(this); + headerBinding.subChannelTitleView.setOnClickListener(this); + headerBinding.subChannelAvatarView.setOnClickListener(this); } /*////////////////////////////////////////////////////////////////////////// @@ -241,7 +224,7 @@ public class ChannelFragment extends BaseListInfoFragment private void monitorSubscription(final ChannelInfo info) { final Consumer onError = (Throwable throwable) -> { - animateView(headerSubscribeButton, false, 100); + animateView(headerBinding.channelSubscribeButton, false, 100); showSnackBarError(throwable, UserAction.SUBSCRIPTION, NewPipe.getNameOfService(currentInfo.getServiceId()), "Get subscription status", 0); @@ -351,15 +334,15 @@ public class ChannelFragment extends BaseListInfoFragment info.getAvatarUrl(), info.getDescription(), info.getSubscriberCount()); - subscribeButtonMonitor = monitorSubscribeButton(headerSubscribeButton, - mapOnSubscribe(channel, info)); + subscribeButtonMonitor = monitorSubscribeButton( + headerBinding.channelSubscribeButton, mapOnSubscribe(channel, info)); } else { if (DEBUG) { Log.d(TAG, "Found subscription to this channel!"); } final SubscriptionEntity subscription = subscriptionEntities.get(0); - subscribeButtonMonitor = monitorSubscribeButton(headerSubscribeButton, - mapOnUnsubscribe(subscription)); + subscribeButtonMonitor = monitorSubscribeButton( + headerBinding.channelSubscribeButton, mapOnUnsubscribe(subscription)); } }; } @@ -370,7 +353,8 @@ public class ChannelFragment extends BaseListInfoFragment + "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 textDuration = isButtonVisible ? 200 : 0; @@ -382,18 +366,21 @@ public class ChannelFragment extends BaseListInfoFragment final int subscribedText = ContextCompat.getColor(activity, R.color.subscribed_text_color); if (!isSubscribed) { - headerSubscribeButton.setText(R.string.subscribe_button_title); - animateBackgroundColor(headerSubscribeButton, backgroundDuration, subscribedBackground, - subscribeBackground); - animateTextColor(headerSubscribeButton, textDuration, subscribedText, subscribeText); + headerBinding.channelSubscribeButton.setText(R.string.subscribe_button_title); + animateBackgroundColor(headerBinding.channelSubscribeButton, backgroundDuration, + subscribedBackground, subscribeBackground); + animateTextColor(headerBinding.channelSubscribeButton, textDuration, subscribedText, + subscribeText); } else { - headerSubscribeButton.setText(R.string.subscribed_button_title); - animateBackgroundColor(headerSubscribeButton, backgroundDuration, subscribeBackground, - subscribedBackground); - animateTextColor(headerSubscribeButton, textDuration, subscribeText, subscribedText); + headerBinding.channelSubscribeButton.setText(R.string.subscribed_button_title); + animateBackgroundColor(headerBinding.channelSubscribeButton, backgroundDuration, + subscribeBackground, subscribedBackground); + 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 public void showLoading() { super.showLoading(); - IMAGE_LOADER.cancelDisplayTask(headerChannelBanner); - IMAGE_LOADER.cancelDisplayTask(headerAvatarView); - IMAGE_LOADER.cancelDisplayTask(headerSubChannelAvatarView); - animateView(headerSubscribeButton, false, 100); + IMAGE_LOADER.cancelDisplayTask(headerBinding.channelBannerImage); + IMAGE_LOADER.cancelDisplayTask(headerBinding.channelAvatarView); + IMAGE_LOADER.cancelDisplayTask(headerBinding.subChannelAvatarView); + animateView(headerBinding.channelSubscribeButton, false, 100); } @Override public void handleResult(@NonNull final ChannelInfo result) { super.handleResult(result); - headerRootLayout.setVisibility(View.VISIBLE); - IMAGE_LOADER.displayImage(result.getBannerUrl(), headerChannelBanner, + headerBinding.getRoot().setVisibility(View.VISIBLE); + IMAGE_LOADER.displayImage(result.getBannerUrl(), headerBinding.channelBannerImage, ImageDisplayConstants.DISPLAY_BANNER_OPTIONS); - IMAGE_LOADER.displayImage(result.getAvatarUrl(), headerAvatarView, + IMAGE_LOADER.displayImage(result.getAvatarUrl(), headerBinding.channelAvatarView, ImageDisplayConstants.DISPLAY_AVATAR_OPTIONS); - IMAGE_LOADER.displayImage(result.getParentChannelAvatarUrl(), headerSubChannelAvatarView, + IMAGE_LOADER.displayImage(result.getParentChannelAvatarUrl(), + headerBinding.subChannelAvatarView, ImageDisplayConstants.DISPLAY_AVATAR_OPTIONS); - headerSubscribersTextView.setVisibility(View.VISIBLE); + headerBinding.channelSubscriberView.setVisibility(View.VISIBLE); if (result.getSubscriberCount() >= 0) { - headerSubscribersTextView.setText(Localization + headerBinding.channelSubscriberView.setText(Localization .shortSubscriberCount(activity, result.getSubscriberCount())); } else { - headerSubscribersTextView.setText(R.string.subscribers_count_not_available); + headerBinding.channelSubscriberView.setText(R.string.subscribers_count_not_available); } if (!TextUtils.isEmpty(currentInfo.getParentChannelName())) { - headerSubChannelTitleView.setText(String.format( + headerBinding.subChannelTitleView.setText(String.format( getString(R.string.channel_created_by), currentInfo.getParentChannelName()) ); - headerSubChannelTitleView.setVisibility(View.VISIBLE); - headerSubChannelAvatarView.setVisibility(View.VISIBLE); + headerBinding.subChannelTitleView.setVisibility(View.VISIBLE); + headerBinding.subChannelAvatarView.setVisibility(View.VISIBLE); } else { - headerSubChannelTitleView.setVisibility(View.GONE); + headerBinding.subChannelTitleView.setVisibility(View.GONE); } if (menuRssButton != null) { menuRssButton.setVisible(!TextUtils.isEmpty(result.getFeedUrl())); } - playlistCtrl.setVisibility(View.VISIBLE); + playlistControlBinding.getRoot().setVisibility(View.VISIBLE); final List errors = new ArrayList<>(result.getErrors()); if (!errors.isEmpty()) { @@ -516,29 +504,32 @@ public class ChannelFragment extends BaseListInfoFragment updateSubscription(result); monitorSubscription(result); - headerPlayAllButton.setOnClickListener(view -> NavigationHelper - .playOnMainPlayer(activity, getPlayQueue())); - headerPopupButton.setOnClickListener(view -> NavigationHelper - .playOnPopupPlayer(activity, getPlayQueue(), false)); - headerBackgroundButton.setOnClickListener(view -> NavigationHelper - .playOnBackgroundPlayer(activity, getPlayQueue(), false)); + playlistControlBinding.playlistCtrlPlayAllButton + .setOnClickListener(view -> NavigationHelper + .playOnMainPlayer(activity, getPlayQueue())); + playlistControlBinding.playlistCtrlPlayPopupButton + .setOnClickListener(view -> NavigationHelper + .playOnPopupPlayer(activity, getPlayQueue(), false)); + playlistControlBinding.playlistCtrlPlayBgButton + .setOnClickListener(view -> NavigationHelper + .playOnBackgroundPlayer(activity, getPlayQueue(), false)); - headerPopupButton.setOnLongClickListener(view -> { + playlistControlBinding.playlistCtrlPlayPopupButton.setOnLongClickListener(view -> { NavigationHelper.enqueueOnPopupPlayer(activity, getPlayQueue(), true); return true; }); - headerBackgroundButton.setOnLongClickListener(view -> { + playlistControlBinding.playlistCtrlPlayBgButton.setOnLongClickListener(view -> { NavigationHelper.enqueueOnBackgroundPlayer(activity, getPlayQueue(), true); return true; }); } private void showContentNotSupported() { - contentNotSupportedTextView.setVisibility(View.VISIBLE); - kaomojiTextView.setText("(︶︹︺)"); - kaomojiTextView.setTextSize(TypedValue.COMPLEX_UNIT_SP, 45f); - noVideosTextView.setVisibility(View.GONE); + channelBinding.errorContentNotSupported.setVisibility(View.VISIBLE); + channelBinding.channelKaomoji.setText("(︶︹︺)"); + channelBinding.channelKaomoji.setTextSize(TypedValue.COMPLEX_UNIT_SP, 45f); + channelBinding.channelNoVideos.setVisibility(View.GONE); } private PlayQueue getPlayQueue() { @@ -596,7 +587,7 @@ public class ChannelFragment extends BaseListInfoFragment public void setTitle(final String title) { super.setTitle(title); if (!useAsFrontPage) { - headerTitleView.setText(title); + headerBinding.channelTitleView.setText(title); } } } diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/playlist/PlaylistFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/playlist/PlaylistFragment.java index d1a964fb2..6e723a686 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/playlist/PlaylistFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/playlist/PlaylistFragment.java @@ -11,23 +11,27 @@ import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; -import android.widget.ImageView; -import android.widget.TextView; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.AppCompatActivity; +import androidx.appcompat.content.res.AppCompatResources; +import androidx.viewbinding.ViewBinding; import org.reactivestreams.Subscriber; import org.reactivestreams.Subscription; import org.schabi.newpipe.NewPipeDatabase; import org.schabi.newpipe.R; 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.ListExtractor; import org.schabi.newpipe.extractor.NewPipe; +import org.schabi.newpipe.extractor.ServiceList; import org.schabi.newpipe.extractor.exceptions.ExtractionException; 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.StreamType; 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.util.ExtractorHelper; import org.schabi.newpipe.util.ImageDisplayConstants; +import org.schabi.newpipe.util.KoreUtil; import org.schabi.newpipe.util.Localization; import org.schabi.newpipe.util.NavigationHelper; import org.schabi.newpipe.util.ShareUtils; import org.schabi.newpipe.util.StreamDialogEntry; -import org.schabi.newpipe.util.ThemeHelper; import java.util.ArrayList; import java.util.Arrays; @@ -58,6 +62,7 @@ import io.reactivex.rxjava3.disposables.CompositeDisposable; import io.reactivex.rxjava3.disposables.Disposable; import static org.schabi.newpipe.util.AnimationUtils.animateView; +import static org.schabi.newpipe.util.ThemeHelper.resolveResourceIdFromAttr; public class PlaylistFragment extends BaseListInfoFragment { private CompositeDisposable disposables; @@ -70,17 +75,8 @@ public class PlaylistFragment extends BaseListInfoFragment { // Views //////////////////////////////////////////////////////////////////////////*/ - private View headerRootLayout; - private TextView headerTitleView; - 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 PlaylistHeaderBinding headerBinding; + private PlaylistControlBinding playlistControlBinding; private MenuItem playlistBookmarkButton; @@ -115,22 +111,13 @@ public class PlaylistFragment extends BaseListInfoFragment { // Init //////////////////////////////////////////////////////////////////////////*/ - protected View getListHeader() { - headerRootLayout = activity.getLayoutInflater() - .inflate(R.layout.playlist_header, itemsList, false); - headerTitleView = headerRootLayout.findViewById(R.id.playlist_title_view); - headerUploaderLayout = headerRootLayout.findViewById(R.id.uploader_layout); - 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); + @Override + protected ViewBinding getListHeader() { + headerBinding = PlaylistHeaderBinding + .inflate(activity.getLayoutInflater(), itemsList, false); + playlistControlBinding = headerBinding.playlistControl; - 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; + return headerBinding; } @Override @@ -171,6 +158,9 @@ public class PlaylistFragment extends BaseListInfoFragment { StreamDialogEntry.share )); } + if (KoreUtil.shouldShowPlayWithKodi(context, item.getServiceId())) { + entries.add(StreamDialogEntry.play_with_kodi); + } StreamDialogEntry.setEnabledEntries(entries); StreamDialogEntry.start_here_on_background.setCustomAction((fragment, infoItem) -> @@ -196,6 +186,9 @@ public class PlaylistFragment extends BaseListInfoFragment { @Override public void onDestroyView() { + headerBinding = null; + playlistControlBinding = null; + super.onDestroyView(); if (isBookmarkButtonReady != null) { isBookmarkButtonReady.set(false); @@ -268,25 +261,25 @@ public class PlaylistFragment extends BaseListInfoFragment { @Override public void showLoading() { super.showLoading(); - animateView(headerRootLayout, false, 200); + animateView(headerBinding.getRoot(), false, 200); animateView(itemsList, false, 100); - IMAGE_LOADER.cancelDisplayTask(headerUploaderAvatar); - animateView(headerUploaderLayout, false, 200); + IMAGE_LOADER.cancelDisplayTask(headerBinding.uploaderAvatarView); + animateView(headerBinding.uploaderLayout, false, 200); } @Override public void handleResult(@NonNull final PlaylistInfo result) { super.handleResult(result); - animateView(headerRootLayout, true, 100); - animateView(headerUploaderLayout, true, 300); - headerUploaderLayout.setOnClickListener(null); + animateView(headerBinding.getRoot(), true, 100); + animateView(headerBinding.uploaderLayout, true, 300); + headerBinding.uploaderLayout.setOnClickListener(null); // If we have an uploader put them into the UI if (!TextUtils.isEmpty(result.getUploaderName())) { - headerUploaderName.setText(result.getUploaderName()); + headerBinding.uploaderName.setText(result.getUploaderName()); if (!TextUtils.isEmpty(result.getUploaderUrl())) { - headerUploaderLayout.setOnClickListener(v -> { + headerBinding.uploaderLayout.setOnClickListener(v -> { try { NavigationHelper.openChannelFragment(getFM(), result.getServiceId(), result.getUploaderUrl(), result.getUploaderName()); @@ -296,14 +289,29 @@ public class PlaylistFragment extends BaseListInfoFragment { }); } } 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, - ImageDisplayConstants.DISPLAY_AVATAR_OPTIONS); - headerStreamCount.setText(Localization + 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); + } + + headerBinding.playlistStreamCount.setText(Localization .localizeStreamCount(getContext(), result.getStreamCount())); if (!result.getErrors().isEmpty()) { @@ -317,19 +325,19 @@ public class PlaylistFragment extends BaseListInfoFragment { .observeOn(AndroidSchedulers.mainThread()) .subscribe(getPlaylistBookmarkSubscriber()); - headerPlayAllButton.setOnClickListener(view -> + playlistControlBinding.playlistCtrlPlayAllButton.setOnClickListener(view -> NavigationHelper.playOnMainPlayer(activity, getPlayQueue())); - headerPopupButton.setOnClickListener(view -> + playlistControlBinding.playlistCtrlPlayPopupButton.setOnClickListener(view -> NavigationHelper.playOnPopupPlayer(activity, getPlayQueue(), false)); - headerBackgroundButton.setOnClickListener(view -> + playlistControlBinding.playlistCtrlPlayBgButton.setOnClickListener(view -> NavigationHelper.playOnBackgroundPlayer(activity, getPlayQueue(), false)); - headerPopupButton.setOnLongClickListener(view -> { + playlistControlBinding.playlistCtrlPlayPopupButton.setOnLongClickListener(view -> { NavigationHelper.enqueueOnPopupPlayer(activity, getPlayQueue(), true); return true; }); - headerBackgroundButton.setOnLongClickListener(view -> { + playlistControlBinding.playlistCtrlPlayBgButton.setOnLongClickListener(view -> { NavigationHelper.enqueueOnBackgroundPlayer(activity, getPlayQueue(), true); return true; }); @@ -438,7 +446,7 @@ public class PlaylistFragment extends BaseListInfoFragment { @Override public void setTitle(final String title) { super.setTitle(title); - headerTitleView.setText(title); + headerBinding.playlistTitleView.setText(title); } private void onBookmarkClicked() { @@ -476,7 +484,7 @@ public class PlaylistFragment extends BaseListInfoFragment { final int titleRes = playlistEntity == null ? R.string.bookmark_playlist : R.string.unbookmark_playlist; - playlistBookmarkButton.setIcon(ThemeHelper.resolveResourceIdFromAttr(activity, iconAttr)); + playlistBookmarkButton.setIcon(resolveResourceIdFromAttr(activity, iconAttr)); playlistBookmarkButton.setTitle(titleRes); } } diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/search/SearchFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/search/SearchFragment.java index 02dbf176b..511040827 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/search/SearchFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/search/SearchFragment.java @@ -37,16 +37,21 @@ import androidx.recyclerview.widget.RecyclerView; import org.schabi.newpipe.R; import org.schabi.newpipe.ReCaptchaActivity; 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.ListExtractor; +import org.schabi.newpipe.extractor.MetaInfo; import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipe.extractor.Page; import org.schabi.newpipe.extractor.StreamingService; import org.schabi.newpipe.extractor.exceptions.ParsingException; import org.schabi.newpipe.extractor.search.SearchExtractor; 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.list.BaseListFragment; +import org.schabi.newpipe.ktx.ExceptionUtils; import org.schabi.newpipe.local.history.HistoryRecordManager; import org.schabi.newpipe.report.ErrorActivity; 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.Constants; import org.schabi.newpipe.util.DeviceUtils; -import org.schabi.newpipe.util.ExceptionUtils; import org.schabi.newpipe.util.ExtractorHelper; import org.schabi.newpipe.util.NavigationHelper; 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 java.util.Arrays.asList; import static org.schabi.newpipe.util.AnimationUtils.animateView; +import static org.schabi.newpipe.util.ExtractorHelper.showMetaInfoInTextView; public class SearchFragment extends BaseListFragment> implements BackPressable { @@ -129,6 +134,9 @@ public class SearchFragment extends BaseListFragment()); @@ -620,7 +644,8 @@ public class SearchFragment extends BaseListFragment suggestionListAdapter.setItems(suggestions)); + searchBinding.suggestionsList.smoothScrollToPosition(0); + searchBinding.suggestionsList.post(() -> suggestionListAdapter.setItems(suggestions)); if (suggestionsPanelVisible && errorPanelRoot.getVisibility() == View.VISIBLE) { hideLoading(); @@ -973,8 +999,14 @@ public class SearchFragment extends BaseListFragment cannot be bundled without creating some containers + metaInfo = new MetaInfo[result.getMetaInfo().size()]; + metaInfo = result.getMetaInfo().toArray(metaInfo); + handleSearchSuggestion(); + showMetaInfoInTextView(result.getMetaInfo(), metaInfoTextView, metaInfoSeparator); + lastSearchedString = searchString; nextPage = result.getNextPage(); @@ -993,7 +1025,7 @@ public class SearchFragment extends BaseListFragment"; 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 -> { - correctSuggestion.setVisibility(View.GONE); + searchBinding.correctSuggestion.setOnClickListener(v -> { + searchBinding.correctSuggestion.setVisibility(View.GONE); search(searchSuggestion, contentFilter, sortFilter); searchEditText.setText(searchSuggestion); }); - correctSuggestion.setOnLongClickListener(v -> { + searchBinding.correctSuggestion.setOnLongClickListener(v -> { searchEditText.setText(searchSuggestion); searchEditText.setSelection(searchSuggestion.length()); showKeyboardSearch(); return true; }); - correctSuggestion.setVisibility(View.VISIBLE); + searchBinding.correctSuggestion.setVisibility(View.VISIBLE); } } diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/videos/RelatedVideosFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/videos/RelatedVideosFragment.java index 907615b45..7fb41f0e5 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/videos/RelatedVideosFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/videos/RelatedVideosFragment.java @@ -8,13 +8,14 @@ import android.view.Menu; import android.view.MenuInflater; import android.view.View; import android.view.ViewGroup; -import android.widget.Switch; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.preference.PreferenceManager; +import androidx.viewbinding.ViewBinding; import org.schabi.newpipe.R; +import org.schabi.newpipe.databinding.RelatedStreamsHeaderBinding; import org.schabi.newpipe.extractor.ListExtractor; import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipe.extractor.stream.StreamInfo; @@ -38,8 +39,7 @@ public class RelatedVideosFragment extends BaseListInfoFragment + headerBinding.autoplaySwitch.setChecked(autoplay); + headerBinding.autoplaySwitch.setOnCheckedChangeListener((compoundButton, b) -> PreferenceManager.getDefaultSharedPreferences(requireContext()).edit() .putBoolean(getString(R.string.auto_queue_key), b).apply()); - return headerRootLayout; + return headerBinding; } else { return null; } @@ -107,8 +111,8 @@ public class RelatedVideosFragment extends BaseListInfoFragment() { + + 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) + } +} diff --git a/app/src/main/java/org/schabi/newpipe/info_list/StreamSegmentItem.kt b/app/src/main/java/org/schabi/newpipe/info_list/StreamSegmentItem.kt new file mode 100644 index 000000000..798b9b287 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/info_list/StreamSegmentItem.kt @@ -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() { + + 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(R.id.previewImage), + ImageDisplayConstants.DISPLAY_THUMBNAIL_OPTIONS + ) + } + viewHolder.root.findViewById(R.id.textViewTitle).text = item.title + viewHolder.root.findViewById(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) { + if (payloads.contains(PAYLOAD_SELECT)) { + viewHolder.root.isSelected = isSelected + return + } + super.bind(viewHolder, position, payloads) + } + + override fun getLayout() = R.layout.item_stream_segment +} diff --git a/app/src/main/java/org/schabi/newpipe/info_list/holder/StreamMiniInfoItemHolder.java b/app/src/main/java/org/schabi/newpipe/info_list/holder/StreamMiniInfoItemHolder.java index c0096ed10..12eab4734 100644 --- a/app/src/main/java/org/schabi/newpipe/info_list/holder/StreamMiniInfoItemHolder.java +++ b/app/src/main/java/org/schabi/newpipe/info_list/holder/StreamMiniInfoItemHolder.java @@ -70,7 +70,8 @@ public class StreamMiniInfoItemHolder extends InfoItemHolder { } else { 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.setBackgroundColor(ContextCompat.getColor(itemBuilder.getContext(), R.color.live_duration_background_color)); diff --git a/app/src/main/java/org/schabi/newpipe/ktx/OffsetDateTime.kt b/app/src/main/java/org/schabi/newpipe/ktx/OffsetDateTime.kt index b3df83c25..0d1a534b9 100644 --- a/app/src/main/java/org/schabi/newpipe/ktx/OffsetDateTime.kt +++ b/app/src/main/java/org/schabi/newpipe/ktx/OffsetDateTime.kt @@ -1,10 +1,29 @@ package org.schabi.newpipe.ktx import java.time.OffsetDateTime -import java.time.ZoneId +import java.time.ZoneOffset +import java.time.temporal.ChronoField import java.util.Calendar +import java.util.Date import java.util.GregorianCalendar +import java.util.TimeZone -fun OffsetDateTime.toCalendar(zoneId: ZoneId = ZoneId.systemDefault()): Calendar { - return GregorianCalendar.from(if (zoneId != offset) atZoneSameInstant(zoneId) else toZonedDateTime()) +// This method is a modified version of GregorianCalendar.from(ZonedDateTime). +// 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 } diff --git a/app/src/main/java/org/schabi/newpipe/ktx/Throwable.kt b/app/src/main/java/org/schabi/newpipe/ktx/Throwable.kt new file mode 100644 index 000000000..b95f46fd4 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/ktx/Throwable.kt @@ -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() + +/** + * 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 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 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 +} diff --git a/app/src/main/java/org/schabi/newpipe/local/BaseLocalListFragment.java b/app/src/main/java/org/schabi/newpipe/local/BaseLocalListFragment.java index 8e88ceaed..38ecc1c63 100644 --- a/app/src/main/java/org/schabi/newpipe/local/BaseLocalListFragment.java +++ b/app/src/main/java/org/schabi/newpipe/local/BaseLocalListFragment.java @@ -4,6 +4,8 @@ import android.content.SharedPreferences; import android.content.res.Configuration; import android.content.res.Resources; import android.os.Bundle; + +import androidx.annotation.Nullable; import androidx.preference.PreferenceManager; import android.util.Log; import android.view.Menu; @@ -15,8 +17,10 @@ import androidx.fragment.app.Fragment; import androidx.recyclerview.widget.GridLayoutManager; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; +import androidx.viewbinding.ViewBinding; import org.schabi.newpipe.R; +import org.schabi.newpipe.databinding.PignateFooterBinding; import org.schabi.newpipe.fragments.BaseStateFragment; import org.schabi.newpipe.fragments.list.ListViewContract; @@ -42,8 +46,8 @@ public abstract class BaseLocalListFragment extends BaseStateFragment //////////////////////////////////////////////////////////////////////////*/ private static final int LIST_MODE_UPDATE_FLAG = 0x32; - private View headerRootView; - private View footerRootView; + private ViewBinding headerRootBinding; + private ViewBinding footerRootBinding; protected LocalItemListAdapter itemListAdapter; protected RecyclerView itemsList; private int updateFlags = 0; @@ -86,12 +90,13 @@ public abstract class BaseLocalListFragment extends BaseStateFragment // Lifecycle - View //////////////////////////////////////////////////////////////////////////*/ - protected View getListHeader() { + @Nullable + protected ViewBinding getListHeader() { return null; } - protected View getListFooter() { - return activity.getLayoutInflater().inflate(R.layout.pignate_footer, itemsList, false); + protected ViewBinding getListFooter() { + return PignateFooterBinding.inflate(activity.getLayoutInflater(), itemsList, false); } protected RecyclerView.LayoutManager getGridLayoutManager() { @@ -120,10 +125,12 @@ public abstract class BaseLocalListFragment extends BaseStateFragment itemsList.setLayoutManager(useGrid ? getGridLayoutManager() : getListLayoutManager()); itemListAdapter.setUseGridVariant(useGrid); - headerRootView = getListHeader(); - itemListAdapter.setHeader(headerRootView); - footerRootView = getListFooter(); - itemListAdapter.setFooter(footerRootView); + headerRootBinding = getListHeader(); + if (headerRootBinding != null) { + itemListAdapter.setHeader(headerRootBinding.getRoot()); + } + footerRootBinding = getListFooter(); + itemListAdapter.setFooter(footerRootBinding.getRoot()); itemsList.setAdapter(itemListAdapter); } @@ -180,8 +187,8 @@ public abstract class BaseLocalListFragment extends BaseStateFragment if (itemsList != null) { animateView(itemsList, false, 200); } - if (headerRootView != null) { - animateView(headerRootView, false, 200); + if (headerRootBinding != null) { + animateView(headerRootBinding.getRoot(), false, 200); } } @@ -191,8 +198,8 @@ public abstract class BaseLocalListFragment extends BaseStateFragment if (itemsList != null) { animateView(itemsList, true, 200); } - if (headerRootView != null) { - animateView(headerRootView, true, 200); + if (headerRootBinding != null) { + animateView(headerRootBinding.getRoot(), true, 200); } } @@ -204,8 +211,8 @@ public abstract class BaseLocalListFragment extends BaseStateFragment if (itemsList != null) { animateView(itemsList, false, 200); } - if (headerRootView != null) { - animateView(headerRootView, false, 200); + if (headerRootBinding != null) { + animateView(headerRootBinding.getRoot(), false, 200); } } diff --git a/app/src/main/java/org/schabi/newpipe/local/feed/FeedFragment.kt b/app/src/main/java/org/schabi/newpipe/local/feed/FeedFragment.kt index dbe91ec55..6df15c8b2 100644 --- a/app/src/main/java/org/schabi/newpipe/local/feed/FeedFragment.kt +++ b/app/src/main/java/org/schabi/newpipe/local/feed/FeedFragment.kt @@ -37,18 +37,10 @@ import androidx.lifecycle.ViewModelProvider import androidx.preference.PreferenceManager import androidx.swiperefreshlayout.widget.SwipeRefreshLayout 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.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.local.feed.service.FeedLoadService import org.schabi.newpipe.report.UserAction @@ -57,6 +49,12 @@ import org.schabi.newpipe.util.Localization import java.util.Calendar class FeedFragment : BaseListFragment() { + 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 swipeRefreshLayout: SwipeRefreshLayout @State @@ -86,15 +84,17 @@ class FeedFragment : BaseListFragment() { override fun onViewCreated(rootView: View, savedInstanceState: Bundle?) { super.onViewCreated(rootView, savedInstanceState) - swipeRefreshLayout = requireView().findViewById(R.id.swiperefresh) - swipeRefreshLayout.setOnRefreshListener { reloadContent() } + _feedBinding = FragmentFeedBinding.bind(rootView) + _errorBinding = feedBinding.errorPanel + + feedBinding.swiperefresh.setOnRefreshListener { reloadContent() } viewModel = ViewModelProvider(this, FeedViewModel.Factory(requireContext(), groupId)).get(FeedViewModel::class.java) viewModel.stateLiveData.observe(viewLifecycleOwner, Observer { it?.let(::handleResult) }) } override fun onPause() { super.onPause() - listState = items_list?.layoutManager?.onSaveInstanceState() + listState = _feedBinding?.itemsList?.layoutManager?.onSaveInstanceState() } override fun onResume() { @@ -112,7 +112,8 @@ class FeedFragment : BaseListFragment() { override fun initListeners() { super.initListeners() - refresh_root_view.setOnClickListener { + // Using the non-null property may result in a NullPointerException + _feedBinding?.refreshRootView?.setOnClickListener { triggerUpdate() } } @@ -169,55 +170,60 @@ class FeedFragment : BaseListFragment() { activity?.supportActionBar?.subtitle = null } + override fun onDestroyView() { + _feedBinding = null + super.onDestroyView() + } + // ///////////////////////////////////////////////////////////////////////// // Handling // ///////////////////////////////////////////////////////////////////////// override fun showLoading() { - animateView(refresh_root_view, false, 0) - animateView(items_list, false, 0) + animateView(feedBinding.refreshRootView, false, 0) + animateView(feedBinding.itemsList, false, 0) - animateView(loading_progress_bar, true, 200) - animateView(loading_progress_text, true, 200) + animateView(feedBinding.loadingProgressBar, true, 200) + animateView(feedBinding.loadingProgressText, true, 200) - empty_state_view?.let { animateView(it, false, 0) } - animateView(error_panel, false, 0) + animateView(feedBinding.emptyStateView.root, false, 0) + animateView(errorBinding.root, false, 0) } override fun hideLoading() { - animateView(refresh_root_view, true, 200) - animateView(items_list, true, 300) + animateView(feedBinding.refreshRootView, true, 200) + animateView(feedBinding.itemsList, true, 300) - animateView(loading_progress_bar, false, 0) - animateView(loading_progress_text, false, 0) + animateView(feedBinding.loadingProgressBar, false, 0) + animateView(feedBinding.loadingProgressText, false, 0) - empty_state_view?.let { animateView(it, false, 0) } - animateView(error_panel, false, 0) - swipeRefreshLayout.isRefreshing = false + animateView(feedBinding.emptyStateView.root, false, 0) + animateView(errorBinding.root, false, 0) + feedBinding.swiperefresh.isRefreshing = false } override fun showEmptyState() { - animateView(refresh_root_view, true, 200) - animateView(items_list, false, 0) + animateView(feedBinding.refreshRootView, true, 200) + animateView(feedBinding.itemsList, false, 0) - animateView(loading_progress_bar, false, 0) - animateView(loading_progress_text, false, 0) + animateView(feedBinding.loadingProgressBar, false, 0) + animateView(feedBinding.loadingProgressText, false, 0) - empty_state_view?.let { animateView(it, true, 800) } - animateView(error_panel, false, 0) + animateView(feedBinding.emptyStateView.root, true, 800) + animateView(errorBinding.root, false, 0) } override fun showError(message: String, showRetryButton: Boolean) { infoListAdapter.clearStreamItemList() - animateView(refresh_root_view, false, 120) - animateView(items_list, false, 120) + animateView(feedBinding.refreshRootView, false, 120) + animateView(feedBinding.itemsList, false, 120) - animateView(loading_progress_bar, false, 120) - animateView(loading_progress_text, false, 120) + animateView(feedBinding.loadingProgressBar, false, 120) + animateView(feedBinding.loadingProgressText, false, 120) - error_message_view.text = message - animateView(error_button_retry, showRetryButton, if (showRetryButton) 600 else 0) - animateView(error_panel, true, 300) + errorBinding.errorMessageView.text = message + animateView(errorBinding.errorButtonRetry, showRetryButton, if (showRetryButton) 600 else 0) + animateView(errorBinding.root, true, 300) } override fun handleResult(result: FeedState) { @@ -237,33 +243,36 @@ class FeedFragment : BaseListFragment() { progressState.maxProgress == -1 if (!isIndeterminate) { - loading_progress_text.text = "${progressState.currentProgress}/${progressState.maxProgress}" + feedBinding.loadingProgressText.text = "${progressState.currentProgress}/${progressState.maxProgress}" } else if (progressState.progressMessage > 0) { - loading_progress_text?.setText(progressState.progressMessage) + _feedBinding?.loadingProgressText?.setText(progressState.progressMessage) } else { - loading_progress_text?.text = "∞/∞" + _feedBinding?.loadingProgressText?.text = "∞/∞" } - loading_progress_bar.isIndeterminate = isIndeterminate || + feedBinding.loadingProgressBar.isIndeterminate = isIndeterminate || (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) { infoListAdapter.setInfoItemList(loadedState.items) listState?.run { - items_list.layoutManager?.onRestoreInstanceState(listState) + feedBinding.itemsList.layoutManager?.onRestoreInstanceState(listState) listState = null } oldestSubscriptionUpdate = loadedState.oldestUpdate val loadedCount = loadedState.notLoadedCount > 0 - refresh_subtitle_text.isVisible = loadedCount + feedBinding.refreshSubtitleText.isVisible = 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()) { @@ -300,7 +309,7 @@ class FeedFragment : BaseListFragment() { else -> "—" } - refresh_text?.text = getString(R.string.feed_oldest_subscription_update, oldestSubscriptionUpdateText) + feedBinding.refreshText.text = getString(R.string.feed_oldest_subscription_update, oldestSubscriptionUpdateText) } // ///////////////////////////////////////////////////////////////////////// diff --git a/app/src/main/java/org/schabi/newpipe/local/feed/service/FeedLoadService.kt b/app/src/main/java/org/schabi/newpipe/local/feed/service/FeedLoadService.kt index ddbbea23d..5ed7998d2 100644 --- a/app/src/main/java/org/schabi/newpipe/local/feed/service/FeedLoadService.kt +++ b/app/src/main/java/org/schabi/newpipe/local/feed/service/FeedLoadService.kt @@ -30,6 +30,7 @@ import android.os.IBinder import android.util.Log import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat +import androidx.core.app.ServiceCompat import androidx.preference.PreferenceManager import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers import io.reactivex.rxjava3.core.Flowable @@ -42,19 +43,20 @@ import io.reactivex.rxjava3.processors.PublishProcessor import io.reactivex.rxjava3.schedulers.Schedulers import org.reactivestreams.Subscriber import org.reactivestreams.Subscription +import org.schabi.newpipe.App import org.schabi.newpipe.MainActivity.DEBUG import org.schabi.newpipe.R import org.schabi.newpipe.database.feed.model.FeedGroupEntity import org.schabi.newpipe.extractor.ListInfo import org.schabi.newpipe.extractor.exceptions.ReCaptchaException 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.service.FeedEventManager.Event.ErrorResultEvent 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.postEvent import org.schabi.newpipe.local.subscription.SubscriptionManager -import org.schabi.newpipe.util.ExceptionUtils import org.schabi.newpipe.util.ExtractorHelper import java.io.IOException import java.time.OffsetDateTime @@ -67,7 +69,7 @@ class FeedLoadService : Service() { companion object { private val TAG = FeedLoadService::class.java.simpleName 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. @@ -147,7 +149,7 @@ class FeedLoadService : Service() { private fun stopService() { disposeAll() - stopForeground(true) + ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE) notificationManager.cancel(NOTIFICATION_ID) stopSelf() } @@ -342,7 +344,7 @@ class FeedLoadService : Service() { error is IOException -> throw error cause is IOException -> throw cause - ExceptionUtils.isNetworkRelated(error) -> throw IOException(error) + error.isNetworkRelated -> throw IOException(error) } } } diff --git a/app/src/main/java/org/schabi/newpipe/local/history/StatisticsPlaylistFragment.java b/app/src/main/java/org/schabi/newpipe/local/history/StatisticsPlaylistFragment.java index 9750d9820..f9aa38054 100644 --- a/app/src/main/java/org/schabi/newpipe/local/history/StatisticsPlaylistFragment.java +++ b/app/src/main/java/org/schabi/newpipe/local/history/StatisticsPlaylistFragment.java @@ -10,13 +10,12 @@ import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; -import android.widget.ImageView; -import android.widget.TextView; import android.widget.Toast; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.AlertDialog; +import androidx.viewbinding.ViewBinding; 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.stream.StreamStatisticsEntry; 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.StreamType; 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.UserAction; import org.schabi.newpipe.settings.SettingsActivity; +import org.schabi.newpipe.util.KoreUtil; import org.schabi.newpipe.util.NavigationHelper; import org.schabi.newpipe.util.OnClickGesture; import org.schabi.newpipe.util.StreamDialogEntry; @@ -59,13 +61,10 @@ public class StatisticsPlaylistFragment @State Parcelable itemsListState; private StatisticSortMode sortMode = StatisticSortMode.LAST_PLAYED; - private View headerPlayAllButton; - private View headerPopupButton; - private View headerBackgroundButton; - private View playlistCtrl; - private View sortButton; - private ImageView sortButtonIcon; - private TextView sortButtonText; + + private StatisticPlaylistControlBinding headerBinding; + private PlaylistControlBinding playlistControlBinding; + /* Used for independent events */ private Subscription databaseSubscription; private HistoryRecordManager recordManager; @@ -130,17 +129,12 @@ public class StatisticsPlaylistFragment } @Override - protected View getListHeader() { - final View headerRootLayout = activity.getLayoutInflater() - .inflate(R.layout.statistic_playlist_control, itemsList, false); - playlistCtrl = 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); - sortButton = headerRootLayout.findViewById(R.id.sortButton); - sortButtonIcon = headerRootLayout.findViewById(R.id.sortButtonIcon); - sortButtonText = headerRootLayout.findViewById(R.id.sortButtonText); - return headerRootLayout; + protected ViewBinding getListHeader() { + headerBinding = StatisticPlaylistControlBinding.inflate(activity.getLayoutInflater(), + itemsList, false); + playlistControlBinding = headerBinding.playlistControl; + + return headerBinding; } @Override @@ -244,14 +238,13 @@ public class StatisticsPlaylistFragment if (itemListAdapter != null) { itemListAdapter.unsetSelectedListener(); } - if (headerBackgroundButton != null) { - headerBackgroundButton.setOnClickListener(null); - } - if (headerPlayAllButton != null) { - headerPlayAllButton.setOnClickListener(null); - } - if (headerPopupButton != null) { - headerPopupButton.setOnClickListener(null); + if (playlistControlBinding != null) { + playlistControlBinding.playlistCtrlPlayBgButton.setOnClickListener(null); + playlistControlBinding.playlistCtrlPlayAllButton.setOnClickListener(null); + playlistControlBinding.playlistCtrlPlayPopupButton.setOnClickListener(null); + + headerBinding = null; + playlistControlBinding = null; } if (databaseSubscription != null) { @@ -310,7 +303,7 @@ public class StatisticsPlaylistFragment return; } - playlistCtrl.setVisibility(View.VISIBLE); + playlistControlBinding.getRoot().setVisibility(View.VISIBLE); itemListAdapter.clearStreamItemList(); @@ -325,13 +318,13 @@ public class StatisticsPlaylistFragment itemsListState = null; } - headerPlayAllButton.setOnClickListener(view -> + playlistControlBinding.playlistCtrlPlayAllButton.setOnClickListener(view -> NavigationHelper.playOnMainPlayer(activity, getPlayQueue())); - headerPopupButton.setOnClickListener(view -> + playlistControlBinding.playlistCtrlPlayPopupButton.setOnClickListener(view -> NavigationHelper.playOnPopupPlayer(activity, getPlayQueue(), false)); - headerBackgroundButton.setOnClickListener(view -> + playlistControlBinding.playlistCtrlPlayBgButton.setOnClickListener(view -> NavigationHelper.playOnBackgroundPlayer(activity, getPlayQueue(), false)); - sortButton.setOnClickListener(view -> toggleSortMode()); + headerBinding.sortButton.setOnClickListener(view -> toggleSortMode()); hideLoading(); } @@ -367,15 +360,15 @@ public class StatisticsPlaylistFragment if (sortMode == StatisticSortMode.LAST_PLAYED) { sortMode = StatisticSortMode.MOST_PLAYED; setTitle(getString(R.string.title_most_played)); - sortButtonIcon.setImageResource( + headerBinding.sortButtonIcon.setImageResource( ThemeHelper.resolveResourceIdFromAttr(requireContext(), R.attr.ic_history)); - sortButtonText.setText(R.string.title_last_played); + headerBinding.sortButtonText.setText(R.string.title_last_played); } else { sortMode = StatisticSortMode.LAST_PLAYED; setTitle(getString(R.string.title_last_played)); - sortButtonIcon.setImageResource( + headerBinding.sortButtonIcon.setImageResource( 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); } @@ -413,6 +406,9 @@ public class StatisticsPlaylistFragment StreamDialogEntry.share )); } + if (KoreUtil.shouldShowPlayWithKodi(context, infoItem.getServiceId())) { + entries.add(StreamDialogEntry.play_with_kodi); + } StreamDialogEntry.setEnabledEntries(entries); StreamDialogEntry.start_here_on_background.setCustomAction((fragment, infoItemDuplicate) -> diff --git a/app/src/main/java/org/schabi/newpipe/local/playlist/LocalPlaylistFragment.java b/app/src/main/java/org/schabi/newpipe/local/playlist/LocalPlaylistFragment.java index a8974e018..08b7101e6 100644 --- a/app/src/main/java/org/schabi/newpipe/local/playlist/LocalPlaylistFragment.java +++ b/app/src/main/java/org/schabi/newpipe/local/playlist/LocalPlaylistFragment.java @@ -14,7 +14,6 @@ import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.widget.EditText; -import android.widget.TextView; import android.widget.Toast; import androidx.annotation.NonNull; @@ -22,6 +21,7 @@ import androidx.annotation.Nullable; import androidx.appcompat.app.AlertDialog; import androidx.recyclerview.widget.ItemTouchHelper; import androidx.recyclerview.widget.RecyclerView; +import androidx.viewbinding.ViewBinding; import org.reactivestreams.Subscriber; 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.stream.model.StreamEntity; 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.StreamType; 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.SinglePlayQueue; import org.schabi.newpipe.report.UserAction; +import org.schabi.newpipe.util.KoreUtil; import org.schabi.newpipe.util.Localization; import org.schabi.newpipe.util.NavigationHelper; import org.schabi.newpipe.util.OnClickGesture; @@ -76,13 +79,8 @@ public class LocalPlaylistFragment extends BaseLocalListFragment createRenameDialog()); + headerBinding.playlistTitleView.setOnClickListener(view -> createRenameDialog()); itemTouchHelper = new ItemTouchHelper(getItemTouchCallback()); itemTouchHelper.attachToRecyclerView(itemsList); @@ -209,22 +200,18 @@ public class LocalPlaylistFragment extends BaseLocalListFragment + playlistControlBinding.playlistCtrlPlayAllButton.setOnClickListener(view -> NavigationHelper.playOnMainPlayer(activity, getPlayQueue())); - headerPopupButton.setOnClickListener(view -> + playlistControlBinding.playlistCtrlPlayPopupButton.setOnClickListener(view -> NavigationHelper.playOnPopupPlayer(activity, getPlayQueue(), false)); - headerBackgroundButton.setOnClickListener(view -> + playlistControlBinding.playlistCtrlPlayBgButton.setOnClickListener(view -> NavigationHelper.playOnBackgroundPlayer(activity, getPlayQueue(), false)); - headerPopupButton.setOnLongClickListener(view -> { + playlistControlBinding.playlistCtrlPlayPopupButton.setOnLongClickListener(view -> { NavigationHelper.enqueueOnPopupPlayer(activity, getPlayQueue(), true); return true; }); - headerBackgroundButton.setOnLongClickListener(view -> { + playlistControlBinding.playlistCtrlPlayBgButton.setOnLongClickListener(view -> { NavigationHelper.enqueueOnBackgroundPlayer(activity, getPlayQueue(), true); return true; }); @@ -781,6 +767,9 @@ public class LocalPlaylistFragment extends BaseLocalListFragment @@ -802,8 +791,9 @@ public class LocalPlaylistFragment extends BaseLocalListFragment() { + private var _binding: FragmentSubscriptionBinding? = null + private val binding get() = _binding!! + private lateinit var viewModel: SubscriptionViewModel private lateinit var subscriptionManager: SubscriptionManager private val disposables: CompositeDisposable = CompositeDisposable() @@ -129,7 +132,7 @@ class SubscriptionFragment : BaseStateFragment() { override fun onPause() { super.onPause() - itemsListState = items_list.layoutManager?.onSaveInstanceState() + itemsListState = binding.itemsList.layoutManager?.onSaveInstanceState() feedGroupsListState = feedGroupsCarousel?.onSaveInstanceState() importExportItemExpandedState = importExportItem.isExpanded @@ -169,7 +172,7 @@ class SubscriptionFragment : BaseStateFragment() { filters.addAction(IMPORT_COMPLETE_ACTION) subscriptionBroadcastReceiver = object : BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent) { - items_list?.post { + _binding?.itemsList?.post { importExportItem.isExpanded = false importExportItem.notifyChanged(FeedImportExportItem.REFRESH_EXPANDED_STATUS) } @@ -275,17 +278,18 @@ class SubscriptionFragment : BaseStateFragment() { override fun initViews(rootView: View, savedInstanceState: Bundle?) { super.initViews(rootView, savedInstanceState) + _binding = FragmentSubscriptionBinding.bind(rootView) val shouldUseGridLayout = shouldUseGridLayout() 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 } - items_list.adapter = groupAdapter + binding.itemsList.adapter = groupAdapter viewModel = ViewModelProvider(this).get(SubscriptionViewModel::class.java) - viewModel.stateLiveData.observe(viewLifecycleOwner, androidx.lifecycle.Observer { it?.let(this::handleResult) }) - viewModel.feedGroupsLiveData.observe(viewLifecycleOwner, androidx.lifecycle.Observer { it?.let(this::handleFeedGroups) }) + viewModel.stateLiveData.observe(viewLifecycleOwner, Observer { it?.let(this::handleResult) }) + viewModel.feedGroupsLiveData.observe(viewLifecycleOwner, Observer { it?.let(this::handleFeedGroups) }) } private fun showLongTapDialog(selectedItem: ChannelInfoItem) { @@ -301,13 +305,13 @@ class SubscriptionFragment : BaseStateFragment() { } } - val bannerView = View.inflate(requireContext(), R.layout.dialog_title, null) - bannerView.isSelected = true - bannerView.itemTitleView.text = selectedItem.name - bannerView.itemAdditionalDetails.visibility = View.GONE + val dialogTitleBinding = DialogTitleBinding.inflate(LayoutInflater.from(requireContext())) + dialogTitleBinding.root.isSelected = true + dialogTitleBinding.itemTitleView.text = selectedItem.name + dialogTitleBinding.itemAdditionalDetails.visibility = View.GONE AlertDialog.Builder(requireContext()) - .setCustomTitle(bannerView) + .setCustomTitle(dialogTitleBinding.root) .setItems(commands, actions) .create() .show() @@ -368,14 +372,14 @@ class SubscriptionFragment : BaseStateFragment() { subscriptionsSection.setHideWhenEmpty(false) if (result.subscriptions.isEmpty() && importExportItemExpandedState == null) { - items_list.post { + binding.itemsList.post { importExportItem.isExpanded = true importExportItem.notifyChanged(FeedImportExportItem.REFRESH_EXPANDED_STATUS) } } if (itemsListState != null) { - items_list.layoutManager?.onRestoreInstanceState(itemsListState) + binding.itemsList.layoutManager?.onRestoreInstanceState(itemsListState) itemsListState = null } } @@ -394,7 +398,7 @@ class SubscriptionFragment : BaseStateFragment() { } 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() { override fun showLoading() { super.showLoading() - animateView(items_list, false, 100) + animateView(binding.itemsList, false, 100) } override fun hideLoading() { super.hideLoading() - animateView(items_list, true, 200) + animateView(binding.itemsList, true, 200) } // ///////////////////////////////////////////////////////////////////////// diff --git a/app/src/main/java/org/schabi/newpipe/local/subscription/dialog/FeedGroupDialog.kt b/app/src/main/java/org/schabi/newpipe/local/subscription/dialog/FeedGroupDialog.kt index 1d5650a99..7c7ebdea9 100644 --- a/app/src/main/java/org/schabi/newpipe/local/subscription/dialog/FeedGroupDialog.kt +++ b/app/src/main/java/org/schabi/newpipe/local/subscription/dialog/FeedGroupDialog.kt @@ -24,10 +24,10 @@ import com.xwray.groupie.Section import com.xwray.groupie.kotlinandroidextensions.GroupieViewHolder import icepick.Icepick 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.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.local.subscription.FeedGroupIcon import org.schabi.newpipe.local.subscription.dialog.FeedGroupDialog.ScreenState.DeleteScreen @@ -45,6 +45,12 @@ import java.io.Serializable import kotlin.collections.contains 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 var groupId: Long = NO_GROUP_SELECTED private var groupIcon: FeedGroupIcon? = null @@ -107,14 +113,16 @@ class FeedGroupDialog : DialogFragment(), BackPressable { override fun onSaveInstanceState(outState: Bundle) { super.onSaveInstanceState(outState) - iconsListState = icon_selector.layoutManager?.onSaveInstanceState() - subscriptionsListState = subscriptions_selector_list.layoutManager?.onSaveInstanceState() + iconsListState = feedGroupCreateBinding.iconSelector.layoutManager?.onSaveInstanceState() + subscriptionsListState = feedGroupCreateBinding.subscriptionsSelectorList.layoutManager?.onSaveInstanceState() Icepick.saveInstanceState(this, outState) } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) + _feedGroupCreateBinding = DialogFeedGroupCreateBinding.bind(view) + _searchLayoutBinding = feedGroupCreateBinding.subscriptionsHeaderSearchContainer viewModel = ViewModelProvider( this, @@ -146,7 +154,7 @@ class FeedGroupDialog : DialogFragment(), BackPressable { add(subscriptionEmptyFooter) spanCount = 4 } - subscriptions_selector_list.apply { + feedGroupCreateBinding.subscriptionsSelectorList.apply { // Disable animations, too distracting. itemAnimator = null adapter = subscriptionGroupAdapter @@ -172,8 +180,11 @@ class FeedGroupDialog : DialogFragment(), BackPressable { override fun onDestroyView() { super.onDestroyView() - subscriptions_selector_list?.adapter = null - icon_selector?.adapter = null + feedGroupCreateBinding.subscriptionsSelectorList.adapter = null + feedGroupCreateBinding.iconSelector.adapter = null + + _feedGroupCreateBinding = null + _searchLayoutBinding = null } /*/​////////////////////////////////////////////////////////////////////////// @@ -193,30 +204,30 @@ class FeedGroupDialog : DialogFragment(), BackPressable { } private fun setupListeners() { - delete_button.setOnClickListener { showScreen(DeleteScreen) } + feedGroupCreateBinding.deleteButton.setOnClickListener { showScreen(DeleteScreen) } - cancel_button.setOnClickListener { + feedGroupCreateBinding.cancelButton.setOnClickListener { when (currentScreen) { InitialScreen -> dismiss() else -> showScreen(InitialScreen) } } - group_name_input_container.error = null - group_name_input.doOnTextChanged { text, _, _, _ -> - if (group_name_input_container.isErrorEnabled && !text.isNullOrBlank()) { - group_name_input_container.error = null + feedGroupCreateBinding.groupNameInputContainer.error = null + feedGroupCreateBinding.groupNameInput.doOnTextChanged { text, _, _, _ -> + if (feedGroupCreateBinding.groupNameInputContainer.isErrorEnabled && !text.isNullOrBlank()) { + feedGroupCreateBinding.groupNameInputContainer.error = null } } - confirm_button.setOnClickListener { handlePositiveButton() } + feedGroupCreateBinding.confirmButton.setOnClickListener { handlePositiveButton() } - select_channel_button.setOnClickListener { - subscriptions_selector_list.scrollToPosition(0) + feedGroupCreateBinding.selectChannelButton.setOnClickListener { + feedGroupCreateBinding.subscriptionsSelectorList.scrollToPosition(0) showScreen(SubscriptionsPickerScreen) } - val headerMenu = subscriptions_header_toolbar.menu + val headerMenu = feedGroupCreateBinding.subscriptionsHeaderToolbar.menu requireActivity().menuInflater.inflate(R.menu.menu_feed_group_dialog, headerMenu) headerMenu.findItem(R.id.action_search).setOnMenuItemClickListener { @@ -234,8 +245,8 @@ class FeedGroupDialog : DialogFragment(), BackPressable { } } - toolbar_search_clear.setOnClickListener { - if (toolbar_search_edit_text.text.isEmpty()) { + searchLayoutBinding.toolbarSearchClear.setOnClickListener { + if (searchLayoutBinding.toolbarSearchEditText.text.isNullOrEmpty()) { hideSearch() return@setOnClickListener } @@ -243,14 +254,14 @@ class FeedGroupDialog : DialogFragment(), BackPressable { showKeyboardSearch() } - toolbar_search_edit_text.setOnClickListener { + searchLayoutBinding.toolbarSearchEditText.setOnClickListener { if (DeviceUtils.isTv(context)) { showKeyboardSearch() } } - toolbar_search_edit_text.doOnTextChanged { _, _, _, _ -> - val newQuery: String = toolbar_search_edit_text.text.toString() + searchLayoutBinding.toolbarSearchEditText.doOnTextChanged { _, _, _, _ -> + val newQuery: String = searchLayoutBinding.toolbarSearchEditText.text.toString() subscriptionsCurrentSearchQuery = newQuery viewModel.filterSubscriptionsBy(newQuery) } @@ -266,16 +277,16 @@ class FeedGroupDialog : DialogFragment(), BackPressable { } private fun handlePositiveButtonInitialScreen() { - val name = group_name_input.text.toString().trim() + val name = feedGroupCreateBinding.groupNameInput.text.toString().trim() val icon = selectedIcon ?: groupIcon ?: FeedGroupIcon.ALL if (name.isBlank()) { - group_name_input_container.error = getString(R.string.feed_group_dialog_empty_name) - group_name_input.text = null - group_name_input.requestFocus() + feedGroupCreateBinding.groupNameInputContainer.error = getString(R.string.feed_group_dialog_empty_name) + feedGroupCreateBinding.groupNameInput.text = null + feedGroupCreateBinding.groupNameInput.requestFocus() return } else { - group_name_input_container.error = null + feedGroupCreateBinding.groupNameInputContainer.error = null } if (selectedSubscriptions.isEmpty()) { @@ -296,10 +307,10 @@ class FeedGroupDialog : DialogFragment(), BackPressable { groupSortOrder = feedGroupEntity?.sortOrder ?: -1 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()) { - group_name_input.setText(name) + if (feedGroupCreateBinding.groupNameInput.text.isNullOrBlank()) { + feedGroupCreateBinding.groupNameInput.setText(name) } } @@ -346,10 +357,10 @@ class FeedGroupDialog : DialogFragment(), BackPressable { subscriptionMainSection.update(subscriptions, false) if (subscriptionsListState != null) { - subscriptions_selector_list.layoutManager?.onRestoreInstanceState(subscriptionsListState) + feedGroupCreateBinding.subscriptionsSelectorList.layoutManager?.onRestoreInstanceState(subscriptionsListState) subscriptionsListState = null } 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, selectedCount, selectedCount ) - selected_subscription_count_view.text = selectedCountText - subscriptions_header_info.text = selectedCountText + feedGroupCreateBinding.selectedSubscriptionCountView.text = selectedCountText + feedGroupCreateBinding.subscriptionsHeaderInfo.text = selectedCountText } private fun setupIconPicker() { val groupAdapter = GroupAdapter() groupAdapter.addAll(FeedGroupIcon.values().map { PickerIconItem(requireContext(), it) }) - icon_selector.apply { + feedGroupCreateBinding.iconSelector.apply { layoutManager = GridLayoutManager(requireContext(), 7, RecyclerView.VERTICAL, false) adapter = groupAdapter @@ -381,20 +392,20 @@ class FeedGroupDialog : DialogFragment(), BackPressable { when (item) { is PickerIconItem -> { selectedIcon = item.icon - icon_preview.setImageResource(item.iconRes) + feedGroupCreateBinding.iconPreview.setImageResource(item.iconRes) showScreen(InitialScreen) } } } - icon_preview.setOnClickListener { - icon_selector.scrollToPosition(0) + feedGroupCreateBinding.iconPreview.setOnClickListener { + feedGroupCreateBinding.iconSelector.scrollToPosition(0) showScreen(IconPickerScreen) } if (groupId == NO_GROUP_SELECTED) { 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) { currentScreen = screen - options_root.onlyVisibleIn(InitialScreen) - icon_selector.onlyVisibleIn(IconPickerScreen) - subscriptions_selector.onlyVisibleIn(SubscriptionsPickerScreen) - delete_screen_message.onlyVisibleIn(DeleteScreen) + feedGroupCreateBinding.optionsRoot.onlyVisibleIn(InitialScreen) + feedGroupCreateBinding.iconSelector.onlyVisibleIn(IconPickerScreen) + feedGroupCreateBinding.subscriptionsSelector.onlyVisibleIn(SubscriptionsPickerScreen) + feedGroupCreateBinding.deleteScreenMessage.onlyVisibleIn(DeleteScreen) - separator.onlyVisibleIn(SubscriptionsPickerScreen, IconPickerScreen) - cancel_button.onlyVisibleIn(InitialScreen, DeleteScreen) + feedGroupCreateBinding.separator.onlyVisibleIn(SubscriptionsPickerScreen, IconPickerScreen) + feedGroupCreateBinding.cancelButton.onlyVisibleIn(InitialScreen, DeleteScreen) - confirm_button.setText( + feedGroupCreateBinding.confirmButton.setText( when { currentScreen == InitialScreen && groupId == NO_GROUP_SELECTED -> R.string.create else -> android.R.string.ok } ) - delete_button.isGone = currentScreen != InitialScreen || groupId == NO_GROUP_SELECTED + feedGroupCreateBinding.deleteButton.isGone = currentScreen != InitialScreen || groupId == NO_GROUP_SELECTED hideKeyboard() hideSearch() @@ -434,26 +445,26 @@ class FeedGroupDialog : DialogFragment(), BackPressable { // Utils //​//////////////////////////////////////////////////////////////////////// */ - private fun isSearchVisible() = subscriptions_header_search_container?.visibility == View.VISIBLE + private fun isSearchVisible() = _searchLayoutBinding?.root?.visibility == View.VISIBLE private fun resetSearch() { - toolbar_search_edit_text.setText("") + searchLayoutBinding.toolbarSearchEditText.setText("") subscriptionsCurrentSearchQuery = "" viewModel.clearSubscriptionsFilter() } private fun hideSearch() { resetSearch() - subscriptions_header_search_container.visibility = View.GONE - subscriptions_header_info_container.visibility = View.VISIBLE - subscriptions_header_toolbar.menu.findItem(R.id.action_search).isVisible = true + searchLayoutBinding.root.visibility = View.GONE + feedGroupCreateBinding.subscriptionsHeaderInfoContainer.visibility = View.VISIBLE + feedGroupCreateBinding.subscriptionsHeaderToolbar.menu.findItem(R.id.action_search).isVisible = true hideKeyboardSearch() } private fun showSearch() { - subscriptions_header_search_container.visibility = View.VISIBLE - subscriptions_header_info_container.visibility = View.GONE - subscriptions_header_toolbar.menu.findItem(R.id.action_search).isVisible = false + searchLayoutBinding.root.visibility = View.VISIBLE + feedGroupCreateBinding.subscriptionsHeaderInfoContainer.visibility = View.GONE + feedGroupCreateBinding.subscriptionsHeaderToolbar.menu.findItem(R.id.action_search).isVisible = false showKeyboardSearch() } @@ -462,37 +473,43 @@ class FeedGroupDialog : DialogFragment(), BackPressable { } private fun showKeyboardSearch() { - if (toolbar_search_edit_text.requestFocus()) { - inputMethodManager.showSoftInput(toolbar_search_edit_text, InputMethodManager.SHOW_IMPLICIT) + if (searchLayoutBinding.toolbarSearchEditText.requestFocus()) { + inputMethodManager.showSoftInput( + searchLayoutBinding.toolbarSearchEditText, + InputMethodManager.SHOW_IMPLICIT + ) } } private fun hideKeyboardSearch() { inputMethodManager.hideSoftInputFromWindow( - toolbar_search_edit_text.windowToken, + searchLayoutBinding.toolbarSearchEditText.windowToken, InputMethodManager.RESULT_UNCHANGED_SHOWN ) - toolbar_search_edit_text.clearFocus() + searchLayoutBinding.toolbarSearchEditText.clearFocus() } private fun showKeyboard() { - if (group_name_input.requestFocus()) { - inputMethodManager.showSoftInput(group_name_input, InputMethodManager.SHOW_IMPLICIT) + if (feedGroupCreateBinding.groupNameInput.requestFocus()) { + inputMethodManager.showSoftInput( + feedGroupCreateBinding.groupNameInput, + InputMethodManager.SHOW_IMPLICIT + ) } } private fun hideKeyboard() { inputMethodManager.hideSoftInputFromWindow( - group_name_input.windowToken, + feedGroupCreateBinding.groupNameInput.windowToken, InputMethodManager.RESULT_UNCHANGED_SHOWN ) - group_name_input.clearFocus() + feedGroupCreateBinding.groupNameInput.clearFocus() } private fun disableInput() { - delete_button?.isEnabled = false - confirm_button?.isEnabled = false - cancel_button?.isEnabled = false + _feedGroupCreateBinding?.deleteButton?.isEnabled = false + _feedGroupCreateBinding?.confirmButton?.isEnabled = false + _feedGroupCreateBinding?.cancelButton?.isEnabled = false isCancelable = false hideKeyboard() diff --git a/app/src/main/java/org/schabi/newpipe/local/subscription/dialog/FeedGroupReorderDialog.kt b/app/src/main/java/org/schabi/newpipe/local/subscription/dialog/FeedGroupReorderDialog.kt index 380bf13f5..3b74ddc74 100644 --- a/app/src/main/java/org/schabi/newpipe/local/subscription/dialog/FeedGroupReorderDialog.kt +++ b/app/src/main/java/org/schabi/newpipe/local/subscription/dialog/FeedGroupReorderDialog.kt @@ -16,10 +16,9 @@ import com.xwray.groupie.TouchCallback import com.xwray.groupie.kotlinandroidextensions.GroupieViewHolder import icepick.Icepick 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.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.SuccessEvent import org.schabi.newpipe.local.subscription.item.FeedGroupReorderItem @@ -27,6 +26,9 @@ import org.schabi.newpipe.util.ThemeHelper import java.util.Collections class FeedGroupReorderDialog : DialogFragment() { + private var _binding: DialogFeedGroupReorderBinding? = null + private val binding get() = _binding!! + private lateinit var viewModel: FeedGroupReorderDialogViewModel @State @@ -48,6 +50,7 @@ class FeedGroupReorderDialog : DialogFragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) + _binding = DialogFeedGroupReorderBinding.bind(view) viewModel = ViewModelProvider(this).get(FeedGroupReorderDialogViewModel::class.java) viewModel.groupsLiveData.observe(viewLifecycleOwner, Observer(::handleGroups)) @@ -61,15 +64,20 @@ class FeedGroupReorderDialog : DialogFragment() { } ) - feed_groups_list.layoutManager = LinearLayoutManager(requireContext()) - feed_groups_list.adapter = groupAdapter - itemTouchHelper.attachToRecyclerView(feed_groups_list) + binding.feedGroupsList.layoutManager = LinearLayoutManager(requireContext()) + binding.feedGroupsList.adapter = groupAdapter + itemTouchHelper.attachToRecyclerView(binding.feedGroupsList) - confirm_button.setOnClickListener { + binding.confirmButton.setOnClickListener { viewModel.updateOrder(groupOrderedIdList) } } + override fun onDestroyView() { + _binding = null + super.onDestroyView() + } + override fun onSaveInstanceState(outState: Bundle) { super.onSaveInstanceState(outState) Icepick.saveInstanceState(this, outState) @@ -89,7 +97,7 @@ class FeedGroupReorderDialog : DialogFragment() { } private fun disableInput() { - confirm_button?.isEnabled = false + _binding?.confirmButton?.isEnabled = false isCancelable = false } diff --git a/app/src/main/java/org/schabi/newpipe/local/subscription/services/BaseImportExportService.java b/app/src/main/java/org/schabi/newpipe/local/subscription/services/BaseImportExportService.java index 73c0d23a0..f573f4679 100644 --- a/app/src/main/java/org/schabi/newpipe/local/subscription/services/BaseImportExportService.java +++ b/app/src/main/java/org/schabi/newpipe/local/subscription/services/BaseImportExportService.java @@ -31,15 +31,16 @@ import androidx.annotation.Nullable; import androidx.annotation.StringRes; import androidx.core.app.NotificationCompat; import androidx.core.app.NotificationManagerCompat; +import androidx.core.app.ServiceCompat; import org.reactivestreams.Publisher; import org.schabi.newpipe.R; import org.schabi.newpipe.extractor.subscription.SubscriptionExtractor; +import org.schabi.newpipe.ktx.ExceptionUtils; import org.schabi.newpipe.local.subscription.SubscriptionManager; import org.schabi.newpipe.report.ErrorActivity; import org.schabi.newpipe.report.ErrorInfo; import org.schabi.newpipe.report.UserAction; -import org.schabi.newpipe.util.ExceptionUtils; import java.io.FileNotFoundException; import java.util.Collections; @@ -162,7 +163,7 @@ public abstract class BaseImportExportService extends Service { protected void postErrorResult(final String title, final String text) { disposeAll(); - stopForeground(true); + ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE); stopSelf(); if (title == null) { diff --git a/app/src/main/java/org/schabi/newpipe/local/subscription/services/ImportExportEventListener.java b/app/src/main/java/org/schabi/newpipe/local/subscription/services/ImportExportEventListener.java index 34bd68f5e..7352d1f12 100644 --- a/app/src/main/java/org/schabi/newpipe/local/subscription/services/ImportExportEventListener.java +++ b/app/src/main/java/org/schabi/newpipe/local/subscription/services/ImportExportEventListener.java @@ -9,7 +9,7 @@ public interface ImportExportEventListener { 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 */ diff --git a/app/src/main/java/org/schabi/newpipe/local/subscription/services/SubscriptionsExportService.java b/app/src/main/java/org/schabi/newpipe/local/subscription/services/SubscriptionsExportService.java index 982701d1f..5dfb1bfe5 100644 --- a/app/src/main/java/org/schabi/newpipe/local/subscription/services/SubscriptionsExportService.java +++ b/app/src/main/java/org/schabi/newpipe/local/subscription/services/SubscriptionsExportService.java @@ -27,6 +27,7 @@ import androidx.localbroadcastmanager.content.LocalBroadcastManager; import org.reactivestreams.Subscriber; import org.reactivestreams.Subscription; +import org.schabi.newpipe.App; import org.schabi.newpipe.R; import org.schabi.newpipe.database.subscription.SubscriptionEntity; 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 * 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"; private Subscription subscription; diff --git a/app/src/main/java/org/schabi/newpipe/local/subscription/services/SubscriptionsImportService.java b/app/src/main/java/org/schabi/newpipe/local/subscription/services/SubscriptionsImportService.java index b1c67719c..af94934b2 100644 --- a/app/src/main/java/org/schabi/newpipe/local/subscription/services/SubscriptionsImportService.java +++ b/app/src/main/java/org/schabi/newpipe/local/subscription/services/SubscriptionsImportService.java @@ -29,13 +29,14 @@ import androidx.localbroadcastmanager.content.LocalBroadcastManager; import org.reactivestreams.Subscriber; import org.reactivestreams.Subscription; +import org.schabi.newpipe.App; import org.schabi.newpipe.R; import org.schabi.newpipe.database.subscription.SubscriptionEntity; import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipe.extractor.channel.ChannelInfo; import org.schabi.newpipe.extractor.subscription.SubscriptionItem; +import org.schabi.newpipe.ktx.ExceptionUtils; import org.schabi.newpipe.util.Constants; -import org.schabi.newpipe.util.ExceptionUtils; import org.schabi.newpipe.util.ExtractorHelper; import java.io.File; @@ -66,7 +67,7 @@ public class SubscriptionsImportService extends BaseImportExportService { * A {@link LocalBroadcastManager local broadcast} will be made with this action * 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"; /** diff --git a/app/src/main/java/org/schabi/newpipe/player/BackgroundPlayerActivity.java b/app/src/main/java/org/schabi/newpipe/player/BackgroundPlayerActivity.java deleted file mode 100644 index 2fc710fb0..000000000 --- a/app/src/main/java/org/schabi/newpipe/player/BackgroundPlayerActivity.java +++ /dev/null @@ -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()); - } -} diff --git a/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java b/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java deleted file mode 100644 index ac205d2d4..000000000 --- a/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java +++ /dev/null @@ -1,1628 +0,0 @@ -/* - * Copyright 2017 Mauricio Colli - * BasePlayer.java is part of NewPipe - * - * License: GPL-3.0+ - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package org.schabi.newpipe.player; - -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; -import android.content.SharedPreferences; -import android.graphics.Bitmap; -import android.graphics.BitmapFactory; -import android.media.AudioManager; -import android.util.Log; -import android.view.View; -import android.widget.Toast; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.preference.PreferenceManager; - -import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.DefaultRenderersFactory; -import com.google.android.exoplayer2.ExoPlaybackException; -import com.google.android.exoplayer2.LoadControl; -import com.google.android.exoplayer2.PlaybackParameters; -import com.google.android.exoplayer2.Player; -import com.google.android.exoplayer2.RenderersFactory; -import com.google.android.exoplayer2.SimpleExoPlayer; -import com.google.android.exoplayer2.Timeline; -import com.google.android.exoplayer2.source.BehindLiveWindowException; -import com.google.android.exoplayer2.source.MediaSource; -import com.google.android.exoplayer2.source.TrackGroupArray; -import com.google.android.exoplayer2.trackselection.TrackSelection; -import com.google.android.exoplayer2.trackselection.TrackSelectionArray; -import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter; -import com.nostra13.universalimageloader.core.ImageLoader; -import com.nostra13.universalimageloader.core.assist.FailReason; -import com.nostra13.universalimageloader.core.listener.ImageLoadingListener; - -import org.schabi.newpipe.DownloaderImpl; -import org.schabi.newpipe.MainActivity; -import org.schabi.newpipe.R; -import org.schabi.newpipe.extractor.stream.StreamInfo; -import org.schabi.newpipe.local.history.HistoryRecordManager; -import org.schabi.newpipe.player.helper.AudioReactor; -import org.schabi.newpipe.player.helper.LoadController; -import org.schabi.newpipe.player.helper.MediaSessionManager; -import org.schabi.newpipe.player.helper.PlayerDataSource; -import org.schabi.newpipe.player.helper.PlayerHelper; -import org.schabi.newpipe.player.playback.BasePlayerMediaSession; -import org.schabi.newpipe.player.playback.CustomTrackSelector; -import org.schabi.newpipe.player.playback.MediaSourceManager; -import org.schabi.newpipe.player.playback.PlaybackListener; -import org.schabi.newpipe.player.playqueue.PlayQueue; -import org.schabi.newpipe.player.playqueue.PlayQueueAdapter; -import org.schabi.newpipe.player.playqueue.PlayQueueItem; -import org.schabi.newpipe.player.resolver.MediaSourceTag; -import org.schabi.newpipe.util.ImageDisplayConstants; -import org.schabi.newpipe.util.SerializedCache; - -import java.io.IOException; - -import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; -import io.reactivex.rxjava3.core.Observable; -import io.reactivex.rxjava3.disposables.CompositeDisposable; -import io.reactivex.rxjava3.disposables.Disposable; -import io.reactivex.rxjava3.disposables.SerialDisposable; - -import static com.google.android.exoplayer2.Player.DISCONTINUITY_REASON_INTERNAL; -import static com.google.android.exoplayer2.Player.DISCONTINUITY_REASON_PERIOD_TRANSITION; -import static com.google.android.exoplayer2.Player.DISCONTINUITY_REASON_SEEK; -import static com.google.android.exoplayer2.Player.DISCONTINUITY_REASON_SEEK_ADJUSTMENT; -import static java.util.concurrent.TimeUnit.MILLISECONDS; - -/** - * Base for the players, joining the common properties. - * - * @author mauriciocolli - */ -@SuppressWarnings({"WeakerAccess"}) -public abstract class BasePlayer implements - Player.EventListener, PlaybackListener, ImageLoadingListener { - public static final boolean DEBUG = MainActivity.DEBUG; - @NonNull - public static final String TAG = "BasePlayer"; - - public static final int STATE_PREFLIGHT = -1; - public static final int STATE_BLOCKED = 123; - public static final int STATE_PLAYING = 124; - public static final int STATE_BUFFERING = 125; - public static final int STATE_PAUSED = 126; - public static final int STATE_PAUSED_SEEK = 127; - public static final int STATE_COMPLETED = 128; - - /*////////////////////////////////////////////////////////////////////////// - // Intent - //////////////////////////////////////////////////////////////////////////*/ - - @NonNull - public static final String REPEAT_MODE = "repeat_mode"; - @NonNull - public static final String PLAYBACK_QUALITY = "playback_quality"; - @NonNull - public static final String PLAY_QUEUE_KEY = "play_queue_key"; - @NonNull - public static final String APPEND_ONLY = "append_only"; - @NonNull - public static final String RESUME_PLAYBACK = "resume_playback"; - @NonNull - public static final String PLAY_WHEN_READY = "play_when_ready"; - @NonNull - public static final String SELECT_ON_APPEND = "select_on_append"; - @NonNull - public static final String PLAYER_TYPE = "player_type"; - @NonNull - public static final String IS_MUTED = "is_muted"; - - /*////////////////////////////////////////////////////////////////////////// - // Playback - //////////////////////////////////////////////////////////////////////////*/ - - protected static final float[] PLAYBACK_SPEEDS = {0.5f, 0.75f, 1.0f, 1.25f, 1.5f, 1.75f, 2.0f}; - - protected PlayQueue playQueue; - protected PlayQueueAdapter playQueueAdapter; - - @Nullable - protected MediaSourceManager playbackManager; - - @Nullable - private PlayQueueItem currentItem; - @Nullable - private MediaSourceTag currentMetadata; - @Nullable - private Bitmap currentThumbnail; - - @Nullable - protected Toast errorToast; - - /*////////////////////////////////////////////////////////////////////////// - // Player - //////////////////////////////////////////////////////////////////////////*/ - - protected static final int PLAY_PREV_ACTIVATION_LIMIT_MILLIS = 5000; // 5 seconds - protected static final int PROGRESS_LOOP_INTERVAL_MILLIS = 500; - - public static final int PLAYER_TYPE_VIDEO = 0; - public static final int PLAYER_TYPE_AUDIO = 1; - public static final int PLAYER_TYPE_POPUP = 2; - - protected SimpleExoPlayer simpleExoPlayer; - protected AudioReactor audioReactor; - protected MediaSessionManager mediaSessionManager; - - - @NonNull - protected final Context context; - @NonNull - protected final BroadcastReceiver broadcastReceiver; - @NonNull - protected final IntentFilter intentFilter; - @NonNull - protected final HistoryRecordManager recordManager; - @NonNull - protected final SharedPreferences sharedPreferences; - @NonNull - protected final CustomTrackSelector trackSelector; - @NonNull - protected final PlayerDataSource dataSource; - @NonNull - private final LoadControl loadControl; - - @NonNull - private final RenderersFactory renderFactory; - @NonNull - private final SerialDisposable progressUpdateReactor; - @NonNull - private final CompositeDisposable databaseUpdateReactor; - - private boolean isPrepared = false; - private Disposable stateLoader; - - protected int currentState = STATE_PREFLIGHT; - - public BasePlayer(@NonNull final Context context) { - this.context = context; - - this.broadcastReceiver = new BroadcastReceiver() { - @Override - public void onReceive(final Context ctx, final Intent intent) { - onBroadcastReceived(intent); - } - }; - this.intentFilter = new IntentFilter(); - setupBroadcastReceiver(intentFilter); - - this.recordManager = new HistoryRecordManager(context); - this.sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context); - - this.progressUpdateReactor = new SerialDisposable(); - this.databaseUpdateReactor = new CompositeDisposable(); - - final String userAgent = DownloaderImpl.USER_AGENT; - final DefaultBandwidthMeter bandwidthMeter = new DefaultBandwidthMeter.Builder(context) - .build(); - this.dataSource = new PlayerDataSource(context, userAgent, bandwidthMeter); - - final TrackSelection.Factory trackSelectionFactory = PlayerHelper - .getQualitySelector(); - this.trackSelector = new CustomTrackSelector(context, trackSelectionFactory); - - this.loadControl = new LoadController(); - this.renderFactory = new DefaultRenderersFactory(context); - } - - public void setup() { - if (simpleExoPlayer == null) { - initPlayer(true); - } - initListeners(); - } - - public void initPlayer(final boolean playOnReady) { - if (DEBUG) { - Log.d(TAG, "initPlayer() called with: playOnReady = [" + playOnReady + "]"); - } - - simpleExoPlayer = new SimpleExoPlayer.Builder(context, renderFactory) - .setTrackSelector(trackSelector) - .setLoadControl(loadControl) - .build(); - simpleExoPlayer.addListener(this); - simpleExoPlayer.setPlayWhenReady(playOnReady); - simpleExoPlayer.setSeekParameters(PlayerHelper.getSeekParameters(context)); - simpleExoPlayer.setWakeMode(C.WAKE_MODE_NETWORK); - simpleExoPlayer.setHandleAudioBecomingNoisy(true); - - audioReactor = new AudioReactor(context, simpleExoPlayer); - mediaSessionManager = new MediaSessionManager(context, simpleExoPlayer, - new BasePlayerMediaSession(this)); - - registerBroadcastReceiver(); - } - - public void initListeners() { - } - - public void handleIntent(final Intent intent) { - if (DEBUG) { - Log.d(TAG, "handleIntent() called with: intent = [" + intent + "]"); - } - if (intent == null) { - return; - } - - // Resolve play queue - if (!intent.hasExtra(PLAY_QUEUE_KEY)) { - return; - } - final String intentCacheKey = intent.getStringExtra(PLAY_QUEUE_KEY); - final PlayQueue queue = SerializedCache.getInstance().take(intentCacheKey, PlayQueue.class); - if (queue == null) { - return; - } - - // Resolve append intents - if (intent.getBooleanExtra(APPEND_ONLY, false) && playQueue != null) { - final int sizeBeforeAppend = playQueue.size(); - playQueue.append(queue.getStreams()); - - if ((intent.getBooleanExtra(SELECT_ON_APPEND, false) - || getCurrentState() == STATE_COMPLETED) && queue.getStreams().size() > 0) { - playQueue.setIndex(sizeBeforeAppend); - } - - return; - } - - final PlaybackParameters savedParameters = retrievePlaybackParametersFromPreferences(); - final float playbackSpeed = savedParameters.speed; - final float playbackPitch = savedParameters.pitch; - final boolean playbackSkipSilence = savedParameters.skipSilence; - - final boolean samePlayQueue = playQueue != null && playQueue.equals(queue); - - final int repeatMode = intent.getIntExtra(REPEAT_MODE, getRepeatMode()); - final boolean playWhenReady = intent.getBooleanExtra(PLAY_WHEN_READY, true); - final boolean isMuted = intent - .getBooleanExtra(IS_MUTED, simpleExoPlayer != null && isMuted()); - - /* - * There are 3 situations when playback shouldn't be started from scratch (zero timestamp): - * 1. User pressed on a timestamp link and the same video should be rewound to the timestamp - * 2. User changed a player from, for example. main to popup, or from audio to main, etc - * 3. User chose to resume a video based on a saved timestamp from history of played videos - * In those cases time will be saved because re-init of the play queue is a not an instant - * task and requires network calls - * */ - // seek to timestamp if stream is already playing - if (simpleExoPlayer != null - && queue.size() == 1 - && playQueue != null - && playQueue.size() == 1 - && playQueue.getItem() != null - && queue.getItem().getUrl().equals(playQueue.getItem().getUrl()) - && queue.getItem().getRecoveryPosition() != PlayQueueItem.RECOVERY_UNSET) { - // Player can have state = IDLE when playback is stopped or failed - // and we should retry() in this case - if (simpleExoPlayer.getPlaybackState() == Player.STATE_IDLE) { - simpleExoPlayer.retry(); - } - simpleExoPlayer.seekTo(playQueue.getIndex(), queue.getItem().getRecoveryPosition()); - simpleExoPlayer.setPlayWhenReady(playWhenReady); - - } else if (simpleExoPlayer != null - && samePlayQueue - && playQueue != null - && !playQueue.isDisposed()) { - // Do not re-init the same PlayQueue. Save time - // Player can have state = IDLE when playback is stopped or failed - // and we should retry() in this case - if (simpleExoPlayer.getPlaybackState() == Player.STATE_IDLE) { - simpleExoPlayer.retry(); - } - simpleExoPlayer.setPlayWhenReady(playWhenReady); - - } else if (intent.getBooleanExtra(RESUME_PLAYBACK, false) - && isPlaybackResumeEnabled() - && !samePlayQueue) { - final PlayQueueItem item = queue.getItem(); - if (item != null && item.getRecoveryPosition() == PlayQueueItem.RECOVERY_UNSET) { - stateLoader = recordManager.loadStreamState(item) - .observeOn(AndroidSchedulers.mainThread()) - // Do not place initPlayback() in doFinally() because - // it restarts playback after destroy() - //.doFinally() - .subscribe( - state -> { - queue.setRecovery(queue.getIndex(), state.getProgressTime()); - initPlayback(queue, repeatMode, playbackSpeed, playbackPitch, - playbackSkipSilence, playWhenReady, isMuted); - }, - error -> { - if (DEBUG) { - error.printStackTrace(); - } - // In case any error we can start playback without history - initPlayback(queue, repeatMode, playbackSpeed, playbackPitch, - playbackSkipSilence, playWhenReady, isMuted); - }, - () -> { - // Completed but not found in history - initPlayback(queue, repeatMode, playbackSpeed, playbackPitch, - playbackSkipSilence, playWhenReady, isMuted); - } - ); - databaseUpdateReactor.add(stateLoader); - } - } else { - // Good to go... - // In a case of equal PlayQueues we can re-init old one but only when it is disposed - initPlayback(samePlayQueue ? playQueue : queue, repeatMode, playbackSpeed, - playbackPitch, playbackSkipSilence, playWhenReady, isMuted); - } - } - - private PlaybackParameters retrievePlaybackParametersFromPreferences() { - final SharedPreferences preferences = - PreferenceManager.getDefaultSharedPreferences(context); - - final float speed = preferences.getFloat( - context.getString(R.string.playback_speed_key), getPlaybackSpeed()); - final float pitch = preferences.getFloat( - context.getString(R.string.playback_pitch_key), getPlaybackPitch()); - final boolean skipSilence = preferences.getBoolean( - context.getString(R.string.playback_skip_silence_key), getPlaybackSkipSilence()); - return new PlaybackParameters(speed, pitch, skipSilence); - } - - protected void initPlayback(@NonNull final PlayQueue queue, - @Player.RepeatMode final int repeatMode, - final float playbackSpeed, - final float playbackPitch, - final boolean playbackSkipSilence, - final boolean playOnReady, - final boolean isMuted) { - destroyPlayer(); - initPlayer(playOnReady); - setRepeatMode(repeatMode); - setPlaybackParameters(playbackSpeed, playbackPitch, playbackSkipSilence); - - playQueue = queue; - playQueue.init(); - if (playbackManager != null) { - playbackManager.dispose(); - } - playbackManager = new MediaSourceManager(this, playQueue); - - if (playQueueAdapter != null) { - playQueueAdapter.dispose(); - } - playQueueAdapter = new PlayQueueAdapter(context, playQueue); - - simpleExoPlayer.setVolume(isMuted ? 0 : 1); - } - - public void destroyPlayer() { - if (DEBUG) { - Log.d(TAG, "destroyPlayer() called"); - } - if (simpleExoPlayer != null) { - simpleExoPlayer.removeListener(this); - simpleExoPlayer.stop(); - simpleExoPlayer.release(); - } - if (isProgressLoopRunning()) { - stopProgressLoop(); - } - if (playQueue != null) { - playQueue.dispose(); - } - if (audioReactor != null) { - audioReactor.dispose(); - } - if (playbackManager != null) { - playbackManager.dispose(); - } - if (mediaSessionManager != null) { - mediaSessionManager.dispose(); - } - if (stateLoader != null) { - stateLoader.dispose(); - } - - if (playQueueAdapter != null) { - playQueueAdapter.unsetSelectedListener(); - playQueueAdapter.dispose(); - } - } - - public void destroy() { - if (DEBUG) { - Log.d(TAG, "destroy() called"); - } - destroyPlayer(); - unregisterBroadcastReceiver(); - - databaseUpdateReactor.clear(); - progressUpdateReactor.set(null); - ImageLoader.getInstance().stop(); - } - - /*////////////////////////////////////////////////////////////////////////// - // Thumbnail Loading - //////////////////////////////////////////////////////////////////////////*/ - - private void initThumbnail(final String url) { - if (DEBUG) { - Log.d(TAG, "Thumbnail - initThumbnail() called"); - } - if (url == null || url.isEmpty()) { - return; - } - ImageLoader.getInstance().resume(); - ImageLoader.getInstance() - .loadImage(url, ImageDisplayConstants.DISPLAY_THUMBNAIL_OPTIONS, this); - } - - @Override - public void onLoadingStarted(final String imageUri, final View view) { - if (DEBUG) { - Log.d(TAG, "Thumbnail - onLoadingStarted() called on: " - + "imageUri = [" + imageUri + "], view = [" + view + "]"); - } - } - - @Override - public void onLoadingFailed(final String imageUri, final View view, - final FailReason failReason) { - Log.e(TAG, "Thumbnail - onLoadingFailed() called on imageUri = [" + imageUri + "]", - failReason.getCause()); - currentThumbnail = null; - } - - @Override - public void onLoadingComplete(final String imageUri, final View view, - final Bitmap loadedImage) { - final float width = Math.min( - context.getResources().getDimension(R.dimen.player_notification_thumbnail_width), - loadedImage.getWidth()); - currentThumbnail = Bitmap.createScaledBitmap(loadedImage, - (int) width, - (int) (loadedImage.getHeight() / (loadedImage.getWidth() / width)), true); - if (DEBUG) { - Log.d(TAG, "Thumbnail - onLoadingComplete() called with: " - + "imageUri = [" + imageUri + "], view = [" + view + "], " - + "loadedImage = [" + loadedImage + "], " - + loadedImage.getWidth() + "x" + loadedImage.getHeight() - + ", scaled width = " + width); - } - } - - @Override - public void onLoadingCancelled(final String imageUri, final View view) { - if (DEBUG) { - Log.d(TAG, "Thumbnail - onLoadingCancelled() called with: " - + "imageUri = [" + imageUri + "], view = [" + view + "]"); - } - currentThumbnail = null; - } - - /*////////////////////////////////////////////////////////////////////////// - // Broadcast Receiver - //////////////////////////////////////////////////////////////////////////*/ - - /** - * Add your action in the intentFilter. - * - * @param intentFltr intent filter that will be used for register the receiver - */ - protected void setupBroadcastReceiver(final IntentFilter intentFltr) { - intentFltr.addAction(AudioManager.ACTION_AUDIO_BECOMING_NOISY); - } - - public void onBroadcastReceived(final Intent intent) { - if (intent == null || intent.getAction() == null) { - return; - } - switch (intent.getAction()) { - case AudioManager.ACTION_AUDIO_BECOMING_NOISY: - onPause(); - break; - } - } - - protected void registerBroadcastReceiver() { - // Try to unregister current first - unregisterBroadcastReceiver(); - context.registerReceiver(broadcastReceiver, intentFilter); - } - - protected void unregisterBroadcastReceiver() { - try { - context.unregisterReceiver(broadcastReceiver); - } catch (final IllegalArgumentException unregisteredException) { - Log.w(TAG, "Broadcast receiver already unregistered " - + "(" + unregisteredException.getMessage() + ")"); - } - } - - /*////////////////////////////////////////////////////////////////////////// - // States Implementation - //////////////////////////////////////////////////////////////////////////*/ - - public void changeState(final int state) { - if (DEBUG) { - Log.d(TAG, "changeState() called with: state = [" + state + "]"); - } - currentState = state; - switch (state) { - case STATE_BLOCKED: - onBlocked(); - break; - case STATE_PLAYING: - onPlaying(); - break; - case STATE_BUFFERING: - onBuffering(); - break; - case STATE_PAUSED: - onPaused(); - break; - case STATE_PAUSED_SEEK: - onPausedSeek(); - break; - case STATE_COMPLETED: - onCompleted(); - break; - } - } - - public void onBlocked() { - if (DEBUG) { - Log.d(TAG, "onBlocked() called"); - } - if (!isProgressLoopRunning()) { - startProgressLoop(); - } - } - - public void onPlaying() { - if (DEBUG) { - Log.d(TAG, "onPlaying() called"); - } - if (!isProgressLoopRunning()) { - startProgressLoop(); - } - } - - public void onBuffering() { - } - - public void onPaused() { - if (isProgressLoopRunning()) { - stopProgressLoop(); - } - } - - public void onPausedSeek() { - } - - public void onCompleted() { - if (DEBUG) { - Log.d(TAG, "onCompleted() called"); - } - if (playQueue.getIndex() < playQueue.size() - 1) { - playQueue.offsetIndex(+1); - } - if (isProgressLoopRunning()) { - stopProgressLoop(); - } - } - - /*////////////////////////////////////////////////////////////////////////// - // Repeat and shuffle - //////////////////////////////////////////////////////////////////////////*/ - - public void onRepeatClicked() { - if (DEBUG) { - Log.d(TAG, "onRepeatClicked() called"); - } - - final int mode; - - switch (getRepeatMode()) { - case Player.REPEAT_MODE_OFF: - mode = Player.REPEAT_MODE_ONE; - break; - case Player.REPEAT_MODE_ONE: - mode = Player.REPEAT_MODE_ALL; - break; - case Player.REPEAT_MODE_ALL: - default: - mode = Player.REPEAT_MODE_OFF; - break; - } - - setRepeatMode(mode); - if (DEBUG) { - Log.d(TAG, "onRepeatClicked() currentRepeatMode = " + getRepeatMode()); - } - } - - public void onShuffleClicked() { - if (DEBUG) { - Log.d(TAG, "onShuffleClicked() called"); - } - - if (simpleExoPlayer == null) { - return; - } - simpleExoPlayer.setShuffleModeEnabled(!simpleExoPlayer.getShuffleModeEnabled()); - } - /*////////////////////////////////////////////////////////////////////////// - // Mute / Unmute - //////////////////////////////////////////////////////////////////////////*/ - - public void onMuteUnmuteButtonClicked() { - if (DEBUG) { - Log.d(TAG, "onMuteUnmuteButtonClicled() called"); - } - simpleExoPlayer.setVolume(isMuted() ? 1 : 0); - } - - public boolean isMuted() { - return simpleExoPlayer.getVolume() == 0; - } - - /*////////////////////////////////////////////////////////////////////////// - // Progress Updates - //////////////////////////////////////////////////////////////////////////*/ - - public abstract void onUpdateProgress(int currentProgress, int duration, int bufferPercent); - - protected void startProgressLoop() { - progressUpdateReactor.set(getProgressReactor()); - } - - protected void stopProgressLoop() { - progressUpdateReactor.set(null); - } - - public void triggerProgressUpdate() { - if (simpleExoPlayer == null) { - return; - } - onUpdateProgress( - Math.max((int) simpleExoPlayer.getCurrentPosition(), 0), - (int) simpleExoPlayer.getDuration(), - simpleExoPlayer.getBufferedPercentage() - ); - } - - private Disposable getProgressReactor() { - return Observable.interval(PROGRESS_LOOP_INTERVAL_MILLIS, MILLISECONDS, - AndroidSchedulers.mainThread()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(ignored -> triggerProgressUpdate(), - error -> Log.e(TAG, "Progress update failure: ", error)); - } - - /*////////////////////////////////////////////////////////////////////////// - // ExoPlayer Listener - //////////////////////////////////////////////////////////////////////////*/ - - @Override - public void onTimelineChanged(final Timeline timeline, final int reason) { - if (DEBUG) { - Log.d(TAG, "ExoPlayer - onTimelineChanged() called with " - + "timeline size = [" + timeline.getWindowCount() + "], " - + "reason = [" + reason + "]"); - } - - maybeUpdateCurrentMetadata(); - } - - @Override - public void onTracksChanged(final TrackGroupArray trackGroups, - final TrackSelectionArray trackSelections) { - if (DEBUG) { - Log.d(TAG, "ExoPlayer - onTracksChanged(), " - + "track group size = " + trackGroups.length); - } - - maybeUpdateCurrentMetadata(); - } - - @Override - public void onPlaybackParametersChanged(final PlaybackParameters playbackParameters) { - if (DEBUG) { - Log.d(TAG, "ExoPlayer - playbackParameters(), " - + "speed: " + playbackParameters.speed + ", " - + "pitch: " + playbackParameters.pitch); - } - } - - @Override - public void onLoadingChanged(final boolean isLoading) { - if (DEBUG) { - Log.d(TAG, "ExoPlayer - onLoadingChanged() called with: " - + "isLoading = [" + isLoading + "]"); - } - - if (!isLoading && getCurrentState() == STATE_PAUSED && isProgressLoopRunning()) { - stopProgressLoop(); - } else if (isLoading && !isProgressLoopRunning()) { - startProgressLoop(); - } - - maybeUpdateCurrentMetadata(); - } - - @Override - public void onPlayerStateChanged(final boolean playWhenReady, final int playbackState) { - if (DEBUG) { - Log.d(TAG, "ExoPlayer - onPlayerStateChanged() called with: " - + "playWhenReady = [" + playWhenReady + "], " - + "playbackState = [" + playbackState + "]"); - } - - if (getCurrentState() == STATE_PAUSED_SEEK) { - if (DEBUG) { - Log.d(TAG, "ExoPlayer - onPlayerStateChanged() is currently blocked"); - } - return; - } - - switch (playbackState) { - case Player.STATE_IDLE: // 1 - isPrepared = false; - break; - case Player.STATE_BUFFERING: // 2 - if (isPrepared) { - changeState(STATE_BUFFERING); - } - break; - case Player.STATE_READY: //3 - maybeUpdateCurrentMetadata(); - maybeCorrectSeekPosition(); - if (!isPrepared) { - isPrepared = true; - onPrepared(playWhenReady); - break; - } - changeState(playWhenReady ? STATE_PLAYING : STATE_PAUSED); - break; - case Player.STATE_ENDED: // 4 - changeState(STATE_COMPLETED); - if (currentMetadata != null) { - resetPlaybackState(currentMetadata.getMetadata()); - } - isPrepared = false; - break; - } - } - - private void maybeCorrectSeekPosition() { - if (playQueue == null || simpleExoPlayer == null || currentMetadata == null) { - return; - } - - final PlayQueueItem currentSourceItem = playQueue.getItem(); - if (currentSourceItem == null) { - return; - } - - final StreamInfo currentInfo = currentMetadata.getMetadata(); - final long presetStartPositionMillis = currentInfo.getStartPosition() * 1000; - if (presetStartPositionMillis > 0L) { - // Has another start position? - if (DEBUG) { - Log.d(TAG, "Playback - Seeking to preset start " - + "position=[" + presetStartPositionMillis + "]"); - } - seekTo(presetStartPositionMillis); - } - } - - /** - * Process exceptions produced by {@link com.google.android.exoplayer2.ExoPlayer ExoPlayer}. - *

There are multiple types of errors:

- *
    - *
  • {@link ExoPlaybackException#TYPE_SOURCE TYPE_SOURCE}
  • - *
  • {@link ExoPlaybackException#TYPE_UNEXPECTED TYPE_UNEXPECTED}: - * If a runtime error occurred, then we can try to recover it by restarting the playback - * after setting the timestamp recovery.
  • - *
  • {@link ExoPlaybackException#TYPE_RENDERER TYPE_RENDERER}: - * If the renderer failed, treat the error as unrecoverable.
  • - *
- * - * @see #processSourceError(IOException) - * @see Player.EventListener#onPlayerError(ExoPlaybackException) - */ - @Override - public void onPlayerError(final ExoPlaybackException error) { - if (DEBUG) { - Log.d(TAG, "ExoPlayer - onPlayerError() called with: " + "error = [" + error + "]"); - } - if (errorToast != null) { - errorToast.cancel(); - errorToast = null; - } - - savePlaybackState(); - - switch (error.type) { - case ExoPlaybackException.TYPE_SOURCE: - processSourceError(error.getSourceException()); - showStreamError(error); - break; - case ExoPlaybackException.TYPE_UNEXPECTED: - showRecoverableError(error); - setRecovery(); - reload(); - break; - default: - showUnrecoverableError(error); - onPlaybackShutdown(); - break; - } - } - - private void processSourceError(final IOException error) { - if (simpleExoPlayer == null || playQueue == null) { - return; - } - setRecovery(); - - if (error instanceof BehindLiveWindowException) { - reload(); - } else { - playQueue.error(); - } - } - - @Override - public void onPositionDiscontinuity(@Player.DiscontinuityReason final int reason) { - if (DEBUG) { - Log.d(TAG, "ExoPlayer - onPositionDiscontinuity() called with " - + "reason = [" + reason + "]"); - } - if (playQueue == null) { - return; - } - - // Refresh the playback if there is a transition to the next video - final int newWindowIndex = simpleExoPlayer.getCurrentWindowIndex(); - switch (reason) { - case DISCONTINUITY_REASON_PERIOD_TRANSITION: - // When player is in single repeat mode and a period transition occurs, - // we need to register a view count here since no metadata has changed - if (getRepeatMode() == Player.REPEAT_MODE_ONE - && newWindowIndex == playQueue.getIndex()) { - registerView(); - break; - } - case DISCONTINUITY_REASON_SEEK: - case DISCONTINUITY_REASON_SEEK_ADJUSTMENT: - case DISCONTINUITY_REASON_INTERNAL: - if (playQueue.getIndex() != newWindowIndex) { - resetPlaybackState(playQueue.getItem()); - playQueue.setIndex(newWindowIndex); - } - break; - } - - maybeUpdateCurrentMetadata(); - } - - @Override - public void onRepeatModeChanged(@Player.RepeatMode final int reason) { - if (DEBUG) { - Log.d(TAG, "ExoPlayer - onRepeatModeChanged() called with: " - + "mode = [" + reason + "]"); - } - } - - @Override - public void onShuffleModeEnabledChanged(final boolean shuffleModeEnabled) { - if (DEBUG) { - Log.d(TAG, "ExoPlayer - onShuffleModeEnabledChanged() called with: " - + "mode = [" + shuffleModeEnabled + "]"); - } - if (playQueue == null) { - return; - } - if (shuffleModeEnabled) { - playQueue.shuffle(); - } else { - playQueue.unshuffle(); - } - } - - @Override - public void onSeekProcessed() { - if (DEBUG) { - Log.d(TAG, "ExoPlayer - onSeekProcessed() called"); - } - if (isPrepared) { - savePlaybackState(); - } - } - /*////////////////////////////////////////////////////////////////////////// - // Playback Listener - //////////////////////////////////////////////////////////////////////////*/ - - @Override - public boolean isApproachingPlaybackEdge(final long timeToEndMillis) { - // If live, then not near playback edge - // If not playing, then not approaching playback edge - if (simpleExoPlayer == null || isLive() || !isPlaying()) { - return false; - } - - final long currentPositionMillis = simpleExoPlayer.getCurrentPosition(); - final long currentDurationMillis = simpleExoPlayer.getDuration(); - return currentDurationMillis - currentPositionMillis < timeToEndMillis; - } - - @Override - public void onPlaybackBlock() { - if (simpleExoPlayer == null) { - return; - } - if (DEBUG) { - Log.d(TAG, "Playback - onPlaybackBlock() called"); - } - - currentItem = null; - currentMetadata = null; - simpleExoPlayer.stop(); - isPrepared = false; - - changeState(STATE_BLOCKED); - } - - @Override - public void onPlaybackUnblock(final MediaSource mediaSource) { - if (simpleExoPlayer == null) { - return; - } - if (DEBUG) { - Log.d(TAG, "Playback - onPlaybackUnblock() called"); - } - - if (getCurrentState() == STATE_BLOCKED) { - changeState(STATE_BUFFERING); - } - - simpleExoPlayer.prepare(mediaSource); - } - - public void onPlaybackSynchronize(@NonNull final PlayQueueItem item) { - if (DEBUG) { - Log.d(TAG, "Playback - onPlaybackSynchronize() called with " - + "item=[" + item.getTitle() + "], url=[" + item.getUrl() + "]"); - } - if (simpleExoPlayer == null || playQueue == null) { - return; - } - - final boolean onPlaybackInitial = currentItem == null; - final boolean hasPlayQueueItemChanged = currentItem != item; - - final int currentPlayQueueIndex = playQueue.indexOf(item); - final int currentPlaylistIndex = simpleExoPlayer.getCurrentWindowIndex(); - final int currentPlaylistSize = simpleExoPlayer.getCurrentTimeline().getWindowCount(); - - // If nothing to synchronize - if (!hasPlayQueueItemChanged) { - return; - } - currentItem = item; - - // Check if on wrong window - if (currentPlayQueueIndex != playQueue.getIndex()) { - Log.e(TAG, "Playback - Play Queue may be desynchronized: item " - + "index=[" + currentPlayQueueIndex + "], " - + "queue index=[" + playQueue.getIndex() + "]"); - - // Check if bad seek position - } else if ((currentPlaylistSize > 0 && currentPlayQueueIndex >= currentPlaylistSize) - || currentPlayQueueIndex < 0) { - Log.e(TAG, "Playback - Trying to seek to invalid " - + "index=[" + currentPlayQueueIndex + "] with " - + "playlist length=[" + currentPlaylistSize + "]"); - - } else if (currentPlaylistIndex != currentPlayQueueIndex || onPlaybackInitial - || !isPlaying()) { - if (DEBUG) { - Log.d(TAG, "Playback - Rewinding to correct " - + "index=[" + currentPlayQueueIndex + "], " - + "from=[" + currentPlaylistIndex + "], " - + "size=[" + currentPlaylistSize + "]."); - } - - if (item.getRecoveryPosition() != PlayQueueItem.RECOVERY_UNSET) { - simpleExoPlayer.seekTo(currentPlayQueueIndex, item.getRecoveryPosition()); - playQueue.unsetRecovery(currentPlayQueueIndex); - } else { - simpleExoPlayer.seekToDefaultPosition(currentPlayQueueIndex); - } - } - } - - protected void onMetadataChanged(@NonNull final MediaSourceTag tag) { - final StreamInfo info = tag.getMetadata(); - if (DEBUG) { - Log.d(TAG, "Playback - onMetadataChanged() called, playing: " + info.getName()); - } - - initThumbnail(info.getThumbnailUrl()); - registerView(); - } - - /*////////////////////////////////////////////////////////////////////////// - // General Player - //////////////////////////////////////////////////////////////////////////*/ - - public void showStreamError(final Exception exception) { - exception.printStackTrace(); - - if (errorToast == null) { - errorToast = Toast - .makeText(context, R.string.player_stream_failure, Toast.LENGTH_SHORT); - errorToast.show(); - } - } - - public void showRecoverableError(final Exception exception) { - exception.printStackTrace(); - - if (errorToast == null) { - errorToast = Toast - .makeText(context, R.string.player_recoverable_failure, Toast.LENGTH_SHORT); - errorToast.show(); - } - } - - public void showUnrecoverableError(final Exception exception) { - exception.printStackTrace(); - - if (errorToast != null) { - errorToast.cancel(); - } - errorToast = Toast - .makeText(context, R.string.player_unrecoverable_failure, Toast.LENGTH_SHORT); - errorToast.show(); - } - - public void onPrepared(final boolean playWhenReady) { - if (DEBUG) { - Log.d(TAG, "onPrepared() called with: playWhenReady = [" + playWhenReady + "]"); - } - if (playWhenReady) { - audioReactor.requestAudioFocus(); - } - changeState(playWhenReady ? STATE_PLAYING : STATE_PAUSED); - } - - public void onPlay() { - if (DEBUG) { - Log.d(TAG, "onPlay() called"); - } - if (audioReactor == null || playQueue == null || simpleExoPlayer == null) { - return; - } - - audioReactor.requestAudioFocus(); - - if (getCurrentState() == STATE_COMPLETED) { - if (playQueue.getIndex() == 0) { - seekToDefault(); - } else { - playQueue.setIndex(0); - } - } - - simpleExoPlayer.setPlayWhenReady(true); - savePlaybackState(); - } - - public void onPause() { - if (DEBUG) { - Log.d(TAG, "onPause() called"); - } - if (audioReactor == null || simpleExoPlayer == null) { - return; - } - - audioReactor.abandonAudioFocus(); - simpleExoPlayer.setPlayWhenReady(false); - savePlaybackState(); - } - - public void onPlayPause() { - if (DEBUG) { - Log.d(TAG, "onPlayPause() called"); - } - - if (isPlaying()) { - onPause(); - } else { - onPlay(); - } - } - - public void onFastRewind() { - if (DEBUG) { - Log.d(TAG, "onFastRewind() called"); - } - seekBy(-getSeekDuration()); - triggerProgressUpdate(); - } - - public void onFastForward() { - if (DEBUG) { - Log.d(TAG, "onFastForward() called"); - } - seekBy(getSeekDuration()); - triggerProgressUpdate(); - } - - private int getSeekDuration() { - final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); - final String key = context.getString(R.string.seek_duration_key); - final String value = prefs - .getString(key, context.getString(R.string.seek_duration_default_value)); - return Integer.parseInt(value); - } - - public void onPlayPrevious() { - if (simpleExoPlayer == null || playQueue == null) { - return; - } - if (DEBUG) { - Log.d(TAG, "onPlayPrevious() called"); - } - - /* If current playback has run for PLAY_PREV_ACTIVATION_LIMIT_MILLIS milliseconds, - * restart current track. Also restart the track if the current track - * is the first in a queue.*/ - if (simpleExoPlayer.getCurrentPosition() > PLAY_PREV_ACTIVATION_LIMIT_MILLIS - || playQueue.getIndex() == 0) { - seekToDefault(); - playQueue.offsetIndex(0); - } else { - savePlaybackState(); - playQueue.offsetIndex(-1); - } - } - - public void onPlayNext() { - if (playQueue == null) { - return; - } - if (DEBUG) { - Log.d(TAG, "onPlayNext() called"); - } - - savePlaybackState(); - playQueue.offsetIndex(+1); - } - - public void onSelected(final PlayQueueItem item) { - if (playQueue == null || simpleExoPlayer == null) { - return; - } - - final int index = playQueue.indexOf(item); - if (index == -1) { - return; - } - - if (playQueue.getIndex() == index && simpleExoPlayer.getCurrentWindowIndex() == index) { - seekToDefault(); - } else { - savePlaybackState(); - } - playQueue.setIndex(index); - } - - public void seekTo(final long positionMillis) { - if (DEBUG) { - Log.d(TAG, "seekBy() called with: position = [" + positionMillis + "]"); - } - if (simpleExoPlayer != null) { - // prevent invalid positions when fast-forwarding/-rewinding - long normalizedPositionMillis = positionMillis; - if (normalizedPositionMillis < 0) { - normalizedPositionMillis = 0; - } else if (normalizedPositionMillis > simpleExoPlayer.getDuration()) { - normalizedPositionMillis = simpleExoPlayer.getDuration(); - } - - simpleExoPlayer.seekTo(normalizedPositionMillis); - } - } - - public void seekBy(final long offsetMillis) { - if (DEBUG) { - Log.d(TAG, "seekBy() called with: offsetMillis = [" + offsetMillis + "]"); - } - seekTo(simpleExoPlayer.getCurrentPosition() + offsetMillis); - } - - public boolean isCurrentWindowValid() { - return simpleExoPlayer != null && simpleExoPlayer.getDuration() >= 0 - && simpleExoPlayer.getCurrentPosition() >= 0; - } - - public void seekToDefault() { - if (simpleExoPlayer != null) { - simpleExoPlayer.seekToDefaultPosition(); - } - } - - /*////////////////////////////////////////////////////////////////////////// - // Utils - //////////////////////////////////////////////////////////////////////////*/ - - private void registerView() { - if (currentMetadata == null) { - return; - } - final StreamInfo currentInfo = currentMetadata.getMetadata(); - final Disposable viewRegister = recordManager.onViewed(currentInfo).onErrorComplete() - .subscribe( - ignored -> { /* successful */ }, - error -> Log.e(TAG, "Player onViewed() failure: ", error) - ); - databaseUpdateReactor.add(viewRegister); - } - - protected void reload() { - if (playbackManager != null) { - playbackManager.dispose(); - } - - if (playQueue != null) { - playbackManager = new MediaSourceManager(this, playQueue); - } - } - - private void savePlaybackState(final StreamInfo info, final long progress) { - if (info == null) { - return; - } - if (DEBUG) { - Log.d(TAG, "savePlaybackState() called"); - } - final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); - if (prefs.getBoolean(context.getString(R.string.enable_watch_history_key), true)) { - final Disposable stateSaver = recordManager.saveStreamState(info, progress) - .observeOn(AndroidSchedulers.mainThread()) - .doOnError((e) -> { - if (DEBUG) { - e.printStackTrace(); - } - }) - .onErrorComplete() - .subscribe(); - databaseUpdateReactor.add(stateSaver); - } - } - - private void resetPlaybackState(final PlayQueueItem queueItem) { - if (queueItem == null) { - return; - } - final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); - if (prefs.getBoolean(context.getString(R.string.enable_watch_history_key), true)) { - final Disposable stateSaver = queueItem.getStream() - .flatMapCompletable(info -> recordManager.saveStreamState(info, 0)) - .observeOn(AndroidSchedulers.mainThread()) - .doOnError((e) -> { - if (DEBUG) { - e.printStackTrace(); - } - }) - .onErrorComplete() - .subscribe(); - databaseUpdateReactor.add(stateSaver); - } - } - - public void resetPlaybackState(final StreamInfo info) { - savePlaybackState(info, 0); - } - - public void savePlaybackState() { - if (simpleExoPlayer == null || currentMetadata == null) { - return; - } - final StreamInfo currentInfo = currentMetadata.getMetadata(); - if (playQueue != null) { - // Save current position. It will help to restore this position once a user - // wants to play prev or next stream from the queue - playQueue.setRecovery(playQueue.getIndex(), simpleExoPlayer.getContentPosition()); - } - savePlaybackState(currentInfo, simpleExoPlayer.getCurrentPosition()); - } - - private void maybeUpdateCurrentMetadata() { - if (simpleExoPlayer == null) { - return; - } - - final MediaSourceTag metadata; - try { - metadata = (MediaSourceTag) simpleExoPlayer.getCurrentTag(); - } catch (IndexOutOfBoundsException | ClassCastException error) { - if (DEBUG) { - Log.d(TAG, "Could not update metadata: " + error.getMessage()); - error.printStackTrace(); - } - return; - } - - if (metadata == null) { - return; - } - maybeAutoQueueNextStream(metadata); - - if (currentMetadata == metadata) { - return; - } - currentMetadata = metadata; - onMetadataChanged(metadata); - } - - private void maybeAutoQueueNextStream(@NonNull final MediaSourceTag metadata) { - if (playQueue == null || playQueue.getIndex() != playQueue.size() - 1 - || getRepeatMode() != Player.REPEAT_MODE_OFF - || !PlayerHelper.isAutoQueueEnabled(context)) { - return; - } - // auto queue when starting playback on the last item when not repeating - final PlayQueue autoQueue = PlayerHelper.autoQueueOf(metadata.getMetadata(), - playQueue.getStreams()); - if (autoQueue != null) { - playQueue.append(autoQueue.getStreams()); - } - } - - /*////////////////////////////////////////////////////////////////////////// - // Getters and Setters - //////////////////////////////////////////////////////////////////////////*/ - - public SimpleExoPlayer getPlayer() { - return simpleExoPlayer; - } - - public AudioReactor getAudioReactor() { - return audioReactor; - } - - public int getCurrentState() { - return currentState; - } - - @Nullable - public MediaSourceTag getCurrentMetadata() { - return currentMetadata; - } - - @NonNull - public LoadController getLoadController() { - return (LoadController) loadControl; - } - - @NonNull - public String getVideoUrl() { - return currentMetadata == null - ? context.getString(R.string.unknown_content) - : currentMetadata.getMetadata().getUrl(); - } - - @NonNull - public String getVideoTitle() { - return currentMetadata == null - ? context.getString(R.string.unknown_content) - : currentMetadata.getMetadata().getName(); - } - - @NonNull - public String getUploaderName() { - return currentMetadata == null - ? context.getString(R.string.unknown_content) - : currentMetadata.getMetadata().getUploaderName(); - } - - @Nullable - public Bitmap getThumbnail() { - return currentThumbnail == null - ? BitmapFactory.decodeResource(context.getResources(), R.drawable.dummy_thumbnail) - : currentThumbnail; - } - - /** - * Checks if the current playback is a livestream AND is playing at or beyond the live edge. - * - * @return whether the livestream is playing at or beyond the edge - */ - @SuppressWarnings("BooleanMethodIsAlwaysInverted") - public boolean isLiveEdge() { - if (simpleExoPlayer == null || !isLive()) { - return false; - } - - final Timeline currentTimeline = simpleExoPlayer.getCurrentTimeline(); - final int currentWindowIndex = simpleExoPlayer.getCurrentWindowIndex(); - if (currentTimeline.isEmpty() || currentWindowIndex < 0 - || currentWindowIndex >= currentTimeline.getWindowCount()) { - return false; - } - - final Timeline.Window timelineWindow = new Timeline.Window(); - currentTimeline.getWindow(currentWindowIndex, timelineWindow); - return timelineWindow.getDefaultPositionMs() <= simpleExoPlayer.getCurrentPosition(); - } - - public boolean isLive() { - if (simpleExoPlayer == null) { - return false; - } - try { - return simpleExoPlayer.isCurrentWindowDynamic(); - } catch (@NonNull final IndexOutOfBoundsException e) { - // Why would this even happen =( - // But lets log it anyway. Save is save - if (DEBUG) { - Log.d(TAG, "Could not update metadata: " + e.getMessage()); - e.printStackTrace(); - } - return false; - } - } - - public boolean isPlaying() { - return simpleExoPlayer != null && simpleExoPlayer.isPlaying(); - } - - public boolean isLoading() { - return simpleExoPlayer != null && simpleExoPlayer.isLoading(); - } - - @Player.RepeatMode - public int getRepeatMode() { - return simpleExoPlayer == null - ? Player.REPEAT_MODE_OFF - : simpleExoPlayer.getRepeatMode(); - } - - public void setRepeatMode(@Player.RepeatMode final int repeatMode) { - if (simpleExoPlayer != null) { - simpleExoPlayer.setRepeatMode(repeatMode); - } - } - - public float getPlaybackSpeed() { - return getPlaybackParameters().speed; - } - - public void setPlaybackSpeed(final float speed) { - setPlaybackParameters(speed, getPlaybackPitch(), getPlaybackSkipSilence()); - } - - public float getPlaybackPitch() { - return getPlaybackParameters().pitch; - } - - public boolean getPlaybackSkipSilence() { - return getPlaybackParameters().skipSilence; - } - - public PlaybackParameters getPlaybackParameters() { - if (simpleExoPlayer == null) { - return PlaybackParameters.DEFAULT; - } - return simpleExoPlayer.getPlaybackParameters(); - } - - /** - * Sets the playback parameters of the player, and also saves them to shared preferences. - * Speed and pitch are rounded up to 2 decimal places before being used or saved. - * - * @param speed the playback speed, will be rounded to up to 2 decimal places - * @param pitch the playback pitch, will be rounded to up to 2 decimal places - * @param skipSilence skip silence during playback - */ - public void setPlaybackParameters(final float speed, final float pitch, - final boolean skipSilence) { - final float roundedSpeed = Math.round(speed * 100.0f) / 100.0f; - final float roundedPitch = Math.round(pitch * 100.0f) / 100.0f; - - savePlaybackParametersToPreferences(roundedSpeed, roundedPitch, skipSilence); - simpleExoPlayer.setPlaybackParameters( - new PlaybackParameters(roundedSpeed, roundedPitch, skipSilence)); - } - - private void savePlaybackParametersToPreferences(final float speed, final float pitch, - final boolean skipSilence) { - PreferenceManager.getDefaultSharedPreferences(context) - .edit() - .putFloat(context.getString(R.string.playback_speed_key), speed) - .putFloat(context.getString(R.string.playback_pitch_key), pitch) - .putBoolean(context.getString(R.string.playback_skip_silence_key), skipSilence) - .apply(); - } - - public PlayQueue getPlayQueue() { - return playQueue; - } - - public PlayQueueAdapter getPlayQueueAdapter() { - return playQueueAdapter; - } - - public boolean isPrepared() { - return isPrepared; - } - - public boolean isProgressLoopRunning() { - return progressUpdateReactor.get() != null; - } - - public void setRecovery() { - if (playQueue == null || simpleExoPlayer == null) { - return; - } - - final int queuePos = playQueue.getIndex(); - final long windowPos = simpleExoPlayer.getCurrentPosition(); - - if (windowPos > 0 && windowPos <= simpleExoPlayer.getDuration()) { - setRecovery(queuePos, windowPos); - } - } - - public void setRecovery(final int queuePos, final long windowPos) { - if (playQueue.size() <= queuePos) { - return; - } - - if (DEBUG) { - Log.d(TAG, "Setting recovery, queue: " + queuePos + ", pos: " + windowPos); - } - playQueue.setRecovery(queuePos, windowPos); - } - - public boolean gotDestroyed() { - return simpleExoPlayer == null; - } - - private boolean isPlaybackResumeEnabled() { - final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); - return prefs.getBoolean(context.getString(R.string.enable_watch_history_key), true) - && prefs.getBoolean(context.getString(R.string.enable_playback_resume_key), true); - } -} diff --git a/app/src/main/java/org/schabi/newpipe/player/MainPlayer.java b/app/src/main/java/org/schabi/newpipe/player/MainPlayer.java index 63f6a400e..e9ae1a1db 100644 --- a/app/src/main/java/org/schabi/newpipe/player/MainPlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/MainPlayer.java @@ -26,6 +26,7 @@ import android.os.Binder; import android.os.IBinder; import android.util.DisplayMetrics; import android.util.Log; +import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.view.WindowManager; @@ -33,7 +34,8 @@ import android.view.WindowManager; import androidx.annotation.Nullable; 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 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 { 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 final IBinder mBinder = new MainPlayer.LocalBinder(); @@ -64,25 +66,23 @@ public final class MainPlayer extends Service { //////////////////////////////////////////////////////////////////////////*/ static final String ACTION_CLOSE - = "org.schabi.newpipe.player.MainPlayer.CLOSE"; + = App.PACKAGE_NAME + ".player.MainPlayer.CLOSE"; static final String ACTION_PLAY_PAUSE - = "org.schabi.newpipe.player.MainPlayer.PLAY_PAUSE"; - static final String ACTION_OPEN_CONTROLS - = "org.schabi.newpipe.player.MainPlayer.OPEN_CONTROLS"; + = App.PACKAGE_NAME + ".player.MainPlayer.PLAY_PAUSE"; static final String ACTION_REPEAT - = "org.schabi.newpipe.player.MainPlayer.REPEAT"; + = App.PACKAGE_NAME + ".player.MainPlayer.REPEAT"; 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 - = "org.schabi.newpipe.player.MainPlayer.ACTION_PLAY_PREVIOUS"; + = App.PACKAGE_NAME + ".player.MainPlayer.ACTION_PLAY_PREVIOUS"; 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 - = "org.schabi.newpipe.player.MainPlayer.ACTION_FAST_FORWARD"; + = App.PACKAGE_NAME + ".player.MainPlayer.ACTION_FAST_FORWARD"; 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 - = "org.schabi.newpipe.player.MainPlayer.ACTION_RECREATE_NOTIFICATION"; + = App.PACKAGE_NAME + ".player.MainPlayer.ACTION_RECREATE_NOTIFICATION"; /*////////////////////////////////////////////////////////////////////////// // Service's LifeCycle @@ -101,13 +101,12 @@ public final class MainPlayer extends Service { } 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); - playerImpl.setup(layout); - playerImpl.shouldUpdateOnProgress = true; + player = new Player(this); + player.setupFromView(binding); - NotificationUtil.getInstance().createNotificationAndStartForeground(playerImpl, this); + NotificationUtil.getInstance().createNotificationAndStartForeground(player, this); } @Override @@ -117,19 +116,19 @@ public final class MainPlayer extends Service { + "], flags = [" + flags + "], startId = [" + startId + "]"); } 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 return START_NOT_STICKY; } if (Intent.ACTION_MEDIA_BUTTON.equals(intent.getAction()) - || intent.getStringExtra(VideoPlayer.PLAY_QUEUE_KEY) != null) { - NotificationUtil.getInstance().createNotificationAndStartForeground(playerImpl, this); + || intent.getStringExtra(Player.PLAY_QUEUE_KEY) != null) { + NotificationUtil.getInstance().createNotificationAndStartForeground(player, this); } - playerImpl.handleIntent(intent); - if (playerImpl.mediaSessionManager != null) { - playerImpl.mediaSessionManager.handleMediaButtonIntent(intent); + player.handleIntent(intent); + if (player.getMediaSessionManager() != null) { + player.getMediaSessionManager().handleMediaButtonIntent(intent); } return START_NOT_STICKY; } @@ -139,20 +138,20 @@ public final class MainPlayer extends Service { Log.d(TAG, "stop() called"); } - if (playerImpl.getPlayer() != null) { - playerImpl.wasPlaying = playerImpl.getPlayer().getPlayWhenReady(); + if (!player.exoPlayerIsNull()) { + player.saveWasPlaying(); // Releases wifi & cpu, disables keepScreenOn, etc. if (!autoplayEnabled) { - playerImpl.onPause(); + player.pause(); } // We can't just pause the player here because it will make transition // from one stream to a new stream not smooth - playerImpl.getPlayer().stop(false); - playerImpl.setRecovery(); + player.smoothStopPlayer(); + player.setRecovery(); // Android TV will handle back button in case controls will be visible // (one more additional unneeded click while the player is hidden) - playerImpl.hideControls(0, 0); - playerImpl.onQueueClosed(); + player.hideControls(0, 0); + player.closeItemsList(); // Notification shows information about old stream but if a user selects // a stream from backStack it's not actual anymore // So we should hide the notification at all. @@ -166,7 +165,7 @@ public final class MainPlayer extends Service { @Override public void onTaskRemoved(final Intent rootIntent) { super.onTaskRemoved(rootIntent); - if (!playerImpl.videoPlayerSelected()) { + if (!player.videoPlayerSelected()) { return; } onDestroy(); @@ -179,7 +178,23 @@ public final class MainPlayer extends Service { if (DEBUG) { 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 @@ -192,32 +207,6 @@ public final class MainPlayer extends Service { 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 //////////////////////////////////////////////////////////////////////////*/ @@ -225,25 +214,25 @@ public final class MainPlayer extends Service { boolean isLandscape() { // DisplayMetrics from activity context knows about MultiWindow feature // while DisplayMetrics from app context doesn't - final DisplayMetrics metrics = (playerImpl != null - && playerImpl.getParentActivity() != null - ? playerImpl.getParentActivity().getResources() + final DisplayMetrics metrics = (player != null + && player.getParentActivity() != null + ? player.getParentActivity().getResources() : getResources()).getDisplayMetrics(); return metrics.heightPixels < metrics.widthPixels; } @Nullable public View getView() { - if (playerImpl == null) { + if (player == null) { return null; } - return playerImpl.getRootView(); + return player.getRootView(); } public void removeViewFromParent() { if (getView() != null && getView().getParent() != null) { - if (playerImpl.getParentActivity() != null) { + if (player.getParentActivity() != null) { // This means view was added to fragment final ViewGroup parent = (ViewGroup) getView().getParent(); parent.removeView(getView()); @@ -261,8 +250,8 @@ public final class MainPlayer extends Service { return MainPlayer.this; } - public VideoPlayerImpl getPlayer() { - return MainPlayer.this.playerImpl; + public Player getPlayer() { + return MainPlayer.this.player; } } } diff --git a/app/src/main/java/org/schabi/newpipe/player/NotificationUtil.java b/app/src/main/java/org/schabi/newpipe/player/NotificationUtil.java index 860ace84c..43c1b4405 100644 --- a/app/src/main/java/org/schabi/newpipe/player/NotificationUtil.java +++ b/app/src/main/java/org/schabi/newpipe/player/NotificationUtil.java @@ -15,6 +15,7 @@ import androidx.annotation.Nullable; import androidx.annotation.StringRes; import androidx.core.app.NotificationCompat; import androidx.core.app.NotificationManagerCompat; +import androidx.core.app.ServiceCompat; import androidx.core.content.ContextCompat; import org.schabi.newpipe.MainActivity; @@ -42,7 +43,7 @@ import static org.schabi.newpipe.player.MainPlayer.ACTION_SHUFFLE; */ public final class NotificationUtil { 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; @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 * exists */ - synchronized void createNotificationIfNeededAndUpdate(final VideoPlayerImpl player, + synchronized void createNotificationIfNeededAndUpdate(final Player player, final boolean forceRecreate) { if (forceRecreate || notificationBuilder == null) { notificationBuilder = createNotification(player); @@ -84,14 +85,14 @@ public final class NotificationUtil { notificationManager.notify(NOTIFICATION_ID, notificationBuilder.build()); } - private synchronized NotificationCompat.Builder createNotification( - final VideoPlayerImpl player) { + private synchronized NotificationCompat.Builder createNotification(final Player player) { if (DEBUG) { Log.d(TAG, "createNotification()"); } - notificationManager = NotificationManagerCompat.from(player.context); - final NotificationCompat.Builder builder = new NotificationCompat.Builder(player.context, - player.context.getString(R.string.notification_channel_id)); + notificationManager = NotificationManagerCompat.from(player.getContext()); + final NotificationCompat.Builder builder = + new NotificationCompat.Builder(player.getContext(), + player.getContext().getString(R.string.notification_channel_id)); initializeNotificationSlots(player); @@ -106,25 +107,25 @@ public final class NotificationUtil { // build the compact slot indices array (need code to convert from Integer... because Java) final List compactSlotList = NotificationConstants.getCompactSlotsFromPreferences( - player.context, player.sharedPreferences, nonNothingSlotCount); + player.getContext(), player.getPrefs(), nonNothingSlotCount); final int[] compactSlots = new int[compactSlotList.size()]; for (int i = 0; i < compactSlotList.size(); i++) { compactSlots[i] = compactSlotList.get(i); } builder.setStyle(new androidx.media.app.NotificationCompat.MediaStyle() - .setMediaSession(player.mediaSessionManager.getSessionToken()) + .setMediaSession(player.getMediaSessionManager().getSessionToken()) .setShowActionsInCompactView(compactSlots)) .setPriority(NotificationCompat.PRIORITY_HIGH) .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) .setCategory(NotificationCompat.CATEGORY_TRANSPORT) .setShowWhen(false) .setSmallIcon(R.drawable.ic_newpipe_triangle_white) - .setColor(ContextCompat.getColor(player.context, R.color.dark_background_color)) - .setColorized(player.sharedPreferences.getBoolean( - player.context.getString(R.string.notification_colorize_key), - true)) - .setDeleteIntent(PendingIntent.getBroadcast(player.context, NOTIFICATION_ID, + .setColor(ContextCompat.getColor(player.getContext(), + R.color.dark_background_color)) + .setColorized(player.getPrefs().getBoolean( + player.getContext().getString(R.string.notification_colorize_key), true)) + .setDeleteIntent(PendingIntent.getBroadcast(player.getContext(), NOTIFICATION_ID, new Intent(ACTION_CLOSE), FLAG_UPDATE_CURRENT)); return builder; @@ -134,20 +135,20 @@ public final class NotificationUtil { * Updates the notification builder and the button icons depending on the playback state. * @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) { Log.d(TAG, "updateNotification()"); } // 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)); notificationBuilder.setContentTitle(player.getVideoTitle()); notificationBuilder.setContentText(player.getUploaderName()); notificationBuilder.setTicker(player.getVideoTitle()); updateActions(notificationBuilder, player); - final boolean showThumbnail = player.sharedPreferences.getBoolean( - player.context.getString(R.string.show_thumbnail_key), true); + final boolean showThumbnail = player.getPrefs().getBoolean( + player.getContext().getString(R.string.show_thumbnail_key), true); if (showThumbnail) { 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) { notificationBuilder = createNotification(player); } @@ -188,7 +189,7 @@ public final class NotificationUtil { } void cancelNotificationAndStopForeground(final Service service) { - service.stopForeground(true); + ServiceCompat.stopForeground(service, ServiceCompat.STOP_FOREGROUND_REMOVE); if (notificationManager != null) { notificationManager.cancel(NOTIFICATION_ID); @@ -202,17 +203,16 @@ public final class NotificationUtil { // ACTIONS ///////////////////////////////////////////////////// - private void initializeNotificationSlots(final VideoPlayerImpl player) { + private void initializeNotificationSlots(final Player player) { for (int i = 0; i < 5; ++i) { - notificationSlots[i] = player.sharedPreferences.getInt( - player.context.getString(NotificationConstants.SLOT_PREF_KEYS[i]), + notificationSlots[i] = player.getPrefs().getInt( + player.getContext().getString(NotificationConstants.SLOT_PREF_KEYS[i]), NotificationConstants.SLOT_DEFAULTS[i]); } } @SuppressLint("RestrictedApi") - private void updateActions(final NotificationCompat.Builder builder, - final VideoPlayerImpl player) { + private void updateActions(final NotificationCompat.Builder builder, final Player player) { builder.mActions.clear(); for (int i = 0; i < 5; ++i) { addAction(builder, player, notificationSlots[i]); @@ -220,7 +220,7 @@ public final class NotificationUtil { } private void addAction(final NotificationCompat.Builder builder, - final VideoPlayerImpl player, + final Player player, @NotificationConstants.Action final int slot) { final NotificationCompat.Action action = getAction(player, slot); if (action != null) { @@ -230,7 +230,7 @@ public final class NotificationUtil { @Nullable private NotificationCompat.Action getAction( - final VideoPlayerImpl player, + final Player player, @NotificationConstants.Action final int selectedAction) { final int baseActionIcon = NotificationConstants.ACTION_ICONS[selectedAction]; switch (selectedAction) { @@ -251,7 +251,7 @@ public final class NotificationUtil { R.string.exo_controls_fastforward_description, ACTION_FAST_FORWARD); 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, R.string.exo_controls_previous_description, ACTION_PLAY_PREVIOUS); } else { @@ -260,7 +260,7 @@ public final class NotificationUtil { } 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, R.string.exo_controls_next_description, ACTION_PLAY_NEXT); } else { @@ -269,23 +269,23 @@ public final class NotificationUtil { } case NotificationConstants.PLAY_PAUSE_BUFFERING: - if (player.getCurrentState() == BasePlayer.STATE_PREFLIGHT - || player.getCurrentState() == BasePlayer.STATE_BLOCKED - || player.getCurrentState() == BasePlayer.STATE_BUFFERING) { + if (player.getCurrentState() == Player.STATE_PREFLIGHT + || player.getCurrentState() == Player.STATE_BLOCKED + || player.getCurrentState() == Player.STATE_BUFFERING) { // null intent -> show hourglass icon that does nothing when clicked 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); } 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, R.string.exo_controls_pause_description, ACTION_PLAY_PAUSE); } else if (player.isPlaying() - || player.getCurrentState() == BasePlayer.STATE_PREFLIGHT - || player.getCurrentState() == BasePlayer.STATE_BLOCKED - || player.getCurrentState() == BasePlayer.STATE_BUFFERING) { + || player.getCurrentState() == Player.STATE_PREFLIGHT + || player.getCurrentState() == Player.STATE_BLOCKED + || player.getCurrentState() == Player.STATE_BUFFERING) { return getAction(player, R.drawable.exo_notification_pause, R.string.exo_controls_pause_description, ACTION_PLAY_PAUSE); } else { @@ -306,7 +306,7 @@ public final class NotificationUtil { } 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, R.string.exo_controls_shuffle_on_description, ACTION_SHUFFLE); } 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, @StringRes final int title, final String intentAction) { - return new NotificationCompat.Action(drawable, player.context.getString(title), - PendingIntent.getBroadcast(player.context, NOTIFICATION_ID, + return new NotificationCompat.Action(drawable, player.getContext().getString(title), + PendingIntent.getBroadcast(player.getContext(), NOTIFICATION_ID, new Intent(intentAction), FLAG_UPDATE_CURRENT)); } - private Intent getIntentForNotification(final VideoPlayerImpl player) { + private Intent getIntentForNotification(final Player player) { if (player.audioPlayerSelected() || player.popupPlayerSelected()) { // 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 { // We are playing in fragment. Don't open another activity just show fragment. That's it 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.setAction(Intent.ACTION_MAIN); intent.addCategory(Intent.CATEGORY_LAUNCHER); @@ -354,10 +354,9 @@ public final class NotificationUtil { // BITMAP ///////////////////////////////////////////////////// - private void setLargeIcon(final NotificationCompat.Builder builder, - final VideoPlayerImpl player) { - final boolean scaleImageToSquareAspectRatio = player.sharedPreferences.getBoolean( - player.context.getString(R.string.scale_to_square_image_in_notifications_key), + private void setLargeIcon(final NotificationCompat.Builder builder, final Player player) { + final boolean scaleImageToSquareAspectRatio = player.getPrefs().getBoolean( + player.getContext().getString(R.string.scale_to_square_image_in_notifications_key), false); if (scaleImageToSquareAspectRatio) { builder.setLargeIcon(getBitmapWithSquareAspectRatio(player.getThumbnail())); diff --git a/app/src/main/java/org/schabi/newpipe/player/ServicePlayerActivity.java b/app/src/main/java/org/schabi/newpipe/player/PlayQueueActivity.java similarity index 67% rename from app/src/main/java/org/schabi/newpipe/player/ServicePlayerActivity.java rename to app/src/main/java/org/schabi/newpipe/player/PlayQueueActivity.java index ad4c603cd..6ea7ecda3 100644 --- a/app/src/main/java/org/schabi/newpipe/player/ServicePlayerActivity.java +++ b/app/src/main/java/org/schabi/newpipe/player/PlayQueueActivity.java @@ -1,6 +1,7 @@ package org.schabi.newpipe.player; import android.content.ComponentName; +import android.content.Context; import android.content.Intent; import android.content.ServiceConnection; import android.os.Bundle; @@ -11,23 +12,18 @@ import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; -import android.widget.ImageButton; -import android.widget.LinearLayout; import android.widget.PopupMenu; -import android.widget.ProgressBar; import android.widget.SeekBar; -import android.widget.TextView; import androidx.appcompat.app.AppCompatActivity; -import androidx.appcompat.widget.Toolbar; import androidx.recyclerview.widget.ItemTouchHelper; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; import com.google.android.exoplayer2.PlaybackParameters; -import com.google.android.exoplayer2.Player; import org.schabi.newpipe.R; +import org.schabi.newpipe.databinding.ActivityPlayerQueueControlBinding; import org.schabi.newpipe.extractor.stream.StreamInfo; import org.schabi.newpipe.fragments.OnScrollBelowItemsListener; 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.util.Localization.assureCorrectAppLanguage; -public abstract class ServicePlayerActivity extends AppCompatActivity +public final class PlayQueueActivity extends AppCompatActivity implements PlayerEventListener, SeekBar.OnSeekBarChangeListener, 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 SMOOTH_SCROLL_MAXIMUM_DISTANCE = 80; - protected BasePlayer player; + protected Player player; private boolean serviceBound; private ServiceConnection serviceConnection; private boolean seeking; - private boolean redraw; //////////////////////////////////////////////////////////////////////////// // Views //////////////////////////////////////////////////////////////////////////// - private View rootView; + private ActivityPlayerQueueControlBinding queueControlBinding; - private RecyclerView itemsList; 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; - //////////////////////////////////////////////////////////////////////////// - // 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 //////////////////////////////////////////////////////////////////////////// @@ -122,42 +82,39 @@ public abstract class ServicePlayerActivity extends AppCompatActivity assureCorrectAppLanguage(this); super.onCreate(savedInstanceState); ThemeHelper.setTheme(this); - setContentView(R.layout.activity_player_queue_control); - rootView = findViewById(R.id.main_content); - final Toolbar toolbar = rootView.findViewById(R.id.toolbar); - setSupportActionBar(toolbar); + queueControlBinding = ActivityPlayerQueueControlBinding.inflate(getLayoutInflater()); + setContentView(queueControlBinding.getRoot()); + + setSupportActionBar(queueControlBinding.toolbar); if (getSupportActionBar() != null) { getSupportActionBar().setDisplayHomeAsUpEnabled(true); - getSupportActionBar().setTitle(getSupportActionTitle()); + getSupportActionBar().setTitle(R.string.title_activity_play_queue); } serviceConnection = getServiceConnection(); bind(); } - @Override - protected void onResume() { - super.onResume(); - if (redraw) { - recreate(); - redraw = false; - } - } - @Override public boolean onCreateOptionsMenu(final Menu m) { this.menu = m; getMenuInflater().inflate(R.menu.menu_play_queue, m); - getMenuInflater().inflate(getPlayerOptionMenuResource(), m); + getMenuInflater().inflate(R.menu.menu_play_queue_bg, m); onMaybeMuteChanged(); + onPlaybackParameterChanged(player.getPlaybackParameters()); return true; } // Allow to setup visibility of menuItems @Override 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); } @@ -189,14 +146,14 @@ public abstract class ServicePlayerActivity extends AppCompatActivity case R.id.action_switch_popup: if (PermissionHelper.isPopupEnabled(this)) { this.player.setRecovery(); - NavigationHelper.playOnPopupPlayer(this, player.playQueue, true); + NavigationHelper.playOnPopupPlayer(this, player.getPlayQueue(), true); } else { PermissionHelper.showPopupEnablementToast(this); } return true; case R.id.action_switch_background: this.player.setRecovery(); - NavigationHelper.playOnBackgroundPlayer(this, player.playQueue, true); + NavigationHelper.playOnBackgroundPlayer(this, player.getPlayQueue(), true); return true; } return super.onOptionsItemSelected(item); @@ -213,7 +170,8 @@ public abstract class ServicePlayerActivity extends AppCompatActivity //////////////////////////////////////////////////////////////////////////// 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) { unbindService(serviceConnection); } @@ -224,19 +182,18 @@ public abstract class ServicePlayerActivity extends AppCompatActivity if (serviceBound) { unbindService(serviceConnection); serviceBound = false; - stopPlayerListener(); + if (player != null) { + player.removeActivityListener(this); + } if (player != null && player.getPlayQueueAdapter() != null) { player.getPlayQueueAdapter().unsetSelectedListener(); } - if (itemsList != null) { - itemsList.setAdapter(null); - } + queueControlBinding.playQueue.setAdapter(null); if (itemTouchHelper != null) { itemTouchHelper.attachToRecyclerView(null); } - itemsList = null; itemTouchHelper = null; player = null; } @@ -246,12 +203,12 @@ public abstract class ServicePlayerActivity extends AppCompatActivity return new ServiceConnection() { @Override public void onServiceDisconnected(final ComponentName name) { - Log.d(getTag(), "Player service is disconnected"); + Log.d(TAG, "Player service is disconnected"); } @Override 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) { player = ((PlayerServiceBinder) service).getPlayerInstance(); @@ -260,12 +217,14 @@ public abstract class ServicePlayerActivity extends AppCompatActivity } if (player == null || player.getPlayQueue() == null - || player.getPlayQueueAdapter() == null || player.getPlayer() == null) { + || player.getPlayQueueAdapter() == null || player.exoPlayerIsNull()) { unbind(); finish(); } else { buildComponents(); - startPlayerListener(); + if (player != null) { + player.setActivityListener(PlayQueueActivity.this); + } } } }; @@ -283,58 +242,38 @@ public abstract class ServicePlayerActivity extends AppCompatActivity } private void buildQueue() { - itemsList = findViewById(R.id.play_queue); - itemsList.setLayoutManager(new LinearLayoutManager(this)); - itemsList.setAdapter(player.getPlayQueueAdapter()); - itemsList.setClickable(true); - itemsList.setLongClickable(true); - itemsList.clearOnScrollListeners(); - itemsList.addOnScrollListener(getQueueScrollListener()); + queueControlBinding.playQueue.setLayoutManager(new LinearLayoutManager(this)); + queueControlBinding.playQueue.setAdapter(player.getPlayQueueAdapter()); + queueControlBinding.playQueue.setClickable(true); + queueControlBinding.playQueue.setLongClickable(true); + queueControlBinding.playQueue.clearOnScrollListeners(); + queueControlBinding.playQueue.addOnScrollListener(getQueueScrollListener()); itemTouchHelper = new ItemTouchHelper(getItemTouchCallback()); - itemTouchHelper.attachToRecyclerView(itemsList); + itemTouchHelper.attachToRecyclerView(queueControlBinding.playQueue); player.getPlayQueueAdapter().setSelectedListener(getOnSelectedListener()); } private void buildMetadata() { - metadata = rootView.findViewById(R.id.metadata); - metadataTitle = rootView.findViewById(R.id.song_name); - metadataArtist = rootView.findViewById(R.id.artist_name); - - metadata.setOnClickListener(this); - metadataTitle.setSelected(true); - metadataArtist.setSelected(true); + queueControlBinding.metadata.setOnClickListener(this); + queueControlBinding.songName.setSelected(true); + queueControlBinding.artistName.setSelected(true); } private void buildSeekBar() { - progressCurrentTime = rootView.findViewById(R.id.current_time); - progressSeekBar = rootView.findViewById(R.id.seek_bar); - 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); + queueControlBinding.seekBar.setOnSeekBarChangeListener(this); + queueControlBinding.liveSync.setOnClickListener(this); } private void buildControls() { - repeatButton = rootView.findViewById(R.id.control_repeat); - backwardButton = rootView.findViewById(R.id.control_backward); - fastRewindButton = rootView.findViewById(R.id.control_fast_rewind); - playPauseButton = rootView.findViewById(R.id.control_play_pause); - fastForwardButton = rootView.findViewById(R.id.control_fast_forward); - forwardButton = rootView.findViewById(R.id.control_forward); - shuffleButton = rootView.findViewById(R.id.control_shuffle); - 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); + queueControlBinding.controlRepeat.setOnClickListener(this); + queueControlBinding.controlBackward.setOnClickListener(this); + queueControlBinding.controlFastRewind.setOnClickListener(this); + queueControlBinding.controlPlayPause.setOnClickListener(this); + queueControlBinding.controlFastForward.setOnClickListener(this); + queueControlBinding.controlForward.setOnClickListener(this); + queueControlBinding.controlShuffle.setOnClickListener(this); } 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 && !player.getPlayQueue().isComplete()) { player.getPlayQueue().fetch(); - } else if (itemsList != null) { - itemsList.clearOnScrollListeners(); + } else { + queueControlBinding.playQueue.clearOnScrollListeners(); } } }; @@ -420,7 +359,7 @@ public abstract class ServicePlayerActivity extends AppCompatActivity @Override public void selected(final PlayQueueItem item, final View view) { 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 currentVisibleIndex; - if (itemsList.getLayoutManager() instanceof LinearLayoutManager) { - final LinearLayoutManager layout = ((LinearLayoutManager) itemsList.getLayoutManager()); + if (queueControlBinding.playQueue.getLayoutManager() instanceof LinearLayoutManager) { + final LinearLayoutManager layout = + (LinearLayoutManager) queueControlBinding.playQueue.getLayoutManager(); currentVisibleIndex = layout.findFirstVisibleItemPosition(); } else { currentVisibleIndex = 0; @@ -461,9 +401,9 @@ public abstract class ServicePlayerActivity extends AppCompatActivity final int distance = Math.abs(currentPlayingIndex - currentVisibleIndex); if (distance < SMOOTH_SCROLL_MAXIMUM_DISTANCE) { - itemsList.smoothScrollToPosition(currentPlayingIndex); + queueControlBinding.playQueue.smoothScrollToPosition(currentPlayingIndex); } else { - itemsList.scrollToPosition(currentPlayingIndex); + queueControlBinding.playQueue.scrollToPosition(currentPlayingIndex); } } @@ -477,23 +417,23 @@ public abstract class ServicePlayerActivity extends AppCompatActivity return; } - if (view.getId() == repeatButton.getId()) { + if (view.getId() == queueControlBinding.controlRepeat.getId()) { player.onRepeatClicked(); - } else if (view.getId() == backwardButton.getId()) { - player.onPlayPrevious(); - } else if (view.getId() == fastRewindButton.getId()) { - player.onFastRewind(); - } else if (view.getId() == playPauseButton.getId()) { - player.onPlayPause(); - } else if (view.getId() == fastForwardButton.getId()) { - player.onFastForward(); - } else if (view.getId() == forwardButton.getId()) { - player.onPlayNext(); - } else if (view.getId() == shuffleButton.getId()) { + } else if (view.getId() == queueControlBinding.controlBackward.getId()) { + player.playPrevious(); + } else if (view.getId() == queueControlBinding.controlFastRewind.getId()) { + player.fastRewind(); + } else if (view.getId() == queueControlBinding.controlPlayPause.getId()) { + player.playPause(); + } else if (view.getId() == queueControlBinding.controlFastForward.getId()) { + player.fastForward(); + } else if (view.getId() == queueControlBinding.controlForward.getId()) { + player.playNext(); + } else if (view.getId() == queueControlBinding.controlShuffle.getId()) { player.onShuffleClicked(); - } else if (view.getId() == metadata.getId()) { + } else if (view.getId() == queueControlBinding.metadata.getId()) { scrollToSelected(); - } else if (view.getId() == progressLiveSync.getId()) { + } else if (view.getId() == queueControlBinding.liveSync.getId()) { player.seekToDefault(); } } @@ -507,7 +447,7 @@ public abstract class ServicePlayerActivity extends AppCompatActivity return; } PlaybackParameterDialog.newInstance(player.getPlaybackSpeed(), player.getPlaybackPitch(), - player.getPlaybackSkipSilence(), this).show(getSupportFragmentManager(), getTag()); + player.getPlaybackSkipSilence(), this).show(getSupportFragmentManager(), TAG); } @Override @@ -527,15 +467,15 @@ public abstract class ServicePlayerActivity extends AppCompatActivity final boolean fromUser) { if (fromUser) { final String seekTime = Localization.getDurationString(progress / 1000); - progressCurrentTime.setText(seekTime); - seekDisplay.setText(seekTime); + queueControlBinding.currentTime.setText(seekTime); + queueControlBinding.seekDisplay.setText(seekTime); } } @Override public void onStartTrackingTouch(final SeekBar seekBar) { seeking = true; - seekDisplay.setVisibility(View.VISIBLE); + queueControlBinding.seekDisplay.setVisibility(View.VISIBLE); } @Override @@ -543,7 +483,7 @@ public abstract class ServicePlayerActivity extends AppCompatActivity if (player != null) { player.seekTo(seekBar.getProgress()); } - seekDisplay.setVisibility(View.GONE); + queueControlBinding.seekDisplay.setVisibility(View.GONE); seeking = false; } @@ -561,10 +501,8 @@ public abstract class ServicePlayerActivity extends AppCompatActivity final PlaylistAppendDialog d = PlaylistAppendDialog.fromPlayQueueItems(playlist); PlaylistAppendDialog.onPlaylistFound(getApplicationContext(), - () -> d.show(getSupportFragmentManager(), getTag()), - () -> PlaylistCreationDialog.newInstance(d) - .show(getSupportFragmentManager(), getTag() - )); + () -> d.show(getSupportFragmentManager(), TAG), + () -> PlaylistCreationDialog.newInstance(d).show(getSupportFragmentManager(), TAG)); } //////////////////////////////////////////////////////////////////////////// @@ -601,45 +539,46 @@ public abstract class ServicePlayerActivity extends AppCompatActivity public void onProgressUpdate(final int currentProgress, final int duration, final int bufferPercent) { // Set buffer progress - progressSeekBar.setSecondaryProgress((int) (progressSeekBar.getMax() + queueControlBinding.seekBar.setSecondaryProgress((int) (queueControlBinding.seekBar.getMax() * ((float) bufferPercent / 100))); // Set Duration - progressSeekBar.setMax(duration); - progressEndTime.setText(Localization.getDurationString(duration / 1000)); + queueControlBinding.seekBar.setMax(duration); + queueControlBinding.endTime.setText(Localization.getDurationString(duration / 1000)); // Set current time if not seeking if (!seeking) { - progressSeekBar.setProgress(currentProgress); - progressCurrentTime.setText(Localization.getDurationString(currentProgress / 1000)); + queueControlBinding.seekBar.setProgress(currentProgress); + queueControlBinding.currentTime.setText(Localization + .getDurationString(currentProgress / 1000)); } if (player != null) { - progressLiveSync.setClickable(!player.isLiveEdge()); + queueControlBinding.liveSync.setClickable(!player.isLiveEdge()); } - // this will make shure progressCurrentTime has the same width as progressEndTime - final ViewGroup.LayoutParams endTimeParams = progressEndTime.getLayoutParams(); - final ViewGroup.LayoutParams currentTimeParams = progressCurrentTime.getLayoutParams(); - currentTimeParams.width = progressEndTime.getWidth(); - progressCurrentTime.setLayoutParams(currentTimeParams); + // this will make sure progressCurrentTime has the same width as progressEndTime + final ViewGroup.LayoutParams currentTimeParams = + queueControlBinding.currentTime.getLayoutParams(); + currentTimeParams.width = queueControlBinding.endTime.getWidth(); + queueControlBinding.currentTime.setLayoutParams(currentTimeParams); } @Override public void onMetadataUpdate(final StreamInfo info, final PlayQueue queue) { if (info != null) { - metadataTitle.setText(info.getName()); - metadataArtist.setText(info.getUploaderName()); + queueControlBinding.songName.setText(info.getName()); + queueControlBinding.artistName.setText(info.getUploaderName()); - progressEndTime.setVisibility(View.GONE); - progressLiveSync.setVisibility(View.GONE); + queueControlBinding.endTime.setVisibility(View.GONE); + queueControlBinding.liveSync.setVisibility(View.GONE); switch (info.getStreamType()) { case LIVE_STREAM: case AUDIO_LIVE_STREAM: - progressLiveSync.setVisibility(View.VISIBLE); + queueControlBinding.liveSync.setVisibility(View.VISIBLE); break; default: - progressEndTime.setVisibility(View.VISIBLE); + queueControlBinding.endTime.setVisibility(View.VISIBLE); break; } @@ -659,50 +598,56 @@ public abstract class ServicePlayerActivity extends AppCompatActivity private void onStateChanged(final int state) { switch (state) { - case BasePlayer.STATE_PAUSED: - playPauseButton.setImageResource(R.drawable.ic_play_arrow_white_24dp); + case Player.STATE_PAUSED: + queueControlBinding.controlPlayPause + .setImageResource(R.drawable.ic_play_arrow_white_24dp); break; - case BasePlayer.STATE_PLAYING: - playPauseButton.setImageResource(R.drawable.ic_pause_white_24dp); + case Player.STATE_PLAYING: + queueControlBinding.controlPlayPause + .setImageResource(R.drawable.ic_pause_white_24dp); break; - case BasePlayer.STATE_COMPLETED: - playPauseButton.setImageResource(R.drawable.ic_replay_white_24dp); + case Player.STATE_COMPLETED: + queueControlBinding.controlPlayPause + .setImageResource(R.drawable.ic_replay_white_24dp); break; default: break; } switch (state) { - case BasePlayer.STATE_PAUSED: - case BasePlayer.STATE_PLAYING: - case BasePlayer.STATE_COMPLETED: - playPauseButton.setClickable(true); - playPauseButton.setVisibility(View.VISIBLE); - progressBar.setVisibility(View.GONE); + case Player.STATE_PAUSED: + case Player.STATE_PLAYING: + case Player.STATE_COMPLETED: + queueControlBinding.controlPlayPause.setClickable(true); + queueControlBinding.controlPlayPause.setVisibility(View.VISIBLE); + queueControlBinding.controlProgressBar.setVisibility(View.GONE); break; default: - playPauseButton.setClickable(false); - playPauseButton.setVisibility(View.INVISIBLE); - progressBar.setVisibility(View.VISIBLE); + queueControlBinding.controlPlayPause.setClickable(false); + queueControlBinding.controlPlayPause.setVisibility(View.INVISIBLE); + queueControlBinding.controlProgressBar.setVisibility(View.VISIBLE); break; } } private void onPlayModeChanged(final int repeatMode, final boolean shuffled) { switch (repeatMode) { - case Player.REPEAT_MODE_OFF: - repeatButton.setImageResource(R.drawable.exo_controls_repeat_off); + case com.google.android.exoplayer2.Player.REPEAT_MODE_OFF: + queueControlBinding.controlRepeat + .setImageResource(R.drawable.exo_controls_repeat_off); break; - case Player.REPEAT_MODE_ONE: - repeatButton.setImageResource(R.drawable.exo_controls_repeat_one); + case com.google.android.exoplayer2.Player.REPEAT_MODE_ONE: + queueControlBinding.controlRepeat + .setImageResource(R.drawable.exo_controls_repeat_one); break; - case Player.REPEAT_MODE_ALL: - repeatButton.setImageResource(R.drawable.exo_controls_repeat_all); + case com.google.android.exoplayer2.Player.REPEAT_MODE_ALL: + queueControlBinding.controlRepeat + .setImageResource(R.drawable.exo_controls_repeat_all); break; } final int shuffleAlpha = shuffled ? 255 : 77; - shuffleButton.setImageAlpha(shuffleAlpha); + queueControlBinding.controlShuffle.setImageAlpha(shuffleAlpha); } private void onPlaybackParameterChanged(final PlaybackParameters parameters) { @@ -715,12 +660,13 @@ public abstract class ServicePlayerActivity extends AppCompatActivity } private void onMaybePlaybackAdapterChanged() { - if (itemsList == null || player == null) { + if (player == null) { return; } final PlayQueueAdapter maybeNewAdapter = player.getPlayQueueAdapter(); - if (maybeNewAdapter != null && itemsList.getAdapter() != maybeNewAdapter) { - itemsList.setAdapter(maybeNewAdapter); + if (maybeNewAdapter != null + && 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 // using rootView.getContext() because getApplicationContext() didn't work - item.setIcon(ThemeHelper.resolveResourceIdFromAttr(rootView.getContext(), - player.isMuted() - ? R.attr.ic_volume_off - : R.attr.ic_volume_up)); + final Context context = queueControlBinding.getRoot().getContext(); + item.setIcon(ThemeHelper.resolveResourceIdFromAttr(context, + player.isMuted() ? R.attr.ic_volume_off : R.attr.ic_volume_up)); } } } diff --git a/app/src/main/java/org/schabi/newpipe/player/Player.java b/app/src/main/java/org/schabi/newpipe/player/Player.java new file mode 100644 index 000000000..4e2edaa10 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/player/Player.java @@ -0,0 +1,4075 @@ +package org.schabi.newpipe.player; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.ObjectAnimator; +import android.animation.PropertyValuesHolder; +import android.animation.ValueAnimator; +import android.annotation.SuppressLint; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.SharedPreferences; +import android.content.res.Resources; +import android.database.ContentObserver; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.Color; +import android.graphics.PorterDuff; +import android.graphics.PorterDuffColorFilter; +import android.media.AudioManager; +import android.net.Uri; +import android.os.Build; +import android.os.Handler; +import android.provider.Settings; +import android.util.DisplayMetrics; +import android.util.Log; +import android.util.TypedValue; +import android.view.GestureDetector; +import android.view.KeyEvent; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuItem; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewGroup; +import android.view.WindowManager; +import android.view.animation.AnticipateInterpolator; +import android.widget.FrameLayout; +import android.widget.ImageButton; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.PopupMenu; +import android.widget.ProgressBar; +import android.widget.RelativeLayout; +import android.widget.SeekBar; +import android.widget.TextView; +import android.widget.Toast; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.app.AppCompatActivity; +import androidx.appcompat.content.res.AppCompatResources; +import androidx.core.content.ContextCompat; +import androidx.core.view.DisplayCutoutCompat; +import androidx.core.view.ViewCompat; +import androidx.preference.PreferenceManager; +import androidx.recyclerview.widget.ItemTouchHelper; +import androidx.recyclerview.widget.RecyclerView; + +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.DefaultRenderersFactory; +import com.google.android.exoplayer2.ExoPlaybackException; +import com.google.android.exoplayer2.PlaybackParameters; +import com.google.android.exoplayer2.RenderersFactory; +import com.google.android.exoplayer2.SimpleExoPlayer; +import com.google.android.exoplayer2.Timeline; +import com.google.android.exoplayer2.source.BehindLiveWindowException; +import com.google.android.exoplayer2.source.MediaSource; +import com.google.android.exoplayer2.source.TrackGroup; +import com.google.android.exoplayer2.source.TrackGroupArray; +import com.google.android.exoplayer2.text.CaptionStyleCompat; +import com.google.android.exoplayer2.trackselection.TrackSelectionArray; +import com.google.android.exoplayer2.ui.AspectRatioFrameLayout; +import com.google.android.exoplayer2.ui.SubtitleView; +import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter; +import com.google.android.exoplayer2.video.VideoListener; +import com.google.android.material.floatingactionbutton.FloatingActionButton; +import com.nostra13.universalimageloader.core.ImageLoader; +import com.nostra13.universalimageloader.core.assist.FailReason; +import com.nostra13.universalimageloader.core.listener.ImageLoadingListener; + +import org.schabi.newpipe.DownloaderImpl; +import org.schabi.newpipe.MainActivity; +import org.schabi.newpipe.R; +import org.schabi.newpipe.databinding.PlayerBinding; +import org.schabi.newpipe.databinding.PlayerPopupCloseOverlayBinding; +import org.schabi.newpipe.extractor.MediaFormat; +import org.schabi.newpipe.extractor.stream.StreamInfo; +import org.schabi.newpipe.extractor.stream.StreamSegment; +import org.schabi.newpipe.extractor.stream.VideoStream; +import org.schabi.newpipe.fragments.OnScrollBelowItemsListener; +import org.schabi.newpipe.fragments.detail.VideoDetailFragment; +import org.schabi.newpipe.info_list.StreamSegmentAdapter; +import org.schabi.newpipe.local.history.HistoryRecordManager; +import org.schabi.newpipe.player.MainPlayer.PlayerType; +import org.schabi.newpipe.player.event.PlayerEventListener; +import org.schabi.newpipe.player.event.PlayerGestureListener; +import org.schabi.newpipe.player.event.PlayerServiceEventListener; +import org.schabi.newpipe.player.helper.AudioReactor; +import org.schabi.newpipe.player.helper.LoadController; +import org.schabi.newpipe.player.helper.MediaSessionManager; +import org.schabi.newpipe.player.helper.PlaybackParameterDialog; +import org.schabi.newpipe.player.helper.PlayerDataSource; +import org.schabi.newpipe.player.helper.PlayerHelper; +import org.schabi.newpipe.player.playback.CustomTrackSelector; +import org.schabi.newpipe.player.playback.MediaSourceManager; +import org.schabi.newpipe.player.playback.PlaybackListener; +import org.schabi.newpipe.player.playback.PlayerMediaSession; +import org.schabi.newpipe.player.playqueue.PlayQueue; +import org.schabi.newpipe.player.playqueue.PlayQueueAdapter; +import org.schabi.newpipe.player.playqueue.PlayQueueItem; +import org.schabi.newpipe.player.playqueue.PlayQueueItemBuilder; +import org.schabi.newpipe.player.playqueue.PlayQueueItemHolder; +import org.schabi.newpipe.player.playqueue.PlayQueueItemTouchCallback; +import org.schabi.newpipe.player.resolver.AudioPlaybackResolver; +import org.schabi.newpipe.player.resolver.MediaSourceTag; +import org.schabi.newpipe.player.resolver.VideoPlaybackResolver; +import org.schabi.newpipe.util.AnimationUtils; +import org.schabi.newpipe.util.DeviceUtils; +import org.schabi.newpipe.util.ImageDisplayConstants; +import org.schabi.newpipe.util.KoreUtil; +import org.schabi.newpipe.util.ListHelper; +import org.schabi.newpipe.util.NavigationHelper; +import org.schabi.newpipe.util.SerializedCache; +import org.schabi.newpipe.util.ShareUtils; +import org.schabi.newpipe.views.ExpandableSurfaceView; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; +import io.reactivex.rxjava3.core.Observable; +import io.reactivex.rxjava3.disposables.CompositeDisposable; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.disposables.SerialDisposable; + +import static com.google.android.exoplayer2.Player.DISCONTINUITY_REASON_AD_INSERTION; +import static com.google.android.exoplayer2.Player.DISCONTINUITY_REASON_INTERNAL; +import static com.google.android.exoplayer2.Player.DISCONTINUITY_REASON_PERIOD_TRANSITION; +import static com.google.android.exoplayer2.Player.DISCONTINUITY_REASON_SEEK; +import static com.google.android.exoplayer2.Player.DISCONTINUITY_REASON_SEEK_ADJUSTMENT; +import static com.google.android.exoplayer2.Player.DiscontinuityReason; +import static com.google.android.exoplayer2.Player.EventListener; +import static com.google.android.exoplayer2.Player.REPEAT_MODE_ALL; +import static com.google.android.exoplayer2.Player.REPEAT_MODE_OFF; +import static com.google.android.exoplayer2.Player.REPEAT_MODE_ONE; +import static com.google.android.exoplayer2.Player.RepeatMode; +import static java.util.concurrent.TimeUnit.MILLISECONDS; +import static org.schabi.newpipe.extractor.ServiceList.YouTube; +import static org.schabi.newpipe.player.MainPlayer.ACTION_CLOSE; +import static org.schabi.newpipe.player.MainPlayer.ACTION_FAST_FORWARD; +import static org.schabi.newpipe.player.MainPlayer.ACTION_FAST_REWIND; +import static org.schabi.newpipe.player.MainPlayer.ACTION_PLAY_NEXT; +import static org.schabi.newpipe.player.MainPlayer.ACTION_PLAY_PAUSE; +import static org.schabi.newpipe.player.MainPlayer.ACTION_PLAY_PREVIOUS; +import static org.schabi.newpipe.player.MainPlayer.ACTION_RECREATE_NOTIFICATION; +import static org.schabi.newpipe.player.MainPlayer.ACTION_REPEAT; +import static org.schabi.newpipe.player.MainPlayer.ACTION_SHUFFLE; +import static org.schabi.newpipe.player.helper.PlayerHelper.MinimizeMode.MINIMIZE_ON_EXIT_MODE_BACKGROUND; +import static org.schabi.newpipe.player.helper.PlayerHelper.MinimizeMode.MINIMIZE_ON_EXIT_MODE_NONE; +import static org.schabi.newpipe.player.helper.PlayerHelper.MinimizeMode.MINIMIZE_ON_EXIT_MODE_POPUP; +import static org.schabi.newpipe.player.helper.PlayerHelper.buildCloseOverlayLayoutParams; +import static org.schabi.newpipe.player.helper.PlayerHelper.formatSpeed; +import static org.schabi.newpipe.player.helper.PlayerHelper.getMinimizeOnExitAction; +import static org.schabi.newpipe.player.helper.PlayerHelper.getMinimumVideoHeight; +import static org.schabi.newpipe.player.helper.PlayerHelper.getTimeString; +import static org.schabi.newpipe.player.helper.PlayerHelper.globalScreenOrientationLocked; +import static org.schabi.newpipe.player.helper.PlayerHelper.isPlaybackResumeEnabled; +import static org.schabi.newpipe.player.helper.PlayerHelper.nextRepeatMode; +import static org.schabi.newpipe.player.helper.PlayerHelper.nextResizeModeAndSaveToPrefs; +import static org.schabi.newpipe.player.helper.PlayerHelper.retrievePlaybackParametersFromPrefs; +import static org.schabi.newpipe.player.helper.PlayerHelper.retrievePlayerTypeFromIntent; +import static org.schabi.newpipe.player.helper.PlayerHelper.retrievePopupLayoutParamsFromPrefs; +import static org.schabi.newpipe.player.helper.PlayerHelper.retrieveSeekDurationFromPreferences; +import static org.schabi.newpipe.player.helper.PlayerHelper.savePlaybackParametersToPrefs; +import static org.schabi.newpipe.util.AnimationUtils.Type.SLIDE_AND_ALPHA; +import static org.schabi.newpipe.util.AnimationUtils.animateRotation; +import static org.schabi.newpipe.util.AnimationUtils.animateView; +import static org.schabi.newpipe.util.ListHelper.getPopupResolutionIndex; +import static org.schabi.newpipe.util.ListHelper.getResolutionIndex; +import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage; +import static org.schabi.newpipe.util.Localization.containsCaseInsensitive; + +public final class Player implements + EventListener, + PlaybackListener, + ImageLoadingListener, + VideoListener, + SeekBar.OnSeekBarChangeListener, + View.OnClickListener, + PopupMenu.OnMenuItemClickListener, + PopupMenu.OnDismissListener, + View.OnLongClickListener { + public static final boolean DEBUG = MainActivity.DEBUG; + public static final String TAG = Player.class.getSimpleName(); + + /*////////////////////////////////////////////////////////////////////////// + // States + //////////////////////////////////////////////////////////////////////////*/ + + public static final int STATE_PREFLIGHT = -1; + public static final int STATE_BLOCKED = 123; + public static final int STATE_PLAYING = 124; + public static final int STATE_BUFFERING = 125; + public static final int STATE_PAUSED = 126; + public static final int STATE_PAUSED_SEEK = 127; + public static final int STATE_COMPLETED = 128; + + /*////////////////////////////////////////////////////////////////////////// + // Intent + //////////////////////////////////////////////////////////////////////////*/ + + public static final String REPEAT_MODE = "repeat_mode"; + public static final String PLAYBACK_QUALITY = "playback_quality"; + public static final String PLAY_QUEUE_KEY = "play_queue_key"; + public static final String APPEND_ONLY = "append_only"; + public static final String RESUME_PLAYBACK = "resume_playback"; + public static final String PLAY_WHEN_READY = "play_when_ready"; + public static final String SELECT_ON_APPEND = "select_on_append"; + public static final String PLAYER_TYPE = "player_type"; + public static final String IS_MUTED = "is_muted"; + + /*////////////////////////////////////////////////////////////////////////// + // Time constants + //////////////////////////////////////////////////////////////////////////*/ + + public static final int PLAY_PREV_ACTIVATION_LIMIT_MILLIS = 5000; // 5 seconds + public static final int PROGRESS_LOOP_INTERVAL_MILLIS = 500; // 500 millis + public static final int DEFAULT_CONTROLS_DURATION = 300; // 300 millis + public static final int DEFAULT_CONTROLS_HIDE_TIME = 2000; // 2 Seconds + public static final int DPAD_CONTROLS_HIDE_TIME = 7000; // 7 Seconds + + /*////////////////////////////////////////////////////////////////////////// + // Other constants + //////////////////////////////////////////////////////////////////////////*/ + + private static final float[] PLAYBACK_SPEEDS = {0.5f, 0.75f, 1.0f, 1.25f, 1.5f, 1.75f, 2.0f}; + + private static final int RENDERER_UNAVAILABLE = -1; + + /*////////////////////////////////////////////////////////////////////////// + // Playback + //////////////////////////////////////////////////////////////////////////*/ + + private PlayQueue playQueue; + private PlayQueueAdapter playQueueAdapter; + private StreamSegmentAdapter segmentAdapter; + + @Nullable private MediaSourceManager playQueueManager; + + @Nullable private PlayQueueItem currentItem; + @Nullable private MediaSourceTag currentMetadata; + @Nullable private Bitmap currentThumbnail; + + @Nullable private Toast errorToast; + + /*////////////////////////////////////////////////////////////////////////// + // Player + //////////////////////////////////////////////////////////////////////////*/ + + private SimpleExoPlayer simpleExoPlayer; + private AudioReactor audioReactor; + private MediaSessionManager mediaSessionManager; + + @NonNull private final CustomTrackSelector trackSelector; + @NonNull private final LoadController loadController; + @NonNull private final RenderersFactory renderFactory; + + @NonNull private final VideoPlaybackResolver videoResolver; + @NonNull private final AudioPlaybackResolver audioResolver; + + private final MainPlayer service; //TODO try to remove and replace everything with context + + /*////////////////////////////////////////////////////////////////////////// + // Player states + //////////////////////////////////////////////////////////////////////////*/ + + private PlayerType playerType = PlayerType.VIDEO; + private int currentState = STATE_PREFLIGHT; + + // audio only mode does not mean that player type is background, but that the player was + // minimized to background but will resume automatically to the original player type + private boolean isAudioOnly = false; + private boolean isPrepared = false; + private boolean wasPlaying = false; + private boolean isFullscreen = false; + private boolean isVerticalVideo = false; + private boolean fragmentIsVisible = false; + + private List availableStreams; + private int selectedStreamIndex; + + /*////////////////////////////////////////////////////////////////////////// + // Views + //////////////////////////////////////////////////////////////////////////*/ + + private PlayerBinding binding; + + private ValueAnimator controlViewAnimator; + private final Handler controlsVisibilityHandler = new Handler(); + + // fullscreen player + private boolean isQueueVisible = false; + private boolean areSegmentsVisible = false; + private ItemTouchHelper itemTouchHelper; + + /*////////////////////////////////////////////////////////////////////////// + // Popup menus ("popup" means that they pop up, not that they belong to the popup player) + //////////////////////////////////////////////////////////////////////////*/ + + private static final int POPUP_MENU_ID_QUALITY = 69; + private static final int POPUP_MENU_ID_PLAYBACK_SPEED = 79; + private static final int POPUP_MENU_ID_CAPTION = 89; + + private boolean isSomePopupMenuVisible = false; + private PopupMenu qualityPopupMenu; + private PopupMenu playbackSpeedPopupMenu; + private PopupMenu captionPopupMenu; + + /*////////////////////////////////////////////////////////////////////////// + // Popup player + //////////////////////////////////////////////////////////////////////////*/ + + private PlayerPopupCloseOverlayBinding closeOverlayBinding; + + private boolean isPopupClosing = false; + + private float screenWidth; + private float screenHeight; + + /*////////////////////////////////////////////////////////////////////////// + // Popup player window manager + //////////////////////////////////////////////////////////////////////////*/ + + public static final int IDLE_WINDOW_FLAGS = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE + | WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM; + public static final int ONGOING_PLAYBACK_WINDOW_FLAGS = IDLE_WINDOW_FLAGS + | WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON; + + @Nullable private WindowManager.LayoutParams popupLayoutParams; // null if player is not popup + @Nullable private final WindowManager windowManager; + + /*////////////////////////////////////////////////////////////////////////// + // Gestures + //////////////////////////////////////////////////////////////////////////*/ + + private static final float MAX_GESTURE_LENGTH = 0.75f; + + private int maxGestureLength; // scaled + private GestureDetector gestureDetector; + + /*////////////////////////////////////////////////////////////////////////// + // Listeners and disposables + //////////////////////////////////////////////////////////////////////////*/ + + private BroadcastReceiver broadcastReceiver; + private IntentFilter intentFilter; + private PlayerServiceEventListener fragmentListener; + private PlayerEventListener activityListener; + private ContentObserver settingsContentObserver; + + @NonNull private final SerialDisposable progressUpdateDisposable = new SerialDisposable(); + @NonNull private final CompositeDisposable databaseUpdateDisposable = new CompositeDisposable(); + + /*////////////////////////////////////////////////////////////////////////// + // Utils + //////////////////////////////////////////////////////////////////////////*/ + + @NonNull private final Context context; + @NonNull private final SharedPreferences prefs; + @NonNull private final HistoryRecordManager recordManager; + + + + /*////////////////////////////////////////////////////////////////////////// + // Constructor + //////////////////////////////////////////////////////////////////////////*/ + //region + + public Player(@NonNull final MainPlayer service) { + this.service = service; + context = service; + prefs = PreferenceManager.getDefaultSharedPreferences(context); + recordManager = new HistoryRecordManager(context); + + setupBroadcastReceiver(); + + trackSelector = new CustomTrackSelector(context, PlayerHelper.getQualitySelector()); + final PlayerDataSource dataSource = new PlayerDataSource(context, DownloaderImpl.USER_AGENT, + new DefaultBandwidthMeter.Builder(context).build()); + loadController = new LoadController(); + renderFactory = new DefaultRenderersFactory(context); + + videoResolver = new VideoPlaybackResolver(context, dataSource, getQualityResolver()); + audioResolver = new AudioPlaybackResolver(context, dataSource); + + windowManager = ContextCompat.getSystemService(context, WindowManager.class); + } + + private VideoPlaybackResolver.QualityResolver getQualityResolver() { + return new VideoPlaybackResolver.QualityResolver() { + @Override + public int getDefaultResolutionIndex(final List sortedVideos) { + return videoPlayerSelected() + ? ListHelper.getDefaultResolutionIndex(context, sortedVideos) + : ListHelper.getPopupDefaultResolutionIndex(context, sortedVideos); + } + + @Override + public int getOverrideResolutionIndex(final List sortedVideos, + final String playbackQuality) { + return videoPlayerSelected() + ? getResolutionIndex(context, sortedVideos, playbackQuality) + : getPopupResolutionIndex(context, sortedVideos, playbackQuality); + } + }; + } + //endregion + + + + /*////////////////////////////////////////////////////////////////////////// + // Setup and initialization + //////////////////////////////////////////////////////////////////////////*/ + //region + + public void setupFromView(@NonNull final PlayerBinding playerBinding) { + initViews(playerBinding); + if (exoPlayerIsNull()) { + initPlayer(true); + } + initListeners(); + } + + private void initViews(@NonNull final PlayerBinding playerBinding) { + binding = playerBinding; + setupSubtitleView(); + + binding.resizeTextView + .setText(PlayerHelper.resizeTypeOf(context, binding.surfaceView.getResizeMode())); + + binding.playbackSeekBar.getThumb() + .setColorFilter(new PorterDuffColorFilter(Color.RED, PorterDuff.Mode.SRC_IN)); + binding.playbackSeekBar.getProgressDrawable() + .setColorFilter(new PorterDuffColorFilter(Color.RED, PorterDuff.Mode.MULTIPLY)); + + qualityPopupMenu = new PopupMenu(context, binding.qualityTextView); + playbackSpeedPopupMenu = new PopupMenu(context, binding.playbackSpeed); + captionPopupMenu = new PopupMenu(context, binding.captionTextView); + + binding.progressBarLoadingPanel.getIndeterminateDrawable() + .setColorFilter(new PorterDuffColorFilter(Color.WHITE, PorterDuff.Mode.MULTIPLY)); + + binding.titleTextView.setSelected(true); + binding.channelTextView.setSelected(true); + + // Prevent hiding of bottom sheet via swipe inside queue + binding.itemsList.setNestedScrollingEnabled(false); + } + + private void initPlayer(final boolean playOnReady) { + if (DEBUG) { + Log.d(TAG, "initPlayer() called with: playOnReady = [" + playOnReady + "]"); + } + + simpleExoPlayer = new SimpleExoPlayer.Builder(context, renderFactory) + .setTrackSelector(trackSelector) + .setLoadControl(loadController) + .build(); + simpleExoPlayer.addListener(this); + simpleExoPlayer.setPlayWhenReady(playOnReady); + simpleExoPlayer.setSeekParameters(PlayerHelper.getSeekParameters(context)); + simpleExoPlayer.setWakeMode(C.WAKE_MODE_NETWORK); + simpleExoPlayer.setHandleAudioBecomingNoisy(true); + + audioReactor = new AudioReactor(context, simpleExoPlayer); + mediaSessionManager = new MediaSessionManager(context, simpleExoPlayer, + new PlayerMediaSession(this)); + + registerBroadcastReceiver(); + + // Setup video view + simpleExoPlayer.setVideoSurfaceView(binding.surfaceView); + simpleExoPlayer.addVideoListener(this); + + // Setup subtitle view + simpleExoPlayer.addTextOutput(binding.subtitleView); + + // Setup audio session with onboard equalizer + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + trackSelector.setParameters(trackSelector.buildUponParameters() + .setTunnelingAudioSessionId(C.generateAudioSessionIdV21(context))); + } + } + + private void initListeners() { + binding.playbackSeekBar.setOnSeekBarChangeListener(this); + binding.playbackSpeed.setOnClickListener(this); + binding.qualityTextView.setOnClickListener(this); + binding.captionTextView.setOnClickListener(this); + binding.resizeTextView.setOnClickListener(this); + binding.playbackLiveSync.setOnClickListener(this); + + final PlayerGestureListener listener = new PlayerGestureListener(this, service); + gestureDetector = new GestureDetector(context, listener); + binding.getRoot().setOnTouchListener(listener); + + binding.queueButton.setOnClickListener(this); + binding.segmentsButton.setOnClickListener(this); + binding.repeatButton.setOnClickListener(this); + binding.shuffleButton.setOnClickListener(this); + + binding.playPauseButton.setOnClickListener(this); + binding.playPreviousButton.setOnClickListener(this); + binding.playNextButton.setOnClickListener(this); + + binding.moreOptionsButton.setOnClickListener(this); + binding.moreOptionsButton.setOnLongClickListener(this); + binding.share.setOnClickListener(this); + binding.fullScreenButton.setOnClickListener(this); + binding.screenRotationButton.setOnClickListener(this); + binding.playWithKodi.setOnClickListener(this); + binding.openInBrowser.setOnClickListener(this); + binding.playerCloseButton.setOnClickListener(this); + binding.switchMute.setOnClickListener(this); + + settingsContentObserver = new ContentObserver(new Handler()) { + @Override + public void onChange(final boolean selfChange) { + setupScreenRotationButton(); + } + }; + context.getContentResolver().registerContentObserver( + Settings.System.getUriFor(Settings.System.ACCELEROMETER_ROTATION), false, + settingsContentObserver); + binding.getRoot().addOnLayoutChangeListener(this::onLayoutChange); + + ViewCompat.setOnApplyWindowInsetsListener(binding.itemsListPanel, (view, windowInsets) -> { + final DisplayCutoutCompat cutout = windowInsets.getDisplayCutout(); + if (cutout != null) { + view.setPadding(cutout.getSafeInsetLeft(), cutout.getSafeInsetTop(), + cutout.getSafeInsetRight(), cutout.getSafeInsetBottom()); + } + return windowInsets; + }); + + // PlaybackControlRoot already consumed window insets but we should pass them to + // player_overlays too. Without it they will be off-centered + binding.playbackControlRoot.addOnLayoutChangeListener( + (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> + binding.playerOverlays.setPadding( + v.getPaddingLeft(), + v.getPaddingTop(), + v.getPaddingRight(), + v.getPaddingBottom())); + } + //endregion + + + + /*////////////////////////////////////////////////////////////////////////// + // Playback initialization via intent + //////////////////////////////////////////////////////////////////////////*/ + //region + + public void handleIntent(@NonNull final Intent intent) { + // fail fast if no play queue was provided + final String queueCache = intent.getStringExtra(PLAY_QUEUE_KEY); + if (queueCache == null) { + return; + } + final PlayQueue newQueue = SerializedCache.getInstance().take(queueCache, PlayQueue.class); + if (newQueue == null) { + return; + } + + final PlayerType oldPlayerType = playerType; + playerType = retrievePlayerTypeFromIntent(intent); + // We need to setup audioOnly before super(), see "sourceOf" + isAudioOnly = audioPlayerSelected(); + + if (intent.hasExtra(PLAYBACK_QUALITY)) { + setPlaybackQuality(intent.getStringExtra(PLAYBACK_QUALITY)); + } + + // Resolve append intents + if (intent.getBooleanExtra(APPEND_ONLY, false) && playQueue != null) { + final int sizeBeforeAppend = playQueue.size(); + playQueue.append(newQueue.getStreams()); + + if ((intent.getBooleanExtra(SELECT_ON_APPEND, false) + || currentState == STATE_COMPLETED) && newQueue.getStreams().size() > 0) { + playQueue.setIndex(sizeBeforeAppend); + } + + return; + } + + final PlaybackParameters savedParameters = retrievePlaybackParametersFromPrefs(this); + final float playbackSpeed = savedParameters.speed; + final float playbackPitch = savedParameters.pitch; + final boolean playbackSkipSilence = savedParameters.skipSilence; + + final boolean samePlayQueue = playQueue != null && playQueue.equals(newQueue); + final int repeatMode = intent.getIntExtra(REPEAT_MODE, getRepeatMode()); + final boolean playWhenReady = intent.getBooleanExtra(PLAY_WHEN_READY, true); + final boolean isMuted = intent.getBooleanExtra(IS_MUTED, isMuted()); + + /* + * There are 3 situations when playback shouldn't be started from scratch (zero timestamp): + * 1. User pressed on a timestamp link and the same video should be rewound to the timestamp + * 2. User changed a player from, for example. main to popup, or from audio to main, etc + * 3. User chose to resume a video based on a saved timestamp from history of played videos + * In those cases time will be saved because re-init of the play queue is a not an instant + * task and requires network calls + * */ + // seek to timestamp if stream is already playing + if (!exoPlayerIsNull() + && newQueue.size() == 1 && newQueue.getItem() != null + && playQueue != null && playQueue.size() == 1 && playQueue.getItem() != null + && newQueue.getItem().getUrl().equals(playQueue.getItem().getUrl()) + && newQueue.getItem().getRecoveryPosition() != PlayQueueItem.RECOVERY_UNSET) { + // Player can have state = IDLE when playback is stopped or failed + // and we should retry() in this case + if (simpleExoPlayer.getPlaybackState() + == com.google.android.exoplayer2.Player.STATE_IDLE) { + simpleExoPlayer.retry(); + } + simpleExoPlayer.seekTo(playQueue.getIndex(), newQueue.getItem().getRecoveryPosition()); + simpleExoPlayer.setPlayWhenReady(playWhenReady); + + } else if (!exoPlayerIsNull() + && samePlayQueue + && playQueue != null + && !playQueue.isDisposed()) { + // Do not re-init the same PlayQueue. Save time + // Player can have state = IDLE when playback is stopped or failed + // and we should retry() in this case + if (simpleExoPlayer.getPlaybackState() + == com.google.android.exoplayer2.Player.STATE_IDLE) { + simpleExoPlayer.retry(); + } + simpleExoPlayer.setPlayWhenReady(playWhenReady); + + } else if (intent.getBooleanExtra(RESUME_PLAYBACK, false) + && isPlaybackResumeEnabled(this) + && !samePlayQueue + && !newQueue.isEmpty() + && newQueue.getItem().getRecoveryPosition() == PlayQueueItem.RECOVERY_UNSET) { + databaseUpdateDisposable.add(recordManager.loadStreamState(newQueue.getItem()) + .observeOn(AndroidSchedulers.mainThread()) + // Do not place initPlayback() in doFinally() because + // it restarts playback after destroy() + //.doFinally() + .subscribe( + state -> { + newQueue.setRecovery(newQueue.getIndex(), state.getProgressTime()); + initPlayback(newQueue, repeatMode, playbackSpeed, playbackPitch, + playbackSkipSilence, playWhenReady, isMuted); + }, + error -> { + if (DEBUG) { + error.printStackTrace(); + } + // In case any error we can start playback without history + initPlayback(newQueue, repeatMode, playbackSpeed, playbackPitch, + playbackSkipSilence, playWhenReady, isMuted); + }, + () -> { + // Completed but not found in history + initPlayback(newQueue, repeatMode, playbackSpeed, playbackPitch, + playbackSkipSilence, playWhenReady, isMuted); + } + )); + } else { + // Good to go... + // In a case of equal PlayQueues we can re-init old one but only when it is disposed + initPlayback(samePlayQueue ? playQueue : newQueue, repeatMode, playbackSpeed, + playbackPitch, playbackSkipSilence, playWhenReady, isMuted); + } + + if (oldPlayerType != playerType && playQueue != null) { + // If playerType changes from one to another we should reload the player + // (to disable/enable video stream or to set quality) + setRecovery(); + reloadPlayQueueManager(); + } + + setupElementsVisibility(); + setupElementsSize(); + + if (audioPlayerSelected()) { + service.removeViewFromParent(); + } else if (popupPlayerSelected()) { + binding.getRoot().setVisibility(View.VISIBLE); + initPopup(); + initPopupCloseOverlay(); + binding.playPauseButton.requestFocus(); + } else { + binding.getRoot().setVisibility(View.VISIBLE); + initVideoPlayer(); + closeItemsList(); + // Android TV: without it focus will frame the whole player + binding.playPauseButton.requestFocus(); + + if (simpleExoPlayer.getPlayWhenReady()) { + play(); + } else { + pause(); + } + } + NavigationHelper.sendPlayerStartedEvent(context); + } + + private void initPlayback(@NonNull final PlayQueue queue, + @RepeatMode final int repeatMode, + final float playbackSpeed, + final float playbackPitch, + final boolean playbackSkipSilence, + final boolean playOnReady, + final boolean isMuted) { + destroyPlayer(); + initPlayer(playOnReady); + setRepeatMode(repeatMode); + setPlaybackParameters(playbackSpeed, playbackPitch, playbackSkipSilence); + + playQueue = queue; + playQueue.init(); + reloadPlayQueueManager(); + + if (playQueueAdapter != null) { + playQueueAdapter.dispose(); + } + playQueueAdapter = new PlayQueueAdapter(context, playQueue); + segmentAdapter = new StreamSegmentAdapter(getStreamSegmentListener()); + + simpleExoPlayer.setVolume(isMuted ? 0 : 1); + notifyQueueUpdateToListeners(); + } + //endregion + + + + /*////////////////////////////////////////////////////////////////////////// + // Destroy and recovery + //////////////////////////////////////////////////////////////////////////*/ + //region + + private void destroyPlayer() { + if (DEBUG) { + Log.d(TAG, "destroyPlayer() called"); + } + if (!exoPlayerIsNull()) { + simpleExoPlayer.removeListener(this); + simpleExoPlayer.stop(); + simpleExoPlayer.release(); + } + if (isProgressLoopRunning()) { + stopProgressLoop(); + } + if (playQueue != null) { + playQueue.dispose(); + } + if (audioReactor != null) { + audioReactor.dispose(); + } + if (playQueueManager != null) { + playQueueManager.dispose(); + } + if (mediaSessionManager != null) { + mediaSessionManager.dispose(); + } + + if (playQueueAdapter != null) { + playQueueAdapter.unsetSelectedListener(); + playQueueAdapter.dispose(); + } + } + + public void destroy() { + if (DEBUG) { + Log.d(TAG, "destroy() called"); + } + destroyPlayer(); + unregisterBroadcastReceiver(); + + databaseUpdateDisposable.clear(); + progressUpdateDisposable.set(null); + ImageLoader.getInstance().stop(); + + if (binding != null) { + binding.endScreen.setImageBitmap(null); + } + + context.getContentResolver().unregisterContentObserver(settingsContentObserver); + } + + public void setRecovery() { + if (playQueue == null || exoPlayerIsNull()) { + return; + } + + final int queuePos = playQueue.getIndex(); + final long windowPos = simpleExoPlayer.getCurrentPosition(); + + if (windowPos > 0 && windowPos <= simpleExoPlayer.getDuration()) { + setRecovery(queuePos, windowPos); + } + } + + private void setRecovery(final int queuePos, final long windowPos) { + if (playQueue.size() <= queuePos) { + return; + } + + if (DEBUG) { + Log.d(TAG, "Setting recovery, queue: " + queuePos + ", pos: " + windowPos); + } + playQueue.setRecovery(queuePos, windowPos); + } + + private void reloadPlayQueueManager() { + if (playQueueManager != null) { + playQueueManager.dispose(); + } + + if (playQueue != null) { + playQueueManager = new MediaSourceManager(this, playQueue); + } + } + + @Override // own playback listener + public void onPlaybackShutdown() { + if (DEBUG) { + Log.d(TAG, "onPlaybackShutdown() called"); + } + // destroys the service, which in turn will destroy the player + service.onDestroy(); + } + + public void smoothStopPlayer() { + // Pausing would make transition from one stream to a new stream not smooth, so only stop + simpleExoPlayer.stop(false); + } + //endregion + + + + /*////////////////////////////////////////////////////////////////////////// + // Player type specific setup + //////////////////////////////////////////////////////////////////////////*/ + //region + + private void initVideoPlayer() { + // restore last resize mode + setResizeMode(prefs.getInt(context.getString(R.string.last_resize_mode), + AspectRatioFrameLayout.RESIZE_MODE_FIT)); + binding.getRoot().setLayoutParams(new FrameLayout.LayoutParams( + FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT)); + } + + @SuppressLint("RtlHardcoded") + private void initPopup() { + if (DEBUG) { + Log.d(TAG, "initPopup() called"); + } + + // Popup is already added to windowManager + if (popupHasParent()) { + return; + } + + updateScreenSize(); + + popupLayoutParams = retrievePopupLayoutParamsFromPrefs(this); + binding.surfaceView.setHeights(popupLayoutParams.height, popupLayoutParams.height); + + checkPopupPositionBounds(); + + binding.loadingPanel.setMinimumWidth(popupLayoutParams.width); + binding.loadingPanel.setMinimumHeight(popupLayoutParams.height); + + service.removeViewFromParent(); + Objects.requireNonNull(windowManager).addView(binding.getRoot(), popupLayoutParams); + + // Popup doesn't have aspectRatio selector, using FIT automatically + setResizeMode(AspectRatioFrameLayout.RESIZE_MODE_FIT); + } + + @SuppressLint("RtlHardcoded") + private void initPopupCloseOverlay() { + if (DEBUG) { + Log.d(TAG, "initPopupCloseOverlay() called"); + } + + // closeOverlayView is already added to windowManager + if (closeOverlayBinding != null) { + return; + } + + closeOverlayBinding = PlayerPopupCloseOverlayBinding.inflate(LayoutInflater.from(context)); + + final WindowManager.LayoutParams closeOverlayLayoutParams = buildCloseOverlayLayoutParams(); + closeOverlayBinding.closeButton.setVisibility(View.GONE); + Objects.requireNonNull(windowManager).addView( + closeOverlayBinding.getRoot(), closeOverlayLayoutParams); + } + //endregion + + + + /*////////////////////////////////////////////////////////////////////////// + // Elements visibility and size: popup and main players have different look + //////////////////////////////////////////////////////////////////////////*/ + //region + + /** + * This method ensures that popup and main players have different look. + * We use one layout for both players and need to decide what to show and what to hide. + * Additional measuring should be done inside {@link #setupElementsSize}. + */ + private void setupElementsVisibility() { + if (popupPlayerSelected()) { + binding.fullScreenButton.setVisibility(View.VISIBLE); + binding.screenRotationButton.setVisibility(View.GONE); + binding.resizeTextView.setVisibility(View.GONE); + binding.getRoot().findViewById(R.id.metadataView).setVisibility(View.GONE); + binding.queueButton.setVisibility(View.GONE); + binding.segmentsButton.setVisibility(View.GONE); + binding.moreOptionsButton.setVisibility(View.GONE); + binding.topControls.setOrientation(LinearLayout.HORIZONTAL); + binding.primaryControls.getLayoutParams().width + = LinearLayout.LayoutParams.WRAP_CONTENT; + binding.secondaryControls.setAlpha(1.0f); + binding.secondaryControls.setVisibility(View.VISIBLE); + binding.secondaryControls.setTranslationY(0); + binding.share.setVisibility(View.GONE); + binding.playWithKodi.setVisibility(View.GONE); + binding.openInBrowser.setVisibility(View.GONE); + binding.switchMute.setVisibility(View.GONE); + binding.playerCloseButton.setVisibility(View.GONE); + binding.topControls.bringToFront(); + binding.topControls.setClickable(false); + binding.topControls.setFocusable(false); + binding.bottomControls.bringToFront(); + closeItemsList(); + } else if (videoPlayerSelected()) { + binding.fullScreenButton.setVisibility(View.GONE); + setupScreenRotationButton(); + binding.resizeTextView.setVisibility(View.VISIBLE); + binding.getRoot().findViewById(R.id.metadataView).setVisibility(View.VISIBLE); + binding.moreOptionsButton.setVisibility(View.VISIBLE); + binding.topControls.setOrientation(LinearLayout.VERTICAL); + binding.primaryControls.getLayoutParams().width + = LinearLayout.LayoutParams.MATCH_PARENT; + binding.secondaryControls.setVisibility(View.INVISIBLE); + binding.moreOptionsButton.setImageDrawable(AppCompatResources.getDrawable(context, + R.drawable.ic_expand_more_white_24dp)); + binding.share.setVisibility(View.VISIBLE); + binding.openInBrowser.setVisibility(View.VISIBLE); + binding.switchMute.setVisibility(View.VISIBLE); + binding.playerCloseButton.setVisibility(isFullscreen ? View.GONE : View.VISIBLE); + // Top controls have a large minHeight which is allows to drag the player + // down in fullscreen mode (just larger area to make easy to locate by finger) + binding.topControls.setClickable(true); + binding.topControls.setFocusable(true); + } + showHideKodiButton(); + + if (isFullscreen) { + binding.titleTextView.setVisibility(View.VISIBLE); + binding.channelTextView.setVisibility(View.VISIBLE); + } else { + binding.titleTextView.setVisibility(View.GONE); + binding.channelTextView.setVisibility(View.GONE); + } + setMuteButton(binding.switchMute, isMuted()); + + animateRotation(binding.moreOptionsButton, DEFAULT_CONTROLS_DURATION, 0); + } + + /** + * Changes padding, size of elements based on player selected right now. + * Popup player has small padding in comparison with the main player + */ + private void setupElementsSize() { + final Resources res = context.getResources(); + final int buttonsMinWidth; + final int playerTopPad; + final int controlsPad; + final int buttonsPad; + + if (popupPlayerSelected()) { + buttonsMinWidth = 0; + playerTopPad = 0; + controlsPad = res.getDimensionPixelSize(R.dimen.player_popup_controls_padding); + buttonsPad = res.getDimensionPixelSize(R.dimen.player_popup_buttons_padding); + } else if (videoPlayerSelected()) { + buttonsMinWidth = res.getDimensionPixelSize(R.dimen.player_main_buttons_min_width); + playerTopPad = res.getDimensionPixelSize(R.dimen.player_main_top_padding); + controlsPad = res.getDimensionPixelSize(R.dimen.player_main_controls_padding); + buttonsPad = res.getDimensionPixelSize(R.dimen.player_main_buttons_padding); + } else { + return; + } + + binding.topControls.setPaddingRelative(controlsPad, playerTopPad, controlsPad, 0); + binding.bottomControls.setPaddingRelative(controlsPad, 0, controlsPad, 0); + binding.qualityTextView.setPadding(buttonsPad, buttonsPad, buttonsPad, buttonsPad); + binding.playbackSpeed.setPadding(buttonsPad, buttonsPad, buttonsPad, buttonsPad); + binding.playbackSpeed.setMinimumWidth(buttonsMinWidth); + binding.captionTextView.setPadding(buttonsPad, buttonsPad, buttonsPad, buttonsPad); + } + + private void showHideKodiButton() { + // show kodi button if it supports the current service and it is enabled in settings + binding.playWithKodi.setVisibility(videoPlayerSelected() + && playQueue != null && playQueue.getItem() != null + && KoreUtil.shouldShowPlayWithKodi(context, playQueue.getItem().getServiceId()) + ? View.VISIBLE : View.GONE); + } + //endregion + + + + /*////////////////////////////////////////////////////////////////////////// + // Broadcast receiver + //////////////////////////////////////////////////////////////////////////*/ + //region + + private void setupBroadcastReceiver() { + if (DEBUG) { + Log.d(TAG, "setupBroadcastReceiver() called"); + } + + broadcastReceiver = new BroadcastReceiver() { + @Override + public void onReceive(final Context ctx, final Intent intent) { + onBroadcastReceived(intent); + } + }; + intentFilter = new IntentFilter(); + + intentFilter.addAction(AudioManager.ACTION_AUDIO_BECOMING_NOISY); + + intentFilter.addAction(ACTION_CLOSE); + intentFilter.addAction(ACTION_PLAY_PAUSE); + intentFilter.addAction(ACTION_PLAY_PREVIOUS); + intentFilter.addAction(ACTION_PLAY_NEXT); + intentFilter.addAction(ACTION_FAST_REWIND); + intentFilter.addAction(ACTION_FAST_FORWARD); + intentFilter.addAction(ACTION_REPEAT); + intentFilter.addAction(ACTION_SHUFFLE); + intentFilter.addAction(ACTION_RECREATE_NOTIFICATION); + + intentFilter.addAction(VideoDetailFragment.ACTION_VIDEO_FRAGMENT_RESUMED); + intentFilter.addAction(VideoDetailFragment.ACTION_VIDEO_FRAGMENT_STOPPED); + + intentFilter.addAction(Intent.ACTION_CONFIGURATION_CHANGED); + intentFilter.addAction(Intent.ACTION_SCREEN_ON); + intentFilter.addAction(Intent.ACTION_SCREEN_OFF); + intentFilter.addAction(Intent.ACTION_HEADSET_PLUG); + } + + private void onBroadcastReceived(final Intent intent) { + if (intent == null || intent.getAction() == null) { + return; + } + + if (DEBUG) { + Log.d(TAG, "onBroadcastReceived() called with: intent = [" + intent + "]"); + } + + switch (intent.getAction()) { + case AudioManager.ACTION_AUDIO_BECOMING_NOISY: + pause(); + break; + case ACTION_CLOSE: + service.onDestroy(); + break; + case ACTION_PLAY_PAUSE: + playPause(); + if (!fragmentIsVisible) { + // Ensure that we have audio-only stream playing when a user + // started to play from notification's play button from outside of the app + onFragmentStopped(); + } + break; + case ACTION_PLAY_PREVIOUS: + playPrevious(); + break; + case ACTION_PLAY_NEXT: + playNext(); + break; + case ACTION_FAST_REWIND: + fastRewind(); + break; + case ACTION_FAST_FORWARD: + fastForward(); + break; + case ACTION_REPEAT: + onRepeatClicked(); + break; + case ACTION_SHUFFLE: + onShuffleClicked(); + break; + case ACTION_RECREATE_NOTIFICATION: + NotificationUtil.getInstance().createNotificationIfNeededAndUpdate(this, true); + break; + case VideoDetailFragment.ACTION_VIDEO_FRAGMENT_RESUMED: + fragmentIsVisible = true; + useVideoSource(true); + break; + case VideoDetailFragment.ACTION_VIDEO_FRAGMENT_STOPPED: + fragmentIsVisible = false; + onFragmentStopped(); + break; + case Intent.ACTION_CONFIGURATION_CHANGED: + assureCorrectAppLanguage(service); + if (DEBUG) { + Log.d(TAG, "onConfigurationChanged() called"); + } + if (popupPlayerSelected()) { + updateScreenSize(); + changePopupSize(popupLayoutParams.width); + checkPopupPositionBounds(); + } + // Close it because when changing orientation from portrait + // (in fullscreen mode) the size of queue layout can be larger than the screen size + closeItemsList(); + break; + case Intent.ACTION_SCREEN_ON: + // Interrupt playback only when screen turns on + // and user is watching video in popup player. + // Same actions for video player will be handled in ACTION_VIDEO_FRAGMENT_RESUMED + if (popupPlayerSelected() && (isPlaying() || isLoading())) { + useVideoSource(true); + } + break; + case Intent.ACTION_SCREEN_OFF: + // Interrupt playback only when screen turns off with popup player working + if (popupPlayerSelected() && (isPlaying() || isLoading())) { + useVideoSource(false); + } + break; + case Intent.ACTION_HEADSET_PLUG: //FIXME + /*notificationManager.cancel(NOTIFICATION_ID); + mediaSessionManager.dispose(); + mediaSessionManager.enable(getBaseContext(), basePlayerImpl.simpleExoPlayer);*/ + break; + } + } + + private void registerBroadcastReceiver() { + // Try to unregister current first + unregisterBroadcastReceiver(); + context.registerReceiver(broadcastReceiver, intentFilter); + } + + private void unregisterBroadcastReceiver() { + try { + context.unregisterReceiver(broadcastReceiver); + } catch (final IllegalArgumentException unregisteredException) { + Log.w(TAG, "Broadcast receiver already unregistered: " + + unregisteredException.getMessage()); + } + } + //endregion + + + + /*////////////////////////////////////////////////////////////////////////// + // Thumbnail loading + //////////////////////////////////////////////////////////////////////////*/ + //region + + private void initThumbnail(final String url) { + if (DEBUG) { + Log.d(TAG, "Thumbnail - initThumbnail() called"); + } + if (url == null || url.isEmpty()) { + return; + } + ImageLoader.getInstance().resume(); + ImageLoader.getInstance() + .loadImage(url, ImageDisplayConstants.DISPLAY_THUMBNAIL_OPTIONS, this); + } + + @Override + public void onLoadingStarted(final String imageUri, final View view) { + if (DEBUG) { + Log.d(TAG, "Thumbnail - onLoadingStarted() called on: " + + "imageUri = [" + imageUri + "], view = [" + view + "]"); + } + } + + @Override + public void onLoadingFailed(final String imageUri, final View view, + final FailReason failReason) { + Log.e(TAG, "Thumbnail - onLoadingFailed() called on imageUri = [" + imageUri + "]", + failReason.getCause()); + currentThumbnail = null; + NotificationUtil.getInstance().createNotificationIfNeededAndUpdate(this, false); + } + + @Override + public void onLoadingComplete(final String imageUri, final View view, + final Bitmap loadedImage) { + final float width = Math.min( + context.getResources().getDimension(R.dimen.player_notification_thumbnail_width), + loadedImage.getWidth()); + + if (DEBUG) { + Log.d(TAG, "Thumbnail - onLoadingComplete() called with: " + + "imageUri = [" + imageUri + "], view = [" + view + "], " + + "loadedImage = [" + loadedImage + "], " + + loadedImage.getWidth() + "x" + loadedImage.getHeight() + + ", scaled width = " + width); + } + + currentThumbnail = Bitmap.createScaledBitmap(loadedImage, + (int) width, + (int) (loadedImage.getHeight() / (loadedImage.getWidth() / width)), true); + binding.endScreen.setImageBitmap(loadedImage); + NotificationUtil.getInstance().createNotificationIfNeededAndUpdate(this, false); + } + + @Override + public void onLoadingCancelled(final String imageUri, final View view) { + if (DEBUG) { + Log.d(TAG, "Thumbnail - onLoadingCancelled() called with: " + + "imageUri = [" + imageUri + "], view = [" + view + "]"); + } + currentThumbnail = null; + NotificationUtil.getInstance().createNotificationIfNeededAndUpdate(this, false); + } + //endregion + + + + /*////////////////////////////////////////////////////////////////////////// + // Popup player utils + //////////////////////////////////////////////////////////////////////////*/ + //region + + /** + * Check if {@link #popupLayoutParams}' position is within a arbitrary boundary + * that goes from (0, 0) to (screenWidth, screenHeight). + *

+ * If it's out of these boundaries, {@link #popupLayoutParams}' position is changed + * and {@code true} is returned to represent this change. + *

+ */ + public void checkPopupPositionBounds() { + if (DEBUG) { + Log.d(TAG, "checkPopupPositionBounds() called with: " + + "screenWidth = [" + screenWidth + "], " + + "screenHeight = [" + screenHeight + "]"); + } + if (popupLayoutParams == null) { + return; + } + + if (popupLayoutParams.x < 0) { + popupLayoutParams.x = 0; + } else if (popupLayoutParams.x > screenWidth - popupLayoutParams.width) { + popupLayoutParams.x = (int) (screenWidth - popupLayoutParams.width); + } + + if (popupLayoutParams.y < 0) { + popupLayoutParams.y = 0; + } else if (popupLayoutParams.y > screenHeight - popupLayoutParams.height) { + popupLayoutParams.y = (int) (screenHeight - popupLayoutParams.height); + } + } + + public void updateScreenSize() { + if (windowManager != null) { + final DisplayMetrics metrics = new DisplayMetrics(); + windowManager.getDefaultDisplay().getMetrics(metrics); + + screenWidth = metrics.widthPixels; + screenHeight = metrics.heightPixels; + if (DEBUG) { + Log.d(TAG, "updateScreenSize() called: screenWidth = [" + + screenWidth + "], screenHeight = [" + screenHeight + "]"); + } + } + } + + /** + * Changes the size of the popup based on the width. + * @param width the new width, height is calculated with + * {@link PlayerHelper#getMinimumVideoHeight(float)} + */ + public void changePopupSize(final int width) { + if (DEBUG) { + Log.d(TAG, "changePopupSize() called with: width = [" + width + "]"); + } + + if (anyPopupViewIsNull()) { + return; + } + + final float minimumWidth = context.getResources().getDimension(R.dimen.popup_minimum_width); + final int actualWidth = (int) (width > screenWidth ? screenWidth + : (width < minimumWidth ? minimumWidth : width)); + final int actualHeight = (int) getMinimumVideoHeight(width); + if (DEBUG) { + Log.d(TAG, "updatePopupSize() updated values:" + + " width = [" + actualWidth + "], height = [" + actualHeight + "]"); + } + + popupLayoutParams.width = actualWidth; + popupLayoutParams.height = actualHeight; + binding.surfaceView.setHeights(popupLayoutParams.height, popupLayoutParams.height); + Objects.requireNonNull(windowManager) + .updateViewLayout(binding.getRoot(), popupLayoutParams); + } + + private void changePopupWindowFlags(final int flags) { + if (DEBUG) { + Log.d(TAG, "changePopupWindowFlags() called with: flags = [" + flags + "]"); + } + + if (!anyPopupViewIsNull()) { + popupLayoutParams.flags = flags; + Objects.requireNonNull(windowManager) + .updateViewLayout(binding.getRoot(), popupLayoutParams); + } + } + + public void closePopup() { + if (DEBUG) { + Log.d(TAG, "closePopup() called, isPopupClosing = " + isPopupClosing); + } + if (isPopupClosing) { + return; + } + isPopupClosing = true; + + saveStreamProgressState(); + Objects.requireNonNull(windowManager).removeView(binding.getRoot()); + + animatePopupOverlayAndFinishService(); + } + + public void removePopupFromView() { + if (windowManager != null) { + final boolean isCloseOverlayHasParent = closeOverlayBinding != null + && closeOverlayBinding.closeButton.getParent() != null; + if (popupHasParent()) { + windowManager.removeView(binding.getRoot()); + } + if (isCloseOverlayHasParent) { + windowManager.removeView(closeOverlayBinding.getRoot()); + } + } + } + + private void animatePopupOverlayAndFinishService() { + final int targetTranslationY = + (int) (closeOverlayBinding.closeButton.getRootView().getHeight() + - closeOverlayBinding.closeButton.getY()); + + closeOverlayBinding.closeButton.animate().setListener(null).cancel(); + closeOverlayBinding.closeButton.animate() + .setInterpolator(new AnticipateInterpolator()) + .translationY(targetTranslationY) + .setDuration(400) + .setListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationCancel(final Animator animation) { + end(); + } + + @Override + public void onAnimationEnd(final Animator animation) { + end(); + } + + private void end() { + Objects.requireNonNull(windowManager) + .removeView(closeOverlayBinding.getRoot()); + closeOverlayBinding = null; + service.onDestroy(); + } + }).start(); + } + + private boolean popupHasParent() { + return binding != null + && binding.getRoot().getLayoutParams() instanceof WindowManager.LayoutParams + && binding.getRoot().getParent() != null; + } + + private boolean anyPopupViewIsNull() { + // TODO understand why checking getParentActivity() != null + return popupLayoutParams == null || windowManager == null + || getParentActivity() != null || binding.getRoot().getParent() == null; + } + //endregion + + + + /*////////////////////////////////////////////////////////////////////////// + // Playback parameters + //////////////////////////////////////////////////////////////////////////*/ + //region + + public float getPlaybackSpeed() { + return getPlaybackParameters().speed; + } + + private void setPlaybackSpeed(final float speed) { + setPlaybackParameters(speed, getPlaybackPitch(), getPlaybackSkipSilence()); + } + + public float getPlaybackPitch() { + return getPlaybackParameters().pitch; + } + + public boolean getPlaybackSkipSilence() { + return getPlaybackParameters().skipSilence; + } + + public PlaybackParameters getPlaybackParameters() { + if (exoPlayerIsNull()) { + return PlaybackParameters.DEFAULT; + } + return simpleExoPlayer.getPlaybackParameters(); + } + + /** + * Sets the playback parameters of the player, and also saves them to shared preferences. + * Speed and pitch are rounded up to 2 decimal places before being used or saved. + * + * @param speed the playback speed, will be rounded to up to 2 decimal places + * @param pitch the playback pitch, will be rounded to up to 2 decimal places + * @param skipSilence skip silence during playback + */ + public void setPlaybackParameters(final float speed, final float pitch, + final boolean skipSilence) { + final float roundedSpeed = Math.round(speed * 100.0f) / 100.0f; + final float roundedPitch = Math.round(pitch * 100.0f) / 100.0f; + + savePlaybackParametersToPrefs(this, roundedSpeed, roundedPitch, skipSilence); + simpleExoPlayer.setPlaybackParameters( + new PlaybackParameters(roundedSpeed, roundedPitch, skipSilence)); + } + //endregion + + + + /*////////////////////////////////////////////////////////////////////////// + // Progress loop and updates + //////////////////////////////////////////////////////////////////////////*/ + //region + + private void onUpdateProgress(final int currentProgress, + final int duration, + final int bufferPercent) { + if (!isPrepared) { + return; + } + + if (duration != binding.playbackSeekBar.getMax()) { + binding.playbackEndTime.setText(getTimeString(duration)); + binding.playbackSeekBar.setMax(duration); + } + if (currentState != STATE_PAUSED) { + if (currentState != STATE_PAUSED_SEEK) { + binding.playbackSeekBar.setProgress(currentProgress); + } + binding.playbackCurrentTime.setText(getTimeString(currentProgress)); + } + if (simpleExoPlayer.isLoading() || bufferPercent > 90) { + binding.playbackSeekBar.setSecondaryProgress( + (int) (binding.playbackSeekBar.getMax() * ((float) bufferPercent / 100))); + } + if (DEBUG && bufferPercent % 20 == 0) { //Limit log + Log.d(TAG, "notifyProgressUpdateToListeners() called with: " + + "isVisible = " + isControlsVisible() + ", " + + "currentProgress = [" + currentProgress + "], " + + "duration = [" + duration + "], bufferPercent = [" + bufferPercent + "]"); + } + binding.playbackLiveSync.setClickable(!isLiveEdge()); + + notifyProgressUpdateToListeners(currentProgress, duration, bufferPercent); + + if (areSegmentsVisible) { + segmentAdapter.selectSegmentAt(getNearestStreamSegmentPosition(currentProgress)); + } + + final boolean showThumbnail = prefs.getBoolean( + context.getString(R.string.show_thumbnail_key), true); + // setMetadata only updates the metadata when any of the metadata keys are null + mediaSessionManager.setMetadata(getVideoTitle(), getUploaderName(), + showThumbnail ? getThumbnail() : null, duration); + } + + private void startProgressLoop() { + progressUpdateDisposable.set(getProgressUpdateDisposable()); + } + + private void stopProgressLoop() { + progressUpdateDisposable.set(null); + } + + private boolean isProgressLoopRunning() { + return progressUpdateDisposable.get() != null; + } + + private void triggerProgressUpdate() { + if (exoPlayerIsNull()) { + return; + } + onUpdateProgress( + Math.max((int) simpleExoPlayer.getCurrentPosition(), 0), + (int) simpleExoPlayer.getDuration(), + simpleExoPlayer.getBufferedPercentage() + ); + } + + private Disposable getProgressUpdateDisposable() { + return Observable.interval(PROGRESS_LOOP_INTERVAL_MILLIS, MILLISECONDS, + AndroidSchedulers.mainThread()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(ignored -> triggerProgressUpdate(), + error -> Log.e(TAG, "Progress update failure: ", error)); + } + + @Override // seekbar listener + public void onProgressChanged(final SeekBar seekBar, final int progress, + final boolean fromUser) { + if (DEBUG && fromUser) { + Log.d(TAG, "onProgressChanged() called with: " + + "seekBar = [" + seekBar + "], progress = [" + progress + "]"); + } + if (fromUser) { + binding.currentDisplaySeek.setText(getTimeString(progress)); + } + } + + @Override // seekbar listener + public void onStartTrackingTouch(final SeekBar seekBar) { + if (DEBUG) { + Log.d(TAG, "onStartTrackingTouch() called with: seekBar = [" + seekBar + "]"); + } + if (currentState != STATE_PAUSED_SEEK) { + changeState(STATE_PAUSED_SEEK); + } + + saveWasPlaying(); + if (isPlaying()) { + simpleExoPlayer.setPlayWhenReady(false); + } + + showControls(0); + animateView(binding.currentDisplaySeek, AnimationUtils.Type.SCALE_AND_ALPHA, true, + DEFAULT_CONTROLS_DURATION); + } + + @Override // seekbar listener + public void onStopTrackingTouch(final SeekBar seekBar) { + if (DEBUG) { + Log.d(TAG, "onStopTrackingTouch() called with: seekBar = [" + seekBar + "]"); + } + + seekTo(seekBar.getProgress()); + if (wasPlaying || simpleExoPlayer.getDuration() == seekBar.getProgress()) { + simpleExoPlayer.setPlayWhenReady(true); + } + + binding.playbackCurrentTime.setText(getTimeString(seekBar.getProgress())); + animateView(binding.currentDisplaySeek, AnimationUtils.Type.SCALE_AND_ALPHA, false, 200); + + if (currentState == STATE_PAUSED_SEEK) { + changeState(STATE_BUFFERING); + } + if (!isProgressLoopRunning()) { + startProgressLoop(); + } + if (wasPlaying) { + showControlsThenHide(); + } + } + + public void saveWasPlaying() { + this.wasPlaying = simpleExoPlayer.getPlayWhenReady(); + } + //endregion + + + + /*////////////////////////////////////////////////////////////////////////// + // Controls showing / hiding + //////////////////////////////////////////////////////////////////////////*/ + //region + + public boolean isControlsVisible() { + return binding != null && binding.playbackControlRoot.getVisibility() == View.VISIBLE; + } + + /** + * Show a animation, and depending on goneOnEnd, will stay on the screen or be gone. + * + * @param drawableId the drawable that will be used to animate, + * pass -1 to clear any animation that is visible + * @param goneOnEnd will set the animation view to GONE on the end of the animation + */ + public void showAndAnimateControl(final int drawableId, final boolean goneOnEnd) { + if (DEBUG) { + Log.d(TAG, "showAndAnimateControl() called with: " + + "drawableId = [" + drawableId + "], goneOnEnd = [" + goneOnEnd + "]"); + } + if (controlViewAnimator != null && controlViewAnimator.isRunning()) { + if (DEBUG) { + Log.d(TAG, "showAndAnimateControl: controlViewAnimator.isRunning"); + } + controlViewAnimator.end(); + } + + if (drawableId == -1) { + if (binding.controlAnimationView.getVisibility() == View.VISIBLE) { + controlViewAnimator = ObjectAnimator.ofPropertyValuesHolder( + binding.controlAnimationView, + PropertyValuesHolder.ofFloat(View.ALPHA, 1.0f, 0.0f), + PropertyValuesHolder.ofFloat(View.SCALE_X, 1.4f, 1.0f), + PropertyValuesHolder.ofFloat(View.SCALE_Y, 1.4f, 1.0f) + ).setDuration(DEFAULT_CONTROLS_DURATION); + controlViewAnimator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(final Animator animation) { + binding.controlAnimationView.setVisibility(View.GONE); + } + }); + controlViewAnimator.start(); + } + return; + } + + final float scaleFrom = goneOnEnd ? 1f : 1f; + final float scaleTo = goneOnEnd ? 1.8f : 1.4f; + final float alphaFrom = goneOnEnd ? 1f : 0f; + final float alphaTo = goneOnEnd ? 0f : 1f; + + + controlViewAnimator = ObjectAnimator.ofPropertyValuesHolder( + binding.controlAnimationView, + PropertyValuesHolder.ofFloat(View.ALPHA, alphaFrom, alphaTo), + PropertyValuesHolder.ofFloat(View.SCALE_X, scaleFrom, scaleTo), + PropertyValuesHolder.ofFloat(View.SCALE_Y, scaleFrom, scaleTo) + ); + controlViewAnimator.setDuration(goneOnEnd ? 1000 : 500); + controlViewAnimator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(final Animator animation) { + binding.controlAnimationView.setVisibility(goneOnEnd ? View.GONE : View.VISIBLE); + } + }); + + + binding.controlAnimationView.setVisibility(View.VISIBLE); + binding.controlAnimationView.setImageDrawable( + AppCompatResources.getDrawable(context, drawableId)); + controlViewAnimator.start(); + } + + public void showControlsThenHide() { + if (DEBUG) { + Log.d(TAG, "showControlsThenHide() called"); + } + showOrHideButtons(); + showSystemUIPartially(); + + final int hideTime = binding.playbackControlRoot.isInTouchMode() + ? DEFAULT_CONTROLS_HIDE_TIME + : DPAD_CONTROLS_HIDE_TIME; + + showHideShadow(true, DEFAULT_CONTROLS_DURATION); + animateView(binding.playbackControlRoot, true, DEFAULT_CONTROLS_DURATION, 0, + () -> hideControls(DEFAULT_CONTROLS_DURATION, hideTime)); + } + + public void showControls(final long duration) { + if (DEBUG) { + Log.d(TAG, "showControls() called"); + } + showOrHideButtons(); + showSystemUIPartially(); + controlsVisibilityHandler.removeCallbacksAndMessages(null); + showHideShadow(true, duration); + animateView(binding.playbackControlRoot, true, duration); + } + + public void hideControls(final long duration, final long delay) { + if (DEBUG) { + Log.d(TAG, "hideControls() called with: duration = [" + duration + + "], delay = [" + delay + "]"); + } + + showOrHideButtons(); + + controlsVisibilityHandler.removeCallbacksAndMessages(null); + controlsVisibilityHandler.postDelayed(() -> { + showHideShadow(false, duration); + animateView(binding.playbackControlRoot, false, duration, 0, + this::hideSystemUIIfNeeded); + }, delay); + } + + private void showHideShadow(final boolean show, final long duration) { + animateView(binding.playerTopShadow, show, duration, 0, null); + animateView(binding.playerBottomShadow, show, duration, 0, null); + } + + private void showOrHideButtons() { + if (playQueue == null) { + return; + } + + final boolean showPrev = playQueue.getIndex() != 0; + final boolean showNext = playQueue.getIndex() + 1 != playQueue.getStreams().size(); + final boolean showQueue = playQueue.getStreams().size() > 1 && !popupPlayerSelected(); + boolean showSegment = false; + if (currentMetadata != null) { + showSegment = !currentMetadata.getMetadata().getStreamSegments().isEmpty() + && !popupPlayerSelected(); + } + + binding.playPreviousButton.setVisibility(showPrev ? View.VISIBLE : View.INVISIBLE); + binding.playPreviousButton.setAlpha(showPrev ? 1.0f : 0.0f); + binding.playNextButton.setVisibility(showNext ? View.VISIBLE : View.INVISIBLE); + binding.playNextButton.setAlpha(showNext ? 1.0f : 0.0f); + binding.queueButton.setVisibility(showQueue ? View.VISIBLE : View.GONE); + binding.queueButton.setAlpha(showQueue ? 1.0f : 0.0f); + binding.segmentsButton.setVisibility(showSegment ? View.VISIBLE : View.GONE); + binding.segmentsButton.setAlpha(showSegment ? 1.0f : 0.0f); + } + + private void showSystemUIPartially() { + final AppCompatActivity activity = getParentActivity(); + if (isFullscreen && activity != null) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + activity.getWindow().setStatusBarColor(Color.TRANSPARENT); + activity.getWindow().setNavigationBarColor(Color.TRANSPARENT); + } + final int visibility = View.SYSTEM_UI_FLAG_LAYOUT_STABLE + | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN + | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION; + activity.getWindow().getDecorView().setSystemUiVisibility(visibility); + activity.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN); + } + } + + private void hideSystemUIIfNeeded() { + if (fragmentListener != null) { + fragmentListener.hideSystemUiIfNeeded(); + } + } + //endregion + + + + /*////////////////////////////////////////////////////////////////////////// + // Playback states + //////////////////////////////////////////////////////////////////////////*/ + //region + + @Override // exoplayer listener + public void onPlayerStateChanged(final boolean playWhenReady, final int playbackState) { + if (DEBUG) { + Log.d(TAG, "ExoPlayer - onPlayerStateChanged() called with: " + + "playWhenReady = [" + playWhenReady + "], " + + "playbackState = [" + playbackState + "]"); + } + + if (currentState == STATE_PAUSED_SEEK) { + if (DEBUG) { + Log.d(TAG, "ExoPlayer - onPlayerStateChanged() is currently blocked"); + } + return; + } + + switch (playbackState) { + case com.google.android.exoplayer2.Player.STATE_IDLE: // 1 + isPrepared = false; + break; + case com.google.android.exoplayer2.Player.STATE_BUFFERING: // 2 + if (isPrepared) { + changeState(STATE_BUFFERING); + } + break; + case com.google.android.exoplayer2.Player.STATE_READY: //3 + maybeUpdateCurrentMetadata(); + maybeCorrectSeekPosition(); + if (!isPrepared) { + isPrepared = true; + onPrepared(playWhenReady); + } + changeState(playWhenReady ? STATE_PLAYING : STATE_PAUSED); + break; + case com.google.android.exoplayer2.Player.STATE_ENDED: // 4 + changeState(STATE_COMPLETED); + if (currentMetadata != null) { + resetStreamProgressState(currentMetadata.getMetadata()); + } + isPrepared = false; + break; + } + } + + @Override // exoplayer listener + public void onLoadingChanged(final boolean isLoading) { + if (DEBUG) { + Log.d(TAG, "ExoPlayer - onLoadingChanged() called with: " + + "isLoading = [" + isLoading + "]"); + } + + if (!isLoading && currentState == STATE_PAUSED && isProgressLoopRunning()) { + stopProgressLoop(); + } else if (isLoading && !isProgressLoopRunning()) { + startProgressLoop(); + } + + maybeUpdateCurrentMetadata(); + } + + @Override // own playback listener + public void onPlaybackBlock() { + if (exoPlayerIsNull()) { + return; + } + if (DEBUG) { + Log.d(TAG, "Playback - onPlaybackBlock() called"); + } + + currentItem = null; + currentMetadata = null; + simpleExoPlayer.stop(); + isPrepared = false; + + changeState(STATE_BLOCKED); + } + + @Override // own playback listener + public void onPlaybackUnblock(final MediaSource mediaSource) { + if (DEBUG) { + Log.d(TAG, "Playback - onPlaybackUnblock() called"); + } + + if (exoPlayerIsNull()) { + return; + } + if (currentState == STATE_BLOCKED) { + changeState(STATE_BUFFERING); + } + simpleExoPlayer.prepare(mediaSource); + } + + public void changeState(final int state) { + if (DEBUG) { + Log.d(TAG, "changeState() called with: state = [" + state + "]"); + } + currentState = state; + switch (state) { + case STATE_BLOCKED: + onBlocked(); + break; + case STATE_PLAYING: + onPlaying(); + break; + case STATE_BUFFERING: + onBuffering(); + break; + case STATE_PAUSED: + onPaused(); + break; + case STATE_PAUSED_SEEK: + onPausedSeek(); + break; + case STATE_COMPLETED: + onCompleted(); + break; + } + notifyPlaybackUpdateToListeners(); + } + + private void onPrepared(final boolean playWhenReady) { + if (DEBUG) { + Log.d(TAG, "onPrepared() called with: playWhenReady = [" + playWhenReady + "]"); + } + + binding.playbackSeekBar.setMax((int) simpleExoPlayer.getDuration()); + binding.playbackEndTime.setText(getTimeString((int) simpleExoPlayer.getDuration())); + binding.playbackSpeed.setText(formatSpeed(getPlaybackSpeed())); + + if (playWhenReady) { + audioReactor.requestAudioFocus(); + } + } + + private void onBlocked() { + if (DEBUG) { + Log.d(TAG, "onBlocked() called"); + } + if (!isProgressLoopRunning()) { + startProgressLoop(); + } + + controlsVisibilityHandler.removeCallbacksAndMessages(null); + animateView(binding.playbackControlRoot, false, DEFAULT_CONTROLS_DURATION); + + binding.playbackSeekBar.setEnabled(false); + binding.playbackSeekBar.getThumb() + .setColorFilter(new PorterDuffColorFilter(Color.RED, PorterDuff.Mode.SRC_IN)); + + binding.loadingPanel.setBackgroundColor(Color.BLACK); + animateView(binding.loadingPanel, true, 0); + animateView(binding.surfaceForeground, true, 100); + + binding.playPauseButton.setImageResource(R.drawable.ic_play_arrow_white_24dp); + animatePlayButtons(false, 100); + binding.getRoot().setKeepScreenOn(false); + + NotificationUtil.getInstance().createNotificationIfNeededAndUpdate(this, false); + } + + private void onPlaying() { + if (DEBUG) { + Log.d(TAG, "onPlaying() called"); + } + if (!isProgressLoopRunning()) { + startProgressLoop(); + } + + updateStreamRelatedViews(); + + showAndAnimateControl(-1, true); + + binding.playbackSeekBar.setEnabled(true); + binding.playbackSeekBar.getThumb() + .setColorFilter(new PorterDuffColorFilter(Color.RED, PorterDuff.Mode.SRC_IN)); + + binding.loadingPanel.setVisibility(View.GONE); + + animateView(binding.currentDisplaySeek, AnimationUtils.Type.SCALE_AND_ALPHA, false, 200); + + animateView(binding.playPauseButton, AnimationUtils.Type.SCALE_AND_ALPHA, false, 80, 0, + () -> { + binding.playPauseButton.setImageResource(R.drawable.ic_pause_white_24dp); + animatePlayButtons(true, 200); + if (!isQueueVisible) { + binding.playPauseButton.requestFocus(); + } + }); + + changePopupWindowFlags(ONGOING_PLAYBACK_WINDOW_FLAGS); + checkLandscape(); + binding.getRoot().setKeepScreenOn(true); + + NotificationUtil.getInstance().createNotificationIfNeededAndUpdate(this, false); + } + + private void onBuffering() { + if (DEBUG) { + Log.d(TAG, "onBuffering() called"); + } + binding.loadingPanel.setBackgroundColor(Color.TRANSPARENT); + + binding.getRoot().setKeepScreenOn(true); + + if (NotificationUtil.getInstance().shouldUpdateBufferingSlot()) { + NotificationUtil.getInstance().createNotificationIfNeededAndUpdate(this, false); + } + } + + private void onPaused() { + if (DEBUG) { + Log.d(TAG, "onPaused() called"); + } + + if (isProgressLoopRunning()) { + stopProgressLoop(); + } + + showControls(400); + binding.loadingPanel.setVisibility(View.GONE); + + animateView(binding.playPauseButton, AnimationUtils.Type.SCALE_AND_ALPHA, false, 80, 0, + () -> { + binding.playPauseButton.setImageResource(R.drawable.ic_play_arrow_white_24dp); + animatePlayButtons(true, 200); + if (!isQueueVisible) { + binding.playPauseButton.requestFocus(); + } + }); + + changePopupWindowFlags(IDLE_WINDOW_FLAGS); + + // Remove running notification when user does not want minimization to background or popup + if (PlayerHelper.isMinimizeOnExitDisabled(context) && videoPlayerSelected()) { + NotificationUtil.getInstance().cancelNotificationAndStopForeground(service); + } else { + NotificationUtil.getInstance().createNotificationIfNeededAndUpdate(this, false); + } + + binding.getRoot().setKeepScreenOn(false); + } + + private void onPausedSeek() { + if (DEBUG) { + Log.d(TAG, "onPausedSeek() called"); + } + showAndAnimateControl(-1, true); + + animatePlayButtons(false, 100); + binding.getRoot().setKeepScreenOn(true); + + NotificationUtil.getInstance().createNotificationIfNeededAndUpdate(this, false); + } + + private void onCompleted() { + if (DEBUG) { + Log.d(TAG, "onCompleted() called"); + } + + animateView(binding.playPauseButton, AnimationUtils.Type.SCALE_AND_ALPHA, false, 0, 0, + () -> { + binding.playPauseButton.setImageResource(R.drawable.ic_replay_white_24dp); + animatePlayButtons(true, DEFAULT_CONTROLS_DURATION); + }); + + binding.getRoot().setKeepScreenOn(false); + changePopupWindowFlags(IDLE_WINDOW_FLAGS); + + NotificationUtil.getInstance().createNotificationIfNeededAndUpdate(this, false); + if (isFullscreen) { + toggleFullscreen(); + } + + if (playQueue.getIndex() < playQueue.size() - 1) { + playQueue.offsetIndex(+1); + } + if (isProgressLoopRunning()) { + stopProgressLoop(); + } + + showControls(500); + animateView(binding.currentDisplaySeek, AnimationUtils.Type.SCALE_AND_ALPHA, false, 200); + binding.loadingPanel.setVisibility(View.GONE); + animateView(binding.surfaceForeground, true, 100); + } + + private void animatePlayButtons(final boolean show, final int duration) { + animateView(binding.playPauseButton, AnimationUtils.Type.SCALE_AND_ALPHA, show, duration); + + boolean showQueueButtons = show; + if (playQueue == null) { + showQueueButtons = false; + } + + if (!showQueueButtons || playQueue.getIndex() > 0) { + animateView( + binding.playPreviousButton, + AnimationUtils.Type.SCALE_AND_ALPHA, + showQueueButtons, + duration); + } + if (!showQueueButtons || playQueue.getIndex() + 1 < playQueue.getStreams().size()) { + animateView( + binding.playNextButton, + AnimationUtils.Type.SCALE_AND_ALPHA, + showQueueButtons, + duration); + } + } + //endregion + + + + /*////////////////////////////////////////////////////////////////////////// + // Repeat and shuffle + //////////////////////////////////////////////////////////////////////////*/ + //region + + public void onRepeatClicked() { + if (DEBUG) { + Log.d(TAG, "onRepeatClicked() called"); + } + setRepeatMode(nextRepeatMode(getRepeatMode())); + } + + public void onShuffleClicked() { + if (DEBUG) { + Log.d(TAG, "onShuffleClicked() called"); + } + + if (exoPlayerIsNull()) { + return; + } + simpleExoPlayer.setShuffleModeEnabled(!simpleExoPlayer.getShuffleModeEnabled()); + } + + @RepeatMode + public int getRepeatMode() { + return exoPlayerIsNull() ? REPEAT_MODE_OFF : simpleExoPlayer.getRepeatMode(); + } + + private void setRepeatMode(@RepeatMode final int repeatMode) { + if (!exoPlayerIsNull()) { + simpleExoPlayer.setRepeatMode(repeatMode); + } + } + + @Override + public void onRepeatModeChanged(@RepeatMode final int repeatMode) { + if (DEBUG) { + Log.d(TAG, "ExoPlayer - onRepeatModeChanged() called with: " + + "repeatMode = [" + repeatMode + "]"); + } + setRepeatModeButton(binding.repeatButton, repeatMode); + onShuffleOrRepeatModeChanged(); + } + + @Override + public void onShuffleModeEnabledChanged(final boolean shuffleModeEnabled) { + if (DEBUG) { + Log.d(TAG, "ExoPlayer - onShuffleModeEnabledChanged() called with: " + + "mode = [" + shuffleModeEnabled + "]"); + } + + if (playQueue != null) { + if (shuffleModeEnabled) { + playQueue.shuffle(); + } else { + playQueue.unshuffle(); + } + } + + setShuffleButton(binding.shuffleButton, shuffleModeEnabled); + onShuffleOrRepeatModeChanged(); + } + + private void onShuffleOrRepeatModeChanged() { + notifyPlaybackUpdateToListeners(); + NotificationUtil.getInstance().createNotificationIfNeededAndUpdate(this, false); + } + + private void setRepeatModeButton(final ImageButton imageButton, final int repeatMode) { + switch (repeatMode) { + case REPEAT_MODE_OFF: + imageButton.setImageResource(R.drawable.exo_controls_repeat_off); + break; + case REPEAT_MODE_ONE: + imageButton.setImageResource(R.drawable.exo_controls_repeat_one); + break; + case REPEAT_MODE_ALL: + imageButton.setImageResource(R.drawable.exo_controls_repeat_all); + break; + } + } + + private void setShuffleButton(final ImageButton button, final boolean shuffled) { + button.setImageAlpha(shuffled ? 255 : 77); + } + //endregion + + + + /*////////////////////////////////////////////////////////////////////////// + // Mute / Unmute + //////////////////////////////////////////////////////////////////////////*/ + //region + + public void onMuteUnmuteButtonClicked() { + if (DEBUG) { + Log.d(TAG, "onMuteUnmuteButtonClicked() called"); + } + simpleExoPlayer.setVolume(isMuted() ? 1 : 0); + notifyPlaybackUpdateToListeners(); + setMuteButton(binding.switchMute, isMuted()); + } + + boolean isMuted() { + return !exoPlayerIsNull() && simpleExoPlayer.getVolume() == 0; + } + + private void setMuteButton(final ImageButton button, final boolean isMuted) { + button.setImageDrawable(AppCompatResources.getDrawable(context, isMuted + ? R.drawable.ic_volume_off_white_24dp : R.drawable.ic_volume_up_white_24dp)); + } + //endregion + + + + /*////////////////////////////////////////////////////////////////////////// + // ExoPlayer listeners (that didn't fit in other categories) + //////////////////////////////////////////////////////////////////////////*/ + //region + + @Override + public void onTimelineChanged(@NonNull final Timeline timeline, final int reason) { + if (DEBUG) { + Log.d(TAG, "ExoPlayer - onTimelineChanged() called with " + + "timeline size = [" + timeline.getWindowCount() + "], " + + "reason = [" + reason + "]"); + } + + maybeUpdateCurrentMetadata(); + // force recreate notification to ensure seek bar is shown when preparation finishes + NotificationUtil.getInstance().createNotificationIfNeededAndUpdate(this, true); + } + + @Override + public void onTracksChanged(@NonNull final TrackGroupArray trackGroups, + @NonNull final TrackSelectionArray trackSelections) { + if (DEBUG) { + Log.d(TAG, "ExoPlayer - onTracksChanged(), " + + "track group size = " + trackGroups.length); + } + maybeUpdateCurrentMetadata(); + onTextTracksChanged(); + } + + @Override + public void onPlaybackParametersChanged(@NonNull final PlaybackParameters playbackParameters) { + if (DEBUG) { + Log.d(TAG, "ExoPlayer - playbackParameters(), speed = [" + playbackParameters.speed + + "], pitch = [" + playbackParameters.pitch + "]"); + } + binding.playbackSpeed.setText(formatSpeed(playbackParameters.speed)); + } + + @Override + public void onPositionDiscontinuity(@DiscontinuityReason final int discontinuityReason) { + if (DEBUG) { + Log.d(TAG, "ExoPlayer - onPositionDiscontinuity() called with " + + "discontinuityReason = [" + discontinuityReason + "]"); + } + if (playQueue == null) { + return; + } + + // Refresh the playback if there is a transition to the next video + final int newWindowIndex = simpleExoPlayer.getCurrentWindowIndex(); + switch (discontinuityReason) { + case DISCONTINUITY_REASON_PERIOD_TRANSITION: + // When player is in single repeat mode and a period transition occurs, + // we need to register a view count here since no metadata has changed + if (getRepeatMode() == REPEAT_MODE_ONE && newWindowIndex == playQueue.getIndex()) { + registerStreamViewed(); + break; + } + case DISCONTINUITY_REASON_SEEK: + case DISCONTINUITY_REASON_SEEK_ADJUSTMENT: + case DISCONTINUITY_REASON_INTERNAL: + if (playQueue.getIndex() != newWindowIndex) { + resetStreamProgressState(playQueue.getItem()); + playQueue.setIndex(newWindowIndex); + } + break; + case DISCONTINUITY_REASON_AD_INSERTION: + break; // only makes Android Studio linter happy, as there are no ads + } + + maybeUpdateCurrentMetadata(); + } + + @Override + public void onRenderedFirstFrame() { + //TODO check if this causes black screen when switching to fullscreen + animateView(binding.surfaceForeground, false, DEFAULT_CONTROLS_DURATION); + } + //endregion + + + + /*////////////////////////////////////////////////////////////////////////// + // Errors + //////////////////////////////////////////////////////////////////////////*/ + //region + /** + * Process exceptions produced by {@link com.google.android.exoplayer2.ExoPlayer ExoPlayer}. + *

There are multiple types of errors:

+ *
    + *
  • {@link ExoPlaybackException#TYPE_SOURCE TYPE_SOURCE}
  • + *
  • {@link ExoPlaybackException#TYPE_UNEXPECTED TYPE_UNEXPECTED}: + * If a runtime error occurred, then we can try to recover it by restarting the playback + * after setting the timestamp recovery.
  • + *
  • {@link ExoPlaybackException#TYPE_RENDERER TYPE_RENDERER}: + * If the renderer failed, treat the error as unrecoverable.
  • + *
+ * + * @see #processSourceError(IOException) + * @see com.google.android.exoplayer2.Player.EventListener#onPlayerError(ExoPlaybackException) + */ + @Override + public void onPlayerError(@NonNull final ExoPlaybackException error) { + if (DEBUG) { + Log.d(TAG, "ExoPlayer - onPlayerError() called with: " + "error = [" + error + "]"); + } + if (errorToast != null) { + errorToast.cancel(); + errorToast = null; + } + + saveStreamProgressState(); + + switch (error.type) { + case ExoPlaybackException.TYPE_SOURCE: + processSourceError(error.getSourceException()); + showStreamError(error); + break; + case ExoPlaybackException.TYPE_UNEXPECTED: + showRecoverableError(error); + setRecovery(); + reloadPlayQueueManager(); + break; + case ExoPlaybackException.TYPE_OUT_OF_MEMORY: + case ExoPlaybackException.TYPE_REMOTE: + case ExoPlaybackException.TYPE_RENDERER: + default: + showUnrecoverableError(error); + onPlaybackShutdown(); + break; + } + + if (fragmentListener != null) { + fragmentListener.onPlayerError(error); + } + } + + private void processSourceError(final IOException error) { + if (exoPlayerIsNull() || playQueue == null) { + return; + } + setRecovery(); + + if (error instanceof BehindLiveWindowException) { + reloadPlayQueueManager(); + } else { + playQueue.error(); + } + } + + private void showStreamError(final Exception exception) { + exception.printStackTrace(); + + if (errorToast == null) { + errorToast = Toast + .makeText(context, R.string.player_stream_failure, Toast.LENGTH_SHORT); + errorToast.show(); + } + } + + private void showRecoverableError(final Exception exception) { + exception.printStackTrace(); + + if (errorToast == null) { + errorToast = Toast + .makeText(context, R.string.player_recoverable_failure, Toast.LENGTH_SHORT); + errorToast.show(); + } + } + + private void showUnrecoverableError(final Exception exception) { + exception.printStackTrace(); + + if (errorToast != null) { + errorToast.cancel(); + } + errorToast = Toast + .makeText(context, R.string.player_unrecoverable_failure, Toast.LENGTH_SHORT); + errorToast.show(); + } + //endregion + + + + /*////////////////////////////////////////////////////////////////////////// + // Playback position and seek + //////////////////////////////////////////////////////////////////////////*/ + //region + + @Override // own playback listener (this is a getter) + public boolean isApproachingPlaybackEdge(final long timeToEndMillis) { + // If live, then not near playback edge + // If not playing, then not approaching playback edge + if (exoPlayerIsNull() || isLive() || !isPlaying()) { + return false; + } + + final long currentPositionMillis = simpleExoPlayer.getCurrentPosition(); + final long currentDurationMillis = simpleExoPlayer.getDuration(); + return currentDurationMillis - currentPositionMillis < timeToEndMillis; + } + + /** + * Checks if the current playback is a livestream AND is playing at or beyond the live edge. + * + * @return whether the livestream is playing at or beyond the edge + */ + @SuppressWarnings("BooleanMethodIsAlwaysInverted") + public boolean isLiveEdge() { + if (exoPlayerIsNull() || !isLive()) { + return false; + } + + final Timeline currentTimeline = simpleExoPlayer.getCurrentTimeline(); + final int currentWindowIndex = simpleExoPlayer.getCurrentWindowIndex(); + if (currentTimeline.isEmpty() || currentWindowIndex < 0 + || currentWindowIndex >= currentTimeline.getWindowCount()) { + return false; + } + + final Timeline.Window timelineWindow = new Timeline.Window(); + currentTimeline.getWindow(currentWindowIndex, timelineWindow); + return timelineWindow.getDefaultPositionMs() <= simpleExoPlayer.getCurrentPosition(); + } + + @Override // own playback listener + public void onPlaybackSynchronize(@NonNull final PlayQueueItem item) { + if (DEBUG) { + Log.d(TAG, "Playback - onPlaybackSynchronize() called with " + + "item=[" + item.getTitle() + "], url=[" + item.getUrl() + "]"); + } + if (exoPlayerIsNull() || playQueue == null) { + return; + } + + final boolean onPlaybackInitial = currentItem == null; + final boolean hasPlayQueueItemChanged = currentItem != item; + + final int currentPlayQueueIndex = playQueue.indexOf(item); + final int currentPlaylistIndex = simpleExoPlayer.getCurrentWindowIndex(); + final int currentPlaylistSize = simpleExoPlayer.getCurrentTimeline().getWindowCount(); + + // If nothing to synchronize + if (!hasPlayQueueItemChanged) { + return; + } + currentItem = item; + + // Check if on wrong window + if (currentPlayQueueIndex != playQueue.getIndex()) { + Log.e(TAG, "Playback - Play Queue may be desynchronized: item " + + "index=[" + currentPlayQueueIndex + "], " + + "queue index=[" + playQueue.getIndex() + "]"); + + // Check if bad seek position + } else if ((currentPlaylistSize > 0 && currentPlayQueueIndex >= currentPlaylistSize) + || currentPlayQueueIndex < 0) { + Log.e(TAG, "Playback - Trying to seek to invalid " + + "index=[" + currentPlayQueueIndex + "] with " + + "playlist length=[" + currentPlaylistSize + "]"); + + } else if (currentPlaylistIndex != currentPlayQueueIndex || onPlaybackInitial + || !isPlaying()) { + if (DEBUG) { + Log.d(TAG, "Playback - Rewinding to correct " + + "index=[" + currentPlayQueueIndex + "], " + + "from=[" + currentPlaylistIndex + "], " + + "size=[" + currentPlaylistSize + "]."); + } + + if (item.getRecoveryPosition() != PlayQueueItem.RECOVERY_UNSET) { + simpleExoPlayer.seekTo(currentPlayQueueIndex, item.getRecoveryPosition()); + playQueue.unsetRecovery(currentPlayQueueIndex); + } else { + simpleExoPlayer.seekToDefaultPosition(currentPlayQueueIndex); + } + } + } + + private void maybeCorrectSeekPosition() { + if (playQueue == null || exoPlayerIsNull() || currentMetadata == null) { + return; + } + + final PlayQueueItem currentSourceItem = playQueue.getItem(); + if (currentSourceItem == null) { + return; + } + + final StreamInfo currentInfo = currentMetadata.getMetadata(); + final long presetStartPositionMillis = currentInfo.getStartPosition() * 1000; + if (presetStartPositionMillis > 0L) { + // Has another start position? + if (DEBUG) { + Log.d(TAG, "Playback - Seeking to preset start " + + "position=[" + presetStartPositionMillis + "]"); + } + seekTo(presetStartPositionMillis); + } + } + + public void seekTo(final long positionMillis) { + if (DEBUG) { + Log.d(TAG, "seekBy() called with: position = [" + positionMillis + "]"); + } + if (!exoPlayerIsNull()) { + // prevent invalid positions when fast-forwarding/-rewinding + long normalizedPositionMillis = positionMillis; + if (normalizedPositionMillis < 0) { + normalizedPositionMillis = 0; + } else if (normalizedPositionMillis > simpleExoPlayer.getDuration()) { + normalizedPositionMillis = simpleExoPlayer.getDuration(); + } + + simpleExoPlayer.seekTo(normalizedPositionMillis); + } + } + + private void seekBy(final long offsetMillis) { + if (DEBUG) { + Log.d(TAG, "seekBy() called with: offsetMillis = [" + offsetMillis + "]"); + } + seekTo(simpleExoPlayer.getCurrentPosition() + offsetMillis); + } + + public void seekToDefault() { + if (!exoPlayerIsNull()) { + simpleExoPlayer.seekToDefaultPosition(); + } + } + + @Override // exoplayer override + public void onSeekProcessed() { + if (DEBUG) { + Log.d(TAG, "ExoPlayer - onSeekProcessed() called"); + } + if (isPrepared) { + saveStreamProgressState(); + } + } + //endregion + + + + /*////////////////////////////////////////////////////////////////////////// + // Player actions (play, pause, previous, fast-forward, ...) + //////////////////////////////////////////////////////////////////////////*/ + //region + + public void play() { + if (DEBUG) { + Log.d(TAG, "play() called"); + } + if (audioReactor == null || playQueue == null || exoPlayerIsNull()) { + return; + } + + audioReactor.requestAudioFocus(); + + if (currentState == STATE_COMPLETED) { + if (playQueue.getIndex() == 0) { + seekToDefault(); + } else { + playQueue.setIndex(0); + } + } + + simpleExoPlayer.setPlayWhenReady(true); + saveStreamProgressState(); + } + + public void pause() { + if (DEBUG) { + Log.d(TAG, "pause() called"); + } + if (audioReactor == null || exoPlayerIsNull()) { + return; + } + + audioReactor.abandonAudioFocus(); + simpleExoPlayer.setPlayWhenReady(false); + saveStreamProgressState(); + } + + public void playPause() { + if (DEBUG) { + Log.d(TAG, "onPlayPause() called"); + } + + if (isPlaying()) { + pause(); + } else { + play(); + } + } + + public void playPrevious() { + if (DEBUG) { + Log.d(TAG, "onPlayPrevious() called"); + } + if (exoPlayerIsNull() || playQueue == null) { + return; + } + + /* If current playback has run for PLAY_PREV_ACTIVATION_LIMIT_MILLIS milliseconds, + * restart current track. Also restart the track if the current track + * is the first in a queue.*/ + if (simpleExoPlayer.getCurrentPosition() > PLAY_PREV_ACTIVATION_LIMIT_MILLIS + || playQueue.getIndex() == 0) { + seekToDefault(); + playQueue.offsetIndex(0); + } else { + saveStreamProgressState(); + playQueue.offsetIndex(-1); + } + triggerProgressUpdate(); + } + + public void playNext() { + if (DEBUG) { + Log.d(TAG, "onPlayNext() called"); + } + if (playQueue == null) { + return; + } + + saveStreamProgressState(); + playQueue.offsetIndex(+1); + triggerProgressUpdate(); + } + + public void fastForward() { + if (DEBUG) { + Log.d(TAG, "fastRewind() called"); + } + seekBy(retrieveSeekDurationFromPreferences(this)); + triggerProgressUpdate(); + showAndAnimateControl(R.drawable.ic_fast_forward_white_24dp, true); + } + + public void fastRewind() { + if (DEBUG) { + Log.d(TAG, "fastRewind() called"); + } + seekBy(-retrieveSeekDurationFromPreferences(this)); + triggerProgressUpdate(); + showAndAnimateControl(R.drawable.ic_fast_rewind_white_24dp, true); + } + //endregion + + + + /*////////////////////////////////////////////////////////////////////////// + // StreamInfo history: views and progress + //////////////////////////////////////////////////////////////////////////*/ + //region + + private void registerStreamViewed() { + if (currentMetadata != null) { + databaseUpdateDisposable.add(recordManager.onViewed(currentMetadata.getMetadata()) + .onErrorComplete().subscribe()); + } + } + + private void saveStreamProgressState(final StreamInfo info, final long progress) { + if (info == null) { + return; + } + if (DEBUG) { + Log.d(TAG, "saveStreamProgressState() called"); + } + if (prefs.getBoolean(context.getString(R.string.enable_watch_history_key), true)) { + final Disposable stateSaver = recordManager.saveStreamState(info, progress) + .observeOn(AndroidSchedulers.mainThread()) + .doOnError((e) -> { + if (DEBUG) { + e.printStackTrace(); + } + }) + .onErrorComplete() + .subscribe(); + databaseUpdateDisposable.add(stateSaver); + } + } + + private void resetStreamProgressState(final PlayQueueItem queueItem) { + if (queueItem == null) { + return; + } + if (prefs.getBoolean(context.getString(R.string.enable_watch_history_key), true)) { + final Disposable stateSaver = queueItem.getStream() + .flatMapCompletable(info -> recordManager.saveStreamState(info, 0)) + .observeOn(AndroidSchedulers.mainThread()) + .doOnError((e) -> { + if (DEBUG) { + e.printStackTrace(); + } + }) + .onErrorComplete() + .subscribe(); + databaseUpdateDisposable.add(stateSaver); + } + } + + private void resetStreamProgressState(final StreamInfo info) { + saveStreamProgressState(info, 0); + } + + public void saveStreamProgressState() { + if (exoPlayerIsNull() || currentMetadata == null) { + return; + } + final StreamInfo currentInfo = currentMetadata.getMetadata(); + if (playQueue != null) { + // Save current position. It will help to restore this position once a user + // wants to play prev or next stream from the queue + playQueue.setRecovery(playQueue.getIndex(), simpleExoPlayer.getContentPosition()); + } + saveStreamProgressState(currentInfo, simpleExoPlayer.getCurrentPosition()); + } + //endregion + + + + /*////////////////////////////////////////////////////////////////////////// + // Metadata + //////////////////////////////////////////////////////////////////////////*/ + //region + + private void onMetadataChanged(@NonNull final MediaSourceTag tag) { + final StreamInfo info = tag.getMetadata(); + if (DEBUG) { + Log.d(TAG, "Playback - onMetadataChanged() called, playing: " + info.getName()); + } + + initThumbnail(info.getThumbnailUrl()); + registerStreamViewed(); + updateStreamRelatedViews(); + showHideKodiButton(); + + binding.titleTextView.setText(tag.getMetadata().getName()); + binding.channelTextView.setText(tag.getMetadata().getUploaderName()); + + NotificationUtil.getInstance().createNotificationIfNeededAndUpdate(this, false); + notifyMetadataUpdateToListeners(); + + if (areSegmentsVisible) { + if (segmentAdapter.setItems(info)) { + final int adapterPosition = getNearestStreamSegmentPosition( + simpleExoPlayer.getCurrentPosition()); + segmentAdapter.selectSegmentAt(adapterPosition); + binding.itemsList.scrollToPosition(adapterPosition); + } else { + closeItemsList(); + } + } + } + + private void maybeUpdateCurrentMetadata() { + if (exoPlayerIsNull()) { + return; + } + + final MediaSourceTag metadata; + try { + metadata = (MediaSourceTag) simpleExoPlayer.getCurrentTag(); + } catch (IndexOutOfBoundsException | ClassCastException error) { + if (DEBUG) { + Log.d(TAG, "Could not update metadata: " + error.getMessage()); + error.printStackTrace(); + } + return; + } + + if (metadata == null) { + return; + } + maybeAutoQueueNextStream(metadata); + + if (currentMetadata == metadata) { + return; + } + currentMetadata = metadata; + onMetadataChanged(metadata); + } + + @NonNull + private String getVideoUrl() { + return currentMetadata == null + ? context.getString(R.string.unknown_content) + : currentMetadata.getMetadata().getUrl(); + } + + @NonNull + public String getVideoTitle() { + return currentMetadata == null + ? context.getString(R.string.unknown_content) + : currentMetadata.getMetadata().getName(); + } + + @NonNull + public String getUploaderName() { + return currentMetadata == null + ? context.getString(R.string.unknown_content) + : currentMetadata.getMetadata().getUploaderName(); + } + + @Nullable + public Bitmap getThumbnail() { + return currentThumbnail == null + ? BitmapFactory.decodeResource(context.getResources(), R.drawable.dummy_thumbnail) + : currentThumbnail; + } + //endregion + + + + /*////////////////////////////////////////////////////////////////////////// + // Play queue, segments and streams + //////////////////////////////////////////////////////////////////////////*/ + //region + + private void maybeAutoQueueNextStream(@NonNull final MediaSourceTag metadata) { + if (playQueue == null || playQueue.getIndex() != playQueue.size() - 1 + || getRepeatMode() != REPEAT_MODE_OFF + || !PlayerHelper.isAutoQueueEnabled(context)) { + return; + } + // auto queue when starting playback on the last item when not repeating + final PlayQueue autoQueue = PlayerHelper.autoQueueOf(metadata.getMetadata(), + playQueue.getStreams()); + if (autoQueue != null) { + playQueue.append(autoQueue.getStreams()); + } + } + + public void selectQueueItem(final PlayQueueItem item) { + if (playQueue == null || exoPlayerIsNull()) { + return; + } + + final int index = playQueue.indexOf(item); + if (index == -1) { + return; + } + + if (playQueue.getIndex() == index && simpleExoPlayer.getCurrentWindowIndex() == index) { + seekToDefault(); + } else { + saveStreamProgressState(); + } + playQueue.setIndex(index); + } + + @Override + public void onPlayQueueEdited() { + notifyPlaybackUpdateToListeners(); + showOrHideButtons(); + NotificationUtil.getInstance().createNotificationIfNeededAndUpdate(this, false); + } + + private void onQueueClicked() { + isQueueVisible = true; + + hideSystemUIIfNeeded(); + buildQueue(); + + binding.itemsListHeaderTitle.setVisibility(View.GONE); + binding.shuffleButton.setVisibility(View.VISIBLE); + binding.repeatButton.setVisibility(View.VISIBLE); + + hideControls(0, 0); + binding.itemsListPanel.requestFocus(); + animateView(binding.itemsListPanel, SLIDE_AND_ALPHA, true, + DEFAULT_CONTROLS_DURATION); + + binding.itemsList.scrollToPosition(playQueue.getIndex()); + } + + private void buildQueue() { + binding.itemsList.setAdapter(playQueueAdapter); + binding.itemsList.setClickable(true); + binding.itemsList.setLongClickable(true); + + binding.itemsList.clearOnScrollListeners(); + binding.itemsList.addOnScrollListener(getQueueScrollListener()); + + itemTouchHelper = new ItemTouchHelper(getItemTouchCallback()); + itemTouchHelper.attachToRecyclerView(binding.itemsList); + + playQueueAdapter.setSelectedListener(getOnSelectedListener()); + + binding.itemsListClose.setOnClickListener(view -> closeItemsList()); + } + + private void onSegmentsClicked() { + areSegmentsVisible = true; + + hideSystemUIIfNeeded(); + buildSegments(); + + binding.itemsListHeaderTitle.setVisibility(View.VISIBLE); + binding.shuffleButton.setVisibility(View.GONE); + binding.repeatButton.setVisibility(View.GONE); + + hideControls(0, 0); + binding.itemsListPanel.requestFocus(); + animateView(binding.itemsListPanel, SLIDE_AND_ALPHA, true, + DEFAULT_CONTROLS_DURATION); + + final int adapterPosition = getNearestStreamSegmentPosition(simpleExoPlayer + .getCurrentPosition()); + segmentAdapter.selectSegmentAt(adapterPosition); + binding.itemsList.scrollToPosition(adapterPosition); + } + + private void buildSegments() { + binding.itemsList.setAdapter(segmentAdapter); + binding.itemsList.setClickable(true); + binding.itemsList.setLongClickable(false); + + binding.itemsList.clearOnScrollListeners(); + if (itemTouchHelper != null) { + itemTouchHelper.attachToRecyclerView(null); + } + + if (currentMetadata != null) { + segmentAdapter.setItems(currentMetadata.getMetadata()); + } + + binding.shuffleButton.setVisibility(View.GONE); + binding.repeatButton.setVisibility(View.GONE); + binding.itemsListClose.setOnClickListener(view -> closeItemsList()); + } + + public void closeItemsList() { + if (isQueueVisible || areSegmentsVisible) { + isQueueVisible = false; + areSegmentsVisible = false; + + if (itemTouchHelper != null) { + itemTouchHelper.attachToRecyclerView(null); + } + + animateView(binding.itemsListPanel, SLIDE_AND_ALPHA, false, + DEFAULT_CONTROLS_DURATION, 0, () -> { + // Even when queueLayout is GONE it receives touch events + // and ruins normal behavior of the app. This line fixes it + binding.itemsListPanel.setTranslationY( + -binding.itemsListPanel.getHeight() * 5); + }); + binding.playPauseButton.requestFocus(); + } + } + + private OnScrollBelowItemsListener getQueueScrollListener() { + return new OnScrollBelowItemsListener() { + @Override + public void onScrolledDown(final RecyclerView recyclerView) { + if (playQueue != null && !playQueue.isComplete()) { + playQueue.fetch(); + } else if (binding != null) { + binding.itemsList.clearOnScrollListeners(); + } + } + }; + } + + private StreamSegmentAdapter.StreamSegmentListener getStreamSegmentListener() { + return (item, seconds) -> { + segmentAdapter.selectSegment(item); + seekTo(seconds * 1000); + triggerProgressUpdate(); + }; + } + + private int getNearestStreamSegmentPosition(final long playbackPosition) { + int nearestPosition = 0; + final List segments = currentMetadata.getMetadata().getStreamSegments(); + + for (int i = 0; i < segments.size(); i++) { + if (segments.get(i).getStartTimeSeconds() * 1000 > playbackPosition) { + break; + } + nearestPosition++; + } + return Math.max(0, nearestPosition - 1); + } + + private ItemTouchHelper.SimpleCallback getItemTouchCallback() { + return new PlayQueueItemTouchCallback() { + @Override + public void onMove(final int sourceIndex, final int targetIndex) { + if (playQueue != null) { + playQueue.move(sourceIndex, targetIndex); + } + } + + @Override + public void onSwiped(final int index) { + if (index != -1) { + playQueue.remove(index); + } + } + }; + } + + private PlayQueueItemBuilder.OnSelectedListener getOnSelectedListener() { + return new PlayQueueItemBuilder.OnSelectedListener() { + @Override + public void selected(final PlayQueueItem item, final View view) { + selectQueueItem(item); + } + + @Override + public void held(final PlayQueueItem item, final View view) { + final int index = playQueue.indexOf(item); + if (index != -1) { + playQueue.remove(index); + } + } + + @Override + public void onStartDrag(final PlayQueueItemHolder viewHolder) { + if (itemTouchHelper != null) { + itemTouchHelper.startDrag(viewHolder); + } + } + }; + } + + @Override // own playback listener + @Nullable + public MediaSource sourceOf(final PlayQueueItem item, final StreamInfo info) { + return (isAudioOnly ? audioResolver : videoResolver).resolve(info); + } + + public void disablePreloadingOfCurrentTrack() { + loadController.disablePreloadingOfCurrentTrack(); + } + + @Nullable + public VideoStream getSelectedVideoStream() { + return (selectedStreamIndex >= 0 && availableStreams != null + && availableStreams.size() > selectedStreamIndex) + ? availableStreams.get(selectedStreamIndex) : null; + } + + private void updateStreamRelatedViews() { + if (currentMetadata == null) { + return; + } + final StreamInfo info = currentMetadata.getMetadata(); + + binding.qualityTextView.setVisibility(View.GONE); + binding.playbackSpeed.setVisibility(View.GONE); + + binding.playbackEndTime.setVisibility(View.GONE); + binding.playbackLiveSync.setVisibility(View.GONE); + + switch (info.getStreamType()) { + case AUDIO_STREAM: + binding.surfaceView.setVisibility(View.GONE); + binding.endScreen.setVisibility(View.VISIBLE); + binding.playbackEndTime.setVisibility(View.VISIBLE); + break; + + case AUDIO_LIVE_STREAM: + binding.surfaceView.setVisibility(View.GONE); + binding.endScreen.setVisibility(View.VISIBLE); + binding.playbackLiveSync.setVisibility(View.VISIBLE); + break; + + case LIVE_STREAM: + binding.surfaceView.setVisibility(View.VISIBLE); + binding.endScreen.setVisibility(View.GONE); + binding.playbackLiveSync.setVisibility(View.VISIBLE); + break; + + case VIDEO_STREAM: + if (info.getVideoStreams().size() + info.getVideoOnlyStreams().size() == 0) { + break; + } + + availableStreams = currentMetadata.getSortedAvailableVideoStreams(); + selectedStreamIndex = currentMetadata.getSelectedVideoStreamIndex(); + buildQualityMenu(); + + binding.qualityTextView.setVisibility(View.VISIBLE); + binding.surfaceView.setVisibility(View.VISIBLE); + default: + binding.endScreen.setVisibility(View.GONE); + binding.playbackEndTime.setVisibility(View.VISIBLE); + break; + } + + buildPlaybackSpeedMenu(); + binding.playbackSpeed.setVisibility(View.VISIBLE); + } + //endregion + + + + /*////////////////////////////////////////////////////////////////////////// + // Popup menus ("popup" means that they pop up, not that they belong to the popup player) + //////////////////////////////////////////////////////////////////////////*/ + //region + + private void buildQualityMenu() { + if (qualityPopupMenu == null) { + return; + } + qualityPopupMenu.getMenu().removeGroup(POPUP_MENU_ID_QUALITY); + + for (int i = 0; i < availableStreams.size(); i++) { + final VideoStream videoStream = availableStreams.get(i); + qualityPopupMenu.getMenu().add(POPUP_MENU_ID_QUALITY, i, Menu.NONE, MediaFormat + .getNameById(videoStream.getFormatId()) + " " + videoStream.resolution); + } + if (getSelectedVideoStream() != null) { + binding.qualityTextView.setText(getSelectedVideoStream().resolution); + } + qualityPopupMenu.setOnMenuItemClickListener(this); + qualityPopupMenu.setOnDismissListener(this); + } + + private void buildPlaybackSpeedMenu() { + if (playbackSpeedPopupMenu == null) { + return; + } + playbackSpeedPopupMenu.getMenu().removeGroup(POPUP_MENU_ID_PLAYBACK_SPEED); + + for (int i = 0; i < PLAYBACK_SPEEDS.length; i++) { + playbackSpeedPopupMenu.getMenu().add(POPUP_MENU_ID_PLAYBACK_SPEED, i, Menu.NONE, + formatSpeed(PLAYBACK_SPEEDS[i])); + } + binding.playbackSpeed.setText(formatSpeed(getPlaybackSpeed())); + playbackSpeedPopupMenu.setOnMenuItemClickListener(this); + playbackSpeedPopupMenu.setOnDismissListener(this); + } + + private void buildCaptionMenu(final List availableLanguages) { + if (captionPopupMenu == null) { + return; + } + captionPopupMenu.getMenu().removeGroup(POPUP_MENU_ID_CAPTION); + + final String userPreferredLanguage = + prefs.getString(context.getString(R.string.caption_user_set_key), null); + /* + * only search for autogenerated cc as fallback + * if "(auto-generated)" was not already selected + * we are only looking for "(" instead of "(auto-generated)" to hopefully get all + * internationalized variants such as "(automatisch-erzeugt)" and so on + */ + boolean searchForAutogenerated = userPreferredLanguage != null + && !userPreferredLanguage.contains("("); + + // Add option for turning off caption + final MenuItem captionOffItem = captionPopupMenu.getMenu().add(POPUP_MENU_ID_CAPTION, + 0, Menu.NONE, R.string.caption_none); + captionOffItem.setOnMenuItemClickListener(menuItem -> { + final int textRendererIndex = getCaptionRendererIndex(); + if (textRendererIndex != RENDERER_UNAVAILABLE) { + trackSelector.setParameters(trackSelector.buildUponParameters() + .setRendererDisabled(textRendererIndex, true)); + } + prefs.edit().remove(context.getString(R.string.caption_user_set_key)).apply(); + return true; + }); + + // Add all available captions + for (int i = 0; i < availableLanguages.size(); i++) { + final String captionLanguage = availableLanguages.get(i); + final MenuItem captionItem = captionPopupMenu.getMenu().add(POPUP_MENU_ID_CAPTION, + i + 1, Menu.NONE, captionLanguage); + captionItem.setOnMenuItemClickListener(menuItem -> { + final int textRendererIndex = getCaptionRendererIndex(); + if (textRendererIndex != RENDERER_UNAVAILABLE) { + trackSelector.setPreferredTextLanguage(captionLanguage); + trackSelector.setParameters(trackSelector.buildUponParameters() + .setRendererDisabled(textRendererIndex, false)); + prefs.edit().putString(context.getString(R.string.caption_user_set_key), + captionLanguage).apply(); + } + return true; + }); + // apply caption language from previous user preference + if (userPreferredLanguage != null + && (captionLanguage.equals(userPreferredLanguage) + || (searchForAutogenerated && captionLanguage.startsWith(userPreferredLanguage)) + || (userPreferredLanguage.contains("(") && captionLanguage.startsWith( + userPreferredLanguage.substring(0, userPreferredLanguage.indexOf('(')))))) { + final int textRendererIndex = getCaptionRendererIndex(); + if (textRendererIndex != RENDERER_UNAVAILABLE) { + trackSelector.setPreferredTextLanguage(captionLanguage); + trackSelector.setParameters(trackSelector.buildUponParameters() + .setRendererDisabled(textRendererIndex, false)); + } + searchForAutogenerated = false; + } + } + captionPopupMenu.setOnDismissListener(this); + } + + /** + * Called when an item of the quality selector or the playback speed selector is selected. + */ + @Override + public boolean onMenuItemClick(final MenuItem menuItem) { + if (DEBUG) { + Log.d(TAG, "onMenuItemClick() called with: " + + "menuItem = [" + menuItem + "], " + + "menuItem.getItemId = [" + menuItem.getItemId() + "]"); + } + + if (menuItem.getGroupId() == POPUP_MENU_ID_QUALITY) { + final int menuItemIndex = menuItem.getItemId(); + if (selectedStreamIndex == menuItemIndex || availableStreams == null + || availableStreams.size() <= menuItemIndex) { + return true; + } + + saveStreamProgressState(); //TODO added, check if good + final String newResolution = availableStreams.get(menuItemIndex).resolution; + setRecovery(); + setPlaybackQuality(newResolution); + reloadPlayQueueManager(); + + binding.qualityTextView.setText(menuItem.getTitle()); + return true; + } else if (menuItem.getGroupId() == POPUP_MENU_ID_PLAYBACK_SPEED) { + final int speedIndex = menuItem.getItemId(); + final float speed = PLAYBACK_SPEEDS[speedIndex]; + + setPlaybackSpeed(speed); + binding.playbackSpeed.setText(formatSpeed(speed)); + } + + return false; + } + + /** + * Called when some popup menu is dismissed. + */ + @Override + public void onDismiss(final PopupMenu menu) { + if (DEBUG) { + Log.d(TAG, "onDismiss() called with: menu = [" + menu + "]"); + } + isSomePopupMenuVisible = false; //TODO check if this works + if (getSelectedVideoStream() != null) { + binding.qualityTextView.setText(getSelectedVideoStream().resolution); + } + if (isPlaying()) { + hideControls(DEFAULT_CONTROLS_DURATION, 0); + hideSystemUIIfNeeded(); + } + } + + private void onQualitySelectorClicked() { + if (DEBUG) { + Log.d(TAG, "onQualitySelectorClicked() called"); + } + qualityPopupMenu.show(); + isSomePopupMenuVisible = true; + + final VideoStream videoStream = getSelectedVideoStream(); + if (videoStream != null) { + final String qualityText = MediaFormat.getNameById(videoStream.getFormatId()) + " " + + videoStream.resolution; + binding.qualityTextView.setText(qualityText); + } + + saveWasPlaying(); + } + + private void onPlaybackSpeedClicked() { + if (DEBUG) { + Log.d(TAG, "onPlaybackSpeedClicked() called"); + } + if (videoPlayerSelected()) { + PlaybackParameterDialog.newInstance(getPlaybackSpeed(), getPlaybackPitch(), + getPlaybackSkipSilence(), this::setPlaybackParameters) + .show(getParentActivity().getSupportFragmentManager(), null); + } else { + playbackSpeedPopupMenu.show(); + isSomePopupMenuVisible = true; + } + } + + private void onCaptionClicked() { + if (DEBUG) { + Log.d(TAG, "onCaptionClicked() called"); + } + captionPopupMenu.show(); + isSomePopupMenuVisible = true; + } + + private void setPlaybackQuality(final String quality) { + videoResolver.setPlaybackQuality(quality); + } + //endregion + + + + /*////////////////////////////////////////////////////////////////////////// + // Captions (text tracks) + //////////////////////////////////////////////////////////////////////////*/ + //region + + private void setupSubtitleView() { + final float captionScale = PlayerHelper.getCaptionScale(context); + final CaptionStyleCompat captionStyle = PlayerHelper.getCaptionStyle(context); + if (popupPlayerSelected()) { + final float captionRatio = (captionScale - 1.0f) / 5.0f + 1.0f; + binding.subtitleView.setFractionalTextSize( + SubtitleView.DEFAULT_TEXT_SIZE_FRACTION * captionRatio); + } else { + final DisplayMetrics metrics = context.getResources().getDisplayMetrics(); + final int minimumLength = Math.min(metrics.heightPixels, metrics.widthPixels); + final float captionRatioInverse = 20f + 4f * (1.0f - captionScale); + binding.subtitleView.setFixedTextSize( + TypedValue.COMPLEX_UNIT_PX, (float) minimumLength / captionRatioInverse); + } + binding.subtitleView.setApplyEmbeddedStyles(captionStyle == CaptionStyleCompat.DEFAULT); + binding.subtitleView.setStyle(captionStyle); + } + + private void onTextTracksChanged() { + final int textRenderer = getCaptionRendererIndex(); + + if (binding == null) { + return; + } + if (trackSelector.getCurrentMappedTrackInfo() == null + || textRenderer == RENDERER_UNAVAILABLE) { + binding.captionTextView.setVisibility(View.GONE); + return; + } + + final TrackGroupArray textTracks = trackSelector.getCurrentMappedTrackInfo() + .getTrackGroups(textRenderer); + + // Extract all loaded languages + final List availableLanguages = new ArrayList<>(textTracks.length); + for (int i = 0; i < textTracks.length; i++) { + final TrackGroup textTrack = textTracks.get(i); + if (textTrack.length > 0 && textTrack.getFormat(0) != null) { + availableLanguages.add(textTrack.getFormat(0).language); + } + } + + // Normalize mismatching language strings + final String preferredLanguage = trackSelector.getPreferredTextLanguage(); + // Build UI + buildCaptionMenu(availableLanguages); + if (trackSelector.getParameters().getRendererDisabled(textRenderer) + || preferredLanguage == null || (!availableLanguages.contains(preferredLanguage) + && !containsCaseInsensitive(availableLanguages, preferredLanguage))) { + binding.captionTextView.setText(R.string.caption_none); + } else { + binding.captionTextView.setText(preferredLanguage); + } + binding.captionTextView.setVisibility( + availableLanguages.isEmpty() ? View.GONE : View.VISIBLE); + } + + private int getCaptionRendererIndex() { + if (exoPlayerIsNull()) { + return RENDERER_UNAVAILABLE; + } + + for (int t = 0; t < simpleExoPlayer.getRendererCount(); t++) { + if (simpleExoPlayer.getRendererType(t) == C.TRACK_TYPE_TEXT) { + return t; + } + } + + return RENDERER_UNAVAILABLE; + } + //endregion + + + + /*////////////////////////////////////////////////////////////////////////// + // Click listeners + //////////////////////////////////////////////////////////////////////////*/ + //region + + @Override + public void onClick(final View v) { + if (DEBUG) { + Log.d(TAG, "onClick() called with: v = [" + v + "]"); + } + if (v.getId() == binding.qualityTextView.getId()) { + onQualitySelectorClicked(); + } else if (v.getId() == binding.playbackSpeed.getId()) { + onPlaybackSpeedClicked(); + } else if (v.getId() == binding.resizeTextView.getId()) { + onResizeClicked(); + } else if (v.getId() == binding.captionTextView.getId()) { + onCaptionClicked(); + } else if (v.getId() == binding.playbackLiveSync.getId()) { + seekToDefault(); + } else if (v.getId() == binding.playPauseButton.getId()) { + playPause(); + } else if (v.getId() == binding.playPreviousButton.getId()) { + playPrevious(); + } else if (v.getId() == binding.playNextButton.getId()) { + playNext(); + } else if (v.getId() == binding.queueButton.getId()) { + onQueueClicked(); + return; + } else if (v.getId() == binding.segmentsButton.getId()) { + onSegmentsClicked(); + return; + } else if (v.getId() == binding.repeatButton.getId()) { + onRepeatClicked(); + return; + } else if (v.getId() == binding.shuffleButton.getId()) { + onShuffleClicked(); + return; + } else if (v.getId() == binding.moreOptionsButton.getId()) { + onMoreOptionsClicked(); + } else if (v.getId() == binding.share.getId()) { + onShareClicked(); + } else if (v.getId() == binding.playWithKodi.getId()) { + onPlayWithKodiClicked(); + } else if (v.getId() == binding.openInBrowser.getId()) { + onOpenInBrowserClicked(); + } else if (v.getId() == binding.fullScreenButton.getId()) { + setRecovery(); + NavigationHelper.playOnMainPlayer(context, playQueue, true); + return; + } else if (v.getId() == binding.screenRotationButton.getId()) { + // Only if it's not a vertical video or vertical video but in landscape with locked + // orientation a screen orientation can be changed automatically + if (!isVerticalVideo + || (service.isLandscape() && globalScreenOrientationLocked(context))) { + fragmentListener.onScreenRotationButtonClicked(); + } else { + toggleFullscreen(); + } + } else if (v.getId() == binding.switchMute.getId()) { + onMuteUnmuteButtonClicked(); + } else if (v.getId() == binding.playerCloseButton.getId()) { + context.sendBroadcast(new Intent(VideoDetailFragment.ACTION_HIDE_MAIN_PLAYER)); + } + + if (currentState != STATE_COMPLETED) { + controlsVisibilityHandler.removeCallbacksAndMessages(null); + showHideShadow(true, DEFAULT_CONTROLS_DURATION); + animateView(binding.playbackControlRoot, true, DEFAULT_CONTROLS_DURATION, 0, () -> { + if (currentState == STATE_PLAYING && !isSomePopupMenuVisible) { + if (v.getId() == binding.playPauseButton.getId() + // Hide controls in fullscreen immediately + || (v.getId() == binding.screenRotationButton.getId() + && isFullscreen)) { + hideControls(0, 0); + } else { + hideControls(DEFAULT_CONTROLS_DURATION, DEFAULT_CONTROLS_HIDE_TIME); + } + } + }); + } + } + + @Override + public boolean onLongClick(final View v) { + if (v.getId() == binding.moreOptionsButton.getId() && isFullscreen) { + fragmentListener.onMoreOptionsLongClicked(); + hideControls(0, 0); + hideSystemUIIfNeeded(); + } + return true; + } + + public boolean onKeyDown(final int keyCode) { + switch (keyCode) { + default: + break; + case KeyEvent.KEYCODE_SPACE: + if (isFullscreen) { + playPause(); + } + break; + case KeyEvent.KEYCODE_BACK: + if (DeviceUtils.isTv(context) && isControlsVisible()) { + hideControls(0, 0); + return true; + } + break; + case KeyEvent.KEYCODE_DPAD_UP: + case KeyEvent.KEYCODE_DPAD_LEFT: + case KeyEvent.KEYCODE_DPAD_DOWN: + case KeyEvent.KEYCODE_DPAD_RIGHT: + case KeyEvent.KEYCODE_DPAD_CENTER: + if (binding.getRoot().hasFocus() && !binding.playbackControlRoot.hasFocus()) { + // do not interfere with focus in playlist etc. + return false; + } + + if (currentState == Player.STATE_BLOCKED) { + return true; + } + + if (!isControlsVisible()) { + if (!isQueueVisible) { + binding.playPauseButton.requestFocus(); + } + showControlsThenHide(); + showSystemUIPartially(); + return true; + } else { + hideControls(DEFAULT_CONTROLS_DURATION, DPAD_CONTROLS_HIDE_TIME); + } + break; + } + + return false; + } + + private void onMoreOptionsClicked() { + if (DEBUG) { + Log.d(TAG, "onMoreOptionsClicked() called"); + } + + final boolean isMoreControlsVisible = + binding.secondaryControls.getVisibility() == View.VISIBLE; + + animateRotation(binding.moreOptionsButton, DEFAULT_CONTROLS_DURATION, + isMoreControlsVisible ? 0 : 180); + animateView(binding.secondaryControls, SLIDE_AND_ALPHA, !isMoreControlsVisible, + DEFAULT_CONTROLS_DURATION, 0, + () -> { + // Fix for a ripple effect on background drawable. + // When view returns from GONE state it takes more milliseconds than returning + // from INVISIBLE state. And the delay makes ripple background end to fast + if (isMoreControlsVisible) { + binding.secondaryControls.setVisibility(View.INVISIBLE); + } + }); + showControls(DEFAULT_CONTROLS_DURATION); + } + + private void onShareClicked() { + // share video at the current time (youtube.com/watch?v=ID&t=SECONDS) + // Timestamp doesn't make sense in a live stream so drop it + + final int ts = binding.playbackSeekBar.getProgress() / 1000; + String videoUrl = getVideoUrl(); + if (!isLive() && ts >= 0 && currentMetadata != null + && currentMetadata.getMetadata().getServiceId() == YouTube.getServiceId()) { + videoUrl += ("&t=" + ts); + } + ShareUtils.shareUrl(context, getVideoTitle(), videoUrl); + } + + private void onPlayWithKodiClicked() { + if (currentMetadata != null) { + pause(); + try { + NavigationHelper.playWithKore(context, Uri.parse(getVideoUrl())); + } catch (final Exception e) { + if (DEBUG) { + Log.i(TAG, "Failed to start kore", e); + } + KoreUtil.showInstallKoreDialog(getParentActivity()); + } + } + } + + private void onOpenInBrowserClicked() { + if (currentMetadata != null) { + ShareUtils.openUrlInBrowser(getParentActivity(), + currentMetadata.getMetadata().getOriginalUrl()); + } + } + //endregion + + + + /*////////////////////////////////////////////////////////////////////////// + // Video size, resize, orientation, fullscreen + //////////////////////////////////////////////////////////////////////////*/ + //region + + private void setupScreenRotationButton() { + binding.screenRotationButton.setVisibility(videoPlayerSelected() + && (globalScreenOrientationLocked(context) || isVerticalVideo + || DeviceUtils.isTablet(context)) + ? View.VISIBLE : View.GONE); + binding.screenRotationButton.setImageDrawable(AppCompatResources.getDrawable(context, + isFullscreen ? R.drawable.ic_fullscreen_exit_white_24dp + : R.drawable.ic_fullscreen_white_24dp)); + } + + private void setResizeMode(@AspectRatioFrameLayout.ResizeMode final int resizeMode) { + binding.surfaceView.setResizeMode(resizeMode); + binding.resizeTextView.setText(PlayerHelper.resizeTypeOf(context, resizeMode)); + } + + void onResizeClicked() { + if (binding != null) { + setResizeMode(nextResizeModeAndSaveToPrefs(this, binding.surfaceView.getResizeMode())); + } + } + + @Override // exoplayer listener + public void onVideoSizeChanged(final int width, final int height, + final int unappliedRotationDegrees, + final float pixelWidthHeightRatio) { + if (DEBUG) { + Log.d(TAG, "onVideoSizeChanged() called with: " + + "width / height = [" + width + " / " + height + + " = " + (((float) width) / height) + "], " + + "unappliedRotationDegrees = [" + unappliedRotationDegrees + "], " + + "pixelWidthHeightRatio = [" + pixelWidthHeightRatio + "]"); + } + + binding.surfaceView.setAspectRatio(((float) width) / height); + isVerticalVideo = width < height; + + if (globalScreenOrientationLocked(context) + && isFullscreen + && service.isLandscape() == isVerticalVideo + && !DeviceUtils.isTv(context) + && !DeviceUtils.isTablet(context) + && fragmentListener != null) { + // set correct orientation + fragmentListener.onScreenRotationButtonClicked(); + } + + setupScreenRotationButton(); + } + + public void toggleFullscreen() { + if (DEBUG) { + Log.d(TAG, "toggleFullscreen() called"); + } + if (popupPlayerSelected() || exoPlayerIsNull() || currentMetadata == null + || fragmentListener == null) { + return; + } + //changeState(STATE_BLOCKED); TODO check what this does + + isFullscreen = !isFullscreen; + if (!isFullscreen) { + // Apply window insets because Android will not do it when orientation changes + // from landscape to portrait (open vertical video to reproduce) + binding.playbackControlRoot.setPadding(0, 0, 0, 0); + } else { + // Android needs tens milliseconds to send new insets but a user is able to see + // how controls changes it's position from `0` to `nav bar height` padding. + // So just hide the controls to hide this visual inconsistency + hideControls(0, 0); + } + fragmentListener.onFullscreenStateChanged(isFullscreen); + + if (isFullscreen) { + binding.titleTextView.setVisibility(View.VISIBLE); + binding.channelTextView.setVisibility(View.VISIBLE); + binding.playerCloseButton.setVisibility(View.GONE); + } else { + binding.titleTextView.setVisibility(View.GONE); + binding.channelTextView.setVisibility(View.GONE); + binding.playerCloseButton.setVisibility( + videoPlayerSelected() ? View.VISIBLE : View.GONE); + } + setupScreenRotationButton(); + } + + public void checkLandscape() { + final AppCompatActivity parent = getParentActivity(); + final boolean videoInLandscapeButNotInFullscreen = + service.isLandscape() && !isFullscreen && videoPlayerSelected() && !isAudioOnly; + + final boolean notPaused = currentState != STATE_COMPLETED && currentState != STATE_PAUSED; + if (parent != null + && videoInLandscapeButNotInFullscreen + && notPaused + && !DeviceUtils.isTablet(context)) { + toggleFullscreen(); + } + } + //endregion + + + + /*////////////////////////////////////////////////////////////////////////// + // Gestures + //////////////////////////////////////////////////////////////////////////*/ + //region + + @SuppressWarnings("checkstyle:ParameterNumber") + private void onLayoutChange(final View view, final int l, final int t, final int r, final int b, + final int ol, final int ot, final int or, final int ob) { + if (l != ol || t != ot || r != or || b != ob) { + // Use smaller value to be consistent between screen orientations + // (and to make usage easier) + final int width = r - l; + final int height = b - t; + final int min = Math.min(width, height); + maxGestureLength = (int) (min * MAX_GESTURE_LENGTH); + + if (DEBUG) { + Log.d(TAG, "maxGestureLength = " + maxGestureLength); + } + + binding.volumeProgressBar.setMax(maxGestureLength); + binding.brightnessProgressBar.setMax(maxGestureLength); + + setInitialGestureValues(); + binding.itemsListPanel.getLayoutParams().height + = height - binding.itemsListPanel.getTop(); + } + } + + private void setInitialGestureValues() { + if (audioReactor != null) { + final float currentVolumeNormalized = + (float) audioReactor.getVolume() / audioReactor.getMaxVolume(); + binding.volumeProgressBar.setProgress( + (int) (binding.volumeProgressBar.getMax() * currentVolumeNormalized)); + } + } + + private int distanceFromCloseButton(final MotionEvent popupMotionEvent) { + final int closeOverlayButtonX = closeOverlayBinding.closeButton.getLeft() + + closeOverlayBinding.closeButton.getWidth() / 2; + final int closeOverlayButtonY = closeOverlayBinding.closeButton.getTop() + + closeOverlayBinding.closeButton.getHeight() / 2; + + final float fingerX = popupLayoutParams.x + popupMotionEvent.getX(); + final float fingerY = popupLayoutParams.y + popupMotionEvent.getY(); + + return (int) Math.sqrt(Math.pow(closeOverlayButtonX - fingerX, 2) + + Math.pow(closeOverlayButtonY - fingerY, 2)); + } + + private float getClosingRadius() { + final int buttonRadius = closeOverlayBinding.closeButton.getWidth() / 2; + // 20% wider than the button itself + return buttonRadius * 1.2f; + } + + public boolean isInsideClosingRadius(final MotionEvent popupMotionEvent) { + return distanceFromCloseButton(popupMotionEvent) <= getClosingRadius(); + } + //endregion + + + + /*////////////////////////////////////////////////////////////////////////// + // Activity / fragment binding + //////////////////////////////////////////////////////////////////////////*/ + //region + + public void setFragmentListener(final PlayerServiceEventListener listener) { + fragmentListener = listener; + fragmentIsVisible = true; + // Apply window insets because Android will not do it when orientation changes + // from landscape to portrait + if (!isFullscreen) { + binding.playbackControlRoot.setPadding(0, 0, 0, 0); + } + binding.itemsListPanel.setPadding(0, 0, 0, 0); + notifyQueueUpdateToListeners(); + notifyMetadataUpdateToListeners(); + notifyPlaybackUpdateToListeners(); + triggerProgressUpdate(); + } + + public void removeFragmentListener(final PlayerServiceEventListener listener) { + if (fragmentListener == listener) { + fragmentListener = null; + } + } + + void setActivityListener(final PlayerEventListener listener) { + activityListener = listener; + // TODO why not queue update? + notifyMetadataUpdateToListeners(); + notifyPlaybackUpdateToListeners(); + triggerProgressUpdate(); + } + + void removeActivityListener(final PlayerEventListener listener) { + if (activityListener == listener) { + activityListener = null; + } + } + + void stopActivityBinding() { + if (fragmentListener != null) { + fragmentListener.onServiceStopped(); + fragmentListener = null; + } + if (activityListener != null) { + activityListener.onServiceStopped(); + activityListener = null; + } + } + + /** + * This will be called when a user goes to another app/activity, turns off a screen. + * We don't want to interrupt playback and don't want to see notification so + * next lines of code will enable audio-only playback only if needed + */ + private void onFragmentStopped() { + if (videoPlayerSelected() && (isPlaying() || isLoading())) { + switch (getMinimizeOnExitAction(context)) { + case MINIMIZE_ON_EXIT_MODE_BACKGROUND: + useVideoSource(false); + case MINIMIZE_ON_EXIT_MODE_POPUP: + setRecovery(); + NavigationHelper.playOnPopupPlayer(getParentActivity(), playQueue, true); + case MINIMIZE_ON_EXIT_MODE_NONE: default: + pause(); + } + } + } + + private void notifyQueueUpdateToListeners() { + if (fragmentListener != null && playQueue != null) { + fragmentListener.onQueueUpdate(playQueue); + } + if (activityListener != null && playQueue != null) { + activityListener.onQueueUpdate(playQueue); + } + } + + private void notifyMetadataUpdateToListeners() { + if (fragmentListener != null && currentMetadata != null) { + fragmentListener.onMetadataUpdate(currentMetadata.getMetadata(), playQueue); + } + if (activityListener != null && currentMetadata != null) { + activityListener.onMetadataUpdate(currentMetadata.getMetadata(), playQueue); + } + } + + private void notifyPlaybackUpdateToListeners() { + if (fragmentListener != null && !exoPlayerIsNull() && playQueue != null) { + fragmentListener.onPlaybackUpdate(currentState, getRepeatMode(), + playQueue.isShuffled(), simpleExoPlayer.getPlaybackParameters()); + } + if (activityListener != null && !exoPlayerIsNull() && playQueue != null) { + activityListener.onPlaybackUpdate(currentState, getRepeatMode(), + playQueue.isShuffled(), getPlaybackParameters()); + } + } + + private void notifyProgressUpdateToListeners(final int currentProgress, + final int duration, + final int bufferPercent) { + if (fragmentListener != null) { + fragmentListener.onProgressUpdate(currentProgress, duration, bufferPercent); + } + if (activityListener != null) { + activityListener.onProgressUpdate(currentProgress, duration, bufferPercent); + } + } + + public AppCompatActivity getParentActivity() { + // ! instanceof ViewGroup means that view was added via windowManager for Popup + if (binding == null || !(binding.getRoot().getParent() instanceof ViewGroup)) { + return null; + } + + return (AppCompatActivity) ((ViewGroup) binding.getRoot().getParent()).getContext(); + } + + private void useVideoSource(final boolean video) { + if (playQueue == null || isAudioOnly == !video || audioPlayerSelected()) { + return; + } + + isAudioOnly = !video; + // When a user returns from background controls could be hidden + // but systemUI will be shown 100%. Hide it + if (!isAudioOnly && !isControlsVisible()) { + hideSystemUIIfNeeded(); + } + setRecovery(); + reloadPlayQueueManager(); + } + //endregion + + + /*////////////////////////////////////////////////////////////////////////// + // Getters + //////////////////////////////////////////////////////////////////////////*/ + //region + + public int getCurrentState() { + return currentState; + } + + public boolean exoPlayerIsNull() { + return simpleExoPlayer == null; + } + + public boolean isStopped() { + return exoPlayerIsNull() + || simpleExoPlayer.getPlaybackState() == SimpleExoPlayer.STATE_IDLE; + } + + public boolean isPlaying() { + return !exoPlayerIsNull() && simpleExoPlayer.isPlaying(); + } + + private boolean isLoading() { + return !exoPlayerIsNull() && simpleExoPlayer.isLoading(); + } + + private boolean isLive() { + try { + return !exoPlayerIsNull() && simpleExoPlayer.isCurrentWindowDynamic(); + } catch (@NonNull final IndexOutOfBoundsException e) { + // Why would this even happen =(... but lets log it anyway, better safe than sorry + if (DEBUG) { + Log.d(TAG, "player.isCurrentWindowDynamic() failed: " + e.getMessage()); + e.printStackTrace(); + } + return false; + } + } + + + @NonNull + public Context getContext() { + return context; + } + + @NonNull + public SharedPreferences getPrefs() { + return prefs; + } + + public MediaSessionManager getMediaSessionManager() { + return mediaSessionManager; + } + + + public PlayerType getPlayerType() { + return playerType; + } + + public boolean audioPlayerSelected() { + return playerType == PlayerType.AUDIO; + } + + public boolean videoPlayerSelected() { + return playerType == PlayerType.VIDEO; + } + + public boolean popupPlayerSelected() { + return playerType == PlayerType.POPUP; + } + + + public PlayQueue getPlayQueue() { + return playQueue; + } + + public AudioReactor getAudioReactor() { + return audioReactor; + } + + public GestureDetector getGestureDetector() { + return gestureDetector; + } + + public boolean isFullscreen() { + return isFullscreen; + } + + public boolean isVerticalVideo() { + return isVerticalVideo; + } + + public boolean isPopupClosing() { + return isPopupClosing; + } + + + public boolean isSomePopupMenuVisible() { + return isSomePopupMenuVisible; + } + + public ImageButton getPlayPauseButton() { + return binding.playPauseButton; + } + + public View getClosingOverlayView() { + return closeOverlayBinding.getRoot(); + } + + public ProgressBar getVolumeProgressBar() { + return binding.volumeProgressBar; + } + + public ProgressBar getBrightnessProgressBar() { + return binding.brightnessProgressBar; + } + + public int getMaxGestureLength() { + return maxGestureLength; + } + + public ImageView getVolumeImageView() { + return binding.volumeImageView; + } + + public RelativeLayout getVolumeRelativeLayout() { + return binding.volumeRelativeLayout; + } + + public ImageView getBrightnessImageView() { + return binding.brightnessImageView; + } + + public RelativeLayout getBrightnessRelativeLayout() { + return binding.brightnessRelativeLayout; + } + + public FloatingActionButton getCloseOverlayButton() { + return closeOverlayBinding.closeButton; + } + + public View getLoadingPanel() { + return binding.loadingPanel; + } + + public TextView getCurrentDisplaySeek() { + return binding.currentDisplaySeek; + } + + public TextView getResizingIndicator() { + return binding.resizingIndicator; + } + + @Nullable + public WindowManager.LayoutParams getPopupLayoutParams() { + return popupLayoutParams; + } + + @Nullable + public WindowManager getWindowManager() { + return windowManager; + } + + public float getScreenWidth() { + return screenWidth; + } + + public float getScreenHeight() { + return screenHeight; + } + + public View getRootView() { + return binding.getRoot(); + } + + public ExpandableSurfaceView getSurfaceView() { + return binding.surfaceView; + } + + public PlayQueueAdapter getPlayQueueAdapter() { + return playQueueAdapter; + } + + //endregion +} diff --git a/app/src/main/java/org/schabi/newpipe/player/PlayerServiceBinder.java b/app/src/main/java/org/schabi/newpipe/player/PlayerServiceBinder.java index e8bd7dc85..5c28c6c7b 100644 --- a/app/src/main/java/org/schabi/newpipe/player/PlayerServiceBinder.java +++ b/app/src/main/java/org/schabi/newpipe/player/PlayerServiceBinder.java @@ -5,13 +5,13 @@ import android.os.Binder; import androidx.annotation.NonNull; class PlayerServiceBinder extends Binder { - private final BasePlayer basePlayer; + private final Player player; - PlayerServiceBinder(@NonNull final BasePlayer basePlayer) { - this.basePlayer = basePlayer; + PlayerServiceBinder(@NonNull final Player player) { + this.player = player; } - BasePlayer getPlayerInstance() { - return basePlayer; + Player getPlayerInstance() { + return player; } } diff --git a/app/src/main/java/org/schabi/newpipe/player/VideoPlayer.java b/app/src/main/java/org/schabi/newpipe/player/VideoPlayer.java deleted file mode 100644 index 67ea673c3..000000000 --- a/app/src/main/java/org/schabi/newpipe/player/VideoPlayer.java +++ /dev/null @@ -1,1132 +0,0 @@ -/* - * Copyright 2017 Mauricio Colli - * VideoPlayer.java is part of NewPipe - * - * License: GPL-3.0+ - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package org.schabi.newpipe.player; - -import android.animation.Animator; -import android.animation.AnimatorListenerAdapter; -import android.animation.ObjectAnimator; -import android.animation.PropertyValuesHolder; -import android.animation.ValueAnimator; -import android.content.Context; -import android.content.Intent; -import android.content.SharedPreferences; -import android.graphics.Bitmap; -import android.graphics.Color; -import android.graphics.PorterDuff; -import android.graphics.PorterDuffColorFilter; -import android.os.Build; -import android.os.Handler; -import androidx.preference.PreferenceManager; -import android.util.Log; - -import android.view.Menu; -import android.view.MenuItem; -import android.view.View; -import android.widget.ImageView; -import android.widget.LinearLayout; -import android.widget.PopupMenu; -import android.widget.ProgressBar; -import android.widget.SeekBar; -import android.widget.TextView; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.appcompat.content.res.AppCompatResources; - -import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.PlaybackParameters; -import com.google.android.exoplayer2.Player; -import com.google.android.exoplayer2.source.MediaSource; -import com.google.android.exoplayer2.source.TrackGroup; -import com.google.android.exoplayer2.source.TrackGroupArray; -import com.google.android.exoplayer2.text.CaptionStyleCompat; -import com.google.android.exoplayer2.trackselection.TrackSelectionArray; -import com.google.android.exoplayer2.ui.AspectRatioFrameLayout; -import com.google.android.exoplayer2.ui.SubtitleView; -import com.google.android.exoplayer2.video.VideoListener; - -import org.schabi.newpipe.R; -import org.schabi.newpipe.extractor.MediaFormat; -import org.schabi.newpipe.extractor.stream.StreamInfo; -import org.schabi.newpipe.extractor.stream.VideoStream; -import org.schabi.newpipe.player.helper.PlayerHelper; -import org.schabi.newpipe.player.playqueue.PlayQueueItem; -import org.schabi.newpipe.player.resolver.MediaSourceTag; -import org.schabi.newpipe.player.resolver.VideoPlaybackResolver; -import org.schabi.newpipe.util.AnimationUtils; -import org.schabi.newpipe.views.ExpandableSurfaceView; - -import java.util.ArrayList; -import java.util.List; - -import static org.schabi.newpipe.player.helper.PlayerHelper.formatSpeed; -import static org.schabi.newpipe.player.helper.PlayerHelper.getTimeString; -import static org.schabi.newpipe.util.AnimationUtils.animateView; - -/** - * Base for video players. - * - * @author mauriciocolli - */ -@SuppressWarnings({"WeakerAccess", "unused"}) -public abstract class VideoPlayer extends BasePlayer - implements VideoListener, - SeekBar.OnSeekBarChangeListener, - View.OnClickListener, - Player.EventListener, - PopupMenu.OnMenuItemClickListener, - PopupMenu.OnDismissListener { - public final String TAG; - public static final boolean DEBUG = BasePlayer.DEBUG; - - /*////////////////////////////////////////////////////////////////////////// - // Player - //////////////////////////////////////////////////////////////////////////*/ - - public static final int DEFAULT_CONTROLS_DURATION = 300; // 300 millis - public static final int DEFAULT_CONTROLS_HIDE_TIME = 2000; // 2 Seconds - public static final int DPAD_CONTROLS_HIDE_TIME = 7000; // 7 Seconds - - protected static final int RENDERER_UNAVAILABLE = -1; - - @NonNull - private final VideoPlaybackResolver resolver; - - private List availableStreams; - private int selectedStreamIndex; - - protected boolean wasPlaying = false; - - /*////////////////////////////////////////////////////////////////////////// - // Views - //////////////////////////////////////////////////////////////////////////*/ - - private View rootView; - - private ExpandableSurfaceView surfaceView; - private View surfaceForeground; - - private View loadingPanel; - private ImageView endScreen; - private ImageView controlAnimationView; - - private View controlsRoot; - private TextView currentDisplaySeek; - private View playerTopShadow; - private View playerBottomShadow; - - private View bottomControlsRoot; - private SeekBar playbackSeekBar; - private TextView playbackCurrentTime; - private TextView playbackEndTime; - private TextView playbackLiveSync; - private TextView playbackSpeedTextView; - - private LinearLayout topControlsRoot; - private TextView qualityTextView; - - private SubtitleView subtitleView; - - private TextView resizeView; - private TextView captionTextView; - - private ValueAnimator controlViewAnimator; - private final Handler controlsVisibilityHandler = new Handler(); - - boolean isSomePopupMenuVisible = false; - - private final int qualityPopupMenuGroupId = 69; - private PopupMenu qualityPopupMenu; - - private final int playbackSpeedPopupMenuGroupId = 79; - private PopupMenu playbackSpeedPopupMenu; - - private final int captionPopupMenuGroupId = 89; - private PopupMenu captionPopupMenu; - - /////////////////////////////////////////////////////////////////////////// - - public VideoPlayer(final String debugTag, final Context context) { - super(context); - this.TAG = debugTag; - this.resolver = new VideoPlaybackResolver(context, dataSource, getQualityResolver()); - } - - // workaround to match normalized captions like english to English or deutsch to Deutsch - private static boolean containsCaseInsensitive(final List list, final String toFind) { - for (final String i : list) { - if (i.equalsIgnoreCase(toFind)) { - return true; - } - } - return false; - } - - public void setup(final View view) { - initViews(view); - setup(); - } - - public void initViews(final View view) { - this.rootView = view; - this.surfaceView = view.findViewById(R.id.surfaceView); - this.surfaceForeground = view.findViewById(R.id.surfaceForeground); - this.loadingPanel = view.findViewById(R.id.loading_panel); - this.endScreen = view.findViewById(R.id.endScreen); - this.controlAnimationView = view.findViewById(R.id.controlAnimationView); - this.controlsRoot = view.findViewById(R.id.playbackControlRoot); - this.currentDisplaySeek = view.findViewById(R.id.currentDisplaySeek); - this.playerTopShadow = view.findViewById(R.id.playerTopShadow); - this.playerBottomShadow = view.findViewById(R.id.playerBottomShadow); - this.playbackSeekBar = view.findViewById(R.id.playbackSeekBar); - this.playbackCurrentTime = view.findViewById(R.id.playbackCurrentTime); - this.playbackEndTime = view.findViewById(R.id.playbackEndTime); - this.playbackLiveSync = view.findViewById(R.id.playbackLiveSync); - this.playbackSpeedTextView = view.findViewById(R.id.playbackSpeed); - this.bottomControlsRoot = view.findViewById(R.id.bottomControls); - this.topControlsRoot = view.findViewById(R.id.topControls); - this.qualityTextView = view.findViewById(R.id.qualityTextView); - - this.subtitleView = view.findViewById(R.id.subtitleView); - - final float captionScale = PlayerHelper.getCaptionScale(context); - final CaptionStyleCompat captionStyle = PlayerHelper.getCaptionStyle(context); - setupSubtitleView(subtitleView, captionScale, captionStyle); - - this.resizeView = view.findViewById(R.id.resizeTextView); - resizeView.setText(PlayerHelper - .resizeTypeOf(context, getSurfaceView().getResizeMode())); - - this.captionTextView = view.findViewById(R.id.captionTextView); - - playbackSeekBar.getThumb() - .setColorFilter(new PorterDuffColorFilter(Color.RED, PorterDuff.Mode.SRC_IN)); - this.playbackSeekBar.getProgressDrawable() - .setColorFilter(new PorterDuffColorFilter(Color.RED, PorterDuff.Mode.MULTIPLY)); - - this.qualityPopupMenu = new PopupMenu(context, qualityTextView); - this.playbackSpeedPopupMenu = new PopupMenu(context, playbackSpeedTextView); - this.captionPopupMenu = new PopupMenu(context, captionTextView); - - ((ProgressBar) this.loadingPanel.findViewById(R.id.progressBarLoadingPanel)) - .getIndeterminateDrawable() - .setColorFilter(new PorterDuffColorFilter(Color.WHITE, PorterDuff.Mode.MULTIPLY)); - } - - protected abstract void setupSubtitleView(@NonNull SubtitleView view, float captionScale, - @NonNull CaptionStyleCompat captionStyle); - - @Override - public void initListeners() { - playbackSeekBar.setOnSeekBarChangeListener(this); - playbackSpeedTextView.setOnClickListener(this); - qualityTextView.setOnClickListener(this); - captionTextView.setOnClickListener(this); - resizeView.setOnClickListener(this); - playbackLiveSync.setOnClickListener(this); - } - - @Override - public void initPlayer(final boolean playOnReady) { - super.initPlayer(playOnReady); - - // Setup video view - simpleExoPlayer.setVideoSurfaceView(surfaceView); - simpleExoPlayer.addVideoListener(this); - - // Setup subtitle view - simpleExoPlayer.addTextOutput(cues -> subtitleView.onCues(cues)); - - // Setup audio session with onboard equalizer - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - trackSelector.setParameters(trackSelector.buildUponParameters() - .setTunnelingAudioSessionId(C.generateAudioSessionIdV21(context))); - } - } - - @Override - public void handleIntent(final Intent intent) { - if (intent == null) { - return; - } - - if (intent.hasExtra(PLAYBACK_QUALITY)) { - setPlaybackQuality(intent.getStringExtra(PLAYBACK_QUALITY)); - } - - super.handleIntent(intent); - } - - /*////////////////////////////////////////////////////////////////////////// - // UI Builders - //////////////////////////////////////////////////////////////////////////*/ - - public void buildQualityMenu() { - if (qualityPopupMenu == null) { - return; - } - - qualityPopupMenu.getMenu().removeGroup(qualityPopupMenuGroupId); - for (int i = 0; i < availableStreams.size(); i++) { - final VideoStream videoStream = availableStreams.get(i); - qualityPopupMenu.getMenu().add(qualityPopupMenuGroupId, i, Menu.NONE, MediaFormat - .getNameById(videoStream.getFormatId()) + " " + videoStream.resolution); - } - if (getSelectedVideoStream() != null) { - qualityTextView.setText(getSelectedVideoStream().resolution); - } - qualityPopupMenu.setOnMenuItemClickListener(this); - qualityPopupMenu.setOnDismissListener(this); - } - - private void buildPlaybackSpeedMenu() { - if (playbackSpeedPopupMenu == null) { - return; - } - - playbackSpeedPopupMenu.getMenu().removeGroup(playbackSpeedPopupMenuGroupId); - for (int i = 0; i < PLAYBACK_SPEEDS.length; i++) { - playbackSpeedPopupMenu.getMenu().add(playbackSpeedPopupMenuGroupId, i, Menu.NONE, - formatSpeed(PLAYBACK_SPEEDS[i])); - } - playbackSpeedTextView.setText(formatSpeed(getPlaybackSpeed())); - playbackSpeedPopupMenu.setOnMenuItemClickListener(this); - playbackSpeedPopupMenu.setOnDismissListener(this); - } - - private void buildCaptionMenu(final List availableLanguages) { - if (captionPopupMenu == null) { - return; - } - captionPopupMenu.getMenu().removeGroup(captionPopupMenuGroupId); - - final String userPreferredLanguage = PreferenceManager.getDefaultSharedPreferences(context) - .getString(context.getString(R.string.caption_user_set_key), null); - /* - * only search for autogenerated cc as fallback - * if "(auto-generated)" was not already selected - * we are only looking for "(" instead of "(auto-generated)" to hopefully get all - * internationalized variants such as "(automatisch-erzeugt)" and so on - */ - boolean searchForAutogenerated = userPreferredLanguage != null - && !userPreferredLanguage.contains("("); - - // Add option for turning off caption - final MenuItem captionOffItem = captionPopupMenu.getMenu().add(captionPopupMenuGroupId, - 0, Menu.NONE, R.string.caption_none); - captionOffItem.setOnMenuItemClickListener(menuItem -> { - final int textRendererIndex = getRendererIndex(C.TRACK_TYPE_TEXT); - if (textRendererIndex != RENDERER_UNAVAILABLE) { - trackSelector.setParameters(trackSelector.buildUponParameters() - .setRendererDisabled(textRendererIndex, true)); - } - final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); - prefs.edit().remove(context.getString(R.string.caption_user_set_key)).commit(); - return true; - }); - - // Add all available captions - for (int i = 0; i < availableLanguages.size(); i++) { - final String captionLanguage = availableLanguages.get(i); - final MenuItem captionItem = captionPopupMenu.getMenu().add(captionPopupMenuGroupId, - i + 1, Menu.NONE, captionLanguage); - captionItem.setOnMenuItemClickListener(menuItem -> { - final int textRendererIndex = getRendererIndex(C.TRACK_TYPE_TEXT); - if (textRendererIndex != RENDERER_UNAVAILABLE) { - trackSelector.setPreferredTextLanguage(captionLanguage); - trackSelector.setParameters(trackSelector.buildUponParameters() - .setRendererDisabled(textRendererIndex, false)); - final SharedPreferences prefs = PreferenceManager - .getDefaultSharedPreferences(context); - prefs.edit().putString(context.getString(R.string.caption_user_set_key), - captionLanguage).commit(); - } - return true; - }); - // apply caption language from previous user preference - if (userPreferredLanguage != null - && (captionLanguage.equals(userPreferredLanguage) - || (searchForAutogenerated && captionLanguage.startsWith(userPreferredLanguage)) - || (userPreferredLanguage.contains("(") && captionLanguage.startsWith( - userPreferredLanguage.substring(0, userPreferredLanguage.indexOf('(')))))) { - final int textRendererIndex = getRendererIndex(C.TRACK_TYPE_TEXT); - if (textRendererIndex != RENDERER_UNAVAILABLE) { - trackSelector.setPreferredTextLanguage(captionLanguage); - trackSelector.setParameters(trackSelector.buildUponParameters() - .setRendererDisabled(textRendererIndex, false)); - } - searchForAutogenerated = false; - } - } - captionPopupMenu.setOnDismissListener(this); - } - - private void updateStreamRelatedViews() { - if (getCurrentMetadata() == null) { - return; - } - - final MediaSourceTag tag = getCurrentMetadata(); - final StreamInfo metadata = tag.getMetadata(); - - qualityTextView.setVisibility(View.GONE); - playbackSpeedTextView.setVisibility(View.GONE); - - playbackEndTime.setVisibility(View.GONE); - playbackLiveSync.setVisibility(View.GONE); - - switch (metadata.getStreamType()) { - case AUDIO_STREAM: - surfaceView.setVisibility(View.GONE); - endScreen.setVisibility(View.VISIBLE); - playbackEndTime.setVisibility(View.VISIBLE); - break; - - case AUDIO_LIVE_STREAM: - surfaceView.setVisibility(View.GONE); - endScreen.setVisibility(View.VISIBLE); - playbackLiveSync.setVisibility(View.VISIBLE); - break; - - case LIVE_STREAM: - surfaceView.setVisibility(View.VISIBLE); - endScreen.setVisibility(View.GONE); - playbackLiveSync.setVisibility(View.VISIBLE); - break; - - case VIDEO_STREAM: - if (metadata.getVideoStreams().size() + metadata.getVideoOnlyStreams().size() - == 0) { - break; - } - - availableStreams = tag.getSortedAvailableVideoStreams(); - selectedStreamIndex = tag.getSelectedVideoStreamIndex(); - buildQualityMenu(); - - qualityTextView.setVisibility(View.VISIBLE); - surfaceView.setVisibility(View.VISIBLE); - default: - endScreen.setVisibility(View.GONE); - playbackEndTime.setVisibility(View.VISIBLE); - break; - } - - buildPlaybackSpeedMenu(); - playbackSpeedTextView.setVisibility(View.VISIBLE); - } - - /*////////////////////////////////////////////////////////////////////////// - // Playback Listener - //////////////////////////////////////////////////////////////////////////*/ - - protected abstract VideoPlaybackResolver.QualityResolver getQualityResolver(); - - protected void onMetadataChanged(@NonNull final MediaSourceTag tag) { - super.onMetadataChanged(tag); - updateStreamRelatedViews(); - } - - @Override - @Nullable - public MediaSource sourceOf(final PlayQueueItem item, final StreamInfo info) { - return resolver.resolve(info); - } - - /*////////////////////////////////////////////////////////////////////////// - // States Implementation - //////////////////////////////////////////////////////////////////////////*/ - - @Override - public void onBlocked() { - super.onBlocked(); - - controlsVisibilityHandler.removeCallbacksAndMessages(null); - animateView(controlsRoot, false, DEFAULT_CONTROLS_DURATION); - - playbackSeekBar.setEnabled(false); - playbackSeekBar.getThumb() - .setColorFilter(new PorterDuffColorFilter(Color.RED, PorterDuff.Mode.SRC_IN)); - - loadingPanel.setBackgroundColor(Color.BLACK); - animateView(loadingPanel, true, 0); - animateView(surfaceForeground, true, 100); - } - - @Override - public void onPlaying() { - super.onPlaying(); - - updateStreamRelatedViews(); - - showAndAnimateControl(-1, true); - - playbackSeekBar.setEnabled(true); - playbackSeekBar.getThumb() - .setColorFilter(new PorterDuffColorFilter(Color.RED, PorterDuff.Mode.SRC_IN)); - - loadingPanel.setVisibility(View.GONE); - - animateView(currentDisplaySeek, AnimationUtils.Type.SCALE_AND_ALPHA, false, 200); - } - - @Override - public void onBuffering() { - if (DEBUG) { - Log.d(TAG, "onBuffering() called"); - } - loadingPanel.setBackgroundColor(Color.TRANSPARENT); - } - - @Override - public void onPaused() { - if (DEBUG) { - Log.d(TAG, "onPaused() called"); - } - showControls(400); - loadingPanel.setVisibility(View.GONE); - } - - @Override - public void onPausedSeek() { - if (DEBUG) { - Log.d(TAG, "onPausedSeek() called"); - } - showAndAnimateControl(-1, true); - } - - @Override - public void onCompleted() { - super.onCompleted(); - - showControls(500); - animateView(currentDisplaySeek, AnimationUtils.Type.SCALE_AND_ALPHA, false, 200); - loadingPanel.setVisibility(View.GONE); - - animateView(surfaceForeground, true, 100); - } - - /*////////////////////////////////////////////////////////////////////////// - // ExoPlayer Video Listener - //////////////////////////////////////////////////////////////////////////*/ - - @Override - public void onTracksChanged(final TrackGroupArray trackGroups, - final TrackSelectionArray trackSelections) { - super.onTracksChanged(trackGroups, trackSelections); - onTextTrackUpdate(); - } - - @Override - public void onPlaybackParametersChanged(final PlaybackParameters playbackParameters) { - super.onPlaybackParametersChanged(playbackParameters); - playbackSpeedTextView.setText(formatSpeed(playbackParameters.speed)); - } - - @Override - public void onVideoSizeChanged(final int width, final int height, - final int unappliedRotationDegrees, - final float pixelWidthHeightRatio) { - if (DEBUG) { - Log.d(TAG, "onVideoSizeChanged() called with: " - + "width / height = [" + width + " / " + height - + " = " + (((float) width) / height) + "], " - + "unappliedRotationDegrees = [" + unappliedRotationDegrees + "], " - + "pixelWidthHeightRatio = [" + pixelWidthHeightRatio + "]"); - } - getSurfaceView().setAspectRatio(((float) width) / height); - } - - @Override - public void onRenderedFirstFrame() { - animateView(surfaceForeground, false, 100); - } - - /*////////////////////////////////////////////////////////////////////////// - // ExoPlayer Track Updates - //////////////////////////////////////////////////////////////////////////*/ - - private void onTextTrackUpdate() { - final int textRenderer = getRendererIndex(C.TRACK_TYPE_TEXT); - - if (captionTextView == null) { - return; - } - if (trackSelector.getCurrentMappedTrackInfo() == null - || textRenderer == RENDERER_UNAVAILABLE) { - captionTextView.setVisibility(View.GONE); - return; - } - - final TrackGroupArray textTracks = trackSelector.getCurrentMappedTrackInfo() - .getTrackGroups(textRenderer); - - // Extract all loaded languages - final List availableLanguages = new ArrayList<>(textTracks.length); - for (int i = 0; i < textTracks.length; i++) { - final TrackGroup textTrack = textTracks.get(i); - if (textTrack.length > 0 && textTrack.getFormat(0) != null) { - availableLanguages.add(textTrack.getFormat(0).language); - } - } - - // Normalize mismatching language strings - final String preferredLanguage = trackSelector.getPreferredTextLanguage(); - // Build UI - buildCaptionMenu(availableLanguages); - if (trackSelector.getParameters().getRendererDisabled(textRenderer) - || preferredLanguage == null || (!availableLanguages.contains(preferredLanguage) - && !containsCaseInsensitive(availableLanguages, preferredLanguage))) { - captionTextView.setText(R.string.caption_none); - } else { - captionTextView.setText(preferredLanguage); - } - captionTextView.setVisibility(availableLanguages.isEmpty() ? View.GONE : View.VISIBLE); - } - - /*////////////////////////////////////////////////////////////////////////// - // General Player - //////////////////////////////////////////////////////////////////////////*/ - - @Override - public void onPrepared(final boolean playWhenReady) { - if (DEBUG) { - Log.d(TAG, "onPrepared() called with: playWhenReady = [" + playWhenReady + "]"); - } - - playbackSeekBar.setMax((int) simpleExoPlayer.getDuration()); - playbackEndTime.setText(getTimeString((int) simpleExoPlayer.getDuration())); - playbackSpeedTextView.setText(formatSpeed(getPlaybackSpeed())); - - super.onPrepared(playWhenReady); - } - - @Override - public void destroy() { - super.destroy(); - if (endScreen != null) { - endScreen.setImageBitmap(null); - } - } - - @Override - public void onUpdateProgress(final int currentProgress, final int duration, - final int bufferPercent) { - if (!isPrepared()) { - return; - } - - if (duration != playbackSeekBar.getMax()) { - playbackEndTime.setText(getTimeString(duration)); - playbackSeekBar.setMax(duration); - } - if (currentState != STATE_PAUSED) { - if (currentState != STATE_PAUSED_SEEK) { - playbackSeekBar.setProgress(currentProgress); - } - playbackCurrentTime.setText(getTimeString(currentProgress)); - } - if (simpleExoPlayer.isLoading() || bufferPercent > 90) { - playbackSeekBar.setSecondaryProgress( - (int) (playbackSeekBar.getMax() * ((float) bufferPercent / 100))); - } - if (DEBUG && bufferPercent % 20 == 0) { //Limit log - Log.d(TAG, "updateProgress() called with: " - + "isVisible = " + isControlsVisible() + ", " - + "currentProgress = [" + currentProgress + "], " - + "duration = [" + duration + "], bufferPercent = [" + bufferPercent + "]"); - } - playbackLiveSync.setClickable(!isLiveEdge()); - } - - @Override - public void onLoadingComplete(final String imageUri, final View view, - final Bitmap loadedImage) { - super.onLoadingComplete(imageUri, view, loadedImage); - if (loadedImage != null) { - endScreen.setImageBitmap(loadedImage); - } - } - - protected void toggleFullscreen() { - changeState(STATE_BLOCKED); - } - - @Override - public void onFastRewind() { - super.onFastRewind(); - showAndAnimateControl(R.drawable.ic_fast_rewind_white_24dp, true); - } - - @Override - public void onFastForward() { - super.onFastForward(); - showAndAnimateControl(R.drawable.ic_fast_forward_white_24dp, true); - } - - /*////////////////////////////////////////////////////////////////////////// - // OnClick related - //////////////////////////////////////////////////////////////////////////*/ - - @Override - public void onClick(final View v) { - if (DEBUG) { - Log.d(TAG, "onClick() called with: v = [" + v + "]"); - } - if (v.getId() == qualityTextView.getId()) { - onQualitySelectorClicked(); - } else if (v.getId() == playbackSpeedTextView.getId()) { - onPlaybackSpeedClicked(); - } else if (v.getId() == resizeView.getId()) { - onResizeClicked(); - } else if (v.getId() == captionTextView.getId()) { - onCaptionClicked(); - } else if (v.getId() == playbackLiveSync.getId()) { - seekToDefault(); - } - } - - /** - * Called when an item of the quality selector or the playback speed selector is selected. - */ - @Override - public boolean onMenuItemClick(final MenuItem menuItem) { - if (DEBUG) { - Log.d(TAG, "onMenuItemClick() called with: " - + "menuItem = [" + menuItem + "], " - + "menuItem.getItemId = [" + menuItem.getItemId() + "]"); - } - - if (qualityPopupMenuGroupId == menuItem.getGroupId()) { - final int menuItemIndex = menuItem.getItemId(); - if (selectedStreamIndex == menuItemIndex || availableStreams == null - || availableStreams.size() <= menuItemIndex) { - return true; - } - - final String newResolution = availableStreams.get(menuItemIndex).resolution; - setRecovery(); - setPlaybackQuality(newResolution); - reload(); - - qualityTextView.setText(menuItem.getTitle()); - return true; - } else if (playbackSpeedPopupMenuGroupId == menuItem.getGroupId()) { - final int speedIndex = menuItem.getItemId(); - final float speed = PLAYBACK_SPEEDS[speedIndex]; - - setPlaybackSpeed(speed); - playbackSpeedTextView.setText(formatSpeed(speed)); - } - - return false; - } - - /** - * Called when some popup menu is dismissed. - */ - @Override - public void onDismiss(final PopupMenu menu) { - if (DEBUG) { - Log.d(TAG, "onDismiss() called with: menu = [" + menu + "]"); - } - isSomePopupMenuVisible = false; - if (getSelectedVideoStream() != null) { - qualityTextView.setText(getSelectedVideoStream().resolution); - } - } - - public void onQualitySelectorClicked() { - if (DEBUG) { - Log.d(TAG, "onQualitySelectorClicked() called"); - } - qualityPopupMenu.show(); - isSomePopupMenuVisible = true; - - final VideoStream videoStream = getSelectedVideoStream(); - if (videoStream != null) { - final String qualityText = MediaFormat.getNameById(videoStream.getFormatId()) + " " - + videoStream.resolution; - qualityTextView.setText(qualityText); - } - - wasPlaying = simpleExoPlayer.getPlayWhenReady(); - } - - public void onPlaybackSpeedClicked() { - if (DEBUG) { - Log.d(TAG, "onPlaybackSpeedClicked() called"); - } - playbackSpeedPopupMenu.show(); - isSomePopupMenuVisible = true; - } - - private void onCaptionClicked() { - if (DEBUG) { - Log.d(TAG, "onCaptionClicked() called"); - } - captionPopupMenu.show(); - isSomePopupMenuVisible = true; - } - - void onResizeClicked() { - if (getSurfaceView() != null) { - final int currentResizeMode = getSurfaceView().getResizeMode(); - final int newResizeMode = nextResizeMode(currentResizeMode); - setResizeMode(newResizeMode); - } - } - - protected void setResizeMode(@AspectRatioFrameLayout.ResizeMode final int resizeMode) { - getSurfaceView().setResizeMode(resizeMode); - getResizeView().setText(PlayerHelper.resizeTypeOf(context, resizeMode)); - } - - protected abstract int nextResizeMode(@AspectRatioFrameLayout.ResizeMode int resizeMode); - - /*////////////////////////////////////////////////////////////////////////// - // SeekBar Listener - //////////////////////////////////////////////////////////////////////////*/ - - @Override - public void onProgressChanged(final SeekBar seekBar, final int progress, - final boolean fromUser) { - if (DEBUG && fromUser) { - Log.d(TAG, "onProgressChanged() called with: " - + "seekBar = [" + seekBar + "], progress = [" + progress + "]"); - } - //if (fromUser) playbackCurrentTime.setText(getTimeString(progress)); - if (fromUser) { - currentDisplaySeek.setText(getTimeString(progress)); - } - } - - @Override - public void onStartTrackingTouch(final SeekBar seekBar) { - if (DEBUG) { - Log.d(TAG, "onStartTrackingTouch() called with: seekBar = [" + seekBar + "]"); - } - if (getCurrentState() != STATE_PAUSED_SEEK) { - changeState(STATE_PAUSED_SEEK); - } - - wasPlaying = simpleExoPlayer.getPlayWhenReady(); - if (isPlaying()) { - simpleExoPlayer.setPlayWhenReady(false); - } - - showControls(0); - animateView(currentDisplaySeek, AnimationUtils.Type.SCALE_AND_ALPHA, true, - DEFAULT_CONTROLS_DURATION); - } - - @Override - public void onStopTrackingTouch(final SeekBar seekBar) { - if (DEBUG) { - Log.d(TAG, "onStopTrackingTouch() called with: seekBar = [" + seekBar + "]"); - } - - seekTo(seekBar.getProgress()); - if (wasPlaying || simpleExoPlayer.getDuration() == seekBar.getProgress()) { - simpleExoPlayer.setPlayWhenReady(true); - } - - playbackCurrentTime.setText(getTimeString(seekBar.getProgress())); - animateView(currentDisplaySeek, AnimationUtils.Type.SCALE_AND_ALPHA, false, 200); - - if (getCurrentState() == STATE_PAUSED_SEEK) { - changeState(STATE_BUFFERING); - } - if (!isProgressLoopRunning()) { - startProgressLoop(); - } - } - - /*////////////////////////////////////////////////////////////////////////// - // Utils - //////////////////////////////////////////////////////////////////////////*/ - - public int getRendererIndex(final int trackIndex) { - if (simpleExoPlayer == null) { - return RENDERER_UNAVAILABLE; - } - - for (int t = 0; t < simpleExoPlayer.getRendererCount(); t++) { - if (simpleExoPlayer.getRendererType(t) == trackIndex) { - return t; - } - } - - return RENDERER_UNAVAILABLE; - } - - public boolean isControlsVisible() { - return controlsRoot != null && controlsRoot.getVisibility() == View.VISIBLE; - } - - /** - * Show a animation, and depending on goneOnEnd, will stay on the screen or be gone. - * - * @param drawableId the drawable that will be used to animate, - * pass -1 to clear any animation that is visible - * @param goneOnEnd will set the animation view to GONE on the end of the animation - */ - public void showAndAnimateControl(final int drawableId, final boolean goneOnEnd) { - if (DEBUG) { - Log.d(TAG, "showAndAnimateControl() called with: " - + "drawableId = [" + drawableId + "], goneOnEnd = [" + goneOnEnd + "]"); - } - if (controlViewAnimator != null && controlViewAnimator.isRunning()) { - if (DEBUG) { - Log.d(TAG, "showAndAnimateControl: controlViewAnimator.isRunning"); - } - controlViewAnimator.end(); - } - - if (drawableId == -1) { - if (controlAnimationView.getVisibility() == View.VISIBLE) { - controlViewAnimator = ObjectAnimator.ofPropertyValuesHolder(controlAnimationView, - PropertyValuesHolder.ofFloat(View.ALPHA, 1.0f, 0.0f), - PropertyValuesHolder.ofFloat(View.SCALE_X, 1.4f, 1.0f), - PropertyValuesHolder.ofFloat(View.SCALE_Y, 1.4f, 1.0f) - ).setDuration(DEFAULT_CONTROLS_DURATION); - controlViewAnimator.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(final Animator animation) { - controlAnimationView.setVisibility(View.GONE); - } - }); - controlViewAnimator.start(); - } - return; - } - - final float scaleFrom = goneOnEnd ? 1f : 1f; - final float scaleTo = goneOnEnd ? 1.8f : 1.4f; - final float alphaFrom = goneOnEnd ? 1f : 0f; - final float alphaTo = goneOnEnd ? 0f : 1f; - - - controlViewAnimator = ObjectAnimator.ofPropertyValuesHolder(controlAnimationView, - PropertyValuesHolder.ofFloat(View.ALPHA, alphaFrom, alphaTo), - PropertyValuesHolder.ofFloat(View.SCALE_X, scaleFrom, scaleTo), - PropertyValuesHolder.ofFloat(View.SCALE_Y, scaleFrom, scaleTo) - ); - controlViewAnimator.setDuration(goneOnEnd ? 1000 : 500); - controlViewAnimator.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(final Animator animation) { - if (goneOnEnd) { - controlAnimationView.setVisibility(View.GONE); - } else { - controlAnimationView.setVisibility(View.VISIBLE); - } - } - }); - - - controlAnimationView.setVisibility(View.VISIBLE); - controlAnimationView.setImageDrawable(AppCompatResources.getDrawable(context, drawableId)); - controlViewAnimator.start(); - } - - public boolean isSomePopupMenuVisible() { - return isSomePopupMenuVisible; - } - - public void showControlsThenHide() { - if (DEBUG) { - Log.d(TAG, "showControlsThenHide() called"); - } - - final int hideTime = controlsRoot.isInTouchMode() - ? DEFAULT_CONTROLS_HIDE_TIME - : DPAD_CONTROLS_HIDE_TIME; - - showHideShadow(true, DEFAULT_CONTROLS_DURATION, 0); - animateView(controlsRoot, true, DEFAULT_CONTROLS_DURATION, 0, - () -> hideControls(DEFAULT_CONTROLS_DURATION, hideTime)); - } - - public void showControls(final long duration) { - if (DEBUG) { - Log.d(TAG, "showControls() called"); - } - controlsVisibilityHandler.removeCallbacksAndMessages(null); - showHideShadow(true, duration, 0); - animateView(controlsRoot, true, duration); - } - - public void safeHideControls(final long duration, final long delay) { - if (DEBUG) { - Log.d(TAG, "safeHideControls() called with: delay = [" + delay + "]"); - } - if (rootView.isInTouchMode()) { - controlsVisibilityHandler.removeCallbacksAndMessages(null); - controlsVisibilityHandler.postDelayed( - () -> animateView(controlsRoot, false, duration), delay); - } - } - - public void hideControls(final long duration, final long delay) { - if (DEBUG) { - Log.d(TAG, "hideControls() called with: delay = [" + delay + "]"); - } - controlsVisibilityHandler.removeCallbacksAndMessages(null); - controlsVisibilityHandler.postDelayed(() -> { - showHideShadow(false, duration, 0); - animateView(controlsRoot, false, duration); - }, delay); - } - - public void hideControlsAndButton(final long duration, final long delay, final View button) { - if (DEBUG) { - Log.d(TAG, "hideControls() called with: delay = [" + delay + "]"); - } - controlsVisibilityHandler.removeCallbacksAndMessages(null); - controlsVisibilityHandler - .postDelayed(hideControlsAndButtonHandler(duration, button), delay); - } - - private Runnable hideControlsAndButtonHandler(final long duration, final View videoPlayPause) { - return () -> { - videoPlayPause.setVisibility(View.INVISIBLE); - animateView(controlsRoot, false, duration); - }; - } - - void showHideShadow(final boolean show, final long duration, final long delay) { - animateView(playerTopShadow, show, duration, delay, null); - animateView(playerBottomShadow, show, duration, delay, null); - } - - public abstract void hideSystemUIIfNeeded(); - - /*////////////////////////////////////////////////////////////////////////// - // Getters and Setters - //////////////////////////////////////////////////////////////////////////*/ - - @Nullable - public String getPlaybackQuality() { - return resolver.getPlaybackQuality(); - } - - public void setPlaybackQuality(final String quality) { - this.resolver.setPlaybackQuality(quality); - } - - public ExpandableSurfaceView getSurfaceView() { - return surfaceView; - } - - public boolean wasPlaying() { - return wasPlaying; - } - - @Nullable - public VideoStream getSelectedVideoStream() { - return (selectedStreamIndex >= 0 && availableStreams != null - && availableStreams.size() > selectedStreamIndex) - ? availableStreams.get(selectedStreamIndex) : null; - } - - public Handler getControlsVisibilityHandler() { - return controlsVisibilityHandler; - } - - public View getRootView() { - return rootView; - } - - public void setRootView(final View rootView) { - this.rootView = rootView; - } - - public View getLoadingPanel() { - return loadingPanel; - } - - public ImageView getEndScreen() { - return endScreen; - } - - public ImageView getControlAnimationView() { - return controlAnimationView; - } - - public View getControlsRoot() { - return controlsRoot; - } - - public View getBottomControlsRoot() { - return bottomControlsRoot; - } - - public SeekBar getPlaybackSeekBar() { - return playbackSeekBar; - } - - public TextView getPlaybackCurrentTime() { - return playbackCurrentTime; - } - - public TextView getPlaybackEndTime() { - return playbackEndTime; - } - - public LinearLayout getTopControlsRoot() { - return topControlsRoot; - } - - public TextView getQualityTextView() { - return qualityTextView; - } - - public PopupMenu getQualityPopupMenu() { - return qualityPopupMenu; - } - - public TextView getPlaybackSpeedTextView() { - return playbackSpeedTextView; - } - - public PopupMenu getPlaybackSpeedPopupMenu() { - return playbackSpeedPopupMenu; - } - - public View getSurfaceForeground() { - return surfaceForeground; - } - - public TextView getCurrentDisplaySeek() { - return currentDisplaySeek; - } - - public SubtitleView getSubtitleView() { - return subtitleView; - } - - public TextView getResizeView() { - return resizeView; - } - - public TextView getCaptionTextView() { - return captionTextView; - } -} diff --git a/app/src/main/java/org/schabi/newpipe/player/VideoPlayerImpl.java b/app/src/main/java/org/schabi/newpipe/player/VideoPlayerImpl.java deleted file mode 100644 index 3cbcb87a3..000000000 --- a/app/src/main/java/org/schabi/newpipe/player/VideoPlayerImpl.java +++ /dev/null @@ -1,2110 +0,0 @@ -/* - * Copyright 2017 Mauricio Colli - * Part of NewPipe - * - * License: GPL-3.0+ - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package org.schabi.newpipe.player; - -import android.animation.Animator; -import android.animation.AnimatorListenerAdapter; -import android.annotation.SuppressLint; -import android.content.Intent; -import android.content.IntentFilter; -import android.content.SharedPreferences; -import android.database.ContentObserver; -import android.graphics.Bitmap; -import android.graphics.Color; -import android.graphics.PixelFormat; -import android.net.Uri; -import android.os.Build; -import android.os.Handler; -import android.provider.Settings; -import android.util.DisplayMetrics; -import android.util.Log; -import android.util.TypedValue; -import android.view.GestureDetector; -import android.view.Gravity; -import android.view.KeyEvent; -import android.view.MotionEvent; -import android.view.View; -import android.view.ViewGroup; -import android.view.WindowManager; -import android.view.animation.AnticipateInterpolator; -import android.widget.FrameLayout; -import android.widget.ImageButton; -import android.widget.ImageView; -import android.widget.LinearLayout; -import android.widget.PopupMenu; -import android.widget.ProgressBar; -import android.widget.RelativeLayout; -import android.widget.SeekBar; -import android.widget.TextView; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.appcompat.app.AppCompatActivity; -import androidx.appcompat.content.res.AppCompatResources; -import androidx.core.content.ContextCompat; -import androidx.core.view.DisplayCutoutCompat; -import androidx.core.view.ViewCompat; -import androidx.preference.PreferenceManager; -import androidx.recyclerview.widget.ItemTouchHelper; -import androidx.recyclerview.widget.RecyclerView; - -import com.google.android.exoplayer2.ExoPlaybackException; -import com.google.android.exoplayer2.Player; -import com.google.android.exoplayer2.SimpleExoPlayer; -import com.google.android.exoplayer2.Timeline; -import com.google.android.exoplayer2.source.MediaSource; -import com.google.android.exoplayer2.text.CaptionStyleCompat; -import com.google.android.exoplayer2.ui.AspectRatioFrameLayout; -import com.google.android.exoplayer2.ui.SubtitleView; -import com.google.android.material.floatingactionbutton.FloatingActionButton; -import com.nostra13.universalimageloader.core.assist.FailReason; - -import org.schabi.newpipe.R; -import org.schabi.newpipe.extractor.stream.StreamInfo; -import org.schabi.newpipe.extractor.stream.VideoStream; -import org.schabi.newpipe.fragments.OnScrollBelowItemsListener; -import org.schabi.newpipe.fragments.detail.VideoDetailFragment; -import org.schabi.newpipe.player.event.PlayerEventListener; -import org.schabi.newpipe.player.event.PlayerGestureListener; -import org.schabi.newpipe.player.event.PlayerServiceEventListener; -import org.schabi.newpipe.player.helper.PlaybackParameterDialog; -import org.schabi.newpipe.player.helper.PlayerHelper; -import org.schabi.newpipe.player.playqueue.PlayQueue; -import org.schabi.newpipe.player.playqueue.PlayQueueItem; -import org.schabi.newpipe.player.playqueue.PlayQueueItemBuilder; -import org.schabi.newpipe.player.playqueue.PlayQueueItemHolder; -import org.schabi.newpipe.player.playqueue.PlayQueueItemTouchCallback; -import org.schabi.newpipe.player.resolver.AudioPlaybackResolver; -import org.schabi.newpipe.player.resolver.MediaSourceTag; -import org.schabi.newpipe.player.resolver.VideoPlaybackResolver; -import org.schabi.newpipe.util.AnimationUtils; -import org.schabi.newpipe.util.DeviceUtils; -import org.schabi.newpipe.util.KoreUtil; -import org.schabi.newpipe.util.ListHelper; -import org.schabi.newpipe.util.NavigationHelper; -import org.schabi.newpipe.util.ShareUtils; - -import java.util.List; - -import static org.schabi.newpipe.player.MainPlayer.ACTION_CLOSE; -import static org.schabi.newpipe.player.MainPlayer.ACTION_FAST_FORWARD; -import static org.schabi.newpipe.player.MainPlayer.ACTION_FAST_REWIND; -import static org.schabi.newpipe.player.MainPlayer.ACTION_OPEN_CONTROLS; -import static org.schabi.newpipe.player.MainPlayer.ACTION_PLAY_NEXT; -import static org.schabi.newpipe.player.MainPlayer.ACTION_PLAY_PAUSE; -import static org.schabi.newpipe.player.MainPlayer.ACTION_PLAY_PREVIOUS; -import static org.schabi.newpipe.player.MainPlayer.ACTION_RECREATE_NOTIFICATION; -import static org.schabi.newpipe.player.MainPlayer.ACTION_REPEAT; -import static org.schabi.newpipe.player.MainPlayer.ACTION_SHUFFLE; -import static org.schabi.newpipe.player.helper.PlayerHelper.MinimizeMode.MINIMIZE_ON_EXIT_MODE_BACKGROUND; -import static org.schabi.newpipe.player.helper.PlayerHelper.globalScreenOrientationLocked; -import static org.schabi.newpipe.util.AnimationUtils.Type.SLIDE_AND_ALPHA; -import static org.schabi.newpipe.util.AnimationUtils.animateRotation; -import static org.schabi.newpipe.util.AnimationUtils.animateView; -import static org.schabi.newpipe.util.ListHelper.getPopupResolutionIndex; -import static org.schabi.newpipe.util.ListHelper.getResolutionIndex; -import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage; - -/** - * Unified UI for all players. - * - * @author mauriciocolli - */ - -public class VideoPlayerImpl extends VideoPlayer - implements View.OnLayoutChangeListener, - PlaybackParameterDialog.Callback, - View.OnLongClickListener { - private static final String TAG = ".VideoPlayerImpl"; - - static final String POPUP_SAVED_WIDTH = "popup_saved_width"; - static final String POPUP_SAVED_X = "popup_saved_x"; - static final String POPUP_SAVED_Y = "popup_saved_y"; - private static final int IDLE_WINDOW_FLAGS = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE - | WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM; - private static final int ONGOING_PLAYBACK_WINDOW_FLAGS = IDLE_WINDOW_FLAGS - | WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON; - - private static final float MAX_GESTURE_LENGTH = 0.75f; - - private TextView titleTextView; - private TextView channelTextView; - private RelativeLayout volumeRelativeLayout; - private ProgressBar volumeProgressBar; - private ImageView volumeImageView; - private RelativeLayout brightnessRelativeLayout; - private ProgressBar brightnessProgressBar; - private ImageView brightnessImageView; - private TextView resizingIndicator; - private ImageButton queueButton; - private ImageButton repeatButton; - private ImageButton shuffleButton; - private ImageButton playWithKodi; - private ImageButton openInBrowser; - private ImageButton fullscreenButton; - private ImageButton playerCloseButton; - private ImageButton screenRotationButton; - private ImageButton muteButton; - - private ImageButton playPauseButton; - private ImageButton playPreviousButton; - private ImageButton playNextButton; - - private RelativeLayout queueLayout; - private ImageButton itemsListCloseButton; - private RecyclerView itemsList; - private ItemTouchHelper itemTouchHelper; - - private RelativeLayout playerOverlays; - - private boolean queueVisible; - private MainPlayer.PlayerType playerType = MainPlayer.PlayerType.VIDEO; - - private ImageButton moreOptionsButton; - private ImageButton shareButton; - - private View primaryControls; - private View secondaryControls; - - private int maxGestureLength; - - private boolean audioOnly = false; - private boolean isFullscreen = false; - private boolean isVerticalVideo = false; - private boolean fragmentIsVisible = false; - boolean shouldUpdateOnProgress; - - private final MainPlayer service; - private PlayerServiceEventListener fragmentListener; - private PlayerEventListener activityListener; - private GestureDetector gestureDetector; - private final SharedPreferences defaultPreferences; - private ContentObserver settingsContentObserver; - @NonNull - private final AudioPlaybackResolver resolver; - - // Popup - private WindowManager.LayoutParams popupLayoutParams; - public WindowManager windowManager; - - private View closingOverlayView; - private View closeOverlayView; - private FloatingActionButton closeOverlayButton; - - public boolean isPopupClosing = false; - - private float screenWidth; - private float screenHeight; - private float popupWidth; - private float popupHeight; - private float minimumWidth; - private float minimumHeight; - private float maximumWidth; - private float maximumHeight; - // Popup end - - - @Override - public void handleIntent(final Intent intent) { - if (intent.getStringExtra(VideoPlayer.PLAY_QUEUE_KEY) == null) { - return; - } - - final MainPlayer.PlayerType oldPlayerType = playerType; - choosePlayerTypeFromIntent(intent); - audioOnly = audioPlayerSelected(); - - // We need to setup audioOnly before super(), see "sourceOf" - super.handleIntent(intent); - - if (oldPlayerType != playerType && playQueue != null) { - // If playerType changes from one to another we should reload the player - // (to disable/enable video stream or to set quality) - setRecovery(); - reload(); - } - - setupElementsVisibility(); - setupElementsSize(); - - if (audioPlayerSelected()) { - service.removeViewFromParent(); - } else if (popupPlayerSelected()) { - getRootView().setVisibility(View.VISIBLE); - initPopup(); - initPopupCloseOverlay(); - playPauseButton.requestFocus(); - } else { - getRootView().setVisibility(View.VISIBLE); - initVideoPlayer(); - onQueueClosed(); - // Android TV: without it focus will frame the whole player - playPauseButton.requestFocus(); - - if (simpleExoPlayer.getPlayWhenReady()) { - onPlay(); - } else { - onPause(); - } - } - NavigationHelper.sendPlayerStartedEvent(service); - } - - VideoPlayerImpl(final MainPlayer service) { - super("MainPlayer" + TAG, service); - this.service = service; - this.shouldUpdateOnProgress = true; - this.windowManager = ContextCompat.getSystemService(service, WindowManager.class); - this.defaultPreferences = PreferenceManager.getDefaultSharedPreferences(service); - this.resolver = new AudioPlaybackResolver(context, dataSource); - } - - @SuppressLint("ClickableViewAccessibility") - @Override - public void initViews(final View view) { - super.initViews(view); - this.titleTextView = view.findViewById(R.id.titleTextView); - this.channelTextView = view.findViewById(R.id.channelTextView); - this.volumeRelativeLayout = view.findViewById(R.id.volumeRelativeLayout); - this.volumeProgressBar = view.findViewById(R.id.volumeProgressBar); - this.volumeImageView = view.findViewById(R.id.volumeImageView); - this.brightnessRelativeLayout = view.findViewById(R.id.brightnessRelativeLayout); - this.brightnessProgressBar = view.findViewById(R.id.brightnessProgressBar); - this.brightnessImageView = view.findViewById(R.id.brightnessImageView); - this.resizingIndicator = view.findViewById(R.id.resizing_indicator); - this.queueButton = view.findViewById(R.id.queueButton); - this.repeatButton = view.findViewById(R.id.repeatButton); - this.shuffleButton = view.findViewById(R.id.shuffleButton); - this.playWithKodi = view.findViewById(R.id.playWithKodi); - this.openInBrowser = view.findViewById(R.id.openInBrowser); - this.fullscreenButton = view.findViewById(R.id.fullScreenButton); - this.screenRotationButton = view.findViewById(R.id.screenRotationButton); - this.playerCloseButton = view.findViewById(R.id.playerCloseButton); - this.muteButton = view.findViewById(R.id.switchMute); - - this.playPauseButton = view.findViewById(R.id.playPauseButton); - this.playPreviousButton = view.findViewById(R.id.playPreviousButton); - this.playNextButton = view.findViewById(R.id.playNextButton); - - this.moreOptionsButton = view.findViewById(R.id.moreOptionsButton); - this.primaryControls = view.findViewById(R.id.primaryControls); - this.secondaryControls = view.findViewById(R.id.secondaryControls); - this.shareButton = view.findViewById(R.id.share); - - this.queueLayout = view.findViewById(R.id.playQueuePanel); - this.itemsListCloseButton = view.findViewById(R.id.playQueueClose); - this.itemsList = view.findViewById(R.id.playQueue); - - this.playerOverlays = view.findViewById(R.id.player_overlays); - - closingOverlayView = view.findViewById(R.id.closingOverlay); - - titleTextView.setSelected(true); - channelTextView.setSelected(true); - - // Prevent hiding of bottom sheet via swipe inside queue - this.itemsList.setNestedScrollingEnabled(false); - } - - @Override - protected void setupSubtitleView(final @NonNull SubtitleView view, - final float captionScale, - @NonNull final CaptionStyleCompat captionStyle) { - if (popupPlayerSelected()) { - final float captionRatio = (captionScale - 1.0f) / 5.0f + 1.0f; - view.setFractionalTextSize(SubtitleView.DEFAULT_TEXT_SIZE_FRACTION * captionRatio); - } else { - final DisplayMetrics metrics = context.getResources().getDisplayMetrics(); - final int minimumLength = Math.min(metrics.heightPixels, metrics.widthPixels); - final float captionRatioInverse = 20f + 4f * (1.0f - captionScale); - view.setFixedTextSize(TypedValue.COMPLEX_UNIT_PX, - (float) minimumLength / captionRatioInverse); - } - view.setApplyEmbeddedStyles(captionStyle == CaptionStyleCompat.DEFAULT); - view.setStyle(captionStyle); - } - - /** - * This method ensures that popup and main players have different look. - * We use one layout for both players and need to decide what to show and what to hide. - * Additional measuring should be done inside {@link #setupElementsSize}. - */ - private void setupElementsVisibility() { - if (popupPlayerSelected()) { - fullscreenButton.setVisibility(View.VISIBLE); - screenRotationButton.setVisibility(View.GONE); - getResizeView().setVisibility(View.GONE); - getRootView().findViewById(R.id.metadataView).setVisibility(View.GONE); - queueButton.setVisibility(View.GONE); - moreOptionsButton.setVisibility(View.GONE); - getTopControlsRoot().setOrientation(LinearLayout.HORIZONTAL); - primaryControls.getLayoutParams().width = LinearLayout.LayoutParams.WRAP_CONTENT; - secondaryControls.setAlpha(1.0f); - secondaryControls.setVisibility(View.VISIBLE); - secondaryControls.setTranslationY(0); - shareButton.setVisibility(View.GONE); - playWithKodi.setVisibility(View.GONE); - openInBrowser.setVisibility(View.GONE); - muteButton.setVisibility(View.GONE); - playerCloseButton.setVisibility(View.GONE); - getTopControlsRoot().bringToFront(); - getTopControlsRoot().setClickable(false); - getTopControlsRoot().setFocusable(false); - getBottomControlsRoot().bringToFront(); - onQueueClosed(); - } else { - fullscreenButton.setVisibility(View.GONE); - setupScreenRotationButton(); - getResizeView().setVisibility(View.VISIBLE); - getRootView().findViewById(R.id.metadataView).setVisibility(View.VISIBLE); - moreOptionsButton.setVisibility(View.VISIBLE); - getTopControlsRoot().setOrientation(LinearLayout.VERTICAL); - primaryControls.getLayoutParams().width = LinearLayout.LayoutParams.MATCH_PARENT; - secondaryControls.setVisibility(View.INVISIBLE); - moreOptionsButton.setImageDrawable(AppCompatResources.getDrawable(service, - R.drawable.ic_expand_more_white_24dp)); - shareButton.setVisibility(View.VISIBLE); - showHideKodiButton(); - openInBrowser.setVisibility(View.VISIBLE); - muteButton.setVisibility(View.VISIBLE); - playerCloseButton.setVisibility(isFullscreen ? View.GONE : View.VISIBLE); - // Top controls have a large minHeight which is allows to drag the player - // down in fullscreen mode (just larger area to make easy to locate by finger) - getTopControlsRoot().setClickable(true); - getTopControlsRoot().setFocusable(true); - } - if (!isFullscreen()) { - titleTextView.setVisibility(View.GONE); - channelTextView.setVisibility(View.GONE); - } else { - titleTextView.setVisibility(View.VISIBLE); - channelTextView.setVisibility(View.VISIBLE); - } - setMuteButton(muteButton, isMuted()); - - animateRotation(moreOptionsButton, DEFAULT_CONTROLS_DURATION, 0); - } - - /** - * Changes padding, size of elements based on player selected right now. - * Popup player has small padding in comparison with the main player - */ - private void setupElementsSize() { - if (popupPlayerSelected()) { - final int controlsPadding = service.getResources() - .getDimensionPixelSize(R.dimen.player_popup_controls_padding); - final int buttonsPadding = service.getResources() - .getDimensionPixelSize(R.dimen.player_popup_buttons_padding); - getTopControlsRoot().setPaddingRelative(controlsPadding, 0, controlsPadding, 0); - getBottomControlsRoot().setPaddingRelative(controlsPadding, 0, controlsPadding, 0); - getQualityTextView().setPadding( - buttonsPadding, buttonsPadding, buttonsPadding, buttonsPadding); - getPlaybackSpeedTextView().setPadding( - buttonsPadding, buttonsPadding, buttonsPadding, buttonsPadding); - getCaptionTextView().setPadding( - buttonsPadding, buttonsPadding, buttonsPadding, buttonsPadding); - getPlaybackSpeedTextView().setMinimumWidth(0); - } else if (videoPlayerSelected()) { - final int buttonsMinWidth = service.getResources() - .getDimensionPixelSize(R.dimen.player_main_buttons_min_width); - final int playerTopPadding = service.getResources() - .getDimensionPixelSize(R.dimen.player_main_top_padding); - final int controlsPadding = service.getResources() - .getDimensionPixelSize(R.dimen.player_main_controls_padding); - final int buttonsPadding = service.getResources() - .getDimensionPixelSize(R.dimen.player_main_buttons_padding); - getTopControlsRoot().setPaddingRelative( - controlsPadding, playerTopPadding, controlsPadding, 0); - getBottomControlsRoot().setPaddingRelative(controlsPadding, 0, controlsPadding, 0); - getQualityTextView().setPadding( - buttonsPadding, buttonsPadding, buttonsPadding, buttonsPadding); - getPlaybackSpeedTextView().setPadding( - buttonsPadding, buttonsPadding, buttonsPadding, buttonsPadding); - getPlaybackSpeedTextView().setMinimumWidth(buttonsMinWidth); - getCaptionTextView().setPadding( - buttonsPadding, buttonsPadding, buttonsPadding, buttonsPadding); - } - } - - @Override - public void initListeners() { - super.initListeners(); - - final PlayerGestureListener listener = new PlayerGestureListener(this, service); - gestureDetector = new GestureDetector(context, listener); - getRootView().setOnTouchListener(listener); - - queueButton.setOnClickListener(this); - repeatButton.setOnClickListener(this); - shuffleButton.setOnClickListener(this); - - playPauseButton.setOnClickListener(this); - playPreviousButton.setOnClickListener(this); - playNextButton.setOnClickListener(this); - - moreOptionsButton.setOnClickListener(this); - moreOptionsButton.setOnLongClickListener(this); - shareButton.setOnClickListener(this); - fullscreenButton.setOnClickListener(this); - screenRotationButton.setOnClickListener(this); - playWithKodi.setOnClickListener(this); - openInBrowser.setOnClickListener(this); - playerCloseButton.setOnClickListener(this); - muteButton.setOnClickListener(this); - - settingsContentObserver = new ContentObserver(new Handler()) { - @Override - public void onChange(final boolean selfChange) { - setupScreenRotationButton(); - } - }; - service.getContentResolver().registerContentObserver( - Settings.System.getUriFor(Settings.System.ACCELEROMETER_ROTATION), false, - settingsContentObserver); - getRootView().addOnLayoutChangeListener(this); - - ViewCompat.setOnApplyWindowInsetsListener(queueLayout, (view, windowInsets) -> { - final DisplayCutoutCompat cutout = windowInsets.getDisplayCutout(); - if (cutout != null) { - view.setPadding(cutout.getSafeInsetLeft(), cutout.getSafeInsetTop(), - cutout.getSafeInsetRight(), cutout.getSafeInsetBottom()); - } - return windowInsets; - }); - - // PlaybackControlRoot already consumed window insets but we should pass them to - // player_overlays too. Without it they will be off-centered - getControlsRoot().addOnLayoutChangeListener((v, left, top, right, bottom, - oldLeft, oldTop, oldRight, oldBottom) -> - playerOverlays.setPadding( - v.getPaddingLeft(), - v.getPaddingTop(), - v.getPaddingRight(), - v.getPaddingBottom())); - } - - public boolean onKeyDown(final int keyCode) { - switch (keyCode) { - default: - break; - case KeyEvent.KEYCODE_BACK: - if (DeviceUtils.isTv(service) && isControlsVisible()) { - hideControls(0, 0); - return true; - } - break; - case KeyEvent.KEYCODE_DPAD_UP: - case KeyEvent.KEYCODE_DPAD_LEFT: - case KeyEvent.KEYCODE_DPAD_DOWN: - case KeyEvent.KEYCODE_DPAD_RIGHT: - case KeyEvent.KEYCODE_DPAD_CENTER: - if (getRootView().hasFocus() && !getControlsRoot().hasFocus()) { - // do not interfere with focus in playlist etc. - return false; - } - - if (getCurrentState() == BasePlayer.STATE_BLOCKED) { - return true; - } - - if (!isControlsVisible()) { - if (!queueVisible) { - playPauseButton.requestFocus(); - } - showControlsThenHide(); - showSystemUIPartially(); - return true; - } else { - hideControls(DEFAULT_CONTROLS_DURATION, DPAD_CONTROLS_HIDE_TIME); - } - break; - } - - return false; - } - - public AppCompatActivity getParentActivity() { - // ! instanceof ViewGroup means that view was added via windowManager for Popup - if (getRootView() == null - || getRootView().getParent() == null - || !(getRootView().getParent() instanceof ViewGroup)) { - return null; - } - - final ViewGroup parent = (ViewGroup) getRootView().getParent(); - return (AppCompatActivity) parent.getContext(); - } - - /*////////////////////////////////////////////////////////////////////////// - // View - //////////////////////////////////////////////////////////////////////////*/ - - private void setRepeatModeButton(final ImageButton imageButton, final int repeatMode) { - switch (repeatMode) { - case Player.REPEAT_MODE_OFF: - imageButton.setImageResource(R.drawable.exo_controls_repeat_off); - break; - case Player.REPEAT_MODE_ONE: - imageButton.setImageResource(R.drawable.exo_controls_repeat_one); - break; - case Player.REPEAT_MODE_ALL: - imageButton.setImageResource(R.drawable.exo_controls_repeat_all); - break; - } - } - - private void setShuffleButton(final ImageButton button, final boolean shuffled) { - final int shuffleAlpha = shuffled ? 255 : 77; - button.setImageAlpha(shuffleAlpha); - } - - //////////////////////////////////////////////////////////////////////////// - // Playback Parameters Listener - //////////////////////////////////////////////////////////////////////////// - - @Override - public void onPlaybackParameterChanged(final float playbackTempo, final float playbackPitch, - final boolean playbackSkipSilence) { - setPlaybackParameters(playbackTempo, playbackPitch, playbackSkipSilence); - } - - @Override - public void onVideoSizeChanged(final int width, final int height, - final int unappliedRotationDegrees, - final float pixelWidthHeightRatio) { - super.onVideoSizeChanged(width, height, unappliedRotationDegrees, pixelWidthHeightRatio); - isVerticalVideo = width < height; - prepareOrientation(); - setupScreenRotationButton(); - } - - /*////////////////////////////////////////////////////////////////////////// - // ExoPlayer Video Listener - //////////////////////////////////////////////////////////////////////////*/ - - void onShuffleOrRepeatModeChanged() { - updatePlaybackButtons(); - updatePlayback(); - NotificationUtil.getInstance().createNotificationIfNeededAndUpdate(this, false); - } - - @Override - public void onRepeatModeChanged(final int i) { - super.onRepeatModeChanged(i); - onShuffleOrRepeatModeChanged(); - } - - @Override - public void onShuffleClicked() { - super.onShuffleClicked(); - onShuffleOrRepeatModeChanged(); - - } - - /*////////////////////////////////////////////////////////////////////////// - // Playback Listener - //////////////////////////////////////////////////////////////////////////*/ - - @Override - public void onPlayerError(final ExoPlaybackException error) { - super.onPlayerError(error); - - if (fragmentListener != null) { - fragmentListener.onPlayerError(error); - } - } - - @Override - public void onTimelineChanged(final Timeline timeline, final int reason) { - super.onTimelineChanged(timeline, reason); - // force recreate notification to ensure seek bar is shown when preparation finishes - NotificationUtil.getInstance().createNotificationIfNeededAndUpdate(this, true); - } - - protected void onMetadataChanged(@NonNull final MediaSourceTag tag) { - super.onMetadataChanged(tag); - - showHideKodiButton(); - - titleTextView.setText(tag.getMetadata().getName()); - channelTextView.setText(tag.getMetadata().getUploaderName()); - - NotificationUtil.getInstance().createNotificationIfNeededAndUpdate(this, false); - updateMetadata(); - } - - @Override - public void onPlaybackShutdown() { - if (DEBUG) { - Log.d(TAG, "onPlaybackShutdown() called"); - } - service.onDestroy(); - } - - @Override - public void onMuteUnmuteButtonClicked() { - super.onMuteUnmuteButtonClicked(); - updatePlayback(); - setMuteButton(muteButton, isMuted()); - } - - @Override - public void onUpdateProgress(final int currentProgress, - final int duration, final int bufferPercent) { - super.onUpdateProgress(currentProgress, duration, bufferPercent); - updateProgress(currentProgress, duration, bufferPercent); - - final boolean showThumbnail = - sharedPreferences.getBoolean( - context.getString(R.string.show_thumbnail_key), - true); - // setMetadata only updates the metadata when any of the metadata keys are null - mediaSessionManager.setMetadata(getVideoTitle(), getUploaderName(), - showThumbnail ? getThumbnail() : null, duration); - } - - @Override - public void onPlayQueueEdited() { - updatePlayback(); - showOrHideButtons(); - NotificationUtil.getInstance().createNotificationIfNeededAndUpdate(this, false); - } - - @Override - @Nullable - public MediaSource sourceOf(final PlayQueueItem item, final StreamInfo info) { - // For LiveStream or video/popup players we can use super() method - // but not for audio player - if (!audioOnly) { - return super.sourceOf(item, info); - } else { - return resolver.resolve(info); - } - } - - @Override - public void onPlayPrevious() { - super.onPlayPrevious(); - triggerProgressUpdate(); - } - - @Override - public void onPlayNext() { - super.onPlayNext(); - triggerProgressUpdate(); - } - - @Override - protected void initPlayback(@NonNull final PlayQueue queue, final int repeatMode, - final float playbackSpeed, final float playbackPitch, - final boolean playbackSkipSilence, - final boolean playOnReady, final boolean isMuted) { - super.initPlayback(queue, repeatMode, playbackSpeed, playbackPitch, - playbackSkipSilence, playOnReady, isMuted); - updateQueue(); - } - - /*////////////////////////////////////////////////////////////////////////// - // Player Overrides - //////////////////////////////////////////////////////////////////////////*/ - - @Override - public void toggleFullscreen() { - if (DEBUG) { - Log.d(TAG, "toggleFullscreen() called"); - } - if (popupPlayerSelected() - || simpleExoPlayer == null - || getCurrentMetadata() == null - || fragmentListener == null) { - return; - } - - isFullscreen = !isFullscreen; - if (!isFullscreen) { - // Apply window insets because Android will not do it when orientation changes - // from landscape to portrait (open vertical video to reproduce) - getControlsRoot().setPadding(0, 0, 0, 0); - } else { - // Android needs tens milliseconds to send new insets but a user is able to see - // how controls changes it's position from `0` to `nav bar height` padding. - // So just hide the controls to hide this visual inconsistency - hideControls(0, 0); - } - fragmentListener.onFullscreenStateChanged(isFullscreen()); - - if (!isFullscreen()) { - titleTextView.setVisibility(View.GONE); - channelTextView.setVisibility(View.GONE); - playerCloseButton.setVisibility(videoPlayerSelected() ? View.VISIBLE : View.GONE); - } else { - titleTextView.setVisibility(View.VISIBLE); - channelTextView.setVisibility(View.VISIBLE); - playerCloseButton.setVisibility(View.GONE); - } - setupScreenRotationButton(); - } - - @Override - public void onClick(final View v) { - super.onClick(v); - if (v.getId() == playPauseButton.getId()) { - onPlayPause(); - } else if (v.getId() == playPreviousButton.getId()) { - onPlayPrevious(); - } else if (v.getId() == playNextButton.getId()) { - onPlayNext(); - } else if (v.getId() == queueButton.getId()) { - onQueueClicked(); - return; - } else if (v.getId() == repeatButton.getId()) { - onRepeatClicked(); - return; - } else if (v.getId() == shuffleButton.getId()) { - onShuffleClicked(); - return; - } else if (v.getId() == moreOptionsButton.getId()) { - onMoreOptionsClicked(); - } else if (v.getId() == shareButton.getId()) { - onShareClicked(); - } else if (v.getId() == playWithKodi.getId()) { - onPlayWithKodiClicked(); - } else if (v.getId() == openInBrowser.getId()) { - onOpenInBrowserClicked(); - } else if (v.getId() == fullscreenButton.getId()) { - setRecovery(); - NavigationHelper.playOnMainPlayer(context, getPlayQueue(), true); - return; - } else if (v.getId() == screenRotationButton.getId()) { - // Only if it's not a vertical video or vertical video but in landscape with locked - // orientation a screen orientation can be changed automatically - if (!isVerticalVideo - || (service.isLandscape() && globalScreenOrientationLocked(service))) { - fragmentListener.onScreenRotationButtonClicked(); - } else { - toggleFullscreen(); - } - } else if (v.getId() == muteButton.getId()) { - onMuteUnmuteButtonClicked(); - } else if (v.getId() == playerCloseButton.getId()) { - service.sendBroadcast(new Intent(VideoDetailFragment.ACTION_HIDE_MAIN_PLAYER)); - } - - if (getCurrentState() != STATE_COMPLETED) { - getControlsVisibilityHandler().removeCallbacksAndMessages(null); - showHideShadow(true, DEFAULT_CONTROLS_DURATION, 0); - animateView(getControlsRoot(), true, DEFAULT_CONTROLS_DURATION, 0, () -> { - if (getCurrentState() == STATE_PLAYING && !isSomePopupMenuVisible()) { - if (v.getId() == playPauseButton.getId() - // Hide controls in fullscreen immediately - || (v.getId() == screenRotationButton.getId() && isFullscreen)) { - hideControls(0, 0); - } else { - hideControls(DEFAULT_CONTROLS_DURATION, DEFAULT_CONTROLS_HIDE_TIME); - } - } - }); - } - } - - @Override - public boolean onLongClick(final View v) { - if (v.getId() == moreOptionsButton.getId() && isFullscreen()) { - fragmentListener.onMoreOptionsLongClicked(); - hideControls(0, 0); - hideSystemUIIfNeeded(); - } - return true; - } - - private void onQueueClicked() { - queueVisible = true; - - hideSystemUIIfNeeded(); - buildQueue(); - updatePlaybackButtons(); - - hideControls(0, 0); - queueLayout.requestFocus(); - animateView(queueLayout, SLIDE_AND_ALPHA, true, - DEFAULT_CONTROLS_DURATION); - - itemsList.scrollToPosition(playQueue.getIndex()); - } - - public void onQueueClosed() { - if (!queueVisible) { - return; - } - - animateView(queueLayout, SLIDE_AND_ALPHA, false, - DEFAULT_CONTROLS_DURATION, 0, () -> { - // Even when queueLayout is GONE it receives touch events - // and ruins normal behavior of the app. This line fixes it - queueLayout.setTranslationY(-queueLayout.getHeight() * 5); - }); - queueVisible = false; - playPauseButton.requestFocus(); - } - - private void onMoreOptionsClicked() { - if (DEBUG) { - Log.d(TAG, "onMoreOptionsClicked() called"); - } - - final boolean isMoreControlsVisible = secondaryControls.getVisibility() == View.VISIBLE; - - animateRotation(moreOptionsButton, DEFAULT_CONTROLS_DURATION, - isMoreControlsVisible ? 0 : 180); - animateView(secondaryControls, SLIDE_AND_ALPHA, !isMoreControlsVisible, - DEFAULT_CONTROLS_DURATION, 0, - () -> { - // Fix for a ripple effect on background drawable. - // When view returns from GONE state it takes more milliseconds than returning - // from INVISIBLE state. And the delay makes ripple background end to fast - if (isMoreControlsVisible) { - secondaryControls.setVisibility(View.INVISIBLE); - } - }); - showControls(DEFAULT_CONTROLS_DURATION); - } - - private void onShareClicked() { - // share video at the current time (youtube.com/watch?v=ID&t=SECONDS) - // Timestamp doesn't make sense in a live stream so drop it - final String ts = isLive() ? "" : ("&t=" + (getPlaybackSeekBar().getProgress() / 1000)); - ShareUtils.shareUrl(service, - getVideoTitle(), - getVideoUrl() + ts); - } - - private void onPlayWithKodiClicked() { - if (getCurrentMetadata() == null) { - return; - } - onPause(); - try { - NavigationHelper.playWithKore(getParentActivity(), Uri.parse(getVideoUrl())); - } catch (final Exception e) { - if (DEBUG) { - Log.i(TAG, "Failed to start kore", e); - } - KoreUtil.showInstallKoreDialog(getParentActivity()); - } - } - - private void onOpenInBrowserClicked() { - if (getCurrentMetadata() == null) { - return; - } - - ShareUtils.openUrlInBrowser(getParentActivity(), - getCurrentMetadata().getMetadata().getOriginalUrl()); - } - - private void showHideKodiButton() { - final boolean kodiEnabled = defaultPreferences.getBoolean( - service.getString(R.string.show_play_with_kodi_key), false); - // show kodi button if it supports the current service and it is enabled in settings - final boolean showKodiButton = playQueue != null && playQueue.getItem() != null - && KoreUtil.isServiceSupportedByKore(playQueue.getItem().getServiceId()) - && PreferenceManager.getDefaultSharedPreferences(context) - .getBoolean(context.getString(R.string.show_play_with_kodi_key), false); - playWithKodi.setVisibility(videoPlayerSelected() && kodiEnabled && showKodiButton - ? View.VISIBLE : View.GONE); - } - - private void setupScreenRotationButton() { - final boolean orientationLocked = PlayerHelper.globalScreenOrientationLocked(service); - final boolean showButton = videoPlayerSelected() - && (orientationLocked || isVerticalVideo || DeviceUtils.isTablet(service)); - screenRotationButton.setVisibility(showButton ? View.VISIBLE : View.GONE); - screenRotationButton.setImageDrawable(AppCompatResources.getDrawable(service, isFullscreen() - ? R.drawable.ic_fullscreen_exit_white_24dp - : R.drawable.ic_fullscreen_white_24dp)); - } - - private void prepareOrientation() { - final boolean orientationLocked = PlayerHelper.globalScreenOrientationLocked(service); - if (orientationLocked - && isFullscreen() - && service.isLandscape() == isVerticalVideo - && !DeviceUtils.isTv(service) - && !DeviceUtils.isTablet(service) - && fragmentListener != null) { - fragmentListener.onScreenRotationButtonClicked(); - } - } - - @Override - public void onPlaybackSpeedClicked() { - if (videoPlayerSelected()) { - PlaybackParameterDialog - .newInstance( - getPlaybackSpeed(), getPlaybackPitch(), getPlaybackSkipSilence(), this) - .show(getParentActivity().getSupportFragmentManager(), null); - } else { - super.onPlaybackSpeedClicked(); - } - } - - @Override - public void onStopTrackingTouch(final SeekBar seekBar) { - super.onStopTrackingTouch(seekBar); - if (wasPlaying()) { - showControlsThenHide(); - } - } - - @Override - public void onDismiss(final PopupMenu menu) { - super.onDismiss(menu); - if (isPlaying()) { - hideControls(DEFAULT_CONTROLS_DURATION, 0); - hideSystemUIIfNeeded(); - } - } - - @Override - @SuppressWarnings("checkstyle:ParameterNumber") - public void onLayoutChange(final View view, final int l, final int t, final int r, final int b, - final int ol, final int ot, final int or, final int ob) { - if (l != ol || t != ot || r != or || b != ob) { - // Use smaller value to be consistent between screen orientations - // (and to make usage easier) - final int width = r - l; - final int height = b - t; - final int min = Math.min(width, height); - maxGestureLength = (int) (min * MAX_GESTURE_LENGTH); - - if (DEBUG) { - Log.d(TAG, "maxGestureLength = " + maxGestureLength); - } - - volumeProgressBar.setMax(maxGestureLength); - brightnessProgressBar.setMax(maxGestureLength); - - setInitialGestureValues(); - queueLayout.getLayoutParams().height = height - queueLayout.getTop(); - } - } - - @Override - protected int nextResizeMode(final int currentResizeMode) { - final int newResizeMode; - switch (currentResizeMode) { - case AspectRatioFrameLayout.RESIZE_MODE_FIT: - newResizeMode = AspectRatioFrameLayout.RESIZE_MODE_FILL; - break; - case AspectRatioFrameLayout.RESIZE_MODE_FILL: - newResizeMode = AspectRatioFrameLayout.RESIZE_MODE_ZOOM; - break; - default: - newResizeMode = AspectRatioFrameLayout.RESIZE_MODE_FIT; - break; - } - - storeResizeMode(newResizeMode); - return newResizeMode; - } - - private void storeResizeMode(final @AspectRatioFrameLayout.ResizeMode int resizeMode) { - defaultPreferences.edit() - .putInt(service.getString(R.string.last_resize_mode), resizeMode) - .apply(); - } - - private void restoreResizeMode() { - setResizeMode(defaultPreferences.getInt( - service.getString(R.string.last_resize_mode), - AspectRatioFrameLayout.RESIZE_MODE_FIT)); - } - - @Override - protected VideoPlaybackResolver.QualityResolver getQualityResolver() { - return new VideoPlaybackResolver.QualityResolver() { - @Override - public int getDefaultResolutionIndex(final List sortedVideos) { - return videoPlayerSelected() - ? ListHelper.getDefaultResolutionIndex(context, sortedVideos) - : ListHelper.getPopupDefaultResolutionIndex(context, sortedVideos); - } - - @Override - public int getOverrideResolutionIndex(final List sortedVideos, - final String playbackQuality) { - return videoPlayerSelected() - ? getResolutionIndex(context, sortedVideos, playbackQuality) - : getPopupResolutionIndex(context, sortedVideos, playbackQuality); - } - }; - } - - /*////////////////////////////////////////////////////////////////////////// - // States - //////////////////////////////////////////////////////////////////////////*/ - - private void animatePlayButtons(final boolean show, final int duration) { - animateView(playPauseButton, AnimationUtils.Type.SCALE_AND_ALPHA, show, duration); - if (playQueue.getIndex() > 0 || !show) { - animateView(playPreviousButton, AnimationUtils.Type.SCALE_AND_ALPHA, show, duration); - } - if (playQueue.getIndex() + 1 < playQueue.getStreams().size() || !show) { - animateView(playNextButton, AnimationUtils.Type.SCALE_AND_ALPHA, show, duration); - } - - } - - @Override - public void changeState(final int state) { - super.changeState(state); - updatePlayback(); - } - - @Override - public void onBlocked() { - super.onBlocked(); - playPauseButton.setImageResource(R.drawable.ic_play_arrow_white_24dp); - animatePlayButtons(false, 100); - getRootView().setKeepScreenOn(false); - - NotificationUtil.getInstance().createNotificationIfNeededAndUpdate(this, false); - } - - @Override - public void onBuffering() { - super.onBuffering(); - getRootView().setKeepScreenOn(true); - - if (NotificationUtil.getInstance().shouldUpdateBufferingSlot()) { - NotificationUtil.getInstance().createNotificationIfNeededAndUpdate(this, false); - } - } - - @Override - public void onPlaying() { - super.onPlaying(); - animateView(playPauseButton, AnimationUtils.Type.SCALE_AND_ALPHA, false, 80, 0, () -> { - playPauseButton.setImageResource(R.drawable.ic_pause_white_24dp); - animatePlayButtons(true, 200); - if (!queueVisible) { - playPauseButton.requestFocus(); - } - }); - - updateWindowFlags(ONGOING_PLAYBACK_WINDOW_FLAGS); - checkLandscape(); - getRootView().setKeepScreenOn(true); - - NotificationUtil.getInstance().createNotificationIfNeededAndUpdate(this, false); - } - - @Override - public void onPaused() { - super.onPaused(); - animateView(playPauseButton, AnimationUtils.Type.SCALE_AND_ALPHA, false, 80, 0, () -> { - playPauseButton.setImageResource(R.drawable.ic_play_arrow_white_24dp); - animatePlayButtons(true, 200); - if (!queueVisible) { - playPauseButton.requestFocus(); - } - }); - - updateWindowFlags(IDLE_WINDOW_FLAGS); - - // Remove running notification when user don't want music (or video in popup) - // to be played in background - if (!minimizeOnPopupEnabled() && !backgroundPlaybackEnabled() && videoPlayerSelected()) { - NotificationUtil.getInstance().cancelNotificationAndStopForeground(service); - } else { - NotificationUtil.getInstance().createNotificationIfNeededAndUpdate(this, false); - } - - getRootView().setKeepScreenOn(false); - } - - @Override - public void onPausedSeek() { - super.onPausedSeek(); - animatePlayButtons(false, 100); - getRootView().setKeepScreenOn(true); - - NotificationUtil.getInstance().createNotificationIfNeededAndUpdate(this, false); - } - - - @Override - public void onCompleted() { - animateView(playPauseButton, AnimationUtils.Type.SCALE_AND_ALPHA, false, 0, 0, () -> { - playPauseButton.setImageResource(R.drawable.ic_replay_white_24dp); - animatePlayButtons(true, DEFAULT_CONTROLS_DURATION); - }); - - getRootView().setKeepScreenOn(false); - updateWindowFlags(IDLE_WINDOW_FLAGS); - - NotificationUtil.getInstance().createNotificationIfNeededAndUpdate(this, false); - if (isFullscreen) { - toggleFullscreen(); - } - super.onCompleted(); - } - - @Override - public void destroy() { - super.destroy(); - service.getContentResolver().unregisterContentObserver(settingsContentObserver); - } - - /*////////////////////////////////////////////////////////////////////////// - // Broadcast Receiver - //////////////////////////////////////////////////////////////////////////*/ - - @Override - protected void setupBroadcastReceiver(final IntentFilter intentFilter) { - super.setupBroadcastReceiver(intentFilter); - if (DEBUG) { - Log.d(TAG, "setupBroadcastReceiver() called with: " - + "intentFilter = [" + intentFilter + "]"); - } - - intentFilter.addAction(ACTION_CLOSE); - intentFilter.addAction(ACTION_PLAY_PAUSE); - intentFilter.addAction(ACTION_OPEN_CONTROLS); - intentFilter.addAction(ACTION_REPEAT); - intentFilter.addAction(ACTION_PLAY_PREVIOUS); - intentFilter.addAction(ACTION_PLAY_NEXT); - intentFilter.addAction(ACTION_FAST_REWIND); - intentFilter.addAction(ACTION_FAST_FORWARD); - intentFilter.addAction(ACTION_SHUFFLE); - intentFilter.addAction(ACTION_RECREATE_NOTIFICATION); - - intentFilter.addAction(VideoDetailFragment.ACTION_VIDEO_FRAGMENT_RESUMED); - intentFilter.addAction(VideoDetailFragment.ACTION_VIDEO_FRAGMENT_STOPPED); - - intentFilter.addAction(Intent.ACTION_CONFIGURATION_CHANGED); - intentFilter.addAction(Intent.ACTION_SCREEN_ON); - intentFilter.addAction(Intent.ACTION_SCREEN_OFF); - - intentFilter.addAction(Intent.ACTION_HEADSET_PLUG); - } - - @Override - public void onBroadcastReceived(final Intent intent) { - super.onBroadcastReceived(intent); - if (intent == null || intent.getAction() == null) { - return; - } - - if (DEBUG) { - Log.d(TAG, "onBroadcastReceived() called with: intent = [" + intent + "]"); - } - - switch (intent.getAction()) { - case ACTION_CLOSE: - service.onDestroy(); - break; - case ACTION_PLAY_NEXT: - onPlayNext(); - break; - case ACTION_PLAY_PREVIOUS: - onPlayPrevious(); - break; - case ACTION_FAST_FORWARD: - onFastForward(); - break; - case ACTION_FAST_REWIND: - onFastRewind(); - break; - case ACTION_PLAY_PAUSE: - onPlayPause(); - if (!fragmentIsVisible) { - // Ensure that we have audio-only stream playing when a user - // started to play from notification's play button from outside of the app - onFragmentStopped(); - } - break; - case ACTION_REPEAT: - onRepeatClicked(); - break; - case ACTION_SHUFFLE: - onShuffleClicked(); - break; - case ACTION_RECREATE_NOTIFICATION: - NotificationUtil.getInstance().createNotificationIfNeededAndUpdate(this, true); - break; - case Intent.ACTION_HEADSET_PLUG: //FIXME - /*notificationManager.cancel(NOTIFICATION_ID); - mediaSessionManager.dispose(); - mediaSessionManager.enable(getBaseContext(), basePlayerImpl.simpleExoPlayer);*/ - break; - case VideoDetailFragment.ACTION_VIDEO_FRAGMENT_RESUMED: - fragmentIsVisible = true; - useVideoSource(true); - break; - case VideoDetailFragment.ACTION_VIDEO_FRAGMENT_STOPPED: - fragmentIsVisible = false; - onFragmentStopped(); - break; - case Intent.ACTION_CONFIGURATION_CHANGED: - assureCorrectAppLanguage(service); - if (DEBUG) { - Log.d(TAG, "onConfigurationChanged() called"); - } - if (popupPlayerSelected()) { - updateScreenSize(); - updatePopupSize(getPopupLayoutParams().width, -1); - checkPopupPositionBounds(); - } - // Close it because when changing orientation from portrait - // (in fullscreen mode) the size of queue layout can be larger than the screen size - onQueueClosed(); - break; - case Intent.ACTION_SCREEN_ON: - shouldUpdateOnProgress = true; - // Interrupt playback only when screen turns on - // and user is watching video in popup player. - // Same actions for video player will be handled in ACTION_VIDEO_FRAGMENT_RESUMED - if (popupPlayerSelected() && (isPlaying() || isLoading())) { - useVideoSource(true); - } - break; - case Intent.ACTION_SCREEN_OFF: - shouldUpdateOnProgress = false; - // Interrupt playback only when screen turns off with popup player working - if (popupPlayerSelected() && (isPlaying() || isLoading())) { - useVideoSource(false); - } - break; - } - } - - /*////////////////////////////////////////////////////////////////////////// - // Thumbnail Loading - //////////////////////////////////////////////////////////////////////////*/ - - @Override - public void onLoadingComplete(final String imageUri, - final View view, - final Bitmap loadedImage) { - super.onLoadingComplete(imageUri, view, loadedImage); - NotificationUtil.getInstance().createNotificationIfNeededAndUpdate(this, false); - } - - @Override - public void onLoadingFailed(final String imageUri, - final View view, - final FailReason failReason) { - super.onLoadingFailed(imageUri, view, failReason); - NotificationUtil.getInstance().createNotificationIfNeededAndUpdate(this, false); - } - - @Override - public void onLoadingCancelled(final String imageUri, final View view) { - super.onLoadingCancelled(imageUri, view); - NotificationUtil.getInstance().createNotificationIfNeededAndUpdate(this, false); - } - - /*////////////////////////////////////////////////////////////////////////// - // Utils - //////////////////////////////////////////////////////////////////////////*/ - - private void setInitialGestureValues() { - if (getAudioReactor() != null) { - final float currentVolumeNormalized = (float) getAudioReactor() - .getVolume() / getAudioReactor().getMaxVolume(); - volumeProgressBar.setProgress( - (int) (volumeProgressBar.getMax() * currentVolumeNormalized)); - } - } - - private void choosePlayerTypeFromIntent(final Intent intent) { - // If you want to open popup from the app just include Constants.POPUP_ONLY into an extra - if (intent.getIntExtra(PLAYER_TYPE, PLAYER_TYPE_VIDEO) == PLAYER_TYPE_AUDIO) { - playerType = MainPlayer.PlayerType.AUDIO; - } else if (intent.getIntExtra(PLAYER_TYPE, PLAYER_TYPE_VIDEO) == PLAYER_TYPE_POPUP) { - playerType = MainPlayer.PlayerType.POPUP; - } else { - playerType = MainPlayer.PlayerType.VIDEO; - } - } - - public boolean backgroundPlaybackEnabled() { - return PlayerHelper.getMinimizeOnExitAction(service) == MINIMIZE_ON_EXIT_MODE_BACKGROUND; - } - - public boolean minimizeOnPopupEnabled() { - return PlayerHelper.getMinimizeOnExitAction(service) - == PlayerHelper.MinimizeMode.MINIMIZE_ON_EXIT_MODE_POPUP; - } - - public boolean audioPlayerSelected() { - return playerType == MainPlayer.PlayerType.AUDIO; - } - - public boolean videoPlayerSelected() { - return playerType == MainPlayer.PlayerType.VIDEO; - } - - public boolean popupPlayerSelected() { - return playerType == MainPlayer.PlayerType.POPUP; - } - - public boolean isPlayerStopped() { - return getPlayer() == null || getPlayer().getPlaybackState() == SimpleExoPlayer.STATE_IDLE; - } - - private int distanceFromCloseButton(final MotionEvent popupMotionEvent) { - final int closeOverlayButtonX = closeOverlayButton.getLeft() - + closeOverlayButton.getWidth() / 2; - final int closeOverlayButtonY = closeOverlayButton.getTop() - + closeOverlayButton.getHeight() / 2; - - final float fingerX = popupLayoutParams.x + popupMotionEvent.getX(); - final float fingerY = popupLayoutParams.y + popupMotionEvent.getY(); - - return (int) Math.sqrt(Math.pow(closeOverlayButtonX - fingerX, 2) - + Math.pow(closeOverlayButtonY - fingerY, 2)); - } - - private float getClosingRadius() { - final int buttonRadius = closeOverlayButton.getWidth() / 2; - // 20% wider than the button itself - return buttonRadius * 1.2f; - } - - public boolean isInsideClosingRadius(final MotionEvent popupMotionEvent) { - return distanceFromCloseButton(popupMotionEvent) <= getClosingRadius(); - } - - public boolean isFullscreen() { - return isFullscreen; - } - - public void showControlsThenHide() { - if (DEBUG) { - Log.d(TAG, "showControlsThenHide() called"); - } - showOrHideButtons(); - showSystemUIPartially(); - super.showControlsThenHide(); - } - - @Override - public void showControls(final long duration) { - if (DEBUG) { - Log.d(TAG, "showControls() called with: duration = [" + duration + "]"); - } - showOrHideButtons(); - showSystemUIPartially(); - super.showControls(duration); - } - - @Override - public void hideControls(final long duration, final long delay) { - if (DEBUG) { - Log.d(TAG, "hideControls() called with: delay = [" + delay + "]"); - } - - showOrHideButtons(); - - getControlsVisibilityHandler().removeCallbacksAndMessages(null); - getControlsVisibilityHandler().postDelayed(() -> { - showHideShadow(false, duration, 0); - animateView(getControlsRoot(), false, duration, 0, this::hideSystemUIIfNeeded); - }, delay - ); - } - - @Override - public void safeHideControls(final long duration, final long delay) { - if (getControlsRoot().isInTouchMode()) { - hideControls(duration, delay); - } - } - - private void showOrHideButtons() { - if (playQueue == null) { - return; - } - - final boolean showPrev = playQueue.getIndex() != 0; - final boolean showNext = playQueue.getIndex() + 1 != playQueue.getStreams().size(); - final boolean showQueue = playQueue.getStreams().size() > 1 && !popupPlayerSelected(); - - playPreviousButton.setVisibility(showPrev ? View.VISIBLE : View.INVISIBLE); - playPreviousButton.setAlpha(showPrev ? 1.0f : 0.0f); - playNextButton.setVisibility(showNext ? View.VISIBLE : View.INVISIBLE); - playNextButton.setAlpha(showNext ? 1.0f : 0.0f); - queueButton.setVisibility(showQueue ? View.VISIBLE : View.GONE); - queueButton.setAlpha(showQueue ? 1.0f : 0.0f); - } - - private void showSystemUIPartially() { - final AppCompatActivity activity = getParentActivity(); - if (isFullscreen() && activity != null) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - activity.getWindow().setStatusBarColor(Color.TRANSPARENT); - activity.getWindow().setNavigationBarColor(Color.TRANSPARENT); - } - final int visibility = View.SYSTEM_UI_FLAG_LAYOUT_STABLE - | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN - | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION; - activity.getWindow().getDecorView().setSystemUiVisibility(visibility); - activity.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN); - } - } - - @Override - public void hideSystemUIIfNeeded() { - if (fragmentListener != null) { - fragmentListener.hideSystemUiIfNeeded(); - } - } - - public void disablePreloadingOfCurrentTrack() { - getLoadController().disablePreloadingOfCurrentTrack(); - } - - protected void setMuteButton(final ImageButton button, final boolean isMuted) { - button.setImageDrawable(AppCompatResources.getDrawable(service, isMuted - ? R.drawable.ic_volume_off_white_24dp : R.drawable.ic_volume_up_white_24dp)); - } - - /** - * @return true if main player is attached to activity and activity inside multiWindow mode - */ - private boolean isInMultiWindow() { - final AppCompatActivity parent = getParentActivity(); - return Build.VERSION.SDK_INT >= Build.VERSION_CODES.N - && parent != null - && parent.isInMultiWindowMode(); - } - - private void updatePlaybackButtons() { - if (repeatButton == null - || shuffleButton == null - || simpleExoPlayer == null - || playQueue == null) { - return; - } - - setRepeatModeButton(repeatButton, getRepeatMode()); - setShuffleButton(shuffleButton, playQueue.isShuffled()); - } - - public void checkLandscape() { - final AppCompatActivity parent = getParentActivity(); - final boolean videoInLandscapeButNotInFullscreen = service.isLandscape() - && !isFullscreen() - && videoPlayerSelected() - && !audioOnly; - - final boolean playingState = getCurrentState() != STATE_COMPLETED - && getCurrentState() != STATE_PAUSED; - if (parent != null - && videoInLandscapeButNotInFullscreen - && playingState - && !DeviceUtils.isTablet(service)) { - toggleFullscreen(); - } - } - - private void buildQueue() { - itemsList.setAdapter(playQueueAdapter); - itemsList.setClickable(true); - itemsList.setLongClickable(true); - - itemsList.clearOnScrollListeners(); - itemsList.addOnScrollListener(getQueueScrollListener()); - - itemTouchHelper = new ItemTouchHelper(getItemTouchCallback()); - itemTouchHelper.attachToRecyclerView(itemsList); - - playQueueAdapter.setSelectedListener(getOnSelectedListener()); - - itemsListCloseButton.setOnClickListener(view -> onQueueClosed()); - } - - public void useVideoSource(final boolean video) { - if (playQueue == null || audioOnly == !video || audioPlayerSelected()) { - return; - } - - audioOnly = !video; - // When a user returns from background controls could be hidden - // but systemUI will be shown 100%. Hide it - if (!audioOnly && !isControlsVisible()) { - hideSystemUIIfNeeded(); - } - setRecovery(); - reload(); - } - - private OnScrollBelowItemsListener getQueueScrollListener() { - return new OnScrollBelowItemsListener() { - @Override - public void onScrolledDown(final RecyclerView recyclerView) { - if (playQueue != null && !playQueue.isComplete()) { - playQueue.fetch(); - } else if (itemsList != null) { - itemsList.clearOnScrollListeners(); - } - } - }; - } - - private ItemTouchHelper.SimpleCallback getItemTouchCallback() { - return new PlayQueueItemTouchCallback() { - @Override - public void onMove(final int sourceIndex, final int targetIndex) { - if (playQueue != null) { - playQueue.move(sourceIndex, targetIndex); - } - } - - @Override - public void onSwiped(final int index) { - if (index != -1) { - playQueue.remove(index); - } - } - }; - } - - private PlayQueueItemBuilder.OnSelectedListener getOnSelectedListener() { - return new PlayQueueItemBuilder.OnSelectedListener() { - @Override - public void selected(final PlayQueueItem item, final View view) { - onSelected(item); - } - - @Override - public void held(final PlayQueueItem item, final View view) { - final int index = playQueue.indexOf(item); - if (index != -1) { - playQueue.remove(index); - } - } - - @Override - public void onStartDrag(final PlayQueueItemHolder viewHolder) { - if (itemTouchHelper != null) { - itemTouchHelper.startDrag(viewHolder); - } - } - }; - } - - /*////////////////////////////////////////////////////////////////////////// - // Init - //////////////////////////////////////////////////////////////////////////*/ - - @SuppressLint("RtlHardcoded") - private void initPopup() { - if (DEBUG) { - Log.d(TAG, "initPopup() called"); - } - - // Popup is already added to windowManager - if (popupHasParent()) { - return; - } - - updateScreenSize(); - - final boolean popupRememberSizeAndPos = PlayerHelper.isRememberingPopupDimensions(service); - final float defaultSize = service.getResources().getDimension(R.dimen.popup_default_width); - final SharedPreferences sharedPreferences = - PreferenceManager.getDefaultSharedPreferences(service); - popupWidth = popupRememberSizeAndPos - ? sharedPreferences.getFloat(POPUP_SAVED_WIDTH, defaultSize) - : defaultSize; - popupHeight = getMinimumVideoHeight(popupWidth); - - 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; - getSurfaceView().setHeights((int) popupHeight, (int) popupHeight); - - final int centerX = (int) (screenWidth / 2f - popupWidth / 2f); - final int centerY = (int) (screenHeight / 2f - popupHeight / 2f); - popupLayoutParams.x = popupRememberSizeAndPos - ? sharedPreferences.getInt(POPUP_SAVED_X, centerX) : centerX; - popupLayoutParams.y = popupRememberSizeAndPos - ? sharedPreferences.getInt(POPUP_SAVED_Y, centerY) : centerY; - - checkPopupPositionBounds(); - - getLoadingPanel().setMinimumWidth(popupLayoutParams.width); - getLoadingPanel().setMinimumHeight(popupLayoutParams.height); - - service.removeViewFromParent(); - windowManager.addView(getRootView(), popupLayoutParams); - - // Popup doesn't have aspectRatio selector, using FIT automatically - setResizeMode(AspectRatioFrameLayout.RESIZE_MODE_FIT); - } - - @SuppressLint("RtlHardcoded") - private void initPopupCloseOverlay() { - if (DEBUG) { - Log.d(TAG, "initPopupCloseOverlay() called"); - } - - // closeOverlayView is already added to windowManager - if (closeOverlayView != null) { - return; - } - - closeOverlayView = View.inflate(service, R.layout.player_popup_close_overlay, null); - closeOverlayButton = closeOverlayView.findViewById(R.id.closeButton); - - 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; - - closeOverlayButton.setVisibility(View.GONE); - windowManager.addView(closeOverlayView, closeOverlayLayoutParams); - } - - private void initVideoPlayer() { - restoreResizeMode(); - getRootView().setLayoutParams(new FrameLayout.LayoutParams( - FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT)); - } - - /*////////////////////////////////////////////////////////////////////////// - // Popup utils - //////////////////////////////////////////////////////////////////////////*/ - - /** - * @return if the popup was out of bounds and have been moved back to it - * @see #checkPopupPositionBounds(float, float) - */ - @SuppressWarnings("UnusedReturnValue") - public boolean checkPopupPositionBounds() { - return checkPopupPositionBounds(screenWidth, screenHeight); - } - - /** - * Check if {@link #popupLayoutParams}' position is within a arbitrary boundary - * that goes from (0, 0) to (boundaryWidth, boundaryHeight). - *

- * If it's out of these boundaries, {@link #popupLayoutParams}' position is changed - * and {@code true} is returned to represent this change. - *

- * - * @param boundaryWidth width of the boundary - * @param boundaryHeight height of the boundary - * @return if the popup was out of bounds and have been moved back to it - */ - public boolean checkPopupPositionBounds(final float boundaryWidth, final float boundaryHeight) { - if (DEBUG) { - Log.d(TAG, "checkPopupPositionBounds() called with: " - + "boundaryWidth = [" + boundaryWidth + "], " - + "boundaryHeight = [" + boundaryHeight + "]"); - } - - if (popupLayoutParams.x < 0) { - popupLayoutParams.x = 0; - return true; - } else if (popupLayoutParams.x > boundaryWidth - popupLayoutParams.width) { - popupLayoutParams.x = (int) (boundaryWidth - popupLayoutParams.width); - return true; - } - - if (popupLayoutParams.y < 0) { - popupLayoutParams.y = 0; - return true; - } else if (popupLayoutParams.y > boundaryHeight - popupLayoutParams.height) { - popupLayoutParams.y = (int) (boundaryHeight - popupLayoutParams.height); - return true; - } - - return false; - } - - public void savePositionAndSize() { - final SharedPreferences sharedPreferences = PreferenceManager - .getDefaultSharedPreferences(service); - sharedPreferences.edit().putInt(POPUP_SAVED_X, popupLayoutParams.x).apply(); - sharedPreferences.edit().putInt(POPUP_SAVED_Y, popupLayoutParams.y).apply(); - sharedPreferences.edit().putFloat(POPUP_SAVED_WIDTH, popupLayoutParams.width).apply(); - } - - private float getMinimumVideoHeight(final float width) { - final float height = width / (16.0f / 9.0f); // Respect the 16:9 ratio that most videos have - /*if (DEBUG) { - Log.d(TAG, "getMinimumVideoHeight() called with: width = [" - + width + "], returned: " + height); - }*/ - return height; - } - - public void updateScreenSize() { - final DisplayMetrics metrics = new DisplayMetrics(); - windowManager.getDefaultDisplay().getMetrics(metrics); - - screenWidth = metrics.widthPixels; - screenHeight = metrics.heightPixels; - if (DEBUG) { - Log.d(TAG, "updateScreenSize() called > screenWidth = " - + screenWidth + ", screenHeight = " + screenHeight); - } - - popupWidth = service.getResources().getDimension(R.dimen.popup_default_width); - popupHeight = getMinimumVideoHeight(popupWidth); - - minimumWidth = service.getResources().getDimension(R.dimen.popup_minimum_width); - minimumHeight = getMinimumVideoHeight(minimumWidth); - - maximumWidth = screenWidth; - maximumHeight = screenHeight; - } - - public void updatePopupSize(final int width, final int height) { - if (DEBUG) { - Log.d(TAG, "updatePopupSize() called with: width = [" - + width + "], height = [" + height + "]"); - } - - if (popupLayoutParams == null - || windowManager == null - || getParentActivity() != null - || getRootView().getParent() == null) { - return; - } - - final int actualWidth = (int) (width > maximumWidth - ? maximumWidth : width < minimumWidth ? minimumWidth : width); - final int actualHeight; - if (height == -1) { - actualHeight = (int) getMinimumVideoHeight(width); - } else { - actualHeight = (int) (height > maximumHeight - ? maximumHeight : height < minimumHeight - ? minimumHeight : height); - } - - popupLayoutParams.width = actualWidth; - popupLayoutParams.height = actualHeight; - popupWidth = actualWidth; - popupHeight = actualHeight; - getSurfaceView().setHeights((int) popupHeight, (int) popupHeight); - - if (DEBUG) { - Log.d(TAG, "updatePopupSize() updated values:" - + " width = [" + actualWidth + "], height = [" + actualHeight + "]"); - } - windowManager.updateViewLayout(getRootView(), popupLayoutParams); - } - - private void updateWindowFlags(final int flags) { - if (popupLayoutParams == null - || windowManager == null - || getParentActivity() != null - || getRootView().getParent() == null) { - return; - } - - popupLayoutParams.flags = flags; - windowManager.updateViewLayout(getRootView(), popupLayoutParams); - } - - private int popupLayoutParamType() { - return Build.VERSION.SDK_INT < Build.VERSION_CODES.O - ? WindowManager.LayoutParams.TYPE_PHONE - : WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY; - } - - /*////////////////////////////////////////////////////////////////////////// - // Misc - //////////////////////////////////////////////////////////////////////////*/ - - public void closePopup() { - if (DEBUG) { - Log.d(TAG, "closePopup() called, isPopupClosing = " + isPopupClosing); - } - if (isPopupClosing) { - return; - } - isPopupClosing = true; - - savePlaybackState(); - windowManager.removeView(getRootView()); - - animateOverlayAndFinishService(); - } - - public void removePopupFromView() { - final boolean isCloseOverlayHasParent = closeOverlayView != null - && closeOverlayView.getParent() != null; - if (popupHasParent()) { - windowManager.removeView(getRootView()); - } - if (isCloseOverlayHasParent) { - windowManager.removeView(closeOverlayView); - } - } - - private void animateOverlayAndFinishService() { - final int targetTranslationY = (int) (closeOverlayButton.getRootView().getHeight() - - closeOverlayButton.getY()); - - closeOverlayButton.animate().setListener(null).cancel(); - closeOverlayButton.animate() - .setInterpolator(new AnticipateInterpolator()) - .translationY(targetTranslationY) - .setDuration(400) - .setListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationCancel(final Animator animation) { - end(); - } - - @Override - public void onAnimationEnd(final Animator animation) { - end(); - } - - private void end() { - windowManager.removeView(closeOverlayView); - closeOverlayView = null; - - service.onDestroy(); - } - }).start(); - } - - private boolean popupHasParent() { - final View root = getRootView(); - return root != null - && root.getLayoutParams() instanceof WindowManager.LayoutParams - && root.getParent() != null; - } - - /////////////////////////////////////////////////////////////////////////// - // Manipulations with listener - /////////////////////////////////////////////////////////////////////////// - - public void setFragmentListener(final PlayerServiceEventListener listener) { - fragmentListener = listener; - fragmentIsVisible = true; - // Apply window insets because Android will not do it when orientation changes - // from landscape to portrait - if (!isFullscreen) { - getControlsRoot().setPadding(0, 0, 0, 0); - } - queueLayout.setPadding(0, 0, 0, 0); - updateQueue(); - updateMetadata(); - updatePlayback(); - triggerProgressUpdate(); - } - - public void removeFragmentListener(final PlayerServiceEventListener listener) { - if (fragmentListener == listener) { - fragmentListener = null; - } - } - - void setActivityListener(final PlayerEventListener listener) { - activityListener = listener; - updateMetadata(); - updatePlayback(); - triggerProgressUpdate(); - } - - void removeActivityListener(final PlayerEventListener listener) { - if (activityListener == listener) { - activityListener = null; - } - } - - private void updateQueue() { - if (fragmentListener != null && playQueue != null) { - fragmentListener.onQueueUpdate(playQueue); - } - if (activityListener != null && playQueue != null) { - activityListener.onQueueUpdate(playQueue); - } - } - - private void updateMetadata() { - if (fragmentListener != null && getCurrentMetadata() != null) { - fragmentListener.onMetadataUpdate(getCurrentMetadata().getMetadata(), playQueue); - } - if (activityListener != null && getCurrentMetadata() != null) { - activityListener.onMetadataUpdate(getCurrentMetadata().getMetadata(), playQueue); - } - } - - private void updatePlayback() { - if (fragmentListener != null && simpleExoPlayer != null && playQueue != null) { - fragmentListener.onPlaybackUpdate(currentState, getRepeatMode(), - playQueue.isShuffled(), simpleExoPlayer.getPlaybackParameters()); - } - if (activityListener != null && simpleExoPlayer != null && playQueue != null) { - activityListener.onPlaybackUpdate(currentState, getRepeatMode(), - playQueue.isShuffled(), getPlaybackParameters()); - } - } - - private void updateProgress(final int currentProgress, final int duration, - final int bufferPercent) { - if (fragmentListener != null) { - fragmentListener.onProgressUpdate(currentProgress, duration, bufferPercent); - } - if (activityListener != null) { - activityListener.onProgressUpdate(currentProgress, duration, bufferPercent); - } - } - - void stopActivityBinding() { - if (fragmentListener != null) { - fragmentListener.onServiceStopped(); - fragmentListener = null; - } - if (activityListener != null) { - activityListener.onServiceStopped(); - activityListener = null; - } - } - - /** - * This will be called when a user goes to another app/activity, turns off a screen. - * We don't want to interrupt playback and don't want to see notification so - * next lines of code will enable audio-only playback only if needed - */ - private void onFragmentStopped() { - if (videoPlayerSelected() && (isPlaying() || isLoading())) { - if (backgroundPlaybackEnabled()) { - useVideoSource(false); - } else if (minimizeOnPopupEnabled()) { - setRecovery(); - NavigationHelper.playOnPopupPlayer(getParentActivity(), playQueue, true); - } else { - onPause(); - } - } - } - - /////////////////////////////////////////////////////////////////////////// - // Getters - /////////////////////////////////////////////////////////////////////////// - - public RelativeLayout getVolumeRelativeLayout() { - return volumeRelativeLayout; - } - - public ProgressBar getVolumeProgressBar() { - return volumeProgressBar; - } - - public ImageView getVolumeImageView() { - return volumeImageView; - } - - public RelativeLayout getBrightnessRelativeLayout() { - return brightnessRelativeLayout; - } - - public ProgressBar getBrightnessProgressBar() { - return brightnessProgressBar; - } - - public ImageView getBrightnessImageView() { - return brightnessImageView; - } - - public ImageButton getPlayPauseButton() { - return playPauseButton; - } - - public int getMaxGestureLength() { - return maxGestureLength; - } - - public TextView getResizingIndicator() { - return resizingIndicator; - } - - public GestureDetector getGestureDetector() { - return gestureDetector; - } - - public WindowManager.LayoutParams getPopupLayoutParams() { - return popupLayoutParams; - } - - public MainPlayer.PlayerType getPlayerType() { - return playerType; - } - - public float getScreenWidth() { - return screenWidth; - } - - public float getScreenHeight() { - return screenHeight; - } - - public float getPopupWidth() { - return popupWidth; - } - - public float getPopupHeight() { - return popupHeight; - } - - public void setPopupWidth(final float width) { - popupWidth = width; - } - - public void setPopupHeight(final float height) { - popupHeight = height; - } - - public View getCloseOverlayButton() { - return closeOverlayButton; - } - - public View getClosingOverlayView() { - return closingOverlayView; - } - - public boolean isVerticalVideo() { - return isVerticalVideo; - } -} diff --git a/app/src/main/java/org/schabi/newpipe/player/event/BasePlayerGestureListener.kt b/app/src/main/java/org/schabi/newpipe/player/event/BasePlayerGestureListener.kt index 043e7f31d..46502a270 100644 --- a/app/src/main/java/org/schabi/newpipe/player/event/BasePlayerGestureListener.kt +++ b/app/src/main/java/org/schabi/newpipe/player/event/BasePlayerGestureListener.kt @@ -7,10 +7,10 @@ import android.view.GestureDetector import android.view.MotionEvent import android.view.View import android.view.ViewConfiguration -import org.schabi.newpipe.player.BasePlayer 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.savePopupPositionAndSizeToPrefs import org.schabi.newpipe.util.AnimationUtils import kotlin.math.abs import kotlin.math.hypot @@ -18,14 +18,14 @@ import kotlin.math.max 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 * and provides some abstract methods to make it easier separating the logic from the UI. */ abstract class BasePlayerGestureListener( @JvmField - protected val playerImpl: VideoPlayerImpl, + protected val player: Player, @JvmField protected val service: MainPlayer ) : GestureDetector.SimpleOnGestureListener(), View.OnTouchListener { @@ -78,7 +78,7 @@ abstract class BasePlayerGestureListener( // /////////////////////////////////////////////////////////////////// override fun onTouch(v: View, event: MotionEvent): Boolean { - return if (playerImpl.popupPlayerSelected()) { + return if (player.popupPlayerSelected()) { onTouchInPopup(v, event) } else { onTouchInMain(v, event) @@ -86,14 +86,14 @@ abstract class BasePlayerGestureListener( } private fun onTouchInMain(v: View, event: MotionEvent): Boolean { - playerImpl.gestureDetector.onTouchEvent(event) + player.gestureDetector.onTouchEvent(event) if (event.action == MotionEvent.ACTION_UP && isMovingInMain) { isMovingInMain = false onScrollEnd(MainPlayer.PlayerType.VIDEO, event) } return when (event.action) { MotionEvent.ACTION_DOWN, MotionEvent.ACTION_MOVE -> { - v.parent.requestDisallowInterceptTouchEvent(playerImpl.isFullscreen) + v.parent.requestDisallowInterceptTouchEvent(player.isFullscreen) true } MotionEvent.ACTION_UP -> { @@ -105,7 +105,7 @@ abstract class BasePlayerGestureListener( } private fun onTouchInPopup(v: View, event: MotionEvent): Boolean { - playerImpl.gestureDetector.onTouchEvent(event) + player.gestureDetector.onTouchEvent(event) if (event.pointerCount == 2 && !isMovingInPopup && !isResizing) { if (DEBUG) { Log.d(TAG, "onTouch() 2 finger pointer detected, enabling resizing.") @@ -157,10 +157,10 @@ abstract class BasePlayerGestureListener( initSecPointerY = (-1).toFloat() onPopupResizingEnd() - playerImpl.changeState(playerImpl.currentState) + player.changeState(player.currentState) } - if (!playerImpl.isPopupClosing) { - playerImpl.savePositionAndSize() + if (!player.isPopupClosing) { + savePopupPositionAndSizeToPrefs(player) } } @@ -190,19 +190,15 @@ abstract class BasePlayerGestureListener( 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 val newWidth = popupWidth * currentPointerDistance / initPointerDistance initPointerDistance = currentPointerDistance - playerImpl.popupLayoutParams.x += ((popupWidth - newWidth) / 2.0).toInt() + player.popupLayoutParams!!.x += ((popupWidth - newWidth) / 2.0).toInt() - playerImpl.checkPopupPositionBounds() - playerImpl.updateScreenSize() - - playerImpl.updatePopupSize( - min(playerImpl.screenWidth.toDouble(), newWidth).toInt(), - -1 - ) + player.checkPopupPositionBounds() + player.updateScreenSize() + player.changePopupSize(min(player.screenWidth.toDouble(), newWidth).toInt()) return true } } @@ -222,7 +218,7 @@ abstract class BasePlayerGestureListener( return true } - return if (playerImpl.popupPlayerSelected()) + return if (player.popupPlayerSelected()) onDownInPopup(e) else true @@ -231,12 +227,10 @@ abstract class BasePlayerGestureListener( private fun onDownInPopup(e: MotionEvent): Boolean { // 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). - playerImpl.updateScreenSize() - playerImpl.checkPopupPositionBounds() - initialPopupX = playerImpl.popupLayoutParams.x - initialPopupY = playerImpl.popupLayoutParams.y - playerImpl.popupWidth = playerImpl.popupLayoutParams.width.toFloat() - playerImpl.popupHeight = playerImpl.popupLayoutParams.height.toFloat() + player.updateScreenSize() + player.checkPopupPositionBounds() + initialPopupX = player.popupLayoutParams!!.x + initialPopupY = player.popupLayoutParams!!.y return super.onDown(e) } @@ -255,15 +249,15 @@ abstract class BasePlayerGestureListener( if (isDoubleTapping) return true - if (playerImpl.popupPlayerSelected()) { - if (playerImpl.player == null) + if (player.popupPlayerSelected()) { + if (player.exoPlayerIsNull()) return false onSingleTap(MainPlayer.PlayerType.POPUP) return true } else { super.onSingleTapConfirmed(e) - if (playerImpl.currentState == BasePlayer.STATE_BLOCKED) + if (player.currentState == Player.STATE_BLOCKED) return true onSingleTap(MainPlayer.PlayerType.VIDEO) @@ -272,10 +266,10 @@ abstract class BasePlayerGestureListener( } override fun onLongPress(e: MotionEvent?) { - if (playerImpl.popupPlayerSelected()) { - playerImpl.updateScreenSize() - playerImpl.checkPopupPositionBounds() - playerImpl.updatePopupSize(playerImpl.screenWidth.toInt(), -1) + if (player.popupPlayerSelected()) { + player.updateScreenSize() + player.checkPopupPositionBounds() + player.changePopupSize(player.screenWidth.toInt()) } } @@ -285,7 +279,7 @@ abstract class BasePlayerGestureListener( distanceX: Float, distanceY: Float ): Boolean { - return if (playerImpl.popupPlayerSelected()) { + return if (player.popupPlayerSelected()) { onScrollInPopup(initialEvent, movingEvent, distanceX, distanceY) } else { onScrollInMain(initialEvent, movingEvent, distanceX, distanceY) @@ -298,19 +292,18 @@ abstract class BasePlayerGestureListener( velocityX: Float, velocityY: Float ): Boolean { - return if (playerImpl.popupPlayerSelected()) { + return if (player.popupPlayerSelected()) { val absVelocityX = abs(velocityX) val absVelocityY = abs(velocityY) if (absVelocityX.coerceAtLeast(absVelocityY) > tossFlingVelocity) { if (absVelocityX > tossFlingVelocity) { - playerImpl.popupLayoutParams.x = velocityX.toInt() + player.popupLayoutParams!!.x = velocityX.toInt() } if (absVelocityY > tossFlingVelocity) { - playerImpl.popupLayoutParams.y = velocityY.toInt() + player.popupLayoutParams!!.y = velocityY.toInt() } - playerImpl.checkPopupPositionBounds() - playerImpl.windowManager - .updateViewLayout(playerImpl.rootView, playerImpl.popupLayoutParams) + player.checkPopupPositionBounds() + player.windowManager!!.updateViewLayout(player.rootView, player.popupLayoutParams) return true } return false @@ -326,13 +319,13 @@ abstract class BasePlayerGestureListener( distanceY: Float ): Boolean { - if (!playerImpl.isFullscreen) { + if (!player.isFullscreen) { return false } val isTouchingStatusBar: Boolean = initialEvent.y < getStatusBarHeight(service) val isTouchingNavigationBar: Boolean = - initialEvent.y > (playerImpl.rootView.height - getNavigationBarHeight(service)) + initialEvent.y > (player.rootView.height - getNavigationBarHeight(service)) if (isTouchingStatusBar || isTouchingNavigationBar) { return false } @@ -340,7 +333,7 @@ abstract class BasePlayerGestureListener( val insideThreshold = abs(movingEvent.y - initialEvent.y) <= MOVEMENT_THRESHOLD if ( !isMovingInMain && (insideThreshold || abs(distanceX) > abs(distanceY)) || - playerImpl.currentState == BasePlayer.STATE_COMPLETED + player.currentState == Player.STATE_COMPLETED ) { return false } @@ -371,7 +364,7 @@ abstract class BasePlayerGestureListener( } if (!isMovingInPopup) { - AnimationUtils.animateView(playerImpl.closeOverlayButton, true, 200) + AnimationUtils.animateView(player.closeOverlayButton, true, 200) } isMovingInPopup = true @@ -381,20 +374,20 @@ abstract class BasePlayerGestureListener( val diffY: Float = (movingEvent.rawY - initialEvent.rawY) var posY: Float = (initialPopupY + diffY) - if (posX > playerImpl.screenWidth - playerImpl.popupWidth) { - posX = (playerImpl.screenWidth - playerImpl.popupWidth) + if (posX > player.screenWidth - player.popupLayoutParams!!.width) { + posX = (player.screenWidth - player.popupLayoutParams!!.width) } else if (posX < 0) { posX = 0f } - if (posY > playerImpl.screenHeight - playerImpl.popupHeight) { - posY = (playerImpl.screenHeight - playerImpl.popupHeight) + if (posY > player.screenHeight - player.popupLayoutParams!!.height) { + posY = (player.screenHeight - player.popupLayoutParams!!.height) } else if (posY < 0) { posY = 0f } - playerImpl.popupLayoutParams.x = posX.toInt() - playerImpl.popupLayoutParams.y = posY.toInt() + player.popupLayoutParams!!.x = posX.toInt() + player.popupLayoutParams!!.y = posY.toInt() onScroll( MainPlayer.PlayerType.POPUP, @@ -405,8 +398,7 @@ abstract class BasePlayerGestureListener( distanceY ) - playerImpl.windowManager - .updateViewLayout(playerImpl.rootView, playerImpl.popupLayoutParams) + player.windowManager!!.updateViewLayout(player.rootView, player.popupLayoutParams) return true } @@ -474,16 +466,16 @@ abstract class BasePlayerGestureListener( // /////////////////////////////////////////////////////////////////// private fun getDisplayPortion(e: MotionEvent): DisplayPortion { - return if (playerImpl.playerType == MainPlayer.PlayerType.POPUP) { + return if (player.playerType == MainPlayer.PlayerType.POPUP) { when { - e.x < playerImpl.popupWidth / 3.0 -> DisplayPortion.LEFT - e.x > playerImpl.popupWidth * 2.0 / 3.0 -> DisplayPortion.RIGHT + e.x < player.popupLayoutParams!!.width / 3.0 -> DisplayPortion.LEFT + e.x > player.popupLayoutParams!!.width * 2.0 / 3.0 -> DisplayPortion.RIGHT else -> DisplayPortion.MIDDLE } } else /* MainPlayer.PlayerType.VIDEO */ { when { - e.x < playerImpl.rootView.width / 3.0 -> DisplayPortion.LEFT - e.x > playerImpl.rootView.width * 2.0 / 3.0 -> DisplayPortion.RIGHT + e.x < player.rootView.width / 3.0 -> DisplayPortion.LEFT + e.x > player.rootView.width * 2.0 / 3.0 -> DisplayPortion.RIGHT else -> DisplayPortion.MIDDLE } } @@ -491,14 +483,14 @@ abstract class BasePlayerGestureListener( // Currently needed for scrolling since there is no action more the middle portion private fun getDisplayHalfPortion(e: MotionEvent): DisplayPortion { - return if (playerImpl.playerType == MainPlayer.PlayerType.POPUP) { + return if (player.playerType == MainPlayer.PlayerType.POPUP) { 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 /* MainPlayer.PlayerType.VIDEO */ { 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 } } @@ -522,7 +514,7 @@ abstract class BasePlayerGestureListener( companion object { 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 MOVEMENT_THRESHOLD = 40 diff --git a/app/src/main/java/org/schabi/newpipe/player/event/CustomBottomSheetBehavior.java b/app/src/main/java/org/schabi/newpipe/player/event/CustomBottomSheetBehavior.java index 26ecb1871..347118de5 100644 --- a/app/src/main/java/org/schabi/newpipe/player/event/CustomBottomSheetBehavior.java +++ b/app/src/main/java/org/schabi/newpipe/player/event/CustomBottomSheetBehavior.java @@ -24,7 +24,7 @@ public class CustomBottomSheetBehavior extends BottomSheetBehavior private boolean skippingInterception = false; private final List skipInterceptionOfElements = Arrays.asList( 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); @Override diff --git a/app/src/main/java/org/schabi/newpipe/player/event/PlayerGestureListener.java b/app/src/main/java/org/schabi/newpipe/player/event/PlayerGestureListener.java index ab1330f7b..887e32a23 100644 --- a/app/src/main/java/org/schabi/newpipe/player/event/PlayerGestureListener.java +++ b/app/src/main/java/org/schabi/newpipe/player/event/PlayerGestureListener.java @@ -11,15 +11,15 @@ import android.widget.ProgressBar; import androidx.appcompat.content.res.AppCompatResources; import org.jetbrains.annotations.NotNull; +import org.schabi.newpipe.MainActivity; import org.schabi.newpipe.R; -import org.schabi.newpipe.player.BasePlayer; 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 static org.schabi.newpipe.player.BasePlayer.STATE_PLAYING; -import static org.schabi.newpipe.player.VideoPlayer.DEFAULT_CONTROLS_DURATION; -import static org.schabi.newpipe.player.VideoPlayer.DEFAULT_CONTROLS_HIDE_TIME; +import static org.schabi.newpipe.player.Player.STATE_PLAYING; +import static org.schabi.newpipe.player.Player.DEFAULT_CONTROLS_DURATION; +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.animateView; @@ -33,14 +33,14 @@ import static org.schabi.newpipe.util.AnimationUtils.animateView; public class PlayerGestureListener extends BasePlayerGestureListener implements View.OnTouchListener { - private static final String TAG = ".PlayerGestureListener"; - private static final boolean DEBUG = BasePlayer.DEBUG; + private static final String TAG = PlayerGestureListener.class.getSimpleName(); + private static final boolean DEBUG = MainActivity.DEBUG; private final int maxVolume; - public PlayerGestureListener(final VideoPlayerImpl playerImpl, final MainPlayer service) { - super(playerImpl, service); - maxVolume = playerImpl.getAudioReactor().getMaxVolume(); + public PlayerGestureListener(final Player player, final MainPlayer service) { + super(player, service); + maxVolume = player.getAudioReactor().getMaxVolume(); } @Override @@ -48,46 +48,44 @@ public class PlayerGestureListener @NotNull final DisplayPortion portion) { if (DEBUG) { Log.d(TAG, "onDoubleTap called with playerType = [" - + playerImpl.getPlayerType() + "], portion = [" - + portion + "]"); + + player.getPlayerType() + "], portion = [" + portion + "]"); } - if (playerImpl.isSomePopupMenuVisible()) { - playerImpl.hideControls(0, 0); + if (player.isSomePopupMenuVisible()) { + player.hideControls(0, 0); } if (portion == DisplayPortion.LEFT) { - playerImpl.onFastRewind(); + player.fastRewind(); } else if (portion == DisplayPortion.MIDDLE) { - playerImpl.onPlayPause(); + player.playPause(); } else if (portion == DisplayPortion.RIGHT) { - playerImpl.onFastForward(); + player.fastForward(); } } @Override public void onSingleTap(@NotNull final MainPlayer.PlayerType playerType) { if (DEBUG) { - Log.d(TAG, "onSingleTap called with playerType = [" - + playerImpl.getPlayerType() + "]"); + Log.d(TAG, "onSingleTap called with playerType = [" + player.getPlayerType() + "]"); } if (playerType == MainPlayer.PlayerType.POPUP) { - if (playerImpl.isControlsVisible()) { - playerImpl.hideControls(100, 100); + if (player.isControlsVisible()) { + player.hideControls(100, 100); } else { - playerImpl.getPlayPauseButton().requestFocus(); - playerImpl.showControlsThenHide(); + player.getPlayPauseButton().requestFocus(); + player.showControlsThenHide(); } } else /* playerType == MainPlayer.PlayerType.VIDEO */ { - if (playerImpl.isControlsVisible()) { - playerImpl.hideControls(150, 0); + if (player.isControlsVisible()) { + player.hideControls(150, 0); } else { - if (playerImpl.getCurrentState() == BasePlayer.STATE_COMPLETED) { - playerImpl.showControls(0); + if (player.getCurrentState() == Player.STATE_COMPLETED) { + player.showControls(0); } else { - playerImpl.showControlsThenHide(); + player.showControlsThenHide(); } } } @@ -101,8 +99,7 @@ public class PlayerGestureListener final float distanceX, final float distanceY) { if (DEBUG) { Log.d(TAG, "onScroll called with playerType = [" - + playerImpl.getPlayerType() + "], portion = [" - + portion + "]"); + + player.getPlayerType() + "], portion = [" + portion + "]"); } if (playerType == MainPlayer.PlayerType.VIDEO) { final boolean isBrightnessGestureEnabled = @@ -123,8 +120,8 @@ public class PlayerGestureListener } } else /* MainPlayer.PlayerType.POPUP */ { - final View closingOverlayView = playerImpl.getClosingOverlayView(); - if (playerImpl.isInsideClosingRadius(movingEvent)) { + final View closingOverlayView = player.getClosingOverlayView(); + if (player.isInsideClosingRadius(movingEvent)) { if (closingOverlayView.getVisibility() == View.GONE) { animateView(closingOverlayView, true, 250); } @@ -137,17 +134,17 @@ public class PlayerGestureListener } private void onScrollMainVolume(final float distanceX, final float distanceY) { - playerImpl.getVolumeProgressBar().incrementProgressBy((int) distanceY); - final float currentProgressPercent = (float) playerImpl - .getVolumeProgressBar().getProgress() / playerImpl.getMaxGestureLength(); + player.getVolumeProgressBar().incrementProgressBy((int) distanceY); + final float currentProgressPercent = (float) player + .getVolumeProgressBar().getProgress() / player.getMaxGestureLength(); final int currentVolume = (int) (maxVolume * currentProgressPercent); - playerImpl.getAudioReactor().setVolume(currentVolume); + player.getAudioReactor().setVolume(currentVolume); if (DEBUG) { Log.d(TAG, "onScroll().volumeControl, currentVolume = " + currentVolume); } - playerImpl.getVolumeImageView().setImageDrawable( + player.getVolumeImageView().setImageDrawable( AppCompatResources.getDrawable(service, currentProgressPercent <= 0 ? R.drawable.ic_volume_off_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) ); - if (playerImpl.getVolumeRelativeLayout().getVisibility() != View.VISIBLE) { - animateView(playerImpl.getVolumeRelativeLayout(), SCALE_AND_ALPHA, true, 200); + if (player.getVolumeRelativeLayout().getVisibility() != View.VISIBLE) { + animateView(player.getVolumeRelativeLayout(), SCALE_AND_ALPHA, true, 200); } - if (playerImpl.getBrightnessRelativeLayout().getVisibility() == View.VISIBLE) { - playerImpl.getBrightnessRelativeLayout().setVisibility(View.GONE); + if (player.getBrightnessRelativeLayout().getVisibility() == View.VISIBLE) { + player.getBrightnessRelativeLayout().setVisibility(View.GONE); } } private void onScrollMainBrightness(final float distanceX, final float distanceY) { - final Activity parent = playerImpl.getParentActivity(); + final Activity parent = player.getParentActivity(); if (parent == null) { return; } final Window window = parent.getWindow(); final WindowManager.LayoutParams layoutParams = window.getAttributes(); - final ProgressBar bar = playerImpl.getBrightnessProgressBar(); + final ProgressBar bar = player.getBrightnessProgressBar(); final float oldBrightness = layoutParams.screenBrightness; bar.setProgress((int) (bar.getMax() * Math.max(0, Math.min(1, oldBrightness)))); bar.incrementProgressBy((int) distanceY); @@ -188,7 +185,7 @@ public class PlayerGestureListener + "currentBrightness = " + currentProgressPercent); } - playerImpl.getBrightnessImageView().setImageDrawable( + player.getBrightnessImageView().setImageDrawable( AppCompatResources.getDrawable(service, currentProgressPercent < 0.25 ? R.drawable.ic_brightness_low_white_24dp @@ -197,11 +194,11 @@ public class PlayerGestureListener : R.drawable.ic_brightness_high_white_24dp) ); - if (playerImpl.getBrightnessRelativeLayout().getVisibility() != View.VISIBLE) { - animateView(playerImpl.getBrightnessRelativeLayout(), SCALE_AND_ALPHA, true, 200); + if (player.getBrightnessRelativeLayout().getVisibility() != View.VISIBLE) { + animateView(player.getBrightnessRelativeLayout(), SCALE_AND_ALPHA, true, 200); } - if (playerImpl.getVolumeRelativeLayout().getVisibility() == View.VISIBLE) { - playerImpl.getVolumeRelativeLayout().setVisibility(View.GONE); + if (player.getVolumeRelativeLayout().getVisibility() == View.VISIBLE) { + player.getVolumeRelativeLayout().setVisibility(View.GONE); } } @@ -210,40 +207,40 @@ public class PlayerGestureListener @NotNull final MotionEvent event) { if (DEBUG) { Log.d(TAG, "onScrollEnd called with playerType = [" - + playerImpl.getPlayerType() + "]"); + + player.getPlayerType() + "]"); } if (playerType == MainPlayer.PlayerType.VIDEO) { if (DEBUG) { Log.d(TAG, "onScrollEnd() called"); } - if (playerImpl.getVolumeRelativeLayout().getVisibility() == View.VISIBLE) { - animateView(playerImpl.getVolumeRelativeLayout(), SCALE_AND_ALPHA, + if (player.getVolumeRelativeLayout().getVisibility() == View.VISIBLE) { + animateView(player.getVolumeRelativeLayout(), SCALE_AND_ALPHA, false, 200, 200); } - if (playerImpl.getBrightnessRelativeLayout().getVisibility() == View.VISIBLE) { - animateView(playerImpl.getBrightnessRelativeLayout(), SCALE_AND_ALPHA, + if (player.getBrightnessRelativeLayout().getVisibility() == View.VISIBLE) { + animateView(player.getBrightnessRelativeLayout(), SCALE_AND_ALPHA, false, 200, 200); } - if (playerImpl.isControlsVisible() && playerImpl.getCurrentState() == STATE_PLAYING) { - playerImpl.hideControls(DEFAULT_CONTROLS_DURATION, DEFAULT_CONTROLS_HIDE_TIME); + if (player.isControlsVisible() && player.getCurrentState() == STATE_PLAYING) { + player.hideControls(DEFAULT_CONTROLS_DURATION, DEFAULT_CONTROLS_HIDE_TIME); } } else { - if (playerImpl == null) { + if (player == null) { return; } - if (playerImpl.isControlsVisible() && playerImpl.getCurrentState() == STATE_PLAYING) { - playerImpl.hideControls(DEFAULT_CONTROLS_DURATION, DEFAULT_CONTROLS_HIDE_TIME); + if (player.isControlsVisible() && player.getCurrentState() == STATE_PLAYING) { + player.hideControls(DEFAULT_CONTROLS_DURATION, DEFAULT_CONTROLS_HIDE_TIME); } - if (playerImpl.isInsideClosingRadius(event)) { - playerImpl.closePopup(); + if (player.isInsideClosingRadius(event)) { + player.closePopup(); } else { - animateView(playerImpl.getClosingOverlayView(), false, 0); + animateView(player.getClosingOverlayView(), false, 0); - if (!playerImpl.isPopupClosing) { - animateView(playerImpl.getCloseOverlayButton(), false, 200); + if (!player.isPopupClosing()) { + animateView(player.getCloseOverlayButton(), false, 200); } } } @@ -254,12 +251,12 @@ public class PlayerGestureListener if (DEBUG) { Log.d(TAG, "onPopupResizingStart called"); } - playerImpl.showAndAnimateControl(-1, true); - playerImpl.getLoadingPanel().setVisibility(View.GONE); + player.showAndAnimateControl(-1, true); + player.getLoadingPanel().setVisibility(View.GONE); - playerImpl.hideControls(0, 0); - animateView(playerImpl.getCurrentDisplaySeek(), false, 0, 0); - animateView(playerImpl.getResizingIndicator(), true, 200, 0); + player.hideControls(0, 0); + animateView(player.getCurrentDisplaySeek(), false, 0, 0); + animateView(player.getResizingIndicator(), true, 200, 0); } @Override @@ -267,7 +264,7 @@ public class PlayerGestureListener if (DEBUG) { Log.d(TAG, "onPopupResizingEnd called"); } - animateView(playerImpl.getResizingIndicator(), false, 100, 0); + animateView(player.getResizingIndicator(), false, 100, 0); } } diff --git a/app/src/main/java/org/schabi/newpipe/player/event/PlayerServiceExtendedEventListener.java b/app/src/main/java/org/schabi/newpipe/player/event/PlayerServiceExtendedEventListener.java index 93952a811..f774c90a0 100644 --- a/app/src/main/java/org/schabi/newpipe/player/event/PlayerServiceExtendedEventListener.java +++ b/app/src/main/java/org/schabi/newpipe/player/event/PlayerServiceExtendedEventListener.java @@ -1,10 +1,10 @@ package org.schabi.newpipe.player.event; import org.schabi.newpipe.player.MainPlayer; -import org.schabi.newpipe.player.VideoPlayerImpl; +import org.schabi.newpipe.player.Player; public interface PlayerServiceExtendedEventListener extends PlayerServiceEventListener { - void onServiceConnected(VideoPlayerImpl player, + void onServiceConnected(Player player, MainPlayer playerService, boolean playAfterConnect); void onServiceDisconnected(); diff --git a/app/src/main/java/org/schabi/newpipe/player/helper/AudioReactor.java b/app/src/main/java/org/schabi/newpipe/player/helper/AudioReactor.java index ffe19599d..13ee24e16 100644 --- a/app/src/main/java/org/schabi/newpipe/player/helper/AudioReactor.java +++ b/app/src/main/java/org/schabi/newpipe/player/helper/AudioReactor.java @@ -5,14 +5,14 @@ import android.animation.AnimatorListenerAdapter; import android.animation.ValueAnimator; import android.content.Context; import android.content.Intent; -import android.media.AudioFocusRequest; import android.media.AudioManager; import android.media.audiofx.AudioEffect; -import android.os.Build; import android.util.Log; import androidx.annotation.NonNull; import androidx.core.content.ContextCompat; +import androidx.media.AudioFocusRequestCompat; +import androidx.media.AudioManagerCompat; import com.google.android.exoplayer2.SimpleExoPlayer; 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 boolean SHOULD_BUILD_FOCUS_REQUEST = - Build.VERSION.SDK_INT >= Build.VERSION_CODES.O; - private static final int DUCK_DURATION = 1500; 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 final SimpleExoPlayer player; private final Context context; private final AudioManager audioManager; - private final AudioFocusRequest request; + private final AudioFocusRequestCompat request; public AudioReactor(@NonNull final Context context, @NonNull final SimpleExoPlayer player) { @@ -43,15 +40,11 @@ public class AudioReactor implements AudioManager.OnAudioFocusChangeListener, An this.audioManager = ContextCompat.getSystemService(context, AudioManager.class); player.addAnalyticsListener(this); - if (SHOULD_BUILD_FOCUS_REQUEST) { - request = new AudioFocusRequest.Builder(FOCUS_GAIN_TYPE) - .setAcceptsDelayedFocusGain(true) - .setWillPauseWhenDucked(true) - .setOnAudioFocusChangeListener(this) - .build(); - } else { - request = null; - } + request = new AudioFocusRequestCompat.Builder(FOCUS_GAIN_TYPE) + //.setAcceptsDelayedFocusGain(true) + .setWillPauseWhenDucked(true) + .setOnAudioFocusChangeListener(this) + .build(); } public void dispose() { @@ -64,19 +57,11 @@ public class AudioReactor implements AudioManager.OnAudioFocusChangeListener, An //////////////////////////////////////////////////////////////////////////*/ public void requestAudioFocus() { - if (SHOULD_BUILD_FOCUS_REQUEST) { - audioManager.requestAudioFocus(request); - } else { - audioManager.requestAudioFocus(this, STREAM_TYPE, FOCUS_GAIN_TYPE); - } + AudioManagerCompat.requestAudioFocus(audioManager, request); } public void abandonAudioFocus() { - if (SHOULD_BUILD_FOCUS_REQUEST) { - audioManager.abandonAudioFocusRequest(request); - } else { - audioManager.abandonAudioFocus(this); - } + AudioManagerCompat.abandonAudioFocusRequest(audioManager, request); } public int getVolume() { @@ -88,7 +73,7 @@ public class AudioReactor implements AudioManager.OnAudioFocusChangeListener, An } public int getMaxVolume() { - return audioManager.getStreamMaxVolume(STREAM_TYPE); + return AudioManagerCompat.getStreamMaxVolume(audioManager, STREAM_TYPE); } /*////////////////////////////////////////////////////////////////////////// diff --git a/app/src/main/java/org/schabi/newpipe/player/helper/LoadController.java b/app/src/main/java/org/schabi/newpipe/player/helper/LoadController.java index d123a263b..0604e6ae8 100644 --- a/app/src/main/java/org/schabi/newpipe/player/helper/LoadController.java +++ b/app/src/main/java/org/schabi/newpipe/player/helper/LoadController.java @@ -26,12 +26,12 @@ public class LoadController implements LoadControl { } private LoadController(final int initialPlaybackBufferMs, - final int minimumPlaybackbufferMs, + final int minimumPlaybackBufferMs, final int optimalPlaybackBufferMs) { this.initialPlaybackBufferUs = initialPlaybackBufferMs * 1000; final DefaultLoadControl.Builder builder = new DefaultLoadControl.Builder(); - builder.setBufferDurationsMs(minimumPlaybackbufferMs, optimalPlaybackBufferMs, + builder.setBufferDurationsMs(minimumPlaybackBufferMs, optimalPlaybackBufferMs, initialPlaybackBufferMs, initialPlaybackBufferMs); internalLoadControl = builder.createDefaultLoadControl(); } diff --git a/app/src/main/java/org/schabi/newpipe/player/helper/PlaybackParameterDialog.java b/app/src/main/java/org/schabi/newpipe/player/helper/PlaybackParameterDialog.java index 8b2c0e925..253f0fbba 100644 --- a/app/src/main/java/org/schabi/newpipe/player/helper/PlaybackParameterDialog.java +++ b/app/src/main/java/org/schabi/newpipe/player/helper/PlaybackParameterDialog.java @@ -18,7 +18,7 @@ import androidx.fragment.app.DialogFragment; import org.schabi.newpipe.R; 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; public class PlaybackParameterDialog extends DialogFragment { diff --git a/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHelper.java b/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHelper.java index d89b5dd19..54021b616 100644 --- a/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHelper.java +++ b/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHelper.java @@ -1,8 +1,15 @@ package org.schabi.newpipe.player.helper; +import android.annotation.SuppressLint; import android.content.Context; +import android.content.Intent; import android.content.SharedPreferences; +import android.graphics.PixelFormat; +import android.os.Build; import android.provider.Settings; +import android.view.Gravity; +import android.view.ViewGroup; +import android.view.WindowManager; import android.view.accessibility.CaptioningManager; import androidx.annotation.IntDef; @@ -11,11 +18,14 @@ import androidx.annotation.Nullable; import androidx.core.content.ContextCompat; 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.text.CaptionStyleCompat; import com.google.android.exoplayer2.trackselection.AdaptiveTrackSelection; import com.google.android.exoplayer2.trackselection.TrackSelection; import com.google.android.exoplayer2.ui.AspectRatioFrameLayout; +import com.google.android.exoplayer2.ui.AspectRatioFrameLayout.ResizeMode; import com.google.android.exoplayer2.util.MimeTypes; 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.VideoStream; 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.PlayQueueItem; import org.schabi.newpipe.player.playqueue.SinglePlayQueue; @@ -41,13 +53,16 @@ import java.util.Formatter; import java.util.HashSet; import java.util.List; import java.util.Locale; +import java.util.Objects; import java.util.Set; import java.util.concurrent.TimeUnit; -import static com.google.android.exoplayer2.ui.AspectRatioFrameLayout.RESIZE_MODE_FILL; -import static com.google.android.exoplayer2.ui.AspectRatioFrameLayout.RESIZE_MODE_FIT; -import static com.google.android.exoplayer2.ui.AspectRatioFrameLayout.RESIZE_MODE_ZOOM; +import static com.google.android.exoplayer2.Player.REPEAT_MODE_ALL; +import static com.google.android.exoplayer2.Player.REPEAT_MODE_OFF; +import static com.google.android.exoplayer2.Player.REPEAT_MODE_ONE; 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_NEVER; 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; } + @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() { } //////////////////////////////////////////////////////////////////////////// @@ -121,14 +145,16 @@ public final class PlayerHelper { @NonNull public static String resizeTypeOf(@NonNull final Context context, - @AspectRatioFrameLayout.ResizeMode final int resizeMode) { + @ResizeMode final int resizeMode) { switch (resizeMode) { - case RESIZE_MODE_FIT: + case AspectRatioFrameLayout.RESIZE_MODE_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); - case RESIZE_MODE_ZOOM: + case AspectRatioFrameLayout.RESIZE_MODE_ZOOM: return context.getResources().getString(R.string.resize_zoom); + case AspectRatioFrameLayout.RESIZE_MODE_FIXED_HEIGHT: + case AspectRatioFrameLayout.RESIZE_MODE_FIXED_WIDTH: default: throw new IllegalArgumentException("Unrecognized resize mode: " + resizeMode); } @@ -199,23 +225,23 @@ public final class PlayerHelper { //////////////////////////////////////////////////////////////////////////// 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) { - 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) { - return isBrightnessGestureEnabled(context, true); - } - - public static boolean isRememberingPopupDimensions(@NonNull final Context context) { - return isRememberingPopupDimensions(context, true); + return getPreferences(context) + .getBoolean(context.getString(R.string.brightness_gesture_control_key), true); } 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) { @@ -229,7 +255,8 @@ public final class PlayerHelper { 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 action = getMinimizeOnExitAction(context, defaultAction); + final String action = getPreferences(context) + .getString(context.getString(R.string.minimize_on_exit_key), defaultAction); if (action.equals(popupAction)) { return MINIMIZE_ON_EXIT_MODE_POPUP; } 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 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))) { return AUTOPLAY_TYPE_ALWAYS; } else if (type.equals(context.getString(R.string.autoplay_never_key))) { @@ -350,14 +391,32 @@ public final class PlayerHelper { 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) { - //a value of less than 0, the default, means to use the preferred screen brightness - return getScreenBrightness(context, -1); + 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 -1; + } else { + return sp.getFloat(context.getString(R.string.screen_brightness_key), -1); + } } public static void setScreenBrightness(@NonNull final Context context, - final float setScreenBrightness) { - setScreenBrightness(context, setScreenBrightness, System.currentTimeMillis()); + final float screenBrightness) { + 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) { @@ -376,75 +435,11 @@ public final class PlayerHelper { 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) { return getPreferences(context) .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( final StreamInfoItem streamInfoItem) { final SinglePlayQueue singlePlayQueue = new SinglePlayQueue(streamInfoItem); @@ -452,12 +447,168 @@ public final class PlayerHelper { return singlePlayQueue; } - @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; + + //////////////////////////////////////////////////////////////////////////// + // Utils used by player + //////////////////////////////////////////////////////////////////////////// + + public static MainPlayer.PlayerType retrievePlayerTypeFromIntent(final Intent intent) { + // 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)))); } } diff --git a/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHolder.java b/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHolder.java index 854e3eb2b..da1238c81 100644 --- a/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHolder.java +++ b/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHolder.java @@ -16,7 +16,7 @@ import org.schabi.newpipe.App; import org.schabi.newpipe.MainActivity; import org.schabi.newpipe.extractor.stream.StreamInfo; 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.PlayerServiceExtendedEventListener; import org.schabi.newpipe.player.playqueue.PlayQueue; @@ -33,7 +33,7 @@ public final class PlayerHolder { private static ServiceConnection serviceConnection; public static boolean bound; private static MainPlayer playerService; - private static VideoPlayerImpl player; + private static Player player; /** * Returns the current {@link MainPlayer.PlayerType} of the {@link MainPlayer} service, diff --git a/app/src/main/java/org/schabi/newpipe/player/mediasession/MediaSessionCallback.java b/app/src/main/java/org/schabi/newpipe/player/mediasession/MediaSessionCallback.java index 883d9bb4f..c4b02d985 100644 --- a/app/src/main/java/org/schabi/newpipe/player/mediasession/MediaSessionCallback.java +++ b/app/src/main/java/org/schabi/newpipe/player/mediasession/MediaSessionCallback.java @@ -3,11 +3,11 @@ package org.schabi.newpipe.player.mediasession; import android.support.v4.media.MediaDescriptionCompat; public interface MediaSessionCallback { - void onSkipToPrevious(); + void playPrevious(); - void onSkipToNext(); + void playNext(); - void onSkipToIndex(int index); + void playItemAtIndex(int index); int getCurrentPlayingIndex(); @@ -15,7 +15,7 @@ public interface MediaSessionCallback { MediaDescriptionCompat getQueueMetadata(int index); - void onPlay(); + void play(); - void onPause(); + void pause(); } diff --git a/app/src/main/java/org/schabi/newpipe/player/mediasession/PlayQueueNavigator.java b/app/src/main/java/org/schabi/newpipe/player/mediasession/PlayQueueNavigator.java index 764c375af..62664c827 100644 --- a/app/src/main/java/org/schabi/newpipe/player/mediasession/PlayQueueNavigator.java +++ b/app/src/main/java/org/schabi/newpipe/player/mediasession/PlayQueueNavigator.java @@ -65,18 +65,18 @@ public class PlayQueueNavigator implements MediaSessionConnector.QueueNavigator @Override public void onSkipToPrevious(final Player player, final ControlDispatcher controlDispatcher) { - callback.onSkipToPrevious(); + callback.playPrevious(); } @Override public void onSkipToQueueItem(final Player player, final ControlDispatcher controlDispatcher, final long id) { - callback.onSkipToIndex((int) id); + callback.playItemAtIndex((int) id); } @Override public void onSkipToNext(final Player player, final ControlDispatcher controlDispatcher) { - callback.onSkipToNext(); + callback.playNext(); } private void publishFloatingQueueWindow() { diff --git a/app/src/main/java/org/schabi/newpipe/player/mediasession/PlayQueuePlaybackController.java b/app/src/main/java/org/schabi/newpipe/player/mediasession/PlayQueuePlaybackController.java index 21c99859c..8bfbcde6b 100644 --- a/app/src/main/java/org/schabi/newpipe/player/mediasession/PlayQueuePlaybackController.java +++ b/app/src/main/java/org/schabi/newpipe/player/mediasession/PlayQueuePlaybackController.java @@ -14,9 +14,9 @@ public class PlayQueuePlaybackController extends DefaultControlDispatcher { @Override public boolean dispatchSetPlayWhenReady(final Player player, final boolean playWhenReady) { if (playWhenReady) { - callback.onPlay(); + callback.play(); } else { - callback.onPause(); + callback.pause(); } return true; } diff --git a/app/src/main/java/org/schabi/newpipe/player/playback/BasePlayerMediaSession.java b/app/src/main/java/org/schabi/newpipe/player/playback/PlayerMediaSession.java similarity index 77% rename from app/src/main/java/org/schabi/newpipe/player/playback/BasePlayerMediaSession.java rename to app/src/main/java/org/schabi/newpipe/player/playback/PlayerMediaSession.java index 5b20077c3..9dcb12344 100644 --- a/app/src/main/java/org/schabi/newpipe/player/playback/BasePlayerMediaSession.java +++ b/app/src/main/java/org/schabi/newpipe/player/playback/PlayerMediaSession.java @@ -5,33 +5,33 @@ import android.os.Bundle; import android.support.v4.media.MediaDescriptionCompat; 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.playqueue.PlayQueueItem; -public class BasePlayerMediaSession implements MediaSessionCallback { - private final BasePlayer player; +public class PlayerMediaSession implements MediaSessionCallback { + private final Player player; - public BasePlayerMediaSession(final BasePlayer player) { + public PlayerMediaSession(final Player player) { this.player = player; } @Override - public void onSkipToPrevious() { - player.onPlayPrevious(); + public void playPrevious() { + player.playPrevious(); } @Override - public void onSkipToNext() { - player.onPlayNext(); + public void playNext() { + player.playNext(); } @Override - public void onSkipToIndex(final int index) { + public void playItemAtIndex(final int index) { if (player.getPlayQueue() == null) { return; } - player.onSelected(player.getPlayQueue().getItem(index)); + player.selectQueueItem(player.getPlayQueue().getItem(index)); } @Override @@ -52,11 +52,14 @@ public class BasePlayerMediaSession implements MediaSessionCallback { @Override 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; } - final PlayQueueItem item = player.getPlayQueue().getItem(index); final MediaDescriptionCompat.Builder descriptionBuilder = new MediaDescriptionCompat.Builder() .setMediaId(String.valueOf(index)) @@ -83,12 +86,12 @@ public class BasePlayerMediaSession implements MediaSessionCallback { } @Override - public void onPlay() { - player.onPlay(); + public void play() { + player.play(); } @Override - public void onPause() { - player.onPause(); + public void pause() { + player.pause(); } } diff --git a/app/src/main/java/org/schabi/newpipe/player/playqueue/AbstractInfoPlayQueue.java b/app/src/main/java/org/schabi/newpipe/player/playqueue/AbstractInfoPlayQueue.java index 17bb6f4c4..07c8d9f90 100644 --- a/app/src/main/java/org/schabi/newpipe/player/playqueue/AbstractInfoPlayQueue.java +++ b/app/src/main/java/org/schabi/newpipe/player/playqueue/AbstractInfoPlayQueue.java @@ -128,9 +128,9 @@ abstract class AbstractInfoPlayQueue ext fetchReactor = null; } - private static List extractListItems(final List infos) { + private static List extractListItems(final List infoItems) { final List result = new ArrayList<>(); - for (final InfoItem stream : infos) { + for (final InfoItem stream : infoItems) { if (stream instanceof StreamInfoItem) { result.add(new PlayQueueItem((StreamInfoItem) stream)); } diff --git a/app/src/main/java/org/schabi/newpipe/report/ErrorActivity.java b/app/src/main/java/org/schabi/newpipe/report/ErrorActivity.java index 3213821cd..a4b6af2ab 100644 --- a/app/src/main/java/org/schabi/newpipe/report/ErrorActivity.java +++ b/app/src/main/java/org/schabi/newpipe/report/ErrorActivity.java @@ -14,15 +14,11 @@ import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; -import android.widget.Button; -import android.widget.EditText; -import android.widget.TextView; import android.widget.Toast; import androidx.annotation.Nullable; import androidx.appcompat.app.ActionBar; import androidx.appcompat.app.AppCompatActivity; -import androidx.appcompat.widget.Toolbar; import androidx.core.app.NavUtils; 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.MainActivity; import org.schabi.newpipe.R; +import org.schabi.newpipe.databinding.ActivityErrorBinding; import org.schabi.newpipe.util.Localization; import org.schabi.newpipe.util.ShareUtils; import org.schabi.newpipe.util.ThemeHelper; @@ -87,7 +84,8 @@ public class ErrorActivity extends AppCompatActivity { private ErrorInfo errorInfo; private Class returnActivity; private String currentTimeStamp; - private EditText userCommentBox; + + private ActivityErrorBinding activityErrorBinding; public static void reportUiError(final AppCompatActivity activity, final Throwable el) { reportError(activity, el, activity.getClass(), null, ErrorInfo.make(UserAction.UI_ERROR, @@ -181,12 +179,13 @@ public class ErrorActivity extends AppCompatActivity { assureCorrectAppLanguage(this); super.onCreate(savedInstanceState); ThemeHelper.setTheme(this); - setContentView(R.layout.activity_error); + + activityErrorBinding = ActivityErrorBinding.inflate(getLayoutInflater()); + setContentView(activityErrorBinding.getRoot()); final Intent intent = getIntent(); - final Toolbar toolbar = findViewById(R.id.toolbar); - setSupportActionBar(toolbar); + setSupportActionBar(activityErrorBinding.toolbarLayout.toolbar); final ActionBar actionBar = getSupportActionBar(); if (actionBar != null) { @@ -195,15 +194,6 @@ public class ErrorActivity extends AppCompatActivity { 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(); returnActivity = ac.getReturnActivity(); errorInfo = intent.getParcelableExtra(ERROR_INFO); @@ -213,28 +203,27 @@ public class ErrorActivity extends AppCompatActivity { addGuruMeditation(); currentTimeStamp = getCurrentTimeStamp(); - reportEmailButton.setOnClickListener(v -> + activityErrorBinding.errorReportEmailButton.setOnClickListener(v -> openPrivacyPolicyDialog(this, "EMAIL")); - copyButton.setOnClickListener(v -> { + activityErrorBinding.errorReportCopyButton.setOnClickListener(v -> { ShareUtils.copyToClipboard(this, buildMarkdown()); Toast.makeText(this, R.string.msg_copied, Toast.LENGTH_SHORT).show(); }); - reportGithubButton.setOnClickListener(v -> + activityErrorBinding.errorReportGitHubButton.setOnClickListener(v -> openPrivacyPolicyDialog(this, "GITHUB")); - // normal bugreport buildInfo(errorInfo); if (errorInfo.getMessage() != 0) { - errorMessageView.setText(errorInfo.getMessage()); + activityErrorBinding.errorMessageView.setText(errorInfo.getMessage()); } else { - errorMessageView.setVisibility(View.GONE); - findViewById(R.id.messageWhatHappenedView).setVisibility(View.GONE); + activityErrorBinding.errorMessageView.setVisibility(View.GONE); + activityErrorBinding.messageWhatHappenedView.setVisibility(View.GONE); } - errorView.setText(formErrorText(errorList)); + activityErrorBinding.errorView.setText(formErrorText(errorList)); // print stack trace once again for debugging: for (final String e : errorList) { @@ -339,11 +328,10 @@ public class ErrorActivity extends AppCompatActivity { } private void buildInfo(final ErrorInfo info) { - final TextView infoLabelView = findViewById(R.id.errorInfoLabelsView); - final TextView infoView = findViewById(R.id.errorInfosView); 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" + info.getRequest() + "\n" @@ -356,7 +344,7 @@ public class ErrorActivity extends AppCompatActivity { + BuildConfig.VERSION_NAME + "\n" + getOsString(); - infoView.setText(text); + activityErrorBinding.errorInfosView.setText(text); } private String buildJson() { @@ -374,7 +362,8 @@ public class ErrorActivity extends AppCompatActivity { .value("os", getOsString()) .value("time", currentTimeStamp) .array("exceptions", Arrays.asList(errorList)) - .value("user_comment", userCommentBox.getText().toString()) + .value("user_comment", activityErrorBinding.errorCommentBox.getText() + .toString()) .end() .done(); } catch (final Throwable e) { @@ -389,7 +378,7 @@ public class ErrorActivity extends AppCompatActivity { try { final StringBuilder htmlErrorReport = new StringBuilder(); - final String userComment = userCommentBox.getText().toString(); + final String userComment = activityErrorBinding.errorCommentBox.getText().toString(); if (!userComment.isEmpty()) { htmlErrorReport.append(userComment).append("\n"); } @@ -473,10 +462,9 @@ public class ErrorActivity extends AppCompatActivity { private void addGuruMeditation() { //just an easter egg - final TextView sorryView = findViewById(R.id.errorSorryView); - String text = sorryView.getText().toString(); + String text = activityErrorBinding.errorSorryView.getText().toString(); text += "\n" + getString(R.string.guru_meditation); - sorryView.setText(text); + activityErrorBinding.errorSorryView.setText(text); } @Override diff --git a/app/src/main/java/org/schabi/newpipe/settings/AppearanceSettingsFragment.java b/app/src/main/java/org/schabi/newpipe/settings/AppearanceSettingsFragment.java index ab875ed5d..8126bd2c5 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/AppearanceSettingsFragment.java +++ b/app/src/main/java/org/schabi/newpipe/settings/AppearanceSettingsFragment.java @@ -8,6 +8,7 @@ import android.provider.Settings; import android.widget.Toast; import androidx.annotation.Nullable; +import androidx.core.app.ActivityCompat; import androidx.preference.Preference; import org.schabi.newpipe.R; @@ -31,7 +32,7 @@ public class AppearanceSettingsFragment extends BasePreferenceFragment { if (!newValue.equals(startThemeKey) && getActivity() != null) { // If it's not the current theme - getActivity().recreate(); + ActivityCompat.recreate(requireActivity()); } return false; diff --git a/app/src/main/java/org/schabi/newpipe/settings/ContentSettingsFragment.java b/app/src/main/java/org/schabi/newpipe/settings/ContentSettingsFragment.java index b0425ebfa..c0639131c 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/ContentSettingsFragment.java +++ b/app/src/main/java/org/schabi/newpipe/settings/ContentSettingsFragment.java @@ -11,6 +11,7 @@ import android.widget.Toast; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.core.content.ContextCompat; import androidx.preference.Preference; 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.ZipHelper; -import java.io.BufferedOutputStream; 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.util.Date; 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; @@ -50,12 +42,7 @@ public class ContentSettingsFragment extends BasePreferenceFragment { private static final int REQUEST_IMPORT_PATH = 8945; private static final int REQUEST_EXPORT_PATH = 30945; - private File databasesDir; - private File newpipeDb; - private File newpipeDbJournal; - private File newpipeDbShm; - private File newpipeDbWal; - private File newpipeSettings; + private ContentSettingsManager manager; private String thumbnailLoadToggleKey; private String youtubeRestrictedModeEnabledKey; @@ -120,16 +107,9 @@ public class ContentSettingsFragment extends BasePreferenceFragment { @Override public void onCreatePreferences(final Bundle savedInstanceState, final String rootKey) { - - final String homeDir = getActivity().getApplicationInfo().dataDir; - databasesDir = new File(homeDir + "/databases"); - 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(); + final File homeDir = ContextCompat.getDataDir(requireContext()); + manager = new ContentSettingsManager(new NewPipeFileLocator(homeDir)); + manager.deleteSettingsFile(); addPreferencesFromResource(R.xml.content_settings); @@ -212,62 +192,36 @@ public class ContentSettingsFragment extends BasePreferenceFragment { //checkpoint before export NewPipeDatabase.checkpoint(); - try (ZipOutputStream outZip = new ZipOutputStream(new BufferedOutputStream( - new FileOutputStream(path)))) { - ZipHelper.addFileToZip(outZip, newpipeDb.getPath(), "newpipe.db"); + final SharedPreferences preferences = PreferenceManager + .getDefaultSharedPreferences(requireContext()); + manager.exportDatabase(preferences, path); - saveSharedPreferencesToFile(newpipeSettings); - ZipHelper.addFileToZip(outZip, newpipeSettings.getPath(), - "newpipe.settings"); - } - - Toast.makeText(getContext(), R.string.export_complete_toast, Toast.LENGTH_SHORT) - .show(); + Toast.makeText(getContext(), R.string.export_complete_toast, Toast.LENGTH_SHORT).show(); } catch (final Exception 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) { // check if file is supported - try (ZipFile zipFile = new ZipFile(filePath)) { - } catch (final IOException ioe) { + if (!ZipHelper.isValidZipFile(filePath)) { Toast.makeText(getContext(), R.string.no_valid_zip_file, Toast.LENGTH_SHORT) - .show(); + .show(); return; } try { - if (!databasesDir.exists() && !databasesDir.mkdir()) { + if (!manager.ensureDbDirectoryExists()) { throw new Exception("Could not create databases dir"); } - final boolean isDbFileExtracted = ZipHelper.extractFileFromZip(filePath, - newpipeDb.getPath(), "newpipe.db"); - - if (isDbFileExtracted) { - newpipeDbJournal.delete(); - newpipeDbWal.delete(); - newpipeDbShm.delete(); - } else { + if (!manager.extractDb(filePath)) { 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 (ZipHelper.extractFileFromZip(filePath, newpipeSettings.getPath(), - "newpipe.settings")) { + if (manager.extractSettings(filePath)) { final AlertDialog.Builder alert = new AlertDialog.Builder(getContext()); alert.setTitle(R.string.import_settings); @@ -278,7 +232,8 @@ public class ContentSettingsFragment extends BasePreferenceFragment { }); alert.setPositiveButton(getString(R.string.finish), (dialog, which) -> { dialog.dismiss(); - loadSharedPreferences(newpipeSettings); + manager.loadSharedPreferences(PreferenceManager + .getDefaultSharedPreferences(requireContext())); // restart app to properly load db 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 entries = (Map) input.readObject(); - for (final Map.Entry 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 //////////////////////////////////////////////////////////////////////////*/ diff --git a/app/src/main/java/org/schabi/newpipe/settings/ContentSettingsManager.kt b/app/src/main/java/org/schabi/newpipe/settings/ContentSettingsManager.kt new file mode 100644 index 000000000..1730a230e --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/settings/ContentSettingsManager.kt @@ -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 + 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() + } + } +} diff --git a/app/src/main/java/org/schabi/newpipe/settings/DownloadSettingsFragment.java b/app/src/main/java/org/schabi/newpipe/settings/DownloadSettingsFragment.java index a4b29fc49..8742f0937 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/DownloadSettingsFragment.java +++ b/app/src/main/java/org/schabi/newpipe/settings/DownloadSettingsFragment.java @@ -246,10 +246,7 @@ public class DownloadSettingsFragment extends BasePreferenceFragment { // revoke permissions on the old save path (required for SAF only) - final Context context = getContext(); - if (context == null) { - throw new NullPointerException("getContext()"); - } + final Context context = requireContext(); forgetSAFTree(context, defaultPreferences.getString(key, "")); diff --git a/app/src/main/java/org/schabi/newpipe/settings/NewPipeFileLocator.kt b/app/src/main/java/org/schabi/newpipe/settings/NewPipeFileLocator.kt new file mode 100644 index 000000000..c2f93d15f --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/settings/NewPipeFileLocator.kt @@ -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") } +} diff --git a/app/src/main/java/org/schabi/newpipe/settings/SettingsActivity.java b/app/src/main/java/org/schabi/newpipe/settings/SettingsActivity.java index d2d4c2404..4de166a55 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/SettingsActivity.java +++ b/app/src/main/java/org/schabi/newpipe/settings/SettingsActivity.java @@ -7,12 +7,12 @@ import android.view.MenuItem; import androidx.appcompat.app.ActionBar; import androidx.appcompat.app.AppCompatActivity; -import androidx.appcompat.widget.Toolbar; import androidx.fragment.app.Fragment; import androidx.preference.Preference; import androidx.preference.PreferenceFragmentCompat; import org.schabi.newpipe.R; +import org.schabi.newpipe.databinding.SettingsLayoutBinding; import org.schabi.newpipe.util.DeviceUtils; import org.schabi.newpipe.util.ThemeHelper; import org.schabi.newpipe.views.FocusOverlayView; @@ -51,10 +51,12 @@ public class SettingsActivity extends AppCompatActivity setTheme(ThemeHelper.getSettingsThemeStyle(this)); assureCorrectAppLanguage(this); super.onCreate(savedInstanceBundle); - setContentView(R.layout.settings_layout); - final Toolbar toolbar = findViewById(R.id.toolbar); - setSupportActionBar(toolbar); + final SettingsLayoutBinding settingsLayoutBinding = + SettingsLayoutBinding.inflate(getLayoutInflater()); + setContentView(settingsLayoutBinding.getRoot()); + + setSupportActionBar(settingsLayoutBinding.toolbarLayout.toolbar); if (savedInstanceBundle == null) { getSupportFragmentManager().beginTransaction() diff --git a/app/src/main/java/org/schabi/newpipe/streams/Mp4FromDashWriter.java b/app/src/main/java/org/schabi/newpipe/streams/Mp4FromDashWriter.java index 5efffe118..ca3da9d24 100644 --- a/app/src/main/java/org/schabi/newpipe/streams/Mp4FromDashWriter.java +++ b/app/src/main/java/org/schabi/newpipe/streams/Mp4FromDashWriter.java @@ -483,7 +483,7 @@ public class Mp4FromDashWriter { // stsc_table_entry = [first_chunk, samples_per_chunk, sample_description_index] 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++] = firstCount; diff --git a/app/src/main/java/org/schabi/newpipe/streams/OggFromWebMWriter.java b/app/src/main/java/org/schabi/newpipe/streams/OggFromWebMWriter.java index 3b3c74e3a..266cec24a 100644 --- a/app/src/main/java/org/schabi/newpipe/streams/OggFromWebMWriter.java +++ b/app/src/main/java/org/schabi/newpipe/streams/OggFromWebMWriter.java @@ -1,416 +1,416 @@ -package org.schabi.newpipe.streams; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import org.schabi.newpipe.streams.WebMReader.Cluster; -import org.schabi.newpipe.streams.WebMReader.Segment; -import org.schabi.newpipe.streams.WebMReader.SimpleBlock; -import org.schabi.newpipe.streams.WebMReader.WebMTrack; -import org.schabi.newpipe.streams.io.SharpStream; - -import java.io.Closeable; -import java.io.IOException; -import java.nio.ByteBuffer; -import java.nio.ByteOrder; - -/** - * @author kapodamy - */ -public class OggFromWebMWriter implements Closeable { - private static final byte FLAG_UNSET = 0x00; - //private static final byte FLAG_CONTINUED = 0x01; - private static final byte FLAG_FIRST = 0x02; - private static final byte FLAG_LAST = 0x04; - - private static final byte HEADER_CHECKSUM_OFFSET = 22; - private static final byte HEADER_SIZE = 27; - - private static final int TIME_SCALE_NS = 1000000000; - - private boolean done = false; - private boolean parsed = false; - - private final SharpStream source; - private final SharpStream output; - - private int sequenceCount = 0; - private final int streamId; - private byte packetFlag = FLAG_FIRST; - - private WebMReader webm = null; - private WebMTrack webmTrack = null; - private Segment webmSegment = null; - private Cluster webmCluster = null; - private SimpleBlock webmBlock = null; - - private long webmBlockLastTimecode = 0; - private long webmBlockNearDuration = 0; - - private short segmentTableSize = 0; - private final byte[] segmentTable = new byte[255]; - private long segmentTableNextTimestamp = TIME_SCALE_NS; - - private final int[] crc32Table = new int[256]; - - public OggFromWebMWriter(@NonNull final SharpStream source, @NonNull final SharpStream target) { - if (!source.canRead() || !source.canRewind()) { - throw new IllegalArgumentException("source stream must be readable and allows seeking"); - } - if (!target.canWrite() || !target.canRewind()) { - throw new IllegalArgumentException("output stream must be writable and allows seeking"); - } - - this.source = source; - this.output = target; - - this.streamId = (int) System.currentTimeMillis(); - - populateCrc32Table(); - } - - public boolean isDone() { - return done; - } - - public boolean isParsed() { - return parsed; - } - - public WebMTrack[] getTracksFromSource() throws IllegalStateException { - if (!parsed) { - throw new IllegalStateException("source must be parsed first"); - } - - return webm.getAvailableTracks(); - } - - public void parseSource() throws IOException, IllegalStateException { - if (done) { - throw new IllegalStateException("already done"); - } - if (parsed) { - throw new IllegalStateException("already parsed"); - } - - try { - webm = new WebMReader(source); - webm.parse(); - webmSegment = webm.getNextSegment(); - } finally { - parsed = true; - } - } - - public void selectTrack(final int trackIndex) throws IOException { - if (!parsed) { - throw new IllegalStateException("source must be parsed first"); - } - if (done) { - throw new IOException("already done"); - } - if (webmTrack != null) { - throw new IOException("tracks already selected"); - } - - switch (webm.getAvailableTracks()[trackIndex].kind) { - case Audio: - case Video: - break; - default: - throw new UnsupportedOperationException("the track must an audio or video stream"); - } - - try { - webmTrack = webm.selectTrack(trackIndex); - } finally { - parsed = true; - } - } - - @Override - public void close() throws IOException { - done = true; - parsed = true; - - webmTrack = null; - webm = null; - - if (!output.isClosed()) { - output.flush(); - } - - source.close(); - output.close(); - } - - public void build() throws IOException { - final float resolution; - SimpleBlock bloq; - final ByteBuffer header = ByteBuffer.allocate(27 + (255 * 255)); - final ByteBuffer page = ByteBuffer.allocate(64 * 1024); - - header.order(ByteOrder.LITTLE_ENDIAN); - - /* step 1: get the amount of frames per seconds */ - switch (webmTrack.kind) { - case Audio: - resolution = getSampleFrequencyFromTrack(webmTrack.bMetadata); - if (resolution == 0f) { - throw new RuntimeException("cannot get the audio sample rate"); - } - break; - case Video: - // WARNING: untested - if (webmTrack.defaultDuration == 0) { - throw new RuntimeException("missing default frame time"); - } - resolution = 1000f / ((float) webmTrack.defaultDuration - / webmSegment.info.timecodeScale); - break; - default: - throw new RuntimeException("not implemented"); - } - - /* step 2: create packet with code init data */ - if (webmTrack.codecPrivate != null) { - addPacketSegment(webmTrack.codecPrivate.length); - makePacketheader(0x00, header, webmTrack.codecPrivate); - write(header); - output.write(webmTrack.codecPrivate); - } - - /* step 3: create packet with metadata */ - final byte[] buffer = makeMetadata(); - if (buffer != null) { - addPacketSegment(buffer.length); - makePacketheader(0x00, header, buffer); - write(header); - output.write(buffer); - } - - /* step 4: calculate amount of packets */ - while (webmSegment != null) { - bloq = getNextBlock(); - - if (bloq != null && addPacketSegment(bloq)) { - final int pos = page.position(); - //noinspection ResultOfMethodCallIgnored - bloq.data.read(page.array(), pos, bloq.dataSize); - page.position(pos + bloq.dataSize); - continue; - } - - // calculate the current packet duration using the next block - double elapsedNs = webmTrack.codecDelay; - - if (bloq == null) { - packetFlag = FLAG_LAST; // note: if the flag is FLAG_CONTINUED, is changed - elapsedNs += webmBlockLastTimecode; - - if (webmTrack.defaultDuration > 0) { - elapsedNs += webmTrack.defaultDuration; - } else { - // hardcoded way, guess the sample duration - elapsedNs += webmBlockNearDuration; - } - } else { - elapsedNs += bloq.absoluteTimeCodeNs; - } - - // get the sample count in the page - elapsedNs = elapsedNs / TIME_SCALE_NS; - elapsedNs = Math.ceil(elapsedNs * resolution); - - // create header and calculate page checksum - int checksum = makePacketheader((long) elapsedNs, header, null); - checksum = calcCrc32(checksum, page.array(), page.position()); - - header.putInt(HEADER_CHECKSUM_OFFSET, checksum); - - // dump data - write(header); - write(page); - - webmBlock = bloq; - } - } - - private int makePacketheader(final long granPos, @NonNull final ByteBuffer buffer, - final byte[] immediatePage) { - short length = HEADER_SIZE; - - buffer.putInt(0x5367674f); // "OggS" binary string in little-endian - buffer.put((byte) 0x00); // version - buffer.put(packetFlag); // type - - buffer.putLong(granPos); // granulate position - - buffer.putInt(streamId); // bitstream serial number - buffer.putInt(sequenceCount++); // page sequence number - - buffer.putInt(0x00); // page checksum - - buffer.put((byte) segmentTableSize); // segment table - buffer.put(segmentTable, 0, segmentTableSize); // segment size - - length += segmentTableSize; - - clearSegmentTable(); // clear segment table for next header - - int checksumCrc32 = calcCrc32(0x00, buffer.array(), length); - - if (immediatePage != null) { - checksumCrc32 = calcCrc32(checksumCrc32, immediatePage, immediatePage.length); - buffer.putInt(HEADER_CHECKSUM_OFFSET, checksumCrc32); - segmentTableNextTimestamp -= TIME_SCALE_NS; - } - - return checksumCrc32; - } - - @Nullable - private byte[] makeMetadata() { - if ("A_OPUS".equals(webmTrack.codecId)) { - return new byte[]{ - 0x4F, 0x70, 0x75, 0x73, 0x54, 0x61, 0x67, 0x73, // "OpusTags" binary string - 0x00, 0x00, 0x00, 0x00, // writing application string size (not present) - 0x00, 0x00, 0x00, 0x00 // additional tags count (zero means no tags) - }; - } else if ("A_VORBIS".equals(webmTrack.codecId)) { - return new byte[]{ - 0x03, // ¿¿¿??? - 0x76, 0x6f, 0x72, 0x62, 0x69, 0x73, // "vorbis" binary string - 0x00, 0x00, 0x00, 0x00, // writing application string size (not present) - 0x00, 0x00, 0x00, 0x00 // additional tags count (zero means no tags) - }; - } - - // not implemented for the desired codec - return null; - } - - private void write(final ByteBuffer buffer) throws IOException { - output.write(buffer.array(), 0, buffer.position()); - buffer.position(0); - } - - @Nullable - private SimpleBlock getNextBlock() throws IOException { - SimpleBlock res; - - if (webmBlock != null) { - res = webmBlock; - webmBlock = null; - return res; - } - - if (webmSegment == null) { - webmSegment = webm.getNextSegment(); - if (webmSegment == null) { - return null; // no more blocks in the selected track - } - } - - if (webmCluster == null) { - webmCluster = webmSegment.getNextCluster(); - if (webmCluster == null) { - webmSegment = null; - return getNextBlock(); - } - } - - res = webmCluster.getNextSimpleBlock(); - if (res == null) { - webmCluster = null; - return getNextBlock(); - } - - webmBlockNearDuration = res.absoluteTimeCodeNs - webmBlockLastTimecode; - webmBlockLastTimecode = res.absoluteTimeCodeNs; - - return res; - } - - private float getSampleFrequencyFromTrack(final byte[] bMetadata) { - // hardcoded way - final ByteBuffer buffer = ByteBuffer.wrap(bMetadata); - - while (buffer.remaining() >= 6) { - final int id = buffer.getShort() & 0xFFFF; - if (id == 0x0000B584) { - return buffer.getFloat(); - } - } - - return 0.0f; - } - - private void clearSegmentTable() { - segmentTableNextTimestamp += TIME_SCALE_NS; - packetFlag = FLAG_UNSET; - segmentTableSize = 0; - } - - private boolean addPacketSegment(final SimpleBlock block) { - final long timestamp = block.absoluteTimeCodeNs + webmTrack.codecDelay; - - if (timestamp >= segmentTableNextTimestamp) { - return false; - } - - return addPacketSegment(block.dataSize); - } - - private boolean addPacketSegment(final int size) { - if (size > 65025) { - throw new UnsupportedOperationException("page size cannot be larger than 65025"); - } - - int available = (segmentTable.length - segmentTableSize) * 255; - final boolean extra = (size % 255) == 0; - - if (extra) { - // add a zero byte entry in the table - // required to indicate the sample size is multiple of 255 - available -= 255; - } - - // check if possible add the segment, without overflow the table - if (available < size) { - return false; // not enough space on the page - } - - for (int seg = size; seg > 0; seg -= 255) { - segmentTable[segmentTableSize++] = (byte) Math.min(seg, 255); - } - - if (extra) { - segmentTable[segmentTableSize++] = 0x00; - } - - return true; - } - - private void populateCrc32Table() { - for (int i = 0; i < 0x100; i++) { - int crc = i << 24; - for (int j = 0; j < 8; j++) { - final long b = crc >>> 31; - crc <<= 1; - crc ^= (int) (0x100000000L - b) & 0x04c11db7; - } - crc32Table[i] = crc; - } - } - - private int calcCrc32(final int initialCrc, final byte[] buffer, final int size) { - int crc = initialCrc; - for (int i = 0; i < size; i++) { - final int reg = (crc >>> 24) & 0xff; - crc = (crc << 8) ^ crc32Table[reg ^ (buffer[i] & 0xff)]; - } - - return crc; - } -} +package org.schabi.newpipe.streams; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import org.schabi.newpipe.streams.WebMReader.Cluster; +import org.schabi.newpipe.streams.WebMReader.Segment; +import org.schabi.newpipe.streams.WebMReader.SimpleBlock; +import org.schabi.newpipe.streams.WebMReader.WebMTrack; +import org.schabi.newpipe.streams.io.SharpStream; + +import java.io.Closeable; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; + +/** + * @author kapodamy + */ +public class OggFromWebMWriter implements Closeable { + private static final byte FLAG_UNSET = 0x00; + //private static final byte FLAG_CONTINUED = 0x01; + private static final byte FLAG_FIRST = 0x02; + private static final byte FLAG_LAST = 0x04; + + private static final byte HEADER_CHECKSUM_OFFSET = 22; + private static final byte HEADER_SIZE = 27; + + private static final int TIME_SCALE_NS = 1000000000; + + private boolean done = false; + private boolean parsed = false; + + private final SharpStream source; + private final SharpStream output; + + private int sequenceCount = 0; + private final int streamId; + private byte packetFlag = FLAG_FIRST; + + private WebMReader webm = null; + private WebMTrack webmTrack = null; + private Segment webmSegment = null; + private Cluster webmCluster = null; + private SimpleBlock webmBlock = null; + + private long webmBlockLastTimecode = 0; + private long webmBlockNearDuration = 0; + + private short segmentTableSize = 0; + private final byte[] segmentTable = new byte[255]; + private long segmentTableNextTimestamp = TIME_SCALE_NS; + + private final int[] crc32Table = new int[256]; + + public OggFromWebMWriter(@NonNull final SharpStream source, @NonNull final SharpStream target) { + if (!source.canRead() || !source.canRewind()) { + throw new IllegalArgumentException("source stream must be readable and allows seeking"); + } + if (!target.canWrite() || !target.canRewind()) { + throw new IllegalArgumentException("output stream must be writable and allows seeking"); + } + + this.source = source; + this.output = target; + + this.streamId = (int) System.currentTimeMillis(); + + populateCrc32Table(); + } + + public boolean isDone() { + return done; + } + + public boolean isParsed() { + return parsed; + } + + public WebMTrack[] getTracksFromSource() throws IllegalStateException { + if (!parsed) { + throw new IllegalStateException("source must be parsed first"); + } + + return webm.getAvailableTracks(); + } + + public void parseSource() throws IOException, IllegalStateException { + if (done) { + throw new IllegalStateException("already done"); + } + if (parsed) { + throw new IllegalStateException("already parsed"); + } + + try { + webm = new WebMReader(source); + webm.parse(); + webmSegment = webm.getNextSegment(); + } finally { + parsed = true; + } + } + + public void selectTrack(final int trackIndex) throws IOException { + if (!parsed) { + throw new IllegalStateException("source must be parsed first"); + } + if (done) { + throw new IOException("already done"); + } + if (webmTrack != null) { + throw new IOException("tracks already selected"); + } + + switch (webm.getAvailableTracks()[trackIndex].kind) { + case Audio: + case Video: + break; + default: + throw new UnsupportedOperationException("the track must an audio or video stream"); + } + + try { + webmTrack = webm.selectTrack(trackIndex); + } finally { + parsed = true; + } + } + + @Override + public void close() throws IOException { + done = true; + parsed = true; + + webmTrack = null; + webm = null; + + if (!output.isClosed()) { + output.flush(); + } + + source.close(); + output.close(); + } + + public void build() throws IOException { + final float resolution; + SimpleBlock bloq; + final ByteBuffer header = ByteBuffer.allocate(27 + (255 * 255)); + final ByteBuffer page = ByteBuffer.allocate(64 * 1024); + + header.order(ByteOrder.LITTLE_ENDIAN); + + /* step 1: get the amount of frames per seconds */ + switch (webmTrack.kind) { + case Audio: + resolution = getSampleFrequencyFromTrack(webmTrack.bMetadata); + if (resolution == 0f) { + throw new RuntimeException("cannot get the audio sample rate"); + } + break; + case Video: + // WARNING: untested + if (webmTrack.defaultDuration == 0) { + throw new RuntimeException("missing default frame time"); + } + resolution = 1000f / ((float) webmTrack.defaultDuration + / webmSegment.info.timecodeScale); + break; + default: + throw new RuntimeException("not implemented"); + } + + /* step 2: create packet with code init data */ + if (webmTrack.codecPrivate != null) { + addPacketSegment(webmTrack.codecPrivate.length); + makePacketheader(0x00, header, webmTrack.codecPrivate); + write(header); + output.write(webmTrack.codecPrivate); + } + + /* step 3: create packet with metadata */ + final byte[] buffer = makeMetadata(); + if (buffer != null) { + addPacketSegment(buffer.length); + makePacketheader(0x00, header, buffer); + write(header); + output.write(buffer); + } + + /* step 4: calculate amount of packets */ + while (webmSegment != null) { + bloq = getNextBlock(); + + if (bloq != null && addPacketSegment(bloq)) { + final int pos = page.position(); + //noinspection ResultOfMethodCallIgnored + bloq.data.read(page.array(), pos, bloq.dataSize); + page.position(pos + bloq.dataSize); + continue; + } + + // calculate the current packet duration using the next block + double elapsedNs = webmTrack.codecDelay; + + if (bloq == null) { + packetFlag = FLAG_LAST; // note: if the flag is FLAG_CONTINUED, is changed + elapsedNs += webmBlockLastTimecode; + + if (webmTrack.defaultDuration > 0) { + elapsedNs += webmTrack.defaultDuration; + } else { + // hardcoded way, guess the sample duration + elapsedNs += webmBlockNearDuration; + } + } else { + elapsedNs += bloq.absoluteTimeCodeNs; + } + + // get the sample count in the page + elapsedNs = elapsedNs / TIME_SCALE_NS; + elapsedNs = Math.ceil(elapsedNs * resolution); + + // create header and calculate page checksum + int checksum = makePacketheader((long) elapsedNs, header, null); + checksum = calcCrc32(checksum, page.array(), page.position()); + + header.putInt(HEADER_CHECKSUM_OFFSET, checksum); + + // dump data + write(header); + write(page); + + webmBlock = bloq; + } + } + + private int makePacketheader(final long granPos, @NonNull final ByteBuffer buffer, + final byte[] immediatePage) { + short length = HEADER_SIZE; + + buffer.putInt(0x5367674f); // "OggS" binary string in little-endian + buffer.put((byte) 0x00); // version + buffer.put(packetFlag); // type + + buffer.putLong(granPos); // granulate position + + buffer.putInt(streamId); // bitstream serial number + buffer.putInt(sequenceCount++); // page sequence number + + buffer.putInt(0x00); // page checksum + + buffer.put((byte) segmentTableSize); // segment table + buffer.put(segmentTable, 0, segmentTableSize); // segment size + + length += segmentTableSize; + + clearSegmentTable(); // clear segment table for next header + + int checksumCrc32 = calcCrc32(0x00, buffer.array(), length); + + if (immediatePage != null) { + checksumCrc32 = calcCrc32(checksumCrc32, immediatePage, immediatePage.length); + buffer.putInt(HEADER_CHECKSUM_OFFSET, checksumCrc32); + segmentTableNextTimestamp -= TIME_SCALE_NS; + } + + return checksumCrc32; + } + + @Nullable + private byte[] makeMetadata() { + if ("A_OPUS".equals(webmTrack.codecId)) { + return new byte[]{ + 0x4F, 0x70, 0x75, 0x73, 0x54, 0x61, 0x67, 0x73, // "OpusTags" binary string + 0x00, 0x00, 0x00, 0x00, // writing application string size (not present) + 0x00, 0x00, 0x00, 0x00 // additional tags count (zero means no tags) + }; + } else if ("A_VORBIS".equals(webmTrack.codecId)) { + return new byte[]{ + 0x03, // ¿¿¿??? + 0x76, 0x6f, 0x72, 0x62, 0x69, 0x73, // "vorbis" binary string + 0x00, 0x00, 0x00, 0x00, // writing application string size (not present) + 0x00, 0x00, 0x00, 0x00 // additional tags count (zero means no tags) + }; + } + + // not implemented for the desired codec + return null; + } + + private void write(final ByteBuffer buffer) throws IOException { + output.write(buffer.array(), 0, buffer.position()); + buffer.position(0); + } + + @Nullable + private SimpleBlock getNextBlock() throws IOException { + SimpleBlock res; + + if (webmBlock != null) { + res = webmBlock; + webmBlock = null; + return res; + } + + if (webmSegment == null) { + webmSegment = webm.getNextSegment(); + if (webmSegment == null) { + return null; // no more blocks in the selected track + } + } + + if (webmCluster == null) { + webmCluster = webmSegment.getNextCluster(); + if (webmCluster == null) { + webmSegment = null; + return getNextBlock(); + } + } + + res = webmCluster.getNextSimpleBlock(); + if (res == null) { + webmCluster = null; + return getNextBlock(); + } + + webmBlockNearDuration = res.absoluteTimeCodeNs - webmBlockLastTimecode; + webmBlockLastTimecode = res.absoluteTimeCodeNs; + + return res; + } + + private float getSampleFrequencyFromTrack(final byte[] bMetadata) { + // hardcoded way + final ByteBuffer buffer = ByteBuffer.wrap(bMetadata); + + while (buffer.remaining() >= 6) { + final int id = buffer.getShort() & 0xFFFF; + if (id == 0x0000B584) { + return buffer.getFloat(); + } + } + + return 0.0f; + } + + private void clearSegmentTable() { + segmentTableNextTimestamp += TIME_SCALE_NS; + packetFlag = FLAG_UNSET; + segmentTableSize = 0; + } + + private boolean addPacketSegment(final SimpleBlock block) { + final long timestamp = block.absoluteTimeCodeNs + webmTrack.codecDelay; + + if (timestamp >= segmentTableNextTimestamp) { + return false; + } + + return addPacketSegment(block.dataSize); + } + + private boolean addPacketSegment(final int size) { + if (size > 65025) { + throw new UnsupportedOperationException("page size cannot be larger than 65025"); + } + + int available = (segmentTable.length - segmentTableSize) * 255; + final boolean extra = (size % 255) == 0; + + if (extra) { + // add a zero byte entry in the table + // required to indicate the sample size is multiple of 255 + available -= 255; + } + + // check if possible add the segment, without overflow the table + if (available < size) { + return false; // not enough space on the page + } + + for (int seg = size; seg > 0; seg -= 255) { + segmentTable[segmentTableSize++] = (byte) Math.min(seg, 255); + } + + if (extra) { + segmentTable[segmentTableSize++] = 0x00; + } + + return true; + } + + private void populateCrc32Table() { + for (int i = 0; i < 0x100; i++) { + int crc = i << 24; + for (int j = 0; j < 8; j++) { + final long b = crc >>> 31; + crc <<= 1; + crc ^= (int) (0x100000000L - b) & 0x04c11db7; + } + crc32Table[i] = crc; + } + } + + private int calcCrc32(final int initialCrc, final byte[] buffer, final int size) { + int crc = initialCrc; + for (int i = 0; i < size; i++) { + final int reg = (crc >>> 24) & 0xff; + crc = (crc << 8) ^ crc32Table[reg ^ (buffer[i] & 0xff)]; + } + + return crc; + } +} diff --git a/app/src/main/java/org/schabi/newpipe/util/Constants.java b/app/src/main/java/org/schabi/newpipe/util/Constants.java deleted file mode 100644 index e71dd16f9..000000000 --- a/app/src/main/java/org/schabi/newpipe/util/Constants.java +++ /dev/null @@ -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() { } -} diff --git a/app/src/main/java/org/schabi/newpipe/util/Constants.kt b/app/src/main/java/org/schabi/newpipe/util/Constants.kt new file mode 100644 index 000000000..054aadd70 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/util/Constants.kt @@ -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 diff --git a/app/src/main/java/org/schabi/newpipe/util/ConstantsKt.kt b/app/src/main/java/org/schabi/newpipe/util/ConstantsKt.kt deleted file mode 100644 index 8d24cb04e..000000000 --- a/app/src/main/java/org/schabi/newpipe/util/ConstantsKt.kt +++ /dev/null @@ -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 diff --git a/app/src/main/java/org/schabi/newpipe/util/ExceptionUtils.kt b/app/src/main/java/org/schabi/newpipe/util/ExceptionUtils.kt deleted file mode 100644 index 0addb26fb..000000000 --- a/app/src/main/java/org/schabi/newpipe/util/ExceptionUtils.kt +++ /dev/null @@ -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 - } - } -} diff --git a/app/src/main/java/org/schabi/newpipe/util/ExtractorHelper.java b/app/src/main/java/org/schabi/newpipe/util/ExtractorHelper.java index 650c5ae11..103d9a72b 100644 --- a/app/src/main/java/org/schabi/newpipe/util/ExtractorHelper.java +++ b/app/src/main/java/org/schabi/newpipe/util/ExtractorHelper.java @@ -22,9 +22,16 @@ package org.schabi.newpipe.util; import android.content.Context; import android.content.Intent; import android.os.Handler; +import android.text.method.LinkMovementMethod; import android.util.Log; +import android.view.View; +import android.widget.TextView; 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.R; 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.ListExtractor.InfoItemsPage; import org.schabi.newpipe.extractor.ListInfo; +import org.schabi.newpipe.extractor.MetaInfo; import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipe.extractor.Page; 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.StreamInfoItem; import org.schabi.newpipe.extractor.suggestion.SuggestionExtractor; +import org.schabi.newpipe.ktx.ExceptionUtils; import org.schabi.newpipe.report.ErrorActivity; import org.schabi.newpipe.report.ErrorInfo; 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.Single; +import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty; + public final class ExtractorHelper { private static final String TAG = ExtractorHelper.class.getSimpleName(); 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 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("").append(metaInfo.getTitle()).append("") + .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("

"); + } + + stringBuilder + .append("") + .append(capitalizeIfAllUppercase(metaInfo.getUrlTexts().get(i).trim())) + .append(""); + } + } + + 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(); + } + } } diff --git a/app/src/main/java/org/schabi/newpipe/util/KioskTranslator.java b/app/src/main/java/org/schabi/newpipe/util/KioskTranslator.java index b676a1a88..d2daaf6cc 100644 --- a/app/src/main/java/org/schabi/newpipe/util/KioskTranslator.java +++ b/app/src/main/java/org/schabi/newpipe/util/KioskTranslator.java @@ -5,7 +5,7 @@ import android.content.Context; 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. *

* 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); case "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: return kioskId; } @@ -59,9 +63,12 @@ public final class KioskTranslator { case "Local": return ThemeHelper.resolveResourceIdFromAttr(c, R.attr.ic_kiosk_local); case "Recently added": + case "recent": return ThemeHelper.resolveResourceIdFromAttr(c, R.attr.ic_kiosk_recent); case "Most liked": return ThemeHelper.resolveResourceIdFromAttr(c, R.attr.ic_thumb_up); + case "live": + return ThemeHelper.resolveResourceIdFromAttr(c, R.attr.ic_live_tv); default: return 0; } diff --git a/app/src/main/java/org/schabi/newpipe/util/KoreUtil.java b/app/src/main/java/org/schabi/newpipe/util/KoreUtil.java index 7d37b25c4..de6f3fa9a 100644 --- a/app/src/main/java/org/schabi/newpipe/util/KoreUtil.java +++ b/app/src/main/java/org/schabi/newpipe/util/KoreUtil.java @@ -1,9 +1,9 @@ package org.schabi.newpipe.util; - import android.content.Context; import androidx.appcompat.app.AlertDialog; +import androidx.preference.PreferenceManager; import org.schabi.newpipe.R; import org.schabi.newpipe.extractor.ServiceList; @@ -16,6 +16,12 @@ public final class KoreUtil { || 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) { final AlertDialog.Builder builder = new AlertDialog.Builder(context); builder.setMessage(R.string.kore_not_found) diff --git a/app/src/main/java/org/schabi/newpipe/util/ListHelper.java b/app/src/main/java/org/schabi/newpipe/util/ListHelper.java index 0c840f8c3..5f8fb5898 100644 --- a/app/src/main/java/org/schabi/newpipe/util/ListHelper.java +++ b/app/src/main/java/org/schabi/newpipe/util/ListHelper.java @@ -141,7 +141,7 @@ public final class ListHelper { final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context); - // Load the prefered resolution otherwise the best available + // Load the preferred resolution otherwise the best available String resolution = preferences != null ? preferences.getString(context.getString(key), context.getString(value)) : context.getString(R.string.best_resolution_key); @@ -161,7 +161,7 @@ public final class ListHelper { * * @param defaultResolution the default resolution to look for * @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 * @return index of the default resolution&format */ @@ -351,7 +351,7 @@ public final class ListHelper { * @param targetResolution the resolution to look for * @param targetFormat the format to look for * @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, final List videoStreams) { @@ -413,7 +413,7 @@ public final class ListHelper { * @param context Android app context * @param defaultResolution the default resolution * @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, final String defaultResolution, diff --git a/app/src/main/java/org/schabi/newpipe/util/Localization.java b/app/src/main/java/org/schabi/newpipe/util/Localization.java index afe2c0467..c235d7d67 100644 --- a/app/src/main/java/org/schabi/newpipe/util/Localization.java +++ b/app/src/main/java/org/schabi/newpipe/util/Localization.java @@ -9,18 +9,10 @@ import android.icu.text.CompactDecimalFormat; import android.os.Build; import android.text.TextUtils; import android.util.DisplayMetrics; - import androidx.annotation.NonNull; import androidx.annotation.PluralsRes; import androidx.annotation.StringRes; 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.RoundingMode; import java.text.NumberFormat; @@ -30,9 +22,14 @@ import java.time.format.DateTimeFormatter; import java.time.format.FormatStyle; import java.util.Arrays; import java.util.Calendar; -import java.util.GregorianCalendar; import java.util.List; 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 { - private static final String DOT_SEPARATOR = " • "; + public static final String DOT_SEPARATOR = " • "; private static PrettyTime prettyTime; private Localization() { } - public static void init(final Context context) { - initPrettyTime(context); - } - @NonNull public static String concatenateStrings(final String... strings) { return concatenateStrings(Arrays.asList(strings)); @@ -307,14 +300,18 @@ public final class Localization { // Pretty Time //////////////////////////////////////////////////////////////////////////*/ - private static void initPrettyTime(final Context context) { - prettyTime = new PrettyTime(getAppLocale(context)); + public static void initPrettyTime(final PrettyTime time) { + prettyTime = time; // Do not use decades as YouTube doesn't either. prettyTime.removeUnit(Decade.class); } + public static PrettyTime resolvePrettyTime(final Context context) { + return new PrettyTime(getAppLocale(context)); + } + 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) { @@ -354,4 +351,19 @@ public final class Localization { private static double round(final double value, final int places) { 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 list, final String toFind) { + for (final String i : list) { + if (i.equalsIgnoreCase(toFind)) { + return true; + } + } + return false; + } } diff --git a/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java b/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java index b45a1e7b9..c90bb3025 100644 --- a/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java +++ b/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java @@ -46,10 +46,9 @@ import org.schabi.newpipe.local.history.StatisticsPlaylistFragment; import org.schabi.newpipe.local.playlist.LocalPlaylistFragment; import org.schabi.newpipe.local.subscription.SubscriptionFragment; import org.schabi.newpipe.local.subscription.SubscriptionsImportFragment; -import org.schabi.newpipe.player.BackgroundPlayerActivity; -import org.schabi.newpipe.player.BasePlayer; +import org.schabi.newpipe.player.PlayQueueActivity; +import org.schabi.newpipe.player.Player; 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.PlayerHolder; import org.schabi.newpipe.player.playqueue.PlayQueue; @@ -78,11 +77,11 @@ public final class NavigationHelper { if (playQueue != null) { final String cacheKey = SerializedCache.getInstance().put(playQueue, PlayQueue.class); 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(VideoPlayer.PLAYER_TYPE, VideoPlayer.PLAYER_TYPE_VIDEO); + intent.putExtra(Player.RESUME_PLAYBACK, resumePlayback); + intent.putExtra(Player.PLAYER_TYPE, MainPlayer.PlayerType.VIDEO.ordinal()); return intent; } @@ -94,7 +93,7 @@ public final class NavigationHelper { final boolean resumePlayback, final boolean playWhenReady) { return getPlayerIntent(context, targetClazz, playQueue, resumePlayback) - .putExtra(BasePlayer.PLAY_WHEN_READY, playWhenReady); + .putExtra(Player.PLAY_WHEN_READY, playWhenReady); } @NonNull @@ -104,8 +103,8 @@ public final class NavigationHelper { final boolean selectOnAppend, final boolean resumePlayback) { return getPlayerIntent(context, targetClazz, playQueue, resumePlayback) - .putExtra(BasePlayer.APPEND_ONLY, true) - .putExtra(BasePlayer.SELECT_ON_APPEND, selectOnAppend); + .putExtra(Player.APPEND_ONLY, true) + .putExtra(Player.SELECT_ON_APPEND, selectOnAppend); } 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(); 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); } @@ -145,7 +144,7 @@ public final class NavigationHelper { Toast.makeText(context, R.string.background_player_playing_toast, Toast.LENGTH_SHORT) .show(); 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); } @@ -162,7 +161,7 @@ public final class NavigationHelper { final Intent intent = getPlayerEnqueueIntent( 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); } @@ -182,7 +181,7 @@ public final class NavigationHelper { Toast.makeText(context, R.string.enqueued, Toast.LENGTH_SHORT).show(); final Intent intent = getPlayerEnqueueIntent( 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); } @@ -198,7 +197,7 @@ public final class NavigationHelper { Toast.makeText(context, R.string.enqueued, Toast.LENGTH_SHORT).show(); final Intent intent = getPlayerEnqueueIntent( 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); } @@ -493,7 +492,7 @@ public final class NavigationHelper { if (playQueue != null) { final String cacheKey = SerializedCache.getInstance().put(playQueue, PlayQueue.class); if (cacheKey != null) { - intent.putExtra(VideoPlayer.PLAY_QUEUE_KEY, cacheKey); + intent.putExtra(Player.PLAY_QUEUE_KEY, cacheKey); } } context.startActivity(intent); @@ -531,7 +530,7 @@ public final class NavigationHelper { } 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) { intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); } diff --git a/app/src/main/java/org/schabi/newpipe/util/ServiceHelper.java b/app/src/main/java/org/schabi/newpipe/util/ServiceHelper.java index 8f3423b22..a6756991c 100644 --- a/app/src/main/java/org/schabi/newpipe/util/ServiceHelper.java +++ b/app/src/main/java/org/schabi/newpipe/util/ServiceHelper.java @@ -48,6 +48,7 @@ public final class ServiceHelper { case "all": return c.getString(R.string.all); case "videos": + case "sepia_videos": case "music_videos": return c.getString(R.string.videos_string); case "channels": diff --git a/app/src/main/java/org/schabi/newpipe/util/StreamDialogEntry.java b/app/src/main/java/org/schabi/newpipe/util/StreamDialogEntry.java index 34ff637ad..153ba7cf4 100644 --- a/app/src/main/java/org/schabi/newpipe/util/StreamDialogEntry.java +++ b/app/src/main/java/org/schabi/newpipe/util/StreamDialogEntry.java @@ -1,6 +1,7 @@ package org.schabi.newpipe.util; import android.content.Context; +import android.net.Uri; 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) -> ShareUtils.shareUrl(fragment.getContext(), item.getName(), item.getUrl())); diff --git a/app/src/main/java/org/schabi/newpipe/util/ZipHelper.java b/app/src/main/java/org/schabi/newpipe/util/ZipHelper.java index f9a950d2b..e2b766bb0 100644 --- a/app/src/main/java/org/schabi/newpipe/util/ZipHelper.java +++ b/app/src/main/java/org/schabi/newpipe/util/ZipHelper.java @@ -4,7 +4,9 @@ import java.io.BufferedInputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; +import java.io.IOException; import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; import java.util.zip.ZipInputStream; import java.util.zip.ZipOutputStream; @@ -99,4 +101,12 @@ public final class ZipHelper { return found; } } + + public static boolean isValidZipFile(final String filePath) { + try (ZipFile ignored = new ZipFile(filePath)) { + return true; + } catch (final IOException ioe) { + return false; + } + } } diff --git a/app/src/main/java/us/shandian/giga/get/DownloadMission.java b/app/src/main/java/us/shandian/giga/get/DownloadMission.java index d7c586083..2b3faa3e0 100644 --- a/app/src/main/java/us/shandian/giga/get/DownloadMission.java +++ b/app/src/main/java/us/shandian/giga/get/DownloadMission.java @@ -22,6 +22,7 @@ import java.net.SocketTimeoutException; import java.net.URL; import java.net.UnknownHostException; import java.nio.channels.ClosedByInterruptException; +import java.util.Objects; import javax.net.ssl.SSLException; @@ -154,8 +155,8 @@ public class DownloadMission extends Mission { public transient Thread init = null; public DownloadMission(String[] urls, StoredFileHelper storage, char kind, Postprocessing psInstance) { - if (urls == null) throw new NullPointerException("urls is null"); - if (urls.length < 1) throw new IllegalArgumentException("urls is empty"); + if (Objects.requireNonNull(urls).length < 1) + throw new IllegalArgumentException("urls array is empty"); this.urls = urls; this.kind = kind; this.offsets = new long[urls.length]; diff --git a/app/src/main/java/us/shandian/giga/get/DownloadMissionRecover.java b/app/src/main/java/us/shandian/giga/get/DownloadMissionRecover.java index 0e9b9ff00..5b2858aa2 100644 --- a/app/src/main/java/us/shandian/giga/get/DownloadMissionRecover.java +++ b/app/src/main/java/us/shandian/giga/get/DownloadMissionRecover.java @@ -1,313 +1,313 @@ -package us.shandian.giga.get; - -import android.util.Log; - -import org.schabi.newpipe.extractor.NewPipe; -import org.schabi.newpipe.extractor.StreamingService; -import org.schabi.newpipe.extractor.exceptions.ExtractionException; -import org.schabi.newpipe.extractor.stream.AudioStream; -import org.schabi.newpipe.extractor.stream.StreamExtractor; -import org.schabi.newpipe.extractor.stream.SubtitlesStream; -import org.schabi.newpipe.extractor.stream.VideoStream; - -import java.io.IOException; -import java.io.InterruptedIOException; -import java.net.HttpURLConnection; -import java.nio.channels.ClosedByInterruptException; -import java.util.List; - -import us.shandian.giga.get.DownloadMission.HttpError; - -import static us.shandian.giga.get.DownloadMission.ERROR_RESOURCE_GONE; - -public class DownloadMissionRecover extends Thread { - private static final String TAG = "DownloadMissionRecover"; - static final int mID = -3; - - private final DownloadMission mMission; - private final boolean mNotInitialized; - - private final int mErrCode; - - private HttpURLConnection mConn; - private MissionRecoveryInfo mRecovery; - private StreamExtractor mExtractor; - - DownloadMissionRecover(DownloadMission mission, int errCode) { - mMission = mission; - mNotInitialized = mission.blocks == null && mission.current == 0; - mErrCode = errCode; - } - - @Override - public void run() { - if (mMission.source == null) { - mMission.notifyError(mErrCode, null); - return; - } - - Exception err = null; - int attempt = 0; - - while (attempt++ < mMission.maxRetry) { - try { - tryRecover(); - return; - } catch (InterruptedIOException | ClosedByInterruptException e) { - return; - } catch (Exception e) { - if (!mMission.running || super.isInterrupted()) return; - err = e; - } - } - - // give up - mMission.notifyError(mErrCode, err); - } - - private void tryRecover() throws ExtractionException, IOException, HttpError { - if (mExtractor == null) { - try { - StreamingService svr = NewPipe.getServiceByUrl(mMission.source); - mExtractor = svr.getStreamExtractor(mMission.source); - mExtractor.fetchPage(); - } catch (ExtractionException e) { - mExtractor = null; - throw e; - } - } - - // maybe the following check is redundant - if (!mMission.running || super.isInterrupted()) return; - - if (!mNotInitialized) { - // set the current download url to null in case if the recovery - // process is canceled. Next time start() method is called the - // recovery will be executed, saving time - mMission.urls[mMission.current] = null; - - mRecovery = mMission.recoveryInfo[mMission.current]; - resolveStream(); - return; - } - - Log.w(TAG, "mission is not fully initialized, this will take a while"); - - try { - for (; mMission.current < mMission.urls.length; mMission.current++) { - mRecovery = mMission.recoveryInfo[mMission.current]; - - if (test()) continue; - if (!mMission.running) return; - - resolveStream(); - if (!mMission.running) return; - - // before continue, check if the current stream was resolved - if (mMission.urls[mMission.current] == null) { - break; - } - } - } finally { - mMission.current = 0; - } - - mMission.writeThisToFile(); - - if (!mMission.running || super.isInterrupted()) return; - - mMission.running = false; - mMission.start(); - } - - private void resolveStream() throws IOException, ExtractionException, HttpError { - // FIXME: this getErrorMessage() always returns "video is unavailable" - /*if (mExtractor.getErrorMessage() != null) { - mMission.notifyError(mErrCode, new ExtractionException(mExtractor.getErrorMessage())); - return; - }*/ - - String url = null; - - switch (mRecovery.getKind()) { - case 'a': - for (AudioStream audio : mExtractor.getAudioStreams()) { - if (audio.average_bitrate == mRecovery.getDesiredBitrate() && audio.getFormat() == mRecovery.getFormat()) { - url = audio.getUrl(); - break; - } - } - break; - case 'v': - List videoStreams; - if (mRecovery.isDesired2()) - videoStreams = mExtractor.getVideoOnlyStreams(); - else - videoStreams = mExtractor.getVideoStreams(); - for (VideoStream video : videoStreams) { - if (video.resolution.equals(mRecovery.getDesired()) && video.getFormat() == mRecovery.getFormat()) { - url = video.getUrl(); - break; - } - } - break; - case 's': - for (SubtitlesStream subtitles : mExtractor.getSubtitles(mRecovery.getFormat())) { - String tag = subtitles.getLanguageTag(); - if (tag.equals(mRecovery.getDesired()) && subtitles.isAutoGenerated() == mRecovery.isDesired2()) { - url = subtitles.getUrl(); - break; - } - } - break; - default: - throw new RuntimeException("Unknown stream type"); - } - - resolve(url); - } - - private void resolve(String url) throws IOException, HttpError { - if (mRecovery.getValidateCondition() == null) { - Log.w(TAG, "validation condition not defined, the resource can be stale"); - } - - if (mMission.unknownLength || mRecovery.getValidateCondition() == null) { - recover(url, false); - return; - } - - /////////////////////////////////////////////////////////////////////// - ////// Validate the http resource doing a range request - ///////////////////// - try { - mConn = mMission.openConnection(url, true, mMission.length - 10, mMission.length); - mConn.setRequestProperty("If-Range", mRecovery.getValidateCondition()); - mMission.establishConnection(mID, mConn); - - int code = mConn.getResponseCode(); - - switch (code) { - case 200: - case 413: - // stale - recover(url, true); - return; - case 206: - // in case of validation using the Last-Modified date, check the resource length - long[] contentRange = parseContentRange(mConn.getHeaderField("Content-Range")); - boolean lengthMismatch = contentRange[2] != -1 && contentRange[2] != mMission.length; - - recover(url, lengthMismatch); - return; - } - - throw new HttpError(code); - } finally { - disconnect(); - } - } - - private void recover(String url, boolean stale) { - Log.i(TAG, - String.format("recover() name=%s isStale=%s url=%s", mMission.storage.getName(), stale, url) - ); - - mMission.urls[mMission.current] = url; - - if (url == null) { - mMission.urls = new String[0]; - mMission.notifyError(ERROR_RESOURCE_GONE, null); - return; - } - - if (mNotInitialized) return; - - if (stale) { - mMission.resetState(false, false, DownloadMission.ERROR_NOTHING); - } - - mMission.writeThisToFile(); - - if (!mMission.running || super.isInterrupted()) return; - - mMission.running = false; - mMission.start(); - } - - private long[] parseContentRange(String value) { - long[] range = new long[3]; - - if (value == null) { - // this never should happen - return range; - } - - try { - value = value.trim(); - - if (!value.startsWith("bytes")) { - return range;// unknown range type - } - - int space = value.lastIndexOf(' ') + 1; - int dash = value.indexOf('-', space) + 1; - int bar = value.indexOf('/', dash); - - // start - range[0] = Long.parseLong(value.substring(space, dash - 1)); - - // end - range[1] = Long.parseLong(value.substring(dash, bar)); - - // resource length - value = value.substring(bar + 1); - if (value.equals("*")) { - range[2] = -1;// unknown length received from the server but should be valid - } else { - range[2] = Long.parseLong(value); - } - } catch (Exception e) { - // nothing to do - } - - return range; - } - - private boolean test() { - if (mMission.urls[mMission.current] == null) return false; - - try { - mConn = mMission.openConnection(mMission.urls[mMission.current], true, -1, -1); - mMission.establishConnection(mID, mConn); - - if (mConn.getResponseCode() == 200) return true; - } catch (Exception e) { - // nothing to do - } finally { - disconnect(); - } - - return false; - } - - private void disconnect() { - try { - try { - mConn.getInputStream().close(); - } finally { - mConn.disconnect(); - } - } catch (Exception e) { - // nothing to do - } finally { - mConn = null; - } - } - - @Override - public void interrupt() { - super.interrupt(); - if (mConn != null) disconnect(); - } -} +package us.shandian.giga.get; + +import android.util.Log; + +import org.schabi.newpipe.extractor.NewPipe; +import org.schabi.newpipe.extractor.StreamingService; +import org.schabi.newpipe.extractor.exceptions.ExtractionException; +import org.schabi.newpipe.extractor.stream.AudioStream; +import org.schabi.newpipe.extractor.stream.StreamExtractor; +import org.schabi.newpipe.extractor.stream.SubtitlesStream; +import org.schabi.newpipe.extractor.stream.VideoStream; + +import java.io.IOException; +import java.io.InterruptedIOException; +import java.net.HttpURLConnection; +import java.nio.channels.ClosedByInterruptException; +import java.util.List; + +import us.shandian.giga.get.DownloadMission.HttpError; + +import static us.shandian.giga.get.DownloadMission.ERROR_RESOURCE_GONE; + +public class DownloadMissionRecover extends Thread { + private static final String TAG = "DownloadMissionRecover"; + static final int mID = -3; + + private final DownloadMission mMission; + private final boolean mNotInitialized; + + private final int mErrCode; + + private HttpURLConnection mConn; + private MissionRecoveryInfo mRecovery; + private StreamExtractor mExtractor; + + DownloadMissionRecover(DownloadMission mission, int errCode) { + mMission = mission; + mNotInitialized = mission.blocks == null && mission.current == 0; + mErrCode = errCode; + } + + @Override + public void run() { + if (mMission.source == null) { + mMission.notifyError(mErrCode, null); + return; + } + + Exception err = null; + int attempt = 0; + + while (attempt++ < mMission.maxRetry) { + try { + tryRecover(); + return; + } catch (InterruptedIOException | ClosedByInterruptException e) { + return; + } catch (Exception e) { + if (!mMission.running || super.isInterrupted()) return; + err = e; + } + } + + // give up + mMission.notifyError(mErrCode, err); + } + + private void tryRecover() throws ExtractionException, IOException, HttpError { + if (mExtractor == null) { + try { + StreamingService svr = NewPipe.getServiceByUrl(mMission.source); + mExtractor = svr.getStreamExtractor(mMission.source); + mExtractor.fetchPage(); + } catch (ExtractionException e) { + mExtractor = null; + throw e; + } + } + + // maybe the following check is redundant + if (!mMission.running || super.isInterrupted()) return; + + if (!mNotInitialized) { + // set the current download url to null in case if the recovery + // process is canceled. Next time start() method is called the + // recovery will be executed, saving time + mMission.urls[mMission.current] = null; + + mRecovery = mMission.recoveryInfo[mMission.current]; + resolveStream(); + return; + } + + Log.w(TAG, "mission is not fully initialized, this will take a while"); + + try { + for (; mMission.current < mMission.urls.length; mMission.current++) { + mRecovery = mMission.recoveryInfo[mMission.current]; + + if (test()) continue; + if (!mMission.running) return; + + resolveStream(); + if (!mMission.running) return; + + // before continue, check if the current stream was resolved + if (mMission.urls[mMission.current] == null) { + break; + } + } + } finally { + mMission.current = 0; + } + + mMission.writeThisToFile(); + + if (!mMission.running || super.isInterrupted()) return; + + mMission.running = false; + mMission.start(); + } + + private void resolveStream() throws IOException, ExtractionException, HttpError { + // FIXME: this getErrorMessage() always returns "video is unavailable" + /*if (mExtractor.getErrorMessage() != null) { + mMission.notifyError(mErrCode, new ExtractionException(mExtractor.getErrorMessage())); + return; + }*/ + + String url = null; + + switch (mRecovery.getKind()) { + case 'a': + for (AudioStream audio : mExtractor.getAudioStreams()) { + if (audio.average_bitrate == mRecovery.getDesiredBitrate() && audio.getFormat() == mRecovery.getFormat()) { + url = audio.getUrl(); + break; + } + } + break; + case 'v': + List videoStreams; + if (mRecovery.isDesired2()) + videoStreams = mExtractor.getVideoOnlyStreams(); + else + videoStreams = mExtractor.getVideoStreams(); + for (VideoStream video : videoStreams) { + if (video.resolution.equals(mRecovery.getDesired()) && video.getFormat() == mRecovery.getFormat()) { + url = video.getUrl(); + break; + } + } + break; + case 's': + for (SubtitlesStream subtitles : mExtractor.getSubtitles(mRecovery.getFormat())) { + String tag = subtitles.getLanguageTag(); + if (tag.equals(mRecovery.getDesired()) && subtitles.isAutoGenerated() == mRecovery.isDesired2()) { + url = subtitles.getUrl(); + break; + } + } + break; + default: + throw new RuntimeException("Unknown stream type"); + } + + resolve(url); + } + + private void resolve(String url) throws IOException, HttpError { + if (mRecovery.getValidateCondition() == null) { + Log.w(TAG, "validation condition not defined, the resource can be stale"); + } + + if (mMission.unknownLength || mRecovery.getValidateCondition() == null) { + recover(url, false); + return; + } + + /////////////////////////////////////////////////////////////////////// + ////// Validate the http resource doing a range request + ///////////////////// + try { + mConn = mMission.openConnection(url, true, mMission.length - 10, mMission.length); + mConn.setRequestProperty("If-Range", mRecovery.getValidateCondition()); + mMission.establishConnection(mID, mConn); + + int code = mConn.getResponseCode(); + + switch (code) { + case 200: + case 413: + // stale + recover(url, true); + return; + case 206: + // in case of validation using the Last-Modified date, check the resource length + long[] contentRange = parseContentRange(mConn.getHeaderField("Content-Range")); + boolean lengthMismatch = contentRange[2] != -1 && contentRange[2] != mMission.length; + + recover(url, lengthMismatch); + return; + } + + throw new HttpError(code); + } finally { + disconnect(); + } + } + + private void recover(String url, boolean stale) { + Log.i(TAG, + String.format("recover() name=%s isStale=%s url=%s", mMission.storage.getName(), stale, url) + ); + + mMission.urls[mMission.current] = url; + + if (url == null) { + mMission.urls = new String[0]; + mMission.notifyError(ERROR_RESOURCE_GONE, null); + return; + } + + if (mNotInitialized) return; + + if (stale) { + mMission.resetState(false, false, DownloadMission.ERROR_NOTHING); + } + + mMission.writeThisToFile(); + + if (!mMission.running || super.isInterrupted()) return; + + mMission.running = false; + mMission.start(); + } + + private long[] parseContentRange(String value) { + long[] range = new long[3]; + + if (value == null) { + // this never should happen + return range; + } + + try { + value = value.trim(); + + if (!value.startsWith("bytes")) { + return range;// unknown range type + } + + int space = value.lastIndexOf(' ') + 1; + int dash = value.indexOf('-', space) + 1; + int bar = value.indexOf('/', dash); + + // start + range[0] = Long.parseLong(value.substring(space, dash - 1)); + + // end + range[1] = Long.parseLong(value.substring(dash, bar)); + + // resource length + value = value.substring(bar + 1); + if (value.equals("*")) { + range[2] = -1;// unknown length received from the server but should be valid + } else { + range[2] = Long.parseLong(value); + } + } catch (Exception e) { + // nothing to do + } + + return range; + } + + private boolean test() { + if (mMission.urls[mMission.current] == null) return false; + + try { + mConn = mMission.openConnection(mMission.urls[mMission.current], true, -1, -1); + mMission.establishConnection(mID, mConn); + + if (mConn.getResponseCode() == 200) return true; + } catch (Exception e) { + // nothing to do + } finally { + disconnect(); + } + + return false; + } + + private void disconnect() { + try { + try { + mConn.getInputStream().close(); + } finally { + mConn.disconnect(); + } + } catch (Exception e) { + // nothing to do + } finally { + mConn = null; + } + } + + @Override + public void interrupt() { + super.interrupt(); + if (mConn != null) disconnect(); + } +} diff --git a/app/src/main/java/us/shandian/giga/get/DownloadRunnable.java b/app/src/main/java/us/shandian/giga/get/DownloadRunnable.java index 7fb12d088..6f504cea3 100644 --- a/app/src/main/java/us/shandian/giga/get/DownloadRunnable.java +++ b/app/src/main/java/us/shandian/giga/get/DownloadRunnable.java @@ -8,6 +8,7 @@ import java.io.IOException; import java.io.InputStream; import java.net.HttpURLConnection; import java.nio.channels.ClosedByInterruptException; +import java.util.Objects; import us.shandian.giga.get.DownloadMission.Block; import us.shandian.giga.get.DownloadMission.HttpError; @@ -29,8 +30,7 @@ public class DownloadRunnable extends Thread { private HttpURLConnection mConn; DownloadRunnable(DownloadMission mission, int id) { - if (mission == null) throw new NullPointerException("mission is null"); - mMission = mission; + mMission = Objects.requireNonNull(mission); mId = id; } diff --git a/app/src/main/java/us/shandian/giga/get/DownloadRunnableFallback.java b/app/src/main/java/us/shandian/giga/get/DownloadRunnableFallback.java index 9cb40cb32..eed5db463 100644 --- a/app/src/main/java/us/shandian/giga/get/DownloadRunnableFallback.java +++ b/app/src/main/java/us/shandian/giga/get/DownloadRunnableFallback.java @@ -21,7 +21,7 @@ import static us.shandian.giga.get.DownloadMission.ERROR_HTTP_FORBIDDEN; * Single-threaded fallback mode */ public class DownloadRunnableFallback extends Thread { - private static final String TAG = "DownloadRunnableFallbac"; + private static final String TAG = "DownloadRunnableFallback"; private final DownloadMission mMission; diff --git a/app/src/main/java/us/shandian/giga/get/FinishedMission.java b/app/src/main/java/us/shandian/giga/get/FinishedMission.java index 6bc5423b8..29f3c6296 100644 --- a/app/src/main/java/us/shandian/giga/get/FinishedMission.java +++ b/app/src/main/java/us/shandian/giga/get/FinishedMission.java @@ -1,18 +1,18 @@ -package us.shandian.giga.get; - -import androidx.annotation.NonNull; - -public class FinishedMission extends Mission { - - public FinishedMission() { - } - - public FinishedMission(@NonNull DownloadMission mission) { - source = mission.source; - length = mission.length; - timestamp = mission.timestamp; - kind = mission.kind; - storage = mission.storage; - } - -} +package us.shandian.giga.get; + +import androidx.annotation.NonNull; + +public class FinishedMission extends Mission { + + public FinishedMission() { + } + + public FinishedMission(@NonNull DownloadMission mission) { + source = mission.source; + length = mission.length; + timestamp = mission.timestamp; + kind = mission.kind; + storage = mission.storage; + } + +} diff --git a/app/src/main/java/us/shandian/giga/get/Mission.java b/app/src/main/java/us/shandian/giga/get/Mission.java index ff1319884..ecb0eaebd 100644 --- a/app/src/main/java/us/shandian/giga/get/Mission.java +++ b/app/src/main/java/us/shandian/giga/get/Mission.java @@ -1,64 +1,64 @@ -package us.shandian.giga.get; - -import androidx.annotation.NonNull; - -import java.io.Serializable; -import java.util.Calendar; - -import us.shandian.giga.io.StoredFileHelper; - -public abstract class Mission implements Serializable { - private static final long serialVersionUID = 1L;// last bump: 27 march 2019 - - /** - * Source url of the resource - */ - public String source; - - /** - * Length of the current resource - */ - public long length; - - /** - * creation timestamp (and maybe unique identifier) - */ - public long timestamp; - - /** - * pre-defined content type - */ - public char kind; - - /** - * The downloaded file - */ - public StoredFileHelper storage; - - public long getTimestamp() { - return timestamp; - } - - /** - * Delete the downloaded file - * - * @return {@code true] if and only if the file is successfully deleted, otherwise, {@code false} - */ - public boolean delete() { - if (storage != null) return storage.delete(); - return true; - } - - /** - * Indicate if this mission is deleted whatever is stored - */ - public transient boolean deleted = false; - - @NonNull - @Override - public String toString() { - Calendar calendar = Calendar.getInstance(); - calendar.setTimeInMillis(timestamp); - return "[" + calendar.getTime().toString() + "] " + (storage.isInvalid() ? storage.getName() : storage.getUri()); - } -} +package us.shandian.giga.get; + +import androidx.annotation.NonNull; + +import java.io.Serializable; +import java.util.Calendar; + +import us.shandian.giga.io.StoredFileHelper; + +public abstract class Mission implements Serializable { + private static final long serialVersionUID = 1L;// last bump: 27 march 2019 + + /** + * Source url of the resource + */ + public String source; + + /** + * Length of the current resource + */ + public long length; + + /** + * creation timestamp (and maybe unique identifier) + */ + public long timestamp; + + /** + * pre-defined content type + */ + public char kind; + + /** + * The downloaded file + */ + public StoredFileHelper storage; + + public long getTimestamp() { + return timestamp; + } + + /** + * Delete the downloaded file + * + * @return {@code true] if and only if the file is successfully deleted, otherwise, {@code false} + */ + public boolean delete() { + if (storage != null) return storage.delete(); + return true; + } + + /** + * Indicate if this mission is deleted whatever is stored + */ + public transient boolean deleted = false; + + @NonNull + @Override + public String toString() { + Calendar calendar = Calendar.getInstance(); + calendar.setTimeInMillis(timestamp); + return "[" + calendar.getTime().toString() + "] " + (storage.isInvalid() ? storage.getName() : storage.getUri()); + } +} diff --git a/app/src/main/java/us/shandian/giga/get/sqlite/FinishedMissionStore.java b/app/src/main/java/us/shandian/giga/get/sqlite/FinishedMissionStore.java index 1d1dca0df..15c45c6fd 100644 --- a/app/src/main/java/us/shandian/giga/get/sqlite/FinishedMissionStore.java +++ b/app/src/main/java/us/shandian/giga/get/sqlite/FinishedMissionStore.java @@ -12,6 +12,7 @@ import androidx.annotation.NonNull; import java.io.File; import java.util.ArrayList; +import java.util.Objects; import us.shandian.giga.get.DownloadMission; import us.shandian.giga.get.FinishedMission; @@ -140,9 +141,7 @@ public class FinishedMissionStore extends SQLiteOpenHelper { } private FinishedMission getMissionFromCursor(Cursor cursor) { - if (cursor == null) throw new NullPointerException("cursor is null"); - - String kind = cursor.getString(cursor.getColumnIndex(KEY_KIND)); + String kind = Objects.requireNonNull(cursor).getString(cursor.getColumnIndex(KEY_KIND)); if (kind == null || kind.isEmpty()) kind = "?"; String path = cursor.getString(cursor.getColumnIndexOrThrow(KEY_PATH)); @@ -186,15 +185,13 @@ public class FinishedMissionStore extends SQLiteOpenHelper { } public void addFinishedMission(DownloadMission downloadMission) { - if (downloadMission == null) throw new NullPointerException("downloadMission is null"); + ContentValues values = getValuesOfMission(Objects.requireNonNull(downloadMission)); SQLiteDatabase database = getWritableDatabase(); - ContentValues values = getValuesOfMission(downloadMission); database.insert(FINISHED_TABLE_NAME, null, values); } public void deleteMission(Mission mission) { - if (mission == null) throw new NullPointerException("mission is null"); - String ts = String.valueOf(mission.timestamp); + String ts = String.valueOf(Objects.requireNonNull(mission).timestamp); SQLiteDatabase database = getWritableDatabase(); @@ -212,9 +209,8 @@ public class FinishedMissionStore extends SQLiteOpenHelper { } public void updateMission(Mission mission) { - if (mission == null) throw new NullPointerException("mission is null"); + ContentValues values = getValuesOfMission(Objects.requireNonNull(mission)); SQLiteDatabase database = getWritableDatabase(); - ContentValues values = getValuesOfMission(mission); String ts = String.valueOf(mission.timestamp); int rowsAffected; diff --git a/app/src/main/java/us/shandian/giga/io/CircularFileWriter.java b/app/src/main/java/us/shandian/giga/io/CircularFileWriter.java index 4d62ab200..dbceeb091 100644 --- a/app/src/main/java/us/shandian/giga/io/CircularFileWriter.java +++ b/app/src/main/java/us/shandian/giga/io/CircularFileWriter.java @@ -7,6 +7,7 @@ import org.schabi.newpipe.streams.io.SharpStream; import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; +import java.util.Objects; public class CircularFileWriter extends SharpStream { @@ -27,9 +28,7 @@ public class CircularFileWriter extends SharpStream { private BufferedFile aux; public CircularFileWriter(SharpStream target, File temp, OffsetChecker checker) throws IOException { - if (checker == null) { - throw new NullPointerException("checker is null"); - } + Objects.requireNonNull(checker); if (!temp.exists()) { if (!temp.createNewFile()) { diff --git a/app/src/main/java/us/shandian/giga/io/ProgressReport.java b/app/src/main/java/us/shandian/giga/io/ProgressReport.java index 14ae9ded9..e382747f6 100644 --- a/app/src/main/java/us/shandian/giga/io/ProgressReport.java +++ b/app/src/main/java/us/shandian/giga/io/ProgressReport.java @@ -1,11 +1,11 @@ -package us.shandian.giga.io; - -public interface ProgressReport { - - /** - * Report the size of the new file - * - * @param progress the new size - */ - void report(long progress); +package us.shandian.giga.io; + +public interface ProgressReport { + + /** + * Report the size of the new file + * + * @param progress the new size + */ + void report(long progress); } \ No newline at end of file diff --git a/app/src/main/java/us/shandian/giga/postprocessing/OggFromWebmDemuxer.java b/app/src/main/java/us/shandian/giga/postprocessing/OggFromWebmDemuxer.java index 04958c495..dc46ced5d 100644 --- a/app/src/main/java/us/shandian/giga/postprocessing/OggFromWebmDemuxer.java +++ b/app/src/main/java/us/shandian/giga/postprocessing/OggFromWebmDemuxer.java @@ -1,44 +1,44 @@ -package us.shandian.giga.postprocessing; - -import androidx.annotation.NonNull; - -import org.schabi.newpipe.streams.OggFromWebMWriter; -import org.schabi.newpipe.streams.io.SharpStream; - -import java.io.IOException; -import java.nio.ByteBuffer; - -class OggFromWebmDemuxer extends Postprocessing { - - OggFromWebmDemuxer() { - super(true, true, ALGORITHM_OGG_FROM_WEBM_DEMUXER); - } - - @Override - boolean test(SharpStream... sources) throws IOException { - ByteBuffer buffer = ByteBuffer.allocate(4); - sources[0].read(buffer.array()); - - // youtube uses WebM as container, but the file extension (format suffix) is "*.opus" - // check if the file is a webm/mkv file before proceed - - switch (buffer.getInt()) { - case 0x1a45dfa3: - return true;// webm/mkv - case 0x4F676753: - return false;// ogg - } - - throw new UnsupportedOperationException("file not recognized, failed to demux the audio stream"); - } - - @Override - int process(SharpStream out, @NonNull SharpStream... sources) throws IOException { - OggFromWebMWriter demuxer = new OggFromWebMWriter(sources[0], out); - demuxer.parseSource(); - demuxer.selectTrack(0); - demuxer.build(); - - return OK_RESULT; - } -} +package us.shandian.giga.postprocessing; + +import androidx.annotation.NonNull; + +import org.schabi.newpipe.streams.OggFromWebMWriter; +import org.schabi.newpipe.streams.io.SharpStream; + +import java.io.IOException; +import java.nio.ByteBuffer; + +class OggFromWebmDemuxer extends Postprocessing { + + OggFromWebmDemuxer() { + super(true, true, ALGORITHM_OGG_FROM_WEBM_DEMUXER); + } + + @Override + boolean test(SharpStream... sources) throws IOException { + ByteBuffer buffer = ByteBuffer.allocate(4); + sources[0].read(buffer.array()); + + // youtube uses WebM as container, but the file extension (format suffix) is "*.opus" + // check if the file is a webm/mkv file before proceed + + switch (buffer.getInt()) { + case 0x1a45dfa3: + return true;// webm/mkv + case 0x4F676753: + return false;// ogg + } + + throw new UnsupportedOperationException("file not recognized, failed to demux the audio stream"); + } + + @Override + int process(SharpStream out, @NonNull SharpStream... sources) throws IOException { + OggFromWebMWriter demuxer = new OggFromWebMWriter(sources[0], out); + demuxer.parseSource(); + demuxer.selectTrack(0); + demuxer.build(); + + return OK_RESULT; + } +} diff --git a/app/src/main/java/us/shandian/giga/service/DownloadManagerService.java b/app/src/main/java/us/shandian/giga/service/DownloadManagerService.java index b43733a51..e77196445 100755 --- a/app/src/main/java/us/shandian/giga/service/DownloadManagerService.java +++ b/app/src/main/java/us/shandian/giga/service/DownloadManagerService.java @@ -25,6 +25,7 @@ import android.os.IBinder; import android.os.Message; import android.os.Parcelable; +import androidx.core.app.ServiceCompat; import androidx.core.content.ContextCompat; import androidx.preference.PreferenceManager; import android.util.Log; @@ -235,7 +236,7 @@ public class DownloadManagerService extends Service { Log.d(TAG, "Destroying"); } - stopForeground(true); + ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE); if (mNotificationManager != null && downloadDoneNotification != null) { downloadDoneNotification.setDeleteIntent(null);// prevent NewPipe running when is killed, cleared from recent, etc @@ -363,7 +364,7 @@ public class DownloadManagerService extends Service { if (state) { startForeground(FOREGROUND_NOTIFICATION_ID, mNotification); } else { - stopForeground(true); + ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE); } manageLock(state); diff --git a/app/src/main/java/us/shandian/giga/ui/adapter/MissionAdapter.java b/app/src/main/java/us/shandian/giga/ui/adapter/MissionAdapter.java index be7d78299..f102206c1 100644 --- a/app/src/main/java/us/shandian/giga/ui/adapter/MissionAdapter.java +++ b/app/src/main/java/us/shandian/giga/ui/adapter/MissionAdapter.java @@ -1,7 +1,7 @@ package us.shandian.giga.ui.adapter; import android.annotation.SuppressLint; -import android.app.ProgressDialog; +import android.app.NotificationManager; import android.content.Context; import android.content.Intent; import android.graphics.Color; @@ -26,6 +26,8 @@ import android.widget.Toast; import androidx.annotation.NonNull; import androidx.annotation.StringRes; import androidx.appcompat.app.AlertDialog; +import androidx.core.app.NotificationCompat; +import androidx.core.content.ContextCompat; import androidx.core.content.FileProvider; import androidx.core.view.ViewCompat; import androidx.recyclerview.widget.DiffUtil; @@ -91,6 +93,7 @@ public class MissionAdapter extends Adapter implements Handler.Callb private static final String DEFAULT_MIME_TYPE = "*/*"; private static final String UNDEFINED_ETA = "--:--"; + private static final int HASH_NOTIFICATION_ID = 123790; static { ALGORITHMS.put(R.id.md5, "MD5"); @@ -678,28 +681,28 @@ public class MissionAdapter extends Adapter implements Handler.Callb return true; case R.id.md5: case R.id.sha1: - ProgressDialog progressDialog = null; - if (mContext != null) { - // Create dialog - progressDialog = new ProgressDialog(mContext); - progressDialog.setCancelable(false); - progressDialog.setMessage(mContext.getString(R.string.msg_wait)); - progressDialog.show(); - } - final ProgressDialog finalProgressDialog = progressDialog; + final NotificationManager notificationManager + = ContextCompat.getSystemService(mContext, NotificationManager.class); + final NotificationCompat.Builder progressNotificationBuilder + = new NotificationCompat.Builder(mContext, + mContext.getString(R.string.hash_channel_id)) + .setPriority(NotificationCompat.PRIORITY_HIGH) + .setSmallIcon(R.drawable.ic_newpipe_triangle_white) + .setContentTitle(mContext.getString(R.string.msg_calculating_hash)) + .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; compositeDisposable.add( Observable.fromCallable(() -> Utility.checksum(storage, ALGORITHMS.get(id))) .subscribeOn(Schedulers.computation()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(result -> { - if (finalProgressDialog != null) { - Utility.copyToClipboard(finalProgressDialog.getContext(), - result); - if (mContext != null) { - finalProgressDialog.dismiss(); - } - } + Utility.copyToClipboard(mContext, result); + notificationManager.cancel(HASH_NOTIFICATION_ID); }) ); return true; diff --git a/app/src/main/java/us/shandian/giga/ui/common/Deleter.java b/app/src/main/java/us/shandian/giga/ui/common/Deleter.java index 1d57605b9..b42ebbeb4 100644 --- a/app/src/main/java/us/shandian/giga/ui/common/Deleter.java +++ b/app/src/main/java/us/shandian/giga/ui/common/Deleter.java @@ -1,138 +1,138 @@ -package us.shandian.giga.ui.common; - -import android.content.Context; -import android.content.Intent; -import android.graphics.Color; -import android.os.Handler; -import android.view.View; - -import com.google.android.material.snackbar.Snackbar; - -import org.schabi.newpipe.R; - -import java.util.ArrayList; - -import us.shandian.giga.get.FinishedMission; -import us.shandian.giga.get.Mission; -import us.shandian.giga.service.DownloadManager; -import us.shandian.giga.service.DownloadManager.MissionIterator; -import us.shandian.giga.ui.adapter.MissionAdapter; - -public class Deleter { - private static final int TIMEOUT = 5000;// ms - private static final int DELAY = 350;// ms - private static final int DELAY_RESUME = 400;// ms - - private Snackbar snackbar; - private ArrayList items; - private boolean running = true; - - private final Context mContext; - private final MissionAdapter mAdapter; - private final DownloadManager mDownloadManager; - private final MissionIterator mIterator; - private final Handler mHandler; - private final View mView; - - private final Runnable rShow; - private final Runnable rNext; - private final Runnable rCommit; - - public Deleter(View v, Context c, MissionAdapter a, DownloadManager d, MissionIterator i, Handler h) { - mView = v; - mContext = c; - mAdapter = a; - mDownloadManager = d; - mIterator = i; - mHandler = h; - - // use variables to know the reference of the lambdas - rShow = this::show; - rNext = this::next; - rCommit = this::commit; - - items = new ArrayList<>(2); - } - - public void append(Mission item) { - mIterator.hide(item); - items.add(0, item); - - show(); - } - - private void forget() { - mIterator.unHide(items.remove(0)); - mAdapter.applyChanges(); - - show(); - } - - private void show() { - if (items.size() < 1) return; - - pause(); - running = true; - - mHandler.postDelayed(rNext, DELAY); - } - - private void next() { - if (items.size() < 1) return; - - String msg = mContext.getString(R.string.file_deleted).concat(":\n").concat(items.get(0).storage.getName()); - - snackbar = Snackbar.make(mView, msg, Snackbar.LENGTH_INDEFINITE); - snackbar.setAction(R.string.undo, s -> forget()); - snackbar.setActionTextColor(Color.YELLOW); - snackbar.show(); - - mHandler.postDelayed(rCommit, TIMEOUT); - } - - private void commit() { - if (items.size() < 1) return; - - while (items.size() > 0) { - Mission mission = items.remove(0); - if (mission.deleted) continue; - - mIterator.unHide(mission); - mDownloadManager.deleteMission(mission); - - if (mission instanceof FinishedMission) { - mContext.sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, mission.storage.getUri())); - } - break; - } - - if (items.size() < 1) { - pause(); - return; - } - - show(); - } - - public void pause() { - running = false; - mHandler.removeCallbacks(rNext); - mHandler.removeCallbacks(rShow); - mHandler.removeCallbacks(rCommit); - if (snackbar != null) snackbar.dismiss(); - } - - public void resume() { - if (running) return; - mHandler.postDelayed(rShow, DELAY_RESUME); - } - - public void dispose() { - if (items.size() < 1) return; - - pause(); - - for (Mission mission : items) mDownloadManager.deleteMission(mission); - items = null; - } -} +package us.shandian.giga.ui.common; + +import android.content.Context; +import android.content.Intent; +import android.graphics.Color; +import android.os.Handler; +import android.view.View; + +import com.google.android.material.snackbar.Snackbar; + +import org.schabi.newpipe.R; + +import java.util.ArrayList; + +import us.shandian.giga.get.FinishedMission; +import us.shandian.giga.get.Mission; +import us.shandian.giga.service.DownloadManager; +import us.shandian.giga.service.DownloadManager.MissionIterator; +import us.shandian.giga.ui.adapter.MissionAdapter; + +public class Deleter { + private static final int TIMEOUT = 5000;// ms + private static final int DELAY = 350;// ms + private static final int DELAY_RESUME = 400;// ms + + private Snackbar snackbar; + private ArrayList items; + private boolean running = true; + + private final Context mContext; + private final MissionAdapter mAdapter; + private final DownloadManager mDownloadManager; + private final MissionIterator mIterator; + private final Handler mHandler; + private final View mView; + + private final Runnable rShow; + private final Runnable rNext; + private final Runnable rCommit; + + public Deleter(View v, Context c, MissionAdapter a, DownloadManager d, MissionIterator i, Handler h) { + mView = v; + mContext = c; + mAdapter = a; + mDownloadManager = d; + mIterator = i; + mHandler = h; + + // use variables to know the reference of the lambdas + rShow = this::show; + rNext = this::next; + rCommit = this::commit; + + items = new ArrayList<>(2); + } + + public void append(Mission item) { + mIterator.hide(item); + items.add(0, item); + + show(); + } + + private void forget() { + mIterator.unHide(items.remove(0)); + mAdapter.applyChanges(); + + show(); + } + + private void show() { + if (items.size() < 1) return; + + pause(); + running = true; + + mHandler.postDelayed(rNext, DELAY); + } + + private void next() { + if (items.size() < 1) return; + + String msg = mContext.getString(R.string.file_deleted).concat(":\n").concat(items.get(0).storage.getName()); + + snackbar = Snackbar.make(mView, msg, Snackbar.LENGTH_INDEFINITE); + snackbar.setAction(R.string.undo, s -> forget()); + snackbar.setActionTextColor(Color.YELLOW); + snackbar.show(); + + mHandler.postDelayed(rCommit, TIMEOUT); + } + + private void commit() { + if (items.size() < 1) return; + + while (items.size() > 0) { + Mission mission = items.remove(0); + if (mission.deleted) continue; + + mIterator.unHide(mission); + mDownloadManager.deleteMission(mission); + + if (mission instanceof FinishedMission) { + mContext.sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, mission.storage.getUri())); + } + break; + } + + if (items.size() < 1) { + pause(); + return; + } + + show(); + } + + public void pause() { + running = false; + mHandler.removeCallbacks(rNext); + mHandler.removeCallbacks(rShow); + mHandler.removeCallbacks(rCommit); + if (snackbar != null) snackbar.dismiss(); + } + + public void resume() { + if (running) return; + mHandler.postDelayed(rShow, DELAY_RESUME); + } + + public void dispose() { + if (items.size() < 1) return; + + pause(); + + for (Mission mission : items) mDownloadManager.deleteMission(mission); + items = null; + } +} diff --git a/app/src/main/res/drawable/ic_format_list_numbered_white_24.xml b/app/src/main/res/drawable/ic_format_list_numbered_white_24.xml new file mode 100644 index 000000000..429616ec9 --- /dev/null +++ b/app/src/main/res/drawable/ic_format_list_numbered_white_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_live_tv_black_24dp.xml b/app/src/main/res/drawable/ic_live_tv_black_24dp.xml new file mode 100644 index 000000000..1f7957c4a --- /dev/null +++ b/app/src/main/res/drawable/ic_live_tv_black_24dp.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_live_tv_white_24dp.xml b/app/src/main/res/drawable/ic_live_tv_white_24dp.xml new file mode 100644 index 000000000..303858f9d --- /dev/null +++ b/app/src/main/res/drawable/ic_live_tv_white_24dp.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/layout-land/activity_player_queue_control.xml b/app/src/main/res/layout-land/activity_player_queue_control.xml index 2adea9868..b106e7437 100644 --- a/app/src/main/res/layout-land/activity_player_queue_control.xml +++ b/app/src/main/res/layout-land/activity_player_queue_control.xml @@ -6,7 +6,7 @@ android:layout_width="match_parent" android:layout_height="match_parent" android:fitsSystemWindows="true" - tools:context="org.schabi.newpipe.player.BackgroundPlayerActivity"> + tools:context="org.schabi.newpipe.player.PlayQueueActivity"> + + + + -