diff --git a/.circleci/config.yml b/.circleci/config.yml index 1a97e73c5..8063f259c 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -45,7 +45,7 @@ workflows: destination: app-play-debug.apk - run: name: Execute debug unit tests - command: ./gradlew :core:testPlayDebugUnitTest -PdisablePreDex + command: ./gradlew testPlayDebugUnitTest -PdisablePreDex - build: name: Build release build-steps: @@ -54,19 +54,19 @@ workflows: command: ./gradlew assembleRelease -PdisablePreDex - run: name: Execute release unit tests - command: ./gradlew :core:testPlayReleaseUnitTest -PdisablePreDex + command: ./gradlew testPlayReleaseUnitTest -PdisablePreDex - build: name: Build integration tests build-steps: - run: name: Build integration tests - command: ./gradlew :app:assemblePlayDebugAndroidTest -PdisablePreDex + command: ./gradlew assemblePlayDebugAndroidTest -PdisablePreDex - build: name: Build free build-steps: - run: name: Build free (for F-Droid) - command: ./gradlew assembleFreeRelease -PdisablePreDex -PfreeBuild + command: ./gradlew assembleFreeRelease -PdisablePreDex static-analysis: jobs: @@ -85,19 +85,8 @@ workflows: curl -s -L https://github.com/yangziwen/diff-checkstyle/releases/download/0.0.4/diff-checkstyle.jar > diff-checkstyle.jar java -Dconfig_loc=config/checkstyle -jar diff-checkstyle.jar -c config/checkstyle/checkstyle-new-code.xml --git-dir . --base-rev $branchBaseCommit - build: - name: Lint app + name: Lint build-steps: - run: - name: Lint app - command: ./gradlew app:lintPlayRelease - - run: - name: Lint core - command: ./gradlew core:lintPlayRelease - - store_artifacts: - name: Uploading app lint reports - path: app/build/reports/lint-results-playRelease.html - destination: lint-results-app.html - - store_artifacts: - name: Uploading core lint reports - path: core/build/reports/lint-results-playRelease.html - destination: lint-results-core.html + name: Lint + command: ./gradlew lintPlayRelease diff --git a/.github/PULL_REQUEST_TEMPLATE/default.md b/.github/pull_request_template.md similarity index 100% rename from .github/PULL_REQUEST_TEMPLATE/default.md rename to .github/pull_request_template.md diff --git a/.tx/config b/.tx/config index 43e05555a..bc37f39fe 100644 --- a/.tx/config +++ b/.tx/config @@ -4,6 +4,7 @@ host = https://www.transifex.com [antennapod.core-values] source_file = core/src/main/res/values/strings.xml source_lang = en +trans.ar = core/src/main/res/values-ar/strings.xml trans.br = core/src/main/res/values-br/strings.xml trans.ca = core/src/main/res/values-ca/strings.xml trans.cs_CZ = core/src/main/res/values-cs/strings.xml @@ -29,6 +30,7 @@ trans.pl_PL = core/src/main/res/values-pl/strings.xml trans.pt = core/src/main/res/values-pt/strings.xml trans.pt_BR = core/src/main/res/values-pt-rBR/strings.xml trans.ru_RU = core/src/main/res/values-ru/strings.xml +trans.sk = core/src/main/res/values-sk/strings.xml trans.sv_SE = core/src/main/res/values-sv/strings.xml trans.tr = core/src/main/res/values-tr/strings.xml trans.uk_UA = core/src/main/res/values-uk/strings.xml @@ -58,7 +60,6 @@ trans.he_IL = app/src/main/play/listings/iw-IL/full-description.txt trans.hu = app/src/main/play/listings/hu-HU/full-description.txt trans.id = app/src/main/play/listings/id/full-description.txt trans.it_IT = app/src/main/play/listings/it-IT/full-description.txt -trans.iw = app/src/main/play/listings/iw-IL/full-description.txt trans.ja = app/src/main/play/listings/ja-JP/full-description.txt trans.ko = app/src/main/play/listings/ko-KR/full-description.txt trans.lt = app/src/main/play/listings/lt/full-description.txt @@ -68,6 +69,7 @@ trans.pt_BR = app/src/main/play/listings/pt-BR/full-description.txt trans.pt = app/src/main/play/listings/pt-PT/full-description.txt trans.ro_RO = app/src/main/play/listings/ro/full-description.txt trans.ru_RU = app/src/main/play/listings/ru-RU/full-description.txt +trans.sk = app/src/main/play/listings/sk/full-description.txt trans.sl_SI = app/src/main/play/listings/sl/full-description.txt trans.sv_SE = app/src/main/play/listings/sv-SE/full-description.txt trans.tr = app/src/main/play/listings/tr-TR/full-description.txt @@ -98,7 +100,6 @@ trans.he_IL = app/src/main/play/listings/iw-IL/short-description.txt trans.hu = app/src/main/play/listings/hu-HU/short-description.txt trans.id = app/src/main/play/listings/id/short-description.txt trans.it_IT = app/src/main/play/listings/it-IT/short-description.txt -trans.iw = app/src/main/play/listings/iw-IL/short-description.txt trans.ja = app/src/main/play/listings/ja-JP/short-description.txt trans.ko = app/src/main/play/listings/ko-KR/short-description.txt trans.lt = app/src/main/play/listings/lt/short-description.txt @@ -108,6 +109,7 @@ trans.pt_BR = app/src/main/play/listings/pt-BR/short-description.txt trans.pt = app/src/main/play/listings/pt-PT/short-description.txt trans.ro_RO = app/src/main/play/listings/ro/short-description.txt trans.ru_RU = app/src/main/play/listings/ru-RU/short-description.txt +trans.sk = app/src/main/play/listings/sk/short-description.txt trans.sl_SI = app/src/main/play/listings/sl/short-description.txt trans.sv_SE = app/src/main/play/listings/sv-SE/short-description.txt trans.tr = app/src/main/play/listings/tr-TR/short-description.txt diff --git a/CHANGELOG.md b/CHANGELOG.md deleted file mode 100644 index 8fb58a64a..000000000 --- a/CHANGELOG.md +++ /dev/null @@ -1,471 +0,0 @@ -Change Log -========== - -Version 1.8.1 -------------- -* Enabled picture-in-picture for video podcasts by default (by @ByteHamster) -* Fixed podcast discovery not showing local trends (by @tonytamsf) -* Various bug fixes and improvements (by @ByteHamster) - -Version 1.8.0 -------------- -* Added per-feed playback speed setting (by @spacecowboy) -* Support sorting in Podcast screen (by @orionlee) -* Option to show stream button rather than download in lists (by @dsmith47) -* Option to replace Episode cover with Podcast cover (by @xgouchet) -* Transparent widget (by @M-arcel) -* User interface tweaks (by @ByteHamster) -* Tons of bug fixes and improvements - -Version 1.7.3 -------------- -* Display episode image on widget (by @brad) -* Added checkbox to keep queue sorted (by @damoasda) -* New UI for "Add podcast" screen (by @ByteHamster) -* Added batch editing to the queue (by @ByteHamster) -* Added option to adapt remaining time to playback speed (by @CedricCabessa) -* Removed broken Flattr integration (by @ByteHamster) -* Added filter to "All episodes" list (by @jhunnius) -* Tons of bug fixes and performance improvements - -Version 1.7.2 -------------- -* Added configurable behavior of the back button -* Added delete option to episode's context menu -* New UI for batch edit feature -* Set number of columns in subscription list -* Lots of bug fixes - -Version 1.7.1 -------------- - -* Fix for database corruption - -Version 1.7.0 -------------- - -* NEW ExoPlayer (experimental) -* Fix for Bluetooth Forward (Oreo) -* Preference redesign + search -* Notification improvements -* Different screens for feed info and settings -* Sort Queue with Random or Smart Shuffle -* True Black Theme for AMOLED -* Improvements to feed parsing -* Fix for app being killed by Android Oreo - -Version 1.6.5 -------------- - -* Fix database corruption -* Improvements to Feed parsing - -Version 1.6.4 -------------- - -* Fixes issues on Android Oreo -* Avoids duplicate chapters -* Experimental: Database import & export - -Version 1.6.3 -------------- - -* New features: - * Support for Android Auto - * Sort feeds by number of played episodes - * Statistics modes - * Setting: Enqueue downloaded - * Launch screen -* Improvements - * Chapter duration - * Feed title in deletion confirmation -* Fixes: - * Episodes refresh spinner - * Publication date parsing - * Unknown mime type - -Version 1.6.2 -------------- - -* New features: - * Integration of fyyd Podcast Search Engine - * Export subscriptions as HTML - * Rename feeds - * Auto-enable sleep timer - * "has media" filter - * Force gpodder full sync -* Improvements: - * Better support for Atom feeds, e.g. summary tag - * Confirmation dialog on mark all as seen - * Number of downloaded episodes in subscription counter - * Gpodder sync error optional - * Search results - * MRSS support - * Sanitize HTML from Atom feed -* Fixes: - * Reset sleep timer on shake to current waiting time - * Cast dialog image - * Mini player not showing up - * Audio player cover fragment - * Prevent out of memory and casting crashes - -Version 1.6.0 -------------- -* New features: - * Experimental Chromecast support - * Subscription overview - * Proxy support - * Statistics - * Manual gpodder.net sync -* Fixes: - * Audioplayer controls - * Audio ducking - * Video control fade-out - * External media controls - * Feed parsing - -Version 1.5.0 -------------- -* Exclude episodes from auto download by keyword -* Configure feeds to prevent them from refreshing automatically -* Improved audio player -* Improved UI -* Bug fixes - -Version 1.4.1 -------------- -* Performance improvements -* Hardware buttons now ff and rewind instead of skipping -* Option to have forward button skip -* Option to send crash reports directly to developers -* Highlight currently playing episode -* Widget improvements - -Version 1.4.0.12 ----------------- -* Fix for crash on Huawei devices (media buttons may not work) - -Version 1.4 ------------ -* BLUETOOTH PERMISSION: Needed to be able to resume playback when a Bluetooth device reconnects with your phone -* VIBRATE PERMISSION: Used optionally with the sleep timer -* Native variable speed playback (experimental via options) -* Improved sleep timer -* Mark episodes as 'favorite' -* Notification can skip episodes -* Keep episodes when skipping them -* Episode art on lock screen -* Flexible episode cleanup -* Rewind after pause -* Usability improvements -* Bug fixes - -Version 1.3 ------------ -* Bulk actions on feed episodes (download, queue, delete) -* Reduced space used by images -* Automatic refresh at a certain time of day -* Customizable indicators and sorting for feeds -* Ability to share feeds -* Improved auto download -* Many fixes and usability improvements - -Version 1.2 ------------ -* Optionally disable swiping and dragging in the queue -* Resume playback after phone call -* Filter episodes in the Podcast feed -* Hide items in the Nav drawer -* Customize times for fast forward and rewind -* Resolved issues with opening some OPML files -* Various bug fixes and usability improvements - -Version 1.1 ------------ -* iTunes podcast integration -* Swipe to remove items from the queue -* Set the number of parallel downloads -* Fix for gpodder.net on old devices -* Fixed date problems for some feeds -* Display improvements -* Usability improvements -* Several other bugfixes - -Version 1.0 ------------ -* The queue can now be sorted -* Added option to delete episode after playback -* Fixed a bug that caused chapters to be displayed multiple times -* Several other improvements and bugfixes - - -Version 0.9.9.6 ---------------- -* Fixed problems related to variable playback speed plugins -* Fixed automatic feed update problems -* Several other bugfixes and improvements - -Version 0.9.9.5 ---------------- -* Added support for paged feeds -* Improved user interface -* Added Japanese and Turkish translations -* Fixed more image loading problems -* Other bugfixes and improvements - -Version 0.9.9.4 ---------------- -* Added option to keep notification and lockscreen controls when playback is paused -* Fixed a bug where episode images were not loaded correctly -* Fixed battery usage problems - -Version 0.9.9.3 ---------------- -* Fixed video playback problems -* Improved image loading -* Other bugfixes and improvements - -Version 0.9.9.2 ---------------- -* Added support for feed discovery if a website URL is entered -* Added support for 'next'/'previous' media keys -* Improved sleep timer -* Timestamps in shownotes can now be used to jump to a specific position -* Automatic Flattring is now configurable -* Several bugfixes and improvements - -Version 0.9.9.1 ---------------- -* Several bugfixes and improvements - -Version 0.9.9.0 ---------------- -* New user interface -* Failed downloads are now resumed when restarted -* Added support for Podlove Alternate Feeds -* Added support for "pcast"-protocol -* Added backup & restore functionality. This feature has to be enabled in the Android settings in order to work -* Several bugfixes and improvements - -Version 0.9.8.3 ---------------- -* Added support for password-protected feeds and episodes -* Added support for more types of episode images -* Added Hebrew translation -* Several bugfixes and improvements - -Version 0.9.8.2 ---------------- -* Several bugfixes and improvements -* Added Korean translation - -Version 0.9.8.1 ---------------- -* Added option to flattr an episode automatically after 80 percent of the episode have been played -* Added Polish translation -* Several bugfixes and improvements - -Version 0.9.8.0 ---------------- -* Added access to the gpodder.net directory -* Added ability to sync podcast subscriptions with the gpodder.net service -* Automatic download can now be turned on or off for specific podcasts -* Added option to pause playback when another app is playing sounds -* Added Dutch and Hindi translation -* Resolved a problem with automatic podcast updates -* Resolved a problem with the buttons' visibility in the episode screen -* Resolved a problem where episodes would be re-downloaded unnecessarily -* Several other bugfixes and usability improvements - -Version 0.9.7.5 ---------------- -* Reduced application startup time -* Reduced memory usage -* Added option to change the playback speed -* Added Swedish translation -* Several bugfixes and improvements - -Version 0.9.7.4 ---------------- -* Episode cache size can now be set to unlimited -* Removing an episode in the queue via sliding can now be undone -* Added support for Links in MP3 chapters -* Added Czech(Czech Republic), Azerbaijani and Portuguese translations -* Several bugfixes and improvements - -Version 0.9.7.3 ---------------- -* Bluetooth devices now display metadata during playback (requires AVRCP 1.3 or higher) -* User interface improvements -* Several bugfixes - -Version 0.9.7.2 ---------------- -* Automatic download can now be disabled -* Added Italian (Italy) translation -* Several bugfixes - -Version 0.9.7.1 ---------------- -* Added automatic download of new episodes -* Added option to specify the number of downloaded episodes to keep on the device -* Added support for playback of external media files -* Several improvements and bugfixes -* Added Catalan translation - -Version 0.9.7 -------------- -* Improved user interface -* OPML files can now be imported by selecting them in a file browser -* The queue can now be organized via drag & drop -* Added expandable notifications (only supported on Android 4.1 and above) -* Added Danish, French, Romanian (Romania) and Ukrainian (Ukraine) translation (thanks to all translators!) -* Several bugfixes and minor improvements - -Version 0.9.6.4 ---------------- -* Added Chinese translation (Thanks tupunco!) -* Added Portuguese (Brazil) translation (Thanks mbaltar!) -* Several bugfixes - -Version 0.9.6.3 ---------------- -* Added the ability change the location of AntennaPod's data folder -* Added Spanish translation (Thanks frandavid100!) -* Solved problems with several feeds - -Version 0.9.6.2 ---------------- -* Fixed import problems with some OPML files -* Fixed download problems -* AntennaPod now recognizes changes of episode information -* Other improvements and bugfixes - -Version 0.9.6.1 ---------------- -* Added dark theme -* Several bugfixes and improvements - -Version 0.9.6 -------------- -* Added support for VorbisComment chapters -* AntennaPod now shows items as 'in progress' when playback has started -* Reduced memory usage -* Added support for more feed types -* Several bugfixes - - -Version 0.9.5.3 ---------------- -* Fixed crash when trying to start playback on some devices -* Fixed problems with some feeds -* Other bugfixes and improvements - -Version 0.9.5.2 ---------------- -* Media player now doesn't use network bandwidth anymore if not in use -* Other improvements and bugfixes - -Version 0.9.5.1 ---------------- -* Added playback history -* Improved behavior of download report notifications -* Improved support for headset controls -* Bugfixes in the feed parser -* Moved 'OPML import' button into the 'add feed' screen and the 'OPML export' button into the settings screen - -Version 0.9.5 -------------- -* Experimental support for MP3 chapters -* New menu options for the 'new' list and the queue -* Auto-delete feature -* Better Download error reports -* Several Bugfixes - -Version 0.9.4.6 ---------------- -* Enabled support for small-screen devices -* Disabling the sleep timer should now work again - -Version 0.9.4.5 ---------------- -* Added Russian translation (Thanks older!) -* Added German translation -* Several bugfixes - -Version 0.9.4.4 ---------------- -* Added player controls at the bottom of the main screen and the feedlist screens -* Improved media playback - -Version 0.9.4.3 ---------------- -* Fixed several bugs in the feed parser -* Improved behavior of download reports - -Version 0.9.4.2 ---------------- -* Fixed bug in the OPML importer -* Reduced memory usage of images -* Fixed download problems on some devices - -Version 0.9.4.1 ---------------- -* Changed behavior of download notifications - -Version 0.9.4 -------------- -* Faster and more reliable downloads -* Added lockscreen player controls for Android 4.x devices -* Several bugfixes - -Version 0.9.3.1 ---------------- -* Added preference to hide feed items which don't have an episode -* Improved image size for some some screen sizes -* Added grid view for large screens -* Several bugfixes - -Version 0.9.3 -------------- -* MiroGuide integration -* Bugfixes in the audio- and videoplayer -* Automatically add feeds to the queue when they have been downloaded - -Version 0.9.2 -------------- -* Bugfixes in the user interface -* GUID and ID attributes are now recognized by the Feedparser -* Stability improvements when adding several feeds at the same time -* Fixed bugs that occured when adding certain feeds - -Version 0.9.1.1 --------------------- -* Changed Flattr credentials -* Improved layout of Feed information screen -* AntennaPod is now open source! The source code is available at https://github.com/danieloeh/AntennaPod - -Version 0.9.1 ------------------ -* Added support for links in SimpleChapters -* Bugfix: Current Chapter wasn't always displayed correctly - -Version 0.9 --------------- - -* OPML export -* Flattr integration -* Sleep timer - -Version 0.8.2 -------------- - -* Added search -* Improved OPML import experience -* More bugfixes - -Version 0.8.1 ------------- - -* Added support for SimpleChapters -* OPML import diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a7cb59fc2..614e76d87 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -35,3 +35,24 @@ Submit a pull request - Please do not upgrade dependencies or build tools unless you have a good reason for it. Doing so can easily introduce bugs that are hard to track down. - If you plan to do a change that touches many files (10+), please ask beforehand. This usually causes merge conflicts for other developers. - Please follow our code style. You can use Checkstyle within Android Studio using our [coniguration file](https://github.com/AntennaPod/AntennaPod/blob/develop/config/checkstyle/checkstyle-new-code.xml). +- Please only change the English string resources. Translations are handled on [Transifex](https://www.transifex.com/antennapod/antennapod/). + + +Testing and Verifying +-------------------------- +As a developer contributing to AntennaPod, we ask that you test the feature yourself manually and better yet, add unit and functional tests to any feature of bug you fix. + +### Running Unit Tests +* `./gradlew :core:testPlayDebugUnitTest` + +### Running Integration Tests + +#### Using Android Studio +* Create a configuration via 'Run->Edit Configurations...' + +antennapod-run-tests + +#### Using the command line +* Start an AVD or plug in your phone +* `sh .github/workflows/runTests.sh` diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index a2a44e10e..97b6a3c9c 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -1,27 +1,27 @@ # Developers -[ByteHamster](https://github.com/ByteHamster), [danieloeh](https://github.com/danieloeh), [mfietz](https://github.com/mfietz), [TomHennen](https://github.com/TomHennen), [orionlee](https://github.com/orionlee), [domingos86](https://github.com/domingos86), [tonytamsf](https://github.com/tonytamsf), [andersonvom](https://github.com/andersonvom), [damoasda](https://github.com/damoasda), [TacoTheDank](https://github.com/TacoTheDank), [shortspider](https://github.com/shortspider), [ebraminio](https://github.com/ebraminio), [asdoi](https://github.com/asdoi), [spacecowboy](https://github.com/spacecowboy), [patheticpat](https://github.com/patheticpat), [brad](https://github.com/brad), [Cj-Malone](https://github.com/Cj-Malone), [maxbechtold](https://github.com/maxbechtold), [gaul](https://github.com/gaul), [qkolj](https://github.com/qkolj), [keunes](https://github.com/keunes), [pachecosf](https://github.com/pachecosf), [gerardolgvr](https://github.com/gerardolgvr), [bws9000](https://github.com/bws9000), [ahangarha](https://github.com/ahangarha), [hannesa2](https://github.com/hannesa2), [rharriso](https://github.com/rharriso), [xgouchet](https://github.com/xgouchet), [sevenmaster](https://github.com/sevenmaster), [TheRealFalcon](https://github.com/TheRealFalcon), [Slinger](https://github.com/Slinger), [johnjohndoe](https://github.com/johnjohndoe), [jas14](https://github.com/jas14), [udif](https://github.com/udif), [malockin](https://github.com/malockin), [dirkmueller](https://github.com/dirkmueller), [jatinkumarg](https://github.com/jatinkumarg), [peschmae0](https://github.com/peschmae0), [orelogo](https://github.com/orelogo), [txtd](https://github.com/txtd), [ydinath](https://github.com/ydinath), [CedricCabessa](https://github.com/CedricCabessa), [mchelen](https://github.com/mchelen), [dethstar](https://github.com/dethstar), [drabux](https://github.com/drabux), [saqura](https://github.com/saqura), [bibz](https://github.com/bibz), [hzulla](https://github.com/hzulla), [deandreamatias](https://github.com/deandreamatias), [MeirAtIMDDE](https://github.com/MeirAtIMDDE), [egsavage](https://github.com/egsavage), [ligi](https://github.com/ligi), [Xeitor](https://github.com/Xeitor), [dreiss](https://github.com/dreiss), [liesen](https://github.com/liesen), [nereocystis](https://github.com/nereocystis), [rezanejati](https://github.com/rezanejati), [twiceyuan](https://github.com/twiceyuan), [JessieVela](https://github.com/JessieVela), [HaBaLeS](https://github.com/HaBaLeS), [volhol](https://github.com/volhol), [michaelmwhite](https://github.com/michaelmwhite), [CameronBanga](https://github.com/CameronBanga), [HrBDev](https://github.com/HrBDev), [HolgerJeromin](https://github.com/HolgerJeromin), [xisberto](https://github.com/xisberto), [jmue](https://github.com/jmue), [katrinleinweber](https://github.com/katrinleinweber), [LatinSuD](https://github.com/LatinSuD), [24hours](https://github.com/24hours), [SosoTughushi](https://github.com/SosoTughushi), [fabolhak](https://github.com/fabolhak), [archibishop](https://github.com/archibishop), [alifeflow](https://github.com/alifeflow), [avirajrsingh](https://github.com/avirajrsingh), [toggles](https://github.com/toggles), [matdb](https://github.com/matdb), [damlayildiz](https://github.com/damlayildiz), [kingargyle](https://github.com/kingargyle), [dsmith47](https://github.com/dsmith47), [hannesaa2](https://github.com/hannesaa2), [jhunnius](https://github.com/jhunnius), [ShadowIce](https://github.com/ShadowIce), [Niffler](https://github.com/Niffler), [raghulj](https://github.com/raghulj), [raghulrm](https://github.com/raghulrm), [mamehacker](https://github.com/mamehacker), [skitt](https://github.com/skitt), [wseemann](https://github.com/wseemann), [markamaze](https://github.com/markamaze), [mohitshah3111999](https://github.com/mohitshah3111999), [moralesg](https://github.com/moralesg), [mr-intj](https://github.com/mr-intj), [tuxayo](https://github.com/tuxayo), [schlch](https://github.com/schlch), [alimemonzx](https://github.com/alimemonzx), [dev-darrell](https://github.com/dev-darrell), [jmdouglas](https://github.com/jmdouglas), [olivoto](https://github.com/olivoto), [PtilopsisLeucotis](https://github.com/PtilopsisLeucotis), [abhinavg1997](https://github.com/abhinavg1997), [alanorth](https://github.com/alanorth), [alexte](https://github.com/alexte), [andrey-krutov](https://github.com/andrey-krutov), [arantius](https://github.com/arantius), [BoJacobs](https://github.com/BoJacobs), [chetan882777](https://github.com/chetan882777), [chrissicool](https://github.com/chrissicool), [cszucko](https://github.com/cszucko), [CWftw](https://github.com/CWftw), [connectety](https://github.com/connectety), [danielm5](https://github.com/danielm5), [ariedov](https://github.com/ariedov), [brettle](https://github.com/brettle), [edwinhere](https://github.com/edwinhere), [eirikv](https://github.com/eirikv), [eerden](https://github.com/eerden), [jklippel](https://github.com/jklippel), [jannic](https://github.com/jannic), [Foso](https://github.com/Foso), [Kaligule](https://github.com/Kaligule), [kvithayathil](https://github.com/kvithayathil), [luiscruz](https://github.com/luiscruz), [mlasson](https://github.com/mlasson), [schwedenmut](https://github.com/schwedenmut), [M-arcel](https://github.com/M-arcel), [msoose](https://github.com/msoose), [mo](https://github.com/mo), [mdeveloper20](https://github.com/mdeveloper20), [mschuetz](https://github.com/mschuetz), [max-wittig](https://github.com/max-wittig), [MolarAmbiguity](https://github.com/MolarAmbiguity), [mounirlamouri](https://github.com/mounirlamouri), [nikhil097](https://github.com/nikhil097), [panoreak](https://github.com/panoreak), [patrickjkennedy](https://github.com/patrickjkennedy), [ortylp](https://github.com/ortylp), [ramzan](https://github.com/ramzan), [iamrichR](https://github.com/iamrichR), [SamWhited](https://github.com/SamWhited), [selivan](https://github.com/selivan), [sonnayasomnambula](https://github.com/sonnayasomnambula), [sethoscope](https://github.com/sethoscope), [shantanahardy](https://github.com/shantanahardy), [danners](https://github.com/danners), [corecode](https://github.com/corecode), [vimsick](https://github.com/vimsick), [lyallemma](https://github.com/lyallemma), [edent](https://github.com/edent), [atrus6](https://github.com/atrus6), [heyyviv](https://github.com/heyyviv), [waylife](https://github.com/waylife), [amhokies](https://github.com/amhokies), [andrewc1](https://github.com/andrewc1), [axq](https://github.com/axq), [binarytoto](https://github.com/binarytoto), [chrk2205](https://github.com/chrk2205), [fossterer](https://github.com/fossterer), [lightonflux](https://github.com/lightonflux), [minusf](https://github.com/minusf), [zawad2221](https://github.com/zawad2221) +[ByteHamster](https://github.com/ByteHamster), [danieloeh](https://github.com/danieloeh), [mfietz](https://github.com/mfietz), [TomHennen](https://github.com/TomHennen), [orionlee](https://github.com/orionlee), [domingos86](https://github.com/domingos86), [damoasda](https://github.com/damoasda), [tonytamsf](https://github.com/tonytamsf), [andersonvom](https://github.com/andersonvom), [TacoTheDank](https://github.com/TacoTheDank), [shortspider](https://github.com/shortspider), [spacecowboy](https://github.com/spacecowboy), [ebraminio](https://github.com/ebraminio), [asdoi](https://github.com/asdoi), [patheticpat](https://github.com/patheticpat), [brad](https://github.com/brad), [Cj-Malone](https://github.com/Cj-Malone), [maxbechtold](https://github.com/maxbechtold), [gaul](https://github.com/gaul), [qkolj](https://github.com/qkolj), [keunes](https://github.com/keunes), [pachecosf](https://github.com/pachecosf), [gerardolgvr](https://github.com/gerardolgvr), [bws9000](https://github.com/bws9000), [ahangarha](https://github.com/ahangarha), [hannesa2](https://github.com/hannesa2), [rharriso](https://github.com/rharriso), [xgouchet](https://github.com/xgouchet), [sevenmaster](https://github.com/sevenmaster), [TheRealFalcon](https://github.com/TheRealFalcon), [Slinger](https://github.com/Slinger), [johnjohndoe](https://github.com/johnjohndoe), [jas14](https://github.com/jas14), [udif](https://github.com/udif), [malockin](https://github.com/malockin), [dirkmueller](https://github.com/dirkmueller), [jatinkumarg](https://github.com/jatinkumarg), [peschmae0](https://github.com/peschmae0), [orelogo](https://github.com/orelogo), [txtd](https://github.com/txtd), [ydinath](https://github.com/ydinath), [CedricCabessa](https://github.com/CedricCabessa), [mchelen](https://github.com/mchelen), [dethstar](https://github.com/dethstar), [drabux](https://github.com/drabux), [saqura](https://github.com/saqura), [binarytoto](https://github.com/binarytoto), [bibz](https://github.com/bibz), [hzulla](https://github.com/hzulla), [deandreamatias](https://github.com/deandreamatias), [MeirAtIMDDE](https://github.com/MeirAtIMDDE), [egsavage](https://github.com/egsavage), [ligi](https://github.com/ligi), [Xeitor](https://github.com/Xeitor), [dreiss](https://github.com/dreiss), [liesen](https://github.com/liesen), [nereocystis](https://github.com/nereocystis), [rezanejati](https://github.com/rezanejati), [twiceyuan](https://github.com/twiceyuan), [JessieVela](https://github.com/JessieVela), [HaBaLeS](https://github.com/HaBaLeS), [volhol](https://github.com/volhol), [michaelmwhite](https://github.com/michaelmwhite), [CameronBanga](https://github.com/CameronBanga), [HrBDev](https://github.com/HrBDev), [HolgerJeromin](https://github.com/HolgerJeromin), [xisberto](https://github.com/xisberto), [jmue](https://github.com/jmue), [jonasburian](https://github.com/jonasburian), [katrinleinweber](https://github.com/katrinleinweber), [LatinSuD](https://github.com/LatinSuD), [24hours](https://github.com/24hours), [SosoTughushi](https://github.com/SosoTughushi), [fabolhak](https://github.com/fabolhak), [archibishop](https://github.com/archibishop), [alifeflow](https://github.com/alifeflow), [avirajrsingh](https://github.com/avirajrsingh), [toggles](https://github.com/toggles), [connectety](https://github.com/connectety), [matdb](https://github.com/matdb), [damlayildiz](https://github.com/damlayildiz), [kingargyle](https://github.com/kingargyle), [dsmith47](https://github.com/dsmith47), [hannesaa2](https://github.com/hannesaa2), [jhunnius](https://github.com/jhunnius), [a1291762](https://github.com/a1291762), [ShadowIce](https://github.com/ShadowIce), [Niffler](https://github.com/Niffler), [raghulj](https://github.com/raghulj), [raghulrm](https://github.com/raghulrm), [mamehacker](https://github.com/mamehacker), [skitt](https://github.com/skitt), [Thom-Merrilin](https://github.com/Thom-Merrilin), [wseemann](https://github.com/wseemann), [markamaze](https://github.com/markamaze), [mohitshah3111999](https://github.com/mohitshah3111999), [moralesg](https://github.com/moralesg), [mr-intj](https://github.com/mr-intj), [tuxayo](https://github.com/tuxayo), [alimemonzx](https://github.com/alimemonzx), [dev-darrell](https://github.com/dev-darrell), [jmdouglas](https://github.com/jmdouglas), [olivoto](https://github.com/olivoto), [PtilopsisLeucotis](https://github.com/PtilopsisLeucotis), [abhinavg1997](https://github.com/abhinavg1997), [alanorth](https://github.com/alanorth), [alexte](https://github.com/alexte), [andrey-krutov](https://github.com/andrey-krutov), [arantius](https://github.com/arantius), [BoJacobs](https://github.com/BoJacobs), [chetan882777](https://github.com/chetan882777), [chrissicool](https://github.com/chrissicool), [britiger](https://github.com/britiger), [cszucko](https://github.com/cszucko), [CWftw](https://github.com/CWftw), [danielm5](https://github.com/danielm5), [ariedov](https://github.com/ariedov), [brettle](https://github.com/brettle), [edwinhere](https://github.com/edwinhere), [eirikv](https://github.com/eirikv), [eerden](https://github.com/eerden), [Geist5000](https://github.com/Geist5000), [jklippel](https://github.com/jklippel), [jannic](https://github.com/jannic), [Foso](https://github.com/Foso), [Kaligule](https://github.com/Kaligule), [kvithayathil](https://github.com/kvithayathil), [luiscruz](https://github.com/luiscruz), [MStrecke](https://github.com/MStrecke), [mlasson](https://github.com/mlasson), [schwedenmut](https://github.com/schwedenmut), [M-arcel](https://github.com/M-arcel), [mgborowiec](https://github.com/mgborowiec), [msoose](https://github.com/msoose), [mo](https://github.com/mo), [mdeveloper20](https://github.com/mdeveloper20), [mschuetz](https://github.com/mschuetz), [max-wittig](https://github.com/max-wittig), [MolarAmbiguity](https://github.com/MolarAmbiguity), [mounirlamouri](https://github.com/mounirlamouri), [nikhil097](https://github.com/nikhil097), [panoreak](https://github.com/panoreak), [patrickjkennedy](https://github.com/patrickjkennedy), [ortylp](https://github.com/ortylp), [ramzan](https://github.com/ramzan), [iamrichR](https://github.com/iamrichR), [SamWhited](https://github.com/SamWhited), [SebiderSushi](https://github.com/SebiderSushi), [selivan](https://github.com/selivan), [sonnayasomnambula](https://github.com/sonnayasomnambula), [sethoscope](https://github.com/sethoscope), [shantanahardy](https://github.com/shantanahardy), [danners](https://github.com/danners), [corecode](https://github.com/corecode), [vimsick](https://github.com/vimsick), [lyallemma](https://github.com/lyallemma), [edent](https://github.com/edent), [atrus6](https://github.com/atrus6), [timakro](https://github.com/timakro), [heyyviv](https://github.com/heyyviv), [waylife](https://github.com/waylife), [yarons](https://github.com/yarons), [amhokies](https://github.com/amhokies), [andrewc1](https://github.com/andrewc1), [axq](https://github.com/axq), [chrk2205](https://github.com/chrk2205), [fossterer](https://github.com/fossterer), [lightonflux](https://github.com/lightonflux), [minusf](https://github.com/minusf), [s3lph](https://github.com/s3lph), [tamizh138](https://github.com/tamizh138), [zawad2221](https://github.com/zawad2221) # Translators | Language | Translators | | :-- | :-- | -| Arabic | abuzar3.khalid, badarotti, keunes, nabilMaghura, rex07, shubbar | +| Arabic | abuzar3.khalid, badarotti, keunes, MustafaAlgurabi, nabilMaghura, rex07, shubbar | | Asturian (ast_ES) | enolp | | Basque | gaztainalde, keunes, Osoitz, pospolos | | Breton | Belvar, keunes | -| Bulgarian | keunes, solusitor | +| Bulgarian | keunes, ma4ko, solusitor | | Catalan | carles.llacer, dvd1985, exort12, IvanAmarante, javiercoll, keunes, Kintu, lambdani, marcmetallextrem, xc70 | | Chinese (zh_CN) | brnme, cyril3, Felix2yu, gaohongyuan, Guaidaodl, Huck0, iconteral, jhxie, jxj2zzz79pfp9bpo, keunes, kyleehee, molisiye, owen8877, RainSlide, RangerNJU, Sak94664, spice2wolf, tupunco, wongsyrone, yangyang, yiqiok | | Chinese (zh_TW) | bobchao, ijliao, keunes, mapobi, pggdt, ymhuang0808 | -| Czech (cs_CZ) | anotheranonymoususer, elich, Hanzmeister, svetlemodry, Thomaash | -| Danish | JFreak, jhertel, keunes, SebastianKiwiDk, twikedk | +| Czech (cs_CZ) | anotheranonymoususer, elich, Hanzmeister, md.share, svetlemodry, Thomaash | +| Danish | JFreak, jhertel, keunes, petterbejo, SebastianKiwiDk | | Dutch | e2jk, keunes, rwv, Vistaus | | Estonian | Eraser, keunes, mahfiaz | | Finnish | Ban3, keunes, Sahtor | -| French | ChaoticMind, clombion, Cornegidouille, e2jk, keunes, lacouture, LouFex, Matth78, Poussinou, sterylmreep | +| French | ChaoticMind, clombion, Cornegidouille, e2jk, keunes, lacouture, LouFex, Matth78, petterbejo, Poussinou, RomainTT, sterylmreep | | Galician | antiparvos, pikamoku, Raichely | -| German | _Er, ByteHamster, ceving, dadosch, DerSilly, elkangaroo, enz, f_grubm, finsterwalder, hbilke, HolgerJeromin, JoeMcFly, kalei, keunes, mfietz, pudeeh, Quiss42, repat, tomte, tweimer, Willhelm, ypid | +| German | _Er, ByteHamster, ceving, dadosch, DerSilly, elkangaroo, enz, f_grubm, finsterwalder, forght, hbilke, HolgerJeromin, JoeMcFly, kalei, keunes, max.wittig, mfietz, Michael_Strecke, petterbejo, pudeeh, Quiss42, repat, toaskoas, tomte, tweimer, Willhelm, ypid | | Modern Greek (1453-) | AnimaRain, antonist, keunes, pavlosv | | Hebrew (he_IL) | amir.dafnyman, E1i9, mongoose4004, pinkasey, rellieberman, Yaron | | Hindi (hi_IN) | keunes, purple.coder, siddhusengar, thelazyoxymoron | @@ -32,20 +32,20 @@ | Japanese | keunes, KotaKato, Naofumi, sh3llc4t, TranslatorG | | Kannada (kn_IN) | chiraag.nataraj, keunes, thejeshgn | | Ko | changwoo, keunes, libliboom | -| Lithuanian | keunes, naglis | +| Lithuanian | keunes, naglis, Sharper | | Macedonian | krisfremen | | Malayalam | joice, keunes, rashivkp | | Norwegian Bokmål (nb_NO) | abstrakct, ahysing, bablecopherye, corkie, forteller, heraldo, jakobkg, keunes, kongk, sevenmaster, timbast | | Persian | ahangarha, danialbehzadi, ebadi, ebraminio, F7D, hamidrezabayat76, keunes, sinamoghaddas | -| Polish (pl_PL) | befeleme, hiro2020, Iwangelion, keunes, lomapur, mandlus, maniexx, Mephistofeles, shark103, tyle | +| Polish (pl_PL) | befeleme, hiro2020, Iwangelion, kamila.miodek1991, keunes, lomapur, mandlus, maniexx, Mephistofeles, shark103, tyle | | Portuguese | emansije, keunes, smarquespt | -| Portuguese (pt_BR) | alexupits, alysonborges, andersonvom, arua, caioau, carlo_valente, castrors, edman, keunes, lipefire, mbaltar, olivoto, rogervezaro, RubeensVinicius, SamWilliam | +| Portuguese (pt_BR) | alexupits, alysonborges, andersonvom, aracnus, arua, bandreghetti, caioau, carlo_valente, castrors, edman, keunes, lipefire, mbaltar, olivoto, rogervezaro, RubeensVinicius, SamWilliam | | Romanian (ro_RO) | corneliu.e, fuzzmz, keunes, ralienpp | -| Russian (ru_RU) | ashed, btimofeev, Duke_Raven, gammja, homocomputeris, IgorPolyakov, keunes, mercutiy, null, overmind88, Platun0v, PtilopsisLeucotis, s.chebotar, un_logic, Vladryyu, whereisthetea | -| Slovak | ati3, keunes, marulinko, tiborepcek | -| Slovenian (sl_SI) | keunes, panter23 | -| Spanish | AleksSyntek, andersonvom, andrespelaezp, deandreamatias, dvd1985, elojodepajaro, Fitoschido, frandavid100, hard_ware, javiercoll, keunes, LatinSuD, leogrignafini, rafael.osuna, tres.14159, vfmatzkin, wakutiteo | -| Swahili (macrolanguage) | keunes, kmtra | +| Russian (ru_RU) | ashed, btimofeev, Duke_Raven, gammja, homocomputeris, IgorPolyakov, keunes, mercutiy, null, overmind88, Platun0v, PtilopsisLeucotis, s.chebotar, tepxd, un_logic, Vladryyu, whereisthetea | +| Slovak | ati3, jose1711, keunes, marulinko, tiborepcek | +| Slovenian (sl_SI) | asovic, keunes, panter23, trus2 | +| Spanish | AleksSyntek, andersonvom, andrespelaezp, Atreyu94, CaeM0R, deandreamatias, dvd1985, elojodepajaro, Fitoschido, frandavid100, hard_ware, javiercoll, keunes, LatinSuD, leogrignafini, rafael.osuna, tres.14159, vfmatzkin, wakutiteo | +| Swahili (macrolanguage) | 1silvester, keunes, kmtra | | Swedish (sv_SE) | bpnilsson, keunes, nilso, TwoD | | Telugu | keunes, veeven | | Turkish | AhmedDuran, brsata, Erdy, keunes, overbite, Slsdem | diff --git a/README.md b/README.md index c5dd33d7d..8eb13073d 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ You can use the [AntennaPod Forum](https://forum.antennapod.org/) for discussion Bug reports and feature requests can be submitted [here](https://github.com/AntennaPod/AntennaPod/issues) (please read the [instructions](https://github.com/AntennaPod/AntennaPod/blob/master/CONTRIBUTING.md) on how to report a bug and how to submit a feature request first!). ## Help to test AntennaPod -AntennaPod has many users and we don't want them to run into trouble when we add a new feature. It's important that we have a significant group test our app, so that we know all possible combinations of phones, Android versions and use cases work as expected. Check out our wiki on how to join our [Alpha and Beta testing programmes](https://github.com/AntennaPod/AntennaPod/wiki/Help-test-AntennaPod)! +AntennaPod has many users and we don't want them to run into trouble when we add a new feature. It's important that we have a significant group test our app, so that we know all possible combinations of phones, Android versions and use cases work as expected. Check out our wiki on how to join our [Beta testing program](https://antennapod.org/documentation/general/beta)! If a bug is reported during the beta period, chances are high that it will be fixed before the stable version. If it is reported later, fixing might take another full beta cycle. So definitely let us know if something is not right. ## License diff --git a/app/build.gradle b/app/build.gradle index 1d2456dd7..81325a1bf 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -19,11 +19,10 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion // Version code schema: - // "1.2.3-SNAPSHOT" -> 1020300 - // "1.2.3-RC4" -> 1020304 + // "1.2.3-beta4" -> 1020304 // "1.2.3" -> 1020395 - versionCode 2020000 - versionName "2.2.0" + versionCode 2020001 + versionName "2.2.0-beta1" multiDexEnabled false vectorDrawables.useSupportLibrary true @@ -156,14 +155,9 @@ android { } dependencies { - freeImplementation project(":core") - // free build hack: skip some dependencies - if (!doFreeBuild()) { - playImplementation project(":core") - implementation 'com.google.android.play:core:1.8.0' - } else { - System.out.println("app: free build hack, skipping some dependencies") - } + implementation project(":core") + implementation project(':ui:app-start-intent') + implementation project(':ui:common') annotationProcessor "androidx.annotation:annotation:$annotationVersion" implementation "androidx.appcompat:appcompat:$appcompatVersion" @@ -191,14 +185,15 @@ dependencies { implementation "com.joanzapata.iconify:android-iconify-fontawesome:$iconifyVersion" implementation "com.joanzapata.iconify:android-iconify-material:$iconifyVersion" - implementation 'com.yqritc:recyclerview-flexibledivider:1.4.0' implementation 'com.github.shts:TriangleLabelView:1.1.2' - implementation 'com.leinardi.android:speed-dial:3.1.1' + implementation 'com.github.leinardi:FloatingActionButtonSpeedDial:3.1.1' implementation "com.github.AntennaPod:AntennaPod-AudioPlayer:$audioPlayerVersion" implementation 'com.github.mfietz:fyydlin:v0.5.0' implementation 'com.github.ByteHamster:SearchPreference:v2.0.0' implementation 'com.github.skydoves:balloon:1.1.5' + // Non-free dependencies: + playImplementation 'com.google.android.play:core:1.8.0' compileOnly "com.google.android.wearable:wearable:$wearableSupportVersion" androidTestImplementation "org.awaitility:awaitility:$awaitilityVersion" diff --git a/app/src/androidTest/java/de/test/antennapod/EspressoTestUtils.java b/app/src/androidTest/java/de/test/antennapod/EspressoTestUtils.java index 3c8c5d7f0..21498effd 100644 --- a/app/src/androidTest/java/de/test/antennapod/EspressoTestUtils.java +++ b/app/src/androidTest/java/de/test/antennapod/EspressoTestUtils.java @@ -3,8 +3,10 @@ package de.test.antennapod; import android.content.Context; import android.content.Intent; import androidx.annotation.IdRes; +import androidx.annotation.NonNull; import androidx.annotation.StringRes; import androidx.preference.PreferenceManager; +import androidx.test.espresso.NoMatchingViewException; import androidx.test.platform.app.InstrumentationRegistry; import androidx.test.espresso.PerformException; import androidx.test.espresso.UiController; @@ -15,6 +17,9 @@ import androidx.test.espresso.contrib.RecyclerViewActions; import androidx.test.espresso.util.HumanReadables; import androidx.test.espresso.util.TreeIterables; import android.view.View; + +import junit.framework.AssertionFailedError; + import de.danoeh.antennapod.R; import de.danoeh.antennapod.activity.MainActivity; import de.danoeh.antennapod.core.preferences.UserPreferences; @@ -33,6 +38,7 @@ import java.util.concurrent.TimeoutException; import static androidx.test.espresso.Espresso.onView; import static androidx.test.espresso.action.ViewActions.click; +import static androidx.test.espresso.assertion.ViewAssertions.matches; import static androidx.test.espresso.matcher.ViewMatchers.hasDescendant; import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed; import static androidx.test.espresso.matcher.ViewMatchers.isRoot; @@ -57,7 +63,7 @@ public class EspressoTestUtils { @Override public String getDescription() { - return "wait for a specific view for" + millis + " millis."; + return "wait for a specific view for " + millis + " millis."; } @Override @@ -87,6 +93,33 @@ public class EspressoTestUtils { }; } + /** + * Wait until a certain view becomes visible, but at the longest until the timeout. + * Unlike {@link #waitForView(Matcher, long)} it doesn't stick to the initial root view. + * + * @param viewMatcher The view to wait for. + * @param timeoutMillis Maximum waiting period in milliseconds. + * @throws Exception Throws an Exception in case of a timeout. + */ + public static void waitForViewGlobally(@NonNull Matcher viewMatcher, long timeoutMillis) throws Exception { + long startTime = System.currentTimeMillis(); + long endTime = startTime + timeoutMillis; + + do { + try { + onView(viewMatcher).check(matches(isDisplayed())); + // no Exception thrown -> check successful + return; + } catch (NoMatchingViewException | AssertionFailedError ignore) { + // check was not successful "not found" -> continue waiting + } + //noinspection BusyWait + Thread.sleep(50); + } while (System.currentTimeMillis() < endTime); + + throw new Exception("Timeout after " + timeoutMillis + " ms"); + } + /** * Perform action of waiting for a specific view id. * https://stackoverflow.com/a/30338665/ @@ -113,7 +146,7 @@ public class EspressoTestUtils { } /** - * Clear all app databases + * Clear all app databases. */ public static void clearPreferences() { File root = InstrumentationRegistry.getInstrumentation().getTargetContext().getFilesDir().getParentFile(); diff --git a/app/src/androidTest/java/de/test/antennapod/dialogs/ShareDialogTest.java b/app/src/androidTest/java/de/test/antennapod/dialogs/ShareDialogTest.java index 8c628efd5..e31838671 100644 --- a/app/src/androidTest/java/de/test/antennapod/dialogs/ShareDialogTest.java +++ b/app/src/androidTest/java/de/test/antennapod/dialogs/ShareDialogTest.java @@ -11,15 +11,11 @@ import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; -import java.util.List; - import androidx.test.espresso.intent.rule.IntentsTestRule; import androidx.test.platform.app.InstrumentationRegistry; import androidx.test.ext.junit.runners.AndroidJUnit4; import de.danoeh.antennapod.R; import de.danoeh.antennapod.activity.MainActivity; -import de.danoeh.antennapod.core.feed.FeedItem; -import de.danoeh.antennapod.core.storage.DBReader; import de.danoeh.antennapod.fragment.EpisodesFragment; import de.test.antennapod.EspressoTestUtils; import de.test.antennapod.ui.UITestUtils; @@ -70,7 +66,6 @@ public class ShareDialogTest { onView(withText(R.string.all_episodes_short_label)).perform(click()); Matcher allEpisodesMatcher; - final List episodes = DBReader.getRecentlyPublishedEpisodes(0, 10); allEpisodesMatcher = Matchers.allOf(withId(android.R.id.list), isDisplayed(), hasMinimumChildCount(2)); onView(isRoot()).perform(waitForView(allEpisodesMatcher, 1000)); onView(allEpisodesMatcher).perform(actionOnItemAtPosition(0, click())); diff --git a/app/src/androidTest/java/de/test/antennapod/feed/FeedItemTest.java b/app/src/androidTest/java/de/test/antennapod/feed/FeedItemTest.java deleted file mode 100644 index 0b9a67d0a..000000000 --- a/app/src/androidTest/java/de/test/antennapod/feed/FeedItemTest.java +++ /dev/null @@ -1,43 +0,0 @@ -package de.test.antennapod.feed; - -import androidx.test.filters.SmallTest; -import de.danoeh.antennapod.core.feed.FeedItem; -import org.junit.Test; - -import static org.junit.Assert.assertEquals; - -@SmallTest -public class FeedItemTest { - private static final String TEXT_LONG = "Lorem ipsum dolor sit amet, consectetur adipiscing elit."; - private static final String TEXT_SHORT = "Lorem ipsum"; - - /** - * If one of `description` or `content:encoded` is null, use the other one. - */ - @Test - public void testShownotesNullValues() throws Exception { - testShownotes(null, TEXT_LONG); - testShownotes(TEXT_LONG, null); - } - - /** - * If `description` is reasonably longer than `content:encoded`, use `description`. - */ - @Test - public void testShownotesLength() throws Exception { - testShownotes(TEXT_SHORT, TEXT_LONG); - testShownotes(TEXT_LONG, TEXT_SHORT); - } - - /** - * Checks if the shownotes equal TEXT_LONG, using the given `description` and `content:encoded` - * @param description Description of the feed item - * @param contentEncoded `content:encoded` of the feed item - */ - private void testShownotes(String description, String contentEncoded) throws Exception { - FeedItem item = new FeedItem(); - item.setDescription(description); - item.setContentEncoded(contentEncoded); - assertEquals(TEXT_LONG, item.loadShownotes().call()); - } -} diff --git a/app/src/androidTest/java/de/test/antennapod/handler/AtomParserTest.java b/app/src/androidTest/java/de/test/antennapod/handler/AtomParserTest.java deleted file mode 100644 index de9f53ae2..000000000 --- a/app/src/androidTest/java/de/test/antennapod/handler/AtomParserTest.java +++ /dev/null @@ -1,40 +0,0 @@ -package de.test.antennapod.handler; - -import androidx.test.filters.SmallTest; -import de.danoeh.antennapod.core.feed.Feed; -import de.test.antennapod.util.syndication.feedgenerator.AtomGenerator; -import org.junit.Test; -import org.xmlpull.v1.XmlSerializer; - -import java.io.IOException; - -import static org.junit.Assert.assertEquals; - -/** - * Tests for Atom feeds in FeedHandler. - */ -@SmallTest -public class AtomParserTest extends FeedParserTestBase { - @Test - public void testAtomBasic() throws Exception { - Feed f1 = createTestFeed(10, true); - Feed f2 = runFeedTest(f1, new AtomGenerator(), "UTF-8", 0); - feedValid(f1, f2, Feed.TYPE_ATOM1); - } - - @Test - public void testLogoWithWhitespace() throws Exception { - String logo = "https://example.com/image.png"; - Feed f1 = createTestFeed(0, false); - f1.setImageUrl(null); - Feed f2 = runFeedTest(f1, new AtomGenerator() { - @Override - protected void writeAdditionalAttributes(XmlSerializer xml) throws IOException { - xml.startTag(null, "logo"); - xml.text(" " + logo + "\n"); - xml.endTag(null, "logo"); - } - }, "UTF-8", 0); - assertEquals(logo, f2.getImageUrl()); - } -} diff --git a/app/src/androidTest/java/de/test/antennapod/handler/FeedParserTestBase.java b/app/src/androidTest/java/de/test/antennapod/handler/FeedParserTestBase.java deleted file mode 100644 index 83f334633..000000000 --- a/app/src/androidTest/java/de/test/antennapod/handler/FeedParserTestBase.java +++ /dev/null @@ -1,154 +0,0 @@ -package de.test.antennapod.handler; - -import android.content.Context; -import androidx.test.platform.app.InstrumentationRegistry; -import de.danoeh.antennapod.core.feed.Chapter; -import de.danoeh.antennapod.core.feed.Feed; -import de.danoeh.antennapod.core.feed.FeedItem; -import de.danoeh.antennapod.core.feed.FeedMedia; -import de.danoeh.antennapod.core.syndication.handler.FeedHandler; -import de.danoeh.antennapod.core.syndication.handler.UnsupportedFeedtypeException; -import de.test.antennapod.util.syndication.feedgenerator.FeedGenerator; -import org.junit.After; -import org.junit.Before; -import org.xml.sax.SAXException; - -import javax.xml.parsers.ParserConfigurationException; -import java.io.File; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.OutputStream; -import java.util.ArrayList; -import java.util.Date; -import java.util.List; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertTrue; - -/** - * Tests for FeedHandler. - */ -public abstract class FeedParserTestBase { - private static final String FEEDS_DIR = "testfeeds"; - - private File file = null; - private OutputStream outputStream = null; - - @Before - public void setUp() throws Exception { - Context context = InstrumentationRegistry.getInstrumentation().getTargetContext(); - File destDir = context.getExternalFilesDir(FEEDS_DIR); - assertNotNull(destDir); - - file = new File(destDir, "feed.xml"); - file.delete(); - - assertNotNull(file); - assertFalse(file.exists()); - - outputStream = new FileOutputStream(file); - } - - - @After - public void tearDown() throws Exception { - file.delete(); - file = null; - - outputStream.close(); - outputStream = null; - } - - protected Feed runFeedTest(Feed feed, FeedGenerator g, String encoding, long flags) - throws IOException, UnsupportedFeedtypeException, SAXException, ParserConfigurationException { - g.writeFeed(feed, outputStream, encoding, flags); - FeedHandler handler = new FeedHandler(); - Feed parsedFeed = new Feed(feed.getDownload_url(), feed.getLastUpdate()); - parsedFeed.setFile_url(file.getAbsolutePath()); - parsedFeed.setDownloaded(true); - handler.parseFeed(parsedFeed); - return parsedFeed; - } - - protected void feedValid(Feed feed, Feed parsedFeed, String feedType) { - assertEquals(feed.getTitle(), parsedFeed.getTitle()); - if (feedType.equals(Feed.TYPE_ATOM1)) { - assertEquals(feed.getFeedIdentifier(), parsedFeed.getFeedIdentifier()); - } else { - assertEquals(feed.getLanguage(), parsedFeed.getLanguage()); - } - - assertEquals(feed.getLink(), parsedFeed.getLink()); - assertEquals(feed.getDescription(), parsedFeed.getDescription()); - assertEquals(feed.getPaymentLink(), parsedFeed.getPaymentLink()); - assertEquals(feed.getImageUrl(), parsedFeed.getImageUrl()); - - if (feed.getItems() != null) { - assertNotNull(parsedFeed.getItems()); - assertEquals(feed.getItems().size(), parsedFeed.getItems().size()); - - for (int i = 0; i < feed.getItems().size(); i++) { - FeedItem item = feed.getItems().get(i); - FeedItem parsedItem = parsedFeed.getItems().get(i); - - if (item.getItemIdentifier() != null) { - assertEquals(item.getItemIdentifier(), parsedItem.getItemIdentifier()); - } - assertEquals(item.getTitle(), parsedItem.getTitle()); - assertEquals(item.getDescription(), parsedItem.getDescription()); - assertEquals(item.getContentEncoded(), parsedItem.getContentEncoded()); - assertEquals(item.getLink(), parsedItem.getLink()); - assertEquals(item.getPubDate().getTime(), parsedItem.getPubDate().getTime()); - assertEquals(item.getPaymentLink(), parsedItem.getPaymentLink()); - - if (item.hasMedia()) { - assertTrue(parsedItem.hasMedia()); - FeedMedia media = item.getMedia(); - FeedMedia parsedMedia = parsedItem.getMedia(); - - assertEquals(media.getDownload_url(), parsedMedia.getDownload_url()); - assertEquals(media.getSize(), parsedMedia.getSize()); - assertEquals(media.getMime_type(), parsedMedia.getMime_type()); - } - - assertEquals(feed.getImageUrl(), item.getImageLocation()); - - if (item.getChapters() != null) { - assertNotNull(parsedItem.getChapters()); - assertEquals(item.getChapters().size(), parsedItem.getChapters().size()); - List chapters = item.getChapters(); - List parsedChapters = parsedItem.getChapters(); - for (int j = 0; j < chapters.size(); j++) { - Chapter chapter = chapters.get(j); - Chapter parsedChapter = parsedChapters.get(j); - - assertEquals(chapter.getTitle(), parsedChapter.getTitle()); - assertEquals(chapter.getLink(), parsedChapter.getLink()); - } - } - } - } - } - - protected Feed createTestFeed(int numItems, boolean withFeedMedia) { - Feed feed = new Feed(0, null, "title", "http://example.com", "This is the description", - "http://example.com/payment", "Daniel", "en", null, "http://example.com/feed", - "http://example.com/picture", file.getAbsolutePath(), "http://example.com/feed", true); - feed.setItems(new ArrayList<>()); - - for (int i = 0; i < numItems; i++) { - FeedItem item = new FeedItem(0, "item-" + i, "http://example.com/item-" + i, - "http://example.com/items/" + i, new Date(i * 60000), FeedItem.UNPLAYED, feed); - feed.getItems().add(item); - if (withFeedMedia) { - item.setMedia(new FeedMedia(0, item, 4711, 0, 1024 * 1024, "audio/mp3", null, - "http://example.com/media-" + i, false, null, 0, 0)); - } - } - - return feed; - } - -} diff --git a/app/src/androidTest/java/de/test/antennapod/handler/RssParserTest.java b/app/src/androidTest/java/de/test/antennapod/handler/RssParserTest.java deleted file mode 100644 index c2e319233..000000000 --- a/app/src/androidTest/java/de/test/antennapod/handler/RssParserTest.java +++ /dev/null @@ -1,63 +0,0 @@ -package de.test.antennapod.handler; - -import androidx.test.filters.SmallTest; -import de.danoeh.antennapod.core.feed.Feed; -import de.danoeh.antennapod.core.feed.MediaType; -import de.danoeh.antennapod.core.syndication.namespace.NSMedia; -import de.test.antennapod.util.syndication.feedgenerator.Rss2Generator; -import org.junit.Test; -import org.xmlpull.v1.XmlSerializer; - -import java.io.IOException; - -import static org.junit.Assert.assertEquals; - -/** - * Tests for RSS feeds in FeedHandler. - */ -@SmallTest -public class RssParserTest extends FeedParserTestBase { - @Test - public void testRss2Basic() throws Exception { - Feed f1 = createTestFeed(10, true); - Feed f2 = runFeedTest(f1, new Rss2Generator(), "UTF-8", Rss2Generator.FEATURE_WRITE_GUID); - feedValid(f1, f2, Feed.TYPE_RSS2); - } - - @Test - public void testImageWithWhitespace() throws Exception { - String image = "https://example.com/image.png"; - Feed f1 = createTestFeed(0, false); - f1.setImageUrl(null); - Feed f2 = runFeedTest(f1, new Rss2Generator() { - @Override - protected void writeAdditionalAttributes(XmlSerializer xml) throws IOException { - xml.startTag(null, "image"); - xml.startTag(null, "url"); - xml.text(" " + image + "\n"); - xml.endTag(null, "url"); - xml.endTag(null, "image"); - } - }, "UTF-8", 0); - assertEquals(image, f2.getImageUrl()); - } - - @Test - public void testMediaContentMime() throws Exception { - Feed f1 = createTestFeed(0, false); - f1.setImageUrl(null); - Feed f2 = runFeedTest(f1, new Rss2Generator() { - @Override - protected void writeAdditionalAttributes(XmlSerializer xml) throws IOException { - xml.setPrefix(NSMedia.NSTAG, NSMedia.NSURI); - xml.startTag(null, "item"); - xml.startTag(NSMedia.NSURI, "content"); - xml.attribute(null, "url", "https://www.example.com/file.mp4"); - xml.attribute(null, "medium", "video"); - xml.endTag(NSMedia.NSURI, "content"); - xml.endTag(null, "item"); - } - }, "UTF-8", 0); - assertEquals(MediaType.VIDEO, f2.getItems().get(0).getMedia().getMediaType()); - } -} diff --git a/app/src/androidTest/java/de/test/antennapod/playback/PlaybackTest.java b/app/src/androidTest/java/de/test/antennapod/playback/PlaybackTest.java index 419cf2096..e16451763 100644 --- a/app/src/androidTest/java/de/test/antennapod/playback/PlaybackTest.java +++ b/app/src/androidTest/java/de/test/antennapod/playback/PlaybackTest.java @@ -10,6 +10,7 @@ import androidx.test.filters.LargeTest; import androidx.test.platform.app.InstrumentationRegistry; import androidx.test.rule.ActivityTestRule; +import de.danoeh.antennapod.core.feed.FeedItemFilter; import org.awaitility.Awaitility; import org.hamcrest.Matcher; import org.junit.After; @@ -107,7 +108,12 @@ public class PlaybackTest { } private void setupPlaybackController() { - controller = new PlaybackController(activityTestRule.getActivity()); + controller = new PlaybackController(activityTestRule.getActivity()) { + @Override + public void loadMediaInfo() { + // Do nothing + } + }; controller.init(); } @@ -252,7 +258,7 @@ public class PlaybackTest { onView(isRoot()).perform(waitForView(withText(R.string.all_episodes_short_label), 1000)); onView(withText(R.string.all_episodes_short_label)).perform(click()); - final List episodes = DBReader.getRecentlyPublishedEpisodes(0, 10); + final List episodes = DBReader.getRecentlyPublishedEpisodes(0, 10, FeedItemFilter.unfiltered()); Matcher allEpisodesMatcher = allOf(withId(android.R.id.list), isDisplayed(), hasMinimumChildCount(2)); onView(isRoot()).perform(waitForView(allEpisodesMatcher, 1000)); onView(allEpisodesMatcher).perform(actionOnItemAtPosition(0, clickChildViewWithId(R.id.secondaryActionButton))); @@ -287,7 +293,7 @@ public class PlaybackTest { uiTestUtils.addLocalFeedData(true); DBWriter.clearQueue().get(); activityTestRule.launchActivity(new Intent()); - final List episodes = DBReader.getRecentlyPublishedEpisodes(0, 10); + final List episodes = DBReader.getRecentlyPublishedEpisodes(0, 10, FeedItemFilter.unfiltered()); startLocalPlayback(); FeedMedia media = episodes.get(0).getMedia(); diff --git a/app/src/androidTest/java/de/test/antennapod/service/playback/PlaybackServiceTaskManagerTest.java b/app/src/androidTest/java/de/test/antennapod/service/playback/PlaybackServiceTaskManagerTest.java index f039c8bdf..ddd4fe899 100644 --- a/app/src/androidTest/java/de/test/antennapod/service/playback/PlaybackServiceTaskManagerTest.java +++ b/app/src/androidTest/java/de/test/antennapod/service/playback/PlaybackServiceTaskManagerTest.java @@ -6,6 +6,7 @@ import androidx.test.annotation.UiThreadTest; import androidx.test.filters.LargeTest; import de.danoeh.antennapod.core.preferences.SleepTimerPreferences; +import de.danoeh.antennapod.core.widget.WidgetUpdater; import org.awaitility.Awaitility; import org.greenrobot.eventbus.EventBus; import org.junit.After; @@ -187,8 +188,8 @@ public class PlaybackServiceTaskManagerTest { } @Override - public void onWidgetUpdaterTick() { - + public WidgetUpdater.WidgetState requestWidgetState() { + return null; } @Override @@ -248,8 +249,9 @@ public class PlaybackServiceTaskManagerTest { } @Override - public void onWidgetUpdaterTick() { + public WidgetUpdater.WidgetState requestWidgetState() { countDownLatch.countDown(); + return null; } @Override @@ -348,8 +350,8 @@ public class PlaybackServiceTaskManagerTest { } @Override - public void onWidgetUpdaterTick() { - + public WidgetUpdater.WidgetState requestWidgetState() { + return null; } @Override @@ -391,8 +393,8 @@ public class PlaybackServiceTaskManagerTest { } @Override - public void onWidgetUpdaterTick() { - + public WidgetUpdater.WidgetState requestWidgetState() { + return null; } @Override @@ -449,8 +451,8 @@ public class PlaybackServiceTaskManagerTest { } @Override - public void onWidgetUpdaterTick() { - + public WidgetUpdater.WidgetState requestWidgetState() { + return null; } @Override diff --git a/app/src/androidTest/java/de/test/antennapod/storage/AutoDownloadTest.java b/app/src/androidTest/java/de/test/antennapod/storage/AutoDownloadTest.java index 1d2e3d9e8..e74cf49b7 100644 --- a/app/src/androidTest/java/de/test/antennapod/storage/AutoDownloadTest.java +++ b/app/src/androidTest/java/de/test/antennapod/storage/AutoDownloadTest.java @@ -3,13 +3,13 @@ package de.test.antennapod.storage; import android.content.Context; import androidx.annotation.NonNull; import androidx.test.core.app.ApplicationProvider; -import de.danoeh.antennapod.core.ClientConfig; import de.danoeh.antennapod.core.feed.FeedItem; import de.danoeh.antennapod.core.feed.FeedMedia; import de.danoeh.antennapod.core.preferences.PlaybackPreferences; import de.danoeh.antennapod.core.preferences.UserPreferences; import de.danoeh.antennapod.core.storage.AutomaticDownloadAlgorithm; import de.danoeh.antennapod.core.storage.DBReader; +import de.danoeh.antennapod.core.storage.DBTasks; import de.danoeh.antennapod.core.util.playback.PlaybackServiceStarter; import de.test.antennapod.EspressoTestUtils; import de.test.antennapod.ui.UITestUtils; @@ -29,8 +29,7 @@ public class AutoDownloadTest { private Context context; private UITestUtils stubFeedsServer; - - private AutomaticDownloadAlgorithm automaticDownloadAlgorithmOrig; + private StubDownloadAlgorithm stubDownloadAlgorithm; @Before public void setUp() throws Exception { @@ -39,16 +38,19 @@ public class AutoDownloadTest { stubFeedsServer = new UITestUtils(context); stubFeedsServer.setup(); - automaticDownloadAlgorithmOrig = ClientConfig.automaticDownloadAlgorithm; - EspressoTestUtils.clearPreferences(); EspressoTestUtils.clearDatabase(); UserPreferences.setAllowMobileStreaming(true); + + // Setup: enable automatic download + // it is not needed, as the actual automatic download is stubbed. + stubDownloadAlgorithm = new StubDownloadAlgorithm(); + DBTasks.setDownloadAlgorithm(stubDownloadAlgorithm); } @After public void tearDown() throws Exception { - ClientConfig.automaticDownloadAlgorithm = automaticDownloadAlgorithmOrig; + DBTasks.setDownloadAlgorithm(new AutomaticDownloadAlgorithm()); EspressoTestUtils.tryKillPlaybackService(); stubFeedsServer.tearDown(); } @@ -74,11 +76,6 @@ public class AutoDownloadTest { FeedItem item0 = queue.get(0); FeedItem item1 = queue.get(1); - // Setup: enable automatic download - // it is not needed, as the actual automatic download is stubbed. - StubDownloadAlgorithm stubDownloadAlgorithm = new StubDownloadAlgorithm(); - ClientConfig.automaticDownloadAlgorithm = stubDownloadAlgorithm; - // Actual test // Play the first one in the queue playEpisode(item0); @@ -92,11 +89,10 @@ public class AutoDownloadTest { } catch (ConditionTimeoutException cte) { long actual = stubDownloadAlgorithm.getCurrentlyPlayingAtDownload(); fail("when auto download is triggered, the next episode should be playing: (" - + item1.getId() + ", " + item1.getTitle() + ") . " + + item1.getId() + ", " + item1.getTitle() + ") . " + "Actual playing: (" + actual + ")" ); } - } private void playEpisode(@NonNull FeedItem item) { @@ -111,7 +107,7 @@ public class AutoDownloadTest { .until(() -> item.getMedia().getId() == PlaybackPreferences.getCurrentlyPlayingFeedMediaId()); } - private static class StubDownloadAlgorithm implements AutomaticDownloadAlgorithm { + private static class StubDownloadAlgorithm extends AutomaticDownloadAlgorithm { private long currentlyPlaying = -1; @Override diff --git a/app/src/androidTest/java/de/test/antennapod/ui/MainActivityTest.java b/app/src/androidTest/java/de/test/antennapod/ui/MainActivityTest.java index 3f7ebb48b..417a78f02 100644 --- a/app/src/androidTest/java/de/test/antennapod/ui/MainActivityTest.java +++ b/app/src/androidTest/java/de/test/antennapod/ui/MainActivityTest.java @@ -2,16 +2,14 @@ package de.test.antennapod.ui; import android.app.Activity; import android.content.Intent; -import androidx.test.platform.app.InstrumentationRegistry; + import androidx.test.espresso.Espresso; import androidx.test.espresso.intent.rule.IntentsTestRule; import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.platform.app.InstrumentationRegistry; + import com.robotium.solo.Solo; -import de.danoeh.antennapod.R; -import de.danoeh.antennapod.activity.MainActivity; -import de.danoeh.antennapod.core.feed.Feed; -import de.danoeh.antennapod.core.storage.PodDBAdapter; -import de.test.antennapod.EspressoTestUtils; + import org.junit.After; import org.junit.Before; import org.junit.Rule; @@ -20,6 +18,12 @@ import org.junit.runner.RunWith; import java.io.IOException; +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.activity.MainActivity; +import de.danoeh.antennapod.core.feed.Feed; +import de.danoeh.antennapod.core.storage.PodDBAdapter; +import de.test.antennapod.EspressoTestUtils; + import static androidx.test.espresso.Espresso.onView; import static androidx.test.espresso.action.ViewActions.click; import static androidx.test.espresso.action.ViewActions.replaceText; @@ -28,18 +32,17 @@ import static androidx.test.espresso.assertion.ViewAssertions.matches; import static androidx.test.espresso.contrib.ActivityResultMatchers.hasResultCode; import static androidx.test.espresso.matcher.ViewMatchers.hasDescendant; import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed; -import static androidx.test.espresso.matcher.ViewMatchers.isRoot; import static androidx.test.espresso.matcher.ViewMatchers.withId; import static androidx.test.espresso.matcher.ViewMatchers.withText; import static de.test.antennapod.EspressoTestUtils.clickPreference; import static de.test.antennapod.EspressoTestUtils.openNavDrawer; -import static de.test.antennapod.EspressoTestUtils.waitForView; +import static de.test.antennapod.EspressoTestUtils.waitForViewGlobally; import static org.hamcrest.Matchers.allOf; import static org.junit.Assert.assertThat; import static org.junit.Assert.assertTrue; /** - * User interface tests for MainActivity + * User interface tests for MainActivity. */ @RunWith(AndroidJUnit4.class) public class MainActivityTest { @@ -48,19 +51,19 @@ public class MainActivityTest { private UITestUtils uiTestUtils; @Rule - public IntentsTestRule mActivityRule = new IntentsTestRule<>(MainActivity.class, false, false); + public IntentsTestRule activityRule = new IntentsTestRule<>(MainActivity.class, false, false); @Before public void setUp() throws IOException { EspressoTestUtils.clearPreferences(); EspressoTestUtils.clearDatabase(); - mActivityRule.launchActivity(new Intent()); + activityRule.launchActivity(new Intent()); uiTestUtils = new UITestUtils(InstrumentationRegistry.getInstrumentation().getTargetContext()); uiTestUtils.setup(); - solo = new Solo(InstrumentationRegistry.getInstrumentation(), mActivityRule.getActivity()); + solo = new Solo(InstrumentationRegistry.getInstrumentation(), activityRule.getActivity()); } @After @@ -71,6 +74,7 @@ public class MainActivityTest { @Test public void testAddFeed() throws Exception { + // connect to podcast feed uiTestUtils.addHostedFeedData(); final Feed feed = uiTestUtils.hostedFeeds.get(0); openNavDrawer(); @@ -78,9 +82,14 @@ public class MainActivityTest { onView(withId(R.id.addViaUrlButton)).perform(scrollTo(), click()); onView(withId(R.id.urlEditText)).perform(replaceText(feed.getDownload_url())); onView(withText(R.string.confirm_label)).perform(scrollTo(), click()); + + // subscribe podcast Espresso.closeSoftKeyboard(); + waitForViewGlobally(withText(R.string.subscribe_label), 15000); onView(withText(R.string.subscribe_label)).perform(click()); - onView(isRoot()).perform(waitForView(withId(R.id.butShowSettings), 5000)); + + // wait for podcast feed item list + waitForViewGlobally(withId(R.id.butShowSettings), 15000); } @Test @@ -100,7 +109,7 @@ public class MainActivityTest { onView(allOf(withId(R.id.toolbar), isDisplayed())).check( matches(hasDescendant(withText(R.string.subscriptions_label)))); solo.goBack(); - assertThat(mActivityRule.getActivityResult(), hasResultCode(Activity.RESULT_CANCELED)); + assertThat(activityRule.getActivityResult(), hasResultCode(Activity.RESULT_CANCELED)); } @Test @@ -113,7 +122,7 @@ public class MainActivityTest { solo.goBackToActivity(MainActivity.class.getSimpleName()); solo.goBack(); solo.goBack(); - assertTrue(((MainActivity)solo.getCurrentActivity()).isDrawerOpen()); + assertTrue(((MainActivity) solo.getCurrentActivity()).isDrawerOpen()); } @Test @@ -127,7 +136,7 @@ public class MainActivityTest { solo.goBack(); solo.goBack(); solo.goBack(); - assertThat(mActivityRule.getActivityResult(), hasResultCode(Activity.RESULT_CANCELED)); + assertThat(activityRule.getActivityResult(), hasResultCode(Activity.RESULT_CANCELED)); } @Test @@ -142,7 +151,7 @@ public class MainActivityTest { solo.goBack(); onView(withText(R.string.yes)).perform(click()); Thread.sleep(100); - assertThat(mActivityRule.getActivityResult(), hasResultCode(Activity.RESULT_CANCELED)); + assertThat(activityRule.getActivityResult(), hasResultCode(Activity.RESULT_CANCELED)); } @Test @@ -155,6 +164,6 @@ public class MainActivityTest { solo.goBackToActivity(MainActivity.class.getSimpleName()); solo.goBack(); solo.goBack(); - assertThat(mActivityRule.getActivityResult(), hasResultCode(Activity.RESULT_CANCELED)); + assertThat(activityRule.getActivityResult(), hasResultCode(Activity.RESULT_CANCELED)); } } diff --git a/app/src/androidTest/java/de/test/antennapod/ui/PreferencesTest.java b/app/src/androidTest/java/de/test/antennapod/ui/PreferencesTest.java index 3cdb09605..bba546a88 100644 --- a/app/src/androidTest/java/de/test/antennapod/ui/PreferencesTest.java +++ b/app/src/androidTest/java/de/test/antennapod/ui/PreferencesTest.java @@ -15,6 +15,7 @@ import de.danoeh.antennapod.core.storage.APCleanupAlgorithm; import de.danoeh.antennapod.core.storage.APNullCleanupAlgorithm; import de.danoeh.antennapod.core.storage.APQueueCleanupAlgorithm; import de.danoeh.antennapod.core.storage.EpisodeCleanupAlgorithm; +import de.danoeh.antennapod.core.storage.ExceptFavoriteCleanupAlgorithm; import de.danoeh.antennapod.fragment.EpisodesFragment; import de.danoeh.antennapod.fragment.QueueFragment; import de.danoeh.antennapod.fragment.SubscriptionFragment; @@ -371,6 +372,17 @@ public class PreferencesTest { .until(() -> enableAutodownloadOnBattery == UserPreferences.isEnableAutodownloadOnBattery()); } + @Test + public void testEpisodeCleanupFavoriteOnly() { + clickPreference(R.string.network_pref); + onView(withText(R.string.pref_automatic_download_title)).perform(click()); + onView(withText(R.string.pref_episode_cleanup_title)).perform(click()); + onView(isRoot()).perform(waitForView(withText(R.string.episode_cleanup_except_favorite_removal), 1000)); + onView(withText(R.string.episode_cleanup_except_favorite_removal)).perform(click()); + Awaitility.await().atMost(1000, MILLISECONDS) + .until(() -> UserPreferences.getEpisodeCleanupAlgorithm() instanceof ExceptFavoriteCleanupAlgorithm); + } + @Test public void testEpisodeCleanupQueueOnly() { clickPreference(R.string.network_pref); diff --git a/app/src/androidTest/java/de/test/antennapod/ui/SpeedChangeTest.java b/app/src/androidTest/java/de/test/antennapod/ui/SpeedChangeTest.java index 904e17ebf..8027b7dc2 100644 --- a/app/src/androidTest/java/de/test/antennapod/ui/SpeedChangeTest.java +++ b/app/src/androidTest/java/de/test/antennapod/ui/SpeedChangeTest.java @@ -15,6 +15,7 @@ import de.danoeh.antennapod.core.service.playback.PlayerStatus; import de.danoeh.antennapod.core.storage.DBReader; import de.danoeh.antennapod.core.util.playback.PlaybackController; import de.danoeh.antennapod.fragment.QueueFragment; +import de.danoeh.antennapod.ui.appstartintent.MainActivityStarter; import de.test.antennapod.EspressoTestUtils; import de.test.antennapod.IgnoreOnCi; import org.awaitility.Awaitility; @@ -71,8 +72,13 @@ public class SpeedChangeTest { UserPreferences.setPlaybackSpeedArray(Arrays.asList(1.0f, 2.0f, 3.0f)); EspressoTestUtils.tryKillPlaybackService(); - activityRule.launchActivity(new Intent().putExtra(MainActivity.EXTRA_OPEN_PLAYER, true)); - controller = new PlaybackController(activityRule.getActivity()); + activityRule.launchActivity(new Intent().putExtra(MainActivityStarter.EXTRA_OPEN_PLAYER, true)); + controller = new PlaybackController(activityRule.getActivity()) { + @Override + public void loadMediaInfo() { + // Do nothing + } + }; controller.init(); controller.getMedia(); // To load media } diff --git a/app/src/androidTest/java/de/test/antennapod/util/syndication/feedgenerator/AtomGenerator.java b/app/src/androidTest/java/de/test/antennapod/util/syndication/feedgenerator/AtomGenerator.java deleted file mode 100644 index c80e3bbb1..000000000 --- a/app/src/androidTest/java/de/test/antennapod/util/syndication/feedgenerator/AtomGenerator.java +++ /dev/null @@ -1,131 +0,0 @@ -package de.test.antennapod.util.syndication.feedgenerator; - -import android.util.Xml; - -import org.xmlpull.v1.XmlSerializer; - -import java.io.IOException; -import java.io.OutputStream; - -import de.danoeh.antennapod.core.feed.Feed; -import de.danoeh.antennapod.core.feed.FeedItem; -import de.danoeh.antennapod.core.feed.FeedMedia; -import de.danoeh.antennapod.core.util.DateUtils; - -/** - * Creates Atom feeds. See FeedGenerator for more information. - */ -public class AtomGenerator implements FeedGenerator { - - private static final String NS_ATOM = "http://www.w3.org/2005/Atom"; - - private static final long FEATURE_USE_RFC3339LOCAL = 1; - - @Override - public void writeFeed(Feed feed, OutputStream outputStream, String encoding, long flags) throws IOException { - if (feed == null) throw new IllegalArgumentException("feed = null"); - if (outputStream == null) throw new IllegalArgumentException("outputStream = null"); - if (encoding == null) throw new IllegalArgumentException("encoding = null"); - - XmlSerializer xml = Xml.newSerializer(); - xml.setOutput(outputStream, encoding); - xml.startDocument(encoding, null); - - xml.startTag(null, "feed"); - xml.attribute(null, "xmlns", NS_ATOM); - - // Write Feed data - if (feed.getIdentifyingValue() != null) { - xml.startTag(null, "id"); - xml.text(feed.getIdentifyingValue()); - xml.endTag(null, "id"); - } - if (feed.getTitle() != null) { - xml.startTag(null, "title"); - xml.text(feed.getTitle()); - xml.endTag(null, "title"); - } - if (feed.getLink() != null) { - xml.startTag(null, "link"); - xml.attribute(null, "rel", "alternate"); - xml.attribute(null, "href", feed.getLink()); - xml.endTag(null, "link"); - } - if (feed.getDescription() != null) { - xml.startTag(null, "subtitle"); - xml.text(feed.getDescription()); - xml.endTag(null, "subtitle"); - } - if (feed.getImageUrl() != null) { - xml.startTag(null, "logo"); - xml.text(feed.getImageUrl()); - xml.endTag(null, "logo"); - } - - if (feed.getPaymentLink() != null) { - GeneratorUtil.addPaymentLink(xml, feed.getPaymentLink(), false); - } - - // Write FeedItem data - if (feed.getItems() != null) { - for (FeedItem item : feed.getItems()) { - xml.startTag(null, "entry"); - - if (item.getIdentifyingValue() != null) { - xml.startTag(null, "id"); - xml.text(item.getIdentifyingValue()); - xml.endTag(null, "id"); - } - if (item.getTitle() != null) { - xml.startTag(null, "title"); - xml.text(item.getTitle()); - xml.endTag(null, "title"); - } - if (item.getLink() != null) { - xml.startTag(null, "link"); - xml.attribute(null, "rel", "alternate"); - xml.attribute(null, "href", item.getLink()); - xml.endTag(null, "link"); - } - if (item.getPubDate() != null) { - xml.startTag(null, "published"); - if ((flags & FEATURE_USE_RFC3339LOCAL) != 0) { - xml.text(DateUtils.formatRFC3339Local(item.getPubDate())); - } else { - xml.text(DateUtils.formatRFC3339UTC(item.getPubDate())); - } - xml.endTag(null, "published"); - } - if (item.getDescription() != null) { - xml.startTag(null, "content"); - xml.text(item.getDescription()); - xml.endTag(null, "content"); - } - if (item.getMedia() != null) { - FeedMedia media = item.getMedia(); - xml.startTag(null, "link"); - xml.attribute(null, "rel", "enclosure"); - xml.attribute(null, "href", media.getDownload_url()); - xml.attribute(null, "type", media.getMime_type()); - xml.attribute(null, "length", String.valueOf(media.getSize())); - xml.endTag(null, "link"); - } - - if (item.getPaymentLink() != null) { - GeneratorUtil.addPaymentLink(xml, item.getPaymentLink(), false); - } - - xml.endTag(null, "entry"); - } - } - - writeAdditionalAttributes(xml); - - xml.endTag(null, "feed"); - xml.endDocument(); - } - - protected void writeAdditionalAttributes(XmlSerializer xml) throws IOException { - - } -} diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index fb205b1c3..697624337 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -60,7 +60,8 @@ + android:configChanges="keyboardHidden|orientation|screenSize" + android:exported="true"> @@ -83,11 +84,39 @@ android:configChanges="keyboardHidden|orientation|screenSize|smallestScreenSize|screenLayout|density|uiMode|keyboard|navigation" android:windowSoftInputMode="stateAlwaysHidden" android:launchMode="singleTask" - android:label="@string/app_name"> + android:label="@string/app_name" + android:exported="true"> + + + + + + + + + + + + + + + + + + + + + android:label="@string/widget_settings" + android:exported="true"> @@ -114,21 +144,21 @@ android:exported="false"> - + - - - + @@ -136,7 +166,8 @@ + android:label="@string/opml_import_label" + android:exported="true"> @@ -185,18 +216,14 @@ android:name=".activity.VideoplayerActivity" android:configChanges="keyboardHidden|orientation|screenSize|screenLayout|smallestScreenSize" android:supportsPictureInPicture="true" - android:screenOrientation="sensorLandscape"> + android:screenOrientation="sensorLandscape" + android:exported="false"> - - - - - - - + + @@ -204,7 +231,8 @@ android:name=".activity.OnlineFeedViewActivity" android:configChanges="orientation|screenSize" android:theme="@style/Theme.AntennaPod.Dark.Translucent" - android:label="@string/add_feed_label"> + android:label="@string/add_feed_label" + android:exported="true"> @@ -292,33 +320,26 @@ - - - - - - - - - + - + - + @@ -333,6 +354,10 @@ android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/provider_paths"/> + + diff --git a/app/src/main/assets/developers.csv b/app/src/main/assets/developers.csv index a2c54723d..77269ba97 100644 --- a/app/src/main/assets/developers.csv +++ b/app/src/main/assets/developers.csv @@ -4,14 +4,14 @@ mfietz;6860662;Maintainer (retired) TomHennen;5216560;Maintainer (retired) orionlee;250644;Contributor domingos86;9538859;Contributor +damoasda;46045854;Contributor tonytamsf;149837;Contributor andersonvom;69922;Contributor -damoasda;46045854;Contributor TacoTheDank;32376686;Contributor shortspider;5712543;Contributor +spacecowboy;223655;Contributor ebraminio;833473;Contributor asdoi;36813904;Contributor -spacecowboy;223655;Contributor patheticpat;16046;Contributor brad;1614;Contributor Cj-Malone;10121513;Contributor @@ -44,6 +44,7 @@ mchelen;30691;Contributor dethstar;1239177;Contributor drabux;10663142;Contributor saqura;1935380;Contributor +binarytoto;75904760;Contributor bibz;5141956;Contributor hzulla;1705654;Contributor deandreamatias;21011641;Contributor @@ -65,6 +66,7 @@ HrBDev;25826502;Contributor HolgerJeromin;2410353;Contributor xisberto;1914956;Contributor jmue;898577;Contributor +jonasburian;15125616;Contributor katrinleinweber;9948149;Contributor LatinSuD;451487;Contributor 24hours;650407;Contributor @@ -74,25 +76,27 @@ archibishop;36948493;Contributor alifeflow;24603829;Contributor avirajrsingh;69088913;Contributor toggles;14695;Contributor +connectety;26038710;Contributor matdb;48329535;Contributor damlayildiz;56313500;Contributor kingargyle;177042;Contributor dsmith47;14109426;Contributor hannesaa2;18496079;Contributor jhunnius;9149031;Contributor +a1291762;327162;Contributor ShadowIce;59123;Contributor Niffler;8172446;Contributor raghulj;57007;Contributor raghulrm;5362986;Contributor mamehacker;16738348;Contributor skitt;2128935;Contributor +Thom-Merrilin;76849828;Contributor wseemann;2296196;Contributor markamaze;17114678;Contributor mohitshah3111999;42018918;Contributor moralesg;14352147;Contributor mr-intj;6268767;Contributor tuxayo;2678215;Contributor -schlch;56929215;Contributor alimemonzx;44647595;Contributor dev-darrell;52300159;Contributor jmdouglas;10855634;Contributor @@ -106,24 +110,27 @@ arantius;84729;Contributor BoJacobs;25435640;Contributor chetan882777;36985543;Contributor chrissicool;232590;Contributor +britiger;2057760;Contributor cszucko;1810383;Contributor CWftw;1498303;Contributor -connectety;26038710;Contributor danielm5;66779;Contributor ariedov;958646;Contributor brettle;118192;Contributor edwinhere;19705425;Contributor eirikv;4076243;Contributor eerden;277513;Contributor +Geist5000;37940313;Contributor jklippel;8657220;Contributor jannic;232606;Contributor Foso;5015532;Contributor Kaligule;3586246;Contributor kvithayathil;1056073;Contributor luiscruz;1080714;Contributor +MStrecke;5202211;Contributor mlasson;5814258;Contributor schwedenmut;9077622;Contributor M-arcel;56698158;Contributor +mgborowiec;29843126;Contributor msoose;30473690;Contributor mo;7117;Contributor mdeveloper20;2319126;Contributor @@ -138,6 +145,7 @@ ortylp;470439;Contributor ramzan;55637406;Contributor iamrichR;44210678;Contributor SamWhited;512573;Contributor +SebiderSushi;23618858;Contributor selivan;1208989;Contributor sonnayasomnambula;7716779;Contributor sethoscope;534043;Contributor @@ -148,14 +156,17 @@ vimsick;20211590;Contributor lyallemma;25173082;Contributor edent;837136;Contributor atrus6;357881;Contributor +timakro;8438790;Contributor heyyviv;56256802;Contributor waylife;3348620;Contributor +yarons;406826;Contributor amhokies;3124968;Contributor andrewc1;19559401;Contributor axq;5077221;Contributor -binarytoto;75904760;Contributor chrk2205;44704035;Contributor fossterer;4236021;Contributor lightonflux;1377943;Contributor minusf;3632883;Contributor +s3lph;5564491;Contributor +tamizh138;26201258;Contributor zawad2221;32180355;Contributor diff --git a/app/src/main/assets/licenses.xml b/app/src/main/assets/licenses.xml index b6e12cf54..aa0ad740b 100644 --- a/app/src/main/assets/licenses.xml +++ b/app/src/main/assets/licenses.xml @@ -90,12 +90,6 @@ website="https://github.com/square/okio" license="Apache 2.0" licenseText="LICENSE_APACHE-2.0.txt" /> - { + request.setUsername(username); + request.setPassword(password); - if (savedInstanceState != null) { - etxtUsername.setText(savedInstanceState.getString("username")); - etxtPassword.setText(savedInstanceState.getString("password")); - } - - butConfirm.setOnClickListener(v -> { - String username = etxtUsername.getText().toString(); - String password = etxtPassword.getText().toString(); - request.setUsername(username); - request.setPassword(password); - Intent result = new Intent(); - result.putExtra(RESULT_REQUEST, request); - setResult(Activity.RESULT_OK, result); - - if (sendToDownloadRequester) { - DownloadRequester.getInstance().download(DownloadAuthenticationActivity.this, request); + if (request.getFeedfileType() == FeedMedia.FEEDFILETYPE_FEEDMEDIA) { + long mediaId = request.getFeedfileId(); + FeedMedia media = DBReader.getFeedMedia(mediaId); + if (media != null) { + FeedPreferences preferences = media.getItem().getFeed().getPreferences(); + if (TextUtils.isEmpty(preferences.getPassword()) + || TextUtils.isEmpty(preferences.getUsername())) { + preferences.setUsername(username); + preferences.setPassword(password); + DBWriter.setFeedPreferences(preferences); + } + } + } + }) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(() -> { + DownloadRequester.getInstance().download(DownloadAuthenticationActivity.this, request); + finish(); + }); } - finish(); - }); - butCancel.setOnClickListener(v -> { - setResult(Activity.RESULT_CANCELED); - finish(); - }); - - } - - @Override - protected void onSaveInstanceState(@NonNull Bundle outState) { - super.onSaveInstanceState(outState); - outState.putString("username", etxtUsername.getText().toString()); - outState.putString("password", etxtPassword.getText().toString()); + @Override + protected void onCancelled() { + finish(); + } + }.show(); } } diff --git a/app/src/main/java/de/danoeh/antennapod/activity/MainActivity.java b/app/src/main/java/de/danoeh/antennapod/activity/MainActivity.java index d1716e009..b5edcc878 100644 --- a/app/src/main/java/de/danoeh/antennapod/activity/MainActivity.java +++ b/app/src/main/java/de/danoeh/antennapod/activity/MainActivity.java @@ -6,6 +6,7 @@ import android.content.Intent; import android.content.SharedPreferences; import android.content.res.Configuration; import android.media.AudioManager; +import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.os.Handler; @@ -40,7 +41,7 @@ import de.danoeh.antennapod.core.preferences.UserPreferences; import de.danoeh.antennapod.core.receiver.MediaButtonReceiver; import de.danoeh.antennapod.core.service.playback.PlaybackService; import de.danoeh.antennapod.core.util.StorageUtils; -import de.danoeh.antennapod.core.util.ThemeUtils; +import de.danoeh.antennapod.ui.common.ThemeUtils; import de.danoeh.antennapod.core.util.download.AutoUpdateManager; import de.danoeh.antennapod.dialog.RatingDialog; import de.danoeh.antennapod.fragment.AddFeedFragment; @@ -51,9 +52,11 @@ import de.danoeh.antennapod.fragment.FeedItemlistFragment; import de.danoeh.antennapod.fragment.NavDrawerFragment; import de.danoeh.antennapod.fragment.PlaybackHistoryFragment; import de.danoeh.antennapod.fragment.QueueFragment; +import de.danoeh.antennapod.fragment.SearchFragment; import de.danoeh.antennapod.fragment.SubscriptionFragment; import de.danoeh.antennapod.fragment.TransitionEffect; import de.danoeh.antennapod.preferences.PreferenceUpgrader; +import de.danoeh.antennapod.ui.appstartintent.MainActivityStarter; import de.danoeh.antennapod.view.LockableBottomSheetBehavior; import org.apache.commons.lang3.ArrayUtils; import org.apache.commons.lang3.Validate; @@ -75,7 +78,6 @@ public class MainActivity extends CastEnabledActivity { public static final String EXTRA_FRAGMENT_TAG = "fragment_tag"; public static final String EXTRA_FRAGMENT_ARGS = "fragment_args"; public static final String EXTRA_FEED_ID = "fragment_feed_id"; - public static final String EXTRA_OPEN_PLAYER = "open_player"; public static final String EXTRA_REFRESH_ON_START = "refresh_on_start"; public static final String EXTRA_STARTED_FROM_SEARCH = "started_from_search"; public static final String KEY_GENERATED_VIEW_ID = "generated_view_id"; @@ -184,16 +186,16 @@ public class MainActivity extends CastEnabledActivity { } }; - public void setupToolbarToggle(@Nullable Toolbar toolbar) { + public void setupToolbarToggle(@NonNull Toolbar toolbar, boolean displayUpArrow) { if (drawerLayout != null) { // Tablet layout does not have a drawer drawerLayout.removeDrawerListener(drawerToggle); drawerToggle = new ActionBarDrawerToggle(this, drawerLayout, toolbar, R.string.drawer_open, R.string.drawer_close); drawerLayout.addDrawerListener(drawerToggle); drawerToggle.syncState(); - drawerToggle.setDrawerIndicatorEnabled(getSupportFragmentManager().getBackStackEntryCount() == 0); + drawerToggle.setDrawerIndicatorEnabled(!displayUpArrow); drawerToggle.setToolbarNavigationClickListener(v -> getSupportFragmentManager().popBackStack()); - } else if (getSupportFragmentManager().getBackStackEntryCount() == 0) { + } else if (!displayUpArrow) { toolbar.setNavigationIcon(null); } else { toolbar.setNavigationIcon(ThemeUtils.getDrawableFromAttr(this, R.attr.homeAsUpIndicator)); @@ -508,9 +510,11 @@ public class MainActivity extends CastEnabledActivity { } } sheetBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED); - } else if (intent.getBooleanExtra(EXTRA_OPEN_PLAYER, false)) { + } else if (intent.getBooleanExtra(MainActivityStarter.EXTRA_OPEN_PLAYER, false)) { sheetBehavior.setState(BottomSheetBehavior.STATE_EXPANDED); bottomSheetCallback.onSlide(null, 1.0f); + } else if (Intent.ACTION_VIEW.equals(intent.getAction())) { + handleDeeplink(intent.getData()); } // to avoid handling the intent twice when the configuration changes setIntent(new Intent(MainActivity.this, MainActivity.class)); @@ -520,6 +524,7 @@ public class MainActivity extends CastEnabledActivity { protected void onNewIntent(Intent intent) { super.onNewIntent(intent); setIntent(intent); + handleNavIntent(); } public Snackbar showSnackbarAbovePlayer(CharSequence text, int duration) { @@ -540,6 +545,59 @@ public class MainActivity extends CastEnabledActivity { return showSnackbarAbovePlayer(getResources().getText(text), duration); } + /** + * Handles the deep link incoming via App Actions. + * Performs an in-app search or opens the relevant feature of the app + * depending on the query. + * + * @param uri incoming deep link + */ + private void handleDeeplink(Uri uri) { + if (uri == null || uri.getPath() == null) { + return; + } + Log.d(TAG, "Handling deeplink: " + uri.toString()); + switch (uri.getPath()) { + case "/deeplink/search": + String query = uri.getQueryParameter("query"); + if (query == null) { + return; + } + + this.loadChildFragment(SearchFragment.newInstance(query)); + break; + case "/deeplink/main": + String feature = uri.getQueryParameter("page"); + if (feature == null) { + return; + } + switch (feature) { + case "DOWNLOADS": + loadFragment(DownloadsFragment.TAG, null); + break; + case "HISTORY": + loadFragment(PlaybackHistoryFragment.TAG, null); + break; + case "EPISODES": + loadFragment(EpisodesFragment.TAG, null); + break; + case "QUEUE": + loadFragment(QueueFragment.TAG, null); + break; + case "SUBSCRIPTIONS": + loadFragment(SubscriptionFragment.TAG, null); + break; + default: + showSnackbarAbovePlayer(getString(R.string.app_action_not_found, feature), + Snackbar.LENGTH_LONG); + return; + } + break; + default: + break; + } + } + //Hardware keyboard support @Override public boolean onKeyUp(int keyCode, KeyEvent event) { @@ -592,5 +650,4 @@ public class MainActivity extends CastEnabledActivity { } return super.onKeyUp(keyCode, event); } - } diff --git a/app/src/main/java/de/danoeh/antennapod/activity/MediaplayerActivity.java b/app/src/main/java/de/danoeh/antennapod/activity/MediaplayerActivity.java index deb2fe0db..56a66ba93 100644 --- a/app/src/main/java/de/danoeh/antennapod/activity/MediaplayerActivity.java +++ b/app/src/main/java/de/danoeh/antennapod/activity/MediaplayerActivity.java @@ -1,10 +1,8 @@ package de.danoeh.antennapod.activity; -import android.Manifest; import android.annotation.TargetApi; import android.content.Intent; import android.content.SharedPreferences; -import android.content.pm.PackageManager; import android.graphics.PixelFormat; import android.os.Build; import android.os.Bundle; @@ -17,7 +15,6 @@ import android.widget.ImageButton; import android.widget.SeekBar; import android.widget.SeekBar.OnSeekBarChangeListener; import android.widget.TextView; -import android.widget.Toast; import com.bumptech.glide.Glide; @@ -28,17 +25,16 @@ import org.greenrobot.eventbus.ThreadMode; import java.text.NumberFormat; -import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.AlertDialog; -import androidx.core.app.ActivityCompat; +import androidx.cardview.widget.CardView; import androidx.core.app.ActivityOptionsCompat; -import androidx.core.content.ContextCompat; +import androidx.interpolator.view.animation.FastOutSlowInInterpolator; + import de.danoeh.antennapod.R; import de.danoeh.antennapod.core.event.PlaybackPositionEvent; import de.danoeh.antennapod.core.feed.FeedItem; import de.danoeh.antennapod.core.feed.FeedMedia; -import de.danoeh.antennapod.core.feed.MediaType; import de.danoeh.antennapod.core.preferences.UserPreferences; import de.danoeh.antennapod.core.service.playback.PlaybackService; import de.danoeh.antennapod.core.storage.DBReader; @@ -50,11 +46,9 @@ import de.danoeh.antennapod.core.util.ShareUtils; import de.danoeh.antennapod.core.util.StorageUtils; import de.danoeh.antennapod.core.util.TimeSpeedConverter; import de.danoeh.antennapod.core.util.gui.PictureInPictureUtil; -import de.danoeh.antennapod.core.util.playback.ExternalMedia; import de.danoeh.antennapod.core.util.playback.MediaPlayerError; import de.danoeh.antennapod.core.util.playback.Playable; import de.danoeh.antennapod.core.util.playback.PlaybackController; -import de.danoeh.antennapod.core.util.playback.PlaybackServiceStarter; import de.danoeh.antennapod.dialog.PlaybackControlsDialog; import de.danoeh.antennapod.dialog.ShareDialog; import de.danoeh.antennapod.dialog.SkipPreferenceDialog; @@ -64,7 +58,6 @@ import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.disposables.Disposable; import io.reactivex.schedulers.Schedulers; - /** * Provides general features which are both needed for playing audio and video * files. @@ -72,9 +65,6 @@ import io.reactivex.schedulers.Schedulers; public abstract class MediaplayerActivity extends CastEnabledActivity implements OnSeekBarChangeListener { private static final String TAG = "MediaplayerActivity"; private static final String PREFS = "MediaPlayerActivityPreferences"; - private static final String PREF_SHOW_TIME_LEFT = "showTimeLeft"; - private static final int REQUEST_CODE_STORAGE_PLAY_VIDEO = 42; - private static final int REQUEST_CODE_STORAGE_PLAY_AUDIO = 43; PlaybackController controller; @@ -87,6 +77,8 @@ public abstract class MediaplayerActivity extends CastEnabledActivity implements private ImageButton butFF; private TextView txtvFF; private ImageButton butSkip; + private CardView cardViewSeek; + private TextView txtvSeek; private boolean showTimeLeft = false; @@ -96,12 +88,6 @@ public abstract class MediaplayerActivity extends CastEnabledActivity implements private PlaybackController newPlaybackController() { return new PlaybackController(this) { - - @Override - public void setupGUI() { - MediaplayerActivity.this.setupGUI(); - } - @Override public void onPositionObserverUpdate() { MediaplayerActivity.this.onPositionObserverUpdate(); @@ -143,8 +129,8 @@ public abstract class MediaplayerActivity extends CastEnabledActivity implements } @Override - public boolean loadMediaInfo() { - return MediaplayerActivity.this.loadMediaInfo(); + public void loadMediaInfo() { + MediaplayerActivity.this.loadMediaInfo(); } @Override @@ -467,17 +453,15 @@ public abstract class MediaplayerActivity extends CastEnabledActivity implements * to the PlaybackService to ensure that the activity has the right * FeedMedia object. */ - boolean loadMediaInfo() { + void loadMediaInfo() { Log.d(TAG, "loadMediaInfo()"); - if(controller == null || controller.getMedia() == null) { - return false; + if (controller == null || controller.getMedia() == null) { + return; } - SharedPreferences prefs = getSharedPreferences(PREFS, MODE_PRIVATE); - showTimeLeft = prefs.getBoolean(PREF_SHOW_TIME_LEFT, false); + showTimeLeft = UserPreferences.shouldShowRemainingTime(); onPositionObserverUpdate(); checkFavorite(); updatePlaybackSpeedButton(); - return true; } void updatePlaybackSpeedButton() { @@ -492,9 +476,11 @@ public abstract class MediaplayerActivity extends CastEnabledActivity implements setContentView(getContentViewResourceId()); sbPosition = findViewById(R.id.sbPosition); txtvPosition = findViewById(R.id.txtvPosition); + cardViewSeek = findViewById(R.id.cardViewSeek); + txtvSeek = findViewById(R.id.txtvSeek); SharedPreferences prefs = getSharedPreferences(PREFS, MODE_PRIVATE); - showTimeLeft = prefs.getBoolean(PREF_SHOW_TIME_LEFT, false); + showTimeLeft = UserPreferences.shouldShowRemainingTime(); Log.d("timeleft", showTimeLeft ? "true" : "false"); txtvLength = findViewById(R.id.txtvLength); if (txtvLength != null) { @@ -518,9 +504,7 @@ public abstract class MediaplayerActivity extends CastEnabledActivity implements } txtvLength.setText(length); - SharedPreferences.Editor editor = prefs.edit(); - editor.putBoolean(PREF_SHOW_TIME_LEFT, showTimeLeft); - editor.apply(); + UserPreferences.setShowRemainTimeSetting(showTimeLeft); Log.d("timeleft on click", showTimeLeft ? "true" : "false"); }); } @@ -618,21 +602,21 @@ public abstract class MediaplayerActivity extends CastEnabledActivity implements } if (fromUser) { prog = progress / ((float) seekBar.getMax()); - int duration = controller.getDuration(); TimeSpeedConverter converter = new TimeSpeedConverter(controller.getCurrentPlaybackSpeedMultiplier()); - int position = converter.convert((int) (prog * duration)); - txtvPosition.setText(Converter.getDurationStringLong(position)); - - if (showTimeLeft) { - int timeLeft = converter.convert(duration - (int) (prog * duration)); - txtvLength.setText("-" + Converter.getDurationStringLong(timeLeft)); - } + int position = converter.convert((int) (prog * controller.getDuration())); + txtvSeek.setText(Converter.getDurationStringLong(position)); } } @Override public void onStartTrackingTouch(SeekBar seekBar) { - + cardViewSeek.setScaleX(.8f); + cardViewSeek.setScaleY(.8f); + cardViewSeek.animate() + .setInterpolator(new FastOutSlowInInterpolator()) + .alpha(1f).scaleX(1f).scaleY(1f) + .setDuration(200) + .start(); } @Override @@ -640,6 +624,13 @@ public abstract class MediaplayerActivity extends CastEnabledActivity implements if (controller != null) { controller.seekTo((int) (prog * controller.getDuration())); } + cardViewSeek.setScaleX(1f); + cardViewSeek.setScaleY(1f); + cardViewSeek.animate() + .setInterpolator(new FastOutSlowInInterpolator()) + .alpha(0f).scaleX(.8f).scaleY(.8f) + .setDuration(200) + .start(); } private void checkFavorite() { @@ -663,50 +654,6 @@ public abstract class MediaplayerActivity extends CastEnabledActivity implements }, error -> Log.e(TAG, Log.getStackTraceString(error))); } - void playExternalMedia(Intent intent, MediaType type) { - if (intent == null || intent.getData() == null) { - return; - } - if (Build.VERSION.SDK_INT >= 23 - && ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) - != PackageManager.PERMISSION_GRANTED) { - - if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.READ_EXTERNAL_STORAGE)) { - Toast.makeText(this, R.string.needs_storage_permission, Toast.LENGTH_LONG).show(); - } - - int code = REQUEST_CODE_STORAGE_PLAY_AUDIO; - if (type == MediaType.VIDEO) { - code = REQUEST_CODE_STORAGE_PLAY_VIDEO; - } - ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.READ_EXTERNAL_STORAGE}, code); - return; - } - - Log.d(TAG, "Received VIEW intent: " + intent.getData().getPath()); - ExternalMedia media = new ExternalMedia(intent.getData().getPath(), type); - - new PlaybackServiceStarter(this, media) - .callEvenIfRunning(true) - .startWhenPrepared(true) - .shouldStream(false) - .prepareImmediately(true) - .start(); - } - - @Override - public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, int[] grantResults) { - if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { - if (requestCode == REQUEST_CODE_STORAGE_PLAY_AUDIO) { - playExternalMedia(getIntent(), MediaType.AUDIO); - } else if (requestCode == REQUEST_CODE_STORAGE_PLAY_VIDEO) { - playExternalMedia(getIntent(), MediaType.VIDEO); - } - } else { - Toast.makeText(this, R.string.needs_storage_permission, Toast.LENGTH_LONG).show(); - } - } - @Nullable private static FeedItem getFeedItem(@Nullable Playable playable) { if (playable instanceof FeedMedia) { diff --git a/app/src/main/java/de/danoeh/antennapod/activity/OnlineFeedViewActivity.java b/app/src/main/java/de/danoeh/antennapod/activity/OnlineFeedViewActivity.java index 18620a56a..a5883ca14 100644 --- a/app/src/main/java/de/danoeh/antennapod/activity/OnlineFeedViewActivity.java +++ b/app/src/main/java/de/danoeh/antennapod/activity/OnlineFeedViewActivity.java @@ -4,10 +4,14 @@ import android.app.Dialog; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; +import android.content.SharedPreferences; import android.graphics.LightingColorFilter; import android.os.Build; import android.os.Bundle; +import android.text.Spannable; +import android.text.SpannableString; import android.text.TextUtils; +import android.text.style.ForegroundColorSpan; import android.util.Log; import android.view.MenuItem; import android.view.View; @@ -15,6 +19,7 @@ import android.widget.AdapterView; import android.widget.ArrayAdapter; import android.widget.TextView; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.annotation.UiThread; import androidx.appcompat.app.AlertDialog; import androidx.appcompat.app.AppCompatActivity; @@ -28,7 +33,6 @@ import de.danoeh.antennapod.core.event.DownloadEvent; import de.danoeh.antennapod.core.event.FeedListUpdateEvent; import de.danoeh.antennapod.core.event.PlayerStatusEvent; import de.danoeh.antennapod.core.feed.Feed; -import de.danoeh.antennapod.core.feed.FeedItem; import de.danoeh.antennapod.core.feed.FeedPreferences; import de.danoeh.antennapod.core.feed.VolumeAdaptionSetting; import de.danoeh.antennapod.core.glide.ApGlideSettings; @@ -41,6 +45,7 @@ import de.danoeh.antennapod.core.service.download.Downloader; import de.danoeh.antennapod.core.service.download.HttpDownloader; import de.danoeh.antennapod.core.service.playback.PlaybackService; import de.danoeh.antennapod.core.storage.DBReader; +import de.danoeh.antennapod.core.storage.DBWriter; import de.danoeh.antennapod.core.storage.DownloadRequestException; import de.danoeh.antennapod.core.storage.DownloadRequester; import de.danoeh.antennapod.core.syndication.handler.FeedHandler; @@ -49,7 +54,6 @@ import de.danoeh.antennapod.core.syndication.handler.UnsupportedFeedtypeExceptio import de.danoeh.antennapod.core.util.DownloadError; import de.danoeh.antennapod.core.util.FileNameGenerator; import de.danoeh.antennapod.core.util.IntentUtils; -import de.danoeh.antennapod.core.util.Optional; import de.danoeh.antennapod.core.util.StorageUtils; import de.danoeh.antennapod.core.util.URLChecker; import de.danoeh.antennapod.core.util.playback.RemoteMedia; @@ -58,9 +62,11 @@ import de.danoeh.antennapod.core.util.syndication.HtmlToPlainText; import de.danoeh.antennapod.databinding.OnlinefeedviewActivityBinding; import de.danoeh.antennapod.dialog.AuthenticationDialog; import de.danoeh.antennapod.discovery.PodcastSearcherRegistry; +import io.reactivex.Maybe; import io.reactivex.Observable; import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.disposables.Disposable; +import io.reactivex.observers.DisposableMaybeObserver; import io.reactivex.schedulers.Schedulers; import org.apache.commons.lang3.StringUtils; import org.greenrobot.eventbus.EventBus; @@ -87,6 +93,9 @@ public class OnlineFeedViewActivity extends AppCompatActivity { // Optional argument: specify a title for the actionbar. private static final int RESULT_ERROR = 2; private static final String TAG = "OnlineFeedViewActivity"; + private static final String PREFS = "OnlineFeedViewActivityPreferences"; + private static final String PREF_LAST_AUTO_DOWNLOAD = "lastAutoDownload"; + private volatile List feeds; private Feed feed; private String selectedDownloadUrl; @@ -248,7 +257,8 @@ public class OnlineFeedViewActivity extends AppCompatActivity { url = URLChecker.prepareURL(url); feed = new Feed(url, null); if (username != null && password != null) { - feed.setPreferences(new FeedPreferences(0, false, FeedPreferences.AutoDeleteAction.GLOBAL, VolumeAdaptionSetting.OFF, username, password)); + feed.setPreferences(new FeedPreferences(0, false, FeedPreferences.AutoDeleteAction.GLOBAL, + VolumeAdaptionSetting.OFF, username, password)); } String fileUrl = new File(getExternalCacheDir(), FileNameGenerator.generateFileName(feed.getDownload_url())).toString(); @@ -283,11 +293,7 @@ public class OnlineFeedViewActivity extends AppCompatActivity { dialog.show(); } } else { - String errorMsg = status.getReason().getErrorString(OnlineFeedViewActivity.this); - if (status.getReasonDetailed() != null) { - errorMsg += " (" + status.getReasonDetailed() + ")"; - } - showErrorDialog(errorMsg); + showErrorDialog(status.getReason().getErrorString(OnlineFeedViewActivity.this), status.getReasonDetailed()); } } @@ -316,37 +322,47 @@ public class OnlineFeedViewActivity extends AppCompatActivity { } Log.d(TAG, "Parsing feed"); - parser = Observable.fromCallable(this::doParseFeed) + parser = Maybe.fromCallable(this::doParseFeed) .subscribeOn(Schedulers.computation()) .observeOn(AndroidSchedulers.mainThread()) - .subscribe(optionalResult -> { - if(optionalResult.isPresent()) { - FeedHandlerResult result = optionalResult.get(); - beforeShowFeedInformation(result.feed); + .subscribeWith(new DisposableMaybeObserver() { + @Override + public void onSuccess(@NonNull FeedHandlerResult result) { showFeedInformation(result.feed, result.alternateFeedUrls); } - }, error -> { - String errorMsg = DownloadError.ERROR_PARSER_EXCEPTION.getErrorString( - OnlineFeedViewActivity.this) + " (" + error.getMessage() + ")"; - showErrorDialog(errorMsg); - Log.d(TAG, "Feed parser exception: " + Log.getStackTraceString(error)); + + @Override + public void onComplete() { + // Ignore null result: We showed the discovery dialog. + } + + @Override + public void onError(@NonNull Throwable error) { + showErrorDialog(error.getMessage(), ""); + Log.d(TAG, "Feed parser exception: " + Log.getStackTraceString(error)); + } }); } - @NonNull - private Optional doParseFeed() throws Exception { + /** + * Try to parse the feed. + * @return The FeedHandlerResult if successful. + * Null if unsuccessful but we started another attempt. + * @throws Exception If unsuccessful but we do not know a resolution. + */ + @Nullable + private FeedHandlerResult doParseFeed() throws Exception { FeedHandler handler = new FeedHandler(); try { - return Optional.of(handler.parseFeed(feed)); + return handler.parseFeed(feed); } catch (UnsupportedFeedtypeException e) { Log.d(TAG, "Unsupported feed type detected"); if ("html".equalsIgnoreCase(e.getRootElement())) { boolean dialogShown = showFeedDiscoveryDialog(new File(feed.getFile_url()), feed.getDownload_url()); if (dialogShown) { - return Optional.empty(); + return null; // Should not display an error message } else { - Log.d(TAG, "Supplied feed is an HTML web page that has no references to any feed"); - throw e; + throw new UnsupportedFeedtypeException(getString(R.string.download_error_unsupported_type_html)); } } else { throw e; @@ -360,23 +376,6 @@ public class OnlineFeedViewActivity extends AppCompatActivity { } } - /** - * Called after the feed has been downloaded and parsed and before showFeedInformation is called. - * This method is executed on a background thread - */ - private void beforeShowFeedInformation(Feed feed) { - Log.d(TAG, "Removing HTML from feed description"); - - feed.setDescription(HtmlToPlainText.getPlainText(feed.getDescription())); - - Log.d(TAG, "Removing HTML from shownotes"); - if (feed.getItems() != null) { - for (FeedItem item : feed.getItems()) { - item.setDescription(HtmlToPlainText.getPlainText(item.getDescription())); - } - } - } - /** * Called when feed parsed successfully. * This method is executed on the GUI thread. @@ -420,7 +419,7 @@ public class OnlineFeedViewActivity extends AppCompatActivity { viewBinding.titleLabel.setText(feed.getTitle()); viewBinding.authorLabel.setText(feed.getAuthor()); - description.setText(feed.getDescription()); + description.setText(HtmlToPlainText.getPlainText(feed.getDescription())); viewBinding.subscribeButton.setOnClickListener(v -> { if (feedInFeedlist(feed)) { @@ -445,6 +444,11 @@ public class OnlineFeedViewActivity extends AppCompatActivity { IntentUtils.sendLocalBroadcast(this, PlaybackService.ACTION_SHUTDOWN_PLAYBACK_SERVICE); }); + if (UserPreferences.isEnableAutodownload()) { + SharedPreferences preferences = getSharedPreferences(PREFS, MODE_PRIVATE); + viewBinding.autoDownloadCheckBox.setChecked(preferences.getBoolean(PREF_LAST_AUTO_DOWNLOAD, true)); + } + final int MAX_LINES_COLLAPSED = 10; description.setMaxLines(MAX_LINES_COLLAPSED); description.setOnClickListener(v -> { @@ -511,10 +515,17 @@ public class OnlineFeedViewActivity extends AppCompatActivity { if (didPressSubscribe) { didPressSubscribe = false; if (UserPreferences.isEnableAutodownload()) { + boolean autoDownload = viewBinding.autoDownloadCheckBox.isChecked(); + Feed feed1 = DBReader.getFeed(getFeedId(feed)); FeedPreferences feedPreferences = feed1.getPreferences(); - feedPreferences.setAutoDownload(viewBinding.autoDownloadCheckBox.isChecked()); - feed1.savePreferences(); + feedPreferences.setAutoDownload(autoDownload); + DBWriter.setFeedPreferences(feedPreferences); + + SharedPreferences preferences = getSharedPreferences(PREFS, MODE_PRIVATE); + SharedPreferences.Editor editor = preferences.edit(); + editor.putBoolean(PREF_LAST_AUTO_DOWNLOAD, autoDownload); + editor.apply(); } openFeed(); } @@ -553,12 +564,16 @@ public class OnlineFeedViewActivity extends AppCompatActivity { } @UiThread - private void showErrorDialog(String errorMsg) { + private void showErrorDialog(String errorMsg, String details) { if (!isFinishing() && !isPaused) { AlertDialog.Builder builder = new AlertDialog.Builder(this); builder.setTitle(R.string.error_label); if (errorMsg != null) { - builder.setMessage(errorMsg); + String total = errorMsg + "\n\n" + details; + SpannableString errorMessage = new SpannableString(total); + errorMessage.setSpan(new ForegroundColorSpan(0x88888888), + errorMsg.length(), total.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + builder.setMessage(errorMessage); } else { builder.setMessage(R.string.download_error_error_unknown); } diff --git a/app/src/main/java/de/danoeh/antennapod/activity/VideoplayerActivity.java b/app/src/main/java/de/danoeh/antennapod/activity/VideoplayerActivity.java index 1c8619e99..15d0bec4a 100644 --- a/app/src/main/java/de/danoeh/antennapod/activity/VideoplayerActivity.java +++ b/app/src/main/java/de/danoeh/antennapod/activity/VideoplayerActivity.java @@ -17,7 +17,6 @@ import android.widget.ImageView; import androidx.core.view.WindowCompat; import androidx.appcompat.app.ActionBar; -import android.text.TextUtils; import android.util.Log; import android.util.Pair; import android.view.Menu; @@ -37,12 +36,12 @@ import java.lang.ref.WeakReference; import java.util.concurrent.atomic.AtomicBoolean; import de.danoeh.antennapod.R; -import de.danoeh.antennapod.core.feed.MediaType; import de.danoeh.antennapod.core.preferences.UserPreferences; import de.danoeh.antennapod.core.service.playback.PlaybackService; import de.danoeh.antennapod.core.service.playback.PlayerStatus; import de.danoeh.antennapod.core.util.gui.PictureInPictureUtil; import de.danoeh.antennapod.core.util.playback.Playable; +import de.danoeh.antennapod.ui.appstartintent.MainActivityStarter; import de.danoeh.antennapod.view.AspectRatioVideoView; /** @@ -88,9 +87,7 @@ public class VideoplayerActivity extends MediaplayerActivity { @Override protected void onResume() { super.onResume(); - if (TextUtils.equals(getIntent().getAction(), Intent.ACTION_VIEW)) { - playExternalMedia(getIntent(), MediaType.VIDEO); - } else if (PlaybackService.isCasting()) { + if (PlaybackService.isCasting()) { Intent intent = PlaybackService.getPlayerActivityIntent(this); if (!intent.getComponent().getClassName().equals(VideoplayerActivity.class.getName())) { destroyingDueToReload = true; @@ -135,17 +132,13 @@ public class VideoplayerActivity extends MediaplayerActivity { } @Override - protected boolean loadMediaInfo() { - if (!super.loadMediaInfo() || controller == null) { - return false; - } + protected void loadMediaInfo() { + super.loadMediaInfo(); Playable media = controller.getMedia(); if (media != null) { getSupportActionBar().setSubtitle(media.getEpisodeTitle()); getSupportActionBar().setTitle(media.getFeedTitle()); - return true; } - return false; } @Override @@ -347,7 +340,7 @@ public class VideoplayerActivity extends MediaplayerActivity { Log.d(TAG, "ReloadNotification received, switching to Castplayer now"); destroyingDueToReload = true; finish(); - startActivity(new Intent(this, MainActivity.class).putExtra(MainActivity.EXTRA_OPEN_PLAYER, true)); + new MainActivityStarter(this).withOpenPlayer().start(); } } diff --git a/app/src/main/java/de/danoeh/antennapod/activity/WidgetConfigActivity.java b/app/src/main/java/de/danoeh/antennapod/activity/WidgetConfigActivity.java index 4805dba10..3020aba43 100644 --- a/app/src/main/java/de/danoeh/antennapod/activity/WidgetConfigActivity.java +++ b/app/src/main/java/de/danoeh/antennapod/activity/WidgetConfigActivity.java @@ -2,33 +2,34 @@ package de.danoeh.antennapod.activity; import android.Manifest; import android.app.WallpaperManager; -import android.content.pm.PackageManager; -import android.graphics.drawable.Drawable; -import android.os.Build; -import android.widget.ImageView; -import androidx.appcompat.app.AppCompatActivity; - import android.appwidget.AppWidgetManager; import android.content.Intent; import android.content.SharedPreferences; +import android.content.pm.PackageManager; +import android.graphics.drawable.Drawable; +import android.os.Build; import android.os.Bundle; import android.view.View; -import android.widget.RelativeLayout; +import android.widget.CheckBox; +import android.widget.ImageView; import android.widget.SeekBar; import android.widget.TextView; - +import androidx.appcompat.app.AppCompatActivity; import androidx.core.content.ContextCompat; import de.danoeh.antennapod.R; import de.danoeh.antennapod.core.preferences.UserPreferences; import de.danoeh.antennapod.core.receiver.PlayerWidget; -import de.danoeh.antennapod.core.service.PlayerWidgetJobService; +import de.danoeh.antennapod.core.widget.WidgetUpdaterJobService; public class WidgetConfigActivity extends AppCompatActivity { private int appWidgetId = AppWidgetManager.INVALID_APPWIDGET_ID; private SeekBar opacitySeekBar; private TextView opacityTextView; - private RelativeLayout widgetPreview; + private View widgetPreview; + private CheckBox ckRewind; + private CheckBox ckFastForward; + private CheckBox ckSkip; @Override protected void onCreate(Bundle savedInstanceState) { @@ -73,6 +74,32 @@ public class WidgetConfigActivity extends AppCompatActivity { } }); + + widgetPreview.findViewById(R.id.txtNoPlaying).setVisibility(View.GONE); + TextView title = widgetPreview.findViewById(R.id.txtvTitle); + title.setVisibility(View.VISIBLE); + title.setText(R.string.app_name); + TextView progress = widgetPreview.findViewById(R.id.txtvProgress); + progress.setVisibility(View.VISIBLE); + progress.setText(R.string.position_default_label); + + ckRewind = findViewById(R.id.ckRewind); + ckRewind.setOnClickListener(v -> displayPreviewPanel()); + ckFastForward = findViewById(R.id.ckFastForward); + ckFastForward.setOnClickListener(v -> displayPreviewPanel()); + ckSkip = findViewById(R.id.ckSkip); + ckSkip.setOnClickListener(v -> displayPreviewPanel()); + } + + private void displayPreviewPanel() { + boolean showExtendedPreview = ckRewind.isChecked() || ckFastForward.isChecked() || ckSkip.isChecked(); + widgetPreview.findViewById(R.id.extendedButtonsContainer) + .setVisibility(showExtendedPreview ? View.VISIBLE : View.GONE); + widgetPreview.findViewById(R.id.butPlay).setVisibility(showExtendedPreview ? View.GONE : View.VISIBLE); + widgetPreview.findViewById(R.id.butFastForward) + .setVisibility(ckFastForward.isChecked() ? View.VISIBLE : View.GONE); + widgetPreview.findViewById(R.id.butSkip).setVisibility(ckSkip.isChecked() ? View.VISIBLE : View.GONE); + widgetPreview.findViewById(R.id.butRew).setVisibility(ckRewind.isChecked() ? View.VISIBLE : View.GONE); } private void displayDeviceBackground() { @@ -91,13 +118,16 @@ public class WidgetConfigActivity extends AppCompatActivity { SharedPreferences prefs = getSharedPreferences(PlayerWidget.PREFS_NAME, MODE_PRIVATE); SharedPreferences.Editor editor = prefs.edit(); editor.putInt(PlayerWidget.KEY_WIDGET_COLOR + appWidgetId, backgroundColor); + editor.putBoolean(PlayerWidget.KEY_WIDGET_SKIP + appWidgetId, ckSkip.isChecked()); + editor.putBoolean(PlayerWidget.KEY_WIDGET_REWIND + appWidgetId, ckRewind.isChecked()); + editor.putBoolean(PlayerWidget.KEY_WIDGET_FAST_FORWARD + appWidgetId, ckFastForward.isChecked()); editor.apply(); Intent resultValue = new Intent(); resultValue.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId); setResult(RESULT_OK, resultValue); finish(); - PlayerWidgetJobService.updateWidget(this); + WidgetUpdaterJobService.performBackgroundUpdate(this); } private int getColorWithAlpha(int color, int opacity) { diff --git a/app/src/main/java/de/danoeh/antennapod/activity/gpoddernet/GpodnetAuthenticationActivity.java b/app/src/main/java/de/danoeh/antennapod/activity/gpoddernet/GpodnetAuthenticationActivity.java deleted file mode 100644 index cfd6ec702..000000000 --- a/app/src/main/java/de/danoeh/antennapod/activity/gpoddernet/GpodnetAuthenticationActivity.java +++ /dev/null @@ -1,395 +0,0 @@ -package de.danoeh.antennapod.activity.gpoddernet; - -import android.content.Context; -import android.content.Intent; -import android.os.AsyncTask; -import android.os.Build; -import android.os.Bundle; -import androidx.appcompat.app.AppCompatActivity; -import android.util.Log; -import android.view.LayoutInflater; -import android.view.MenuItem; -import android.view.View; -import android.view.inputmethod.EditorInfo; -import android.view.inputmethod.InputMethodManager; -import android.widget.AdapterView; -import android.widget.ArrayAdapter; -import android.widget.Button; -import android.widget.EditText; -import android.widget.ProgressBar; -import android.widget.Spinner; -import android.widget.TextView; -import android.widget.ViewFlipper; - -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.atomic.AtomicReference; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import de.danoeh.antennapod.BuildConfig; -import de.danoeh.antennapod.R; -import de.danoeh.antennapod.activity.MainActivity; -import de.danoeh.antennapod.core.preferences.GpodnetPreferences; -import de.danoeh.antennapod.core.preferences.UserPreferences; -import de.danoeh.antennapod.core.service.download.AntennapodHttpClient; -import de.danoeh.antennapod.core.sync.SyncService; -import de.danoeh.antennapod.core.sync.gpoddernet.GpodnetService; -import de.danoeh.antennapod.core.sync.gpoddernet.GpodnetServiceException; -import de.danoeh.antennapod.core.sync.gpoddernet.model.GpodnetDevice; - -/** - * Guides the user through the authentication process - * Step 1: Request username and password from user - * Step 2: Choose device from a list of available devices or create a new one - * Step 3: Choose from a list of actions - */ -public class GpodnetAuthenticationActivity extends AppCompatActivity { - private static final String TAG = "GpodnetAuthActivity"; - - private ViewFlipper viewFlipper; - - private static final int STEP_DEFAULT = -1; - private static final int STEP_LOGIN = 0; - private static final int STEP_DEVICE = 1; - private static final int STEP_FINISH = 2; - - private int currentStep = -1; - - private GpodnetService service; - private volatile String username; - private volatile String password; - private volatile GpodnetDevice selectedDevice; - - private View[] views; - - @Override - protected void onCreate(Bundle savedInstanceState) { - setTheme(UserPreferences.getTheme()); - super.onCreate(savedInstanceState); - getSupportActionBar().setDisplayHomeAsUpEnabled(true); - - setContentView(R.layout.gpodnetauth_activity); - service = new GpodnetService(AntennapodHttpClient.getHttpClient(), GpodnetPreferences.getHostname()); - - viewFlipper = findViewById(R.id.viewflipper); - LayoutInflater inflater = (LayoutInflater) - getSystemService(Context.LAYOUT_INFLATER_SERVICE); - views = new View[]{ - inflater.inflate(R.layout.gpodnetauth_credentials, viewFlipper, false), - inflater.inflate(R.layout.gpodnetauth_device, viewFlipper, false), - inflater.inflate(R.layout.gpodnetauth_finish, viewFlipper, false) - }; - for (View view : views) { - viewFlipper.addView(view); - } - advance(); - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - if (item.getItemId() == android.R.id.home) { - finish(); - return true; - } - return super.onOptionsItemSelected(item); - } - - private void setupLoginView(View view) { - final EditText username = view.findViewById(R.id.etxtUsername); - final EditText password = view.findViewById(R.id.etxtPassword); - final Button login = view.findViewById(R.id.butLogin); - final TextView txtvError = view.findViewById(R.id.txtvError); - final ProgressBar progressBar = view.findViewById(R.id.progBarLogin); - - password.setOnEditorActionListener((v, actionID, event) -> - actionID == EditorInfo.IME_ACTION_GO && login.performClick()); - - login.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - - final String usernameStr = username.getText().toString(); - final String passwordStr = password.getText().toString(); - - if (usernameHasUnwantedChars(usernameStr)) { - txtvError.setText(R.string.gpodnetsync_username_characters_error); - txtvError.setVisibility(View.VISIBLE); - return; - } - if (BuildConfig.DEBUG) Log.d(TAG, "Checking login credentials"); - AsyncTask authTask = new AsyncTask() { - - volatile Exception exception; - - @Override - protected void onPreExecute() { - super.onPreExecute(); - login.setEnabled(false); - progressBar.setVisibility(View.VISIBLE); - txtvError.setVisibility(View.GONE); - // hide the keyboard - InputMethodManager inputManager = (InputMethodManager) - getSystemService(Context.INPUT_METHOD_SERVICE); - inputManager.hideSoftInputFromWindow(login.getWindowToken(), - InputMethodManager.HIDE_NOT_ALWAYS); - - } - - @Override - protected void onPostExecute(Void aVoid) { - super.onPostExecute(aVoid); - login.setEnabled(true); - progressBar.setVisibility(View.GONE); - - if (exception == null) { - advance(); - } else { - txtvError.setText(exception.getCause().getMessage()); - txtvError.setVisibility(View.VISIBLE); - } - } - - @Override - protected Void doInBackground(GpodnetService... params) { - try { - params[0].authenticate(usernameStr, passwordStr); - GpodnetAuthenticationActivity.this.username = usernameStr; - GpodnetAuthenticationActivity.this.password = passwordStr; - } catch (GpodnetServiceException e) { - e.printStackTrace(); - exception = e; - } - return null; - } - }; - authTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, service); - } - }); - } - - private void setupDeviceView(View view) { - final EditText deviceID = view.findViewById(R.id.etxtDeviceID); - final EditText caption = view.findViewById(R.id.etxtCaption); - final Button createNewDevice = view.findViewById(R.id.butCreateNewDevice); - final Button chooseDevice = view.findViewById(R.id.butChooseExistingDevice); - final TextView txtvError = view.findViewById(R.id.txtvError); - final ProgressBar progBarCreateDevice = view.findViewById(R.id.progbarCreateDevice); - final Spinner spinnerDevices = view.findViewById(R.id.spinnerChooseDevice); - - - // load device list - final AtomicReference> devices = new AtomicReference<>(); - new AsyncTask>() { - - @Override - protected void onPreExecute() { - super.onPreExecute(); - chooseDevice.setEnabled(false); - spinnerDevices.setEnabled(false); - createNewDevice.setEnabled(false); - } - - @Override - protected void onPostExecute(List gpodnetDevices) { - super.onPostExecute(gpodnetDevices); - if (gpodnetDevices != null) { - List deviceNames = new ArrayList<>(); - for (GpodnetDevice device : gpodnetDevices) { - deviceNames.add(device.getCaption()); - } - spinnerDevices.setAdapter(new ArrayAdapter<>(GpodnetAuthenticationActivity.this, - android.R.layout.simple_spinner_dropdown_item, deviceNames)); - spinnerDevices.setEnabled(true); - if (!deviceNames.isEmpty()) { - chooseDevice.setEnabled(true); - } - devices.set(gpodnetDevices); - deviceID.setText(generateDeviceID(gpodnetDevices)); - createNewDevice.setEnabled(true); - } - } - - @Override - protected List doInBackground(GpodnetService... params) { - try { - return params[0].getDevices(); - } catch (GpodnetServiceException e) { - e.printStackTrace(); - return null; - } - } - }.execute(service); - - - createNewDevice.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - if (checkDeviceIDText(deviceID, caption, txtvError, devices.get())) { - final String deviceStr = deviceID.getText().toString(); - final String captionStr = caption.getText().toString(); - - new AsyncTask() { - - private volatile Exception exception; - - @Override - protected void onPreExecute() { - super.onPreExecute(); - createNewDevice.setEnabled(false); - chooseDevice.setEnabled(false); - progBarCreateDevice.setVisibility(View.VISIBLE); - txtvError.setVisibility(View.GONE); - } - - @Override - protected void onPostExecute(GpodnetDevice result) { - super.onPostExecute(result); - createNewDevice.setEnabled(true); - chooseDevice.setEnabled(true); - progBarCreateDevice.setVisibility(View.GONE); - if (exception == null) { - selectedDevice = result; - advance(); - } else { - txtvError.setText(exception.getMessage()); - txtvError.setVisibility(View.VISIBLE); - } - } - - @Override - protected GpodnetDevice doInBackground(GpodnetService... params) { - try { - params[0].configureDevice(deviceStr, captionStr, GpodnetDevice.DeviceType.MOBILE); - return new GpodnetDevice(deviceStr, captionStr, GpodnetDevice.DeviceType.MOBILE.toString(), 0); - } catch (GpodnetServiceException e) { - e.printStackTrace(); - exception = e; - } - return null; - } - }.execute(service); - } - } - }); - - chooseDevice.setOnClickListener(v -> { - final int position = spinnerDevices.getSelectedItemPosition(); - if (position != AdapterView.INVALID_POSITION) { - selectedDevice = devices.get().get(position); - advance(); - } - }); - } - - - private String generateDeviceID(List gpodnetDevices) { - // devices names must be of a certain form: - // https://gpoddernet.readthedocs.org/en/latest/api/reference/general.html#devices - // This is more restrictive than needed, but I think it makes for more readable names. - String baseId = Build.MODEL.replaceAll("\\W", ""); - String id = baseId; - int num = 0; - - while (isDeviceWithIdInList(id, gpodnetDevices)) { - id = baseId + "_" + num; - num++; - } - - return id; - } - - private boolean isDeviceWithIdInList(String id, List gpodnetDevices) { - if (gpodnetDevices == null) { - return false; - } - for (GpodnetDevice device : gpodnetDevices) { - if (device.getId().equals(id)) { - return true; - } - } - return false; - } - - private boolean checkDeviceIDText(EditText deviceID, EditText caption, TextView txtvError, List devices) { - String text = deviceID.getText().toString(); - if (text.length() == 0) { - txtvError.setText(R.string.gpodnetauth_device_errorEmpty); - txtvError.setVisibility(View.VISIBLE); - return false; - } else if (caption.length() == 0) { - txtvError.setText(R.string.gpodnetauth_device_caption_errorEmpty); - txtvError.setVisibility(View.VISIBLE); - return false; - } else { - if (devices != null) { - if (isDeviceWithIdInList(text, devices)) { - txtvError.setText(R.string.gpodnetauth_device_errorAlreadyUsed); - txtvError.setVisibility(View.VISIBLE); - return false; - } - txtvError.setVisibility(View.GONE); - return true; - } - return true; - } - - } - - private void setupFinishView(View view) { - final Button sync = view.findViewById(R.id.butSyncNow); - final Button back = view.findViewById(R.id.butGoMainscreen); - - sync.setOnClickListener(v -> { - finish(); - SyncService.sync(getApplicationContext()); - }); - back.setOnClickListener(v -> { - Intent intent = new Intent(GpodnetAuthenticationActivity.this, MainActivity.class); - intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); - startActivity(intent); - }); - } - - private void writeLoginCredentials() { - if (BuildConfig.DEBUG) Log.d(TAG, "Writing login credentials"); - GpodnetPreferences.setUsername(username); - GpodnetPreferences.setPassword(password); - GpodnetPreferences.setDeviceID(selectedDevice.getId()); - } - - private void advance() { - if (currentStep < STEP_FINISH) { - - View view = views[currentStep + 1]; - if (currentStep == STEP_DEFAULT) { - setupLoginView(view); - } else if (currentStep == STEP_LOGIN) { - if (username == null || password == null) { - throw new IllegalStateException("Username and password must not be null here"); - } else { - setupDeviceView(view); - } - } else if (currentStep == STEP_DEVICE) { - if (selectedDevice == null) { - throw new IllegalStateException("Device must not be null here"); - } else { - writeLoginCredentials(); - setupFinishView(view); - } - } - if (currentStep != STEP_DEFAULT) { - viewFlipper.showNext(); - } - currentStep++; - } else { - finish(); - } - } - - private boolean usernameHasUnwantedChars(String username) { - Pattern special = Pattern.compile("[!@#$%&*()+=|<>?{}\\[\\]~]"); - Matcher containsUnwantedChars = special.matcher(username); - return containsUnwantedChars.find(); - } -} diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/ChaptersListAdapter.java b/app/src/main/java/de/danoeh/antennapod/adapter/ChaptersListAdapter.java index 4fa8acc43..d4b32ee06 100644 --- a/app/src/main/java/de/danoeh/antennapod/adapter/ChaptersListAdapter.java +++ b/app/src/main/java/de/danoeh/antennapod/adapter/ChaptersListAdapter.java @@ -20,9 +20,9 @@ import de.danoeh.antennapod.core.glide.ApGlideSettings; import de.danoeh.antennapod.core.util.Converter; import de.danoeh.antennapod.core.util.EmbeddedChapterImage; import de.danoeh.antennapod.core.util.IntentUtils; -import de.danoeh.antennapod.core.util.ThemeUtils; +import de.danoeh.antennapod.ui.common.ThemeUtils; import de.danoeh.antennapod.core.util.playback.Playable; -import de.danoeh.antennapod.view.CircularProgressBar; +import de.danoeh.antennapod.ui.common.CircularProgressBar; public class ChaptersListAdapter extends RecyclerView.Adapter { private Playable media; @@ -42,7 +42,7 @@ public class ChaptersListAdapter extends RecyclerView.Adapter 0 && media.getDuration() < c.getStart(); - } - public Chapter getItem(int position) { - int i = 0; - for (Chapter chapter : media.getChapters()) { - if (!ignoreChapter(chapter)) { - if (i == position) { - return chapter; - } else { - i++; - } - } - } - return null; + return media.getChapters().get(position); } public interface Callback { diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/DownloadLogAdapter.java b/app/src/main/java/de/danoeh/antennapod/adapter/DownloadLogAdapter.java index 0c4aaf6ed..811e1e31b 100644 --- a/app/src/main/java/de/danoeh/antennapod/adapter/DownloadLogAdapter.java +++ b/app/src/main/java/de/danoeh/antennapod/adapter/DownloadLogAdapter.java @@ -20,7 +20,7 @@ import de.danoeh.antennapod.core.storage.DBReader; import de.danoeh.antennapod.core.storage.DBTasks; import de.danoeh.antennapod.core.storage.DownloadRequestException; import de.danoeh.antennapod.core.storage.DownloadRequester; -import de.danoeh.antennapod.core.util.ThemeUtils; +import de.danoeh.antennapod.ui.common.ThemeUtils; import de.danoeh.antennapod.view.viewholder.DownloadItemViewHolder; /** @@ -68,16 +68,14 @@ public class DownloadLogAdapter extends BaseAdapter { holder.icon.setContentDescription(context.getString(R.string.download_successful)); holder.secondaryActionButton.setVisibility(View.INVISIBLE); holder.reason.setVisibility(View.GONE); + holder.tapForDetails.setVisibility(View.GONE); } else { holder.icon.setTextColor(ContextCompat.getColor(context, R.color.download_failed_red)); holder.icon.setText("{fa-times-circle}"); holder.icon.setContentDescription(context.getString(R.string.error_label)); - String reasonText = status.getReason().getErrorString(context); - if (status.getReasonDetailed() != null) { - reasonText += ": " + status.getReasonDetailed(); - } - holder.reason.setText(reasonText); + holder.reason.setText(status.getReason().getErrorString(context)); holder.reason.setVisibility(View.VISIBLE); + holder.tapForDetails.setVisibility(View.VISIBLE); if (newerWasSuccessful(position, status.getFeedfileType(), status.getFeedfileId())) { holder.secondaryActionButton.setVisibility(View.INVISIBLE); diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/DownloadlistAdapter.java b/app/src/main/java/de/danoeh/antennapod/adapter/DownloadlistAdapter.java index 268a21409..9363edc9f 100644 --- a/app/src/main/java/de/danoeh/antennapod/adapter/DownloadlistAdapter.java +++ b/app/src/main/java/de/danoeh/antennapod/adapter/DownloadlistAdapter.java @@ -15,8 +15,8 @@ import de.danoeh.antennapod.core.feed.FeedMedia; import de.danoeh.antennapod.core.service.download.DownloadRequest; import de.danoeh.antennapod.core.service.download.DownloadStatus; import de.danoeh.antennapod.core.service.download.Downloader; -import de.danoeh.antennapod.core.util.ThemeUtils; -import de.danoeh.antennapod.view.CircularProgressBar; +import de.danoeh.antennapod.ui.common.ThemeUtils; +import de.danoeh.antennapod.ui.common.CircularProgressBar; public class DownloadlistAdapter extends BaseAdapter { @@ -68,8 +68,8 @@ public class DownloadlistAdapter extends BaseAdapter { holder.secondaryActionButton.setContentDescription(context.getString(R.string.cancel_download_label)); holder.secondaryActionButton.setTag(downloader); holder.secondaryActionButton.setOnClickListener(butSecondaryListener); - holder.secondaryActionProgress.setPercentage(0, request); + boolean percentageWasSet = false; String status = ""; if (request.getFeedfileType() == Feed.FEEDFILETYPE_FEED) { status += context.getString(R.string.download_type_feed); @@ -85,8 +85,12 @@ public class DownloadlistAdapter extends BaseAdapter { status += " / " + Formatter.formatShortFileSize(context, request.getSize()); holder.secondaryActionProgress.setPercentage( 0.01f * Math.max(1, request.getProgressPercent()), request); + percentageWasSet = true; } } + if (!percentageWasSet) { + holder.secondaryActionProgress.setPercentage(0, request); + } holder.status.setText(status); return convertView; diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/FeedItemlistDescriptionAdapter.java b/app/src/main/java/de/danoeh/antennapod/adapter/FeedItemlistDescriptionAdapter.java index 8cb0fd30a..50b924e49 100644 --- a/app/src/main/java/de/danoeh/antennapod/adapter/FeedItemlistDescriptionAdapter.java +++ b/app/src/main/java/de/danoeh/antennapod/adapter/FeedItemlistDescriptionAdapter.java @@ -20,6 +20,7 @@ import de.danoeh.antennapod.core.service.playback.PlaybackService; import de.danoeh.antennapod.core.util.DateUtils; import de.danoeh.antennapod.core.util.playback.Playable; import de.danoeh.antennapod.core.util.playback.PlaybackServiceStarter; +import de.danoeh.antennapod.core.util.syndication.HtmlToPlainText; import de.danoeh.antennapod.dialog.StreamingConfirmationDialog; import java.util.List; @@ -59,7 +60,7 @@ public class FeedItemlistDescriptionAdapter extends ArrayAdapter { holder.title.setText(item.getTitle()); holder.pubDate.setText(DateUtils.formatAbbrev(getContext(), item.getPubDate())); if (item.getDescription() != null) { - String description = item.getDescription() + String description = HtmlToPlainText.getPlainText(item.getDescription()) .replaceAll("\n", " ") .replaceAll("\\s+", " ") .trim(); diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/FeedSearchResultAdapter.java b/app/src/main/java/de/danoeh/antennapod/adapter/FeedSearchResultAdapter.java index 2e5ba31c9..dbb9ce0d0 100644 --- a/app/src/main/java/de/danoeh/antennapod/adapter/FeedSearchResultAdapter.java +++ b/app/src/main/java/de/danoeh/antennapod/adapter/FeedSearchResultAdapter.java @@ -10,7 +10,7 @@ import de.danoeh.antennapod.R; import de.danoeh.antennapod.activity.MainActivity; import de.danoeh.antennapod.core.feed.Feed; import de.danoeh.antennapod.fragment.FeedItemlistFragment; -import de.danoeh.antennapod.view.SquareImageView; +import de.danoeh.antennapod.ui.common.SquareImageView; import java.lang.ref.WeakReference; import java.util.ArrayList; diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/NavListAdapter.java b/app/src/main/java/de/danoeh/antennapod/adapter/NavListAdapter.java index 5533197b9..8bfcf66cc 100644 --- a/app/src/main/java/de/danoeh/antennapod/adapter/NavListAdapter.java +++ b/app/src/main/java/de/danoeh/antennapod/adapter/NavListAdapter.java @@ -24,7 +24,6 @@ import de.danoeh.antennapod.core.feed.Feed; import de.danoeh.antennapod.core.glide.ApGlideSettings; import de.danoeh.antennapod.core.preferences.UserPreferences; import de.danoeh.antennapod.core.storage.NavDrawerData; -import de.danoeh.antennapod.core.util.ThemeUtils; import de.danoeh.antennapod.fragment.AddFeedFragment; import de.danoeh.antennapod.fragment.DownloadsFragment; import de.danoeh.antennapod.fragment.EpisodesFragment; @@ -32,6 +31,7 @@ import de.danoeh.antennapod.fragment.NavDrawerFragment; import de.danoeh.antennapod.fragment.PlaybackHistoryFragment; import de.danoeh.antennapod.fragment.QueueFragment; import de.danoeh.antennapod.fragment.SubscriptionFragment; +import de.danoeh.antennapod.ui.common.ThemeUtils; import org.apache.commons.lang3.ArrayUtils; import java.lang.ref.WeakReference; @@ -76,7 +76,7 @@ public class NavListAdapter extends RecyclerView.Adapter } public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { - if (key.equals(UserPreferences.PREF_HIDDEN_DRAWER_ITEMS)) { + if (UserPreferences.PREF_HIDDEN_DRAWER_ITEMS.equals(key)) { loadItems(); } } @@ -314,7 +314,7 @@ public class NavListAdapter extends RecyclerView.Adapter } Glide.with(context) - .load(feed.getImageLocation()) + .load(feed.getImageUrl()) .apply(new RequestOptions() .placeholder(R.color.light_gray) .error(R.color.light_gray) diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/StatisticsListAdapter.java b/app/src/main/java/de/danoeh/antennapod/adapter/StatisticsListAdapter.java index 72482b06d..23b5cfdce 100644 --- a/app/src/main/java/de/danoeh/antennapod/adapter/StatisticsListAdapter.java +++ b/app/src/main/java/de/danoeh/antennapod/adapter/StatisticsListAdapter.java @@ -66,7 +66,7 @@ public abstract class StatisticsListAdapter extends RecyclerView.Adapter= Build.VERSION_CODES.LOLLIPOP) { - i.addFlags(Intent.FLAG_ACTIVITY_NEW_DOCUMENT); - } - return i; - } - } -} diff --git a/app/src/main/java/de/danoeh/antennapod/dialog/AuthenticationDialog.java b/app/src/main/java/de/danoeh/antennapod/dialog/AuthenticationDialog.java index 39d321f18..d7b2dc536 100644 --- a/app/src/main/java/de/danoeh/antennapod/dialog/AuthenticationDialog.java +++ b/app/src/main/java/de/danoeh/antennapod/dialog/AuthenticationDialog.java @@ -1,37 +1,50 @@ package de.danoeh.antennapod.dialog; import android.content.Context; -import android.view.View; -import android.widget.EditText; +import android.text.method.HideReturnsTransformationMethod; +import android.text.method.PasswordTransformationMethod; +import android.view.LayoutInflater; import androidx.appcompat.app.AlertDialog; import de.danoeh.antennapod.R; +import de.danoeh.antennapod.databinding.AuthenticationDialogBinding; /** * Displays a dialog with a username and password text field and an optional checkbox to save username and preferences. */ public abstract class AuthenticationDialog extends AlertDialog.Builder { + boolean passwordHidden = true; public AuthenticationDialog(Context context, int titleRes, boolean enableUsernameField, String usernameInitialValue, String passwordInitialValue) { super(context); setTitle(titleRes); - View rootView = View.inflate(context, R.layout.authentication_dialog, null); - setView(rootView); + AuthenticationDialogBinding viewBinding = AuthenticationDialogBinding.inflate(LayoutInflater.from(context)); + setView(viewBinding.getRoot()); - final EditText etxtUsername = rootView.findViewById(R.id.etxtUsername); - final EditText etxtPassword = rootView.findViewById(R.id.etxtPassword); - - etxtUsername.setEnabled(enableUsernameField); + viewBinding.usernameEditText.setEnabled(enableUsernameField); if (usernameInitialValue != null) { - etxtUsername.setText(usernameInitialValue); + viewBinding.usernameEditText.setText(usernameInitialValue); } if (passwordInitialValue != null) { - etxtPassword.setText(passwordInitialValue); + viewBinding.passwordEditText.setText(passwordInitialValue); } + viewBinding.showPasswordButton.setOnClickListener(v -> { + if (passwordHidden) { + viewBinding.passwordEditText.setTransformationMethod(HideReturnsTransformationMethod.getInstance()); + viewBinding.showPasswordButton.setAlpha(1.0f); + } else { + viewBinding.passwordEditText.setTransformationMethod(PasswordTransformationMethod.getInstance()); + viewBinding.showPasswordButton.setAlpha(0.6f); + } + passwordHidden = !passwordHidden; + }); + setOnCancelListener(dialog -> onCancelled()); + setOnDismissListener(dialog -> onCancelled()); setNegativeButton(R.string.cancel_label, null); setPositiveButton(R.string.confirm_label, (dialog, which) - -> onConfirmed(etxtUsername.getText().toString(), etxtPassword.getText().toString())); + -> onConfirmed(viewBinding.usernameEditText.getText().toString(), + viewBinding.passwordEditText.getText().toString())); } protected void onCancelled() { diff --git a/app/src/main/java/de/danoeh/antennapod/dialog/EpisodesApplyActionFragment.java b/app/src/main/java/de/danoeh/antennapod/dialog/EpisodesApplyActionFragment.java index efaff1da3..e1e8f1c2e 100644 --- a/app/src/main/java/de/danoeh/antennapod/dialog/EpisodesApplyActionFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/dialog/EpisodesApplyActionFragment.java @@ -28,7 +28,7 @@ import de.danoeh.antennapod.core.storage.DownloadRequester; import de.danoeh.antennapod.core.util.FeedItemPermutors; import de.danoeh.antennapod.core.util.LongList; import de.danoeh.antennapod.core.util.SortOrder; -import de.danoeh.antennapod.core.util.ThemeUtils; +import de.danoeh.antennapod.ui.common.ThemeUtils; import java.util.ArrayList; import java.util.Arrays; diff --git a/app/src/main/java/de/danoeh/antennapod/dialog/FilterDialog.java b/app/src/main/java/de/danoeh/antennapod/dialog/FilterDialog.java index 80df87891..779248e2f 100644 --- a/app/src/main/java/de/danoeh/antennapod/dialog/FilterDialog.java +++ b/app/src/main/java/de/danoeh/antennapod/dialog/FilterDialog.java @@ -16,7 +16,7 @@ import java.util.Set; import de.danoeh.antennapod.R; import de.danoeh.antennapod.core.feed.FeedItemFilter; import de.danoeh.antennapod.core.feed.FeedItemFilterGroup; -import de.danoeh.antennapod.view.RecursiveRadioGroup; +import de.danoeh.antennapod.ui.common.RecursiveRadioGroup; public abstract class FilterDialog { diff --git a/app/src/main/java/de/danoeh/antennapod/dialog/GpodnetSetHostnameDialog.java b/app/src/main/java/de/danoeh/antennapod/dialog/GpodnetSetHostnameDialog.java deleted file mode 100644 index 8119dffcb..000000000 --- a/app/src/main/java/de/danoeh/antennapod/dialog/GpodnetSetHostnameDialog.java +++ /dev/null @@ -1,59 +0,0 @@ -package de.danoeh.antennapod.dialog; - -import android.content.Context; -import androidx.appcompat.app.AlertDialog; -import android.text.Editable; -import android.text.InputType; -import android.view.View; -import android.view.ViewGroup; -import android.widget.EditText; -import android.widget.LinearLayout; - -import de.danoeh.antennapod.R; -import de.danoeh.antennapod.core.preferences.GpodnetPreferences; -import de.danoeh.antennapod.core.sync.gpoddernet.GpodnetService; - -/** - * Creates a dialog that lets the user change the hostname for the gpodder.net service. - */ -public class GpodnetSetHostnameDialog { - - private GpodnetSetHostnameDialog(){} - - private static final String TAG = "GpodnetSetHostnameDialog"; - - public static AlertDialog createDialog(final Context context) { - AlertDialog.Builder dialog = new AlertDialog.Builder(context); - final EditText et = new EditText(context); - et.setText(GpodnetPreferences.getHostname()); - et.setInputType(InputType.TYPE_TEXT_VARIATION_URI); - dialog.setTitle(R.string.pref_gpodnet_sethostname_title) - .setView(setupContentView(context, et)) - .setPositiveButton(R.string.confirm_label, (dialog1, which) -> { - final Editable e = et.getText(); - if (e != null) { - GpodnetPreferences.setHostname(e.toString()); - } - dialog1.dismiss(); - }) - .setNegativeButton(R.string.cancel_label, (dialog1, which) -> dialog1.cancel()) - .setNeutralButton(R.string.pref_gpodnet_sethostname_use_default_host, (dialog1, which) -> { - GpodnetPreferences.setHostname(GpodnetService.DEFAULT_BASE_HOST); - dialog1.dismiss(); - }) - .setCancelable(true); - return dialog.show(); - } - - private static View setupContentView(Context context, EditText et) { - LinearLayout ll = new LinearLayout(context); - ll.addView(et); - LinearLayout.LayoutParams params = (LinearLayout.LayoutParams) et.getLayoutParams(); - if (params != null) { - params.setMargins(8, 8, 8, 8); - params.width = ViewGroup.LayoutParams.MATCH_PARENT; - params.height = ViewGroup.LayoutParams.MATCH_PARENT; - } - return ll; - } -} diff --git a/app/src/main/java/de/danoeh/antennapod/dialog/PlaybackControlsDialog.java b/app/src/main/java/de/danoeh/antennapod/dialog/PlaybackControlsDialog.java index 98f6cc117..195891499 100644 --- a/app/src/main/java/de/danoeh/antennapod/dialog/PlaybackControlsDialog.java +++ b/app/src/main/java/de/danoeh/antennapod/dialog/PlaybackControlsDialog.java @@ -41,7 +41,7 @@ public class PlaybackControlsDialog extends DialogFragment { super.onStart(); controller = new PlaybackController(getActivity()) { @Override - public void setupGUI() { + public void loadMediaInfo() { setupUi(); setupAudioTracks(); } diff --git a/app/src/main/java/de/danoeh/antennapod/dialog/SleepTimerDialog.java b/app/src/main/java/de/danoeh/antennapod/dialog/SleepTimerDialog.java index f1a41d753..fa5c2d8c3 100644 --- a/app/src/main/java/de/danoeh/antennapod/dialog/SleepTimerDialog.java +++ b/app/src/main/java/de/danoeh/antennapod/dialog/SleepTimerDialog.java @@ -47,12 +47,12 @@ public class SleepTimerDialog extends DialogFragment { super.onStart(); controller = new PlaybackController(getActivity()) { @Override - public void setupGUI() { + public void onSleepTimerUpdate() { updateTime(); } @Override - public void onSleepTimerUpdate() { + public void loadMediaInfo() { updateTime(); } }; diff --git a/app/src/main/java/de/danoeh/antennapod/dialog/SubscriptionsFilterDialog.java b/app/src/main/java/de/danoeh/antennapod/dialog/SubscriptionsFilterDialog.java index 8a87fef25..29172bb5e 100644 --- a/app/src/main/java/de/danoeh/antennapod/dialog/SubscriptionsFilterDialog.java +++ b/app/src/main/java/de/danoeh/antennapod/dialog/SubscriptionsFilterDialog.java @@ -20,7 +20,7 @@ import de.danoeh.antennapod.core.event.UnreadItemsUpdateEvent; import de.danoeh.antennapod.core.feed.SubscriptionsFilter; import de.danoeh.antennapod.core.feed.SubscriptionsFilterGroup; import de.danoeh.antennapod.core.preferences.UserPreferences; -import de.danoeh.antennapod.view.RecursiveRadioGroup; +import de.danoeh.antennapod.ui.common.RecursiveRadioGroup; public class SubscriptionsFilterDialog { public static void showDialog(Context context) { diff --git a/app/src/main/java/de/danoeh/antennapod/dialog/VariableSpeedDialog.java b/app/src/main/java/de/danoeh/antennapod/dialog/VariableSpeedDialog.java index 1fc7a77b2..65e7c4424 100644 --- a/app/src/main/java/de/danoeh/antennapod/dialog/VariableSpeedDialog.java +++ b/app/src/main/java/de/danoeh/antennapod/dialog/VariableSpeedDialog.java @@ -56,12 +56,12 @@ public class VariableSpeedDialog extends DialogFragment { super.onStart(); controller = new PlaybackController(getActivity()) { @Override - public void setupGUI() { + public void onPlaybackSpeedChange() { updateSpeed(); } @Override - public void onPlaybackSpeedChange() { + public void loadMediaInfo() { updateSpeed(); } }; diff --git a/app/src/main/java/de/danoeh/antennapod/discovery/GpodnetPodcastSearcher.java b/app/src/main/java/de/danoeh/antennapod/discovery/GpodnetPodcastSearcher.java index 53237579f..6de2186e0 100644 --- a/app/src/main/java/de/danoeh/antennapod/discovery/GpodnetPodcastSearcher.java +++ b/app/src/main/java/de/danoeh/antennapod/discovery/GpodnetPodcastSearcher.java @@ -18,7 +18,7 @@ public class GpodnetPodcastSearcher implements PodcastSearcher { return Single.create((SingleOnSubscribe>) subscriber -> { try { GpodnetService service = new GpodnetService(AntennapodHttpClient.getHttpClient(), - GpodnetPreferences.getHostname()); + GpodnetPreferences.getHosturl()); List gpodnetPodcasts = service.searchPodcasts(query, 0); List results = new ArrayList<>(); for (GpodnetPodcast podcast : gpodnetPodcasts) { diff --git a/app/src/main/java/de/danoeh/antennapod/discovery/PodcastSearcherRegistry.java b/app/src/main/java/de/danoeh/antennapod/discovery/PodcastSearcherRegistry.java index ad574cab6..16c5548be 100644 --- a/app/src/main/java/de/danoeh/antennapod/discovery/PodcastSearcherRegistry.java +++ b/app/src/main/java/de/danoeh/antennapod/discovery/PodcastSearcherRegistry.java @@ -15,11 +15,11 @@ public class PodcastSearcherRegistry { public static List getSearchProviders() { if (searchProviders == null) { searchProviders = new ArrayList<>(); - searchProviders.add(new SearcherInfo(new CombinedSearcher(), 1.f)); - searchProviders.add(new SearcherInfo(new ItunesPodcastSearcher(), 1.f)); - searchProviders.add(new SearcherInfo(new FyydPodcastSearcher(), 1.f)); + searchProviders.add(new SearcherInfo(new CombinedSearcher(), 1.0f)); searchProviders.add(new SearcherInfo(new GpodnetPodcastSearcher(), 0.0f)); - searchProviders.add(new SearcherInfo(new PodcastIndexPodcastSearcher(), 0.0f)); + searchProviders.add(new SearcherInfo(new FyydPodcastSearcher(), 1.0f)); + searchProviders.add(new SearcherInfo(new ItunesPodcastSearcher(), 1.0f)); + searchProviders.add(new SearcherInfo(new PodcastIndexPodcastSearcher(), 1.0f)); } return searchProviders; } diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/AddFeedFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/AddFeedFragment.java index 06a974dfd..08e23fc7f 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/AddFeedFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/AddFeedFragment.java @@ -50,9 +50,11 @@ public class AddFeedFragment extends Fragment { public static final String TAG = "AddFeedFragment"; private static final int REQUEST_CODE_CHOOSE_OPML_IMPORT_PATH = 1; private static final int REQUEST_CODE_ADD_LOCAL_FOLDER = 2; + private static final String KEY_UP_ARROW = "up_arrow"; private AddfeedBinding viewBinding; private MainActivity activity; + private boolean displayUpArrow; @Override @Nullable @@ -64,7 +66,11 @@ public class AddFeedFragment extends Fragment { activity = (MainActivity) getActivity(); Toolbar toolbar = viewBinding.toolbar; - ((MainActivity) getActivity()).setupToolbarToggle(toolbar); + displayUpArrow = getParentFragmentManager().getBackStackEntryCount() != 0; + if (savedInstanceState != null) { + displayUpArrow = savedInstanceState.getBoolean(KEY_UP_ARROW); + } + ((MainActivity) getActivity()).setupToolbarToggle(toolbar, displayUpArrow); viewBinding.searchItunesButton.setOnClickListener(v -> activity.loadChildFragment(OnlineSearchFragment.newInstance(ItunesPodcastSearcher.class))); @@ -119,6 +125,12 @@ public class AddFeedFragment extends Fragment { return viewBinding.getRoot(); } + @Override + public void onSaveInstanceState(@NonNull Bundle outState) { + outState.putBoolean(KEY_UP_ARROW, displayUpArrow); + super.onSaveInstanceState(outState); + } + private void showAddViaUrlDialog() { AlertDialog.Builder builder = new AlertDialog.Builder(getContext()); builder.setTitle(R.string.add_podcast_by_url); @@ -196,7 +208,11 @@ public class AddFeedFragment extends Fragment { if (documentFile == null) { throw new IllegalArgumentException("Unable to retrieve document tree"); } - Feed dirFeed = new Feed(Feed.PREFIX_LOCAL_FOLDER + uri.toString(), null, documentFile.getName()); + String title = documentFile.getName(); + if (title == null) { + title = getString(R.string.local_folder); + } + Feed dirFeed = new Feed(Feed.PREFIX_LOCAL_FOLDER + uri.toString(), null, title); dirFeed.setItems(Collections.emptyList()); dirFeed.setSortOrder(SortOrder.EPISODE_TITLE_A_Z); Feed fromDatabase = DBTasks.updateFeed(getContext(), dirFeed, false); diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/AllEpisodesFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/AllEpisodesFragment.java index 4423a2ebe..612959c04 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/AllEpisodesFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/AllEpisodesFragment.java @@ -104,13 +104,12 @@ public class AllEpisodesFragment extends EpisodesListFragment { @NonNull @Override protected List loadData() { - return feedItemFilter.filter(DBReader.getRecentlyPublishedEpisodes(0, page * EPISODES_PER_PAGE)); + return DBReader.getRecentlyPublishedEpisodes(0, page * EPISODES_PER_PAGE, feedItemFilter); } @NonNull @Override protected List loadMoreData() { - return feedItemFilter.filter(DBReader.getRecentlyPublishedEpisodes((page - 1) * EPISODES_PER_PAGE, - EPISODES_PER_PAGE)); + return DBReader.getRecentlyPublishedEpisodes((page - 1) * EPISODES_PER_PAGE, EPISODES_PER_PAGE, feedItemFilter); } } diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/AudioPlayerFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/AudioPlayerFragment.java index 82e2b3a6a..51f264e56 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/AudioPlayerFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/AudioPlayerFragment.java @@ -1,8 +1,6 @@ package de.danoeh.antennapod.fragment; -import android.content.Context; import android.content.Intent; -import android.content.SharedPreferences; import android.os.Bundle; import android.util.Log; import android.view.LayoutInflater; @@ -17,7 +15,9 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.AlertDialog; import androidx.appcompat.widget.Toolbar; +import androidx.cardview.widget.CardView; import androidx.fragment.app.Fragment; +import androidx.interpolator.view.animation.FastOutSlowInInterpolator; import androidx.viewpager2.adapter.FragmentStateAdapter; import androidx.viewpager2.widget.ViewPager2; import com.google.android.material.bottomsheet.BottomSheetBehavior; @@ -29,11 +29,14 @@ import de.danoeh.antennapod.activity.CastEnabledActivity; import de.danoeh.antennapod.activity.MainActivity; import de.danoeh.antennapod.core.event.FavoritesEvent; import de.danoeh.antennapod.core.event.PlaybackPositionEvent; +import de.danoeh.antennapod.core.feed.Chapter; +import de.danoeh.antennapod.core.event.UnreadItemsUpdateEvent; import de.danoeh.antennapod.core.feed.FeedItem; import de.danoeh.antennapod.core.feed.FeedMedia; import de.danoeh.antennapod.core.feed.util.PlaybackSpeedUtils; import de.danoeh.antennapod.core.preferences.UserPreferences; import de.danoeh.antennapod.core.service.playback.PlaybackService; +import de.danoeh.antennapod.core.util.ChapterUtils; import de.danoeh.antennapod.core.util.Converter; import de.danoeh.antennapod.core.util.IntentUtils; import de.danoeh.antennapod.core.util.TimeSpeedConverter; @@ -45,7 +48,8 @@ import de.danoeh.antennapod.dialog.SkipPreferenceDialog; import de.danoeh.antennapod.dialog.SleepTimerDialog; import de.danoeh.antennapod.dialog.VariableSpeedDialog; import de.danoeh.antennapod.menuhandler.FeedItemMenuHandler; -import de.danoeh.antennapod.view.PlaybackSpeedIndicatorView; +import de.danoeh.antennapod.view.ChapterSeekBar; +import de.danoeh.antennapod.ui.common.PlaybackSpeedIndicatorView; import io.reactivex.Maybe; import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.disposables.Disposable; @@ -62,14 +66,13 @@ import java.util.List; * Shows the audio player. */ public class AudioPlayerFragment extends Fragment implements - SeekBar.OnSeekBarChangeListener, Toolbar.OnMenuItemClickListener { + ChapterSeekBar.OnSeekBarChangeListener, Toolbar.OnMenuItemClickListener { public static final String TAG = "AudioPlayerFragment"; private static final int POS_COVER = 0; private static final int POS_DESCR = 1; private static final int POS_CHAPTERS = 2; private static final int NUM_CONTENT_FRAGMENTS = 3; - private static final String PREFS = "AudioPlayerFragmentPreferences"; - private static final String PREF_SHOW_TIME_LEFT = "showTimeLeft"; + public static final String PREFS = "AudioPlayerFragmentPreferences"; private static final float EPSILON = 0.001f; PlaybackSpeedIndicatorView butPlaybackSpeed; @@ -77,7 +80,7 @@ public class AudioPlayerFragment extends Fragment implements private ViewPager2 pager; private TextView txtvPosition; private TextView txtvLength; - private SeekBar sbPosition; + private ChapterSeekBar sbPosition; private ImageButton butRev; private TextView txtvRev; private ImageButton butPlay; @@ -86,6 +89,8 @@ public class AudioPlayerFragment extends Fragment implements private ImageButton butSkip; private Toolbar toolbar; private ProgressBar progressIndicator; + private CardView cardViewSeek; + private TextView txtvSeek; private PlaybackController controller; private Disposable disposable; @@ -122,6 +127,8 @@ public class AudioPlayerFragment extends Fragment implements txtvFF = root.findViewById(R.id.txtvFF); butSkip = root.findViewById(R.id.butSkip); progressIndicator = root.findViewById(R.id.progLoading); + cardViewSeek = root.findViewById(R.id.cardViewSeek); + txtvSeek = root.findViewById(R.id.txtvSeek); setupLengthTextView(); setupControlButtons(); @@ -168,12 +175,33 @@ public class AudioPlayerFragment extends Fragment implements return root; } - public void setHasChapters(boolean hasChapters) { + private void setHasChapters(boolean hasChapters) { this.hasChapters = hasChapters; tabLayoutMediator.detach(); tabLayoutMediator.attach(); } + private void setChapterDividers(Playable media) { + + if (media == null) { + return; + } + + float[] dividerPos = null; + + if (hasChapters) { + List chapters = media.getChapters(); + dividerPos = new float[chapters.size()]; + float duration = media.getDuration(); + + for (int i = 0; i < chapters.size(); i++) { + dividerPos[i] = chapters.get(i).getStart() / duration; + } + } + + sbPosition.setDividerPos(dividerPos); + } + public View getExternalPlayerHolder() { return getView().findViewById(R.id.playerFragment); } @@ -211,16 +239,25 @@ public class AudioPlayerFragment extends Fragment implements IntentUtils.sendLocalBroadcast(getActivity(), PlaybackService.ACTION_SKIP_CURRENT_EPISODE)); } + @Subscribe(threadMode = ThreadMode.MAIN) + public void onUnreadItemsUpdate(UnreadItemsUpdateEvent event) { + if (controller == null) { + return; + } + updatePosition(new PlaybackPositionEvent(controller.getPosition(), + controller.getDuration())); + } + private void setupLengthTextView() { - SharedPreferences prefs = getContext().getSharedPreferences(PREFS, Context.MODE_PRIVATE); - showTimeLeft = prefs.getBoolean(PREF_SHOW_TIME_LEFT, false); + showTimeLeft = UserPreferences.shouldShowRemainingTime(); txtvLength.setOnClickListener(v -> { if (controller == null) { return; } showTimeLeft = !showTimeLeft; - prefs.edit().putBoolean(PREF_SHOW_TIME_LEFT, showTimeLeft).apply(); - updatePosition(new PlaybackPositionEvent(controller.getPosition(), controller.getDuration())); + UserPreferences.setShowRemainTimeSetting(showTimeLeft); + updatePosition(new PlaybackPositionEvent(controller.getPosition(), + controller.getDuration())); }); } @@ -285,26 +322,21 @@ public class AudioPlayerFragment extends Fragment implements disposable = Maybe.create(emitter -> { Playable media = controller.getMedia(); if (media != null) { + ChapterUtils.loadChapters(media, getContext()); emitter.onSuccess(media); } else { emitter.onComplete(); } }) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(media -> updateUi((Playable) media), - error -> Log.e(TAG, Log.getStackTraceString(error)), - () -> updateUi(null)); + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(media -> updateUi((Playable) media), + error -> Log.e(TAG, Log.getStackTraceString(error)), + () -> updateUi(null)); } private PlaybackController newPlaybackController() { return new PlaybackController(getActivity()) { - - @Override - public void setupGUI() { - AudioPlayerFragment.this.loadMediaInfo(); - } - @Override public void onBufferStart() { progressIndicator.setVisibility(View.VISIBLE); @@ -352,9 +384,8 @@ public class AudioPlayerFragment extends Fragment implements } @Override - public boolean loadMediaInfo() { + public void loadMediaInfo() { AudioPlayerFragment.this.loadMediaInfo(); - return true; } @Override @@ -383,8 +414,15 @@ public class AudioPlayerFragment extends Fragment implements if (controller == null) { return; } + + if (media != null && media.getChapters() != null) { + setHasChapters(media.getChapters().size() > 0); + } else { + setHasChapters(false); + } updatePosition(new PlaybackPositionEvent(controller.getPosition(), controller.getDuration())); updatePlaybackSpeedButton(media); + setChapterDividers(media); setupOptionsMenu(media); } @@ -433,6 +471,7 @@ public class AudioPlayerFragment extends Fragment implements return; } txtvPosition.setText(Converter.getDurationStringLong(currentPosition)); + showTimeLeft = UserPreferences.shouldShowRemainingTime(); if (showTimeLeft) { txtvLength.setText("-" + Converter.getDurationStringLong(remainingTime)); } else { @@ -454,22 +493,22 @@ public class AudioPlayerFragment extends Fragment implements } if (fromUser) { float prog = progress / ((float) seekBar.getMax()); - int duration = controller.getDuration(); TimeSpeedConverter converter = new TimeSpeedConverter(controller.getCurrentPlaybackSpeedMultiplier()); - int position = converter.convert((int) (prog * duration)); - txtvPosition.setText(Converter.getDurationStringLong(position)); - - if (showTimeLeft && prog != 0) { - int timeLeft = converter.convert(duration - (int) (prog * duration)); - String length = "-" + Converter.getDurationStringLong(timeLeft); - txtvLength.setText(length); - } + int position = converter.convert((int) (prog * controller.getDuration())); + txtvSeek.setText(Converter.getDurationStringLong(position)); } } @Override public void onStartTrackingTouch(SeekBar seekBar) { // interrupt position Observer, restart later + cardViewSeek.setScaleX(.8f); + cardViewSeek.setScaleY(.8f); + cardViewSeek.animate() + .setInterpolator(new FastOutSlowInInterpolator()) + .alpha(1f).scaleX(1f).scaleY(1f) + .setDuration(200) + .start(); } @Override @@ -478,6 +517,13 @@ public class AudioPlayerFragment extends Fragment implements float prog = seekBar.getProgress() / ((float) seekBar.getMax()); controller.seekTo((int) (prog * controller.getDuration())); } + cardViewSeek.setScaleX(1f); + cardViewSeek.setScaleY(1f); + cardViewSeek.animate() + .setInterpolator(new FastOutSlowInInterpolator()) + .alpha(0f).scaleX(.8f).scaleY(.8f) + .setDuration(200) + .start(); } public void setupOptionsMenu(Playable media) { diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/ChaptersFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/ChaptersFragment.java index d781d0774..acda462bd 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/ChaptersFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/ChaptersFragment.java @@ -8,9 +8,9 @@ import android.view.ViewGroup; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; +import androidx.recyclerview.widget.DividerItemDecoration; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; -import com.yqritc.recyclerviewflexibledivider.HorizontalDividerItemDecoration; import de.danoeh.antennapod.R; import de.danoeh.antennapod.adapter.ChaptersListAdapter; import de.danoeh.antennapod.core.event.PlaybackPositionEvent; @@ -45,7 +45,8 @@ public class ChaptersFragment extends Fragment { RecyclerView recyclerView = root.findViewById(R.id.recyclerView); layoutManager = new LinearLayoutManager(getActivity()); recyclerView.setLayoutManager(layoutManager); - recyclerView.addItemDecoration(new HorizontalDividerItemDecoration.Builder(getActivity()).build()); + recyclerView.addItemDecoration(new DividerItemDecoration(recyclerView.getContext(), + layoutManager.getOrientation())); adapter = new ChaptersListAdapter(getActivity(), pos -> { if (controller.getStatus() != PlayerStatus.PLAYING) { @@ -71,13 +72,7 @@ public class ChaptersFragment extends Fragment { super.onStart(); controller = new PlaybackController(getActivity()) { @Override - public boolean loadMediaInfo() { - ChaptersFragment.this.loadMediaInfo(); - return true; - } - - @Override - public void setupGUI() { + public void loadMediaInfo() { ChaptersFragment.this.loadMediaInfo(); } @@ -123,7 +118,7 @@ public class ChaptersFragment extends Fragment { disposable = Maybe.create(emitter -> { Playable media = controller.getMedia(); if (media != null) { - media.loadChapterMarks(getContext()); + ChapterUtils.loadChapters(media, getContext()); emitter.onSuccess(media); } else { emitter.onComplete(); @@ -142,7 +137,6 @@ public class ChaptersFragment extends Fragment { return; } adapter.setMedia(media); - ((AudioPlayerFragment) getParentFragment()).setHasChapters(adapter.getItemCount() > 0); int positionOfCurrentChapter = getCurrentChapter(media); updateChapterSelection(positionOfCurrentChapter); } diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/CoverFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/CoverFragment.java index 648fc614a..d8c382cb2 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/CoverFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/CoverFragment.java @@ -13,11 +13,9 @@ import android.view.ViewGroup; import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.TextView; - import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; - import com.bumptech.glide.Glide; import com.bumptech.glide.load.resource.bitmap.FitCenter; import com.bumptech.glide.load.resource.bitmap.RoundedCorners; @@ -25,9 +23,11 @@ import com.bumptech.glide.RequestBuilder; import com.bumptech.glide.request.RequestOptions; import de.danoeh.antennapod.R; import de.danoeh.antennapod.core.event.PlaybackPositionEvent; +import de.danoeh.antennapod.core.feed.FeedMedia; import de.danoeh.antennapod.core.feed.util.ImageResourceUtils; import de.danoeh.antennapod.core.glide.ApGlideSettings; import de.danoeh.antennapod.core.util.ChapterUtils; +import de.danoeh.antennapod.core.util.DateUtils; import de.danoeh.antennapod.core.util.EmbeddedChapterImage; import de.danoeh.antennapod.core.util.playback.Playable; import de.danoeh.antennapod.core.util.playback.PlaybackController; @@ -35,6 +35,7 @@ import io.reactivex.Maybe; import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.disposables.Disposable; import io.reactivex.schedulers.Schedulers; +import org.apache.commons.lang3.StringUtils; import org.greenrobot.eventbus.EventBus; import org.greenrobot.eventbus.Subscribe; import org.greenrobot.eventbus.ThreadMode; @@ -93,7 +94,12 @@ public class CoverFragment extends Fragment { } private void displayMediaInfo(@NonNull Playable media) { - txtvPodcastTitle.setText(media.getFeedTitle()); + String pubDateStr = DateUtils.formatAbbrev(getActivity(), ((FeedMedia) media).getPubDate()); + txtvPodcastTitle.setText(StringUtils.stripToEmpty(media.getFeedTitle()) + + "\u00A0" + + "・" + + "\u00A0" + + StringUtils.replace(StringUtils.stripToEmpty(pubDateStr), " ", "\u00A0")); txtvEpisodeTitle.setText(media.getEpisodeTitle()); displayedChapterIndex = -2; // Force refresh displayCoverImage(media.getPosition()); @@ -111,13 +117,7 @@ public class CoverFragment extends Fragment { super.onStart(); controller = new PlaybackController(getActivity()) { @Override - public boolean loadMediaInfo() { - CoverFragment.this.loadMediaInfo(); - return true; - } - - @Override - public void setupGUI() { + public void loadMediaInfo() { CoverFragment.this.loadMediaInfo(); } }; @@ -151,23 +151,25 @@ public class CoverFragment extends Fragment { if (chapter != displayedChapterIndex) { displayedChapterIndex = chapter; + RequestOptions options = new RequestOptions() + .diskCacheStrategy(ApGlideSettings.AP_DISK_CACHE_STRATEGY) + .dontAnimate() + .transforms(new FitCenter(), + new RoundedCorners((int) (16 * getResources().getDisplayMetrics().density))); + RequestBuilder cover = Glide.with(this) - .load(ImageResourceUtils.getImageLocation(media)) - .apply(new RequestOptions() - .diskCacheStrategy(ApGlideSettings.AP_DISK_CACHE_STRATEGY) - .dontAnimate() - .transforms(new FitCenter(), - new RoundedCorners((int) (16 * getResources().getDisplayMetrics().density)))); + .load(media.getImageLocation()) + .error(Glide.with(this) + .load(ImageResourceUtils.getFallbackImageLocation(media)) + .apply(options)) + .apply(options); + if (chapter == -1 || TextUtils.isEmpty(media.getChapters().get(chapter).getImageUrl())) { cover.into(imgvCover); } else { Glide.with(this) .load(EmbeddedChapterImage.getModelFor(media, chapter)) - .apply(new RequestOptions() - .diskCacheStrategy(ApGlideSettings.AP_DISK_CACHE_STRATEGY) - .dontAnimate() - .transforms(new FitCenter(), - new RoundedCorners((int) (16 * getResources().getDisplayMetrics().density)))) + .apply(options) .thumbnail(cover) .error(cover) .into(imgvCover); @@ -208,7 +210,7 @@ public class CoverFragment extends Fragment { imgvCover.setLayoutParams(params); } } else { - double percentageHeight = ratio * 0.8; + double percentageHeight = ratio * 0.6; mainContainer.setOrientation(LinearLayout.HORIZONTAL); if (newConfig.screenHeightDp > 0) { params.height = (int) (convertDpToPixel(newConfig.screenHeightDp) * percentageHeight); diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/DownloadsFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/DownloadsFragment.java index ffb3e71fa..5c83cee57 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/DownloadsFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/DownloadsFragment.java @@ -28,16 +28,17 @@ public class DownloadsFragment extends PagedToolbarFragment { public static final String TAG = "DownloadsFragment"; public static final String ARG_SELECTED_TAB = "selected_tab"; + private static final String PREF_LAST_TAB_POSITION = "tab_position"; + private static final String KEY_UP_ARROW = "up_arrow"; public static final int POS_RUNNING = 0; private static final int POS_COMPLETED = 1; public static final int POS_LOG = 2; private static final int TOTAL_COUNT = 3; - private static final String PREF_LAST_TAB_POSITION = "tab_position"; - private ViewPager2 viewPager; private TabLayout tabLayout; + private boolean displayUpArrow; @Override public View onCreateView(@NonNull LayoutInflater inflater, @@ -48,7 +49,11 @@ public class DownloadsFragment extends PagedToolbarFragment { Toolbar toolbar = root.findViewById(R.id.toolbar); toolbar.setTitle(R.string.downloads_label); toolbar.inflateMenu(R.menu.downloads); - ((MainActivity) getActivity()).setupToolbarToggle(toolbar); + displayUpArrow = getParentFragmentManager().getBackStackEntryCount() != 0; + if (savedInstanceState != null) { + displayUpArrow = savedInstanceState.getBoolean(KEY_UP_ARROW); + } + ((MainActivity) getActivity()).setupToolbarToggle(toolbar, displayUpArrow); viewPager = root.findViewById(R.id.viewpager); viewPager.setAdapter(new DownloadsPagerAdapter(this)); @@ -81,6 +86,12 @@ public class DownloadsFragment extends PagedToolbarFragment { return root; } + @Override + public void onSaveInstanceState(@NonNull Bundle outState) { + outState.putBoolean(KEY_UP_ARROW, displayUpArrow); + super.onSaveInstanceState(outState); + } + @Override public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/EpisodesFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/EpisodesFragment.java index eff23f7a3..1ca5d524b 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/EpisodesFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/EpisodesFragment.java @@ -24,6 +24,7 @@ public class EpisodesFragment extends PagedToolbarFragment { public static final String TAG = "EpisodesFragment"; private static final String PREF_LAST_TAB_POSITION = "tab_position"; + private static final String KEY_UP_ARROW = "up_arrow"; private static final int POS_NEW_EPISODES = 0; private static final int POS_ALL_EPISODES = 1; @@ -31,6 +32,7 @@ public class EpisodesFragment extends PagedToolbarFragment { private static final int TOTAL_COUNT = 3; private TabLayout tabLayout; + private boolean displayUpArrow; public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -44,7 +46,11 @@ public class EpisodesFragment extends PagedToolbarFragment { toolbar.setTitle(R.string.episodes_label); toolbar.inflateMenu(R.menu.episodes); MenuItemUtils.setupSearchItem(toolbar.getMenu(), (MainActivity) getActivity(), 0, ""); - ((MainActivity) getActivity()).setupToolbarToggle(toolbar); + displayUpArrow = getParentFragmentManager().getBackStackEntryCount() != 0; + if (savedInstanceState != null) { + displayUpArrow = savedInstanceState.getBoolean(KEY_UP_ARROW); + } + ((MainActivity) getActivity()).setupToolbarToggle(toolbar, displayUpArrow); ViewPager2 viewPager = rootView.findViewById(R.id.viewpager); viewPager.setAdapter(new EpisodesPagerAdapter(this)); @@ -88,6 +94,12 @@ public class EpisodesFragment extends PagedToolbarFragment { editor.apply(); } + @Override + public void onSaveInstanceState(@NonNull Bundle outState) { + outState.putBoolean(KEY_UP_ARROW, displayUpArrow); + super.onSaveInstanceState(outState); + } + static class EpisodesPagerAdapter extends FragmentStateAdapter { EpisodesPagerAdapter(@NonNull Fragment fragment) { diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/EpisodesListFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/EpisodesListFragment.java index 8dae310ba..39f935bbe 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/EpisodesListFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/EpisodesListFragment.java @@ -383,6 +383,14 @@ public abstract class EpisodesListFragment extends Fragment { @NonNull protected abstract List loadData(); + /** + * Load a new page of data as defined by {@link #page} and {@link #EPISODES_PER_PAGE}. + * If the number of items returned is less than {@link #EPISODES_PER_PAGE}, + * it will be assumed that the underlying data is exhausted + * and this method will not be called again. + * + * @return The items from the next page of data + */ @NonNull protected abstract List loadMoreData(); } diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/ExternalPlayerFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/ExternalPlayerFragment.java index 5d701472f..d77935910 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/ExternalPlayerFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/ExternalPlayerFragment.java @@ -108,12 +108,7 @@ public class ExternalPlayerFragment extends Fragment { } @Override - public boolean loadMediaInfo() { - return ExternalPlayerFragment.this.loadMediaInfo(); - } - - @Override - public void setupGUI() { + public void loadMediaInfo() { ExternalPlayerFragment.this.loadMediaInfo(); } @@ -170,11 +165,11 @@ public class ExternalPlayerFragment extends Fragment { } } - private boolean loadMediaInfo() { + private void loadMediaInfo() { Log.d(TAG, "Loading media info"); if (controller == null) { Log.w(TAG, "loadMediaInfo was called while PlaybackController was null!"); - return false; + return; } if (disposable != null) { @@ -186,7 +181,6 @@ public class ExternalPlayerFragment extends Fragment { .subscribe(this::updateUi, error -> Log.e(TAG, Log.getStackTraceString(error)), () -> ((MainActivity) getActivity()).setPlayerVisible(false)); - return true; } private void updateUi(Playable media) { @@ -198,14 +192,19 @@ public class ExternalPlayerFragment extends Fragment { feedName.setText(media.getFeedTitle()); onPositionObserverUpdate(); + RequestOptions options = new RequestOptions() + .placeholder(R.color.light_gray) + .error(R.color.light_gray) + .diskCacheStrategy(ApGlideSettings.AP_DISK_CACHE_STRATEGY) + .fitCenter() + .dontAnimate(); + Glide.with(getActivity()) - .load(ImageResourceUtils.getImageLocation(media)) - .apply(new RequestOptions() - .placeholder(R.color.light_gray) - .error(R.color.light_gray) - .diskCacheStrategy(ApGlideSettings.AP_DISK_CACHE_STRATEGY) - .fitCenter() - .dontAnimate()) + .load(ImageResourceUtils.getEpisodeListImageLocation(media)) + .error(Glide.with(getActivity()) + .load(ImageResourceUtils.getFallbackImageLocation(media)) + .apply(options)) + .apply(options) .into(imgvCover); if (controller != null && controller.isPlayingVideoLocally()) { diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/FeedInfoFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/FeedInfoFragment.java index abb597e60..25ab925eb 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/FeedInfoFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/FeedInfoFragment.java @@ -45,7 +45,7 @@ import de.danoeh.antennapod.core.storage.DownloadRequestException; import de.danoeh.antennapod.core.storage.StatisticsItem; import de.danoeh.antennapod.core.util.Converter; import de.danoeh.antennapod.core.util.IntentUtils; -import de.danoeh.antennapod.core.util.ThemeUtils; +import de.danoeh.antennapod.ui.common.ThemeUtils; import de.danoeh.antennapod.core.util.syndication.HtmlToPlainText; import de.danoeh.antennapod.fragment.preferences.StatisticsFragment; import de.danoeh.antennapod.menuhandler.FeedMenuHandler; @@ -130,6 +130,8 @@ public class FeedInfoFragment extends Fragment implements Toolbar.OnMenuItemClic protected void doTint(Context themedContext) { toolbar.getMenu().findItem(R.id.visit_website_item) .setIcon(ThemeUtils.getDrawableFromAttr(themedContext, R.attr.location_web_site)); + toolbar.getMenu().findItem(R.id.share_parent) + .setIcon(ThemeUtils.getDrawableFromAttr(themedContext, R.attr.ic_share)); } }; iconTintManager.updateTint(); @@ -201,7 +203,7 @@ public class FeedInfoFragment extends Fragment implements Toolbar.OnMenuItemClic Log.d(TAG, "Author is " + feed.getAuthor()); Log.d(TAG, "URL is " + feed.getDownload_url()); Glide.with(getContext()) - .load(feed.getImageLocation()) + .load(feed.getImageUrl()) .apply(new RequestOptions() .placeholder(R.color.light_gray) .error(R.color.light_gray) @@ -210,7 +212,7 @@ public class FeedInfoFragment extends Fragment implements Toolbar.OnMenuItemClic .dontAnimate()) .into(imgvCover); Glide.with(getContext()) - .load(feed.getImageLocation()) + .load(feed.getImageUrl()) .apply(new RequestOptions() .placeholder(R.color.image_readability_tint) .error(R.color.image_readability_tint) @@ -284,9 +286,13 @@ public class FeedInfoFragment extends Fragment implements Toolbar.OnMenuItemClic } private void refreshToolbarState() { + boolean shareLinkVisible = feed != null && feed.getLink() != null; + boolean downloadUrlVisible = feed != null && !feed.isLocalFeed(); + toolbar.getMenu().findItem(R.id.reconnect_local_folder).setVisible(feed != null && feed.isLocalFeed()); - toolbar.getMenu().findItem(R.id.share_download_url_item).setVisible(feed != null && !feed.isLocalFeed()); - toolbar.getMenu().findItem(R.id.share_link_item).setVisible(feed != null && feed.getLink() != null); + toolbar.getMenu().findItem(R.id.share_download_url_item).setVisible(downloadUrlVisible); + toolbar.getMenu().findItem(R.id.share_link_item).setVisible(shareLinkVisible); + toolbar.getMenu().findItem(R.id.share_parent).setVisible(downloadUrlVisible || shareLinkVisible); toolbar.getMenu().findItem(R.id.visit_website_item).setVisible(feed != null && feed.getLink() != null && IntentUtils.isCallable(getContext(), new Intent(Intent.ACTION_VIEW, Uri.parse(feed.getLink())))); } diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/FeedItemlistFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/FeedItemlistFragment.java index 8e14214d2..acb929dd2 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/FeedItemlistFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/FeedItemlistFragment.java @@ -16,7 +16,6 @@ import android.widget.AdapterView; import android.widget.ImageButton; import android.widget.ImageView; import android.widget.ProgressBar; -import android.widget.RelativeLayout; import android.widget.TextView; import android.widget.Toast; import androidx.annotation.NonNull; @@ -57,8 +56,7 @@ import de.danoeh.antennapod.core.storage.DownloadRequestException; import de.danoeh.antennapod.core.storage.DownloadRequester; import de.danoeh.antennapod.core.util.FeedItemPermutors; import de.danoeh.antennapod.core.util.FeedItemUtil; -import de.danoeh.antennapod.core.util.Optional; -import de.danoeh.antennapod.core.util.ThemeUtils; +import de.danoeh.antennapod.ui.common.ThemeUtils; import de.danoeh.antennapod.core.util.gui.MoreContentListFooterUtil; import de.danoeh.antennapod.dialog.EpisodesApplyActionFragment; import de.danoeh.antennapod.dialog.FilterDialog; @@ -89,6 +87,7 @@ public class FeedItemlistFragment extends Fragment implements AdapterView.OnItem Toolbar.OnMenuItemClickListener { private static final String TAG = "ItemlistFragment"; private static final String ARGUMENT_FEED_ID = "argument.de.danoeh.antennapod.feed_id"; + private static final String KEY_UP_ARROW = "up_arrow"; private FeedItemListAdapter adapter; private MoreContentListFooterUtil nextPageLoader; @@ -106,6 +105,7 @@ public class FeedItemlistFragment extends Fragment implements AdapterView.OnItem private View header; private Toolbar toolbar; private ToolbarIconTintManager iconTintManager; + private boolean displayUpArrow; private long feedID; private Feed feed; @@ -146,7 +146,11 @@ public class FeedItemlistFragment extends Fragment implements AdapterView.OnItem toolbar = root.findViewById(R.id.toolbar); toolbar.inflateMenu(R.menu.feedlist); toolbar.setOnMenuItemClickListener(this); - ((MainActivity) getActivity()).setupToolbarToggle(toolbar); + displayUpArrow = getParentFragmentManager().getBackStackEntryCount() != 0; + if (savedInstanceState != null) { + displayUpArrow = savedInstanceState.getBoolean(KEY_UP_ARROW); + } + ((MainActivity) getActivity()).setupToolbarToggle(toolbar, displayUpArrow); refreshToolbarState(); recyclerView = root.findViewById(R.id.recyclerView); @@ -231,6 +235,12 @@ public class FeedItemlistFragment extends Fragment implements AdapterView.OnItem adapter = null; } + @Override + public void onSaveInstanceState(@NonNull Bundle outState) { + outState.putBoolean(KEY_UP_ARROW, displayUpArrow); + super.onSaveInstanceState(outState); + } + private final MenuItemUtils.UpdateRefreshMenuItemChecker updateRefreshMenuItemChecker = new MenuItemUtils.UpdateRefreshMenuItemChecker() { @Override public boolean isRefreshing() { @@ -451,10 +461,6 @@ public class FeedItemlistFragment extends Fragment implements AdapterView.OnItem if (feed.getItemFilter() != null) { FeedItemFilter filter = feed.getItemFilter(); if (filter.getValues().length > 0) { - if (feed.hasLastUpdateFailed()) { - RelativeLayout.LayoutParams p = (RelativeLayout.LayoutParams) txtvInformation.getLayoutParams(); - p.addRule(RelativeLayout.BELOW, R.id.txtvFailure); - } txtvInformation.setText("{md-info-outline} " + this.getString(R.string.filtered_label)); Iconify.addIcons(txtvInformation); txtvInformation.setOnClickListener((l) -> { @@ -514,7 +520,7 @@ public class FeedItemlistFragment extends Fragment implements AdapterView.OnItem private void loadFeedImage() { Glide.with(getActivity()) - .load(feed.getImageLocation()) + .load(feed.getImageUrl()) .apply(new RequestOptions() .placeholder(R.color.image_readability_tint) .error(R.color.image_readability_tint) @@ -524,7 +530,7 @@ public class FeedItemlistFragment extends Fragment implements AdapterView.OnItem .into(imgvBackground); Glide.with(getActivity()) - .load(feed.getImageLocation()) + .load(feed.getImageUrl()) .apply(new RequestOptions() .placeholder(R.color.light_gray) .error(R.color.light_gray) @@ -542,27 +548,32 @@ public class FeedItemlistFragment extends Fragment implements AdapterView.OnItem disposable = Observable.fromCallable(this::loadData) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) - .subscribe(result -> { - feed = result.orElse(null); - refreshHeaderView(); - displayList(); - }, error -> Log.e(TAG, Log.getStackTraceString(error))); + .subscribe( + result -> { + feed = result; + refreshHeaderView(); + displayList(); + }, error -> { + feed = null; + refreshHeaderView(); + displayList(); + Log.e(TAG, Log.getStackTraceString(error)); + }); } - @NonNull - private Optional loadData() { - Feed feed = DBReader.getFeed(feedID); - if (feed != null && feed.getItemFilter() != null) { - DBReader.loadAdditionalFeedItemListData(feed.getItems()); - FeedItemFilter filter = feed.getItemFilter(); - feed.setItems(filter.filter(feed.getItems())); + @Nullable + private Feed loadData() { + Feed feed = DBReader.getFeed(feedID, true); + if (feed == null) { + return null; } - if (feed != null && feed.getSortOrder() != null) { + DBReader.loadAdditionalFeedItemListData(feed.getItems()); + if (feed.getSortOrder() != null) { List feedItems = feed.getItems(); FeedItemPermutors.getPermutor(feed.getSortOrder()).reorder(feedItems); feed.setItems(feedItems); } - return Optional.ofNullable(feed); + return feed; } private static class FeedItemListAdapter extends EpisodeItemListAdapter { diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/FeedSettingsFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/FeedSettingsFragment.java index 1253a8ad2..c000107a7 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/FeedSettingsFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/FeedSettingsFragment.java @@ -164,6 +164,7 @@ public class FeedSettingsFragment extends Fragment { setupEpisodeFilterPreference(); setupPlaybackSpeedPreference(); setupFeedAutoSkipPreference(); + setupEpisodeNotificationPreference(); setupTags(); updateAutoDeleteSummary(); @@ -198,7 +199,7 @@ public class FeedSettingsFragment extends Fragment { protected void onConfirmed(int skipIntro, int skipEnding) { feedPreferences.setFeedSkipIntro(skipIntro); feedPreferences.setFeedSkipEnding(skipEnding); - feed.savePreferences(); + DBWriter.setFeedPreferences(feedPreferences); EventBus.getDefault().post( new SkipIntroEndingChangedEvent(feedPreferences.getFeedSkipIntro(), feedPreferences.getFeedSkipEnding(), @@ -226,7 +227,7 @@ public class FeedSettingsFragment extends Fragment { feedPlaybackSpeedPreference.setEntries(entries); feedPlaybackSpeedPreference.setOnPreferenceChangeListener((preference, newValue) -> { feedPreferences.setFeedPlaybackSpeed(Float.parseFloat((String) newValue)); - feed.savePreferences(); + DBWriter.setFeedPreferences(feedPreferences); updatePlaybackSpeedPreference(); EventBus.getDefault().post( new SpeedPresetChangedEvent(feedPreferences.getFeedPlaybackSpeed(), feed.getId())); @@ -240,7 +241,7 @@ public class FeedSettingsFragment extends Fragment { @Override protected void onConfirmed(FeedFilter filter) { feedPreferences.setFilter(filter); - feed.savePreferences(); + DBWriter.setFeedPreferences(feedPreferences); } }.show(); return false; @@ -256,7 +257,7 @@ public class FeedSettingsFragment extends Fragment { protected void onConfirmed(String username, String password) { feedPreferences.setUsername(username); feedPreferences.setPassword(password); - feed.savePreferences(); + DBWriter.setFeedPreferences(feedPreferences); } }.show(); return false; @@ -276,7 +277,7 @@ public class FeedSettingsFragment extends Fragment { feedPreferences.setAutoDeleteAction(FeedPreferences.AutoDeleteAction.NO); break; } - feed.savePreferences(); + DBWriter.setFeedPreferences(feedPreferences); updateAutoDeleteSummary(); return false; }); @@ -322,7 +323,7 @@ public class FeedSettingsFragment extends Fragment { feedPreferences.setVolumeAdaptionSetting(VolumeAdaptionSetting.HEAVY_REDUCTION); break; } - feed.savePreferences(); + DBWriter.setFeedPreferences(feedPreferences); updateVolumeReductionValue(); EventBus.getDefault().post( new VolumeAdaptionChangedEvent(feedPreferences.getVolumeAdaptionSetting(), feed.getId())); @@ -353,7 +354,7 @@ public class FeedSettingsFragment extends Fragment { pref.setOnPreferenceChangeListener((preference, newValue) -> { boolean checked = newValue == Boolean.TRUE; feedPreferences.setKeepUpdated(checked); - feed.savePreferences(); + DBWriter.setFeedPreferences(feedPreferences); pref.setChecked(checked); return false; }); @@ -384,7 +385,7 @@ public class FeedSettingsFragment extends Fragment { boolean checked = newValue == Boolean.TRUE; feedPreferences.setAutoDownload(checked); - feed.savePreferences(); + DBWriter.setFeedPreferences(feedPreferences); updateAutoDownloadEnabled(); ApplyToEpisodesDialog dialog = new ApplyToEpisodesDialog(getActivity(), checked); dialog.createNewDialog().show(); @@ -412,7 +413,7 @@ public class FeedSettingsFragment extends Fragment { feedPreferences.getTags().clear(); feedPreferences.getTags().addAll(new HashSet<>(Arrays.asList( foldersString.split(FeedPreferences.TAG_SEPARATOR)))); - feed.savePreferences(); + DBWriter.setFeedPreferences(feedPreferences); }) .setNegativeButton(R.string.cancel_label, null) .show(); @@ -420,6 +421,19 @@ public class FeedSettingsFragment extends Fragment { }); } + private void setupEpisodeNotificationPreference() { + SwitchPreferenceCompat pref = findPreference("episodeNotification"); + + pref.setChecked(feedPreferences.getShowEpisodeNotification()); + pref.setOnPreferenceChangeListener((preference, newValue) -> { + boolean checked = newValue == Boolean.TRUE; + feedPreferences.setShowEpisodeNotification(checked); + DBWriter.setFeedPreferences(feedPreferences); + pref.setChecked(checked); + return false; + }); + } + private class ApplyToEpisodesDialog extends ConfirmationDialog { private final boolean autoDownload; diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/ItemDescriptionFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/ItemDescriptionFragment.java index 18a61f1e6..2e13bbd79 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/ItemDescriptionFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/ItemDescriptionFragment.java @@ -10,6 +10,9 @@ import android.view.View; import android.view.ViewGroup; import androidx.fragment.app.Fragment; import de.danoeh.antennapod.R; +import de.danoeh.antennapod.core.feed.FeedMedia; +import de.danoeh.antennapod.core.storage.DBReader; +import de.danoeh.antennapod.core.util.playback.Playable; import de.danoeh.antennapod.core.util.playback.PlaybackController; import de.danoeh.antennapod.core.util.playback.Timeline; import de.danoeh.antennapod.view.ShownotesWebView; @@ -82,7 +85,15 @@ public class ItemDescriptionFragment extends Fragment { webViewLoader.dispose(); } webViewLoader = Maybe.create(emitter -> { - Timeline timeline = new Timeline(getActivity(), controller.getMedia()); + Playable media = controller.getMedia(); + if (media instanceof FeedMedia) { + FeedMedia feedMedia = ((FeedMedia) media); + if (feedMedia.getItem() == null) { + feedMedia.setItem(DBReader.getFeedItem(feedMedia.getItemId())); + } + DBReader.loadDescriptionOfFeedItem(feedMedia.getItem()); + } + Timeline timeline = new Timeline(getActivity(), media.getDescription(), media.getDuration()); emitter.onSuccess(timeline.processShownotes()); }).subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) @@ -140,14 +151,8 @@ public class ItemDescriptionFragment extends Fragment { super.onStart(); controller = new PlaybackController(getActivity()) { @Override - public boolean loadMediaInfo() { + public void loadMediaInfo() { load(); - return true; - } - - @Override - public void setupGUI() { - ItemDescriptionFragment.this.load(); } }; controller.init(); diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/ItemFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/ItemFragment.java index 07f59bb42..224210d63 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/ItemFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/ItemFragment.java @@ -57,7 +57,7 @@ import de.danoeh.antennapod.core.storage.DBReader; import de.danoeh.antennapod.core.storage.DownloadRequester; import de.danoeh.antennapod.core.util.Converter; import de.danoeh.antennapod.core.util.DateUtils; -import de.danoeh.antennapod.core.util.ThemeUtils; +import de.danoeh.antennapod.ui.common.ThemeUtils; import de.danoeh.antennapod.core.util.playback.PlaybackController; import de.danoeh.antennapod.core.util.playback.Timeline; import de.danoeh.antennapod.view.ShownotesWebView; @@ -238,7 +238,12 @@ public class ItemFragment extends Fragment { public void onStart() { super.onStart(); EventBus.getDefault().register(this); - controller = new PlaybackController(getActivity()); + controller = new PlaybackController(getActivity()) { + @Override + public void loadMediaInfo() { + // Do nothing + } + }; controller.init(); } @@ -291,14 +296,19 @@ public class ItemFragment extends Fragment { txtvPublished.setContentDescription(DateUtils.formatForAccessibility(getContext(), item.getPubDate())); } + RequestOptions options = new RequestOptions() + .error(R.color.light_gray) + .diskCacheStrategy(ApGlideSettings.AP_DISK_CACHE_STRATEGY) + .transforms(new FitCenter(), + new RoundedCorners((int) (4 * getResources().getDisplayMetrics().density))) + .dontAnimate(); + Glide.with(getActivity()) - .load(ImageResourceUtils.getImageLocation(item)) - .apply(new RequestOptions() - .error(R.color.light_gray) - .diskCacheStrategy(ApGlideSettings.AP_DISK_CACHE_STRATEGY) - .transforms(new FitCenter(), - new RoundedCorners((int) (4 * getResources().getDisplayMetrics().density))) - .dontAnimate()) + .load(item.getImageLocation()) + .error(Glide.with(getActivity()) + .load(ImageResourceUtils.getFallbackImageLocation(item)) + .apply(options)) + .apply(options) .into(imgvCover); updateButtons(); } @@ -429,7 +439,9 @@ public class ItemFragment extends Fragment { FeedItem feedItem = DBReader.getFeedItem(itemId); Context context = getContext(); if (feedItem != null && context != null) { - Timeline t = new Timeline(context, feedItem); + int duration = feedItem.getMedia() != null ? feedItem.getMedia().getDuration() : Integer.MAX_VALUE; + DBReader.loadDescriptionOfFeedItem(feedItem); + Timeline t = new Timeline(context, feedItem.getDescription(), duration); webviewData = t.processShownotes(); } return feedItem; diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/NavDrawerFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/NavDrawerFragment.java index 3d82bf7a1..e8c04336f 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/NavDrawerFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/NavDrawerFragment.java @@ -424,7 +424,7 @@ public class NavDrawerFragment extends Fragment implements SharedPreferences.OnS flatItemList = result.second; updateSelection(); // Selected item might be a feed navAdapter.notifyDataSetChanged(); - progressBar.setVisibility(View.GONE); + progressBar.setVisibility(View.GONE); // Stays hidden once there is something in the list }, error -> { Log.e(TAG, Log.getStackTraceString(error)); progressBar.setVisibility(View.GONE); diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/PlaybackHistoryFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/PlaybackHistoryFragment.java index 973fcb978..e97b7cd7f 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/PlaybackHistoryFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/PlaybackHistoryFragment.java @@ -41,6 +41,7 @@ import java.util.List; public class PlaybackHistoryFragment extends Fragment implements Toolbar.OnMenuItemClickListener { public static final String TAG = "PlaybackHistoryFragment"; + private static final String KEY_UP_ARROW = "up_arrow"; private List playbackHistory; private PlaybackHistoryListAdapter adapter; @@ -49,6 +50,7 @@ public class PlaybackHistoryFragment extends Fragment implements Toolbar.OnMenuI private EmptyViewHandler emptyView; private ProgressBar progressBar; private Toolbar toolbar; + private boolean displayUpArrow; @Override public void onCreate(Bundle savedInstanceState) { @@ -63,7 +65,11 @@ public class PlaybackHistoryFragment extends Fragment implements Toolbar.OnMenuI toolbar = root.findViewById(R.id.toolbar); toolbar.setTitle(R.string.playback_history_label); toolbar.setOnMenuItemClickListener(this); - ((MainActivity) getActivity()).setupToolbarToggle(toolbar); + displayUpArrow = getParentFragmentManager().getBackStackEntryCount() != 0; + if (savedInstanceState != null) { + displayUpArrow = savedInstanceState.getBoolean(KEY_UP_ARROW); + } + ((MainActivity) getActivity()).setupToolbarToggle(toolbar, displayUpArrow); toolbar.inflateMenu(R.menu.playback_history); refreshToolbarState(); @@ -98,6 +104,12 @@ public class PlaybackHistoryFragment extends Fragment implements Toolbar.OnMenuI } } + @Override + public void onSaveInstanceState(@NonNull Bundle outState) { + outState.putBoolean(KEY_UP_ARROW, displayUpArrow); + super.onSaveInstanceState(outState); + } + @Subscribe(threadMode = ThreadMode.MAIN) public void onEventMainThread(FeedItemEvent event) { Log.d(TAG, "onEventMainThread() called with: " + "event = [" + event + "]"); diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/QueueFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/QueueFragment.java index 983bf4de1..2850acc15 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/QueueFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/QueueFragment.java @@ -12,6 +12,7 @@ import android.view.ViewGroup; import android.widget.CheckBox; import android.widget.ProgressBar; import android.widget.TextView; +import androidx.annotation.NonNull; import androidx.appcompat.app.AlertDialog; import androidx.appcompat.widget.Toolbar; import androidx.fragment.app.Fragment; @@ -67,6 +68,7 @@ import static de.danoeh.antennapod.dialog.EpisodesApplyActionFragment.ACTION_REM */ public class QueueFragment extends Fragment implements Toolbar.OnMenuItemClickListener { public static final String TAG = "QueueFragment"; + private static final String KEY_UP_ARROW = "up_arrow"; private TextView infoBar; private EpisodeItemListRecyclerView recyclerView; @@ -74,6 +76,7 @@ public class QueueFragment extends Fragment implements Toolbar.OnMenuItemClickLi private EmptyViewHandler emptyView; private ProgressBar progLoading; private Toolbar toolbar; + private boolean displayUpArrow; private List queue; @@ -420,7 +423,11 @@ public class QueueFragment extends Fragment implements Toolbar.OnMenuItemClickLi View root = inflater.inflate(R.layout.queue_fragment, container, false); toolbar = root.findViewById(R.id.toolbar); toolbar.setOnMenuItemClickListener(this); - ((MainActivity) getActivity()).setupToolbarToggle(toolbar); + displayUpArrow = getParentFragmentManager().getBackStackEntryCount() != 0; + if (savedInstanceState != null) { + displayUpArrow = savedInstanceState.getBoolean(KEY_UP_ARROW); + } + ((MainActivity) getActivity()).setupToolbarToggle(toolbar, displayUpArrow); toolbar.inflateMenu(R.menu.queue); MenuItemUtils.setupSearchItem(toolbar.getMenu(), (MainActivity) getActivity(), 0, ""); refreshToolbarState(); @@ -530,6 +537,12 @@ public class QueueFragment extends Fragment implements Toolbar.OnMenuItemClickLi return root; } + @Override + public void onSaveInstanceState(@NonNull Bundle outState) { + outState.putBoolean(KEY_UP_ARROW, displayUpArrow); + super.onSaveInstanceState(outState); + } + private void onFragmentLoaded(final boolean restoreScrollPosition) { if (queue != null && queue.size() > 0) { if (recyclerAdapter == null) { diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/SubscriptionFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/SubscriptionFragment.java index bb00d88e1..3c529d941 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/SubscriptionFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/SubscriptionFragment.java @@ -8,6 +8,7 @@ import android.os.Bundle; import android.os.Handler; import android.os.Looper; import android.widget.ProgressBar; +import androidx.annotation.NonNull; import androidx.annotation.StringRes; import androidx.appcompat.widget.Toolbar; import androidx.fragment.app.Fragment; @@ -67,6 +68,8 @@ public class SubscriptionFragment extends Fragment implements Toolbar.OnMenuItem public static final String TAG = "SubscriptionFragment"; private static final String PREFS = "SubscriptionFragment"; private static final String PREF_NUM_COLUMNS = "columns"; + private static final String KEY_UP_ARROW = "up_arrow"; + private static final int MIN_NUM_COLUMNS = 2; private static final int[] COLUMN_CHECKBOX_IDS = { R.id.subscription_num_columns_2, @@ -85,6 +88,7 @@ public class SubscriptionFragment extends Fragment implements Toolbar.OnMenuItem private int mPosition = -1; private boolean isUpdatingFeeds = false; + private boolean displayUpArrow; private Disposable disposable; private SharedPreferences prefs; @@ -103,7 +107,11 @@ public class SubscriptionFragment extends Fragment implements Toolbar.OnMenuItem View root = inflater.inflate(R.layout.fragment_subscriptions, container, false); toolbar = root.findViewById(R.id.toolbar); toolbar.setOnMenuItemClickListener(this); - ((MainActivity) getActivity()).setupToolbarToggle(toolbar); + displayUpArrow = getParentFragmentManager().getBackStackEntryCount() != 0; + if (savedInstanceState != null) { + displayUpArrow = savedInstanceState.getBoolean(KEY_UP_ARROW); + } + ((MainActivity) getActivity()).setupToolbarToggle(toolbar, displayUpArrow); toolbar.inflateMenu(R.menu.subscriptions); for (int i = 0; i < COLUMN_CHECKBOX_IDS.length; i++) { // Do this in Java to localize numbers @@ -130,6 +138,12 @@ public class SubscriptionFragment extends Fragment implements Toolbar.OnMenuItem return root; } + @Override + public void onSaveInstanceState(@NonNull Bundle outState) { + outState.putBoolean(KEY_UP_ARROW, displayUpArrow); + super.onSaveInstanceState(outState); + } + private void refreshToolbarState() { int columns = prefs.getInt(PREF_NUM_COLUMNS, getDefaultNumOfColumns()); toolbar.getMenu().findItem(COLUMN_CHECKBOX_IDS[columns - MIN_NUM_COLUMNS]).setChecked(true); @@ -218,16 +232,19 @@ public class SubscriptionFragment extends Fragment implements Toolbar.OnMenuItem disposable.dispose(); } emptyView.hide(); - progressBar.setVisibility(View.VISIBLE); disposable = Observable.fromCallable(DBReader::getNavDrawerData) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) - .subscribe(result -> { - navDrawerData = result; - subscriptionAdapter.notifyDataSetChanged(); - emptyView.updateVisibility(); - progressBar.setVisibility(View.GONE); - }, error -> Log.e(TAG, Log.getStackTraceString(error))); + .subscribe( + result -> { + navDrawerData = result; + subscriptionAdapter.notifyDataSetChanged(); + emptyView.updateVisibility(); + progressBar.setVisibility(View.GONE); // Keep hidden to avoid flickering while refreshing + }, error -> { + Log.e(TAG, Log.getStackTraceString(error)); + progressBar.setVisibility(View.GONE); + }); if (UserPreferences.getSubscriptionsFilter().isEnabled()) { feedsFilteredMsg.setText("{md-info-outline} " + getString(R.string.subscriptions_are_filtered)); diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/gpodnet/PodcastListFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/gpodnet/PodcastListFragment.java index 1f5434688..7ee0936d0 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/gpodnet/PodcastListFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/gpodnet/PodcastListFragment.java @@ -1,10 +1,7 @@ package de.danoeh.antennapod.fragment.gpodnet; -import android.content.Context; import android.content.Intent; -import android.os.AsyncTask; import android.os.Bundle; -import androidx.fragment.app.Fragment; import android.util.Log; import android.view.LayoutInflater; import android.view.View; @@ -13,9 +10,7 @@ import android.widget.Button; import android.widget.GridView; import android.widget.ProgressBar; import android.widget.TextView; - -import java.util.List; - +import androidx.fragment.app.Fragment; import de.danoeh.antennapod.R; import de.danoeh.antennapod.activity.MainActivity; import de.danoeh.antennapod.activity.OnlineFeedViewActivity; @@ -25,6 +20,12 @@ import de.danoeh.antennapod.core.service.download.AntennapodHttpClient; import de.danoeh.antennapod.core.sync.gpoddernet.GpodnetService; import de.danoeh.antennapod.core.sync.gpoddernet.GpodnetServiceException; import de.danoeh.antennapod.core.sync.gpoddernet.model.GpodnetPodcast; +import io.reactivex.Observable; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.disposables.Disposable; +import io.reactivex.schedulers.Schedulers; + +import java.util.List; /** * Displays a list of GPodnetPodcast-Objects in a GridView @@ -36,6 +37,7 @@ public abstract class PodcastListFragment extends Fragment { private ProgressBar progressBar; private TextView txtvError; private Button butRetry; + private Disposable disposable; @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { @@ -64,60 +66,44 @@ public abstract class PodcastListFragment extends Fragment { protected abstract List loadPodcastData(GpodnetService service) throws GpodnetServiceException; final void loadData() { - AsyncTask> loaderTask = new AsyncTask>() { - volatile Exception exception = null; - - @Override - protected List doInBackground(Void... params) { - try { + if (disposable != null) { + disposable.dispose(); + } + gridView.setVisibility(View.GONE); + progressBar.setVisibility(View.VISIBLE); + txtvError.setVisibility(View.GONE); + butRetry.setVisibility(View.GONE); + disposable = Observable.fromCallable( + () -> { GpodnetService service = new GpodnetService(AntennapodHttpClient.getHttpClient(), - GpodnetPreferences.getHostname()); + GpodnetPreferences.getHosturl()); return loadPodcastData(service); - } catch (GpodnetServiceException e) { - exception = e; - e.printStackTrace(); - return null; - } - } + }) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + podcasts -> { + progressBar.setVisibility(View.GONE); + butRetry.setVisibility(View.GONE); - @Override - protected void onPostExecute(List gpodnetPodcasts) { - super.onPostExecute(gpodnetPodcasts); - final Context context = getActivity(); - if (context != null && gpodnetPodcasts != null && gpodnetPodcasts.size() > 0) { - PodcastListAdapter listAdapter = new PodcastListAdapter(context, 0, gpodnetPodcasts); - gridView.setAdapter(listAdapter); - listAdapter.notifyDataSetChanged(); - - progressBar.setVisibility(View.GONE); - gridView.setVisibility(View.VISIBLE); - txtvError.setVisibility(View.GONE); - butRetry.setVisibility(View.GONE); - } else if (context != null && gpodnetPodcasts != null) { - gridView.setVisibility(View.GONE); - progressBar.setVisibility(View.GONE); - txtvError.setText(getString(R.string.search_status_no_results)); - txtvError.setVisibility(View.VISIBLE); - butRetry.setVisibility(View.GONE); - } else if (context != null) { - gridView.setVisibility(View.GONE); - progressBar.setVisibility(View.GONE); - txtvError.setText(getString(R.string.error_msg_prefix) + exception.getMessage()); - txtvError.setVisibility(View.VISIBLE); - butRetry.setVisibility(View.VISIBLE); - } - } - - @Override - protected void onPreExecute() { - super.onPreExecute(); - gridView.setVisibility(View.GONE); - progressBar.setVisibility(View.VISIBLE); - txtvError.setVisibility(View.GONE); - butRetry.setVisibility(View.GONE); - } - }; - - loaderTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + if (podcasts.size() > 0) { + PodcastListAdapter listAdapter = new PodcastListAdapter(getContext(), 0, podcasts); + gridView.setAdapter(listAdapter); + listAdapter.notifyDataSetChanged(); + gridView.setVisibility(View.VISIBLE); + txtvError.setVisibility(View.GONE); + } else { + gridView.setVisibility(View.GONE); + txtvError.setText(getString(R.string.search_status_no_results)); + txtvError.setVisibility(View.VISIBLE); + } + }, error -> { + gridView.setVisibility(View.GONE); + progressBar.setVisibility(View.GONE); + txtvError.setText(getString(R.string.error_msg_prefix) + error.getMessage()); + txtvError.setVisibility(View.VISIBLE); + butRetry.setVisibility(View.VISIBLE); + Log.e(TAG, Log.getStackTraceString(error)); + }); } } diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/gpodnet/TagListFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/gpodnet/TagListFragment.java index 2c41ee070..9d0f99aa9 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/gpodnet/TagListFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/gpodnet/TagListFragment.java @@ -1,32 +1,34 @@ package de.danoeh.antennapod.fragment.gpodnet; -import android.content.Context; -import android.os.AsyncTask; import android.os.Bundle; +import android.util.Log; import android.view.View; import android.widget.TextView; +import androidx.annotation.NonNull; import androidx.fragment.app.ListFragment; import de.danoeh.antennapod.activity.MainActivity; import de.danoeh.antennapod.adapter.gpodnet.TagListAdapter; import de.danoeh.antennapod.core.preferences.GpodnetPreferences; import de.danoeh.antennapod.core.service.download.AntennapodHttpClient; import de.danoeh.antennapod.core.sync.gpoddernet.GpodnetService; -import de.danoeh.antennapod.core.sync.gpoddernet.GpodnetServiceException; import de.danoeh.antennapod.core.sync.gpoddernet.model.GpodnetTag; - -import java.util.List; +import io.reactivex.Observable; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.disposables.Disposable; +import io.reactivex.schedulers.Schedulers; public class TagListFragment extends ListFragment { private static final int COUNT = 50; + private static final String TAG = "TagListFragment"; + private Disposable disposable; @Override - public void onViewCreated(View view, Bundle savedInstanceState) { + public void onViewCreated(@NonNull View view, Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); getListView().setOnItemClickListener((parent, view1, position, id) -> { GpodnetTag tag = (GpodnetTag) getListAdapter().getItem(position); - MainActivity activity = (MainActivity) getActivity(); - activity.loadChildFragment(TagFragment.newInstance(tag)); + ((MainActivity) getActivity()).loadChildFragment(TagFragment.newInstance(tag)); }); startLoadTask(); @@ -35,59 +37,36 @@ public class TagListFragment extends ListFragment { @Override public void onDestroyView() { super.onDestroyView(); - cancelLoadTask(); - } - private AsyncTask> loadTask; - - private void cancelLoadTask() { - if (loadTask != null && !loadTask.isCancelled()) { - loadTask.cancel(true); + if (disposable != null) { + disposable.dispose(); } } private void startLoadTask() { - cancelLoadTask(); - loadTask = new AsyncTask>() { - private Exception exception; - - @Override - protected List doInBackground(Void... params) { + if (disposable != null) { + disposable.dispose(); + } + setListShown(false); + disposable = Observable.fromCallable( + () -> { GpodnetService service = new GpodnetService(AntennapodHttpClient.getHttpClient(), - GpodnetPreferences.getHostname()); - try { - return service.getTopTags(COUNT); - } catch (GpodnetServiceException e) { - e.printStackTrace(); - exception = e; - return null; - } - } - - @Override - protected void onPreExecute() { - super.onPreExecute(); - setListShown(false); - } - - @Override - protected void onPostExecute(List gpodnetTags) { - super.onPostExecute(gpodnetTags); - final Context context = getActivity(); - if (context != null) { - if (gpodnetTags != null) { - setListAdapter(new TagListAdapter(context, android.R.layout.simple_list_item_1, gpodnetTags)); - } else if (exception != null) { + GpodnetPreferences.getHosturl()); + return service.getTopTags(COUNT); + }) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + tags -> { + setListAdapter(new TagListAdapter(getContext(), android.R.layout.simple_list_item_1, tags)); + setListShown(true); + }, error -> { TextView txtvError = new TextView(getActivity()); - txtvError.setText(exception.getMessage()); + txtvError.setText(error.getMessage()); getListView().setEmptyView(txtvError); - } - setListShown(true); - - } - } - }; - loadTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + setListShown(true); + Log.e(TAG, Log.getStackTraceString(error)); + }); } } diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/preferences/AutoDownloadPreferencesFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/preferences/AutoDownloadPreferencesFragment.java index 0d6e79e84..ec61c82f2 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/preferences/AutoDownloadPreferencesFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/preferences/AutoDownloadPreferencesFragment.java @@ -174,7 +174,9 @@ public class AutoDownloadPreferencesFragment extends PreferenceFragmentCompat { String[] entries = new String[values.length]; for (int x = 0; x < values.length; x++) { int v = Integer.parseInt(values[x]); - if (v == UserPreferences.EPISODE_CLEANUP_QUEUE) { + if (v == UserPreferences.EPISODE_CLEANUP_EXCEPT_FAVORITE) { + entries[x] = res.getString(R.string.episode_cleanup_except_favorite_removal); + } else if (v == UserPreferences.EPISODE_CLEANUP_QUEUE) { entries[x] = res.getString(R.string.episode_cleanup_queue_removal); } else if (v == UserPreferences.EPISODE_CLEANUP_NULL){ entries[x] = res.getString(R.string.episode_cleanup_never); diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/preferences/GpodderAuthenticationFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/preferences/GpodderAuthenticationFragment.java new file mode 100644 index 000000000..6eb19aff2 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/fragment/preferences/GpodderAuthenticationFragment.java @@ -0,0 +1,307 @@ +package de.danoeh.antennapod.fragment.preferences; + +import android.app.Dialog; +import android.content.Context; +import android.graphics.Paint; +import android.os.Build; +import android.os.Bundle; +import android.view.View; +import android.view.inputmethod.EditorInfo; +import android.view.inputmethod.InputMethodManager; +import android.widget.Button; +import android.widget.EditText; +import android.widget.LinearLayout; +import android.widget.ProgressBar; +import android.widget.RadioGroup; +import android.widget.TextView; +import android.widget.ViewFlipper; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.app.AlertDialog; +import androidx.fragment.app.DialogFragment; +import com.google.android.material.button.MaterialButton; +import com.google.android.material.textfield.TextInputLayout; +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.core.preferences.GpodnetPreferences; +import de.danoeh.antennapod.core.service.download.AntennapodHttpClient; +import de.danoeh.antennapod.core.sync.SyncService; +import de.danoeh.antennapod.core.sync.gpoddernet.GpodnetService; +import de.danoeh.antennapod.core.sync.gpoddernet.model.GpodnetDevice; +import de.danoeh.antennapod.core.util.FileNameGenerator; +import de.danoeh.antennapod.core.util.IntentUtils; +import io.reactivex.Completable; +import io.reactivex.Observable; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.schedulers.Schedulers; + +import java.util.List; +import java.util.Locale; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Guides the user through the authentication process. + */ +public class GpodderAuthenticationFragment extends DialogFragment { + public static final String TAG = "GpodnetAuthActivity"; + + private ViewFlipper viewFlipper; + + private static final int STEP_DEFAULT = -1; + private static final int STEP_HOSTNAME = 0; + private static final int STEP_LOGIN = 1; + private static final int STEP_DEVICE = 2; + private static final int STEP_FINISH = 3; + + private int currentStep = -1; + + private GpodnetService service; + private volatile String username; + private volatile String password; + private volatile GpodnetDevice selectedDevice; + private List devices; + + @NonNull + @Override + public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) { + AlertDialog.Builder dialog = new AlertDialog.Builder(getContext()); + dialog.setTitle(GpodnetService.DEFAULT_BASE_HOST); + dialog.setNegativeButton(R.string.cancel_label, null); + dialog.setCancelable(false); + this.setCancelable(false); + + View root = View.inflate(getContext(), R.layout.gpodnetauth_dialog, null); + viewFlipper = root.findViewById(R.id.viewflipper); + advance(); + dialog.setView(root); + + return dialog.create(); + } + + private void setupHostView(View view) { + final Button selectHost = view.findViewById(R.id.chooseHostButton); + final RadioGroup serverRadioGroup = view.findViewById(R.id.serverRadioGroup); + final EditText serverUrlText = view.findViewById(R.id.serverUrlText); + + if (!GpodnetService.DEFAULT_BASE_HOST.equals(GpodnetPreferences.getHosturl())) { + serverUrlText.setText(GpodnetPreferences.getHosturl()); + } + final TextInputLayout serverUrlTextInput = view.findViewById(R.id.serverUrlTextInput); + serverRadioGroup.setOnCheckedChangeListener((group, checkedId) -> { + serverUrlTextInput.setVisibility(checkedId == R.id.customServerRadio ? View.VISIBLE : View.GONE); + }); + selectHost.setOnClickListener(v -> { + if (serverRadioGroup.getCheckedRadioButtonId() == R.id.customServerRadio) { + GpodnetPreferences.setHosturl(serverUrlText.getText().toString()); + } else { + GpodnetPreferences.setHosturl(GpodnetService.DEFAULT_BASE_HOST); + } + service = new GpodnetService(AntennapodHttpClient.getHttpClient(), GpodnetPreferences.getHosturl()); + getDialog().setTitle(GpodnetPreferences.getHosturl()); + advance(); + }); + } + + private void setupLoginView(View view) { + final EditText username = view.findViewById(R.id.etxtUsername); + final EditText password = view.findViewById(R.id.etxtPassword); + final Button login = view.findViewById(R.id.butLogin); + final TextView txtvError = view.findViewById(R.id.credentialsError); + final ProgressBar progressBar = view.findViewById(R.id.progBarLogin); + final TextView createAccount = view.findViewById(R.id.createAccountButton); + final TextView createAccountWarning = view.findViewById(R.id.createAccountWarning); + + createAccount.setPaintFlags(createAccount.getPaintFlags() | Paint.UNDERLINE_TEXT_FLAG); + createAccount.setOnClickListener(v -> IntentUtils.openInBrowser(getContext(), "https://gpodder.net/register/")); + + if (GpodnetPreferences.getHosturl().startsWith("http://")) { + createAccountWarning.setVisibility(View.VISIBLE); + } + password.setOnEditorActionListener((v, actionID, event) -> + actionID == EditorInfo.IME_ACTION_GO && login.performClick()); + + login.setOnClickListener(v -> { + final String usernameStr = username.getText().toString(); + final String passwordStr = password.getText().toString(); + + if (usernameHasUnwantedChars(usernameStr)) { + txtvError.setText(R.string.gpodnetsync_username_characters_error); + txtvError.setVisibility(View.VISIBLE); + return; + } + + login.setEnabled(false); + progressBar.setVisibility(View.VISIBLE); + txtvError.setVisibility(View.GONE); + InputMethodManager inputManager = (InputMethodManager) getContext() + .getSystemService(Context.INPUT_METHOD_SERVICE); + inputManager.hideSoftInputFromWindow(login.getWindowToken(), InputMethodManager.HIDE_NOT_ALWAYS); + + Completable.fromAction(() -> { + service.authenticate(usernameStr, passwordStr); + devices = service.getDevices(); + GpodderAuthenticationFragment.this.username = usernameStr; + GpodderAuthenticationFragment.this.password = passwordStr; + }) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(() -> { + login.setEnabled(true); + progressBar.setVisibility(View.GONE); + advance(); + }, error -> { + login.setEnabled(true); + progressBar.setVisibility(View.GONE); + txtvError.setText(error.getCause().getMessage()); + txtvError.setVisibility(View.VISIBLE); + }); + + }); + } + + private void setupDeviceView(View view) { + final EditText deviceName = view.findViewById(R.id.deviceName); + final LinearLayout devicesContainer = view.findViewById(R.id.devicesContainer); + deviceName.setText(generateDeviceName()); + + MaterialButton createDeviceButton = view.findViewById(R.id.createDeviceButton); + createDeviceButton.setOnClickListener(v -> createDevice(view)); + + for (GpodnetDevice device : devices) { + View row = View.inflate(getContext(), R.layout.gpodnetauth_device_row, null); + Button selectDeviceButton = row.findViewById(R.id.selectDeviceButton); + selectDeviceButton.setOnClickListener(v -> { + selectedDevice = device; + advance(); + }); + selectDeviceButton.setText(device.getCaption()); + devicesContainer.addView(row); + } + } + + private void createDevice(View view) { + final EditText deviceName = view.findViewById(R.id.deviceName); + final TextView txtvError = view.findViewById(R.id.deviceSelectError); + final ProgressBar progBarCreateDevice = view.findViewById(R.id.progbarCreateDevice); + + String deviceNameStr = deviceName.getText().toString(); + if (isDeviceInList(deviceNameStr)) { + return; + } + progBarCreateDevice.setVisibility(View.VISIBLE); + txtvError.setVisibility(View.GONE); + deviceName.setEnabled(false); + + Observable.fromCallable(() -> { + String deviceId = generateDeviceId(deviceNameStr); + service.configureDevice(deviceId, deviceNameStr, GpodnetDevice.DeviceType.MOBILE); + return new GpodnetDevice(deviceId, deviceNameStr, GpodnetDevice.DeviceType.MOBILE.toString(), 0); + }) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(device -> { + progBarCreateDevice.setVisibility(View.GONE); + selectedDevice = device; + advance(); + }, error -> { + deviceName.setEnabled(true); + progBarCreateDevice.setVisibility(View.GONE); + txtvError.setText(error.getMessage()); + txtvError.setVisibility(View.VISIBLE); + }); + } + + private String generateDeviceName() { + String baseName = getString(R.string.gpodnetauth_device_name_default, Build.MODEL); + String name = baseName; + int num = 1; + while (isDeviceInList(name)) { + name = baseName + " (" + num + ")"; + num++; + } + return name; + } + + private String generateDeviceId(String name) { + // devices names must be of a certain form: + // https://gpoddernet.readthedocs.org/en/latest/api/reference/general.html#devices + return FileNameGenerator.generateFileName(name).replaceAll("\\W", "_").toLowerCase(Locale.US); + } + + private boolean isDeviceInList(String name) { + if (devices == null) { + return false; + } + String id = generateDeviceId(name); + for (GpodnetDevice device : devices) { + if (device.getId().equals(id) || device.getCaption().equals(name)) { + return true; + } + } + return false; + } + + private GpodnetDevice findDevice(String id) { + if (devices == null) { + return null; + } + for (GpodnetDevice device : devices) { + if (device.getId().equals(id)) { + return device; + } + } + return null; + } + + private void setupFinishView(View view) { + final Button sync = view.findViewById(R.id.butSyncNow); + + sync.setOnClickListener(v -> { + dismiss(); + SyncService.sync(getContext()); + }); + } + + private void writeLoginCredentials() { + GpodnetPreferences.setUsername(username); + GpodnetPreferences.setPassword(password); + GpodnetPreferences.setDeviceID(selectedDevice.getId()); + } + + private void advance() { + if (currentStep < STEP_FINISH) { + + View view = viewFlipper.getChildAt(currentStep + 1); + if (currentStep == STEP_DEFAULT) { + setupHostView(view); + } else if (currentStep == STEP_HOSTNAME) { + setupLoginView(view); + } else if (currentStep == STEP_LOGIN) { + if (username == null || password == null) { + throw new IllegalStateException("Username and password must not be null here"); + } else { + setupDeviceView(view); + } + } else if (currentStep == STEP_DEVICE) { + if (selectedDevice == null) { + throw new IllegalStateException("Device must not be null here"); + } else { + writeLoginCredentials(); + setupFinishView(view); + } + } + if (currentStep != STEP_DEFAULT) { + viewFlipper.showNext(); + } + currentStep++; + } else { + dismiss(); + } + } + + private boolean usernameHasUnwantedChars(String username) { + Pattern special = Pattern.compile("[!@#$%&*()+=|<>?{}\\[\\]~]"); + Matcher containsUnwantedChars = special.matcher(username); + return containsUnwantedChars.find(); + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/preferences/GpodderPreferencesFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/preferences/GpodderPreferencesFragment.java index eb23a5eb1..4fb734e17 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/preferences/GpodderPreferencesFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/preferences/GpodderPreferencesFragment.java @@ -14,19 +14,16 @@ import de.danoeh.antennapod.core.event.SyncServiceEvent; import de.danoeh.antennapod.core.preferences.GpodnetPreferences; import de.danoeh.antennapod.core.sync.SyncService; import de.danoeh.antennapod.dialog.AuthenticationDialog; -import de.danoeh.antennapod.dialog.GpodnetSetHostnameDialog; import org.greenrobot.eventbus.EventBus; import org.greenrobot.eventbus.Subscribe; import org.greenrobot.eventbus.ThreadMode; - public class GpodderPreferencesFragment extends PreferenceFragmentCompat { private static final String PREF_GPODNET_LOGIN = "pref_gpodnet_authenticate"; private static final String PREF_GPODNET_SETLOGIN_INFORMATION = "pref_gpodnet_setlogin_information"; private static final String PREF_GPODNET_SYNC = "pref_gpodnet_sync"; private static final String PREF_GPODNET_FORCE_FULL_SYNC = "pref_gpodnet_force_full_sync"; private static final String PREF_GPODNET_LOGOUT = "pref_gpodnet_logout"; - private static final String PREF_GPODNET_HOSTNAME = "pref_gpodnet_hostname"; @Override public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { @@ -51,6 +48,7 @@ public class GpodderPreferencesFragment extends PreferenceFragmentCompat { @Subscribe(threadMode = ThreadMode.MAIN, sticky = true) public void syncStatusChanged(SyncServiceEvent event) { + updateGpodnetPreferenceScreen(); if (!GpodnetPreferences.loggedIn()) { return; } @@ -66,6 +64,10 @@ public class GpodderPreferencesFragment extends PreferenceFragmentCompat { private void setupGpodderScreen() { final Activity activity = getActivity(); + findPreference(PREF_GPODNET_LOGIN).setOnPreferenceClickListener(preference -> { + new GpodderAuthenticationFragment().show(getChildFragmentManager(), GpodderAuthenticationFragment.TAG); + return true; + }); findPreference(PREF_GPODNET_SETLOGIN_INFORMATION) .setOnPreferenceClickListener(preference -> { AuthenticationDialog dialog = new AuthenticationDialog(activity, @@ -94,11 +96,6 @@ public class GpodderPreferencesFragment extends PreferenceFragmentCompat { updateGpodnetPreferenceScreen(); return true; }); - findPreference(PREF_GPODNET_HOSTNAME).setOnPreferenceClickListener(preference -> { - GpodnetSetHostnameDialog.createDialog(activity).setOnDismissListener( - dialog -> updateGpodnetPreferenceScreen()); - return true; - }); } private void updateGpodnetPreferenceScreen() { @@ -119,7 +116,6 @@ public class GpodderPreferencesFragment extends PreferenceFragmentCompat { } else { findPreference(PREF_GPODNET_LOGOUT).setSummary(null); } - findPreference(PREF_GPODNET_HOSTNAME).setSummary(GpodnetPreferences.getHostname()); } private void updateLastGpodnetSyncReport(boolean successful, long lastTime) { diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/preferences/NetworkPreferencesFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/preferences/NetworkPreferencesFragment.java index 77f8063f2..3889034fa 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/preferences/NetworkPreferencesFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/preferences/NetworkPreferencesFragment.java @@ -71,13 +71,13 @@ public class NetworkPreferencesFragment extends PreferenceFragmentCompat { Context context = getActivity().getApplicationContext(); String val; long interval = UserPreferences.getUpdateInterval(); - if(interval > 0) { + if (interval > 0) { int hours = (int) TimeUnit.MILLISECONDS.toHours(interval); - String hoursStr = context.getResources().getQuantityString(R.plurals.time_hours_quantified, hours, hours); - val = String.format(context.getString(R.string.pref_autoUpdateIntervallOrTime_every), hoursStr); + val = context.getResources().getQuantityString( + R.plurals.pref_autoUpdateIntervallOrTime_every_hours, hours, hours); } else { int[] timeOfDay = UserPreferences.getUpdateTimeOfDay(); - if(timeOfDay.length == 2) { + if (timeOfDay.length == 2) { Calendar cal = new GregorianCalendar(); cal.set(Calendar.HOUR_OF_DAY, timeOfDay[0]); cal.set(Calendar.MINUTE, timeOfDay[1]); diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/preferences/UserInterfacePreferencesFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/preferences/UserInterfacePreferencesFragment.java index 689a72ba7..4d1b79965 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/preferences/UserInterfacePreferencesFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/preferences/UserInterfacePreferencesFragment.java @@ -9,11 +9,14 @@ import androidx.preference.PreferenceFragmentCompat; import android.widget.ListView; import de.danoeh.antennapod.R; import de.danoeh.antennapod.activity.PreferenceActivity; +import de.danoeh.antennapod.core.event.PlayerStatusEvent; +import de.danoeh.antennapod.core.event.UnreadItemsUpdateEvent; import de.danoeh.antennapod.core.preferences.UserPreferences; import de.danoeh.antennapod.dialog.SubscriptionsFilterDialog; import de.danoeh.antennapod.dialog.FeedSortDialog; import de.danoeh.antennapod.fragment.NavDrawerFragment; import org.apache.commons.lang3.ArrayUtils; +import org.greenrobot.eventbus.EventBus; import java.util.List; @@ -37,8 +40,17 @@ public class UserInterfacePreferencesFragment extends PreferenceFragmentCompat { (preference, newValue) -> { getActivity().recreate(); return true; - } - ); + }); + + findPreference(UserPreferences.PREF_SHOW_TIME_LEFT) + .setOnPreferenceChangeListener( + (preference, newValue) -> { + UserPreferences.setShowRemainTimeSetting((Boolean) newValue); + EventBus.getDefault().post(new UnreadItemsUpdateEvent()); + EventBus.getDefault().post(new PlayerStatusEvent()); + return true; + }); + findPreference(UserPreferences.PREF_HIDDEN_DRAWER_ITEMS) .setOnPreferenceClickListener(preference -> { showDrawerPreferencesDialog(); diff --git a/app/src/main/java/de/danoeh/antennapod/menuhandler/MenuItemUtils.java b/app/src/main/java/de/danoeh/antennapod/menuhandler/MenuItemUtils.java index 9c54a529b..fbfdf537f 100644 --- a/app/src/main/java/de/danoeh/antennapod/menuhandler/MenuItemUtils.java +++ b/app/src/main/java/de/danoeh/antennapod/menuhandler/MenuItemUtils.java @@ -9,7 +9,7 @@ import androidx.appcompat.widget.SearchView; import de.danoeh.antennapod.R; import de.danoeh.antennapod.activity.MainActivity; import de.danoeh.antennapod.core.preferences.UserPreferences; -import de.danoeh.antennapod.core.util.ThemeUtils; +import de.danoeh.antennapod.ui.common.ThemeUtils; import de.danoeh.antennapod.fragment.SearchFragment; import java.util.HashMap; diff --git a/app/src/main/java/de/danoeh/antennapod/preferences/PreferenceUpgrader.java b/app/src/main/java/de/danoeh/antennapod/preferences/PreferenceUpgrader.java index 311f44881..03a8edbf0 100644 --- a/app/src/main/java/de/danoeh/antennapod/preferences/PreferenceUpgrader.java +++ b/app/src/main/java/de/danoeh/antennapod/preferences/PreferenceUpgrader.java @@ -2,6 +2,7 @@ package de.danoeh.antennapod.preferences; import android.content.Context; import android.content.SharedPreferences; +import android.view.KeyEvent; import androidx.preference.PreferenceManager; import de.danoeh.antennapod.BuildConfig; @@ -92,5 +93,16 @@ public class PreferenceUpgrader { if (oldVersion < 1080100) { prefs.edit().putString(UserPreferences.PREF_VIDEO_BEHAVIOR, "pip").apply(); } + if (oldVersion < 2010300) { + // Migrate hardware button preferences + if (prefs.getBoolean("prefHardwareForwardButtonSkips", false)) { + prefs.edit().putString(UserPreferences.PREF_HARDWARE_FORWARD_BUTTON, + String.valueOf(KeyEvent.KEYCODE_MEDIA_NEXT)).apply(); + } + if (prefs.getBoolean("prefHardwarePreviousButtonRestarts", false)) { + prefs.edit().putString(UserPreferences.PREF_HARDWARE_PREVIOUS_BUTTON, + String.valueOf(KeyEvent.KEYCODE_MEDIA_PREVIOUS)).apply(); + } + } } } diff --git a/app/src/main/java/de/danoeh/antennapod/view/ChapterSeekBar.java b/app/src/main/java/de/danoeh/antennapod/view/ChapterSeekBar.java new file mode 100644 index 000000000..5e80198d5 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/view/ChapterSeekBar.java @@ -0,0 +1,129 @@ +package de.danoeh.antennapod.view; + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.util.AttributeSet; +import de.danoeh.antennapod.ui.common.ThemeUtils; + +public class ChapterSeekBar extends androidx.appcompat.widget.AppCompatSeekBar { + + private float top; + private float width; + private float bottom; + private float density; + private float progressPrimary; + private float progressSecondary; + private float[] dividerPos; + private final Paint paintBackground = new Paint(); + private final Paint paintProgressPrimary = new Paint(); + private final Paint paintProgressSecondary = new Paint(); + + public ChapterSeekBar(Context context) { + super(context); + init(context); + } + + public ChapterSeekBar(Context context, AttributeSet attrs) { + super(context, attrs); + init(context); + } + + public ChapterSeekBar(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + init(context); + } + + private void init(Context context) { + setBackground(null); // Removes the thumb shadow + dividerPos = null; + density = context.getResources().getDisplayMetrics().density; + paintBackground.setColor(ThemeUtils.getColorFromAttr(getContext(), + de.danoeh.antennapod.core.R.attr.currently_playing_background)); + paintBackground.setAlpha(128); + paintProgressPrimary.setColor(ThemeUtils.getColorFromAttr(getContext(), + de.danoeh.antennapod.core.R.attr.colorPrimary)); + paintProgressSecondary.setColor(ThemeUtils.getColorFromAttr(getContext(), + de.danoeh.antennapod.core.R.attr.seek_background)); + } + + /** + * Sets the relative positions of the chapter dividers. + * @param dividerPos of the chapter dividers relative to the duration of the media. + */ + public void setDividerPos(final float[] dividerPos) { + if (dividerPos != null) { + this.dividerPos = new float[dividerPos.length + 2]; + this.dividerPos[0] = 0; + System.arraycopy(dividerPos, 0, this.dividerPos, 1, dividerPos.length); + this.dividerPos[this.dividerPos.length - 1] = 1; + } else { + this.dividerPos = null; + } + } + + @Override + protected synchronized void onDraw(Canvas canvas) { + top = getTop() + density * 7.5f; + bottom = getBottom() - density * 7.5f; + width = (float) (getRight() - getPaddingRight() - getLeft() - getPaddingLeft()); + progressSecondary = getSecondaryProgress() / (float) getMax() * width; + progressPrimary = getProgress() / (float) getMax() * width; + + if (dividerPos == null) { + drawProgress(canvas); + } else { + drawProgressChapters(canvas); + } + drawThumb(canvas); + } + + private void drawProgress(Canvas canvas) { + final int saveCount = canvas.save(); + canvas.translate(getPaddingLeft(), getPaddingTop()); + canvas.drawRect(0, top, width, bottom, paintBackground); + canvas.drawRect(0, top, progressSecondary, bottom, paintProgressSecondary); + canvas.drawRect(0, top, progressPrimary, bottom, paintProgressPrimary); + canvas.restoreToCount(saveCount); + } + + private void drawProgressChapters(Canvas canvas) { + final int saveCount = canvas.save(); + int currChapter = 1; + float chapterMargin = density * 0.6f; + float topExpanded = getTop() + density * 7; + float bottomExpanded = getBottom() - density * 7; + + canvas.translate(getPaddingLeft(), getPaddingTop()); + + for (int i = 1; i < dividerPos.length; i++) { + float right = dividerPos[i] * width - chapterMargin; + float left = dividerPos[i - 1] * width + chapterMargin; + float rightCurr = dividerPos[currChapter] * width - chapterMargin; + float leftCurr = dividerPos[currChapter - 1] * width + chapterMargin; + + canvas.drawRect(left, top, right, bottom, paintBackground); + + if (right < progressPrimary) { + currChapter = i + 1; + canvas.drawRect(left, top, right, bottom, paintProgressPrimary); + } else if (isPressed()) { + canvas.drawRect(leftCurr, topExpanded, rightCurr, bottomExpanded, paintBackground); + canvas.drawRect(leftCurr, topExpanded, progressPrimary, bottomExpanded, paintProgressPrimary); + } else { + if (progressSecondary > leftCurr) { + canvas.drawRect(leftCurr, top, progressSecondary, bottom, paintProgressSecondary); + } + canvas.drawRect(leftCurr, top, progressPrimary, bottom, paintProgressPrimary); + } + } + canvas.restoreToCount(saveCount); + } + + private void drawThumb(Canvas canvas) { + final int saveCount = canvas.save(); + canvas.translate(getPaddingLeft() - getThumbOffset(), getPaddingTop()); + getThumb().draw(canvas); + canvas.restoreToCount(saveCount); + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/view/EpisodeItemListRecyclerView.java b/app/src/main/java/de/danoeh/antennapod/view/EpisodeItemListRecyclerView.java index 83d90f98b..fb1c533c5 100644 --- a/app/src/main/java/de/danoeh/antennapod/view/EpisodeItemListRecyclerView.java +++ b/app/src/main/java/de/danoeh/antennapod/view/EpisodeItemListRecyclerView.java @@ -6,9 +6,9 @@ import android.content.res.Configuration; import android.util.AttributeSet; import android.view.View; import androidx.appcompat.view.ContextThemeWrapper; +import androidx.recyclerview.widget.DividerItemDecoration; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; -import com.yqritc.recyclerviewflexibledivider.HorizontalDividerItemDecoration; import de.danoeh.antennapod.R; import io.reactivex.annotations.Nullable; @@ -39,7 +39,7 @@ public class EpisodeItemListRecyclerView extends RecyclerView { layoutManager.setRecycleChildrenOnDetach(true); setLayoutManager(layoutManager); setHasFixedSize(true); - addItemDecoration(new HorizontalDividerItemDecoration.Builder(getContext()).build()); + addItemDecoration(new DividerItemDecoration(getContext(), layoutManager.getOrientation())); setClipToPadding(false); } diff --git a/app/src/main/java/de/danoeh/antennapod/view/viewholder/DownloadItemViewHolder.java b/app/src/main/java/de/danoeh/antennapod/view/viewholder/DownloadItemViewHolder.java index 274dd4ea8..0e446fb84 100644 --- a/app/src/main/java/de/danoeh/antennapod/view/viewholder/DownloadItemViewHolder.java +++ b/app/src/main/java/de/danoeh/antennapod/view/viewholder/DownloadItemViewHolder.java @@ -20,6 +20,7 @@ public class DownloadItemViewHolder extends RecyclerView.ViewHolder { public final TextView type; public final TextView date; public final TextView reason; + public final TextView tapForDetails; public DownloadItemViewHolder(Context context, ViewGroup parent) { super(LayoutInflater.from(context).inflate(R.layout.downloadlog_item, parent, false)); @@ -27,6 +28,7 @@ public class DownloadItemViewHolder extends RecyclerView.ViewHolder { type = itemView.findViewById(R.id.txtvType); icon = itemView.findViewById(R.id.txtvIcon); reason = itemView.findViewById(R.id.txtvReason); + tapForDetails = itemView.findViewById(R.id.txtvTapForDetails); secondaryActionButton = itemView.findViewById(R.id.secondaryActionButton); secondaryActionIcon = itemView.findViewById(R.id.secondaryActionIcon); title = itemView.findViewById(R.id.txtvTitle); diff --git a/app/src/main/java/de/danoeh/antennapod/view/viewholder/EpisodeItemViewHolder.java b/app/src/main/java/de/danoeh/antennapod/view/viewholder/EpisodeItemViewHolder.java index 35744227f..8b46a781f 100644 --- a/app/src/main/java/de/danoeh/antennapod/view/viewholder/EpisodeItemViewHolder.java +++ b/app/src/main/java/de/danoeh/antennapod/view/viewholder/EpisodeItemViewHolder.java @@ -13,9 +13,7 @@ import android.widget.TextView; import androidx.cardview.widget.CardView; import androidx.recyclerview.widget.RecyclerView; - import com.joanzapata.iconify.Iconify; - import de.danoeh.antennapod.R; import de.danoeh.antennapod.activity.MainActivity; import de.danoeh.antennapod.adapter.CoverLoader; @@ -25,13 +23,15 @@ import de.danoeh.antennapod.core.feed.FeedItem; import de.danoeh.antennapod.core.feed.FeedMedia; import de.danoeh.antennapod.core.feed.MediaType; import de.danoeh.antennapod.core.feed.util.ImageResourceUtils; +import de.danoeh.antennapod.core.preferences.UserPreferences; import de.danoeh.antennapod.core.service.download.DownloadRequest; +import de.danoeh.antennapod.core.service.playback.PlaybackService; import de.danoeh.antennapod.core.storage.DownloadRequester; import de.danoeh.antennapod.core.util.Converter; import de.danoeh.antennapod.core.util.DateUtils; import de.danoeh.antennapod.core.util.NetworkUtils; -import de.danoeh.antennapod.core.util.ThemeUtils; -import de.danoeh.antennapod.view.CircularProgressBar; +import de.danoeh.antennapod.ui.common.ThemeUtils; +import de.danoeh.antennapod.ui.common.CircularProgressBar; /** * Holds the view which shows FeedItems. @@ -121,8 +121,8 @@ public class EpisodeItemViewHolder extends RecyclerView.ViewHolder { if (coverHolder.getVisibility() == View.VISIBLE) { new CoverLoader(activity) - .withUri(ImageResourceUtils.getImageLocation(item)) - .withFallbackUri(item.getFeed().getImageLocation()) + .withUri(ImageResourceUtils.getEpisodeListImageLocation(item)) + .withFallbackUri(item.getFeed().getImageUrl()) .withPlaceholderView(placeholder) .withCoverView(cover) .load(); @@ -132,9 +132,6 @@ public class EpisodeItemViewHolder extends RecyclerView.ViewHolder { private void bind(FeedMedia media) { isVideo.setVisibility(media.getMediaType() == MediaType.VIDEO ? View.VISIBLE : View.GONE); duration.setVisibility(media.getDuration() > 0 ? View.VISIBLE : View.GONE); - duration.setText(Converter.getDurationStringLong(media.getDuration())); - duration.setContentDescription(activity.getString(R.string.chapter_duration, - Converter.getDurationStringLocalized(activity, media.getDuration()))); if (media.isCurrentlyPlaying()) { itemView.setBackgroundColor(ThemeUtils.getColorFromAttr(activity, R.attr.currently_playing_background)); @@ -152,6 +149,9 @@ public class EpisodeItemViewHolder extends RecyclerView.ViewHolder { secondaryActionProgress.setPercentage(0, item); // Animate X% -> 0% } + duration.setText(Converter.getDurationStringLong(media.getDuration())); + duration.setContentDescription(activity.getString(R.string.chapter_duration, + Converter.getDurationStringLocalized(activity, media.getDuration()))); if (item.getState() == FeedItem.State.PLAYING || item.getState() == FeedItem.State.IN_PROGRESS) { int progress = (int) (100.0 * media.getPosition() / media.getDuration()); progressBar.setProgress(progress); @@ -160,6 +160,11 @@ public class EpisodeItemViewHolder extends RecyclerView.ViewHolder { Converter.getDurationStringLocalized(activity, media.getPosition()))); progressBar.setVisibility(View.VISIBLE); position.setVisibility(View.VISIBLE); + if (UserPreferences.shouldShowRemainingTime()) { + duration.setText("-" + Converter.getDurationStringLong(media.getDuration() - media.getPosition())); + duration.setContentDescription(activity.getString(R.string.chapter_duration, + Converter.getDurationStringLocalized(activity, (media.getDuration() - media.getPosition())))); + } } else { progressBar.setVisibility(View.GONE); position.setVisibility(View.GONE); @@ -186,6 +191,22 @@ public class EpisodeItemViewHolder extends RecyclerView.ViewHolder { } } + private void updateDuration(PlaybackPositionEvent event) { + int currentPosition = event.getPosition(); + int timeDuration = event.getDuration(); + int remainingTime = event.getDuration() - event.getPosition(); + Log.d(TAG, "currentPosition " + Converter.getDurationStringLong(currentPosition)); + if (currentPosition == PlaybackService.INVALID_TIME || timeDuration == PlaybackService.INVALID_TIME) { + Log.w(TAG, "Could not react to position observer update because of invalid time"); + return; + } + if (UserPreferences.shouldShowRemainingTime()) { + duration.setText("-" + Converter.getDurationStringLong(remainingTime)); + } else { + duration.setText(Converter.getDurationStringLong(timeDuration)); + } + } + public FeedItem getFeedItem() { return item; } @@ -197,7 +218,7 @@ public class EpisodeItemViewHolder extends RecyclerView.ViewHolder { public void notifyPlaybackPositionUpdated(PlaybackPositionEvent event) { progressBar.setProgress((int) (100.0 * event.getPosition() / event.getDuration())); position.setText(Converter.getDurationStringLong(event.getPosition())); - duration.setText(Converter.getDurationStringLong(event.getDuration())); + updateDuration(event); duration.setVisibility(View.VISIBLE); // Even if the duration was previously unknown, it is now known } diff --git a/app/src/main/play/listings/cs-CZ/full-description.txt b/app/src/main/play/listings/cs-CZ/full-description.txt index 8411a2e7f..dc71e754c 100644 --- a/app/src/main/play/listings/cs-CZ/full-description.txt +++ b/app/src/main/play/listings/cs-CZ/full-description.txt @@ -2,9 +2,9 @@ AntennaPod je správce a přehrávač podcastů, co vám umožňuje okamžitý p Stahujte, streamujte nebo si vytvořte frontu epizod a užijte si poslech tak, jak ho máte rádi s nastavitelnou rychlostí přehrávání, podporou kapitol a s časovačem vypnutí. Ušetři si námahu, baterku i mobilní data s pomocí robustní automatické kontroly nad stahováním epizod (urči časy, intervaly a WIFI sítě) a mazáním epizod (na základě oblíbenosti a nastavení zpoždění). -Vytvořeno nadšenci do podcastů, AntennaPod je otevřený software (OSS), zdarma a bez reklam. +Vytvořeno nadšenci do podcastů, AntennaPod je software s otevřeným zdrojovým kódem, zdarma a bez reklam. -Importujte, zorganizujte a přehrávejte +Importujte, organizujte a přehrávejte • Ovládejte přehrávání odkudkoli: z widgetu na domovské obrazovce, z oznámení nebo pomocí tlačítek na sluchátkách včetně Bluetooth • Přidejte a importujte podcasty přes iTunes anebo gPodder.net, OPML soubory a RSS anebo Atom odkazy • Užijte si poslech s nastavitelnou rychlostí přehrávání, podporou kapitol, zapamatování poslední pozice přehrávání a pokročilým časovačem vypnutí (restart zatřesením, snížení hlasitosti) @@ -21,7 +21,7 @@ Vytvořeno nadšenci do podcastů, AntennaPod je otevřený software (OSS), zdar • Přizpůsobte si aplikaci svému prostředí pomocí světlého nebo tmavého motivu • Zálohujte své sbírky pomocí služby gPodder.net nebo exportem OPML souborů -Přidejte se do komunity AntennaPodu! +Přidejte se ke komunitě AntennaPod! AntennaPod je aktivně vyvíjen dobrovolníky. Můžete přispět také svým kódem nebo komentáři! Naše přátelská členská základna vám ráda zodpoví jakékoli dotazy. Zveme vás k diskuzi o AntennaPodu nebo i podcastech obecně. diff --git a/app/src/main/play/listings/de-DE/full-description.txt b/app/src/main/play/listings/de-DE/full-description.txt index b342dcdee..bd1c7cb1b 100644 --- a/app/src/main/play/listings/de-DE/full-description.txt +++ b/app/src/main/play/listings/de-DE/full-description.txt @@ -1,4 +1,4 @@ -AntennaPod ist ein Podcast-Manager und -Player, der dir unmittelbar Zugriff auf Millionen von freien und bezahlten Podcasts ermöglicht. Angefangen von unabhängigen Podcastern zu großen Rundfunkanstalten oder Hörfunksendern wie BBC, NPR und CNN. Abonniere, importiere und exportiere deine Feeds mühelos mit Hilfe des iTunes-Verzeichnisses, OPML-Dateien oder einfachen RSS-URLs. +AntennaPod ist ein Podcast-Manager und -Player, mit dem Du direkten Zugriff auf Millionen von freien und kostenpflichtigen Podcasts hast. Angefangen von unabhängigen Podcastern zu großen Rundfunkanstalten oder Hörfunksendern wie BBC, NPR und CNN. Abonniere, importiere und exportiere deine Feeds mühelos mit Hilfe des iTunes-Podcast-Verzeichnisses, OPML-Dateien oder RSS-URLs. Downloade, streame oder sortiere Episoden in der Abspielliste und genieße sie mit einstellbarer Abspielgeschwindigkeit, Unterstützung von Kapiteln und Schlummerfunktion. Reduziere Aufwand, Stromverbrauch und Datenverbrauch durch leistungsfähige Kontrolle der Downloads (bestimmte Uhrzeiten, Intervalle, WiFi-Netze) und des Löschens (basierend auf deinen Favoriten und weiteren Einstellungen). diff --git a/app/src/main/play/listings/en-US/graphics/promo-graphic/promo-graphic.png b/app/src/main/play/listings/en-US/graphics/promo-graphic/promo-graphic.png deleted file mode 100644 index 77a6e1c70..000000000 Binary files a/app/src/main/play/listings/en-US/graphics/promo-graphic/promo-graphic.png and /dev/null differ diff --git a/app/src/main/play/listings/pl-PL/full-description.txt b/app/src/main/play/listings/pl-PL/full-description.txt index 8c7236fac..efa98abd1 100644 --- a/app/src/main/play/listings/pl-PL/full-description.txt +++ b/app/src/main/play/listings/pl-PL/full-description.txt @@ -24,8 +24,8 @@ Dodawaj i importuj kanały z iTunes i gPodder.net, plików OPML oraz z adresów Dołącz do społeczności AntennaPod AntennaPod jest ciągle rozwijane przez ochotników. Ty też możesz pomóc, kodem lub komentarzem! -Chcesz zgłosić błąd lub brakuje Ci jakiejś funkcji, a może programujesz? Odwiedź nasz GitHub: -https://www.github.com/AntennaPod/AntennaPod +Życzliwi użytkownicy forum chętnie odpowiedzą na twoje pytania. Zapraszamy do rozmów o funkcjach programu i generalnie o podcastingu. +https://forum.antennapod.org/ Chcesz pomóc tłumaczyć AntennaPod - możesz to zrobić na Transifex: https://www.transifex.com/antennapod/antennapod \ No newline at end of file diff --git a/app/src/main/play/listings/sk/full-description.txt b/app/src/main/play/listings/sk/full-description.txt new file mode 100644 index 000000000..c929fa327 --- /dev/null +++ b/app/src/main/play/listings/sk/full-description.txt @@ -0,0 +1,31 @@ +AntennaPod je správca a prehrávač podcastov, ktorý vám sprostredkuje okamžitý prístup k miliónom bezplatným a spoplatneným podcastom - od nezávislých podcastérov až k vydavateľstvám ako BBC, NPR a CNN. Pridávajte, importujte a exportujte ich zdroje bez problémov pomocou databázy podcastov iTunes, OPML súborov alebo odkazov na RSS. +Stiahnite, streamujte alebo plánujte epizódy a užívajte si ich ako sa vám páči s nastaviteľnou rýchlosťou prehrávania, podporou kapitol a časovačom vypnutia. +Ušetrite si námahu, baterku a mobilné dáta pomocou výkonnej automatickej kontroly sťahovania epizód (určite čas, intervaly a WiFi siete) a mazania epizód (založené na obľúbených epizódach a nastavení oneskorenia). + +AntennaPod je spravovaný podcastovými entuziastami a je slobodný v každom slova zmysle: otvorený zdrojový kód, zadarmo, bez reklamy. + +Importovať, spravovať a prehrať +• Ovládajte prehrávanie odkiaľkoľvek: domovská obrazovka, systémové upozornenia a handsfree a ovládanie cez bluetooth +• Pridať a importovať zdroje pomocou priečinkov iTunes a gPodder.net, OPML súborov a odkazov RSS alebo Atom +• Užívajte si počúvanie s nastaviteľnou rýchlosťou prehrávania, podporou kapitol, zapamätanou pozíciou prehrávania a pokročilým nastavením vypnutia (zatrasením resetuj, stíš hlasitosť) +Počúvajte heslom chránené zdroje a epizódy + +Sledovať, zdielať a oceniť +• Sledujte najlepšie z najlepších pomocou označenia ako obľúbené +• Nájdite epizódu v histórií prehrávaní alebo v nadpisoch a popisoch +• Zdielajte epizódy a zdroje pomocou pokročilých nastavení sociálnych sietí a e-mailu, služieb gPodder.net a cez export do OPML súboru + +Spravovať systém +• Spravujte automatické sťahovanie: vyberte zdroje, vylúčte mobilné siete, vyberte konkrétne WiFi siete, vyžadujte nabíjanie telefónu a nastavte časy a intervaly +• Spravujte úložisko nastavením počtu uložených epizód, inteligentným mazaním a zvolením umiestnenia súborov +• Prispôsobte si vzhľad na svetlý alebo tmavý +• Zálohujte si odbery pomocou gPodder.net a OPML exportu + +Pridať sa do komunity AntennaPod! +AntennaPod aktívne vyvíjajú dobrovoľníci. Tiež môžete prispieť kódom alebo komentárom. + +Na našom fóre môžete v priateľskej atmosfére diskutovať a nájsť odpovede na vaše otázky o nových funkciách alebo všeobecne o podcastingu. +https://forum.antennapod.org/ + +Transifex je miesto, kde môžete pomôcť s prekladom: +https://www.transifex.com/antennapod/antennapod \ No newline at end of file diff --git a/app/src/main/play/listings/sk/short-description.txt b/app/src/main/play/listings/sk/short-description.txt new file mode 100644 index 000000000..d0162e6f9 --- /dev/null +++ b/app/src/main/play/listings/sk/short-description.txt @@ -0,0 +1 @@ +Ľahko použíteľný, flexibilný a open source správca a prehrávač podcastov \ No newline at end of file diff --git a/app/src/main/play/listings/sk/title.txt b/app/src/main/play/listings/sk/title.txt new file mode 100644 index 000000000..31552f353 --- /dev/null +++ b/app/src/main/play/listings/sk/title.txt @@ -0,0 +1 @@ +AntennaPod \ No newline at end of file diff --git a/app/src/main/play/release-notes/en-US/default.txt b/app/src/main/play/release-notes/en-US/default.txt index c911a4f1f..4b4805c6a 100644 --- a/app/src/main/play/release-notes/en-US/default.txt +++ b/app/src/main/play/release-notes/en-US/default.txt @@ -1,7 +1,12 @@ -- A long-standing wish of many: playing local files! In the 'Add podcast' screen simply tap 'Add local folder' and select a location on your phone! (@ByteHamster, @igoralmeida & @damoasda) -- Pick a country for the 'Discover' screen (@tonytamsf) -- Keyboard shortcuts (@asdoi) -- Search the PodcastIndex.org database (@edwinhere) -- Pull to refresh (@asdoi) -- Playback speed & filter dialogs (@ByteHamster & @bws9000) -- Smooth sleep timer volume (@olivoto) +NEW +- Optional notifications for new episodes (@connectety) +- Use PodcastIndex for main search (@tonytamsf) +- Sleep timer extend buttons (@max-wittig) +- Optional rewind, forward & skip buttons on widget (@tonytamsf) +- 'When not favorited' as Episode Cleanup (@spacecowboy) + +IMPROVED +- More actions for hardware buttons (@timakro) +- Android Auto & chapter support (@tonytamsf, @ByteHamster) +- Fixed stuck notification (@a1291762) +- Player screen usability for visually impaired (@ByteHamster) diff --git a/app/src/main/res/layout/activity_widget_config.xml b/app/src/main/res/layout/activity_widget_config.xml index ca8aba52d..6e31aec0d 100644 --- a/app/src/main/res/layout/activity_widget_config.xml +++ b/app/src/main/res/layout/activity_widget_config.xml @@ -22,7 +22,7 @@ android:id="@+id/widget_config_preview" layout="@layout/player_widget" android:layout_width="match_parent" - android:layout_height="80dp" + android:layout_height="96dp" android:layout_gravity="center" android:layout_margin="16dp" /> @@ -68,13 +68,38 @@ android:max="100" android:progress="100" /> + + + + + + + +